@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.
- 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/doctor.ts
CHANGED
|
@@ -1,199 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
return
|
|
20
|
+
const { stdout } = await execAsync(`${cmd} ${args.join(' ')}`);
|
|
21
|
+
return stdout.trim().split('\n')[0] ?? null;
|
|
38
22
|
} catch {
|
|
39
|
-
return
|
|
23
|
+
return null;
|
|
40
24
|
}
|
|
41
25
|
}
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
177
|
-
const icon =
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
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';
|