SPB Git

Repository Public

make-manifest.py 8.8 KB 178 lines python

1#!/usr/bin/env python32"""Generate the mld manifest for Claude Remote (or its second instance, Codex Remote) ON THE GATEWAY (M1M32).34Usage (on M1M32):5  python3 make-manifest.py [--node M5M36] [--port 8620] > ~/dispatch/apps/claude-remote.json6  python3 make-manifest.py --agent codex [--node M5M36] [--port 8630] > ~/dispatch/apps/codex-remote.json7Secrets are generated here (or reused from an existing manifest so redeploys keep them) and are never printed8anywhere else. The agentd token is read from the gateway's copy of the agentd config if available via ssh.910The two instances share the code base and the workspace root, nothing else: each has its own application11directory, data directory, port, domain, database role/database, tmux socket, PM2 processes and backups.12"""13import json, os, secrets, subprocess, sys, argparse1415# Everything that differs between the two instances. All names derive from "app" (database names: dashes16# become underscores). "env" is the agent-specific part of the runtime/server environment, "backup_env" what17# is added to the backup process environment; the Claude entries are the historical ones and must not change.18AGENTS = {19    "claude": {20        "app": "claude-remote", "title": "Claude Remote", "cli": "Claude Code CLI", "auth": "Claude Code",21        "domain": "claude.spboucher.ai", "port": 8620, "backup_cron": "35 3 * * *",22        "env": {"CR_CLAUDE_BIN": "/opt/homebrew/bin/claude"},23        "env_hooks": {"CR_INSTALL_CLAUDE_HOOKS": "1"},24        "backup_env": {},25        "dns_note": "DNS GoDaddy A → 51.161.112.61 posé par le propriétaire (2026-11-09)",26    },27    "codex": {28        "app": "codex-remote", "title": "Codex Remote", "cli": "OpenAI Codex CLI", "auth": "Codex CLI",29        "domain": "codex.spboucher.ai", "port": 8630, "backup_cron": "50 3 * * *",30        "env": {"CR_AGENT": "codex", "CR_AGENT_BIN": "/opt/homebrew/bin/codex"},31        "env_hooks": {"CR_INSTALL_AGENT_HOOKS": "1", "CR_TMUX_SOCKET": "codex-remote"},32        "backup_env": {"CR_APP_NAME": "codex-remote"},33        "dns_note": "DNS GoDaddy A → 51.161.112.61 à poser par le propriétaire avant le premier déploiement",34    },35}3637ap = argparse.ArgumentParser(description="Generate the mld manifest of Claude Remote / Codex Remote (JSON on stdout).")38ap.add_argument("--agent", choices=sorted(AGENTS), default="claude",39                help="which instance: claude → claude-remote (default), codex → codex-remote")40ap.add_argument("--node", default="M5M36")41ap.add_argument("--port", type=int, default=None, help="default: 8620 (claude), 8630 (codex)")42ap.add_argument("--domain", default=None, help="default: claude.spboucher.ai (claude), codex.spboucher.ai (codex)")43ap.add_argument("--agentd-node", default="M4M64a")44ap.add_argument("--agentd-url", default="")45ap.add_argument("--existing", default=None,46                help="manifest whose secrets are reused; default: ~/dispatch/apps/<app>.json")47a = ap.parse_args()48A = AGENTS[a.agent]49NAME = A["app"]                    # claude-remote | codex-remote50DB = NAME.replace("-", "_")        # role and database: claude_remote | codex_remote51if a.port is None:52    a.port = A["port"]53if a.domain is None:54    a.domain = A["domain"]55if a.existing is None:56    a.existing = os.path.expanduser(f"~/dispatch/apps/{NAME}.json")5758old_env = {}59if os.path.exists(a.existing):60    try:61        old_env = json.load(open(a.existing))["processes"][0]["env"]62    except Exception:63        old_env = {}6465def keep(name, gen):66    return old_env.get(name) or gen()6768HOME = "/Users/simon-pierreboucher"69APP = f"{HOME}/apps/{NAME}"70DATA = f"{HOME}/{NAME}-data"71db_pass = None72if old_env.get("DATABASE_URL"):73    database_url = old_env["DATABASE_URL"]74else:75    db_pass = secrets.token_hex(24)76    database_url = f"postgres://{DB}:{db_pass}@127.0.0.1:5432/{DB}"7778agentd_token = old_env.get("CR_AGENTD_TOKEN", "")79if not agentd_token:80    try:81        agentd_token = subprocess.run(82            ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", a.agentd_node,83             "python3 -c 'import json,os;print(json.load(open(os.path.expanduser(\"~/maclustr-agentd/config.json\")))[\"token\"])'"],84            capture_output=True, text=True, timeout=20).stdout.strip()85    except Exception:86        agentd_token = ""8788env = {89    "NODE_ENV": "production",90    "CR_PORT": str(a.port),91    "CR_HOST": "0.0.0.0",92    "CR_PUBLIC_ORIGIN": f"https://{a.domain}",93    "CR_DATA_DIR": DATA,94    "CR_WORKSPACE_ROOTS": f"workspace={HOME}/workspace",95    "CR_RUNTIME_SOCKET": f"{DATA}/run/runtime.sock",96    "CR_RUNTIME_TOKEN": keep("CR_RUNTIME_TOKEN", lambda: secrets.token_hex(32)),97    "CR_SECRET_KEY": keep("CR_SECRET_KEY", lambda: secrets.token_hex(32)),98    "DATABASE_URL": database_url,99    "CR_AGENTD_URL": a.agentd_url or old_env.get("CR_AGENTD_URL", ""),100    "CR_AGENTD_TOKEN": agentd_token,101    **A["env"],102    "CR_MAX_SESSIONS": "12",103    "CR_EXEC_HOSTS": old_env.get("CR_EXEC_HOSTS", ""),104    "CR_TRUSTED_PROXIES": "127.0.0.1,::1,10.67.0.1",105    "CR_NODE_ALIAS": a.node,106    "CR_SKILLS_REGISTRY_URL": "https://skills.spboucher.ai",107    **A["env_hooks"],108    "CR_WEB_DIR": f"{APP}/apps/web/out",109    "SHELL": "/bin/zsh",110    "HOME": HOME,111    "LANG": "en_CA.UTF-8",112    "PATH": "/opt/homebrew/bin:/opt/homebrew/opt/postgresql@17/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",113}114115def proc(name, script_args, **kw):116    p = {"name": name, "manager": "pm2", "script": "/opt/homebrew/bin/node", "args": script_args,117         "interpreter": None, "cwd": f"{{{{HOME}}}}/apps/{NAME}", "env": env,118         "cron_restart": None, "autorestart": True, "max_memory_restart": "1500M"}119    p.update(kw)120    return p121122envfile = "\n".join(f"{k}={v}" for k, v in env.items())123db_setup = ""124if db_pass:125    db_setup = (126        f"psql -d postgres -tAc \"select 1 from pg_roles where rolname='{DB}'\" | grep -q 1 || "127        f"psql -d postgres -c \"create role {DB} login password '{db_pass}'\" >/dev/null; "128        f"psql -d postgres -tAc \"select 1 from pg_database where datname='{DB}'\" | grep -q 1 || "129        f"createdb -O {DB} {DB}; "130        f"psql -d {DB} -c 'create extension if not exists pgcrypto' >/dev/null; "131    )132post_sync = (133    "export PATH=/opt/homebrew/bin:/opt/homebrew/opt/postgresql@17/bin:/usr/bin:/bin:/usr/sbin:/sbin; umask 077; "134    f"mkdir -p {DATA}/run {HOME}/workspace {HOME}/backups/{NAME}; chmod 700 {DATA} {DATA}/run; "135    + db_setup +136    f"cat > .env <<'ENVEOF'\n{envfile}\nENVEOF\nchmod 600 .env; "137    "(pnpm install --frozen-lockfile 2>&1 | tail -2) && pnpm -r --workspace-concurrency=1 build 2>&1 | tail -6 && "138    "test -f apps/server/dist/main.js && test -f apps/runtime/dist/main.js && test -f apps/web/out/index.html && "139    "set -a && . ./.env && set +a && node apps/server/dist/migrate.js 2>&1 | tail -2"140)141142manifest = {143    "app": NAME,144    "label": f"{A['title']} — MacLustr Command Center ({A['domain']})",145    "domain": a.domain,146    "port": a.port,147    "health_path": "/api/v1/health",148    "dir": f"~/apps/{NAME}",149    "extra_paths": [],150    "sync_excludes": [".DS_Store", "node_modules/", ".next/", "out/", "dist/", "data/", ".env", ".env.local",151                      "test-results/", "playwright-report/", "*.tsbuildinfo", "e2e/screenshots/"],152    "requires": {"runtimes": ["node", "pnpm", "pm2", "postgresql@17"], "ram_gb": 4, "ports": [a.port]},153    "ram_mb_observed": 600,154    "size_mb": 30,155    "placement": {"pin": a.node, "prefer": None, "avoid": ["M1M32"],156                  "reason": f"Hôte dédié {A['title']} : {A['auth']} authentifié + tmux + PostgreSQL locaux, sessions persistantes (ne pas déplacer sans ré-authentifier {A['auth']})"},157    "processes": [158        proc(f"{NAME}-runtime", ["apps/runtime/dist/main.js"], kill_timeout=8000),159        proc(f"{NAME}-server", ["apps/server/dist/main.js"]),160        {"name": f"{NAME}-backup", "manager": "pm2", "script": "/bin/zsh", "args": ["deploy/backup.sh"],161         "interpreter": None, "cwd": f"{{{{HOME}}}}/apps/{NAME}",162         "env": {"DATABASE_URL": database_url, "CR_DATA_DIR": DATA, "HOME": HOME,163                 "PATH": env["PATH"], **A["backup_env"]},164         "cron_restart": A["backup_cron"], "autorestart": False, "max_memory_restart": None},165    ],166    "ngrok": None,167    "launchd": [],168    "env_overrides": {},169    "hooks": {"post_sync": [post_sync], "post_start": []},170    "ka_repo": False,171    "tunnel": {"domain": a.domain, "gateway": "BHS64", "note": A["dns_note"]},172    "notes": f"{A['title']} : vrai {A['cli']} dans tmux (socket -L {NAME}), runtime (socket unix) + serveur Fastify :{A['port']}, "173             f"PostgreSQL 17 local (rôle/base {DB}). Les sessions tmux survivent aux redémarrages PM2, pas à un reboot "174             f"(elles deviennent RECOVERABLE). Jeton d'installation à usage unique : ~/{NAME}-data/setup-token. "175             f"Sauvegarde chiffrée nocturne deploy/backup.sh → ~/backups/{NAME} (phrase de passe ~/.config/{NAME}/backup.pass).",176}177json.dump(manifest, sys.stdout, indent=1, ensure_ascii=False)178
Files main
Menu

Theme