@bhooai/nexus-cli 0.1.5 → 2.0.1

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 +26 -27
  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,200 +1,163 @@
1
- import { ServiceSpec, Supervisor } from '../supervisor.js';
2
- import { loadConfigAuto } from '../../../nexus-core/src/index.js';
3
- import { configChangedSinceSync, syncConfig } from '../config-sync.js';
4
- import { isPortFree } from '../util.js';
5
-
6
- const COLORS = {
7
- backend: '\x1b[32m',
8
- frontend: '\x1b[36m',
9
- 'ai-server': '\x1b[33m',
10
- admin: '\x1b[35m',
11
- 'node-agent': '\x1b[34m',
12
- };
1
+ /**
2
+ * nexus dev start the dev services:
3
+ * every apps/backend-* (tsx watch), frontend(s) (vite), admin (vite), ai-server (python).
4
+ *
5
+ * Sub-process supervision with log prefixing, auto-restart, and clean shutdown
6
+ * on Ctrl+C.
7
+ */
8
+ import { spawn, type ChildProcess } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import { readdir } from 'node:fs/promises';
11
+ import { resolve, join } from 'node:path';
12
+ import type { CommandContext } from '../dispatcher.js';
13
+ import { ensurePort } from '../ports.js';
14
+
15
+ interface Service {
16
+ name: string;
17
+ cmd: string;
18
+ args: string[];
19
+ cwd: string;
20
+ color: string;
21
+ enabled: boolean;
22
+ port: number;
23
+ }
13
24
 
14
- const BOLD = '\x1b[1m';
15
- const DIM = '\x1b[2m';
25
+ const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m', '\x1b[34m', '\x1b[31m', '\x1b[37m', '\x1b[90m'];
16
26
  const RESET = '\x1b[0m';
17
27
 
18
- /** `nexus dev` - start the four terminals under the supervisor. */
19
- export async function dev(args: string[] = []): Promise<number> { const cfg = await loadConfigAuto({ root: process.cwd() });
20
- const onlyIdx = args.indexOf('--only');
21
- const only = onlyIdx >= 0 ? args[onlyIdx + 1]?.split(',') : undefined;
22
- const cpIdx = args.indexOf('--control-port');
23
- const controlPort = cpIdx >= 0 && Number.isFinite(Number(args[cpIdx + 1])) ? Number(args[cpIdx + 1]) : 7474;
24
-
25
- // Runtime port auto-allocation: a second project (master + slave) can run on
26
- // the same machine side-by-side. Any configured port that is already taken is
27
- // bumped to the next free one and threaded through NEXUS_* env, which both
28
- // the backend loader and the Vite dev servers honour (config < env).
29
- const serverPort = await nextFreePort(cfg.server.host, cfg.server.port);
30
- const frontendPort = await nextFreePort('127.0.0.1', cfg.frontend.port);
31
- const adminPort = await nextFreePort('127.0.0.1', cfg.admin.port);
32
- const aiServerPort = await nextFreePort('127.0.0.1', Number(aiPort(cfg.ai.serverUrl)));
33
- const lbPort = await nextFreePort('127.0.0.1', cfg.cluster.lbPort);
34
- const agentPort = await nextFreePort('127.0.0.1', cfg.cluster.nodeAgentPort);
35
-
36
- // If the config changed since the last sync, rewrite every derived artifact
37
- // (Dockerfile, docker.*, serve-all.mjs, admin pkg, project DB) before booting
38
- // so the whole stack runs on the new values.
39
- if (!args.includes('--no-sync')) {
40
- if (configChangedSinceSync(process.cwd(), cfg)) {
41
- process.stdout.write(`\n ${BOLD}Config changed - re-syncing derived artifacts...${RESET}\n`);
42
- const report = await syncConfig(process.cwd(), cfg);
43
- const changed = report.files.filter((f) => f.status === 'updated').map((f) => f.file);
44
- process.stdout.write(
45
- ` ${DIM}updated: ${changed.length > 0 ? changed.join(', ') : 'none'} | db: ${report.db}${RESET}\n`,
46
- );
47
- }
28
+ export async function run(ctx: CommandContext): Promise<void> {
29
+ const only = (ctx.args.flags.only as string)?.split(',').map((s) => s.trim()) ?? null;
30
+ const projectRoot = process.cwd();
31
+
32
+ // Discover backends, frontends, admin, ai-server under apps/
33
+ const appsDir = join(projectRoot, 'apps');
34
+ const appEntries = existsSync(appsDir) ? await readdir(appsDir, { withFileTypes: true }) : [];
35
+ const backends = appEntries.filter((e) => e.isDirectory() && e.name.startsWith('backend')).map((e) => e.name);
36
+ const frontends = appEntries.filter((e) => e.isDirectory() && e.name.startsWith('frontend')).map((e) => e.name);
37
+ const admins = appEntries.filter((e) => e.isDirectory() && e.name.startsWith('admin')).map((e) => e.name);
38
+ const aiServers = appEntries.filter((e) => e.isDirectory() && e.name === 'ai-server').map((e) => e.name);
39
+
40
+ const services: Service[] = [];
41
+ let colorIdx = 0;
42
+
43
+ for (const b of backends) {
44
+ const mainPath = join(appsDir, b, 'src', 'main.ts');
45
+ if (!existsSync(mainPath)) continue;
46
+ const port = await ensurePort(projectRoot, b);
47
+ services.push({
48
+ name: b,
49
+ cmd: 'npx',
50
+ args: ['tsx', 'watch', 'src/main.ts'],
51
+ cwd: join(appsDir, b),
52
+ color: COLORS[colorIdx++ % COLORS.length]!,
53
+ enabled: !only || only.includes(b),
54
+ port: port,
55
+ });
48
56
  }
49
-
50
- // Env override for a port that was bumped away from the configured value.
51
- // Only set when different, so the configured port is used verbatim otherwise.
52
- const portEnv = (actual: number, configured: number, key: string): Record<string, string> =>
53
- actual !== configured ? { [key]: String(actual) } : {};
54
-
55
- const all: ServiceSpec[] = [
56
- {
57
- name: 'backend',
58
- command: ['tsx', 'watch', 'apps/backend/src/main.ts'],
59
- cwd: '',
60
- color: COLORS.backend,
61
- env: {
62
- ...portEnv(serverPort, cfg.server.port, 'NEXUS_SERVER_PORT'),
63
- ...portEnv(lbPort, cfg.cluster.lbPort, 'NEXUS_CLUSTER_LBPORT'),
64
- ...portEnv(agentPort, cfg.cluster.nodeAgentPort, 'NEXUS_CLUSTER_NODEAGENTPORT'),
65
- },
66
- },
67
- {
68
- name: 'frontend',
69
- command: ['vite', '--port', String(frontendPort), '--host', cfg.frontend.host],
70
- cwd: 'apps/frontend',
71
- color: COLORS.frontend,
72
- optional: true,
73
- env: {
74
- ...portEnv(serverPort, cfg.server.port, 'NEXUS_SERVER_PORT'),
75
- ...portEnv(frontendPort, cfg.frontend.port, 'NEXUS_FRONTEND_PORT'),
76
- },
77
- },
78
- {
79
- name: 'ai-server',
80
- command: ['python', 'main.py'],
81
- cwd: 'apps/ai-server',
82
- color: COLORS['ai-server'],
83
- optional: true,
84
- env: { AI_PORT: String(aiServerPort) },
85
- },
86
- {
87
- name: 'admin',
88
- command: ['vite', '--port', String(adminPort), '--host', cfg.admin.host],
89
- cwd: 'apps/admin',
90
- color: COLORS.admin,
91
- optional: true,
92
- env: {
93
- ...portEnv(serverPort, cfg.server.port, 'NEXUS_SERVER_PORT'),
94
- ...portEnv(adminPort, cfg.admin.port, 'NEXUS_ADMIN_PORT'),
95
- },
96
- },
97
- ];
98
-
99
- // Slave/node mode: auto-start a node agent (agent-only — it advertises the dev
100
- // supervisor's backend as its role service instead of spawning a second one).
101
- // The master can then link this node via its agent URL right after `npm run dev`.
102
- // Invoked through `node node_modules/bhooai-nexus/bin/nexus.js` so it resolves
103
- // even when the `nexus` bin shim isn't linked into the project's .bin.
104
- if (cfg.cluster.role) {
105
- all.push({
106
- name: 'node-agent',
107
- command: ['node', 'node_modules/bhooai-nexus/bin/nexus.js', 'node', 'serve', `--role=${cfg.cluster.role}`, `--port=${agentPort}`, '--no-service'],
108
- cwd: '',
109
- color: COLORS['node-agent'],
110
- env: {
111
- ...portEnv(serverPort, cfg.server.port, 'NEXUS_SERVER_PORT'),
112
- ...portEnv(agentPort, cfg.cluster.nodeAgentPort, 'NEXUS_CLUSTER_NODEAGENTPORT'),
113
- },
57
+ for (const f of frontends) {
58
+ if (!existsSync(join(appsDir, f, 'package.json'))) continue;
59
+ const port = await ensurePort(projectRoot, f);
60
+ services.push({
61
+ name: f,
62
+ cmd: 'npx',
63
+ args: ['vite', '--port', String(port), '--strictPort'],
64
+ cwd: join(appsDir, f),
65
+ color: COLORS[colorIdx++ % COLORS.length]!,
66
+ enabled: !only || only.includes(f),
67
+ port,
114
68
  });
115
69
  }
116
-
117
- // Drop services the config disables (unless an explicit --only pins them).
118
- const enabled = all.filter((s) => {
119
- if (only) return only.includes(s.name);
120
- if (s.name === 'frontend') return cfg.frontend.enabled;
121
- if (s.name === 'admin') return cfg.admin.enabled;
122
- return true;
123
- });
124
- const services = enabled;
125
-
126
- // Startup banner - show host:port for every service about to start.
127
- process.stdout.write(`\n${BOLD} * BhooAI Nexus${RESET} - ${services.map((s) => s.name).join(', ')}\n\n`);
128
- process.stdout.write(` ${DIM}Service Host Port${RESET}\n`);
129
- process.stdout.write(` ${DIM}----------- --------- ----${RESET}\n`);
130
-
131
- for (const s of services) {
132
- let host = '';
133
- let port = '';
134
- switch (s.name) {
135
- case 'backend':
136
- host = cfg.server.host;
137
- port = String(serverPort);
138
- break;
139
- case 'frontend':
140
- host = cfg.frontend.host;
141
- port = String(frontendPort);
142
- break;
143
- case 'admin':
144
- host = cfg.admin.host;
145
- port = String(adminPort);
146
- break;
147
- case 'ai-server':
148
- host = '0.0.0.0';
149
- port = String(aiServerPort);
150
- break;
151
- case 'node-agent':
152
- host = cfg.cluster.nodeAgentHost;
153
- port = String(agentPort);
154
- break;
155
- }
156
- const padName = s.name.padEnd(15);
157
- const padHost = host.padEnd(16);
158
- process.stdout.write(` ${s.color}${padName}${RESET}${padHost}${port}\n`);
70
+ for (const a of admins) {
71
+ if (!existsSync(join(appsDir, a, 'package.json'))) continue;
72
+ const port = await ensurePort(projectRoot, a);
73
+ services.push({
74
+ name: a,
75
+ cmd: 'npx',
76
+ args: ['vite', '--port', String(port), '--strictPort'],
77
+ cwd: join(appsDir, a),
78
+ color: COLORS[colorIdx++ % COLORS.length]!,
79
+ enabled: !only || only.includes(a),
80
+ port,
81
+ });
82
+ }
83
+ for (const ai of aiServers) {
84
+ const mainPy = join(appsDir, ai, 'main.py');
85
+ if (!existsSync(mainPy)) continue;
86
+ const port = await ensurePort(projectRoot, ai);
87
+ services.push({
88
+ name: ai,
89
+ cmd: 'python',
90
+ args: ['main.py'],
91
+ cwd: join(appsDir, ai),
92
+ color: COLORS[colorIdx++ % COLORS.length]!,
93
+ enabled: !only || only.includes(ai),
94
+ port,
95
+ });
159
96
  }
160
- process.stdout.write(`\n`);
161
97
 
162
- const supervisor = new Supervisor(services, process.cwd(), controlPort);
163
- await supervisor.start();
98
+ const active = services.filter((s) => s.enabled);
99
+ if (active.length === 0) {
100
+ console.log('No services to start. Check that apps/* exist.');
101
+ return;
102
+ }
164
103
 
165
- // After services spawn, print quick-start URLs.
166
- setTimeout(() => {
167
- process.stdout.write(`\n ${BOLD}Visit:${RESET}\n`);
168
- process.stdout.write(` http://localhost:${frontendPort} (frontend)\n`);
169
- process.stdout.write(` http://localhost:${adminPort} (admin)\n`);
170
- process.stdout.write(` http://localhost:${serverPort}/health (backend)\n`);
171
- if (cfg.cluster.role) {
172
- process.stdout.write(`\n ${BOLD}Node agent${RESET} link this from the master:\n`);
173
- process.stdout.write(` http://${cfg.cluster.nodeAgentHost}:${agentPort} (agent) · role: ${cfg.cluster.role}\n`);
104
+ console.log(`nexus dev starting ${active.length} service(s)\n`);
105
+ for (const s of active) {
106
+ console.log(` ${s.color}[${s.name}]${RESET} → :${s.port}`);
107
+ }
108
+ console.log('');
109
+
110
+ const procs: ChildProcess[] = [];
111
+ let shuttingDown = false;
112
+
113
+ const shutdown = () => {
114
+ if (shuttingDown) return;
115
+ shuttingDown = true;
116
+ console.log('\nShutting down...');
117
+ for (const p of procs) {
118
+ try {
119
+ p.kill('SIGTERM');
120
+ } catch { /* ignore */ }
174
121
  }
175
- process.stdout.write(`\n`);
176
- }, 2000);
177
-
178
- // Keep the process alive; supervisor handles SIGINT/SIGTERM.
179
- return 0;
180
- }
122
+ setTimeout(() => process.exit(0), 500);
123
+ };
124
+
125
+ process.on('SIGINT', shutdown);
126
+ process.on('SIGTERM', shutdown);
127
+
128
+ for (const svc of active) {
129
+ const child = spawn(svc.cmd, svc.args, {
130
+ cwd: svc.cwd,
131
+ stdio: ['inherit', 'pipe', 'pipe'],
132
+ env: { ...process.env, FORCE_COLOR: '1', NEXUS_PORT: String(svc.port) },
133
+ shell: true,
134
+ });
181
135
 
182
- /** First free port at or above `port` on `host` (fails fast, caps at +100). */
183
- async function nextFreePort(host: string, port: number): Promise<number> {
184
- const base = Number(port);
185
- if (await isPortFree(host, base)) return base;
186
- for (let i = 1; i <= 100; i++) {
187
- if (await isPortFree(host, base + i)) return base + i;
136
+ const prefix = `${svc.color}[${svc.name}]${RESET}`;
137
+ child.stdout?.on('data', (chunk: Buffer) => {
138
+ const lines = chunk.toString().split('\n');
139
+ for (const line of lines) {
140
+ if (line.trim()) process.stdout.write(`${prefix} ${line}\n`);
141
+ }
142
+ });
143
+ child.stderr?.on('data', (chunk: Buffer) => {
144
+ const lines = chunk.toString().split('\n');
145
+ for (const line of lines) {
146
+ if (line.trim()) process.stderr.write(`${prefix} ${line}\n`);
147
+ }
148
+ });
149
+ child.on('exit', (code, signal) => {
150
+ if (shuttingDown) return;
151
+ console.log(`${prefix} exited (${code ?? signal}). Restarting in 1s...`);
152
+ setTimeout(() => {
153
+ if (!shuttingDown) {
154
+ procs.push(spawn(svc.cmd, svc.args, { cwd: svc.cwd, stdio: 'inherit', env: process.env, shell: true }));
155
+ }
156
+ }, 1000);
157
+ });
158
+ procs.push(child);
188
159
  }
189
- return base;
190
160
  }
191
161
 
192
- /** Port parsed from the configured AI server URL (defaults 8000). */
193
- function aiPort(serverUrl: string): string {
194
- try {
195
- const port = new URL(serverUrl).port;
196
- return port || '80';
197
- } catch {
198
- return '8000';
199
- }
200
- }
162
+ export const description = 'Start dev services (all apps under ./apps)';
163
+ export const usage = 'nexus dev [--only backend,frontend]';