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
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

View File

@ -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
View File

@ -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"