mango-cms 0.3.33 → 0.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/cli.js +57 -0
  2. package/default/infra/vibe/README.md +43 -0
  3. package/default/infra/vibe/cloudflare.ini.template +26 -0
  4. package/default/infra/vibe/ecosystem.vibe.config.cjs +44 -0
  5. package/default/infra/vibe/nginx-vibe-orchestrator.conf.template +50 -0
  6. package/default/infra/vibe/nginx-vibe-staging.conf.template +73 -0
  7. package/default/infra/vibe/vibe-gateway.service +38 -0
  8. package/default/infra/vibe/vibe-orchestrator.service +44 -0
  9. package/default/infra/vibe/vibe.env.template +24 -0
  10. package/default/mango/config/settings.json +35 -1
  11. package/default/vite.config.js +46 -0
  12. package/lib/devProxyGateway.js +173 -0
  13. package/lib/staging-gateway.js +23 -1
  14. package/lib/vibe-orchestrator/README.md +76 -0
  15. package/lib/vibe-orchestrator/scripts/fake-claude.mjs +35 -0
  16. package/lib/vibe-orchestrator/scripts/path-guard-hook.mjs +70 -0
  17. package/lib/vibe-orchestrator/scripts/vibe-recover.sh +63 -0
  18. package/lib/vibe-orchestrator/server.js +344 -0
  19. package/lib/vibe-orchestrator/src/attachments.js +98 -0
  20. package/lib/vibe-orchestrator/src/claudeRunner.js +233 -0
  21. package/lib/vibe-orchestrator/src/config.js +227 -0
  22. package/lib/vibe-orchestrator/src/costMirror.js +64 -0
  23. package/lib/vibe-orchestrator/src/costStore.js +209 -0
  24. package/lib/vibe-orchestrator/src/ownerToken.js +113 -0
  25. package/lib/vibe-orchestrator/src/pathGuard.js +114 -0
  26. package/lib/vibe-orchestrator/src/preamble.js +139 -0
  27. package/lib/vibe-orchestrator/src/publisher.js +376 -0
  28. package/lib/vibe-orchestrator/src/recovery.js +199 -0
  29. package/lib/vibe-orchestrator/src/screenshot.js +38 -0
  30. package/lib/vibe-orchestrator/src/sessionManager.js +291 -0
  31. package/lib/vibe-orchestrator/src/streamParser.js +188 -0
  32. package/lib/vibe-orchestrator/src/tokenMeter.js +64 -0
  33. package/package.json +1 -1
  34. package/readme.md +6 -0
package/cli.js CHANGED
@@ -1210,4 +1210,61 @@ To stop: mango proxy stop
1210
1210
  }
1211
1211
  });
1212
1212
 
1213
+ // --- Vibe Orchestrator Command (HAP-1253) ---
1214
+ // Runs the bundled vibe-orchestrator straight from the installed mango package —
1215
+ // no copied source. The orchestrator drives Claude Code headless in each branch
1216
+ // worktree, streams the chat to the ⌘K drawer, and verifies the owner token that
1217
+ // Mango mints (shared VIBE_ORCH_TOKEN_SECRET). It is dependency-free (node:http
1218
+ // only), so it ships inside lib/ and runs in-process from this CLI.
1219
+ //
1220
+ // Env contract:
1221
+ // VIBE_ORCH_TOKEN_SECRET (required for owner auth — UNSET ⇒ fails closed, 503)
1222
+ // VIBE_STAGING_ROOT root holding each <site> clone (default /root/Staging)
1223
+ // VIBE_ALLOWED_SITES comma-list of site slugs (defaults to this project's
1224
+ // siteName when run from a mango project root)
1225
+ // CLAUDE_CODE_OAUTH_TOKEN or ~/.claude/.credentials.json (Claude credentials)
1226
+ // VIBE_ORCH_PORT loopback listen port (default 7130; --port overrides)
1227
+ // See lib/vibe-orchestrator/README.md for the full contract.
1228
+ program
1229
+ .command('vibe-orchestrator')
1230
+ .description('Run the bundled Vibe orchestrator (Claude Code headless → ⌘K drawer). '
1231
+ + 'Env: VIBE_ORCH_TOKEN_SECRET (required), VIBE_STAGING_ROOT, VIBE_ALLOWED_SITES, '
1232
+ + 'CLAUDE_CODE_OAUTH_TOKEN (or ~/.claude). See lib/vibe-orchestrator/README.md.')
1233
+ .option('-p, --port <port>', 'loopback port to listen on (overrides VIBE_ORCH_PORT)')
1234
+ .action(async (options) => {
1235
+ const serverPath = path.join(__dirname, 'lib', 'vibe-orchestrator', 'server.js');
1236
+ if (!fs.existsSync(serverPath)) {
1237
+ console.error(`Error: bundled orchestrator not found at ${serverPath}`);
1238
+ process.exit(1);
1239
+ }
1240
+
1241
+ const env = { ...process.env };
1242
+ if (options.port) env.VIBE_ORCH_PORT = String(options.port);
1243
+
1244
+ // Convenience: when run from a mango project root and the site allow-list
1245
+ // isn't set explicitly, default it to this project's siteName so the
1246
+ // orchestrator only touches this site's clones. Explicit env always wins.
1247
+ if (!env.VIBE_ALLOWED_SITES) {
1248
+ const settingsPath = path.join(process.cwd(), 'mango/config/settings.json');
1249
+ try {
1250
+ if (fs.existsSync(settingsPath)) {
1251
+ const { siteName } = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1252
+ if (siteName) env.VIBE_ALLOWED_SITES = siteName;
1253
+ }
1254
+ } catch (e) {
1255
+ console.warn(`[vibe-orchestrator] could not read siteName from settings.json: ${e.message}`);
1256
+ }
1257
+ }
1258
+
1259
+ console.log(`[vibe-orchestrator] launching from ${serverPath}`);
1260
+ const child = spawn(process.execPath, [serverPath], { stdio: 'inherit', env });
1261
+ const forward = (sig) => { try { child.kill(sig); } catch { /* already gone */ } };
1262
+ process.on('SIGINT', () => forward('SIGINT'));
1263
+ process.on('SIGTERM', () => forward('SIGTERM'));
1264
+ child.on('exit', (code, signal) => {
1265
+ if (signal) process.kill(process.pid, signal);
1266
+ else process.exit(code ?? 0);
1267
+ });
1268
+ });
1269
+
1213
1270
  program.parse(process.argv);
@@ -0,0 +1,43 @@
1
+ # Vibe infra templates
2
+
3
+ Default, environment-agnostic templates for the **environment-specific layer** of
4
+ the Vibe drop-in (HAP-1255). The rest of Vibe is portable — you `npm`-upgrade
5
+ mango-cms for the server half, flip `settings.vibe.enabled`, and consume the
6
+ mango-ui Vibe module for the front. These templates cover the one part that can't
7
+ be packaged: the box's reverse proxy, process supervision, and DNS.
8
+
9
+ Fill in the `__PLACEHOLDERS__` and install. Nothing here is read automatically by
10
+ Mango — they're starting points you copy to `/etc/...` (or your pm2 setup).
11
+
12
+ | File | What it sets up |
13
+ | --- | --- |
14
+ | `nginx-vibe-staging.conf.template` | nginx vhost: the staging host **and** every `<branch>--<host>` (wildcard `server_name`) → the loopback staging gateway, with the Vite HMR WebSocket upgrade. |
15
+ | `nginx-vibe-orchestrator.conf.template` | nginx vhost: the orchestrator host → loopback `:7130`, tuned for SSE (no buffering, long read timeout). |
16
+ | `vibe-gateway.service` | systemd unit running `lib/staging-gateway.js` from the installed package. |
17
+ | `vibe-orchestrator.service` | systemd unit running `lib/vibe-orchestrator/server.js` from the installed package (secrets via an `EnvironmentFile`). |
18
+ | `ecosystem.vibe.config.cjs` | pm2 alternative that launches both processes. |
19
+ | `cloudflare.ini.template` | the scoped `Zone:DNS:Edit` token file for `settings.vibe.gatewayDns` (per-branch DNS automation). |
20
+ | `vibe.env.template` | the front-build `VITE_*` vars (`VITE_VIBE_SHELL`, `VITE_ORCH_URL`, …). |
21
+
22
+ ## Topology these templates assume
23
+
24
+ ```
25
+ Browser
26
+ │ page + HMR → https://<branch>--staging-vibe.example.com ─┐
27
+ │ ⌘K chat → https://vibe-orch.example.com ─────────────┐│
28
+ ▼ ││
29
+ nginx ──(staging vhost)──► staging gateway :7123 ────────────┘│ demux <branch>--host
30
+ │ │ │ ├─► per-branch dev server (portRange)
31
+ │ └──────────────────────────────┘ └─► shared front :7121 (no branch signal)
32
+ └────(orchestrator vhost)──► vibe-orchestrator :7130 ──► Claude Code headless in the worktree
33
+ verifies owner token (VIBE_ORCH_TOKEN_SECRET,
34
+ Mango backend :7122 ── mints owner token, owns shared with Mango)
35
+ /system/vibe/staging-resolve · /branches · /endpoints/vibe/token
36
+ ```
37
+
38
+ Default ports (all overridable): backend `7122`, shared front `7121`, gateway
39
+ `7123`, orchestrator `7130`, per-branch dev servers from `settings.vibe.portRange`
40
+ (default `[7200, 7399]`). Keep the gateway/orchestrator on loopback; nginx is the
41
+ only public listener.
42
+
43
+ **Full step-by-step:** see [`docs/VIBE_ACTIVATION.md`](../../../docs/VIBE_ACTIVATION.md).
@@ -0,0 +1,26 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Cloudflare credentials for settings.vibe.gatewayDns
3
+ #
4
+ # When settings.vibe.gatewayDns.enabled = true, Mango's per-branch DNS service
5
+ # creates/cleans a proxied DNS record for each `<branch>--<staging-host>` as
6
+ # branches are provisioned and reaped — so you do NOT have to manage a wildcard
7
+ # record by hand. Point settings.vibe.gatewayDns.credentialsFile at a copy of
8
+ # this file (chmod 600, OUTSIDE the repo).
9
+ #
10
+ # This file holds ONLY the token. The zone + control host come from
11
+ # settings.vibe.gatewayDns ({ zone, controlHost }), not from here.
12
+ #
13
+ # The service resolves the token as (first hit wins):
14
+ # 1. env CF_API_TOKEN
15
+ # 2. the `dns_cloudflare_api_token = …` line below (certbot-style)
16
+ # 3. a file whose entire contents are the bare token
17
+ #
18
+ # The token needs Zone:DNS:Edit on the single zone in settings.vibe.gatewayDns.zone.
19
+ # Create it: Cloudflare → My Profile → API Tokens → Create Token
20
+ # → "Edit zone DNS" template → scope to your zone.
21
+ #
22
+ # If the feature is enabled but no token resolves, per-branch DNS is logged and
23
+ # DISABLED (provisioning still succeeds) — it never crashes a dev box.
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ dns_cloudflare_api_token = __CF_ZONE_DNS_EDIT_TOKEN__
@@ -0,0 +1,44 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // pm2 ecosystem — Vibe gateway + orchestrator (alternative to the systemd units)
3
+ //
4
+ // pm2 start ecosystem.vibe.config.cjs && pm2 save
5
+ //
6
+ // Both processes run the bundled code from node_modules/mango-cms — no copied
7
+ // source. Replace the __PLACEHOLDERS__ (or export them in the shell first).
8
+ // Keep secrets OUT of source control: set VIBE_ORCH_TOKEN_SECRET and the Claude
9
+ // credential in the environment that launches pm2, not inline here.
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ const APP_DIR = process.env.VIBE_APP_DIR || '__APP_DIR__';
12
+
13
+ module.exports = {
14
+ apps: [
15
+ {
16
+ name: 'vibe-gateway',
17
+ script: `${APP_DIR}/node_modules/mango-cms/lib/staging-gateway.js`,
18
+ cwd: APP_DIR,
19
+ env: {
20
+ BACKEND_PORT: process.env.BACKEND_PORT || '__BACKEND_PORT__', // e.g. 7122
21
+ LISTEN_PORT: process.env.GATEWAY_PORT || '__GATEWAY_PORT__', // e.g. 7123
22
+ LISTEN_HOST: '127.0.0.1',
23
+ FALLBACK_PORT: process.env.FALLBACK_PORT || '__FALLBACK_PORT__', // e.g. 7121
24
+ STAGING_DOMAIN: process.env.STAGING_HOST || '__STAGING_HOST__',
25
+ READINESS_TIMEOUT_MS: '25000',
26
+ },
27
+ },
28
+ {
29
+ name: 'vibe-orchestrator',
30
+ script: `${APP_DIR}/node_modules/mango-cms/lib/vibe-orchestrator/server.js`,
31
+ cwd: APP_DIR,
32
+ env: {
33
+ VIBE_ORCH_PORT: process.env.VIBE_ORCH_PORT || '__ORCH_PORT__', // e.g. 7130
34
+ VIBE_STAGING_ROOT: process.env.VIBE_STAGING_ROOT || '__STAGING_ROOT__',
35
+ VIBE_ALLOWED_SITES: process.env.VIBE_ALLOWED_SITES || '__ALLOWED_SITES__',
36
+ VIBE_ALLOWED_ORIGINS: process.env.VIBE_ALLOWED_ORIGINS || '__ALLOWED_ORIGINS__',
37
+ // REQUIRED, set in the launching shell — unset ⇒ orchestrator 503s:
38
+ VIBE_ORCH_TOKEN_SECRET: process.env.VIBE_ORCH_TOKEN_SECRET,
39
+ // One of these for live runs (else /health reports canRunLive:false):
40
+ CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN,
41
+ },
42
+ },
43
+ ],
44
+ };
@@ -0,0 +1,50 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Vibe orchestrator — nginx vhost template
3
+ #
4
+ # Fronts the bundled vibe-orchestrator (`mango vibe-orchestrator`, loopback
5
+ # :7130). The browser's ⌘K drawer talks to this host (VITE_ORCH_URL). The
6
+ # orchestrator binds loopback only and verifies a server-minted owner token on
7
+ # every route except GET /health, so nginx just needs to forward and NOT buffer
8
+ # (the /prompt response is a long-lived SSE stream).
9
+ #
10
+ # Placeholders to replace:
11
+ # __ORCH_HOST__ public orchestrator host, e.g. vibe-orch.example.com
12
+ # __ORCH_PORT__ loopback port (VIBE_ORCH_PORT; default 7130)
13
+ # __TLS_CERT__ / __TLS_KEY__ cert covering __ORCH_HOST__
14
+ #
15
+ # CORS is handled by the orchestrator itself (VIBE_ALLOWED_ORIGINS) — do not add
16
+ # add_header Access-Control-* here or you will double up the headers.
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ server {
20
+ listen 443 ssl;
21
+ listen [::]:443 ssl;
22
+ server_name __ORCH_HOST__;
23
+
24
+ ssl_certificate __TLS_CERT__;
25
+ ssl_certificate_key __TLS_KEY__;
26
+
27
+ client_max_body_size 25m; # screenshot + attachment uploads ride on /prompt
28
+
29
+ location / {
30
+ proxy_pass http://127.0.0.1:__ORCH_PORT__;
31
+ proxy_http_version 1.1;
32
+ proxy_set_header Host $host;
33
+ proxy_set_header X-Real-IP $remote_addr;
34
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35
+ proxy_set_header X-Forwarded-Proto $scheme;
36
+
37
+ # SSE: stream tokens to the drawer as they arrive, never buffer.
38
+ proxy_buffering off;
39
+ proxy_cache off;
40
+ proxy_read_timeout 3600s; # a long agent turn can run for minutes
41
+ chunked_transfer_encoding on;
42
+ }
43
+ }
44
+
45
+ server {
46
+ listen 80;
47
+ listen [::]:80;
48
+ server_name __ORCH_HOST__;
49
+ return 301 https://$host$request_uri;
50
+ }
@@ -0,0 +1,73 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Vibe staging gateway — nginx vhost template
3
+ #
4
+ # Fronts the Mango Vibe staging surface. nginx terminates the public hostname(s)
5
+ # and proxy_passes to the loopback **staging gateway** process
6
+ # (`node node_modules/mango-cms/lib/staging-gateway.js`, default :7123), which
7
+ # demuxes by selected branch into each per-branch dev server, and falls back to
8
+ # the shared front for any request without a deliberate branch signal.
9
+ #
10
+ # This single server block matches BOTH:
11
+ # • the base staging host → staging-vibe.example.com
12
+ # • every per-branch hostname → <branch>--staging-vibe.example.com
13
+ # because the gateway parses `<branch>--<host>` itself. The wildcard server_name
14
+ # means you only manage ONE vhost no matter how many branches exist.
15
+ #
16
+ # Placeholders to replace:
17
+ # __STAGING_HOST__ base staging host, e.g. staging-vibe.example.com
18
+ # __GATEWAY_PORT__ loopback port the gateway binds (settings/LISTEN_PORT; default 7123)
19
+ # __TLS_CERT__ / __TLS_KEY__ wildcard cert covering *.__STAGING_HOST__ and __STAGING_HOST__
20
+ # (a Cloudflare Origin cert is simplest when the zone is proxied).
21
+ #
22
+ # DNS: point __STAGING_HOST__ at this box, and EITHER a wildcard
23
+ # `*.__STAGING_HOST__` record OR enable settings.vibe.gatewayDns so Mango creates
24
+ # each `<branch>--__STAGING_HOST__` record on demand (see cloudflare.ini.template).
25
+ # ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ map $http_upgrade $connection_upgrade {
28
+ default upgrade;
29
+ '' close;
30
+ }
31
+
32
+ server {
33
+ listen 443 ssl;
34
+ listen [::]:443 ssl;
35
+
36
+ # Base host AND every per-branch host. The leading dot matches the apex too.
37
+ server_name __STAGING_HOST__ *.__STAGING_HOST__;
38
+
39
+ ssl_certificate __TLS_CERT__;
40
+ ssl_certificate_key __TLS_KEY__;
41
+
42
+ # Vite dev assets are large and the gateway streams them; don't buffer.
43
+ proxy_buffering off;
44
+ client_max_body_size 25m;
45
+
46
+ location / {
47
+ proxy_pass http://127.0.0.1:__GATEWAY_PORT__;
48
+ proxy_http_version 1.1;
49
+
50
+ # Preserve the real Host so the gateway can read `<branch>--<host>`.
51
+ proxy_set_header Host $host;
52
+ proxy_set_header X-Real-IP $remote_addr;
53
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
54
+ proxy_set_header X-Forwarded-Proto $scheme;
55
+
56
+ # Vite HMR WebSocket upgrade — required or the preview never hot-reloads.
57
+ proxy_set_header Upgrade $http_upgrade;
58
+ proxy_set_header Connection $connection_upgrade;
59
+
60
+ # Cold per-branch boots can take a few seconds (the gateway shows a
61
+ # branded "starting" page via READINESS_TIMEOUT_MS); keep nginx patient.
62
+ proxy_read_timeout 120s;
63
+ proxy_send_timeout 120s;
64
+ }
65
+ }
66
+
67
+ # Optional: redirect plain HTTP to HTTPS.
68
+ server {
69
+ listen 80;
70
+ listen [::]:80;
71
+ server_name __STAGING_HOST__ *.__STAGING_HOST__;
72
+ return 301 https://$host$request_uri;
73
+ }
@@ -0,0 +1,38 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # systemd unit — Vibe staging gateway
3
+ #
4
+ # Runs the bundled gateway from the installed mango-cms package (no copied
5
+ # source). It fronts the per-branch dev servers and the shared front.
6
+ # Install:
7
+ # cp vibe-gateway.service /etc/systemd/system/
8
+ # # edit the placeholders below, then:
9
+ # systemctl daemon-reload && systemctl enable --now vibe-gateway
10
+ #
11
+ # Placeholders:
12
+ # __APP_DIR__ project root that contains node_modules/mango-cms
13
+ # __RUN_USER__ unix user to run as
14
+ # __BACKEND_PORT__ Mango port owning /system/vibe/staging-resolve (e.g. 7122)
15
+ # __GATEWAY_PORT__ loopback port to bind (must match the nginx vhost; e.g. 7123)
16
+ # __FALLBACK_PORT__ shared front port for un-signaled requests (e.g. 7121)
17
+ # __STAGING_HOST__ exact staging host, e.g. staging-vibe.example.com
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+ [Unit]
20
+ Description=Mango Vibe staging gateway
21
+ After=network.target
22
+
23
+ [Service]
24
+ Type=simple
25
+ User=__RUN_USER__
26
+ WorkingDirectory=__APP_DIR__
27
+ Environment=BACKEND_PORT=__BACKEND_PORT__
28
+ Environment=LISTEN_PORT=__GATEWAY_PORT__
29
+ Environment=LISTEN_HOST=127.0.0.1
30
+ Environment=FALLBACK_PORT=__FALLBACK_PORT__
31
+ Environment=STAGING_DOMAIN=__STAGING_HOST__
32
+ Environment=READINESS_TIMEOUT_MS=25000
33
+ ExecStart=/usr/bin/node __APP_DIR__/node_modules/mango-cms/lib/staging-gateway.js
34
+ Restart=always
35
+ RestartSec=2
36
+
37
+ [Install]
38
+ WantedBy=multi-user.target
@@ -0,0 +1,44 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # systemd unit — Vibe orchestrator
3
+ #
4
+ # Runs the bundled orchestrator from the installed mango-cms package (no copied
5
+ # source). Drives Claude Code headless and verifies the owner token Mango mints.
6
+ # Install:
7
+ # cp vibe-orchestrator.service /etc/systemd/system/
8
+ # # edit the placeholders below, then:
9
+ # systemctl daemon-reload && systemctl enable --now vibe-orchestrator
10
+ #
11
+ # Placeholders:
12
+ # __APP_DIR__ project root that contains node_modules/mango-cms
13
+ # __RUN_USER__ unix user (its $HOME/.claude holds Claude credentials)
14
+ # __ORCH_PORT__ loopback port (must match the nginx vhost; e.g. 7130)
15
+ # __STAGING_ROOT__ root holding each <site> clone the orchestrator edits
16
+ # __ALLOWED_SITES__ comma-list of site slugs (e.g. mysite)
17
+ # __ALLOWED_ORIGINS__ comma-list of browser origins, e.g.
18
+ # https://staging-vibe.example.com,https://example.com
19
+ #
20
+ # Secrets — set VIBE_ORCH_TOKEN_SECRET (MUST equal Mango's settings.vibe secret)
21
+ # and the Claude credential out of band (an EnvironmentFile is shown below so the
22
+ # secret never lives in this unit). Unset token secret ⇒ orchestrator fails
23
+ # closed (every gated route returns 503).
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ [Unit]
26
+ Description=Mango Vibe orchestrator (Claude Code headless)
27
+ After=network.target
28
+
29
+ [Service]
30
+ Type=simple
31
+ User=__RUN_USER__
32
+ WorkingDirectory=__APP_DIR__
33
+ Environment=VIBE_ORCH_PORT=__ORCH_PORT__
34
+ Environment=VIBE_STAGING_ROOT=__STAGING_ROOT__
35
+ Environment=VIBE_ALLOWED_SITES=__ALLOWED_SITES__
36
+ Environment=VIBE_ALLOWED_ORIGINS=__ALLOWED_ORIGINS__
37
+ # Secrets live here, mode 600, root-owned (VIBE_ORCH_TOKEN_SECRET=..., CLAUDE_CODE_OAUTH_TOKEN=...):
38
+ EnvironmentFile=/etc/vibe/orchestrator.env
39
+ ExecStart=/usr/bin/node __APP_DIR__/node_modules/mango-cms/lib/vibe-orchestrator/server.js
40
+ Restart=always
41
+ RestartSec=2
42
+
43
+ [Install]
44
+ WantedBy=multi-user.target
@@ -0,0 +1,24 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Vibe front-build environment (Vite)
3
+ #
4
+ # These VITE_* vars are read at BUILD time of the project's front (the mango-ui
5
+ # chrome that renders <VibeLauncher/>). Set them in the front's build env so the
6
+ # themed Vibe button appears and the drawer points at your orchestrator.
7
+ # (See default/infra/vibe/vibe-orchestrator.service for the SERVER-side secrets.)
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+
10
+ # Master switch for the Vibe shell + button. Without this the launcher renders
11
+ # nothing, so a plain Mango build is unaffected. (helpers/vibe/orchestrator.js →
12
+ # vibeShellEnabled())
13
+ VITE_VIBE_SHELL=true
14
+
15
+ # Public URL of the orchestrator vhost (nginx-vibe-orchestrator.conf.template).
16
+ VITE_ORCH_URL=https://vibe-orch.example.com
17
+
18
+ # Site slug the drawer edits (defaults to settings.siteName when unset).
19
+ VITE_ORCH_SITE=mysite
20
+
21
+ # OPTIONAL — encode the branch in the hostname instead of a cookie so it stays
22
+ # sticky across the iframe's sub-resources + HMR WebSocket. Must match the
23
+ # wildcard the staging gateway/nginx serve. `{branch}` is substituted live.
24
+ VITE_VIBE_SHELL_HOST_TEMPLATE={branch}--staging-vibe.example.com
@@ -34,5 +34,39 @@
34
34
  "algoliaSearchKey": null,
35
35
  "algoliaIndex": null,
36
36
 
37
- "corsOrigins": "*"
37
+ "corsOrigins": "*",
38
+
39
+ "_vibeDoc": "Optional in-page Vibe coding feature (HAP-1096). DISABLED by default (vibe.enabled = false): with this block inert the site behaves exactly as a normal Mango project. To turn it on, set vibe.enabled = true and configure workspaceRoot + (for the staging gateway) gatewayDns. Every key is documented inline under vibe._doc. settings.json is strict JSON so guidance lives in these _doc fields rather than comments. The block is validated at boot by validateVibeConfig — an invalid value throws with a clear message. See default/vite.config.js for the matching front-end resilient config loader a Vibe project needs.",
40
+
41
+ "vibe": {
42
+ "_doc": {
43
+ "enabled": "Master switch. false = feature inert (site behaves as a normal Mango project). Set true to enable per-branch Vibe workspaces.",
44
+ "portRange": "[min, max] inclusive pool of TCP ports the provisioner allocates to per-branch dev servers. Must not overlap reserved ports 6121, 6122, 7121, 7122, 7130.",
45
+ "workspaceRoot": "Absolute, writable directory where per-branch git worktrees and their dev servers are created.",
46
+ "idleTimeoutMs": "Milliseconds of inactivity before the idle reaper tears a workspace down. Default 1800000 (30 min).",
47
+ "reaperIntervalMs": "How often (ms) the idle reaper sweeps for workspaces past idleTimeoutMs. Default 60000 (1 min).",
48
+ "allowedRoles": "User roles permitted to open the Vibe panel and spawn workspaces. Non-empty array of role-name strings.",
49
+ "defaultBranch": "Branch a request resolves to when none travels with it (e.g. a cold load before a branch is picked).",
50
+ "devServerCommand": "Command the provisioner spawns inside each worktree to start its front dev server. cwd = the worktree; the allocated port is passed in PORT/MANGO_PORT.",
51
+ "sessionCookieDomain": "Optional shared parent domain (e.g. \".example.com\") so the session cookie rides across the gateway and api subdomains. null = host-scoped (default).",
52
+ "sessionCookieName": "Optional override for the express-session cookie name. Pair with sessionCookieDomain to stop staging/prod under one parent from colliding. null = default name.",
53
+ "gatewayDns": "Optional per-branch Cloudflare DNS lifecycle for the staging gateway: { enabled, zone, controlHost, credentialsFile }. enabled = false means no DNS automation (default)."
54
+ },
55
+ "enabled": false,
56
+ "portRange": [7200, 7399],
57
+ "workspaceRoot": "/Staging",
58
+ "idleTimeoutMs": 1800000,
59
+ "reaperIntervalMs": 60000,
60
+ "allowedRoles": ["admin", "owner"],
61
+ "defaultBranch": "main",
62
+ "devServerCommand": "mango dev",
63
+ "sessionCookieDomain": null,
64
+ "sessionCookieName": null,
65
+ "gatewayDns": {
66
+ "enabled": false,
67
+ "zone": "example.com",
68
+ "controlHost": "vibe.example.com",
69
+ "credentialsFile": "/path/to/.cloudflare.ini"
70
+ }
71
+ }
38
72
  }
@@ -25,9 +25,50 @@ const settingsPath = path.resolve(configPath, 'config/settings.json')
25
25
 
26
26
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
27
27
 
28
+ /*
29
+ * Race-safe loader for the regenerated config (Vibe / HAP-1096, HAP-1203).
30
+ *
31
+ * `@collections` / `@endpoints` resolve to `mango/config/.collections.json` and
32
+ * `.endpoints.json`, which mango REWRITES on every rebuild. For a brief window
33
+ * during that rewrite the file is empty/truncated. The front imports them
34
+ * (`src/helpers/mango.js`) and the watch plugin below full-reloads on change —
35
+ * so a reload that lands mid-rewrite makes Vite's `vite:json` throw
36
+ * "Failed to parse JSON file ... position -1", breaking the module graph and
37
+ * blanking the app. This `enforce: 'pre'` loader intercepts those two ids before
38
+ * `vite:json`, caches the last good parse, and serves it whenever the file is
39
+ * transiently unreadable, so a mid-write read can never break the page. Once the
40
+ * file settles, the watch -> full-reload re-runs this loader and the fresh
41
+ * content is picked up. It returns RAW JSON text (not `export default …`) so
42
+ * vite's built-in `vite:json` transform still converts it to ESM normally —
43
+ * returning JS here causes a 500 (HAP-1203). Dev/serve only; prod serves a
44
+ * static `vite build` and is immune.
45
+ */
46
+ function resilientConfigJson(targets) {
47
+ const lastGood = new Map() // absolute path -> last successfully parsed value
48
+ const fallback = (p) => (p === collectionsPath ? [] : {})
49
+ return {
50
+ name: 'vibe-resilient-config-json',
51
+ enforce: 'pre',
52
+ load(id) {
53
+ const clean = id.split('?')[0]
54
+ if (!targets.includes(clean)) return null
55
+ let value
56
+ try {
57
+ value = JSON.parse(fs.readFileSync(clean, 'utf8'))
58
+ lastGood.set(clean, value)
59
+ } catch (e) {
60
+ value = lastGood.has(clean) ? lastGood.get(clean) : fallback(clean)
61
+ this.warn(`[vibe] ${path.basename(clean)} mid-rewrite/unparseable; served ${lastGood.has(clean) ? 'last-good' : 'empty'} config (${e.message})`)
62
+ }
63
+ return JSON.stringify(value)
64
+ },
65
+ }
66
+ }
67
+
28
68
  // https://vitejs.dev/config/
29
69
  export default defineConfig({
30
70
  plugins: [
71
+ resilientConfigJson([collectionsPath, endpointsPath]),
31
72
  vue(),
32
73
  // VitePWA({
33
74
  // registerType: 'autoUpdate',
@@ -50,6 +91,11 @@ export default defineConfig({
50
91
  server.watcher.add(endpointsPath)
51
92
  server.watcher.on('change', (file) => {
52
93
  if (file === collectionsPath || file === endpointsPath) {
94
+ // Only reload once the rewrite has SETTLED (file parses). The
95
+ // 'change' event also fires for the transient empty/truncated
96
+ // state mid-rewrite; reloading then would just bounce off the
97
+ // resilient loader's last-good and reload again. Skip those.
98
+ try { JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return }
53
99
  server.ws.send({
54
100
  type: 'full-reload',
55
101
  })