@bhooai/nexus-cli 0.1.5 → 2.0.0

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 (112) hide show
  1. package/package.json +8 -12
  2. package/src/commands/add.ts +274 -0
  3. package/src/commands/build.ts +91 -0
  4. package/src/commands/db.ts +241 -0
  5. package/src/commands/dev.ts +149 -186
  6. package/src/commands/doctor.ts +102 -184
  7. package/src/commands/init.ts +204 -958
  8. package/src/commands/maintenance-commands.ts +40 -0
  9. package/src/commands/plugin.ts +233 -0
  10. package/src/commands/queue.ts +145 -0
  11. package/src/commands/test.ts +34 -0
  12. package/src/dispatcher.ts +98 -0
  13. package/src/generators.ts +102 -0
  14. package/src/index.ts +32 -90
  15. package/src/make/generators.ts +431 -0
  16. package/src/ports.ts +85 -0
  17. package/src/prompts.ts +68 -0
  18. package/src/templating/render.ts +109 -0
  19. package/src/util.ts +74 -112
  20. package/templates/base/Dockerfile.ejs +68 -0
  21. package/templates/base/README.md.ejs +62 -0
  22. package/templates/base/apps/admin/index.html.ejs +12 -0
  23. package/templates/base/apps/admin/nginx.conf.ejs +16 -0
  24. package/templates/base/apps/admin/package.json.ejs +15 -0
  25. package/templates/base/apps/admin/src/main.tsx.ejs +7 -0
  26. package/templates/base/apps/admin/vite.config.ts.ejs +22 -0
  27. package/templates/base/apps/ai-server/main.py.ejs +30 -0
  28. package/templates/base/apps/ai-server/requirements.txt +4 -0
  29. package/templates/base/apps/backend/src/main.ts +14 -0
  30. package/templates/base/apps/backend/src/routes/index.ts.ejs +11 -0
  31. package/templates/base/apps/frontend/index.html.ejs +12 -0
  32. package/templates/base/apps/frontend/nginx.conf.ejs +33 -0
  33. package/templates/base/apps/frontend/package.json.ejs +11 -0
  34. package/templates/base/apps/frontend/src/main.tsx.ejs +16 -0
  35. package/templates/base/apps/frontend/vite.config.ts.ejs +22 -0
  36. package/templates/base/docker-compose.yml.ejs +93 -0
  37. package/templates/base/dockerignore +21 -0
  38. package/templates/base/gitignore +43 -0
  39. package/templates/base/nexus.config.ts.ejs +97 -0
  40. package/templates/base/package.json.ejs +40 -0
  41. package/templates/{tsconfig.json → base/tsconfig.json} +2 -3
  42. package/tsconfig.json +5 -4
  43. package/PLAN.md +0 -141
  44. package/README.md +0 -34
  45. package/src/commands/cluster.ts +0 -133
  46. package/src/commands/node.ts +0 -106
  47. package/src/commands/pysetup.ts +0 -142
  48. package/src/commands/sync.ts +0 -116
  49. package/src/commands/uninstall.ts +0 -287
  50. package/src/config-sync.ts +0 -384
  51. package/src/dotenv.ts +0 -39
  52. package/src/supervisor.ts +0 -394
  53. package/src/wizard.ts +0 -149
  54. package/templates/Dockerfile +0 -60
  55. package/templates/README.md +0 -69
  56. package/templates/apps/admin/index.html +0 -12
  57. package/templates/apps/admin/package.json +0 -22
  58. package/templates/apps/admin/postcss.config.js +0 -3
  59. package/templates/apps/admin/src/main.tsx +0 -10
  60. package/templates/apps/admin/src/vite-env.d.ts +0 -18
  61. package/templates/apps/admin/tailwind.config.js +0 -9
  62. package/templates/apps/admin/tsconfig.json +0 -17
  63. package/templates/apps/admin/vite.config.ts +0 -64
  64. package/templates/apps/ai-server/main.py +0 -43
  65. package/templates/apps/ai-server/providers/__init__.py +0 -3
  66. package/templates/apps/ai-server/providers/base.py +0 -111
  67. package/templates/apps/ai-server/requirements.txt +0 -3
  68. package/templates/apps/ai-server/routers/__init__.py +0 -3
  69. package/templates/apps/ai-server/routers/chat.py +0 -47
  70. package/templates/apps/ai-server/routers/embeddings.py +0 -30
  71. package/templates/apps/ai-server/routers/lint.py +0 -167
  72. package/templates/apps/ai-server/routers/models.py +0 -23
  73. package/templates/apps/ai-server/routers/preflight.py +0 -169
  74. package/templates/apps/ai-server/settings.py +0 -48
  75. package/templates/apps/backend/package.json +0 -34
  76. package/templates/apps/backend/src/main.ts +0 -375
  77. package/templates/apps/backend/src/modules/admin/adminRoutes.ts +0 -732
  78. package/templates/apps/backend/src/modules/admin/clusterRoutes.ts +0 -391
  79. package/templates/apps/backend/src/modules/admin/databaseRoutes.ts +0 -161
  80. package/templates/apps/backend/src/modules/admin/lintProxy.ts +0 -89
  81. package/templates/apps/backend/src/modules/admin/preflightProxy.ts +0 -242
  82. package/templates/apps/backend/src/modules/admin/roleCatalog.ts +0 -78
  83. package/templates/apps/backend/src/modules/admin/schemaRoutes.ts +0 -449
  84. package/templates/apps/backend/src/modules/ai/aiProxy.ts +0 -265
  85. package/templates/apps/backend/src/modules/auth/authRoutes.ts +0 -220
  86. package/templates/apps/backend/src/modules/payments/paymentRoutes.ts +0 -100
  87. package/templates/apps/backend/src/modules/payments/paymentStore.ts +0 -172
  88. package/templates/apps/backend/src/modules/requests/requestLog.ts +0 -175
  89. package/templates/apps/backend/src/modules/users/userGraph.ts +0 -83
  90. package/templates/apps/backend/src/modules/users/userModel.ts +0 -88
  91. package/templates/apps/backend/src/plugins/CronScheduler.ts +0 -69
  92. package/templates/apps/backend/src/plugins/loadPlugins.ts +0 -107
  93. package/templates/apps/backend/tsconfig.json +0 -14
  94. package/templates/apps/frontend/index.html +0 -12
  95. package/templates/apps/frontend/package.json +0 -20
  96. package/templates/apps/frontend/postcss.config.js +0 -3
  97. package/templates/apps/frontend/src/index.css +0 -44
  98. package/templates/apps/frontend/src/main.tsx +0 -65
  99. package/templates/apps/frontend/tailwind.config.js +0 -9
  100. package/templates/apps/frontend/vite.config.ts +0 -72
  101. package/templates/bin/nexus.js +0 -35
  102. package/templates/bin/serve-all.mjs +0 -45
  103. package/templates/dockerignore +0 -15
  104. package/templates/gitignore +0 -12
  105. package/templates/nexus.config.ts +0 -69
  106. package/templates/package.json +0 -47
  107. package/tests/cli.test.ts +0 -52
  108. package/tests/config-sync.test.ts +0 -201
  109. package/tests/dotenv.test.ts +0 -51
  110. package/vitest.config.ts +0 -9
  111. package/vitest.config.ts.timestamp-1786095205351-444061f6ff5c58.mjs +0 -13
  112. /package/templates/{uploads → base/apps/backend/storage}/.gitkeep +0 -0
@@ -1,133 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { loadConfigAuto } from '../../../nexus-core/src/index.js';
4
- import { ClusterManager } from '@bhooai/nexus-cluster';
5
- import type { DeepPartial, NexusConfig } from '@bhooai/nexus-core';
6
-
7
- const GREEN = '\x1b[32m';
8
- const RED = '\x1b[31m';
9
- const YELLOW = '\x1b[33m';
10
- const CYAN = '\x1b[36m';
11
- const DIM = '\x1b[2m';
12
- const RESET = '\x1b[0m';
13
-
14
- /**
15
- * `nexus cluster <subcommand>` - central-side mesh orchestration.
16
- *
17
- * cluster status list linked nodes + LB RPS
18
- * cluster link <url> handshake + register a node by its agent URL
19
- * cluster unlink <id> forget a node
20
- * cluster start|stop|restart|kill <id>
21
- * cluster scale <n> target exactly n backend nodes
22
- * cluster serve hold process: LB + autoscaler (foreground)
23
- * cluster check poll every node once (health + metrics)
24
- */
25
- export async function cluster(args: string[]): Promise<number> {
26
- const cfg = await loadConfigAuto({ root: process.cwd() });
27
- const [sub, a, b] = args;
28
-
29
- // `serve` is the explicit "start the cluster" command - if a prior admin
30
- // stop wrote {cluster:{enabled:false}} into nexus.runtime.json, that
31
- // override wins over nexus.config.ts and would silently keep the cluster
32
- // off. Re-enable it in runtime.json so the LB actually starts.
33
- if (sub === 'serve' && !cfg.cluster.enabled) {
34
- enableClusterInRuntime(process.cwd());
35
- console.log(`${YELLOW}cluster was disabled in nexus.runtime.json - re-enabling${RESET}`);
36
- }
37
-
38
- // For non-serve commands, warn (don't block) when the cluster is off.
39
- if (sub !== 'serve' && !cfg.cluster.enabled) {
40
- console.warn(`${RED}cluster.enabled is false${RESET} - run ${CYAN}nexus cluster serve${RESET} to start it, or set cluster: { enabled: true } in nexus.config.ts`);
41
- }
42
-
43
- const manager = new ClusterManager({ config: cfg.cluster, root: process.cwd(), aiServerUrl: cfg.ai.serverUrl });
44
-
45
- switch (sub ?? 'help') {
46
- case 'status': {
47
- const nodes = manager.list();
48
- const rps = manager.rps();
49
- console.log(`${CYAN}cluster${RESET} - ${nodes.length} linked node(s)`);
50
- for (const n of nodes) {
51
- const color = n.status === 'ready' ? GREEN : n.status === 'unreachable' ? RED : CYAN;
52
- console.log(` ${color}${n.status}${RESET} ${DIM}${n.identity.id}${RESET} role=${n.identity.role} tier=${n.identity.tier} enabled=${n.enabled} rps=${rps[n.identity.id] ?? 0}${n.lastMetrics ? ` cpu=${n.lastMetrics.cpu}% mem=${n.lastMetrics.memoryMb}MiB` : ''}`);
53
- }
54
- return 0;
55
- }
56
- case 'link': {
57
- if (!a) { console.error('cluster link <agent-url>'); return 1; }
58
- try {
59
- const node = await manager.link(a);
60
- const color = node.status === 'ready' ? GREEN : CYAN;
61
- console.log(`${color}linked${RESET} ${node.identity.id} role=${node.identity.role} base=${node.identity.baseUrl}`);
62
- return 0;
63
- } catch (err) {
64
- console.error(`${RED}link failed${RESET}: ${(err as Error).message}`);
65
- return 1;
66
- }
67
- }
68
- case 'unlink': {
69
- if (!a) { console.error('cluster unlink <id>'); return 1; }
70
- return manager.unlink(a) ? (console.log(`removed ${a}`), 0) : (console.error(`no such node: ${a}`), 1);
71
- }
72
- case 'start':
73
- case 'stop':
74
- case 'restart':
75
- case 'kill': {
76
- if (!a) { console.error(`cluster ${sub} <id>`); return 1; }
77
- await manager.exec(a, sub === 'kill' ? 'stop' : sub);
78
- console.log(`${sub} ${a}`);
79
- return 0;
80
- }
81
- case 'scale': {
82
- if (!a || !Number.isFinite(Number(a))) { console.error('cluster scale <n>'); return 1; }
83
- try {
84
- await manager.scaleTo(Number(a));
85
- console.log(`scaled to ${a} backend node(s)`);
86
- return 0;
87
- } catch (err) {
88
- console.error(`${RED}${(err as Error).message}${RESET}`);
89
- return 1;
90
- }
91
- }
92
- case 'check': {
93
- await manager.pollAll();
94
- console.log(`checked ${manager.list().length} node(s)`);
95
- return 0;
96
- }
97
- case 'serve': {
98
- console.log(`${CYAN}cluster${RESET} LB on ${cfg.cluster.lbHost}:${cfg.cluster.lbPort}${RESET}`);
99
- await manager.listenLb(cfg.cluster.lbHost);
100
- manager.autoscaler.start(10_000);
101
- setInterval(() => void manager.pollAll().catch(() => {}), 15_000);
102
- return 0;
103
- }
104
- case 'help':
105
- default:
106
- console.log(`Usage: nexus cluster <status|link|unlink|start|stop|restart|kill|scale|check|serve>`);
107
- return 0;
108
- }
109
- }
110
-
111
- /**
112
- * Patch `nexus.runtime.json` to set `cluster.enabled = true`. The admin
113
- * "stop cluster" endpoint writes `{cluster:{enabled:false}}` there, and that
114
- * override beats `nexus.config.ts` in the loader precedence - so `nexus
115
- * cluster serve` would read `enabled:false` and the LB would never bind.
116
- * This removes the stale override so `serve` actually starts the cluster.
117
- */
118
- function enableClusterInRuntime(root: string): void {
119
- const runtimePath = resolve(root, 'nexus.runtime.json');
120
- let runtime: Record<string, unknown> = {};
121
- if (existsSync(runtimePath)) {
122
- try {
123
- runtime = JSON.parse(readFileSync(runtimePath, 'utf8')) as Record<string, unknown>;
124
- } catch {
125
- // corrupt runtime file - start fresh
126
- runtime = {};
127
- }
128
- }
129
- const cluster = (runtime['cluster'] ?? {}) as Record<string, unknown>;
130
- cluster['enabled'] = true;
131
- runtime['cluster'] = cluster;
132
- writeFileSync(runtimePath, `${JSON.stringify(runtime, null, 2)}\n`);
133
- }
@@ -1,106 +0,0 @@
1
- import { loadConfigAuto } from '../../../nexus-core/src/index.js';
2
- import { NodeAgent, nodeIdFor } from '@bhooai/nexus-cluster';
3
- const GREEN = '\x1b[32m';
4
- const RED = '\x1b[31m';
5
- const CYAN = '\x1b[36m';
6
- const DIM = '\x1b[2m';
7
- const RESET = '\x1b[0m';
8
-
9
- /**
10
- * `nexus node <subcommand>` - run THIS machine as a cluster node.
11
- *
12
- * node id --role <role> print this node's stable id
13
- * node serve --role=<role> [--port] start the agent + role service
14
- *
15
- * Onboarding per the mesh flow:
16
- * server$ nexus node serve --role=backend
17
- * -> prints agent URL + pairing token; paste the URL on the central:
18
- * central$ nexus cluster link http://<this-host>:7575
19
- */
20
- export async function node(args: string[]): Promise<number> {
21
- const sub = args[0];
22
- const role = argValue(args, '--role') ?? 'backend';
23
-
24
- const cfg = await loadConfigAuto({ root: process.cwd() });
25
- const token = cfg.cluster.token;
26
- const port = Number(argValue(args, '--port') ?? '7575');
27
- const agentHost = cfg.cluster.nodeAgentHost;
28
- const defaultPort = Number.isInteger(cfg.cluster.nodeAgentPort) ? cfg.cluster.nodeAgentPort : 7575;
29
- const effectivePort = Number.isInteger(port) ? port : defaultPort;
30
-
31
- if (sub === 'id') {
32
- console.log(nodeIdFor(process.cwd(), role as never));
33
- return 0;
34
- }
35
-
36
- if (sub === 'serve') {
37
- const description = roleDescription(role);
38
- const command = serviceCommand(role, cfg);
39
- const noService = args.includes('--no-service');
40
- const agent = new NodeAgent({
41
- projectRoot: process.cwd(),
42
- role: role as never,
43
- tier: 'dev',
44
- port: effectivePort,
45
- token,
46
- version: '0.1.0',
47
- services: serviceUrls(role, cfg),
48
- serviceCommand: noService ? [] : command,
49
- serviceEnv: { NEXUS_SERVER_PORT: String(cfg.server.port) },
50
- advertisedUrl: process.env.NEXUS_NODE_ADVERTISED_URL,
51
- });
52
- await agent.listen(agentHost);
53
- console.log(`${CYAN}node-agent${RESET} ready - role ${role} ${description}`);
54
- console.log(` ${GREEN}agent url: http://${agentHost}:${effectivePort}${RESET}`);
55
- console.log(` ${GREEN}token: ${token ? `${token}` : '<empty - set cluster.token>'}`)
56
- console.log(` ${DIM}link it from the central: nexus cluster link http://${agentHost}:${effectivePort}${RESET}`);
57
- if (noService) {
58
- console.log(` ${DIM}agent-only mode (--no-service): not spawning a role service;${RESET}`);
59
- console.log(` ${DIM}advertising ${serviceUrls(role, cfg)[role as 'backend'] ?? '—'} as the role service.${RESET}`);
60
- } else if (command.length) {
61
- agent.startService();
62
- console.log(` started role service: ${command.join(' ')}`);
63
- }
64
- // Hold the process.
65
- return await new Promise<number>(() => {});
66
- }
67
-
68
- console.log('Usage: nexus node <id|serve> [--role=backend|files|database|ai] [--port=N]');
69
- return 1;
70
- }
71
-
72
- function argValue(args: string[], name: string): string | undefined {
73
- const i = args.indexOf(name);
74
- if (i >= 0) return args[i + 1];
75
- return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
76
- }
77
-
78
- function roleDescription(role: string): string {
79
- switch (role) {
80
- case 'backend': return '(API core)';
81
- case 'files': return '(static + uploads storage)';
82
- case 'database': return '(Mongo + Redis)';
83
- case 'ai': return '(AI inference engine)';
84
- default: return '';
85
- }
86
- }
87
-
88
- function serviceCommand(role: string, cfg: Awaited<ReturnType<typeof loadConfigAuto>>): string[] {
89
- switch (role) {
90
- case 'backend':
91
- return ['tsx', 'apps/backend/src/main.ts'];
92
- case 'ai':
93
- return ['python', 'main.py'];
94
- case 'files':
95
- // Fall back to the framework uploads server launcher if present.
96
- return ['tsx', 'apps/backend/src/main.ts'];
97
- case 'database':
98
- return ['tsx', 'apps/backend/src/main.ts'];
99
- default:
100
- return [];
101
- }
102
- }
103
-
104
- function serviceUrls(role: string, cfg: Awaited<ReturnType<typeof loadConfigAuto>>): Partial<Record<'backend' | 'files' | 'database' | 'ai', string>> {
105
- return { [role]: `http://localhost:${cfg.server.port}` };
106
- }
@@ -1,142 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
4
- import { versionOf } from '../util.js';
5
- import { isInteractive as wizardInteractive, confirm, prompt, closeWizard } from '../wizard.js';
6
-
7
- /**
8
- * Install the Python AI server dependencies (plus any extra packages).
9
- *
10
- * Locates `apps/ai-server/requirements.txt` (or a path given with --requirements),
11
- * optionally creates a virtualenv with --venv, and streams pip output through so the
12
- * user can watch progress.
13
- *
14
- * nexus pysetup install from requirements.txt
15
- * nexus pysetup openai pandas install requirements.txt + extra packages
16
- * nexus pysetup --venv create ./apps/ai-server/.venv and install into it
17
- * nexus pysetup --interactive prompt for venv y/n + python path (TTY only)
18
- * nexus pysetup --upgrade upgrade existing packages
19
- * nexus pysetup --python C:/Python/Python314/python.exe
20
- * nexus pysetup --requirements ./my-requirements.txt
21
- */
22
- export async function pysetup(args: string[]): Promise<number> {
23
- let opts: PyOpts = parseArgs(args);
24
-
25
- // -- interactive wizard mode (TTY only) ---------------------------
26
- if (opts.interactive && wizardInteractive()) {
27
- opts = await interactivePrompt(opts);
28
- closeWizard();
29
- }
30
-
31
- const reqFile = resolve(process.cwd(), opts.requirements || findRequirementsFile(process.cwd()));
32
- const py = await resolvePython(opts.python);
33
-
34
- if (!opts.requirements && !findRequirementsFile(process.cwd())) {
35
- console.error('Could not find apps/ai-server/requirements.txt - pass one with --requirements <path>.');
36
- return 1;
37
- }
38
- if (!existsSync(reqFile)) {
39
- console.error(`requirements file not found: ${reqFile}`);
40
- return 1;
41
- }
42
- if (!py) {
43
- console.error(`Python interpreter not found (tried python / python3). Pass --python <path>.`);
44
- return 1;
45
- }
46
-
47
- const venvPy = opts.venv ? join(opts.venvDir, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python') : py;
48
- const createVenv = async () => {
49
- if (existsSync(venvPy)) return 0;
50
- console.log(`\n\x1b[36m[pysetup]\x1b[0m creating venv at ${opts.venvDir}`);
51
- return run(py, ['-m', 'venv', opts.venvDir]);
52
- };
53
-
54
- console.log(`\n\x1b[36m[pysetup]\x1b[0m installing python packages`);
55
- console.log(` python : ${py}${opts.venv ? ` -> ${venvPy}` : ''}`);
56
- console.log(` requirements: ${reqFile}`);
57
- console.log(` extras : ${opts.extras.length ? opts.extras.join(', ') : '(none)'}`);
58
- if (opts.venv) console.log(` venv : ${opts.venvDir}`);
59
-
60
- if (opts.venv) {
61
- const venvCode = await createVenv();
62
- if (venvCode !== 0) return venvCode;
63
- }
64
-
65
- const pipArgs = ['-m', 'pip', 'install'];
66
- if (opts.upgrade) pipArgs.push('--upgrade');
67
- pipArgs.push('-r', reqFile, ...opts.extras);
68
-
69
- const code = await run(venvPy ?? py, pipArgs);
70
- if (code !== 0) {
71
- console.error(`\n[pysetup] install failed (exit ${code}).`);
72
- return code;
73
- }
74
- console.log(`\n\x1b[32m[pysetup]\x1b[0m done - ${opts.extras.length ? opts.extras.join(' ') : 'core requirements'} installed.`);
75
- return 0;
76
- }
77
-
78
- /** Interactive prompts for venv + python path. Returns updated opts. */
79
- async function interactivePrompt(opts: PyOpts): Promise<PyOpts> {
80
- const wantVenv = await confirm('Create a virtualenv for the AI server?', opts.venv);
81
- const pythonPath = await prompt('Python interpreter path (blank = auto-detect)', opts.python || '');
82
- return { ...opts, venv: wantVenv, python: pythonPath.trim() };
83
- }
84
-
85
- interface PyOpts { python: string; requirements: string; extras: string[]; upgrade: boolean; venv: boolean; venvDir: string; interactive: boolean }
86
-
87
- function parseArgs(args: string[]): PyOpts {
88
- let python = '';
89
- let requirements = '';
90
- let upgrade = false;
91
- let venv = false;
92
- let interactive = false;
93
- const extras: string[] = [];
94
- for (let i = 0; i < args.length; i++) {
95
- const a = args[i];
96
- if (a === '--python') { python = args[++i] ?? ''; continue; }
97
- if (a === '--requirements' || a === '-r') { requirements = args[++i] ?? ''; continue; }
98
- if (a === '--upgrade' || a === '-U') { upgrade = true; continue; }
99
- if (a === '--venv') { venv = true; continue; }
100
- if (a === '--interactive') { interactive = true; continue; }
101
- if (a.startsWith('--') || a.startsWith('-')) continue;
102
- extras.push(a);
103
- }
104
- const reqDir = requirements ? resolve(requirements) : join(process.cwd(), 'apps', 'ai-server');
105
- return { python, requirements, extras, upgrade, venv, venvDir: join(reqDir, '.venv'), interactive };
106
- }
107
-
108
- /** Find a requirements.txt under the cwd that belongs to the AI server. */
109
- function findRequirementsFile(cwd: string): string {
110
- const candidates = [
111
- join(cwd, 'apps', 'ai-server', 'requirements.txt'),
112
- join(cwd, 'apps', 'server', 'requirements.txt'),
113
- join(cwd, 'requirements.txt'),
114
- ];
115
- return candidates.find((p) => existsSync(p)) ?? '';
116
- }
117
-
118
- async function resolvePython(explicit: string): Promise<string> {
119
- if (explicit) return existsSync(explicit) ? explicit : explicit;
120
- const candidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
121
- for (const c of candidates) {
122
- if (await versionOf(c)) return c;
123
- }
124
- return '';
125
- }
126
-
127
- function run(cmd: string, args: string[]): Promise<number> {
128
- return new Promise((resolveRun) => {
129
- // Windows: python may be a .cmd shim and needs a shell. Pass the full command
130
- // line to avoid Node's DEP0190 warning (args + shell: true are concatenated
131
- // unescaped anyway).
132
- const win = process.platform === 'win32';
133
- const child = win
134
- ? spawn([cmd, ...args].map((t) => (/\s/.test(t) ? `"${t}"` : t)).join(' '), { stdio: 'inherit', shell: true })
135
- : spawn(cmd, args, { stdio: 'inherit' });
136
- child.on('error', (err) => {
137
- console.error(`\x1b[31m[pysetup]\x1b[0m failed to run ${cmd}: ${err.message}`);
138
- resolveRun(1);
139
- });
140
- child.on('close', (code) => resolveRun(code ?? 0));
141
- });
142
- }
@@ -1,116 +0,0 @@
1
- import { loadConfigAuto } from '../../../nexus-core/src/index.js';
2
- import {
3
- readFingerprint,
4
- syncConfig,
5
- type FileSyncReport,
6
- type SyncReport,
7
- } from '../config-sync.js';
8
-
9
- const GREEN = '\x1b[32m';
10
- const CYAN = '\x1b[36m';
11
- const YELLOW = '\x1b[33m';
12
- const DIM = '\x1b[2m';
13
- const BOLD = '\x1b[1m';
14
- const RESET = '\x1b[0m';
15
-
16
- /**
17
- * `nexus sync` - rewrite every derived artifact (Dockerfile, docker.* helpers,
18
- * bin/serve-all.mjs, apps/admin/package.json, the shared project-info DB) to
19
- * match the single `nexus.config.ts` source of truth.
20
- *
21
- * Returns the process exit code and always prints the resolved values so the
22
- * "one config prints everywhere" contract is visible.
23
- */
24
- export async function syncCommand(args: string[] = []): Promise<number> {
25
- const root = process.cwd();
26
- const opts = {
27
- write: !args.includes('--check') && !args.includes('--dry-run'),
28
- db: !args.includes('--no-db'),
29
- };
30
-
31
- if (!opts.write) {
32
- process.stdout.write(`${DIM} nexus sync - dry run (nothing written)${RESET}\n\n`);
33
- }
34
-
35
- const cfg = await loadConfigAuto({ root });
36
- const report = await syncConfig(root, cfg, opts);
37
-
38
- printReport(report, root);
39
-
40
- if (!opts.write) {
41
- process.stdout.write(`\n ${YELLOW}Dry run - run \`nexus sync\` (no flag) to write the changes.${RESET}\n`);
42
- }
43
-
44
- const changed = report.files.some((f) => f.status === 'updated');
45
- if (changed) {
46
- process.stdout.write(`\n ${GREEN}Synced. Run \`nexus dev\` to boot the stack on the new values.${RESET}\n`);
47
- } else {
48
- process.stdout.write(`\n ${DIM}Everything is already in sync.${RESET}\n`);
49
- }
50
-
51
- return 0;
52
- }
53
-
54
- /** Print the per-value summary + per-file status table. */
55
- function printReport(report: SyncReport, root: string): void {
56
- const v = report.values;
57
-
58
- process.stdout.write(`\n${BOLD} * Nexus config sync${RESET} ${DIM}${root}${RESET}\n\n`);
59
-
60
- process.stdout.write(` ${CYAN}Value${RESET}${' '.repeat(12 - 5)}Config -> everywhere\n`);
61
- process.stdout.write(` -----------------------------\n`);
62
- const rows: Array<[string, string]> = [
63
- ['backend', `${v.serverPort}`],
64
- ['frontend', `${v.frontendPort}`],
65
- ['admin', `${v.adminPort}`],
66
- ['node agent', `${v.nodePort}`],
67
- ['cluster LB', `${v.lbPort}`],
68
- ['AI server', `${v.aiPort}`],
69
- ['database', `${v.dbName}`],
70
- ];
71
- for (const [label, value] of rows) {
72
- const pad = label.padEnd(10);
73
- process.stdout.write(` ${CYAN}${pad}${RESET} ${value}\n`);
74
- }
75
-
76
- process.stdout.write('\n');
77
- for (const f of report.files) {
78
- const status = statusLabel(f);
79
- process.stdout.write(` ${f.file.padEnd(28)}${status}\n`);
80
- if (f.status === 'updated' && f.changes.length > 0) {
81
- for (const line of f.changes) {
82
- process.stdout.write(` ${DIM}${line}${RESET}\n`);
83
- }
84
- }
85
- }
86
-
87
- process.stdout.write(`\n ${BOLD}Database${RESET} ${dbLabel(report.db)}\n`);
88
-
89
- if (readFingerprint(root)) {
90
- process.stdout.write(` ${DIM}fingerprint ${report.fingerprint}${RESET}\n`);
91
- }
92
- }
93
-
94
- function statusLabel(f: FileSyncReport): string {
95
- switch (f.status) {
96
- case 'updated':
97
- return `${GREEN}updated${RESET}`;
98
- case 'in-sync':
99
- return `${DIM}in sync${RESET}`;
100
- case 'missing':
101
- return `${YELLOW}missing${RESET}`;
102
- default:
103
- return `${YELLOW}no match - review${RESET}`;
104
- }
105
- }
106
-
107
- function dbLabel(db: SyncReport['db']): string {
108
- switch (db) {
109
- case 'updated':
110
- return `${GREEN}project record upserted${RESET}`;
111
- case 'failed':
112
- return `${YELLOW}unreachable - skipped${RESET}`;
113
- default:
114
- return `${DIM}skipped${RESET}`;
115
- }
116
- }