mango-cms 0.3.34 → 0.3.36

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 (33) hide show
  1. package/cli.js +113 -23
  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 +40 -1
  11. package/default/package.json +1 -1
  12. package/default/vite.config.js +46 -0
  13. package/lib/vibe-orchestrator/README.md +76 -0
  14. package/lib/vibe-orchestrator/scripts/fake-claude.mjs +35 -0
  15. package/lib/vibe-orchestrator/scripts/path-guard-hook.mjs +70 -0
  16. package/lib/vibe-orchestrator/scripts/vibe-recover.sh +63 -0
  17. package/lib/vibe-orchestrator/server.js +344 -0
  18. package/lib/vibe-orchestrator/src/attachments.js +98 -0
  19. package/lib/vibe-orchestrator/src/claudeRunner.js +233 -0
  20. package/lib/vibe-orchestrator/src/config.js +227 -0
  21. package/lib/vibe-orchestrator/src/costMirror.js +64 -0
  22. package/lib/vibe-orchestrator/src/costStore.js +209 -0
  23. package/lib/vibe-orchestrator/src/ownerToken.js +113 -0
  24. package/lib/vibe-orchestrator/src/pathGuard.js +114 -0
  25. package/lib/vibe-orchestrator/src/preamble.js +139 -0
  26. package/lib/vibe-orchestrator/src/publisher.js +376 -0
  27. package/lib/vibe-orchestrator/src/recovery.js +199 -0
  28. package/lib/vibe-orchestrator/src/screenshot.js +38 -0
  29. package/lib/vibe-orchestrator/src/sessionManager.js +291 -0
  30. package/lib/vibe-orchestrator/src/streamParser.js +188 -0
  31. package/lib/vibe-orchestrator/src/tokenMeter.js +64 -0
  32. package/package.json +1 -1
  33. package/readme.md +6 -0
package/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
- import { execSync, spawn } from 'child_process';
4
+ import { execSync, spawn, spawnSync } from 'child_process';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import inquirer from 'inquirer';
@@ -14,6 +14,22 @@ import os from 'os';
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
16
16
 
17
+ // When `mango` is invoked from a global install (npm i -g mango-cms) inside a
18
+ // project that has its own mango-cms in node_modules, defer to the project-local
19
+ // CLI so the version pinned in package.json always wins. This is what lets users
20
+ // type plain `mango dev` / `mango deploy` instead of prefixing with `yarn`/`npx`.
21
+ const localCli = path.join(process.cwd(), 'node_modules', 'mango-cms', 'cli.js');
22
+ if (fs.existsSync(localCli)) {
23
+ let isSelf = false;
24
+ try {
25
+ isSelf = fs.realpathSync(localCli) === fs.realpathSync(__filename);
26
+ } catch (e) { /* fall through and run this copy */ }
27
+ if (!isSelf) {
28
+ const result = spawnSync(process.execPath, [localCli, ...process.argv.slice(2)], { stdio: 'inherit' });
29
+ process.exit(result.status ?? 0);
30
+ }
31
+ }
32
+
17
33
  const program = new Command();
18
34
 
19
35
  // Helper function to download and extract versioned src zip file
@@ -522,15 +538,16 @@ async function detectWebServer() {
522
538
  }
523
539
 
524
540
  // Helper function to generate nginx config
525
- function generateNginxConfig(settings, buildPath) {
541
+ function generateNginxConfig(settings, buildPath, deployUi = false) {
526
542
  const domain = settings.mangoDomain;
527
543
  const port = settings.port || 3002;
528
544
  const frontPort = settings.frontPort || 3001;
529
545
  const uiDomain = settings.uiDomain;
530
546
  const uiPort = settings.uiPort || 3001;
531
547
 
532
- // Reverse-proxy the Mango UI (Nuxt SSR server) on its own subdomain, when configured.
533
- const uiServerBlock = uiDomain ? `
548
+ // Reverse-proxy the Mango UI (Nuxt SSR server) on its own subdomain,
549
+ // only when the UI is part of this deploy and a uiDomain is configured.
550
+ const uiServerBlock = (deployUi && uiDomain) ? `
534
551
  server {
535
552
  server_name ${uiDomain};
536
553
 
@@ -617,15 +634,16 @@ ${uiServerBlock}`;
617
634
  }
618
635
 
619
636
  // Helper function to generate apache config
620
- function generateApacheConfig(settings, buildPath) {
637
+ function generateApacheConfig(settings, buildPath, deployUi = false) {
621
638
  const domain = settings.mangoDomain;
622
639
  const port = settings.port || 3002;
623
640
  const frontPort = settings.frontPort || 3001;
624
641
  const uiDomain = settings.uiDomain;
625
642
  const uiPort = settings.uiPort || 3001;
626
643
 
627
- // Reverse-proxy the Mango UI (Nuxt SSR server) on its own subdomain, when configured.
628
- const uiVirtualHost = uiDomain ? `
644
+ // Reverse-proxy the Mango UI (Nuxt SSR server) on its own subdomain,
645
+ // only when the UI is part of this deploy and a uiDomain is configured.
646
+ const uiVirtualHost = (deployUi && uiDomain) ? `
629
647
  <VirtualHost *:80>
630
648
  ServerName ${uiDomain}
631
649
 
@@ -697,7 +715,7 @@ ${uiVirtualHost}`;
697
715
  }
698
716
 
699
717
  // Helper function to configure web server
700
- async function configureWebServer(settings, buildPath) {
718
+ async function configureWebServer(settings, buildPath, deployUi = false) {
701
719
  let webServer = await detectWebServer();
702
720
 
703
721
  if (!webServer) {
@@ -713,8 +731,8 @@ async function configureWebServer(settings, buildPath) {
713
731
  }
714
732
 
715
733
  const config = webServer === 'nginx' ?
716
- generateNginxConfig(settings, buildPath) :
717
- generateApacheConfig(settings, buildPath);
734
+ generateNginxConfig(settings, buildPath, deployUi) :
735
+ generateApacheConfig(settings, buildPath, deployUi);
718
736
 
719
737
  const configPath = webServer === 'nginx' ?
720
738
  `/etc/nginx/sites-available/${settings.database.toLowerCase()}` :
@@ -796,7 +814,9 @@ async function managePM2Process(settings) {
796
814
  }
797
815
  }
798
816
 
799
- // Helper function to build and (re)start the Mango UI (Nuxt) under PM2
817
+ // Helper function to build and (re)start the Mango UI (Nuxt) under PM2.
818
+ // Only called when the UI deploy is explicitly enabled (--ui flag or
819
+ // "deployUi": true in settings.json) — the admin UI is opt-in on deploy.
800
820
  async function manageUiPM2Process(settings, mangoCmsRoot, userProjectRoot) {
801
821
  if (!settings.uiDomain) {
802
822
  console.log('No uiDomain configured, skipping Mango UI deploy.');
@@ -843,29 +863,39 @@ async function manageUiPM2Process(settings, mangoCmsRoot, userProjectRoot) {
843
863
  program
844
864
  .command('deploy')
845
865
  .description('Build and deploy Mango CMS using PM2')
846
- .action(async () => {
866
+ .option('--ui', 'also build and deploy the Mango admin UI (off by default; or set "deployUi": true in settings.json)')
867
+ .action(async (options) => {
847
868
  try {
848
869
  const configPath = path.join(process.cwd(), 'mango/config/settings.json');
849
870
  const settings = JSON.parse(fs.readFileSync(configPath, 'utf8'));
850
871
  const mangoCmsRoot = path.resolve(__dirname);
851
872
  const userProjectRoot = process.cwd();
852
873
 
874
+ // The admin UI is opt-in: deployed only with --ui or "deployUi": true.
875
+ const deployUi = Boolean(options.ui || settings.deployUi);
876
+
853
877
  // Run build command
854
878
  console.log('Building Mango CMS...');
855
879
  execSync('npm run mango build', { stdio: 'inherit' });
856
880
 
857
- // Make sure a UI bundle is available for deploy.
858
- await ensureUiExists(mangoCmsRoot);
881
+ // Make sure a UI bundle is available for deploy (only when deploying it).
882
+ if (deployUi) {
883
+ await ensureUiExists(mangoCmsRoot);
884
+ }
859
885
 
860
886
  // Check if PM2 is installed
861
887
  if (await checkPM2()) {
862
888
  await managePM2Process(settings);
863
- await manageUiPM2Process(settings, mangoCmsRoot, userProjectRoot);
889
+ if (deployUi) {
890
+ await manageUiPM2Process(settings, mangoCmsRoot, userProjectRoot);
891
+ } else {
892
+ console.log('Skipping Mango UI deploy (opt in with "mango deploy --ui" or "deployUi": true in settings.json).');
893
+ }
864
894
 
865
895
  // Configure web server
866
896
  const buildPath = path.join(process.cwd(), 'build');
867
897
  try {
868
- await configureWebServer(settings, buildPath);
898
+ await configureWebServer(settings, buildPath, deployUi);
869
899
  } catch (error) {
870
900
  console.warn('Warning: Could not configure web server:', error.message);
871
901
  console.warn('You may need to configure your web server manually.');
@@ -881,8 +911,9 @@ program
881
911
 
882
912
  program
883
913
  .command('launch')
884
- .description('One-shot server deploy: git pull, install, build the site, and deploy backend + UI')
885
- .action(async () => {
914
+ .description('One-shot server deploy: git pull, install, build the site, and deploy the backend (add --ui for the admin UI)')
915
+ .option('--ui', 'also build and deploy the Mango admin UI (off by default; or set "deployUi": true in settings.json)')
916
+ .action(async (options) => {
886
917
  try {
887
918
  const cwd = process.cwd();
888
919
  const useYarn = fs.existsSync(path.join(cwd, 'yarn.lock'));
@@ -909,12 +940,14 @@ program
909
940
  console.log('\n→ Skipping site build (no "build" script found)');
910
941
  }
911
942
 
912
- // 4. Deploy backend + UI + web server config via the freshly installed mango.
913
- // `mango deploy` builds the backend, (re)starts PM2 for {db}-mango, {db}
914
- // and {db}-ui, and writes the nginx/apache blocks for all three domains.
915
- run('Deploying backend + UI', 'npx mango deploy');
943
+ // 4. Deploy backend (+ UI when opted in) + web server config via the
944
+ // freshly installed mango. `mango deploy` builds the backend and
945
+ // (re)starts PM2 for {db}-mango and {db}; with --ui it also builds
946
+ // the admin UI, starts {db}-ui, and writes its web server block.
947
+ const deployCmd = options.ui ? 'npx mango deploy --ui' : 'npx mango deploy';
948
+ run(options.ui ? 'Deploying backend + UI' : 'Deploying backend', deployCmd);
916
949
 
917
- console.log('\n✨ Launch complete! Backend, site, and UI are deployed.');
950
+ console.log(`\n✨ Launch complete! Backend and site are deployed${options.ui ? ', along with the admin UI' : ''}.`);
918
951
  } catch (error) {
919
952
  console.error('\nLaunch failed:', error.message);
920
953
  process.exit(1);
@@ -1210,4 +1243,61 @@ To stop: mango proxy stop
1210
1243
  }
1211
1244
  });
1212
1245
 
1246
+ // --- Vibe Orchestrator Command (HAP-1253) ---
1247
+ // Runs the bundled vibe-orchestrator straight from the installed mango package —
1248
+ // no copied source. The orchestrator drives Claude Code headless in each branch
1249
+ // worktree, streams the chat to the ⌘K drawer, and verifies the owner token that
1250
+ // Mango mints (shared VIBE_ORCH_TOKEN_SECRET). It is dependency-free (node:http
1251
+ // only), so it ships inside lib/ and runs in-process from this CLI.
1252
+ //
1253
+ // Env contract:
1254
+ // VIBE_ORCH_TOKEN_SECRET (required for owner auth — UNSET ⇒ fails closed, 503)
1255
+ // VIBE_STAGING_ROOT root holding each <site> clone (default /root/Staging)
1256
+ // VIBE_ALLOWED_SITES comma-list of site slugs (defaults to this project's
1257
+ // siteName when run from a mango project root)
1258
+ // CLAUDE_CODE_OAUTH_TOKEN or ~/.claude/.credentials.json (Claude credentials)
1259
+ // VIBE_ORCH_PORT loopback listen port (default 7130; --port overrides)
1260
+ // See lib/vibe-orchestrator/README.md for the full contract.
1261
+ program
1262
+ .command('vibe-orchestrator')
1263
+ .description('Run the bundled Vibe orchestrator (Claude Code headless → ⌘K drawer). '
1264
+ + 'Env: VIBE_ORCH_TOKEN_SECRET (required), VIBE_STAGING_ROOT, VIBE_ALLOWED_SITES, '
1265
+ + 'CLAUDE_CODE_OAUTH_TOKEN (or ~/.claude). See lib/vibe-orchestrator/README.md.')
1266
+ .option('-p, --port <port>', 'loopback port to listen on (overrides VIBE_ORCH_PORT)')
1267
+ .action(async (options) => {
1268
+ const serverPath = path.join(__dirname, 'lib', 'vibe-orchestrator', 'server.js');
1269
+ if (!fs.existsSync(serverPath)) {
1270
+ console.error(`Error: bundled orchestrator not found at ${serverPath}`);
1271
+ process.exit(1);
1272
+ }
1273
+
1274
+ const env = { ...process.env };
1275
+ if (options.port) env.VIBE_ORCH_PORT = String(options.port);
1276
+
1277
+ // Convenience: when run from a mango project root and the site allow-list
1278
+ // isn't set explicitly, default it to this project's siteName so the
1279
+ // orchestrator only touches this site's clones. Explicit env always wins.
1280
+ if (!env.VIBE_ALLOWED_SITES) {
1281
+ const settingsPath = path.join(process.cwd(), 'mango/config/settings.json');
1282
+ try {
1283
+ if (fs.existsSync(settingsPath)) {
1284
+ const { siteName } = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1285
+ if (siteName) env.VIBE_ALLOWED_SITES = siteName;
1286
+ }
1287
+ } catch (e) {
1288
+ console.warn(`[vibe-orchestrator] could not read siteName from settings.json: ${e.message}`);
1289
+ }
1290
+ }
1291
+
1292
+ console.log(`[vibe-orchestrator] launching from ${serverPath}`);
1293
+ const child = spawn(process.execPath, [serverPath], { stdio: 'inherit', env });
1294
+ const forward = (sig) => { try { child.kill(sig); } catch { /* already gone */ } };
1295
+ process.on('SIGINT', () => forward('SIGINT'));
1296
+ process.on('SIGTERM', () => forward('SIGTERM'));
1297
+ child.on('exit', (code, signal) => {
1298
+ if (signal) process.kill(process.pid, signal);
1299
+ else process.exit(code ?? 0);
1300
+ });
1301
+ });
1302
+
1213
1303
  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
@@ -19,6 +19,11 @@
19
19
  "mongoURI": "mongodb://127.0.0.1:27017",
20
20
  "database": "exampleMongoDB",
21
21
 
22
+ "s3AccessKeyId": null,
23
+ "s3AccessKeySecret": null,
24
+ "s3Region": null,
25
+ "s3Bucket": "exampleBucket",
26
+
22
27
  "emailProvider": "resend",
23
28
  "emailFrom": "Example <info@example.com>",
24
29
  "resendKey": null,
@@ -29,5 +34,39 @@
29
34
  "algoliaSearchKey": null,
30
35
  "algoliaIndex": null,
31
36
 
32
- "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
+ }
33
72
  }
@@ -24,7 +24,7 @@
24
24
  "dayjs": "^1.10.7",
25
25
  "express": "^4.18.1",
26
26
  "google-maps": "^4.3.3",
27
- "mango-cms": "^0.3.31",
27
+ "mango-cms": "^0.3.36",
28
28
  "mapbox-gl": "^2.7.0",
29
29
  "vite": "^6.2.2",
30
30
  "vue": "^3.2.37",