#!/bin/bash
# alpha-vscode — root helper behind the VS Code Web add-on (sudoers-whitelisted
# for www-data, same pattern as every alpha-* helper).
#   install            download + install code-server, write config/unit, enable + start
#   remove             stop + disable, drop the unit (keeps config/user data)
#   start|stop|restart|enable|disable   systemctl on the unit
#   setpass            generate a fresh password, write config, restart; prints it
#   workspace <dir>    set the default folder the editor opens (regenerates the unit)
#   host <subdomain>   move the editor to its OWN subdomain (vhost + cert), never the panel URL
#   hostinfo           print the current public host
#   vhostsync          regenerate the vhost for the current host (after template changes)
#   kick <ip>          force-close a client's open web connections (session disconnect)
#   seclock on|off     toggle an extra HTTP-auth gate in front of code-server (prints creds on 'on')
#   vpath-init         create /srv/vsc-ws (workspace aliases for the secret address) — idempotent
# Every nginx change is `nginx -t`-guarded and rolled back on failure, so a bad
# edit can never take the shared panel nginx down.
set -u

# Domain + admin host come from the panel config so this helper works on ANY
# install; the fallbacks only matter on a box provisioned before that file.
[ -r /etc/alpha-panel.conf ] && . /etc/alpha-panel.conf
DOMAIN="${PRIMARY_DOMAIN:-$(hostname -d 2>/dev/null || true)}"; DOMAIN="${DOMAIN:-localhost}"   # the owner's domain, never a literal
ADMIN_HOST="${PANEL_SUBDOMAIN:-}"
CONF=/var/www/alpha-private/code-server.yaml          # password — stays in the credential store
HOSTFILE=/var/www/alpha-private/vscode-host.conf      # current public subdomain
HTPASS=/var/www/alpha-private/vscode-htpasswd         # extra HTTP-auth users (seclock)
BASE="/var/www/Addons/VS Code Web"                    # everything else lives visibly in Addons
DATA="$BASE/UserData"
EXT="$BASE/Extensions"
PROJ="$BASE/Projects"
WORK=/workspaces                                      # neutral symlink → PROJ (hides the real path)
UNIT=code-server-alpha
PORT=8321
CERT_EMAIL="${ADMIN_EMAIL:-root@localhost}"
AVAIL=/etc/nginx/sites-available
ENABLED=/etc/nginx/sites-enabled
AUTHSNIP=/etc/nginx/snippets/alpha-vscode-auth.conf
ZONECONF=/etc/nginx/conf.d/alpha-vscode.conf
INSTDIR=/var/www/alpha-private/code-server-inst          # one yaml per instance (port+password)
INSTSNIP=/etc/nginx/snippets/alpha-vscode-instances.conf # /i/<slug>/ location per instance
INSTBASE="$BASE/Instances"                               # per-instance UserData + Extensions

inst_unit() { echo "code-server-alpha-$1"; }

# One /i/<slug>/ proxy block per instance yaml. proxy_cookie_path scopes each
# instance's code-server-session cookie to its own path so it can never clobber
# the main editor's session (same cookie name, same host).
regen_inst_snippet() {
  : > "$INSTSNIP"
  # read-only guest instance: gated like the main editor (invite cookie →
  # injected session), proxied to the vscro-owned instance on :8330
  if [ -f /etc/alpha-vscode-roguest.yaml ]; then
    cat >> "$INSTSNIP" << 'ROEOF'
    location ^~ /i/roguest/ {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        proxy_pass http://127.0.0.1:8330/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
    }
ROEOF
  fi
  for y in "$INSTDIR"/*.yaml; do
    [ -f "$y" ] || continue
    s=$(basename "$y" .yaml)
    p=$(grep -oE '^bind-addr: .*:[0-9]+' "$y" | grep -oE '[0-9]+$')
    [ -n "$p" ] || continue
    cat >> "$INSTSNIP" << IEOF
    location ^~ /i/$s/ {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        proxy_pass http://127.0.0.1:$p/;
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_cookie_path / /i/$s/;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
        client_max_body_size 512m;
    }
IEOF
  done
}

setup_workspaces() {
  # Neutral /workspaces path for the editor URL. A BIND MOUNT (not a symlink):
  # code-server resolves symlinks back to the real path, but a bind mount keeps
  # `/workspaces` in the URL — so the addon storage path never shows.
  mkdir -p "$PROJ"
  [ -L "$WORK" ] && rm -f "$WORK"
  mkdir -p "$WORK"
  if [ ! -f /etc/systemd/system/workspaces.mount ]; then
    cat > /etc/systemd/system/workspaces.mount << MEOF
[Unit]
Description=VS Code Web neutral workspace bind mount
[Mount]
What=$PROJ
Where=$WORK
Type=none
Options=bind
[Install]
WantedBy=multi-user.target
MEOF
    systemctl daemon-reload
    systemctl enable --now workspaces.mount >/dev/null 2>&1
  elif ! mountpoint -q "$WORK" 2>/dev/null; then
    systemctl start workspaces.mount >/dev/null 2>&1
  fi
}

FAVICON=/var/www/alpha-private/vscode-favicon.svg

write_favicon() {
  # the real VS Code logo (blue), served in place of code-server's default so
  # the browser tab / bookmark shows Visual Studio Code.
  cat > "$FAVICON" << 'FAVEOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#0098FF" d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.9 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/></svg>
FAVEOF
  chmod 644 "$FAVICON"; chown www-data:www-data "$FAVICON"
}

ensure_bits() {
  mkdir -p "$DATA/User" "$EXT" "$PROJ"
  setup_workspaces
  write_favicon
  # host-terminal guard: an alias beats PATH lookup, so nothing (e.g. the Claude
  # extension "helpfully" npm-installing the hanging native build into
  # ~/.npm-global and prepending it to PATH) can shadow the pinned wrapper
  grep -q "alias claude=" "$DATA/.bashrc" 2>/dev/null || \
    echo 'alias claude="/usr/local/bin/claude"   # pinned safe build — PATH tricks cannot override an alias' >> "$DATA/.bashrc"
  # trust external links so the OAuth sign-in tab opens without code-server's
  # "Do you want to open the external website?" prompt (reset by code-server upgrades)
  PJ=/usr/lib/code-server/lib/vscode/product.json
  [ -f "$PJ" ] && python3 -c "import json;p='$PJ';d=json.load(open(p));d['linkProtectionTrustedDomains']=['*'];json.dump(d,open(p,'w'),indent=2)" 2>/dev/null || true
  [ -f "$AUTHSNIP" ] || { mkdir -p /etc/nginx/snippets; : > "$AUTHSNIP"; }
  # the secret address's workspace aliases: /srv/vsc-ws/<id> → real folder, owned by
  # www-data so the panel can register folders without root (see lib/vpath.php)
  mkdir -p /srv/vsc-ws; chown www-data:www-data /srv/vsc-ws; chmod 750 /srv/vsc-ws
  [ -f "$INSTSNIP" ] || : > "$INSTSNIP"
  mkdir -p "$INSTDIR"; chown www-data:www-data "$INSTDIR"; chmod 750 "$INSTDIR"
  if [ ! -f "$ZONECONF" ] || ! grep -q alphavscdoor "$ZONECONF"; then
    cat > "$ZONECONF" << 'ZEOF'
# rate-limits for the VS Code Web add-on (per client IP)
#  alphavscode  - password doors (/login and the guest paths): 20/min, burst 10 - anti-brute-force
#  alphavscdoor - the secret-address door (/v/<token>/workspace/<id>/): 60/min, burst 30 for page loads;
#                 a WebSocket upgrade is keyed to "" (= never limited) so the editor's reconnects cannot
#                 hit 429. Token guessing is throttled by the gate itself (8 wrong tokens -> 30 min hold).
map $http_upgrade $alpha_vsc_doorkey { default $binary_remote_addr; "~*^websocket$" ""; }
limit_req_zone $binary_remote_addr zone=alphavscode:10m rate=20r/m;
limit_req_zone $alpha_vsc_doorkey zone=alphavscdoor:10m rate=60r/m;
ZEOF
  fi
}

# Full TLS vhost (80 redirect + 443 proxy). Assumes the cert for $2 already
# exists — the `host` op obtains it (certbot certonly) BEFORE calling this, so
# we own the whole vhost and never let certbot rewrite blocks (that's what broke
# TLS when two blocks shared a server_name).
gen_vhost() { # $1 = target file, $2 = host
  cat > "$1" << 'VHEOF'
server {
    listen 80;
    listen [::]:80;
    server_name __HOST__;
    location ^~ /.well-known/acme-challenge/ { root /var/www/html; allow all; }
    location / { return 301 https://$host$request_uri; }
}
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name __HOST__;
    ssl_certificate /etc/letsencrypt/live/__HOST__/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/__HOST__/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # VS Code Web add-on — its OWN subdomain, never the secret panel host.
    server_tokens off;
    add_header X-Robots-Tag "noindex, nofollow" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    # clickjacking: only this origin may frame the editor (its own webviews are same-origin).
    # No further CSP on purpose — VS Code's workers, webviews and extensions need their own.
    add_header Content-Security-Policy "frame-ancestors 'self'" always;

    # brand the browser tab with the real VS Code logo (overrides code-server's)
    location = /favicon.ico { alias /var/www/alpha-private/vscode-favicon.svg; default_type image/svg+xml; }
    location = /_static/src/browser/media/favicon.svg { alias /var/www/alpha-private/vscode-favicon.svg; default_type image/svg+xml; }
    location = /_static/src/browser/media/favicon-dark-support.svg { alias /var/www/alpha-private/vscode-favicon.svg; default_type image/svg+xml; }

    # ---- the PHP gate behind every editor request (Addons/vscode-web/v1.0.0/guest.php):
    # guests: validates the invite cookie, injects the minted code-server session at
    # the proxy layer only. Owners: allowed ONLY under the secret address (lib/vpath.php)
    # or a per-project instance; anything else is 403 here → 404 to the client.
    location = /__vscgate {
        internal;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/alpha/Addons/vscode-web/v1.0.0/guest.php;
        fastcgi_param VSC_MODE gate;
        fastcgi_param VSC_ORIG_URI $request_uri;
        fastcgi_param CONTENT_LENGTH "";
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
    location ^~ /guest {
        limit_req zone=alphavscode burst=10 nodelay;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/alpha/Addons/vscode-web/v1.0.0/guest.php;
        fastcgi_param VSC_MODE entry;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
    location @vscdenied {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/alpha/Addons/vscode-web/v1.0.0/guest.php;
        fastcgi_param VSC_MODE denied;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
    error_page 401 = @vscdenied;
    # the generic answer for anything the secret-address rules refuse: nothing to learn from it
    location @vsc404 { return 404; }

    # panel tasks scaffolded into projects (curl from the editor terminal;
    # token-gated by vsc-task itself — no cookies, no panel session)
    location = /panel-task {
        limit_req zone=alphavscode burst=10 nodelay;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/alpha/serve/vsc-task.php;
        fastcgi_read_timeout 120s;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # per-project instances — /i/<slug>/ routed to each instance's own port
    include /etc/nginx/snippets/alpha-vscode-instances.conf;

    # ---- the secret address: /v/<token>/workspace/<id>/ ----
    # The token is checked by the gate (constant-time, throttled); the id maps to a
    # neutral alias /srv/vsc-ws/<id> so no real folder path ever reaches a browser.
    # Three locations, matched in this order; everything else under /v/ is 404.
    # 1. no trailing slash → add it (code-server's relative asset URLs need the directory form)
    location ~ "^/v/[A-Za-z0-9]{30,64}/workspace/[a-z0-9][a-z0-9-]{0,39}$" { return 302 $uri/; }
    # 2. the door: opens the REGISTERED folder — a client-supplied ?folder= is discarded
    location ~ "^/v/[A-Za-z0-9]{30,64}/workspace/([a-z0-9][a-z0-9-]{0,39})/$" {
        set $vsc_ws $1;
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        limit_req zone=alphavscdoor burst=30 nodelay;
        limit_req_status 429;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        error_page 401 403 =404 @vsc404;
        # a bare visit is sent to the workspace's ALIAS path (VS Code reads ?folder= client-side, from the
        # browser's own URL, so it has to be visible there; /srv/vsc-ws/<id> is a neutral name, never the
        # real folder). The answer is the same for any token, so it tells a guesser nothing - the request
        # that follows carries a query string and goes through the gate. Requests with their own query
        # string (?folder=, the WebSocket's ?reconnectionToken=...) pass through untouched.
        if ($args = "") { return 302 $uri?folder=/srv/vsc-ws/$vsc_ws; }
        rewrite ^ / break;
        proxy_pass http://127.0.0.1:8321;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_cookie_flags ~ secure httponly samesite=lax;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
    }
    # 3. its login form — rate-limited exactly like the old /login
    location ~ "^/v/[A-Za-z0-9]{30,64}/workspace/[a-z0-9][a-z0-9-]{0,39}/login$" {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        limit_req zone=alphavscode burst=10 nodelay;
        limit_req_status 429;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        error_page 401 403 =404 @vsc404;
        rewrite ^/v/[A-Za-z0-9]+/workspace/[a-z0-9-]+/(login)$ /$1 break;
        proxy_pass http://127.0.0.1:8321;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_cookie_flags ~ secure httponly samesite=lax;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
    }
    # 4. everything else under it: assets, websockets, webviews, uploads — no rate limit here,
    #    an editor load is hundreds of requests and the websocket stays up for hours
    location ~ "^/v/[A-Za-z0-9]{30,64}/workspace/[a-z0-9][a-z0-9-]{0,39}/.+" {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        error_page 401 403 =404 @vsc404;
        rewrite ^/v/[A-Za-z0-9]+/workspace/[a-z0-9-]+/(.*)$ /$1 break;
        proxy_pass http://127.0.0.1:8321;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_cookie_flags ~ secure httponly samesite=lax;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
        client_max_body_size 512m;
    }
    location /v/ { return 404; }

    # ---- the bare host: guests with a valid invite cookie only (the gate injects their
    # session); an owner without the secret address gets 403 from the gate → 404 here
    location = /login {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        limit_req zone=alphavscode burst=10 nodelay;
        limit_req_status 429;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        error_page 403 =404 @vsc404;
        proxy_pass http://127.0.0.1:8321;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_cookie_flags ~ secure httponly samesite=lax;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
    }
    location / {
        include /etc/nginx/snippets/alpha-vscode-auth.conf;
        auth_request /__vscgate;
        auth_request_set $vsc_cookie $upstream_http_x_vsc_cookie;
        error_page 403 =404 @vsc404;
        proxy_pass http://127.0.0.1:8321;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Cookie $vsc_cookie;
        proxy_cookie_flags ~ secure httponly samesite=lax;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
        client_max_body_size 512m;
    }
}
VHEOF
  sed -i "s/__HOST__/$2/g" "$1"
}

cur_host() { local h; h=$(cat "$HOSTFILE" 2>/dev/null); echo "${h:-code.$DOMAIN}"; }

write_unit() { # $1 = workspace dir
  local BIN; BIN=$(command -v code-server || echo /usr/bin/code-server)
  ensure_bits
  if [ ! -f "$DATA/User/settings.json" ]; then
    cat > "$DATA/User/settings.json" << 'SET_EOF'
{
  "security.workspace.trust.enabled": false,
  "window.menuBarVisibility": "classic",
  "window.commandCenter": false,
  "terminal.integrated.defaultProfile.linux": "sandbox (root)",
  "terminal.integrated.profiles.linux": {
    "sandbox (root)": { "path": "sudo", "args": ["-n", "/usr/local/sbin/alpha-vscode-sandbox", "shell"] },
    "bash": { "path": "/bin/bash", "args": ["-l"] },
    "Claude (host)": { "path": "/bin/bash", "args": ["-l"], "cwd": "/workspaces", "icon": "sparkle" }
  },
  "claudeCode.preferredLocation": "panel",
  "claudeCode.claudeProcessWrapper": "/usr/local/bin/claude-sandbox",
  "claudeCode.disableLoginPrompt": true
}
SET_EOF
  fi
  chown -R www-data:www-data "$BASE"
  cat > /etc/systemd/system/$UNIT.service << UNIT_EOF
[Unit]
Description=code-server (Alpha Panel VS Code Web add-on)
After=network.target

[Service]
User=www-data
Group=www-data
Environment="HOME=$DATA"
Environment="SHELL=/bin/bash"
Environment="DISABLE_AUTOUPDATER=1"
ExecStart=$BIN --config $CONF --user-data-dir "$DATA" --extensions-dir "$EXT" "$1"
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
UNIT_EOF
  systemctl daemon-reload
}

reload_ok() { nginx -t >/dev/null 2>&1 && systemctl reload nginx; }

# Claude Code CLI for the (optional) Claude Code extension. The 2.1.x NATIVE
# build (Bun-compiled) deadlocks on this host — every worker thread times out on
# a futex and the process never produces output — so both its own CLI and the
# extension's bundled engine hang. We pin the last pure-JavaScript release, which
# runs on the system node, and turn auto-update OFF (DISABLE_AUTOUPDATER, also set
# on the service unit) so it can never pull the native build back in. The screen
# wires the extension to this via claudeProcessWrapper=/usr/bin/claude.
CLAUDE_CLI_VERSION=2.1.100
CLAUDE_CLI_PREFIX=/opt/alpha-claude
ensure_claude_cli() {
  command -v npm >/dev/null 2>&1 || return 0
  # pinned pure-JS build in a FIXED location, independent of npm-global
  local have; have=$(timeout 8 "$CLAUDE_CLI_PREFIX/bin/claude" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
  if [ "$have" != "$CLAUDE_CLI_VERSION" ]; then
    HOME=/root DISABLE_AUTOUPDATER=1 npm install -g --prefix "$CLAUDE_CLI_PREFIX" \
      "@anthropic-ai/claude-code@$CLAUDE_CLI_VERSION" >/tmp/alpha-claude-cli.log 2>&1 || true
  fi
  # PATH-shadowing wrapper (/usr/local/bin precedes /usr/bin): survives npm-global
  # updates AND extension updates, and never runs the native Bun build.
  cat > /usr/local/bin/claude << WRAP
#!/bin/bash
export DISABLE_AUTOUPDATER=1
# subscription token minted by the panel's guided sign-in (if present)
[ -r /var/www/alpha-private/claude-code-token ] && \\
  export CLAUDE_CODE_OAUTH_TOKEN=\$(cat /var/www/alpha-private/claude-code-token)
exec $CLAUDE_CLI_PREFIX/bin/claude "\$@"
WRAP
  chmod 755 /usr/local/bin/claude
  # Engine-in-sandbox launcher: the extension points claudeProcessWrapper here,
  # so the AI agent runs at root@sandbox (disposable shell, real /workspaces
  # edits via the shared mount). CLAUDE*/ANTHROPIC* env is forwarded as args —
  # sudo env_reset would strip real env vars.
  cat > /usr/local/bin/claude-sandbox << 'SWRAP'
#!/bin/bash
# NOTE: the extension's key-passing file descriptor cannot cross sudo+lxc, so
# CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR is dropped — the engine then falls back
# to the credentials file, which is bind-shared into the box at /root/.claude.
FWD=()
while IFS= read -r kv; do FWD+=(--fwd "$kv"); done < <(env | grep -E "^(CLAUDE|ANTHROPIC)[A-Za-z0-9_]*=" | grep -v "^CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR=")
# wrapper contract: the extension passes ITS claude binary path as $1 ("launch
# this"). We launch the container's claude instead, so drop that path argument.
case "${1:-}" in /*/claude|/*/claude.exe) shift ;; esac
exec sudo -n /usr/local/sbin/alpha-vscode-sandbox run ${FWD[@]+"${FWD[@]}"} -- "$@"
SWRAP
  chmod 755 /usr/local/bin/claude-sandbox
}

# Installer debs: keep ONLY the one rollback needs. Every update used to leave
# its .deb behind in debs/ forever (622 MB of three versions on 2026-10-02 while
# rollback can only ever use one). The rule after any update/rollback: keep the
# deb named in prev-version, delete every other code-server deb. The installed
# version's own deb is not kept — the next update re-downloads it as its
# rollback anchor if it is missing.
prune_debs() {
  local d=/var/lib/alpha-vscode/debs pf=/var/lib/alpha-vscode/prev-version keep="" f
  [ -d "$d" ] || return 0
  [ -f "$pf" ] && keep="code-server_$(tr -dc '0-9.' < "$pf")_amd64.deb"
  for f in "$d"/code-server_*.deb "$d"/*.part; do
    [ -e "$f" ] || continue
    [ -n "$keep" ] && [ "$(basename "$f")" = "$keep" ] && continue
    rm -f -- "$f"
  done
}

case "${1:-}" in
  install)
    export HOME=/root
    curl -fsSL https://code-server.dev/install.sh | sh -s -- > /tmp/alpha-vscode-install.log 2>&1 || { echo "install failed — see /tmp/alpha-vscode-install.log"; exit 1; }
    # install.sh keeps its downloaded .deb in ~/.cache/code-server (~200 MB) — the
    # package is installed now, the copy is dead weight
    rm -rf /root/.cache/code-server
    if [ ! -f "$CONF" ]; then
      PASS=$(openssl rand -hex 12)
      printf 'bind-addr: 127.0.0.1:%s\nauth: password\npassword: %s\ncert: false\n' "$PORT" "$PASS" > "$CONF"
    fi
    chown www-data:www-data "$CONF" && chmod 640 "$CONF"
    ensure_bits
    ensure_claude_cli
    write_unit /var/www
    systemctl enable --now $UNIT >/dev/null 2>&1
    echo ok
    ;;
  remove)
    systemctl disable --now $UNIT >/dev/null 2>&1
    rm -f /etc/systemd/system/$UNIT.service
    systemctl daemon-reload
    echo ok
    ;;
  start|stop|restart|enable|disable)
    systemctl "$1" $UNIT || exit 1; echo ok ;;
  setpass)
    [ -f "$CONF" ] || { echo "not configured"; exit 1; }
    PASS=$(openssl rand -hex 12)
    sed -i "s/^password: .*/password: $PASS/" "$CONF"
    systemctl try-restart $UNIT >/dev/null 2>&1
    echo "$PASS" ;;
  workspace)
    DIR="${2:-}"
    case "$DIR" in
      /workspaces|/var/www|/var/www/*|/home|/home/*) ;;
      *) echo "refused: workspaces live under /workspaces, /var/www or /home"; exit 1 ;;
    esac
    [ -d "$DIR" ] || { echo "no such directory"; exit 1; }
    write_unit "$DIR"; systemctl try-restart $UNIT >/dev/null 2>&1; echo ok ;;

  hostinfo) cur_host ;;

  vpath-init) ensure_bits; echo ok ;;

  hostlist)
    # existing $DOMAIN subdomains that already have a cert (reuse = instant,
    # no new cert). Flags: inuse (served by another site → can't reuse), current.
    CUR=$(cur_host)
    for d in /etc/letsencrypt/live/*/; do
      n=$(basename "$d"); [ "$n" = "README" ] && continue
      case "$n" in *.$DOMAIN) ;; *) continue ;; esac
      [ "$n" = "$ADMIN_HOST" ] && continue
      inuse=0
      for f in "$ENABLED"/*; do
        [ "$(basename "$f")" = "sub-code" ] && continue
        grep -qE "server_name[^;]*[[:space:]]$n([[:space:];])" "$f" 2>/dev/null && { inuse=1; break; }
      done
      cur=0; [ "$n" = "$CUR" ] && cur=1
      printf '%s|%s|%s\n' "$n" "$inuse" "$cur"
    done ;;

  host)
    ensure_bits
    H="${2:-}"
    [ -n "$H" ] || { echo "no-host"; exit 1; }
    case "$H" in *.*) ;; *) H="$H.$DOMAIN" ;; esac       # bare label → <label>.<domain>
    echo "$H" | grep -qE '^[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)+$' || { echo "bad-host"; exit 1; }
    [ "$H" = "$ADMIN_HOST" ] && { echo "refused-admin"; exit 1; }
    # never hijack a name another vhost already serves
    for f in "$ENABLED"/*; do
      [ "$(basename "$f")" = "sub-code" ] && continue
      grep -qE "server_name[^;]*[[:space:]]$H([[:space:];])" "$f" 2>/dev/null && { echo "in-use"; exit 1; }
    done
    # 1) make sure a cert exists (obtain via a throwaway acme-only vhost — NEVER
    #    let certbot rewrite our real vhost). Skip if the cert is already there.
    if [ ! -f "/etc/letsencrypt/live/$H/fullchain.pem" ]; then
      cat > "$AVAIL/sub-code-acme" << ACMEEOF
server { listen 80; listen [::]:80; server_name $H;
    location ^~ /.well-known/acme-challenge/ { root /var/www/html; allow all; }
    location / { return 404; } }
ACMEEOF
      ln -sfn "$AVAIL/sub-code-acme" "$ENABLED/sub-code-acme"
      if ! nginx -t >/dev/null 2>&1; then rm -f "$AVAIL/sub-code-acme" "$ENABLED/sub-code-acme"; echo "nginx-fail"; exit 1; fi
      systemctl reload nginx
      certbot certonly --webroot -w /var/www/html -d "$H" --non-interactive --agree-tos -m "$CERT_EMAIL" >/tmp/alpha-vscode-host.log 2>&1
      rm -f "$AVAIL/sub-code-acme" "$ENABLED/sub-code-acme"
      if [ ! -f "/etc/letsencrypt/live/$H/fullchain.pem" ]; then reload_ok; echo "cert-fail"; exit 1; fi
    fi
    # 2) write the complete TLS vhost ourselves + swing the enabled symlink
    cp -f "$AVAIL/sub-code" "$AVAIL/sub-code.bak" 2>/dev/null || true
    gen_vhost "$AVAIL/sub-code" "$H"
    ln -sfn "$AVAIL/sub-code" "$ENABLED/sub-code"
    if ! nginx -t >/dev/null 2>&1; then cp -f "$AVAIL/sub-code.bak" "$AVAIL/sub-code" 2>/dev/null; reload_ok; echo "nginx-fail"; exit 1; fi
    systemctl reload nginx
    echo "$H" > "$HOSTFILE"; chown www-data:www-data "$HOSTFILE"
    echo ok ;;

  vhostsync)
    # regenerate the vhost for the CURRENT host (used after gen_vhost changes,
    # e.g. adding the guest gate) — same nginx -t guard + rollback as `host`.
    ensure_bits
    H=$(cur_host)
    [ -f "/etc/letsencrypt/live/$H/fullchain.pem" ] || { echo "no-cert"; exit 1; }
    cp -f "$AVAIL/sub-code" "$AVAIL/sub-code.bak" 2>/dev/null || true
    gen_vhost "$AVAIL/sub-code" "$H"
    ln -sfn "$AVAIL/sub-code" "$ENABLED/sub-code"
    if ! nginx -t >/dev/null 2>&1; then cp -f "$AVAIL/sub-code.bak" "$AVAIL/sub-code" 2>/dev/null; reload_ok; echo "nginx-fail"; exit 1; fi
    systemctl reload nginx
    echo ok ;;

  kick)
    # forcibly close a client's open web connections (guest/session disconnect).
    # The invite is revoked separately — this just drops live websockets NOW.
    IP="${2:-}"
    echo "$IP" | grep -qE '^[0-9A-Fa-f:.]{3,45}$' || { echo "bad-ip"; exit 1; }
    ss -K dst "$IP" '( sport = :443 )' >/dev/null 2>&1
    ss -K dst "$IP" '( sport = :80 )'  >/dev/null 2>&1
    echo ok ;;

  # ---- per-project instances: own port, password, user-data, /i/<slug>/ URL ----
  inst-create)  # $2 = slug, $3 = workspace dir; prints "port|password"
    ensure_bits
    S="${2:-}"; DIR="${3:-}"
    echo "$S" | grep -qE '^[a-z0-9][a-z0-9-]{0,30}$' || { echo "bad-slug"; exit 1; }
    case "$DIR" in /workspaces|/workspaces/*|/var/www/*|/home/*) ;; *) echo "refused-dir"; exit 1 ;; esac
    [ -d "$DIR" ] || { echo "no-dir"; exit 1; }
    [ -f "$INSTDIR/$S.yaml" ] && { echo "exists"; exit 1; }
    P=""
    for c in $(seq 8331 8399); do
      grep -qs ":$c\$" "$INSTDIR"/*.yaml 2>/dev/null && continue
      ss -Htln "( sport = :$c )" 2>/dev/null | grep -q . && continue
      P=$c; break
    done
    [ -n "$P" ] || { echo "no-port"; exit 1; }
    PASS=$(openssl rand -hex 12)
    printf 'bind-addr: 127.0.0.1:%s\nauth: password\npassword: %s\ncert: false\n' "$P" "$PASS" > "$INSTDIR/$S.yaml"
    chown www-data:www-data "$INSTDIR/$S.yaml"; chmod 640 "$INSTDIR/$S.yaml"
    IB="$INSTBASE/$S"
    mkdir -p "$IB/UserData/User" "$IB/Extensions"
    [ -f "$IB/UserData/User/settings.json" ] || printf '{\n  "security.workspace.trust.enabled": false\n}\n' > "$IB/UserData/User/settings.json"
    chown -R www-data:www-data "$INSTBASE"
    BIN=$(command -v code-server || echo /usr/bin/code-server)
    cat > "/etc/systemd/system/$(inst_unit "$S").service" << UEOF
[Unit]
Description=code-server instance $S (VS Code Web add-on)
After=network.target

[Service]
User=www-data
Group=www-data
Environment="HOME=$IB/UserData"
Environment="SHELL=/bin/bash"
Environment="DISABLE_AUTOUPDATER=1"
ExecStart=$BIN --config $INSTDIR/$S.yaml --user-data-dir "$IB/UserData" --extensions-dir "$IB/Extensions" "$DIR"
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
UEOF
    systemctl daemon-reload
    regen_inst_snippet
    if ! nginx -t >/dev/null 2>&1; then
      rm -f "$INSTDIR/$S.yaml" "/etc/systemd/system/$(inst_unit "$S").service"
      systemctl daemon-reload; regen_inst_snippet; reload_ok
      echo "nginx-fail"; exit 1
    fi
    systemctl reload nginx
    systemctl enable --now "$(inst_unit "$S")" >/dev/null 2>&1
    echo "$P|$PASS" ;;

  inst-rm)
    S="${2:-}"
    echo "$S" | grep -qE '^[a-z0-9][a-z0-9-]{0,30}$' || { echo "bad-slug"; exit 1; }
    systemctl disable --now "$(inst_unit "$S")" >/dev/null 2>&1
    rm -f "/etc/systemd/system/$(inst_unit "$S").service"; systemctl daemon-reload
    rm -f "$INSTDIR/$S.yaml"
    rm -rf "${INSTBASE:?}/$S"
    regen_inst_snippet; reload_ok
    echo ok ;;

  inst-svc)
    S="${2:-}"; DO="${3:-}"
    echo "$S" | grep -qE '^[a-z0-9][a-z0-9-]{0,30}$' || { echo "bad-slug"; exit 1; }
    case "$DO" in
      start|stop|restart) systemctl "$DO" "$(inst_unit "$S")" || exit 1; echo ok ;;
      *) echo "bad-op"; exit 1 ;;
    esac ;;

  # ---- version manager: update to a release deb, keep the old one for rollback ----
  update)  # $2 = target version (e.g. 4.104.2)
    V="${2:-}"
    echo "$V" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "bad-version"; exit 1; }
    DEBDIR=/var/lib/alpha-vscode/debs; PREVFILE=/var/lib/alpha-vscode/prev-version
    mkdir -p "$DEBDIR"
    CUR=$(dpkg-query -W -f='${Version}' code-server 2>/dev/null | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')
    [ "$CUR" = "$V" ] && { echo "already"; exit 1; }
    # rollback anchor: make sure the CURRENT version's deb is on disk before replacing it
    if [ -n "$CUR" ] && [ ! -f "$DEBDIR/code-server_${CUR}_amd64.deb" ]; then
      curl -fsSL -o "$DEBDIR/code-server_${CUR}_amd64.deb" \
        "https://github.com/coder/code-server/releases/download/v${CUR}/code-server_${CUR}_amd64.deb" \
        || rm -f "$DEBDIR/code-server_${CUR}_amd64.deb"
    fi
    curl -fsSL -o "$DEBDIR/code-server_${V}_amd64.deb" \
      "https://github.com/coder/code-server/releases/download/v${V}/code-server_${V}_amd64.deb" \
      || { prune_debs; echo "download-fail"; exit 1; }
    dpkg -i "$DEBDIR/code-server_${V}_amd64.deb" >/dev/null 2>&1 || { prune_debs; echo "dpkg-fail"; exit 1; }
    [ -n "$CUR" ] && { echo "$CUR" > "$PREVFILE"; chmod 644 "$PREVFILE"; }
    prune_debs    # only the rollback deb (the version just replaced) stays
    ensure_bits   # upgrades reset product.json link trust — re-apply
    systemctl try-restart $UNIT >/dev/null 2>&1
    for y in "$INSTDIR"/*.yaml; do [ -f "$y" ] || continue; systemctl try-restart "$(inst_unit "$(basename "$y" .yaml)")" >/dev/null 2>&1; done
    echo ok ;;

  prune-debs)
    prune_debs; echo ok ;;

  rollback)
    DEBDIR=/var/lib/alpha-vscode/debs; PREVFILE=/var/lib/alpha-vscode/prev-version
    [ -f "$PREVFILE" ] || { echo "no-prev"; exit 1; }
    PV=$(cat "$PREVFILE")
    [ -f "$DEBDIR/code-server_${PV}_amd64.deb" ] || { echo "no-deb"; exit 1; }
    dpkg -i "$DEBDIR/code-server_${PV}_amd64.deb" >/dev/null 2>&1 || { echo "dpkg-fail"; exit 1; }
    rm -f "$PREVFILE"
    prune_debs    # no prev-version left → no deb is needed any more
    ensure_bits
    systemctl try-restart $UNIT >/dev/null 2>&1
    for y in "$INSTDIR"/*.yaml; do [ -f "$y" ] || continue; systemctl try-restart "$(inst_unit "$(basename "$y" .yaml)")" >/dev/null 2>&1; done
    echo ok ;;

  limits)  # $2 = main|<slug>, $3 = cpu% (0 = unlimited), $4 = mem MB (0 = unlimited)
    T="${2:-}"; CPU="${3:-0}"; MEM="${4:-0}"
    if [ "$T" = "main" ]; then U=$UNIT; else
      echo "$T" | grep -qE '^[a-z0-9][a-z0-9-]{0,30}$' || { echo "bad-target"; exit 1; }
      U=$(inst_unit "$T")
    fi
    echo "$CPU" | grep -qE '^[0-9]{1,3}$' || { echo "bad-cpu"; exit 1; }
    echo "$MEM" | grep -qE '^[0-9]{1,5}$' || { echo "bad-mem"; exit 1; }
    [ "$CPU" -le 400 ] || { echo "bad-cpu"; exit 1; }
    [ "$MEM" -le 16384 ] || { echo "bad-mem"; exit 1; }
    # floors: under these the editor is OOM-killed / times out during login
    # instead of merely running slower (128M did exactly that on 2026-07-25)
    if [ "$MEM" -gt 0 ] && [ "$MEM" -lt 512 ]; then MEM=512; fi
    if [ "$CPU" -gt 0 ] && [ "$CPU" -lt 25 ]; then CPU=25; fi
    # set-property applies LIVE (no restart) and persists as a drop-in
    if [ "$CPU" -gt 0 ]; then systemctl set-property "$U" "CPUQuota=${CPU}%" || { echo "set-fail"; exit 1; }
    else systemctl set-property "$U" "CPUQuota=" >/dev/null 2>&1 || true; fi
    if [ "$MEM" -gt 0 ]; then systemctl set-property "$U" "MemoryMax=${MEM}M" "MemorySwapMax=0" || { echo "set-fail"; exit 1; }
    else systemctl set-property "$U" "MemoryMax=infinity" "MemorySwapMax=infinity" >/dev/null 2>&1 || true; fi
    echo ok ;;

  # ---- pre-commit secrets scanning (gitleaks) on project repos ----
  gitleaks-install)
    command -v gitleaks >/dev/null 2>&1 && { echo "already"; exit 0; }
    V=8.28.0
    curl -fsSL -o /tmp/gitleaks.tgz "https://github.com/gitleaks/gitleaks/releases/download/v${V}/gitleaks_${V}_linux_x64.tar.gz" || { echo "download-fail"; exit 1; }
    tar xzf /tmp/gitleaks.tgz -C /tmp gitleaks 2>/dev/null || { echo "extract-fail"; exit 1; }
    install -m 755 /tmp/gitleaks /usr/local/bin/gitleaks
    rm -f /tmp/gitleaks.tgz /tmp/gitleaks
    command -v gitleaks >/dev/null 2>&1 && echo ok || { echo "install-fail"; exit 1; }
    ;;

  gitleaks-protect)  # $2 = abs project dir (must be under the Projects area)
    D="${2:-}"
    case "$D" in "/var/www/Addons/VS Code Web/Projects/"*) ;; *) echo "bad-dir"; exit 1 ;; esac
    case "$D" in *..*) echo "bad-dir"; exit 1 ;; esac
    [ -d "$D" ] || { echo "no-dir"; exit 1; }
    command -v gitleaks >/dev/null 2>&1 || { echo "no-gitleaks"; exit 1; }
    [ -d "$D/.git" ] || ( cd "$D" && git init -q && git config user.email "panel@$DOMAIN" && git config user.name "Alpha Panel" )
    mkdir -p "$D/.git/hooks"
    cat > "$D/.git/hooks/pre-commit" << 'HOOK'
#!/bin/sh
# Alpha Panel — block commits that contain secrets (gitleaks).
if command -v gitleaks >/dev/null 2>&1; then
  if ! gitleaks protect --staged --redact --no-banner 2>/dev/null; then
    echo ""
    echo "✘ Commit blocked: a secret was detected in the staged changes (Alpha Panel / gitleaks)."
    echo "  Remove the secret, or bypass once with:  git commit --no-verify"
    exit 1
  fi
fi
exit 0
HOOK
    chmod 755 "$D/.git/hooks/pre-commit"
    chown -R www-data:www-data "$D/.git"
    echo ok ;;

  gitleaks-unprotect)
    D="${2:-}"
    case "$D" in "/var/www/Addons/VS Code Web/Projects/"*) ;; *) echo "bad-dir"; exit 1 ;; esac
    rm -f "$D/.git/hooks/pre-commit"
    echo ok ;;

  # ---- read-only guest instance: its OWN code-server running as the
  # unprivileged user vscro — every escape vector (terminal, tasks, debug)
  # ends up as vscro, and plain unix permissions make the whole server
  # read-only for it. The gate injects its session for mode=ro invites. ----
  roguest-on)
    ensure_bits
    ROCONF=/etc/alpha-vscode-roguest.yaml
    ROBASE="$BASE/Instances/roguest"
    id vscro >/dev/null 2>&1 || useradd -r -M -d "$ROBASE/UserData" -s /usr/sbin/nologin vscro
    if [ ! -f "$ROCONF" ]; then
      PASS=$(openssl rand -hex 12)
      printf 'bind-addr: 127.0.0.1:8330\nauth: password\npassword: %s\ncert: false\n' "$PASS" > "$ROCONF"
    fi
    chown vscro:www-data "$ROCONF"; chmod 640 "$ROCONF"
    mkdir -p "$ROBASE/UserData/User" "$ROBASE/Extensions"
    # terminal politely disabled; settings are root-owned so the instance
    # (running as vscro) can never rewrite them
    cat > "$ROBASE/UserData/User/settings.json" << 'ROSET'
{
  "security.workspace.trust.enabled": false,
  "terminal.integrated.defaultProfile.linux": "read-only",
  "terminal.integrated.profiles.linux": {
    "read-only": { "path": "/bin/sh", "args": ["-c", "echo This is a read-only guest session - the terminal is disabled.; exec sleep infinity"] }
  }
}
ROSET
    chown -R vscro:vscro "$ROBASE"
    chown root:root "$ROBASE/UserData/User/settings.json"; chmod 644 "$ROBASE/UserData/User/settings.json"
    BIN=$(command -v code-server || echo /usr/bin/code-server)
    cat > /etc/systemd/system/code-server-alpha-roguest.service << ROUNIT
[Unit]
Description=code-server read-only guest instance (VS Code Web add-on)
After=network.target

[Service]
User=vscro
Group=vscro
Environment="HOME=$ROBASE/UserData"
Environment="SHELL=/bin/sh"
Environment="DISABLE_AUTOUPDATER=1"
ExecStart=$BIN --config $ROCONF --user-data-dir "$ROBASE/UserData" --extensions-dir "$ROBASE/Extensions" "$WORK"
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
ROUNIT
    systemctl daemon-reload
    systemctl enable --now code-server-alpha-roguest >/dev/null 2>&1
    regen_inst_snippet
    if ! nginx -t >/dev/null 2>&1; then rm -f "$ROCONF"; regen_inst_snippet; reload_ok; echo "nginx-fail"; exit 1; fi
    systemctl reload nginx
    echo ok ;;

  roguest-off)
    systemctl disable --now code-server-alpha-roguest >/dev/null 2>&1
    rm -f /etc/systemd/system/code-server-alpha-roguest.service /etc/alpha-vscode-roguest.yaml
    systemctl daemon-reload
    regen_inst_snippet; reload_ok
    echo ok ;;

  # ---- live preview subdomains: a dev server inside the sandbox, published
  # at <sub>.<domain> with its own cert + websocket (HMR) support ----
  preview-add)  # $2 = sub label, $3 = container ip, $4 = port
    ensure_bits
    S="${2:-}"; PIP="${3:-}"; PPORT="${4:-}"
    echo "$S" | grep -qE '^[a-z0-9][a-z0-9-]{0,40}$' || { echo "bad-host"; exit 1; }
    H="$S.$DOMAIN"
    [ "$H" = "$ADMIN_HOST" ] && { echo "refused-admin"; exit 1; }
    echo "$PIP" | grep -qE '^10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$|^172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}$|^192\.168\.[0-9]{1,3}\.[0-9]{1,3}$' || { echo "bad-ip"; exit 1; }
    echo "$PPORT" | grep -qE '^[0-9]{2,5}$' || { echo "bad-port"; exit 1; }
    [ "$PPORT" -le 65535 ] || { echo "bad-port"; exit 1; }
    for f in "$ENABLED"/*; do
      [ "$(basename "$f")" = "sub-code-preview-$S" ] && continue
      grep -qE "server_name[^;]*[[:space:]]$H([[:space:];])" "$f" 2>/dev/null && { echo "in-use"; exit 1; }
    done
    if [ ! -f "/etc/letsencrypt/live/$H/fullchain.pem" ]; then
      cat > "$AVAIL/sub-code-pv-acme" << ACMEEOF
server { listen 80; listen [::]:80; server_name $H;
    location ^~ /.well-known/acme-challenge/ { root /var/www/html; allow all; }
    location / { return 404; } }
ACMEEOF
      ln -sfn "$AVAIL/sub-code-pv-acme" "$ENABLED/sub-code-pv-acme"
      if ! nginx -t >/dev/null 2>&1; then rm -f "$AVAIL/sub-code-pv-acme" "$ENABLED/sub-code-pv-acme"; echo "nginx-fail"; exit 1; fi
      systemctl reload nginx
      certbot certonly --webroot -w /var/www/html -d "$H" --non-interactive --agree-tos -m "$CERT_EMAIL" >/tmp/alpha-vscode-preview-cert.log 2>&1
      rm -f "$AVAIL/sub-code-pv-acme" "$ENABLED/sub-code-pv-acme"
      if [ ! -f "/etc/letsencrypt/live/$H/fullchain.pem" ]; then reload_ok; echo "cert-fail"; exit 1; fi
    fi
    cat > "$AVAIL/sub-code-preview-$S" << PVEOF
server {
    listen 80;
    listen [::]:80;
    server_name $H;
    location ^~ /.well-known/acme-challenge/ { root /var/www/html; allow all; }
    location / { return 301 https://\$host\$request_uri; }
}
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name $H;
    ssl_certificate /etc/letsencrypt/live/$H/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$H/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # VS Code Web live preview — dev server inside the LXD sandbox.
    add_header X-Robots-Tag "noindex, nofollow" always;

    location / {
        proxy_pass http://$PIP:$PPORT;
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection \$http_upgrade;
        proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off;
    }
}
PVEOF
    ln -sfn "$AVAIL/sub-code-preview-$S" "$ENABLED/sub-code-preview-$S"
    if ! nginx -t >/dev/null 2>&1; then rm -f "$AVAIL/sub-code-preview-$S" "$ENABLED/sub-code-preview-$S"; reload_ok; echo "nginx-fail"; exit 1; fi
    systemctl reload nginx
    echo ok ;;

  preview-rm)
    S="${2:-}"
    echo "$S" | grep -qE '^[a-z0-9][a-z0-9-]{0,40}$' || { echo "bad-host"; exit 1; }
    rm -f "$AVAIL/sub-code-preview-$S" "$ENABLED/sub-code-preview-$S"
    reload_ok || { echo "reload-fail"; exit 1; }
    echo ok ;;

  seclock)
    ensure_bits
    if [ "${2:-}" = "on" ]; then
      command -v htpasswd >/dev/null 2>&1 || { apt-get install -y -q apache2-utils >/dev/null 2>&1; }
      P=$(openssl rand -hex 9)
      htpasswd -bc "$HTPASS" vscode "$P" >/dev/null 2>&1 || { echo "htpasswd-fail"; exit 1; }
      chmod 640 "$HTPASS"; chown root:www-data "$HTPASS"
      printf 'auth_basic "VS Code Web";\nauth_basic_user_file %s;\n' "$HTPASS" > "$AUTHSNIP"
      reload_ok || { echo "reload-fail"; exit 1; }
      echo "vscode:$P"
    else
      : > "$AUTHSNIP"; rm -f "$HTPASS"
      reload_ok || { echo "reload-fail"; exit 1; }
      echo off
    fi ;;

  # ---- Claude Code guided sign-in (panel-driven; see alpha-claude-login.py) ----
  claude-start)
    ensure_claude_cli
    python3 /usr/local/sbin/alpha-claude-login.py start >/dev/null 2>&1
    echo ok ;;
  claude-code)
    C="${2:-}"
    case "$C" in *[!A-Za-z0-9#_-]*|"") echo "bad-code"; exit 1 ;; esac
    python3 /usr/local/sbin/alpha-claude-login.py code "$C" ;;
  claude-cancel)
    python3 /usr/local/sbin/alpha-claude-login.py cancel ;;
  claude-logout)
    rm -f "$DATA/.claude/.credentials.json" "$DATA/.claude/.alpha-oauth-token" /var/www/alpha-private/claude-code-token
    # also end any Claude process the extension/terminal still has running, so a
    # held-in-memory token can't outlive the sign-out
    pkill -u www-data -f "alpha-claude/lib/node_modules/@anthropic-ai/claude-code" 2>/dev/null
    echo ok ;;
  claude-test)
    # one real prompt through the pinned CLI, as www-data (what the extension runs)
    setpriv --reuid=33 --regid=33 --init-groups \
      env HOME="$DATA" DISABLE_AUTOUPDATER=1 \
      timeout 90 /usr/local/bin/claude -p "${2:-Reply with one short sentence confirming Claude Code works in this editor.}" 2>&1 | tail -c 2000 ;;

  *)
    echo "usage: alpha-vscode install|remove|start|stop|restart|enable|disable|setpass|workspace <dir>|host <sub>|hostinfo|vhostsync|kick <ip>|inst-create <slug> <dir>|inst-rm <slug>|inst-svc <slug> <op>|seclock on|off|claude-start|claude-code <code>|claude-cancel|claude-logout|claude-test"
    exit 1 ;;
esac
