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:
Michael Panchenko 2026-05-27 16:20:00 +02:00 committed by alonso.torres
parent ea5a2f6b4c
commit ed679c15bb
3 changed files with 368 additions and 226 deletions

View File

@ -5,18 +5,74 @@ desc: Dive into agentic Penpot development.
# Agentic Development Environment # Agentic Development Environment
The agentic DevEnv is an extension of the standard DevEnv The agentic DevEnv is an extension of the standard DevEnv (the
(the [general DevEnv instructions](/technical-guide/developer/devenv/) apply), [general DevEnv instructions](/technical-guide/developer/devenv/) apply),
which is optimised for AI agent-based development, optimised for AI agent-based development. It adds MCP servers (Penpot,
adding additional tools and processes that support agentic automation. 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. - **Parallel workspaces are first-class.** Run several devenv instances side
2. Start a debugging-enabled browser and open Penpot, using a Penpot user with by side - one per AI agent if you like - each with its own source-tree
the remote MCP integration enabled. clone, ports, and tmux session. Pass `--ws N` to target one.
3. Use an AI client (MCP client) which is connected to a suite of MCP servers - **Your existing AI-client config is preserved.** The launcher loads a
to solve development tasks. 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 ## Capabilities
@ -46,116 +102,101 @@ i.e. it can ...
* test the changes in the live Penpot instance, and * test the changes in the live Penpot instance, and
* create commits and PRs resolving the issue. * 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 ### First-time setup
the updated docker image, adding support for agentic tools:
```bash **The MCP frontend flag.** Edit `frontend/resources/public/js/config.js`
./manage.sh build-devenv --local (create it if missing) and ensure `penpotFlags` contains `enable-mcp`:
```
**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.
```javascript ```javascript
var penpotFlags = "enable-mcp"; var penpotFlags = "enable-mcp";
``` ```
**Running the DevEnv in Agentic Mode.** Each invocation starts one instance. The file is gitignored and lives in the live repo only. On every
`--ws N` (N≥1) auto-starts ws0 first if it's not already up — ws0 is the `run-devenv-agentic` call it is read directly for ws0; for wsN (N ≥ 1) it is
worker-bearer and must be running whenever any ws1+ 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 ```bash
./manage.sh run-devenv-agentic # main (ws0) google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-debug-profile"
./manage.sh run-devenv-agentic --ws 1 # ws0 if needed, then ws1
``` ```
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 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`, MCP at `http://localhost:24181`, Serena dashboard at
etc.). `manage.sh` prints the full per-instance URL set (Penpot UI, MCP `http://localhost:24182`, etc.). `manage.sh` prints the full URL set on
stream, Serena MCP, Serena dashboard, attach command) every time it brings an every bring-up so you don't compute offsets by hand. See the
instance up, so you don't need to compute the offsets by hand. See the [Dev environment guide](./devenv.md) for the workspace lifecycle, `--sync`
*Parallel workspaces* section in the [Dev environment guide](./devenv.md) for semantics, and stop ordering.
the workspace and lifecycle details (including the `--sync` flag and shutdown
shape).
**Git identity for agent commits.** Coding agents typically need to commit **Git identity for agent commits.** Coding agents typically need to commit
inside the devenv, so `run-devenv-agentic` wires a Git identity into the 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 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 host's effective `git config user.{name,email}` (local repo override wins
over `~/.gitconfig`, matching what `git commit` on the host would record). over `~/.gitconfig`, matching what `git commit` on the host would record).
Override either with `--git-user-name "Full Name"` / Override with `--git-user-name "Full Name"` / `--git-user-email
`--git-user-email you@example.com` — useful when you want agent commits to you@example.com` when you want agent commits to carry an identity different
carry an identity different from your normal one. Without either source the from your normal one. Without either source the script warns and proceeds;
script warns and proceeds; commits made by the agent will fail until you fix commits inside the devenv will fail until you fix it. See the
it. See the
[Dev environment guide](./devenv.md#git-identity-inside-the-container) for [Dev environment guide](./devenv.md#git-identity-inside-the-container) for
the full mechanics. the full mechanics.
> **Note:** the MCP and Serena tmux windows are only added when the session is > **Note:** the MCP and Serena tmux windows are only added when the tmux
> first created. If you've already run `./manage.sh run-devenv` (non-agentic) > session is first created. If a workspace was already brought up with
> in an instance, `run-devenv-agentic` errors out because the instance is > `./manage.sh run-devenv` (non-agentic), stop it before re-running
> already running. Kill the session first to recreate with the agentic > agentically:
> windows:
> >
> ```bash > ```bash
> docker exec penpot-devenv-ws0-main sudo -u penpot tmux kill-session -t penpot
> ./manage.sh stop-devenv > ./manage.sh stop-devenv
> ./manage.sh run-devenv-agentic > ./manage.sh run-devenv-agentic
> ``` > ```
## Opening Penpot with Remote Debugging & MCP Enabled ### Launching an AI client
**Enable Remote Debugging in Your Browser.** Every `run-devenv-agentic` regenerates four MCP-client config files with
Penpot needs to be opened in a browser that has remote debugging enabled. the workspace's ports baked in:
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:
| Client | File | Loaded how | | 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 | | 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) | | 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. [`.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 ```bash
# Default target is ws0 (the live repo). ./manage.sh start-coding-agent claude # ws0
./manage.sh start-coding-agent claude [...args forwarded to claude] ./manage.sh start-coding-agent opencode --ws 1 # ws1
./manage.sh start-coding-agent opencode [...args forwarded to opencode] ./manage.sh start-coding-agent vscode # opens VS Code on ws0
./manage.sh start-coding-agent vscode [...args forwarded to 'code'] ./manage.sh start-coding-agent codex --ws 2 # ws2
./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
``` ```
The launcher `cd`'s into the target workspace before exec'ing the client, so A given AI client session drives **exactly one workspace**, so running N
config files are resolved from the right directory regardless of where you parallel workspaces typically means running N AI client sessions, each
invoke `manage.sh` from. It refuses to launch if the target instance's pointed at a different workspace's ports.
`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`.
What each launcher does: What each launcher does:
* **Claude Code** is started with `--mcp-config .devenv/mcp/claude-code.json`, * **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, available alongside the three entries we ship (Penpot MCP, Serena MCP,
Playwright). To shadow one of our entries with a private one, install it 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 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 opencode's precedence chain is *global → `OPENCODE_CONFIG` → project*, so
the file we generate **overrides** entries with the same name in your the file we generate **overrides** entries with the same name in your
global config. To override our entries in turn, drop a personal global config. To override our entries in turn, drop a personal
`opencode.json` at the repo root it's gitignored on purpose. `opencode.json` at the repo root - it's gitignored on purpose.
* **VS Code Copilot** `code "$PWD"` opens VS Code on the current workspace. * **VS Code Copilot** - `code "$workspace"` opens VS Code on the workspace.
Copilot loads both your user-profile MCP config and the workspace's Copilot loads both your user-profile MCP config and the workspace's
`.vscode/mcp.json`, so our entries land alongside whatever you have `.vscode/mcp.json`, so our entries land alongside whatever you have
globally. Same-name entries can be shadowed via your user profile. globally. Same-name entries can be shadowed via your user profile.
* **Codex CLI** `codex` is exec'd from `$PWD`. Codex auto-discovers * **Codex CLI** - `codex` is exec'd from the workspace dir. Codex
`.codex/config.toml` at the project root, but **only for "trusted" auto-discovers `.codex/config.toml` at the project root, but **only for
projects**; the first launch in a workspace will prompt you to trust it. "trusted" projects** - the first launch in a workspace will prompt you to
Entries in `~/.codex/config.toml` override the project file on name trust it. Entries in `~/.codex/config.toml` override the project file on
conflict. name conflict.
### Manual configuration ## Manual AI-client configuration
The `start-coding-agent` launcher covers Claude Code and opencode. For any The `start-coding-agent` launcher covers Claude Code, opencode, VS Code
other client, or if you prefer to wire things up yourself, configure the Copilot, and the Codex CLI. For any other client (JetBrains AI Assistant,
MCP servers in your client's native config format using the URLs below. 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 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 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 that output. The mechanical rule is `port = base + 10000 × N` for `wsN`,
bases `4401` (Penpot MCP) and `14181` (Serena MCP). Playwright is not 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 workspace-scoped - it connects to your local browser, so the same entry
for every client. 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 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 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 both files. For multi-workspace work, edit the port numbers in each
workspace's copy once to match its offset. workspace's copy once to match its offset.
#### Example configuration ### Example configuration
Below is a JSON-based configuration snippet in Claude Code's `mcpServers` Below is a JSON-based configuration snippet in Claude Code's `mcpServers`
schema, targeting `ws0` (main), using `mcp-remote` to wrap HTTP-based 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** **Penpot MCP Server**
* The URL above connects directly to the server in the DevEnv, which runs in single-user mode. * The URL above connects directly to the server in the DevEnv, which runs
You do not need to use the proxied URL or the user token that is provided by the Penpot UI. 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** **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 ## Working on Development Tasks
After having made the configuration changes, restart your AI client. After having made the configuration changes, restart your AI client. The
The configured MCP servers should now be running and accessible to your client. 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. The agent's entrypoint for development is an activation of the `penpot`
Start by instructing your agent as follows, project with Serena. Start by instructing your agent as follows,
> Activate project penpot. > Activate project penpot.
and it should retrieve fundamental project information, and it should retrieve fundamental project information, expecting further
expecting further instructions on what to do. 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 ### Checking MCP Server Operability

View File

@ -102,12 +102,13 @@ Host ports are offset by `10000 × N`:
| Serena MCP | `http://localhost:14281` | `http://localhost:24281` | `http://localhost:34281` | | Serena MCP | `http://localhost:14281` | `http://localhost:24281` | `http://localhost:34281` |
Container-internal ports stay fixed. Target a specific instance with Container-internal ports stay fixed. Target a specific instance with
`--ws 1` on `attach-devenv`, `run-devenv-agentic`, `stop-devenv`, and `--ws N` on `attach-devenv`, `run-devenv-agentic`, `stop-devenv`,
`start-coding-agent` (`--instance ws1` on `run-devenv-shell` / `start-coding-agent`, `run-devenv-shell`, and `isolated-shell`. `--ws`
`run-devenv`). The `--ws` flag accepts a **non-negative integer only** accepts a **non-negative integer only**`--ws main` or `--ws ws1` is
`--ws main` or `--ws ws1` is rejected, keeping the flag shape uniform across rejected, keeping the flag shape uniform across commands. `run-devenv` is
commands. `run-devenv-agentic` also accepts `--serena-context CTX` and ws0-only and takes no workspace flag. `run-devenv-agentic` also accepts
`--git-user-name NAME` / `--git-user-email EMAIL` (see below). `--serena-context CTX` and `--git-user-name NAME` / `--git-user-email
EMAIL` (see below).
### Git identity inside the container ### Git identity inside the container

262
manage.sh
View File

@ -549,31 +549,68 @@ function stop-devenv {
fi 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 { function drop-devenv {
local ws # Parse args ourselves to decide whether the image gets removed.
for ws in $(list-running-instances); do # stop-devenv then re-parses the same flags and runs the actual stop.
# Never -v: data preservation rule. local target=""
instance-compose "$ws" down -t 2 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 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..." local stop_args=()
docker images $DEVENV_IMGNAME -q | xargs --no-run-if-empty docker rmi [[ -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 { function log-devenv {
# Tail ws0 by default; for multi-instance dev, attach explicitly per project. local target="ws0"
instance-compose ws0 logs -f --tail=50 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 { function run-devenv-tmux {
local extra_env_args=() local extra_env_args=()
local instance="ws0"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--instance)
instance="$(normalize-instance "$2")"; shift 2;;
-e) -e)
extra_env_args+=(-e "$2"); shift 2;; extra_env_args+=(-e "$2"); shift 2;;
-e*) -e*)
@ -584,39 +621,19 @@ function run-devenv-tmux {
esac esac
done done
if ! devenv-main-running "$instance"; then if ! devenv-main-running ws0; then
if [[ "$instance" == "ws0" ]]; then start-devenv
start-devenv echo "Waiting for containers fully start (5s)..."
echo "Waiting for containers fully start (5s)..." sleep 5
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
fi fi
local container local container
container=$(devenv-main-container "$instance") container=$(devenv-main-container ws0)
docker exec -ti \ docker exec -ti \
"${extra_env_args[@]}" \ "${extra_env_args[@]}" \
"$container" sudo -EH -u penpot PENPOT_PLUGIN_DEV=$PENPOT_PLUGIN_DEV /home/start-tmux.sh "$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 # Strict parser for --ws values. Accepts a bare integer in the supported
# range (0..PENPOT_MAX_WS_INDEX) and returns the canonical "wsN" form. # range (0..PENPOT_MAX_WS_INDEX) and returns the canonical "wsN" form.
# Anything else fails fast. The upper bound exists because the host ports # Anything else fails fast. The upper bound exists because the host ports
@ -789,6 +806,23 @@ function run-devenv-agentic {
return 1 return 1
fi 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 pull-devenv-if-not-exists
ensure-devenv-network ensure-devenv-network
ensure-infra-up ensure-infra-up
@ -827,8 +861,8 @@ function run-devenv-shell {
local positional=() local positional=()
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--instance) --ws)
instance="$(normalize-instance "$2")"; shift 2;; instance="$(parse-ws-integer "$2")" || return 1; shift 2;;
*) *)
positional+=("$1"); shift;; positional+=("$1"); shift;;
esac esac
@ -839,12 +873,20 @@ function run-devenv-shell {
start-devenv start-devenv
else else
echo "Instance '$instance' is not running." >&2 echo "Instance '$instance' is not running." >&2
echo "Bring it up first with './manage.sh run-devenv-agentic --ws ${instance#ws}'." >&2
return 1 return 1
fi fi
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 local container
container=$(devenv-main-container "$instance") container=$(devenv-main-container "$instance")
docker exec -ti \ docker exec -ti \
-w /home/penpot/penpot \
-e JAVA_OPTS="$JAVA_OPTS" \ -e JAVA_OPTS="$JAVA_OPTS" \
-e EXTERNAL_UID=$CURRENT_USER_ID \ -e EXTERNAL_UID=$CURRENT_USER_ID \
"$container" sudo -EH -u penpot "${positional[@]}" "$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 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 "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 return 1
fi fi
@ -995,17 +1038,52 @@ function start-coding-agent {
} }
function run-devenv-isolated-shell { 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 \ docker run -ti --rm \
--mount source=${PENPOT_USER_DATA_VOLUME},type=volume,target=/home/penpot/ \ --mount source="$user_data_volume",type=volume,target=/home/penpot/ \
--mount source=`pwd`,type=bind,target=/home/penpot/penpot \ --mount source="$source_path",type=bind,target=/home/penpot/penpot \
-e EXTERNAL_UID=$CURRENT_USER_ID \ -e EXTERNAL_UID=$CURRENT_USER_ID \
-e BUILD_STORYBOOK=$BUILD_STORYBOOK \ -e BUILD_STORYBOOK=$BUILD_STORYBOOK \
-e BUILD_WASM=$BUILD_WASM \ -e BUILD_WASM=$BUILD_WASM \
-e SHADOWCLJS_EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS \ -e SHADOWCLJS_EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS \
-e JAVA_OPTS="$JAVA_OPTS" \ -e JAVA_OPTS="$JAVA_OPTS" \
-w /home/penpot/penpot/$1 \ -w /home/penpot/penpot \
$DEVENV_IMGNAME:latest sudo -EH -u penpot $@ "$DEVENV_IMGNAME:latest" sudo -EH -u penpot "${positional[@]}"
} }
function build-imagemagick-docker-image { function build-imagemagick-docker-image {
@ -1216,40 +1294,68 @@ function build-storybook-docker-image {
function usage { function usage {
echo "PENPOT build & release manager" echo "PENPOT build & release manager"
echo "USAGE: $0 OPTION" echo "USAGE: $0 OPTION"
echo "Options:" echo ""
echo "- pull-devenv Pulls docker development oriented image" echo "Development environment (devenv)"
echo "- build-devenv Build docker development oriented image" echo "--------------------------------"
echo "- build-devenv --local Build a local docker development oriented image" echo "The devenv runs Penpot in a Docker container and supports parallel"
echo "- create-devenv Create the development oriented docker compose service." echo "'workspaces': ws0 (the live repo at \$PWD) and optional wsN (N >= 1, sibling"
echo "- start-devenv Start the development oriented docker compose service." echo "clones). Use --ws N to target a specific workspace; the default is 0."
echo "- stop-devenv Stops the development oriented docker compose service." echo "Full guide: docs/technical-guide/developer/{devenv,agentic-devenv}.md."
echo "- drop-devenv Remove the development oriented docker compose containers, volumes and clean images." echo ""
echo "- run-devenv Brings ws0 up and attaches to its tmux session (no MCP, no Serena)." echo "Image lifecycle"
echo " Optional --instance <wsN> targets a different instance." echo "- pull-devenv Pull the devenv docker image from the registry."
echo " Optional -e flags are forwarded to 'docker exec' (e.g. -e MY_VAR=value)." echo "- build-devenv [--local] Build the devenv docker image (--local skips the registry push)."
echo "- run-devenv-agentic Brings one agentic instance (MCP + Serena) up. Errors out if it is already running." echo ""
echo " Auto-starts ws0 first when --ws N (N>=1) is requested (workers run only on ws0)." echo "Bring a devenv up / down"
echo " Options: --ws N (default: 0; non-negative integer only)," echo "- run-devenv-agentic Bring one workspace up with AI-agent tooling enabled (MCP + Serena),"
echo " --sync (re-seed workspace from live repo; ws1+ only)," echo " start its tmux session in the background, regenerate the per-workspace"
echo " --serena-context CONTEXT (default: desktop-app)," echo " MCP configs, and print the workspace's URLs. Errors out if the target"
echo " --git-user-name NAME / --git-user-email EMAIL" echo " is already running."
echo " (default: host's effective 'git config user.{name,email}'," echo " Options:"
echo " honouring per-repo local overrides)" echo " --ws N target workspace (default: 0). N >= 1 auto-starts"
echo "- attach-devenv Attaches to the tmux session inside a running instance." echo " ws0 first if it is not already up."
echo " Options: --ws N (default: 0; non-negative integer only)" echo " --sync re-seed the wsN clone from the live repo before"
echo "- start-coding-agent <client> Launches an AI coding agent against one workspace with the right MCP config wired in." 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 " client: claude | opencode | vscode | codex"
echo " Options: --ws N (default: 0; non-negative integer only)." echo " Options: --ws N (default: 0). See agentic-devenv.md and"
echo " cd's into the target workspace and refuses to launch if the instance is not running." echo " .devenv/README.md for per-client setup and override paths."
echo " Extra args after --ws (or after the client name) are forwarded to the underlying client." echo "- run-devenv-shell Open a bash shell inside the workspace's running devenv container"
echo " See docs/technical-guide/developer/agentic-devenv.md and .devenv/README.md" echo " ('docker exec' into the live 'main' container alongside the tmux"
echo " for per-client setup, override paths, and the Codex 'trusted project' caveat." echo " session). Requires the workspace to be up."
echo "- run-devenv-shell Opens a bash shell inside a running instance." echo " Options: --ws N (default: 0)."
echo " Options: --instance 0|wsN|N (default: 0)" echo "- run-devenv Start ws0 if needed and attach to its tmux session interactively."
echo "- stop-devenv Stops instances. ws0 must be the last to stop; shared infra stops with ws0." echo " Optional -e flags are forwarded to 'docker exec' (e.g. -e MY_VAR=value)."
echo " Options: --ws N (stop one ws1+) | --ws 0 | (none) (stop ws0 + infra) | --all" echo "- isolated-shell Spawn a fresh, ephemeral devenv container ('docker run', not 'docker exec')"
echo "- isolated-shell Starts a bash shell in a new devenv container." echo " with the workspace's source tree and build-cache volume mounted. Use this"
echo "- log-devenv Show logs of the running devenv docker compose service." 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 ""
echo "- build-bundle Build all bundles (frontend, backend, exporter, storybook and mcp)." echo "- build-bundle Build all bundles (frontend, backend, exporter, storybook and mcp)."
echo "- build-frontend-bundle Build frontend bundle" echo "- build-frontend-bundle Build frontend bundle"