@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,199 +1,117 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { homedir } from 'node:os';
4
- import { loadConfigAuto } from '../../../nexus-core/src/index.js';
5
- import { CheckResult, scanRuntimes, scanServices } from '../util.js';
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 BOLD = '\x1b[1m';
13
- const RESET = '\x1b[0m';
14
-
15
- /** Critical dependency names that must be present. */
16
- const CRITICAL = new Set(['node', 'config', 'jwt-secret', 'jwt token', 'package.json', 'nexus.config']);
17
-
18
- /** Compute total directory size in bytes (shallow - does not recurse). */
19
- function dirSize(dir: string): number {
20
- try {
21
- let size = 0;
22
- for (const entry of readdirSync(dir)) {
23
- try { size += statSync(join(dir, entry)).size; } catch { /* skip */ }
24
- }
25
- return size;
26
- } catch {
27
- return 0;
28
- }
1
+ /**
2
+ * nexus doctor environment health check. Never hard-fails; reports issues.
3
+ */
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { existsSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { createConnection } from 'node:net';
9
+ import type { CommandContext } from '../dispatcher.js';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ interface Check {
14
+ name: string;
15
+ run(): Promise<{ ok: boolean; detail?: string }>;
29
16
  }
30
17
 
31
- /** Check whether a directory exists and contains at least one entry. */
32
- function dirPopulated(root: string, rel: string): { ok: boolean; detail: string } {
33
- const abs = join(root, rel);
34
- if (!existsSync(abs)) return { ok: false, detail: `${rel}/ missing` };
18
+ async function cmdVersion(cmd: string, args: string[] = ['--version']): Promise<string | null> {
35
19
  try {
36
- const count = readdirSync(abs).filter((e) => e !== '.gitkeep').length;
37
- return { ok: count > 0, detail: count > 0 ? `${count} file(s)` : 'empty' };
20
+ const { stdout } = await execAsync(`${cmd} ${args.join(' ')}`);
21
+ return stdout.trim().split('\n')[0] ?? null;
38
22
  } catch {
39
- return { ok: false, detail: `${rel}/ unreadable` };
23
+ return null;
40
24
  }
41
25
  }
42
26
 
43
- /** Check whether node_modules exists and has at least 10 packages. */
44
- function depsInstalled(root: string, rel: string): { ok: boolean; detail: string } {
45
- const mod = join(root, rel, 'node_modules');
46
- if (!existsSync(mod)) return { ok: false, detail: `${rel}/node_modules missing` };
47
- try {
48
- const count = readdirSync(mod).filter((e) => !e.startsWith('.')).length;
49
- return { ok: count >= 10, detail: count >= 10 ? `${count} pkgs` : `only ${count} pkgs` };
50
- } catch {
51
- return { ok: false, detail: `${rel}/node_modules unreadable` };
52
- }
27
+ async function tcpReachable(host: string, port: number, timeoutMs: number = 1000): Promise<boolean> {
28
+ return new Promise((resolveOut) => {
29
+ const socket = createConnection({ host, port, timeout: timeoutMs });
30
+ socket.once('connect', () => {
31
+ socket.end();
32
+ resolveOut(true);
33
+ });
34
+ socket.once('error', () => resolveOut(false));
35
+ socket.once('timeout', () => {
36
+ socket.destroy();
37
+ resolveOut(false);
38
+ });
39
+ });
53
40
  }
54
41
 
55
- /** Verify the environment comprehensively. */
56
- export async function doctor(): Promise<number> {
57
- const root = process.cwd();
58
- const results: CheckResult[] = [];
59
-
60
- // -- runtime versions (shared with `nexus init`) ----------------
61
- const runtimes = await scanRuntimes();
62
- results.push(...runtimes.map((r) => ({ name: r.name, ok: r.ok, detail: r.detail })));
63
-
64
- // -- project files ------------------------------------------------
65
- const pkgJson = existsSync(join(root, 'package.json'));
66
- results.push({ name: 'package.json', ok: pkgJson, detail: pkgJson ? 'exists' : 'missing - not a nexus project' });
67
-
68
- const configExists = ['ts', 'js', 'mjs', 'cjs'].some((ext) => existsSync(join(root, `nexus.config.${ext}`)));
69
- const configExt = ['ts', 'js', 'mjs', 'cjs'].find((ext) => existsSync(join(root, `nexus.config.${ext}`)));
70
- results.push({ name: 'nexus.config', ok: configExists, detail: configExists ? `nexus.config.${configExt}` : 'missing' });
71
-
72
- const envExists = existsSync(join(root, '.env'));
73
- results.push({ name: '.env', ok: envExists, detail: envExists ? 'exists' : 'missing - secrets not loaded' });
74
-
75
- // -- project structure -------------------------------------------
76
- const structDirs: Array<{ rel: string; label: string }> = [
77
- { rel: 'apps/backend', label: 'apps/backend' },
78
- { rel: 'apps/frontend', label: 'apps/frontend' },
79
- { rel: 'apps/admin', label: 'apps/admin' },
80
- { rel: 'apps/ai-server', label: 'ai-server' },
81
- { rel: 'packages', label: 'packages' },
82
- { rel: 'bin', label: 'bin' },
83
- { rel: 'uploads', label: 'uploads' },
84
- { rel: 'plugins', label: 'plugins' },
85
- { rel: 'certs', label: 'certs' },
86
- { rel: 'logs', label: 'logs' },
42
+ export async function run(_ctx: CommandContext): Promise<void> {
43
+ console.log('nexus doctor environment check\n');
44
+
45
+ const projectRoot = process.cwd();
46
+ const checks: Check[] = [
47
+ {
48
+ name: 'Node.js 20.10',
49
+ run: async () => {
50
+ const v = await cmdVersion('node');
51
+ if (!v) return { ok: false, detail: 'node not in PATH' };
52
+ const m = v.match(/v?(\d+)\.(\d+)\.(\d+)/);
53
+ if (!m) return { ok: true, detail: v };
54
+ const major = parseInt(m[1]!, 10);
55
+ const minor = parseInt(m[2]!, 10);
56
+ const ok = major > 20 || (major === 20 && minor >= 10);
57
+ return { ok, detail: v };
58
+ },
59
+ },
60
+ {
61
+ name: 'npm',
62
+ run: async () => {
63
+ const v = await cmdVersion('npm');
64
+ return v ? { ok: true, detail: v } : { ok: false, detail: 'npm not in PATH' };
65
+ },
66
+ },
67
+ {
68
+ name: 'Python (optional — for AI server)',
69
+ run: async () => {
70
+ const v = await cmdVersion('python', ['--version']);
71
+ return v ? { ok: true, detail: v } : { ok: false, detail: 'python not found (optional)' };
72
+ },
73
+ },
74
+ {
75
+ name: 'Project layout (apps/backend)',
76
+ run: async () => ({
77
+ ok: existsSync(resolve(projectRoot, 'apps/backend')),
78
+ detail: existsSync(resolve(projectRoot, 'apps/backend')) ? 'found' : 'apps/backend missing',
79
+ }),
80
+ },
81
+ {
82
+ name: 'nexus.config.ts',
83
+ run: async () => ({
84
+ ok: existsSync(resolve(projectRoot, 'nexus.config.ts')),
85
+ detail: existsSync(resolve(projectRoot, 'nexus.config.ts')) ? 'found' : 'missing',
86
+ }),
87
+ },
88
+ {
89
+ name: 'MongoDB reachable (localhost:27017)',
90
+ run: async () => ({
91
+ ok: await tcpReachable('127.0.0.1', 27017),
92
+ detail: (await tcpReachable('127.0.0.1', 27017)) ? 'ok' : 'not reachable',
93
+ }),
94
+ },
95
+ {
96
+ name: 'Redis reachable (localhost:6379)',
97
+ run: async () => ({
98
+ ok: await tcpReachable('127.0.0.1', 6379),
99
+ detail: (await tcpReachable('127.0.0.1', 6379)) ? 'ok' : 'not reachable (optional)',
100
+ }),
101
+ },
87
102
  ];
88
- for (const d of structDirs) {
89
- const r = dirPopulated(root, d.rel);
90
- results.push({ name: d.label, ok: r.ok, detail: r.detail });
91
- }
92
-
93
- // -- dependencies ------------------------------------------------
94
- const depTargets = [
95
- { rel: '', label: 'root deps' },
96
- { rel: 'apps/backend', label: 'backend deps' },
97
- { rel: 'apps/frontend', label: 'frontend deps' },
98
- { rel: 'apps/admin', label: 'admin deps' },
99
- ];
100
- for (const d of depTargets) {
101
- const r = depsInstalled(root, d.rel);
102
- results.push({ name: d.label, ok: r.ok, detail: r.detail });
103
- }
104
103
 
105
- // -- disk space ---------------------------------------------------
106
- const uploadsDir = join(root, 'uploads');
107
- const uploadsSize = dirSize(uploadsDir);
108
- results.push({ name: 'uploads size', ok: true, detail: existsSync(uploadsDir) ? `${(uploadsSize / 1024 / 1024).toFixed(1)} MB` : 'no dir' });
109
-
110
- const logsDir = join(root, 'logs');
111
- const logsSize = dirSize(logsDir);
112
- results.push({ name: 'logs size', ok: true, detail: existsSync(logsDir) ? `${(logsSize / 1024 / 1024).toFixed(1)} MB` : 'no dir' });
113
-
114
- // -- config + services --------------------------------------------
115
- try {
116
- const cfg = await loadConfigAuto({ root });
117
-
118
- // config validation
119
- results.push({ name: 'config load', ok: true, detail: `env=${cfg.env} server=${cfg.server.host}:${cfg.server.port}` });
120
-
121
- // -- services (shared with `nexus init`) -----------------------
122
- const services = await scanServices(cfg);
123
- results.push(...services.map((r) => ({ name: r.name, ok: r.ok, detail: r.detail })));
124
-
125
- // jwt secret + token round-trip
126
- if (cfg.auth.jwt.secret === 'change-me-please') {
127
- results.push({ name: 'jwt-secret', ok: false, detail: 'still the default - set NEXUS_AUTH_JWT_SECRET in .env' });
128
- results.push({ name: 'jwt token', ok: false, detail: 'skipped - secret not configured' });
129
- } else {
130
- results.push({ name: 'jwt-secret', ok: true, detail: 'configured' });
131
- // Sign + verify a test access token to validate the JWT pipeline end-to-end.
132
- try {
133
- const { SignJWT, jwtVerify } = await import('jose');
134
- const testToken = await new SignJWT({ sub: 'doctor-test', roles: ['admin'], iat: Math.floor(Date.now() / 1000) })
135
- .setProtectedHeader({ alg: 'HS256' })
136
- .setIssuer(cfg.auth.jwt.issuer ?? 'bhooai-nexus')
137
- .setAudience(cfg.auth.jwt.audience ?? 'bhooai-nexus-client')
138
- .setExpirationTime('15m')
139
- .sign(new TextEncoder().encode(cfg.auth.jwt.secret));
140
- const { payload } = await jwtVerify(testToken, new TextEncoder().encode(cfg.auth.jwt.secret));
141
- const ok = payload.sub === 'doctor-test' && payload.roles?.[0] === 'admin';
142
- results.push({ name: 'jwt token', ok, detail: ok ? `HS256 sign+verify OK (sub=${payload.sub})` : 'token payload mismatch' });
143
- } catch (err) {
144
- results.push({ name: 'jwt token', ok: false, detail: `sign/verify failed - ${(err as Error).message}` });
145
- }
146
- }
147
-
148
- // graphql + ws paths
149
- results.push({ name: 'graphql', ok: true, detail: `path=${cfg.graphql.path} federation=${cfg.graphql.federation}` });
150
- results.push({ name: 'websocket', ok: true, detail: `path=${cfg.ws.path} csrf=${cfg.ws.requireCsrf}` });
151
-
152
- // frontend/admin enabled
153
- results.push({ name: 'frontend', ok: cfg.frontend.enabled, detail: cfg.frontend.enabled ? `enabled :${cfg.frontend.port}` : 'disabled' });
154
- results.push({ name: 'admin', ok: cfg.admin.enabled, detail: cfg.admin.enabled ? `enabled :${cfg.admin.port}` : 'disabled' });
155
-
156
- // cluster
157
- results.push({ name: 'cluster', ok: true, detail: cfg.cluster.enabled ? `enabled (LB :${cfg.cluster.lbPort})` : 'disabled' });
158
-
159
- // payments
160
- results.push({ name: 'payments', ok: true, detail: `currency=${cfg.payments.currency}` });
161
-
162
- // email
163
- results.push({ name: 'email', ok: true, detail: `provider=${cfg.email.provider}` });
164
- } catch (err) {
165
- results.push({ name: 'config', ok: false, detail: String((err as Error).message) });
166
- }
167
-
168
- // -- render ------------------------------------------------------
169
- console.log(`\n${BOLD} * BhooAI Nexus Doctor${RESET} ${DIM}${new Date().toLocaleString()}${RESET}\n`);
170
-
171
- let criticalOk = true;
172
- let warnings = 0;
173
- let passed = 0;
174
104
  let failed = 0;
175
-
176
- for (const r of results) {
177
- const icon = r.ok ? `${GREEN}[OK]${RESET}` : `${RED}[X]${RESET}`;
178
- const label = r.name.padEnd(16);
179
- if (r.ok) {
180
- passed++;
181
- console.log(` ${icon} ${CYAN}${label}${RESET} ${DIM}${r.detail}${RESET}`);
182
- } else {
183
- failed++;
184
- const warn = CRITICAL.has(r.name) ? RED : YELLOW;
185
- if (!CRITICAL.has(r.name)) warnings++;
186
- console.log(` ${icon} ${warn}${label}${RESET} ${r.detail}`);
187
- if (CRITICAL.has(r.name)) criticalOk = false;
188
- }
105
+ for (const check of checks) {
106
+ const result = await check.run().catch((e) => ({ ok: false, detail: String(e) }));
107
+ const icon = result.ok ? '✓' : '✗';
108
+ const detail = result.detail ? ` (${result.detail})` : '';
109
+ console.log(`${icon} ${check.name}${detail}`);
110
+ if (!result.ok) failed++;
189
111
  }
190
112
 
191
- console.log(`\n ${BOLD}${passed} passed${RESET}, ${YELLOW}${warnings} warnings${RESET}, ${RED}${failed - warnings} errors${RESET}\n`);
192
-
193
- if (criticalOk) {
194
- console.log(`${GREEN} Nexus environment looks ready.${RESET}\n`);
195
- } else {
196
- console.log(`${RED} Critical issues found - cannot start.${RESET}\n`);
197
- }
198
- return 0; // doctor never hard-fails
113
+ console.log(failed === 0 ? '\nAll checks passed.' : `\n${failed} check(s) need attention.`);
199
114
  }
115
+
116
+ export const description = 'Environment health check';
117
+ export const usage = 'nexus doctor';