From 945c44c505f227ef67f06d97c1cd873d985f50cb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 4 Jun 2026 12:22:35 +0200 Subject: [PATCH] :wrench: Update docker terminal settings and refactor devenv management (#10018) * :wrench: Update docker terminal settings * :wrench: Get back the HTTP listener for devenv * :bug: Fix problem with https port * :paperclip: Fixup Signed-off-by: Andrey Antukh --------- Signed-off-by: Andrey Antukh Co-authored-by: alonso.torres --- docker/devenv/defaults.env | 14 +- docker/devenv/docker-compose.infra.yml | 13 + docker/devenv/docker-compose.main.yml | 30 +- docker/devenv/files/bashrc | 3 + docker/devenv/files/entrypoint.sh | 4 + docker/devenv/files/start-tmux.sh | 6 +- docker/devenv/files/tmux.conf | 3 + manage.sh | 397 ++++++++----------------- 8 files changed, 167 insertions(+), 303 deletions(-) diff --git a/docker/devenv/defaults.env b/docker/devenv/defaults.env index 70368b3c64..c6c2b914b0 100644 --- a/docker/devenv/defaults.env +++ b/docker/devenv/defaults.env @@ -13,23 +13,20 @@ # volumes survive project renames without a data migration. ws0 reuses the # pre-Stage-2 physical volume names (penpotdev_*). PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-ws0-main -PENPOT_VALKEY_CONTAINER_NAME=penpot-devenv-ws0-valkey -PENPOT_VALKEY_HOSTNAME=penpot-devenv-ws0-valkey PENPOT_POSTGRES_DATA_VOLUME=penpotdev_postgres_data_pg16 PENPOT_MINIO_DATA_VOLUME=penpotdev_minio_data PENPOT_USER_DATA_VOLUME=penpotdev_user_data -PENPOT_VALKEY_DATA_VOLUME=penpotdev_valkey_data -# Backend runtime config (passed to the container env block). PENPOT_REDIS_URI -# is set explicitly per instance to match the per-instance Valkey container -# name; ws1+ overlays override this. +# Backend runtime config (passed to the container env block). Valkey is a +# shared infra service at hostname 'valkey'; each workspace uses a different +# database number (0 for ws0, 1 for ws1, ...) set by instance-env-overrides. PENPOT_HOST=devenv PENPOT_PUBLIC_URI=https://localhost:3449 PENPOT_DATABASE_URI=postgresql://postgres/penpot PENPOT_DATABASE_USERNAME=penpot PENPOT_DATABASE_PASSWORD=penpot PENPOT_DATABASE_MAX_POOL_SIZE=20 -PENPOT_REDIS_URI=redis://penpot-devenv-ws0-valkey/0 +PENPOT_REDIS_URI=redis://valkey/0 # Object storage (MinIO user/policy are provisioned by the infra compose file). PENPOT_OBJECTS_STORAGE_BACKEND=s3 @@ -44,7 +41,8 @@ AWS_SECRET_ACCESS_KEY=penpot-devenv # accessed in-process or through the same-origin Caddy/nginx proxy at # PENPOT_PUBLIC_HTTP_PORT. Container-internal ports remain fixed; per-instance # overlays may offset these host-side values. -PENPOT_PUBLIC_HTTP_PORT=3449 +PENPOT_PUBLIC_HTTPS_PORT=3449 +PENPOT_PUBLIC_HTTP_PORT=3450 PENPOT_MCP_SERVER_PORT=4401 PENPOT_MCP_REPL_PORT=4403 diff --git a/docker/devenv/docker-compose.infra.yml b/docker/devenv/docker-compose.infra.yml index 5bf979c4f8..c32c6a6163 100644 --- a/docker/devenv/docker-compose.infra.yml +++ b/docker/devenv/docker-compose.infra.yml @@ -8,6 +8,8 @@ volumes: name: ${PENPOT_POSTGRES_DATA_VOLUME} minio_data: name: ${PENPOT_MINIO_DATA_VOLUME} + valkey_data: + name: penpotdev_valkey_data services: minio: @@ -66,6 +68,17 @@ services: aliases: - postgres + valkey: + image: valkey/valkey:8.1 + restart: always + command: valkey-server --save 120 1 --loglevel warning + volumes: + - "valkey_data:/data" + networks: + default: + aliases: + - valkey + mailer: image: sj26/mailcatcher:latest restart: always diff --git a/docker/devenv/docker-compose.main.yml b/docker/devenv/docker-compose.main.yml index fb8caf78c7..23258fa447 100644 --- a/docker/devenv/docker-compose.main.yml +++ b/docker/devenv/docker-compose.main.yml @@ -6,8 +6,6 @@ networks: volumes: user_data: name: ${PENPOT_USER_DATA_VOLUME} - valkey_data: - name: ${PENPOT_VALKEY_DATA_VOLUME} services: main: @@ -18,21 +16,16 @@ services: container_name: "${PENPOT_MAIN_CONTAINER_NAME}" stop_signal: SIGINT - # postgres / minio / minio-setup live in the penpotdev-infra compose - # project and cannot be referenced via depends_on across projects. - # manage.sh waits for infra readiness before bringing main up. - depends_on: - redis: - condition: service_started - volumes: - "user_data:/home/penpot/" - "${PENPOT_SOURCE_PATH}:/home/penpot/penpot:z" ports: # Host ports are instance-specific; container ports stay fixed. - - ${PENPOT_PUBLIC_HTTP_PORT}:3449 - - ${PENPOT_PUBLIC_HTTP_PORT}:3449/udp + - ${PENPOT_PUBLIC_HTTPS_PORT}:3449 + - ${PENPOT_PUBLIC_HTTPS_PORT}:3449/udp + - ${PENPOT_PUBLIC_HTTP_PORT}:3450 + - ${PENPOT_PUBLIC_HTTP_PORT}:3450/udp # MCP - ${PENPOT_MCP_SERVER_PORT}:4401 @@ -44,6 +37,8 @@ services: environment: - EXTERNAL_UID=${CURRENT_USER_ID} + - COLORTERM=truecolor + - TERM=xterm-256color # SMTP setup (shared infra service; identical across instances) - PENPOT_SMTP_ENABLED=true @@ -83,6 +78,7 @@ services: - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - PENPOT_BACKEND_WORKER=${PENPOT_BACKEND_WORKER} + - PENPOT_TENANT=${PENPOT_TENANT} - PENPOT_TMUX_ATTACH=${PENPOT_TMUX_ATTACH} # Agentic devenv: set to a commit/tag to update Serena on startup, @@ -92,15 +88,3 @@ services: networks: - default - - redis: - image: valkey/valkey:8.1 - hostname: "${PENPOT_VALKEY_HOSTNAME}" - container_name: "${PENPOT_VALKEY_CONTAINER_NAME}" - restart: always - command: valkey-server --save 120 1 --loglevel warning - volumes: - - "valkey_data:/data" - - networks: - - default diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index 799d2f285a..ed497021f6 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -9,6 +9,9 @@ export CARGO_HOME="/home/penpot/.cargo" export PENPOT_MCP_PLUGIN_SERVER_HOST=0.0.0.0 export PENPOT_MCP_SERVER_HOST=0.0.0.0 +export LANG=C.UTF-8 +export LC_ALL=C.UTF-8 + alias l='ls --color -GFlh' alias ll='ls --color -GFlh' alias rm='rm -rf' diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh index e12b062b4d..1cae3f8e3f 100755 --- a/docker/devenv/files/entrypoint.sh +++ b/docker/devenv/files/entrypoint.sh @@ -27,4 +27,8 @@ export JAVA_OPTS="-Djava.net.preferIPv4Stack=true" export PATH="/home/penpot/.cargo/bin:$PATH" export CARGO_HOME="/home/penpot/.cargo" +export LANG=C.UTF-8 +export LC_ALL=C.UTF-8 +export COLORTERM=truecolor + exec "$@" diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index a029dbe84a..864df21841 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -23,6 +23,10 @@ if tmux has-session -t "$PENPOT_TMUX_SESSION" 2>/dev/null; then attach_or_exit fi +# Create the tmux session first so attach-devenv can connect immediately +# even while setup scripts are still running. +tmux -2 new-session -d -s "$PENPOT_TMUX_SESSION" + echo "[start-tmux.sh] Installing node dependencies" pushd ~/penpot/frontend/ ./scripts/setup; @@ -31,8 +35,6 @@ pushd ~/penpot/exporter/ ./scripts/setup; popd -tmux -2 new-session -d -s "$PENPOT_TMUX_SESSION" - tmux rename-window -t "$PENPOT_TMUX_SESSION:0" 'frontend watch' tmux select-window -t "$PENPOT_TMUX_SESSION:0" tmux send-keys -t "$PENPOT_TMUX_SESSION" 'cd penpot/frontend' enter C-l diff --git a/docker/devenv/files/tmux.conf b/docker/devenv/files/tmux.conf index 2044d56556..062c1622c7 100644 --- a/docker/devenv/files/tmux.conf +++ b/docker/devenv/files/tmux.conf @@ -1,3 +1,6 @@ +set -g default-terminal "tmux-256color" +set -ga terminal-overrides ",xterm-256color:Tc" + set -g default-command "${SHELL}" set -g mouse off set -g history-limit 50000 diff --git a/manage.sh b/manage.sh index 38c5edc415..5066e3f608 100755 --- a/manage.sh +++ b/manage.sh @@ -42,16 +42,17 @@ export PENPOT_WORKSPACES_DIR="${PENPOT_WORKSPACES_DIR:-$HOME/.penpot/penpot_work # is missing one. Consumed by instance-env-overrides (the values injected into # the per-instance compose env) and print-instance-info (the startup URLs). PENPOT_INSTANCE_PORT_STRIDE=10000 +PENPOT_PORT_BASE_PUBLIC_HTTPS=${PENPOT_PUBLIC_HTTPS_PORT:?missing in defaults.env} PENPOT_PORT_BASE_PUBLIC=${PENPOT_PUBLIC_HTTP_PORT:?missing in defaults.env} PENPOT_PORT_BASE_MCP=${PENPOT_MCP_SERVER_PORT:?missing in defaults.env} PENPOT_PORT_BASE_MCP_REPL=${PENPOT_MCP_REPL_PORT:?missing in defaults.env} PENPOT_PORT_BASE_SERENA=${SERENA_EXTERNAL_PORT:?missing in defaults.env} PENPOT_PORT_BASE_SERENA_DASHBOARD=${SERENA_DASHBOARD_EXTERNAL_PORT:?missing in defaults.env} -# Per-instance values like PENPOT_REDIS_URI must live in each instance's env -# file (not in this shell), because docker compose's --env-file mechanism -# lets a per-instance overlay override the baseline while the shell env -# would otherwise shadow both for every project. +# Per-instance values like PENPOT_REDIS_URI are injected by +# instance-env-overrides as shell env variables (not set in this shell), +# because docker compose gives shell-env precedence over --env-file, letting +# per-instance values override the defaults.env baseline. export CURRENT_USER_ID=$(id -u); export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD); @@ -80,28 +81,20 @@ set -e # infra-compose wrap 'docker compose' for the shared-infra project # instance-compose wrap 'docker compose' for one instance's main # project, injecting that instance's overrides -# instance-env-overrides the per-instance KEY=VALUE overrides (ws1+) +# instance-env-overrides the per-instance KEY=VALUE overrides # devenv-main-container resolve the 'main' container id via compose ps # devenv-main-running true if 'main' is up # -# Devenv lifecycle (operate on the whole compose project) -# start-devenv, create-devenv, stop-devenv, drop-devenv, log-devenv +# Devenv bring-up commands (bring a workspace up + start background tmux) +# run-devenv bring one workspace up; supports --ws, --sync, +# --agentic (enables MCP + Serena), -e, +# --serena-context, git identity # -# Devenv interactive entry points (all operate on the running 'main' container) -# run-devenv-tmux starts 'main' if needed and execs start-tmux.sh -# interactively (this is what 'run-devenv' resolves to) -# run-devenv-agentic same as run-devenv-tmux but enables MCP + Serena; -# also regenerates .devenv/mcp/.json for the -# target workspace (via write-instance-mcp-configs) -# attach-devenv pure attach to the existing tmux session; fails -# fast if the devenv or session is missing -# start-coding-agent launches Claude Code or opencode against the -# current workspace's generated MCP config -# run-devenv-shell starts 'main' if needed and execs a bash shell -# run-devenv-isolated-shell one-shot 'docker run' (NOT compose) against the -# project user_data volume and the current PWD; used -# for ad-hoc operations that should not touch a -# running devenv +# Devenv interactive entry points (operate on the running 'main' container) +# attach-devenv pure attach to the existing tmux session; fails +# fast if the devenv or session is missing +# start-coding-agent launches Claude Code or opencode against the +# current workspace's generated MCP config # # Production build pipeline # build one-shot 'docker run' that invokes a per-module @@ -181,18 +174,20 @@ function ensure-devenv-network { # # - Shared infrastructure (postgres, minio, mailer, ldap, minio-setup) runs # under project `penpotdev-infra`. -# - Each runtime instance (ws0, ws1, ...) runs its own main + valkey under -# project `penpotdev-wsN`. ws0 uses the `defaults.env` baseline as-is; ws1+ -# override the per-instance values by injecting them as environment -# variables -- see instance-env-overrides (no files are written). +# - Shared infrastructure (postgres, minio, mailer, ldap, valkey, minio-setup) +# runs under project `penpotdev-infra`. +# - Each runtime instance (ws0, ws1, ...) runs only its own main container +# under project `penpotdev-wsN`. All workspaces uniformly overlay their +# per-instance values via instance-env-overrides injected as shell env +# variables (no files are written). # `env -i` strips the ambient shell before invoking docker compose, then we # re-inject exactly what compose needs. The stripping matters because # defaults.env is sourced into manage.sh's own shell at startup, so otherwise # those stale values would leak into substitution. And because Docker Compose # gives shell-env precedence over --env-file, the re-injected per-instance # overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH -# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and for ws1+ -# the instance-env-overrides block. +# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and the +# instance-env-overrides block. function infra-compose { env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \ docker compose -p penpotdev-infra \ @@ -204,13 +199,15 @@ function infra-compose { function instance-compose { local instance="$1"; shift local source_path - local -a overrides=() if [[ "$instance" == "ws0" ]]; then source_path="$PWD" else source_path="$(workspace-path "$instance")" - mapfile -t overrides < <(instance-env-overrides "$instance") fi + + # Per-instance overrides apply to all workspaces uniformly. + mapfile -t overrides < <(instance-env-overrides "$instance") + env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \ CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \ PENPOT_SOURCE_PATH="$source_path" \ @@ -291,19 +288,23 @@ function instance-port { echo $(( base + n * PENPOT_INSTANCE_PORT_STRIDE )) } -# Echo the per-instance Compose variable overrides for a ws1+ instance, one +# Echo the per-instance Compose variable overrides for a workspace, one # KEY=VALUE per line, for instance-compose to inject into its `env -i` line. # Compose gives shell-env precedence over --env-file, so these override the # defaults.env baseline. Every value is a pure function of the instance number, # so nothing is persisted: they are recomputed on every compose invocation and -# can never drift from this logic. ws0 has no overrides (it uses defaults.env -# as-is) and is never passed here. +# can never drift from this logic. Called for every workspace (ws0, ws1, ...). +# All overrides are pure functions of the instance number; no per-instance +# post-processing is needed. # # Omitted on purpose: COMPOSE_PROJECT_NAME (set via compose's -p flag), # PENPOT_SOURCE_PATH (injected directly by instance-compose), and function instance-env-overrides { local instance="$1" - local public mcp mcp_repl serena serena_dash + local n=0 + [[ "$instance" =~ ^ws([0-9]+)$ ]] && n="${BASH_REMATCH[1]}" + local public_https public mcp mcp_repl serena serena_dash + public_https=$(instance-port "$instance" "$PENPOT_PORT_BASE_PUBLIC_HTTPS") public=$(instance-port "$instance" "$PENPOT_PORT_BASE_PUBLIC") mcp=$(instance-port "$instance" "$PENPOT_PORT_BASE_MCP") mcp_repl=$(instance-port "$instance" "$PENPOT_PORT_BASE_MCP_REPL") @@ -311,19 +312,17 @@ function instance-env-overrides { serena_dash=$(instance-port "$instance" "$PENPOT_PORT_BASE_SERENA_DASHBOARD") printf '%s\n' \ "PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-${instance}-main" \ - "PENPOT_VALKEY_CONTAINER_NAME=penpot-devenv-${instance}-valkey" \ - "PENPOT_VALKEY_HOSTNAME=penpot-devenv-${instance}-valkey" \ "PENPOT_USER_DATA_VOLUME=penpotdev_${instance}_user_data" \ - "PENPOT_VALKEY_DATA_VOLUME=penpotdev_${instance}_valkey_data" \ - "PENPOT_PUBLIC_URI=https://localhost:${public}" \ - "PENPOT_REDIS_URI=redis://penpot-devenv-${instance}-valkey/0" \ + "PENPOT_PUBLIC_URI=https://localhost:${public_https}" \ + "PENPOT_REDIS_URI=redis://valkey/${n}" \ + "PENPOT_PUBLIC_HTTPS_PORT=${public_https}" \ "PENPOT_PUBLIC_HTTP_PORT=${public}" \ "PENPOT_MCP_SERVER_PORT=${mcp}" \ "PENPOT_MCP_REPL_PORT=${mcp_repl}" \ "SERENA_EXTERNAL_PORT=${serena}" \ "SERENA_DASHBOARD_EXTERNAL_PORT=${serena_dash}" \ - "PENPOT_BACKEND_WORKER=false" \ - "SHADOW_SERVER_URL=wss://localhost:${public}" + "SHADOW_SERVER_URL=wss://localhost:${public_https}" \ + "PENPOT_TENANT=devenv-${instance}" } # Thin wrapper around .devenv/scripts/merge-mcp-config.py for the JSON clients @@ -460,14 +459,6 @@ function sync-workspace { ) } -function start-devenv { - pull-devenv-if-not-exists $@; - ensure-devenv-network; - - ensure-infra-up - instance-compose ws0 up -d -} - function create-devenv { pull-devenv-if-not-exists $@; ensure-devenv-network; @@ -603,34 +594,6 @@ function log-devenv { instance-compose "$target" logs -f --tail=50 } -function run-devenv-tmux { - local extra_env_args=() - - while [[ $# -gt 0 ]]; do - case "$1" in - -e) - extra_env_args+=(-e "$2"); shift 2;; - -e*) - extra_env_args+=(-e "${1#-e}"); shift;; - *) - echo "run-devenv: unknown argument '$1'" >&2 - return 1;; - esac - done - - if ! devenv-main-running ws0; then - start-devenv - echo "Waiting for containers fully start (5s)..." - sleep 5 - fi - - local container - 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 -} - # 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 @@ -653,16 +616,19 @@ function parse-ws-integer { } -# Bring a single agentic instance up: compose up + detached tmux start with -# MCP and Serena enabled. Workspace sync and env-file generation are the -# caller's responsibility (run-devenv-agentic handles them for ws1+). +# Bring a single instance up: compose up + detached tmux start. When agentic +# is true (the default) the tmux session gets MCP + Serena enabled; when false +# it is a plain non-agentic workspace (no MCP, no Serena). Workspace sync and +# env-file generation are the caller's responsibility (run-devenv handles +# them for ws1+). function start-instance { local instance="$1" - local serena_context="$2" + local serena_context="${2:-}" local git_user_name="${3:-}" local git_user_email="${4:-}" + local agentic="${5:-true}" - instance-compose "$instance" up -d --no-deps main redis + instance-compose "$instance" up -d main # Wait briefly for main to be reachable; the tmux session lives inside. local container deadline @@ -676,6 +642,12 @@ function start-instance { sleep 1 done + # Ensure /home/penpot is writable by the penpot user before touching + # any files inside it (e.g. .gitconfig). start-tmux.sh also does this + # but runs later asynchronously, so the container may still be root-owned + # from a fresh volume mount at this point. + docker exec "$container" sudo chown penpot:users /home/penpot 2>/dev/null || true + # Seed the container's global git config from the values resolved on the # host so commits made inside the devenv carry a real author/committer. Empty # values are skipped — the host-identity warning is the caller's job. @@ -686,13 +658,18 @@ function start-instance { docker exec "$container" sudo -u penpot git config --global user.email "$git_user_email" fi - # Detached tmux so callers don't block on attach. Agentic mode is the only - # mode here, so MCP and Serena are always on. + # Detached tmux so callers don't block on attach. Agentic mode adds the + # MCP and Serena env vars that start-tmux.sh checks. + local -a tmux_env=(-e PENPOT_TMUX_ATTACH=false) + if [[ "$agentic" == "true" ]]; then + tmux_env+=( + -e PENPOT_FLAGS="${PENPOT_FLAGS:-} enable-mcp" + -e SERENA_ENABLED=true + -e SERENA_CONTEXT="$serena_context" + ) + fi docker exec -d \ - -e PENPOT_TMUX_ATTACH=false \ - -e PENPOT_FLAGS="${PENPOT_FLAGS:-} enable-mcp" \ - -e SERENA_ENABLED=true \ - -e SERENA_CONTEXT="$serena_context" \ + "${tmux_env[@]}" \ "$container" \ sudo -EH -u penpot PENPOT_PLUGIN_DEV="${PENPOT_PLUGIN_DEV:-}" /home/start-tmux.sh } @@ -721,7 +698,7 @@ function print-instance-info { echo echo "[$instance]" - echo " Penpot UI: https://localhost:${public}" + echo " Penpot UI: https://localhost:${public_https}" echo " MCP stream: http://localhost:${mcp}/mcp" echo " Serena MCP: http://localhost:${serena}" echo " Serena dashboard: http://localhost:${serena_dash}" @@ -729,31 +706,17 @@ function print-instance-info { echo " Coding agent: ./manage.sh start-coding-agent claude${ws_flag} (or: opencode|vscode|codex)" } -# Bring up a single agentic instance (always with MCP + Serena). -# -# --ws N target instance (non-negative integer). Default: 0 (ws0). -# --sync re-seed the workspace from the live repo. Forbidden on main; -# ws1+ also sync implicitly the first time when their workspace -# directory does not exist yet. -# --serena-context CTX passed to Serena (default: desktop-app). -# --git-user-name NAME Git author/committer name to wire into the -# container's global git config (so commits made inside the -# devenv carry a real identity). Defaults to the host's -# effective `git config user.name` when omitted (resolved at -# the current working directory, so a per-repo local override -# in /.git/config takes precedence over ~/.gitconfig). -# --git-user-email EMAIL matching email; defaults to the host's effective -# `git config user.email` (same local-over-global precedence). -# -# ws0 is the worker-bearer and must be running whenever any ws1+ is up. When -# starting ws1+, ws0 is brought up first automatically if it is not already -# running. Errors out if the requested target itself is already running. -function run-devenv-agentic { +# Bring a single workspace up. Without --agentic it's non-agentic (no MCP, no +# Serena); with --agentic it enables MCP + Serena for AI-driven development. +# Supports --ws for parallel workspace targets; ws0 is the default. +function run-devenv { local target="ws0" local do_sync=false + local agentic=false local serena_context="desktop-app" local git_user_name="" local git_user_email="" + local -a extra_env_args=() while [[ $# -gt 0 ]]; do case "$1" in @@ -761,31 +724,57 @@ function run-devenv-agentic { target="$(parse-ws-integer "$2")" || return 1; shift 2;; --sync) do_sync=true; shift;; + --agentic) + agentic=true; shift;; --serena-context) serena_context="$2"; shift 2;; --git-user-name) git_user_name="$2"; shift 2;; --git-user-email) git_user_email="$2"; shift 2;; + -e) + extra_env_args+=(-e "$2"); shift 2;; + -e*) + extra_env_args+=(-e "${1#-e}"); shift;; + -h|--help) + echo "Usage: run-devenv [--ws N] [--sync] [--agentic] [--serena-context CTX] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]" + echo " Bring a single workspace up." + echo " --ws N target workspace (default: 0)." + echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)." + echo " --agentic enable MCP + Serena (AI-agent mode)." + echo " --serena-context CTX context passed to Serena (default: desktop-app)." + echo " --git-user-name NAME git author name inside the container (default: host git config)." + echo " --git-user-email EMAIL git author email inside the container." + echo " -e KEY=VAL forward env var to docker exec on attach." + return 0;; *) - echo "run-devenv-agentic: unknown argument '$1'" >&2 + echo "run-devenv: unknown argument '$1' (use --help for usage)" >&2 return 1;; esac done if [[ "$target" == "ws0" && "$do_sync" == "true" ]]; then - echo "run-devenv-agentic: --sync is not allowed on main (ws0)." >&2 + echo "run-devenv: --sync is not allowed on main (ws0)." >&2 return 1 fi - # Fall back to the host developer's effective git identity when the - # respective flag is not provided. Plain `git config user.X` (no --global) - # honours the local->global->system precedence, so a per-repo override in - # /.git/config takes precedence over ~/.gitconfig -- matching what - # `git commit` on the host would actually record. `|| true` swallows the - # non-zero exit for a missing entry; the empty result is propagated - # untouched and surfaces as a no-op inside the container (start-tmux.sh - # skips `git config --global` when the env var is empty). + # Pre-flight: config.js must exist for agentic mode. The file is gitignored; + # without it the frontend never sets 'enable-mcp', so the agent can't drive + # Penpot via MCP. + if [[ "$agentic" == "true" ]]; then + 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 with --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 + fi + + # Resolve git identity from the host when flags are omitted. if [[ -z "$git_user_name" ]]; then git_user_name="$(git config user.name 2>/dev/null || true)" fi @@ -799,24 +788,7 @@ function run-devenv-agentic { fi if devenv-main-running "$target"; then - echo "run-devenv-agentic: instance '$target' is already running." >&2 - 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 + echo "run-devenv: instance '$target' is already running." >&2 return 1 fi @@ -824,16 +796,6 @@ function run-devenv-agentic { ensure-devenv-network ensure-infra-up - # ws0 invariant: must be up whenever any ws1+ runs. Bring it up first if a - # non-main instance is being requested and ws0 is not yet running. - if [[ "$target" != "ws0" ]] && ! devenv-main-running "ws0"; then - echo "[ws0] not running; starting it first (workers run only on ws0)." - echo "Starting ws0..." - write-instance-mcp-configs "ws0" - start-instance "ws0" "$serena_context" "$git_user_name" "$git_user_email" - print-instance-info "ws0" - fi - if [[ "$target" != "ws0" ]]; then local workspace workspace=$(workspace-path "$target") @@ -846,48 +808,15 @@ function run-devenv-agentic { fi fi + if [[ "$agentic" == "true" ]]; then + write-instance-mcp-configs "$target" + fi + echo "Starting $target..." - write-instance-mcp-configs "$target" - start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" + start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic" print-instance-info "$target" } -function run-devenv-shell { - 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 - - if ! devenv-main-running "$instance"; then - if [[ "$instance" == "ws0" ]]; then - 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[@]}" -} - function attach-devenv { local instance="ws0" while [[ $# -gt 0 ]]; do @@ -902,7 +831,7 @@ function attach-devenv { if ! devenv-main-running "$instance"; then echo "Instance '$instance' is not running." >&2 - echo "Start it first with './manage.sh run-devenv-agentic [--ws N]'." >&2 + echo "Start it first with './manage.sh run-devenv [--ws N] [--agentic]'." >&2 return 1 fi @@ -968,7 +897,7 @@ function start-coding-agent { fi # --ws is the default-elided flag: only emit it in suggestion strings for - # ws1+; ws0 is the default target so 'run-devenv-agentic' is the right hint. + # ws1+; ws0 is the default target so 'run-devenv --agentic' is the right hint. local ws_flag="" [[ "$instance" != "ws0" ]] && ws_flag=" --ws ${instance#ws}" @@ -981,7 +910,7 @@ function start-coding-agent { workspace="$(workspace-path "$instance")" if [[ ! -d "$workspace" ]]; then echo "start-coding-agent: workspace for $instance not found at $workspace." >&2 - echo "Bring '$instance' up first with './manage.sh run-devenv-agentic${ws_flag}'." >&2 + echo "Bring '$instance' up first with './manage.sh run-devenv${ws_flag} --agentic'." >&2 return 1 fi fi @@ -990,7 +919,7 @@ function start-coding-agent { # Refuse rather than launch an agent that would error on every tool call. if ! devenv-main-running "$instance"; then echo "start-coding-agent: instance '$instance' is not running." >&2 - echo "Start it first with './manage.sh run-devenv-agentic${ws_flag}'." >&2 + echo "Start it first with './manage.sh run-devenv${ws_flag} --agentic'." >&2 return 1 fi @@ -1015,7 +944,7 @@ function start-coding-agent { if [[ ! -f "$workspace/$cfg_rel" ]]; then echo "start-coding-agent: $workspace/$cfg_rel not found." >&2 - echo "Bring '$instance' up with './manage.sh run-devenv-agentic${ws_flag}'," >&2 + echo "Bring '$instance' up with './manage.sh run-devenv${ws_flag} --agentic'," >&2 echo "which sets up the per-workspace MCP config." >&2 return 1 fi @@ -1056,55 +985,6 @@ function start-coding-agent { esac } -function run-devenv-isolated-shell { - 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 - # instance-env-overrides 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="$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 \ - "$DEVENV_IMGNAME:latest" sudo -EH -u penpot "${positional[@]}" -} - function build-imagemagick-docker-image { set +e; echo "Building image penpotapp/imagemagick:$IMAGEMAGICK_VERSION" @@ -1326,23 +1206,22 @@ function usage { 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 "- run-devenv Bring one workspace up, start its tmux session in the background," + echo " and print the workspace's URLs. Pass --agentic to enable MCP + Serena" + echo " for AI-driven development (also regenerates per-workspace MCP configs)." echo " Options:" - echo " --ws N target workspace (default: 0). N >= 1 auto-starts" - echo " ws0 first if it is not already up." + echo " --ws N target workspace (default: 0)." 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 " --agentic enable MCP + Serena (AI-agent mode)." echo " --serena-context CTX passed to Serena (default: desktop-app)." + echo " -e KEY=VAL forwarded to 'docker exec' on attach." 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;" @@ -1364,17 +1243,6 @@ function usage { echo " client: claude | opencode | vscode | codex" 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" @@ -1413,14 +1281,11 @@ case $1 in create-devenv ${@:2} ;; - start-devenv) - start-devenv ${@:2} - ;; run-devenv) - run-devenv-tmux ${@:2} + run-devenv ${@:2} ;; run-devenv-agentic) - run-devenv-agentic ${@:2} + run-devenv --agentic ${@:2} ;; attach-devenv) attach-devenv ${@:2} @@ -1428,14 +1293,6 @@ case $1 in start-coding-agent) start-coding-agent "${@:2}" ;; - run-devenv-shell) - run-devenv-shell ${@:2} - ;; - - isolated-shell) - run-devenv-isolated-shell ${@:2} - ;; - stop-devenv) stop-devenv ${@:2} ;;