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.
- package/cli.js +113 -23
- package/default/infra/vibe/README.md +43 -0
- package/default/infra/vibe/cloudflare.ini.template +26 -0
- package/default/infra/vibe/ecosystem.vibe.config.cjs +44 -0
- package/default/infra/vibe/nginx-vibe-orchestrator.conf.template +50 -0
- package/default/infra/vibe/nginx-vibe-staging.conf.template +73 -0
- package/default/infra/vibe/vibe-gateway.service +38 -0
- package/default/infra/vibe/vibe-orchestrator.service +44 -0
- package/default/infra/vibe/vibe.env.template +24 -0
- package/default/mango/config/settings.json +40 -1
- package/default/package.json +1 -1
- package/default/vite.config.js +46 -0
- package/lib/vibe-orchestrator/README.md +76 -0
- package/lib/vibe-orchestrator/scripts/fake-claude.mjs +35 -0
- package/lib/vibe-orchestrator/scripts/path-guard-hook.mjs +70 -0
- package/lib/vibe-orchestrator/scripts/vibe-recover.sh +63 -0
- package/lib/vibe-orchestrator/server.js +344 -0
- package/lib/vibe-orchestrator/src/attachments.js +98 -0
- package/lib/vibe-orchestrator/src/claudeRunner.js +233 -0
- package/lib/vibe-orchestrator/src/config.js +227 -0
- package/lib/vibe-orchestrator/src/costMirror.js +64 -0
- package/lib/vibe-orchestrator/src/costStore.js +209 -0
- package/lib/vibe-orchestrator/src/ownerToken.js +113 -0
- package/lib/vibe-orchestrator/src/pathGuard.js +114 -0
- package/lib/vibe-orchestrator/src/preamble.js +139 -0
- package/lib/vibe-orchestrator/src/publisher.js +376 -0
- package/lib/vibe-orchestrator/src/recovery.js +199 -0
- package/lib/vibe-orchestrator/src/screenshot.js +38 -0
- package/lib/vibe-orchestrator/src/sessionManager.js +291 -0
- package/lib/vibe-orchestrator/src/streamParser.js +188 -0
- package/lib/vibe-orchestrator/src/tokenMeter.js +64 -0
- package/package.json +1 -1
- 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,
|
|
533
|
-
|
|
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,
|
|
628
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
885
|
-
.
|
|
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
|
|
913
|
-
// `mango deploy` builds the backend
|
|
914
|
-
//
|
|
915
|
-
|
|
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(
|
|
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
|
}
|