@bhooai/nexus-cli 0.1.4 → 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.
- package/package.json +8 -12
- package/src/commands/add.ts +274 -0
- package/src/commands/build.ts +91 -0
- package/src/commands/db.ts +241 -0
- package/src/commands/dev.ts +149 -186
- package/src/commands/doctor.ts +102 -184
- package/src/commands/init.ts +204 -958
- package/src/commands/maintenance-commands.ts +40 -0
- package/src/commands/plugin.ts +233 -0
- package/src/commands/queue.ts +145 -0
- package/src/commands/test.ts +34 -0
- package/src/dispatcher.ts +98 -0
- package/src/generators.ts +102 -0
- package/src/index.ts +32 -90
- package/src/make/generators.ts +431 -0
- package/src/ports.ts +85 -0
- package/src/prompts.ts +68 -0
- package/src/templating/render.ts +109 -0
- package/src/util.ts +74 -112
- package/templates/base/Dockerfile.ejs +68 -0
- package/templates/base/README.md.ejs +62 -0
- package/templates/base/apps/admin/index.html.ejs +12 -0
- package/templates/base/apps/admin/nginx.conf.ejs +16 -0
- package/templates/base/apps/admin/package.json.ejs +15 -0
- package/templates/base/apps/admin/src/main.tsx.ejs +7 -0
- package/templates/base/apps/admin/vite.config.ts.ejs +22 -0
- package/templates/base/apps/ai-server/main.py.ejs +30 -0
- package/templates/base/apps/ai-server/requirements.txt +4 -0
- package/templates/base/apps/backend/src/main.ts +14 -0
- package/templates/base/apps/backend/src/routes/index.ts.ejs +11 -0
- package/templates/base/apps/frontend/index.html.ejs +12 -0
- package/templates/base/apps/frontend/nginx.conf.ejs +33 -0
- package/templates/base/apps/frontend/package.json.ejs +11 -0
- package/templates/base/apps/frontend/src/main.tsx.ejs +16 -0
- package/templates/base/apps/frontend/vite.config.ts.ejs +22 -0
- package/templates/base/docker-compose.yml.ejs +93 -0
- package/templates/base/dockerignore +21 -0
- package/templates/base/gitignore +43 -0
- package/templates/base/nexus.config.ts.ejs +97 -0
- package/templates/base/package.json.ejs +40 -0
- package/templates/{tsconfig.json → base/tsconfig.json} +2 -3
- package/tsconfig.json +5 -4
- package/PLAN.md +0 -141
- package/README.md +0 -34
- package/src/commands/cluster.ts +0 -133
- package/src/commands/node.ts +0 -106
- package/src/commands/pysetup.ts +0 -142
- package/src/commands/sync.ts +0 -116
- package/src/commands/uninstall.ts +0 -287
- package/src/config-sync.ts +0 -384
- package/src/dotenv.ts +0 -39
- package/src/supervisor.ts +0 -394
- package/src/wizard.ts +0 -149
- package/templates/Dockerfile +0 -60
- package/templates/README.md +0 -69
- package/templates/apps/admin/index.html +0 -12
- package/templates/apps/admin/package.json +0 -22
- package/templates/apps/admin/postcss.config.js +0 -3
- package/templates/apps/admin/src/main.tsx +0 -10
- package/templates/apps/admin/src/vite-env.d.ts +0 -18
- package/templates/apps/admin/tailwind.config.js +0 -9
- package/templates/apps/admin/tsconfig.json +0 -17
- package/templates/apps/admin/vite.config.ts +0 -64
- package/templates/apps/ai-server/main.py +0 -43
- package/templates/apps/ai-server/providers/__init__.py +0 -3
- package/templates/apps/ai-server/providers/base.py +0 -111
- package/templates/apps/ai-server/requirements.txt +0 -3
- package/templates/apps/ai-server/routers/__init__.py +0 -3
- package/templates/apps/ai-server/routers/chat.py +0 -47
- package/templates/apps/ai-server/routers/embeddings.py +0 -30
- package/templates/apps/ai-server/routers/lint.py +0 -167
- package/templates/apps/ai-server/routers/models.py +0 -23
- package/templates/apps/ai-server/routers/preflight.py +0 -169
- package/templates/apps/ai-server/settings.py +0 -48
- package/templates/apps/backend/package.json +0 -34
- package/templates/apps/backend/src/main.ts +0 -375
- package/templates/apps/backend/src/modules/admin/adminRoutes.ts +0 -732
- package/templates/apps/backend/src/modules/admin/clusterRoutes.ts +0 -391
- package/templates/apps/backend/src/modules/admin/databaseRoutes.ts +0 -161
- package/templates/apps/backend/src/modules/admin/lintProxy.ts +0 -89
- package/templates/apps/backend/src/modules/admin/preflightProxy.ts +0 -242
- package/templates/apps/backend/src/modules/admin/roleCatalog.ts +0 -78
- package/templates/apps/backend/src/modules/admin/schemaRoutes.ts +0 -449
- package/templates/apps/backend/src/modules/ai/aiProxy.ts +0 -265
- package/templates/apps/backend/src/modules/auth/authRoutes.ts +0 -220
- package/templates/apps/backend/src/modules/payments/paymentRoutes.ts +0 -100
- package/templates/apps/backend/src/modules/payments/paymentStore.ts +0 -172
- package/templates/apps/backend/src/modules/requests/requestLog.ts +0 -175
- package/templates/apps/backend/src/modules/users/userGraph.ts +0 -83
- package/templates/apps/backend/src/modules/users/userModel.ts +0 -88
- package/templates/apps/backend/src/plugins/CronScheduler.ts +0 -69
- package/templates/apps/backend/src/plugins/loadPlugins.ts +0 -107
- package/templates/apps/backend/tsconfig.json +0 -14
- package/templates/apps/frontend/index.html +0 -12
- package/templates/apps/frontend/package.json +0 -20
- package/templates/apps/frontend/postcss.config.js +0 -3
- package/templates/apps/frontend/src/index.css +0 -44
- package/templates/apps/frontend/src/main.tsx +0 -65
- package/templates/apps/frontend/tailwind.config.js +0 -9
- package/templates/apps/frontend/vite.config.ts +0 -72
- package/templates/bin/nexus.js +0 -35
- package/templates/bin/serve-all.mjs +0 -45
- package/templates/dockerignore +0 -15
- package/templates/gitignore +0 -12
- package/templates/nexus.config.ts +0 -69
- package/templates/package.json +0 -47
- package/tests/cli.test.ts +0 -52
- package/tests/config-sync.test.ts +0 -201
- package/tests/dotenv.test.ts +0 -51
- package/vitest.config.ts +0 -9
- package/vitest.config.ts.timestamp-1786095205351-444061f6ff5c58.mjs +0 -13
- /package/templates/{uploads → base/apps/backend/storage}/.gitkeep +0 -0
package/src/commands/cluster.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/node.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/pysetup.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/sync.ts
DELETED
|
@@ -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
|
-
}
|