mirror of
https://github.com/penpot/penpot.git
synced 2026-06-16 04:12:03 +00:00
✨ Polish the devenv CLI and rewrite the agentic-devenv guide
Support --ws in all relevant commands, fix some bugs in the CLI and add more precondition validation. The guide was adjusted to give a better overview of the multi-workspace setup Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea5a2f6b4c
commit
ed679c15bb
@ -5,18 +5,74 @@ desc: Dive into agentic Penpot development.
|
||||
|
||||
# Agentic Development Environment
|
||||
|
||||
The agentic DevEnv is an extension of the standard DevEnv
|
||||
(the [general DevEnv instructions](/technical-guide/developer/devenv/) apply),
|
||||
which is optimised for AI agent-based development,
|
||||
adding additional tools and processes that support agentic automation.
|
||||
The agentic DevEnv is an extension of the standard DevEnv (the
|
||||
[general DevEnv instructions](/technical-guide/developer/devenv/) apply),
|
||||
optimised for AI agent-based development. It adds MCP servers (Penpot,
|
||||
Serena, Playwright) and supports a launcher that wires them into your AI client.
|
||||
|
||||
The general workflow is as follows:
|
||||
Two things to know up front:
|
||||
|
||||
1. Start the agentic DevEnv.
|
||||
2. Start a debugging-enabled browser and open Penpot, using a Penpot user with
|
||||
the remote MCP integration enabled.
|
||||
3. Use an AI client (MCP client) which is connected to a suite of MCP servers
|
||||
to solve development tasks.
|
||||
- **Parallel workspaces are first-class.** Run several devenv instances side
|
||||
by side - one per AI agent if you like - each with its own source-tree
|
||||
clone, ports, and tmux session. Pass `--ws N` to target one.
|
||||
- **Your existing AI-client config is preserved.** The launcher loads a
|
||||
per-workspace MCP config on top of your global one.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Bring up one or more workspaces**[^cfg]:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic # ws0 (the live repo)
|
||||
./manage.sh run-devenv-agentic --ws 1 # ws1 (sibling clone)
|
||||
```
|
||||
|
||||
Add `--ws 2`, `--ws 3`, … for more parallel workspaces.
|
||||
|
||||
2. **Launch a browser with remote debugging enabled:**
|
||||
|
||||
For example, with Chrome:
|
||||
|
||||
```bash
|
||||
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-debug-profile"
|
||||
```
|
||||
|
||||
3. **Open Penpot in that browser:**
|
||||
|
||||
- ws0: <https://localhost:3449>
|
||||
- ws1: <https://localhost:13449>
|
||||
- etc. (ports are offset by `10000 × N` for `wsN`)
|
||||
|
||||
On first login per account, open settings → Integrations and toggle
|
||||
"MCP Server" on. The agentic DevEnv runs the MCP server in single-user
|
||||
mode - the key and proxied URL shown in the UI are not needed.
|
||||
|
||||
4. **Launch your AI client** against the workspace you want it to drive:
|
||||
|
||||
```bash
|
||||
./manage.sh start-coding-agent claude # ws0
|
||||
./manage.sh start-coding-agent claude --ws 1 # ws1
|
||||
```
|
||||
|
||||
Supported clients: `claude` | `opencode` | `vscode` | `codex`. The
|
||||
launcher loads a per-workspace MCP config *on top of* your global config.
|
||||
5. **Attach to the tmux session** for the workspace (optional):
|
||||
|
||||
```bash
|
||||
./manage.sh attach-devenv # ws0
|
||||
./manage.sh attach-devenv --ws 1 # ws1
|
||||
```
|
||||
|
||||
6. **Shut down workspaces** with `./manage.sh stop-devenv`, either one by one or all at once.
|
||||
You cannot shut down `ws0` if any other workspace is still running, since it's the worker-bearer.
|
||||
Shared infrastructure will be cleaned up when the last workspace is stopped.
|
||||
|
||||
Optional: watch Serena's activity in its dashboard
|
||||
(<http://localhost:14182> for ws0, <http://localhost:24182> for ws1, etc.).
|
||||
|
||||
[^cfg]: One-time, if you don't already have it: set
|
||||
`penpotFlags = "enable-mcp"` in `frontend/resources/public/js/config.js`
|
||||
(gitignored; create if missing).
|
||||
|
||||
## Capabilities
|
||||
|
||||
@ -46,116 +102,101 @@ i.e. it can ...
|
||||
* test the changes in the live Penpot instance, and
|
||||
* create commits and PRs resolving the issue.
|
||||
|
||||
## Configuring and Starting the Agentic DevEnv
|
||||
## The flow in detail
|
||||
|
||||
**First-Time Setup: Building the Image.** If you are starting the agentic DevEnv for the first time, you need to build
|
||||
the updated docker image, adding support for agentic tools:
|
||||
### First-time setup
|
||||
|
||||
```bash
|
||||
./manage.sh build-devenv --local
|
||||
```
|
||||
|
||||
**Enable the Penpot MCP Connection in the Frontend.**
|
||||
The agentic DevEnv relies on a connection between the Penpot frontend and the Penpot MCP server
|
||||
being established automatically.
|
||||
Edit the file `frontend/resources/public/js/config.js`,
|
||||
creating it if it does not exist, and make sure the `penpotFlags` variable contains the
|
||||
`enable-mcp` flag.
|
||||
**The MCP frontend flag.** Edit `frontend/resources/public/js/config.js`
|
||||
(create it if missing) and ensure `penpotFlags` contains `enable-mcp`:
|
||||
|
||||
```javascript
|
||||
var penpotFlags = "enable-mcp";
|
||||
```
|
||||
|
||||
**Running the DevEnv in Agentic Mode.** Each invocation starts one instance.
|
||||
`--ws N` (N≥1) auto-starts ws0 first if it's not already up — ws0 is the
|
||||
worker-bearer and must be running whenever any ws1+ is:
|
||||
The file is gitignored and lives in the live repo only. On every
|
||||
`run-devenv-agentic` call it is read directly for ws0; for wsN (N ≥ 1) it is
|
||||
copied into the workspace clone on the **initial** sync only - subsequent
|
||||
`--sync` passes leave the workspace's copy alone so per-workspace
|
||||
customisations survive. `run-devenv-agentic` refuses to start if the file is
|
||||
missing.
|
||||
|
||||
**Browser remote debugging.** The Playwright MCP server drives a real
|
||||
browser instance over the Chrome DevTools protocol. To enable it, launch a
|
||||
Chromium-based browser (Chrome, Vivaldi, Opera, …) with the
|
||||
`--remote-debugging-port` flag and a separate user-data directory:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic # main (ws0)
|
||||
./manage.sh run-devenv-agentic --ws 1 # ws0 if needed, then ws1
|
||||
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-debug-profile"
|
||||
```
|
||||
|
||||
Starting an instance that is already running is an error. Per-instance ports
|
||||
Verify it works by visiting `http://127.0.0.1:9222/json/version`. If you
|
||||
change the port, update the Playwright MCP entry in `.devenv/shared/*.json`
|
||||
accordingly. For security reasons, do not enable remote debugging on the
|
||||
profile you use for regular browsing.
|
||||
|
||||
**Enable the MCP integration in Penpot.** The Penpot UI has a per-account
|
||||
MCP toggle. After logging into your Penpot instance at
|
||||
[https://localhost:3449](https://localhost:3449), open account settings,
|
||||
click "Integrations" in the sidebar, and enable the "MCP Server" toggle.
|
||||
The agentic DevEnv runs the MCP server in single-user mode, so the
|
||||
generated key and proxied URL printed in the UI are *not* needed - only the
|
||||
toggle itself matters.
|
||||
|
||||
**(Optional) custom devenv image.** Only needed if you want to modify the
|
||||
devenv image itself (add a tool, change a base layer):
|
||||
|
||||
```bash
|
||||
./manage.sh build-devenv --local
|
||||
```
|
||||
|
||||
The default `run-devenv-agentic` flow pulls the published image
|
||||
automatically, so regular users never run this.
|
||||
|
||||
### Bringing up workspaces
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic \
|
||||
[--ws N] [--sync] [--serena-context CTX] \
|
||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
||||
```
|
||||
|
||||
Brings one agentic instance up. Errors out if the target is already running.
|
||||
|
||||
`--ws N` (N ≥ 1) auto-starts ws0 first if it is not already up - ws0 is the
|
||||
worker-bearer and must be running whenever any wsN is. Per-instance ports
|
||||
are offset by `10000 × N` (ws1's MCP at `http://localhost:14401/mcp`, Serena
|
||||
MCP at `http://localhost:24181`, Serena dashboard at `http://localhost:24182`,
|
||||
etc.). `manage.sh` prints the full per-instance URL set (Penpot UI, MCP
|
||||
stream, Serena MCP, Serena dashboard, attach command) every time it brings an
|
||||
instance up, so you don't need to compute the offsets by hand. See the
|
||||
*Parallel workspaces* section in the [Dev environment guide](./devenv.md) for
|
||||
the workspace and lifecycle details (including the `--sync` flag and shutdown
|
||||
shape).
|
||||
MCP at `http://localhost:24181`, Serena dashboard at
|
||||
`http://localhost:24182`, etc.). `manage.sh` prints the full URL set on
|
||||
every bring-up so you don't compute offsets by hand. See the
|
||||
[Dev environment guide](./devenv.md) for the workspace lifecycle, `--sync`
|
||||
semantics, and stop ordering.
|
||||
|
||||
**Git identity for agent commits.** Coding agents typically need to commit
|
||||
inside the devenv, so `run-devenv-agentic` wires a Git identity into the
|
||||
container's global config on every bring-up. By default it propagates the
|
||||
host's effective `git config user.{name,email}` (local repo override wins
|
||||
over `~/.gitconfig`, matching what `git commit` on the host would record).
|
||||
Override either with `--git-user-name "Full Name"` /
|
||||
`--git-user-email you@example.com` — useful when you want agent commits to
|
||||
carry an identity different from your normal one. Without either source the
|
||||
script warns and proceeds; commits made by the agent will fail until you fix
|
||||
it. See the
|
||||
Override with `--git-user-name "Full Name"` / `--git-user-email
|
||||
you@example.com` when you want agent commits to carry an identity different
|
||||
from your normal one. Without either source the script warns and proceeds;
|
||||
commits inside the devenv will fail until you fix it. See the
|
||||
[Dev environment guide](./devenv.md#git-identity-inside-the-container) for
|
||||
the full mechanics.
|
||||
|
||||
> **Note:** the MCP and Serena tmux windows are only added when the session is
|
||||
> first created. If you've already run `./manage.sh run-devenv` (non-agentic)
|
||||
> in an instance, `run-devenv-agentic` errors out because the instance is
|
||||
> already running. Kill the session first to recreate with the agentic
|
||||
> windows:
|
||||
> **Note:** the MCP and Serena tmux windows are only added when the tmux
|
||||
> session is first created. If a workspace was already brought up with
|
||||
> `./manage.sh run-devenv` (non-agentic), stop it before re-running
|
||||
> agentically:
|
||||
>
|
||||
> ```bash
|
||||
> docker exec penpot-devenv-ws0-main sudo -u penpot tmux kill-session -t penpot
|
||||
> ./manage.sh stop-devenv
|
||||
> ./manage.sh run-devenv-agentic
|
||||
> ```
|
||||
|
||||
## Opening Penpot with Remote Debugging & MCP Enabled
|
||||
### Launching an AI client
|
||||
|
||||
**Enable Remote Debugging in Your Browser.**
|
||||
Penpot needs to be opened in a browser that has remote debugging enabled.
|
||||
In Chromium-based browsers (such as Google Chrome, Opera, Vivaldi, etc.),
|
||||
this can be achieved by launching the browser with the `--remote-debugging-port` argument.
|
||||
For most newer browsers, you will also need to specify a user data directory,
|
||||
as using debugging with your regular browser profile is disallowed for security reasons.
|
||||
|
||||
```bash
|
||||
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-debug-profile"
|
||||
```
|
||||
|
||||
This enables the Playwright MCP server to connect to the browser and control it.
|
||||
Verify that debugging was enabled correctly by navigating to `http://127.0.0.1:9222/json/version`.
|
||||
If you change the port, adjust the MCP server configuration accordingly (see below).
|
||||
Note: For security reasons, you should not enable remote debugging with a profile
|
||||
that you use for regular browsing activities.
|
||||
|
||||
**Open Penpot with the MCP Integration Enabled.**
|
||||
The Penpot instance in the DevEnv can be accessed at [https://localhost:3449](https://localhost:3449).
|
||||
Once logged in, navigate to your account settings, click on "Integrations" in the sidebar, and enable the "MCP Server" toggle.
|
||||
Note: You do not need to use the generated key (or the provided URL), as the MCP server in the agentic DevEnv is running in single-user mode and does not require authentication.
|
||||
|
||||
## Configuring Your AI Client
|
||||
|
||||
A given AI client session drives **exactly one workspace**. The Penpot and
|
||||
Serena MCP servers it talks to live inside that workspace's `main` container
|
||||
on workspace-specific ports — a client configured against ws0 cannot drive
|
||||
ws1, and vice versa. Running N parallel workspaces therefore means
|
||||
configuring (or launching) N AI clients, each pointed at a different
|
||||
workspace's ports.
|
||||
|
||||
There are two ways to wire this up:
|
||||
|
||||
* **For Claude Code, opencode, VS Code Copilot, and the Codex CLI**, use
|
||||
`./manage.sh start-coding-agent`, which loads a per-workspace MCP config
|
||||
generated by the devenv tooling. See *Quick start* below.
|
||||
* **For any other client** (JetBrains AI Assistant, Claude Desktop,
|
||||
Antigravity, …) — or if you'd rather configure the supported clients
|
||||
yourself — see *Manual configuration*.
|
||||
|
||||
### Quick start: `start-coding-agent`
|
||||
|
||||
Every time you bring a workspace up with `run-devenv-agentic`, `manage.sh`
|
||||
regenerates four MCP config files with that workspace's ports baked in:
|
||||
Every `run-devenv-agentic` regenerates four MCP-client config files with
|
||||
the workspace's ports baked in:
|
||||
|
||||
| Client | File | Loaded how |
|
||||
| ------ | ---- | ---------- |
|
||||
@ -164,39 +205,28 @@ regenerates four MCP config files with that workspace's ports baked in:
|
||||
| VS Code Copilot | `<workspace>/.vscode/mcp.json` | Auto-discovered when opening the workspace |
|
||||
| Codex CLI | `<workspace>/.codex/config.toml` | Auto-discovered from the project root (trusted projects only) |
|
||||
|
||||
The files are committed templates + an envsubst pass; see
|
||||
The files are committed templates + an `envsubst` pass; see
|
||||
[`.devenv/README.md`](../../../.devenv/README.md) for the full layout.
|
||||
|
||||
To launch a coding agent:
|
||||
`./manage.sh start-coding-agent <client> [--ws N] [...passthrough]`
|
||||
launches the chosen client against one workspace, `cd`'ing into the right
|
||||
directory and refusing to launch if the instance is not running:
|
||||
|
||||
```bash
|
||||
# Default target is ws0 (the live repo).
|
||||
./manage.sh start-coding-agent claude [...args forwarded to claude]
|
||||
./manage.sh start-coding-agent opencode [...args forwarded to opencode]
|
||||
./manage.sh start-coding-agent vscode [...args forwarded to 'code']
|
||||
./manage.sh start-coding-agent codex [...args forwarded to codex]
|
||||
|
||||
# Target a specific parallel workspace with --ws N (integer only).
|
||||
./manage.sh start-coding-agent claude --ws 1 # drives ws1
|
||||
./manage.sh start-coding-agent opencode --ws 2 # drives ws2
|
||||
./manage.sh start-coding-agent claude # ws0
|
||||
./manage.sh start-coding-agent opencode --ws 1 # ws1
|
||||
./manage.sh start-coding-agent vscode # opens VS Code on ws0
|
||||
./manage.sh start-coding-agent codex --ws 2 # ws2
|
||||
```
|
||||
|
||||
The launcher `cd`'s into the target workspace before exec'ing the client, so
|
||||
config files are resolved from the right directory regardless of where you
|
||||
invoke `manage.sh` from. It refuses to launch if the target instance's
|
||||
`main` container is not running — the Penpot and Serena MCP servers only
|
||||
exist while the devenv is up. Bring the instance up first with
|
||||
`./manage.sh run-devenv-agentic --ws N`, then retry.
|
||||
|
||||
`--ws` accepts a non-negative integer only (`--ws 0`, `--ws 1`, …). Spellings
|
||||
like `--ws main` or `--ws ws1` are rejected so the flag shape stays uniform
|
||||
across `attach-devenv`, `run-devenv-agentic`, `stop-devenv`, and
|
||||
`start-coding-agent`.
|
||||
A given AI client session drives **exactly one workspace**, so running N
|
||||
parallel workspaces typically means running N AI client sessions, each
|
||||
pointed at a different workspace's ports.
|
||||
|
||||
What each launcher does:
|
||||
|
||||
* **Claude Code** is started with `--mcp-config .devenv/mcp/claude-code.json`,
|
||||
which is **additive** — your existing global Claude Code MCP entries stay
|
||||
which is **additive** - your existing global Claude Code MCP entries stay
|
||||
available alongside the three entries we ship (Penpot MCP, Serena MCP,
|
||||
Playwright). To shadow one of our entries with a private one, install it
|
||||
under Claude Code's local scope (`claude mcp add --scope local …`); local
|
||||
@ -205,31 +235,33 @@ What each launcher does:
|
||||
opencode's precedence chain is *global → `OPENCODE_CONFIG` → project*, so
|
||||
the file we generate **overrides** entries with the same name in your
|
||||
global config. To override our entries in turn, drop a personal
|
||||
`opencode.json` at the repo root — it's gitignored on purpose.
|
||||
* **VS Code Copilot** — `code "$PWD"` opens VS Code on the current workspace.
|
||||
`opencode.json` at the repo root - it's gitignored on purpose.
|
||||
* **VS Code Copilot** - `code "$workspace"` opens VS Code on the workspace.
|
||||
Copilot loads both your user-profile MCP config and the workspace's
|
||||
`.vscode/mcp.json`, so our entries land alongside whatever you have
|
||||
globally. Same-name entries can be shadowed via your user profile.
|
||||
* **Codex CLI** — `codex` is exec'd from `$PWD`. Codex auto-discovers
|
||||
`.codex/config.toml` at the project root, but **only for "trusted"
|
||||
projects**; the first launch in a workspace will prompt you to trust it.
|
||||
Entries in `~/.codex/config.toml` override the project file on name
|
||||
conflict.
|
||||
* **Codex CLI** - `codex` is exec'd from the workspace dir. Codex
|
||||
auto-discovers `.codex/config.toml` at the project root, but **only for
|
||||
"trusted" projects** - the first launch in a workspace will prompt you to
|
||||
trust it. Entries in `~/.codex/config.toml` override the project file on
|
||||
name conflict.
|
||||
|
||||
### Manual configuration
|
||||
## Manual AI-client configuration
|
||||
|
||||
The `start-coding-agent` launcher covers Claude Code and opencode. For any
|
||||
other client, or if you prefer to wire things up yourself, configure the
|
||||
MCP servers in your client's native config format using the URLs below.
|
||||
The `start-coding-agent` launcher covers Claude Code, opencode, VS Code
|
||||
Copilot, and the Codex CLI. For any other client (JetBrains AI Assistant,
|
||||
Claude Desktop, Antigravity, …), or if you prefer to wire things up
|
||||
yourself, configure the MCP servers in your client's native format using
|
||||
the URLs below.
|
||||
|
||||
The Penpot and Serena URLs for the workspace you want to target are printed
|
||||
by `manage.sh` every time it brings an instance up; copy them straight from
|
||||
that output. The mechanical rule is `port = base + 10000 × N` for `wsN`, with
|
||||
bases `4401` (Penpot MCP) and `14181` (Serena MCP). Playwright is not
|
||||
workspace-scoped — it connects to your local browser, so the same entry works
|
||||
for every client.
|
||||
that output. The mechanical rule is `port = base + 10000 × N` for `wsN`,
|
||||
with bases `4401` (Penpot MCP) and `14181` (Serena MCP). Playwright is not
|
||||
workspace-scoped - it connects to your local browser, so the same entry
|
||||
works for every client.
|
||||
|
||||
#### Project-level MCP config files (Claude Code, opencode — manual path)
|
||||
### Project-level MCP config files (Claude Code, opencode - manual path)
|
||||
|
||||
If you'd rather not go through the launcher, both Claude Code and opencode
|
||||
support project-level MCP config files that **merge with** the developer's
|
||||
@ -245,7 +277,7 @@ The schemas differ between the two, so a workspace supporting both ships
|
||||
both files. For multi-workspace work, edit the port numbers in each
|
||||
workspace's copy once to match its offset.
|
||||
|
||||
#### Example configuration
|
||||
### Example configuration
|
||||
|
||||
Below is a JSON-based configuration snippet in Claude Code's `mcpServers`
|
||||
schema, targeting `ws0` (main), using `mcp-remote` to wrap HTTP-based
|
||||
@ -276,26 +308,29 @@ appropriately, referring to your client's documentation.
|
||||
```
|
||||
|
||||
**Penpot MCP Server**
|
||||
* The URL above connects directly to the server in the DevEnv, which runs in single-user mode.
|
||||
You do not need to use the proxied URL or the user token that is provided by the Penpot UI.
|
||||
* The URL above connects directly to the server in the DevEnv, which runs
|
||||
in single-user mode. You do not need to use the proxied URL or the user
|
||||
token that is provided by the Penpot UI.
|
||||
|
||||
**Serena MCP Server**
|
||||
* The matching Serena dashboard lives on the next port (`14182` for ws0, `24182` for ws1, …) and is also printed by `manage.sh` on startup.
|
||||
* The matching Serena dashboard lives on the next port (`14182` for ws0,
|
||||
`24182` for ws1, …) and is also printed by `manage.sh` on startup.
|
||||
|
||||
## Working on Development Tasks
|
||||
|
||||
After having made the configuration changes, restart your AI client.
|
||||
The configured MCP servers should now be running and accessible to your client.
|
||||
After having made the configuration changes, restart your AI client. The
|
||||
configured MCP servers should now be running and accessible to your client.
|
||||
|
||||
The agent's entrypoint for development is an activation of the `penpot` project with Serena.
|
||||
Start by instructing your agent as follows,
|
||||
The agent's entrypoint for development is an activation of the `penpot`
|
||||
project with Serena. Start by instructing your agent as follows,
|
||||
|
||||
> Activate project penpot.
|
||||
|
||||
and it should retrieve fundamental project information,
|
||||
expecting further instructions on what to do.
|
||||
and it should retrieve fundamental project information, expecting further
|
||||
instructions on what to do.
|
||||
|
||||
**Always start your first prompt with these activation instructions**, as this bootstraps the agent's context.
|
||||
**Always start your first prompt with these activation instructions**, as
|
||||
this bootstraps the agent's context.
|
||||
|
||||
### Checking MCP Server Operability
|
||||
|
||||
|
||||
@ -102,12 +102,13 @@ Host ports are offset by `10000 × N`:
|
||||
| Serena MCP | `http://localhost:14281` | `http://localhost:24281` | `http://localhost:34281` |
|
||||
|
||||
Container-internal ports stay fixed. Target a specific instance with
|
||||
`--ws 1` on `attach-devenv`, `run-devenv-agentic`, `stop-devenv`, and
|
||||
`start-coding-agent` (`--instance ws1` on `run-devenv-shell` /
|
||||
`run-devenv`). The `--ws` flag accepts a **non-negative integer only** —
|
||||
`--ws main` or `--ws ws1` is rejected, keeping the flag shape uniform across
|
||||
commands. `run-devenv-agentic` also accepts `--serena-context CTX` and
|
||||
`--git-user-name NAME` / `--git-user-email EMAIL` (see below).
|
||||
`--ws N` on `attach-devenv`, `run-devenv-agentic`, `stop-devenv`,
|
||||
`start-coding-agent`, `run-devenv-shell`, and `isolated-shell`. `--ws`
|
||||
accepts a **non-negative integer only** — `--ws main` or `--ws ws1` is
|
||||
rejected, keeping the flag shape uniform across commands. `run-devenv` is
|
||||
ws0-only and takes no workspace flag. `run-devenv-agentic` also accepts
|
||||
`--serena-context CTX` and `--git-user-name NAME` / `--git-user-email
|
||||
EMAIL` (see below).
|
||||
|
||||
### Git identity inside the container
|
||||
|
||||
|
||||
262
manage.sh
262
manage.sh
@ -549,31 +549,68 @@ function stop-devenv {
|
||||
fi
|
||||
}
|
||||
|
||||
# drop-devenv shares stop-devenv's CLI and invariants exactly; the only
|
||||
# difference is that on a full teardown it also removes the devenv image
|
||||
# (forcing the next bring-up to re-pull/rebuild). Single-workspace drops
|
||||
# keep the image because the rest of the workspaces still depend on it.
|
||||
#
|
||||
# --ws N (N >= 1) delegate to stop-devenv; image is kept.
|
||||
# --ws 0 | (none) delegate to stop-devenv; image is removed.
|
||||
# --all delegate to stop-devenv; image is removed.
|
||||
function drop-devenv {
|
||||
local ws
|
||||
for ws in $(list-running-instances); do
|
||||
# Never -v: data preservation rule.
|
||||
instance-compose "$ws" down -t 2
|
||||
# Parse args ourselves to decide whether the image gets removed.
|
||||
# stop-devenv then re-parses the same flags and runs the actual stop.
|
||||
local target=""
|
||||
local all=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ws)
|
||||
target="$(parse-ws-integer "$2")" || return 1; shift 2;;
|
||||
--all)
|
||||
all=true; shift;;
|
||||
*)
|
||||
echo "drop-devenv: unknown argument '$1'" >&2
|
||||
return 1;;
|
||||
esac
|
||||
done
|
||||
infra-compose down -t 2
|
||||
if [[ -n "$target" && "$all" == "true" ]]; then
|
||||
echo "drop-devenv: --ws and --all are mutually exclusive." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Clean old development image $DEVENV_IMGNAME..."
|
||||
docker images $DEVENV_IMGNAME -q | xargs --no-run-if-empty docker rmi
|
||||
local stop_args=()
|
||||
[[ -n "$target" ]] && stop_args+=(--ws "${target#ws}")
|
||||
[[ "$all" == "true" ]] && stop_args+=(--all)
|
||||
stop-devenv "${stop_args[@]}" || return $?
|
||||
|
||||
# Image removal happens for the full-teardown paths only. A single-wsN
|
||||
# (N >= 1) drop must keep the image since ws0 and any other wsN still
|
||||
# rely on it.
|
||||
if [[ -z "$target" || "$target" == "ws0" ]] || [[ "$all" == "true" ]]; then
|
||||
echo "Clean old development image $DEVENV_IMGNAME..."
|
||||
docker images $DEVENV_IMGNAME -q | xargs --no-run-if-empty docker rmi
|
||||
fi
|
||||
}
|
||||
|
||||
function log-devenv {
|
||||
# Tail ws0 by default; for multi-instance dev, attach explicitly per project.
|
||||
instance-compose ws0 logs -f --tail=50
|
||||
local target="ws0"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ws)
|
||||
target="$(parse-ws-integer "$2")" || return 1; shift 2;;
|
||||
*)
|
||||
echo "log-devenv: unknown argument '$1'" >&2
|
||||
return 1;;
|
||||
esac
|
||||
done
|
||||
instance-compose "$target" logs -f --tail=50
|
||||
}
|
||||
|
||||
function run-devenv-tmux {
|
||||
local extra_env_args=()
|
||||
local instance="ws0"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--instance)
|
||||
instance="$(normalize-instance "$2")"; shift 2;;
|
||||
-e)
|
||||
extra_env_args+=(-e "$2"); shift 2;;
|
||||
-e*)
|
||||
@ -584,39 +621,19 @@ function run-devenv-tmux {
|
||||
esac
|
||||
done
|
||||
|
||||
if ! devenv-main-running "$instance"; then
|
||||
if [[ "$instance" == "ws0" ]]; then
|
||||
start-devenv
|
||||
echo "Waiting for containers fully start (5s)..."
|
||||
sleep 5
|
||||
else
|
||||
echo "Instance '$instance' is not running; bring it up first with './manage.sh run-devenv-agentic --n-instances N'." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! devenv-main-running ws0; then
|
||||
start-devenv
|
||||
echo "Waiting for containers fully start (5s)..."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
local container
|
||||
container=$(devenv-main-container "$instance")
|
||||
container=$(devenv-main-container ws0)
|
||||
docker exec -ti \
|
||||
"${extra_env_args[@]}" \
|
||||
"$container" sudo -EH -u penpot PENPOT_PLUGIN_DEV=$PENPOT_PLUGIN_DEV /home/start-tmux.sh
|
||||
}
|
||||
|
||||
# Normalize an instance specifier ("main", "0", "ws0", "1", "ws3", ...) to "wsN".
|
||||
function normalize-instance {
|
||||
local raw="$1"
|
||||
if [[ "$raw" == "main" ]]; then
|
||||
echo "ws0"
|
||||
elif [[ "$raw" =~ ^ws[0-9]+$ ]]; then
|
||||
echo "$raw"
|
||||
elif [[ "$raw" =~ ^[0-9]+$ ]]; then
|
||||
echo "ws$raw"
|
||||
else
|
||||
echo "Invalid instance value: '$raw' (expected main|0|ws0|1|ws1|...)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Strict parser for --ws values. Accepts a bare integer in the supported
|
||||
# range (0..PENPOT_MAX_WS_INDEX) and returns the canonical "wsN" form.
|
||||
# Anything else fails fast. The upper bound exists because the host ports
|
||||
@ -789,6 +806,23 @@ function run-devenv-agentic {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Pre-flight: frontend/resources/public/js/config.js must exist in the
|
||||
# live repo. The file is gitignored; ws0 reads it directly from $PWD and
|
||||
# wsN gets a one-shot copy on its initial sync. Without it the frontend
|
||||
# never sets the 'enable-mcp' flag, so the agent can't drive Penpot via
|
||||
# MCP. Fail fast (before any side effects) so the developer can fix it
|
||||
# without leaving infra / containers half-started behind.
|
||||
local cfg="frontend/resources/public/js/config.js"
|
||||
if [[ ! -f "$PWD/$cfg" ]]; then
|
||||
echo "$cfg is missing in the live repo." >&2
|
||||
echo "Create it before running run-devenv-agentic -- the file is gitignored," >&2
|
||||
echo "read directly from \$PWD on ws0 and copied into wsN only on its initial" >&2
|
||||
echo "sync. Without it the Penpot frontend will not establish the MCP" >&2
|
||||
echo "connection, so the agent cannot drive it. Minimal content:" >&2
|
||||
echo " var penpotFlags = \"enable-mcp\";" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
pull-devenv-if-not-exists
|
||||
ensure-devenv-network
|
||||
ensure-infra-up
|
||||
@ -827,8 +861,8 @@ function run-devenv-shell {
|
||||
local positional=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--instance)
|
||||
instance="$(normalize-instance "$2")"; shift 2;;
|
||||
--ws)
|
||||
instance="$(parse-ws-integer "$2")" || return 1; shift 2;;
|
||||
*)
|
||||
positional+=("$1"); shift;;
|
||||
esac
|
||||
@ -839,12 +873,20 @@ function run-devenv-shell {
|
||||
start-devenv
|
||||
else
|
||||
echo "Instance '$instance' is not running." >&2
|
||||
echo "Bring it up first with './manage.sh run-devenv-agentic --ws ${instance#ws}'." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# No positional args -> drop the user into a bash shell. Without this,
|
||||
# `sudo -EH -u penpot` would be called with no command and just print its
|
||||
# own usage.
|
||||
if [[ ${#positional[@]} -eq 0 ]]; then
|
||||
positional=(bash)
|
||||
fi
|
||||
local container
|
||||
container=$(devenv-main-container "$instance")
|
||||
docker exec -ti \
|
||||
-w /home/penpot/penpot \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-e EXTERNAL_UID=$CURRENT_USER_ID \
|
||||
"$container" sudo -EH -u penpot "${positional[@]}"
|
||||
@ -874,7 +916,8 @@ function attach-devenv {
|
||||
|
||||
if ! docker exec "$container" sudo -EH -u penpot tmux has-session -t "$session" 2>/dev/null; then
|
||||
echo "No tmux session '$session' inside instance '$instance'." >&2
|
||||
echo "Start it with './manage.sh run-devenv-agentic [--ws N]'." >&2
|
||||
echo "The session may still be starting (the workspace's startup script runs the" >&2
|
||||
echo "project setup before creating it) or it may have been closed. Wait and retry." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@ -995,17 +1038,52 @@ function start-coding-agent {
|
||||
}
|
||||
|
||||
function run-devenv-isolated-shell {
|
||||
docker volume create ${PENPOT_USER_DATA_VOLUME};
|
||||
local instance="ws0"
|
||||
local positional=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ws)
|
||||
instance="$(parse-ws-integer "$2")" || return 1; shift 2;;
|
||||
*)
|
||||
positional+=("$1"); shift;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Resolve the user_data volume and source tree for the target workspace.
|
||||
# ws0 uses the baseline volume name from defaults.env; wsN follows the
|
||||
# write-instance-env naming convention (penpotdev_<instance>_user_data)
|
||||
# and bind-mounts the workspace clone instead of $PWD.
|
||||
local user_data_volume source_path
|
||||
if [[ "$instance" == "ws0" ]]; then
|
||||
user_data_volume="$PENPOT_USER_DATA_VOLUME"
|
||||
source_path="$PWD"
|
||||
else
|
||||
user_data_volume="penpotdev_${instance}_user_data"
|
||||
source_path="$(workspace-path "$instance")"
|
||||
if [[ ! -d "$source_path" ]]; then
|
||||
echo "isolated-shell: workspace for $instance not found at $source_path." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# No command -> drop into bash. Always cwd to the source-tree root;
|
||||
# callers that need a subdir can `cd` inside the shell or pass
|
||||
# `bash -c 'cd subdir && cmd'`.
|
||||
if [[ ${#positional[@]} -eq 0 ]]; then
|
||||
positional=(bash)
|
||||
fi
|
||||
|
||||
docker volume create "$user_data_volume" >/dev/null
|
||||
docker run -ti --rm \
|
||||
--mount source=${PENPOT_USER_DATA_VOLUME},type=volume,target=/home/penpot/ \
|
||||
--mount source=`pwd`,type=bind,target=/home/penpot/penpot \
|
||||
--mount source="$user_data_volume",type=volume,target=/home/penpot/ \
|
||||
--mount source="$source_path",type=bind,target=/home/penpot/penpot \
|
||||
-e EXTERNAL_UID=$CURRENT_USER_ID \
|
||||
-e BUILD_STORYBOOK=$BUILD_STORYBOOK \
|
||||
-e BUILD_WASM=$BUILD_WASM \
|
||||
-e SHADOWCLJS_EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-w /home/penpot/penpot/$1 \
|
||||
$DEVENV_IMGNAME:latest sudo -EH -u penpot $@
|
||||
-w /home/penpot/penpot \
|
||||
"$DEVENV_IMGNAME:latest" sudo -EH -u penpot "${positional[@]}"
|
||||
}
|
||||
|
||||
function build-imagemagick-docker-image {
|
||||
@ -1216,40 +1294,68 @@ function build-storybook-docker-image {
|
||||
function usage {
|
||||
echo "PENPOT build & release manager"
|
||||
echo "USAGE: $0 OPTION"
|
||||
echo "Options:"
|
||||
echo "- pull-devenv Pulls docker development oriented image"
|
||||
echo "- build-devenv Build docker development oriented image"
|
||||
echo "- build-devenv --local Build a local docker development oriented image"
|
||||
echo "- create-devenv Create the development oriented docker compose service."
|
||||
echo "- start-devenv Start the development oriented docker compose service."
|
||||
echo "- stop-devenv Stops the development oriented docker compose service."
|
||||
echo "- drop-devenv Remove the development oriented docker compose containers, volumes and clean images."
|
||||
echo "- run-devenv Brings ws0 up and attaches to its tmux session (no MCP, no Serena)."
|
||||
echo " Optional --instance <wsN> targets a different instance."
|
||||
echo " Optional -e flags are forwarded to 'docker exec' (e.g. -e MY_VAR=value)."
|
||||
echo "- run-devenv-agentic Brings one agentic instance (MCP + Serena) up. Errors out if it is already running."
|
||||
echo " Auto-starts ws0 first when --ws N (N>=1) is requested (workers run only on ws0)."
|
||||
echo " Options: --ws N (default: 0; non-negative integer only),"
|
||||
echo " --sync (re-seed workspace from live repo; ws1+ only),"
|
||||
echo " --serena-context CONTEXT (default: desktop-app),"
|
||||
echo " --git-user-name NAME / --git-user-email EMAIL"
|
||||
echo " (default: host's effective 'git config user.{name,email}',"
|
||||
echo " honouring per-repo local overrides)"
|
||||
echo "- attach-devenv Attaches to the tmux session inside a running instance."
|
||||
echo " Options: --ws N (default: 0; non-negative integer only)"
|
||||
echo "- start-coding-agent <client> Launches an AI coding agent against one workspace with the right MCP config wired in."
|
||||
echo ""
|
||||
echo "Development environment (devenv)"
|
||||
echo "--------------------------------"
|
||||
echo "The devenv runs Penpot in a Docker container and supports parallel"
|
||||
echo "'workspaces': ws0 (the live repo at \$PWD) and optional wsN (N >= 1, sibling"
|
||||
echo "clones). Use --ws N to target a specific workspace; the default is 0."
|
||||
echo "Full guide: docs/technical-guide/developer/{devenv,agentic-devenv}.md."
|
||||
echo ""
|
||||
echo "Image lifecycle"
|
||||
echo "- pull-devenv Pull the devenv docker image from the registry."
|
||||
echo "- build-devenv [--local] Build the devenv docker image (--local skips the registry push)."
|
||||
echo ""
|
||||
echo "Bring a devenv up / down"
|
||||
echo "- run-devenv-agentic Bring one workspace up with AI-agent tooling enabled (MCP + Serena),"
|
||||
echo " start its tmux session in the background, regenerate the per-workspace"
|
||||
echo " MCP configs, and print the workspace's URLs. Errors out if the target"
|
||||
echo " is already running."
|
||||
echo " Options:"
|
||||
echo " --ws N target workspace (default: 0). N >= 1 auto-starts"
|
||||
echo " ws0 first if it is not already up."
|
||||
echo " --sync re-seed the wsN clone from the live repo before"
|
||||
echo " starting (forbidden on ws0; implicit on first"
|
||||
echo " start of a wsN with no on-disk workspace yet)."
|
||||
echo " --serena-context CTX passed to Serena (default: desktop-app)."
|
||||
echo " --git-user-name NAME / --git-user-email EMAIL"
|
||||
echo " identity wired into the container's git config"
|
||||
echo " (default: host's effective 'git config user.X',"
|
||||
echo " honouring per-repo local overrides; see"
|
||||
echo " devenv.md > 'Git identity inside the container')."
|
||||
echo "- start-devenv Bring ws0 + shared infra up in the background (no tmux, no MCP/Serena)."
|
||||
echo "- create-devenv Create ws0 + shared-infra compose services without starting them."
|
||||
echo "- stop-devenv Stop one or more workspaces. Shared infra stops with the last one."
|
||||
echo " Options: --ws N (stop wsN, N >= 1) | (no flag) (stop ws0 + infra;"
|
||||
echo " refused if any wsN is still running) | --all (stop every wsN"
|
||||
echo " highest first, then ws0 + infra)."
|
||||
echo "- drop-devenv Same CLI and invariants as stop-devenv (see above), plus removal of"
|
||||
echo " the shared devenv image on full teardowns (no flag, --ws 0, or --all)."
|
||||
echo " Refused if any wsN (N >= 1) is still running. A single --ws N (N >= 1)"
|
||||
echo " keeps the image since other workspaces still need it."
|
||||
echo "- log-devenv Tail a workspace's compose logs."
|
||||
echo " Options: --ws N (default: 0)."
|
||||
echo ""
|
||||
echo "Work inside a running devenv"
|
||||
echo "- attach-devenv Attach to the tmux session inside a running workspace."
|
||||
echo " Options: --ws N (default: 0)."
|
||||
echo "- start-coding-agent <client> Launch an AI coding agent against one workspace with the right MCP"
|
||||
echo " config wired in. cd's into the workspace, refuses to launch if the"
|
||||
echo " instance is not running, and forwards extra args to the client."
|
||||
echo " client: claude | opencode | vscode | codex"
|
||||
echo " Options: --ws N (default: 0; non-negative integer only)."
|
||||
echo " cd's into the target workspace and refuses to launch if the instance is not running."
|
||||
echo " Extra args after --ws (or after the client name) are forwarded to the underlying client."
|
||||
echo " See docs/technical-guide/developer/agentic-devenv.md and .devenv/README.md"
|
||||
echo " for per-client setup, override paths, and the Codex 'trusted project' caveat."
|
||||
echo "- run-devenv-shell Opens a bash shell inside a running instance."
|
||||
echo " Options: --instance 0|wsN|N (default: 0)"
|
||||
echo "- stop-devenv Stops instances. ws0 must be the last to stop; shared infra stops with ws0."
|
||||
echo " Options: --ws N (stop one ws1+) | --ws 0 | (none) (stop ws0 + infra) | --all"
|
||||
echo "- isolated-shell Starts a bash shell in a new devenv container."
|
||||
echo "- log-devenv Show logs of the running devenv docker compose service."
|
||||
echo " Options: --ws N (default: 0). See agentic-devenv.md and"
|
||||
echo " .devenv/README.md for per-client setup and override paths."
|
||||
echo "- run-devenv-shell Open a bash shell inside the workspace's running devenv container"
|
||||
echo " ('docker exec' into the live 'main' container alongside the tmux"
|
||||
echo " session). Requires the workspace to be up."
|
||||
echo " Options: --ws N (default: 0)."
|
||||
echo "- run-devenv Start ws0 if needed and attach to its tmux session interactively."
|
||||
echo " Optional -e flags are forwarded to 'docker exec' (e.g. -e MY_VAR=value)."
|
||||
echo "- isolated-shell Spawn a fresh, ephemeral devenv container ('docker run', not 'docker exec')"
|
||||
echo " with the workspace's source tree and build-cache volume mounted. Use this"
|
||||
echo " for ad-hoc work that should not touch the running devenv (e.g. manual"
|
||||
echo " builds); the workspace does not need to be running."
|
||||
echo " Options: --ws N (default: 0)."
|
||||
echo ""
|
||||
echo "- build-bundle Build all bundles (frontend, backend, exporter, storybook and mcp)."
|
||||
echo "- build-frontend-bundle Build frontend bundle"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user