diff --git a/docs/technical-guide/developer/agentic-devenv.md b/docs/technical-guide/developer/agentic-devenv.md index c796f16f96..717ba5b4c4 100644 --- a/docs/technical-guide/developer/agentic-devenv.md +++ b/docs/technical-guide/developer/agentic-devenv.md @@ -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: + - ws1: + - 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 +( for ws0, 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 | `/.vscode/mcp.json` | Auto-discovered when opening the workspace | | Codex CLI | `/.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 [--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 diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md index 66a1d7c41b..a27f3a302f 100644 --- a/docs/technical-guide/developer/devenv.md +++ b/docs/technical-guide/developer/devenv.md @@ -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 diff --git a/manage.sh b/manage.sh index ca23c2cc44..b46ef260ee 100755 --- a/manage.sh +++ b/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__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 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 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 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"