@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
package/package.json CHANGED
@@ -1,27 +1,26 @@
1
- {
2
- "name": "@bhooai/nexus-cli",
3
- "version": "0.1.5",
4
- "publishConfig": {
5
- "access": "public"
6
- },
7
- "type": "module",
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
10
- "bin": {
11
- "nexus": "./bin/nexus.js"
12
- },
13
- "scripts": {
14
- "build": "tsc -p tsconfig.json",
15
- "test": "vitest run"
16
- },
17
- "dependencies": {
18
- "@bhooai/nexus-cluster": "^0.1.0",
19
- "@bhooai/nexus-core": "^0.1.0",
20
- "@bhooai/nexus-data": "^0.1.0"
21
- },
22
- "devDependencies": {
23
- "@types/node": "^22.5.0",
24
- "typescript": "^5.6.2",
25
- "vitest": "^2.1.1"
26
- }
27
- }
1
+ {
2
+ "name": "@bhooai/nexus-cli",
3
+ "version": "2.0.1",
4
+ "description": "BhooAI Nexus v2 CLI — init, dev, build, test, scaffolding, plugins, queue, db.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "nexus": "./bin/nexus.js"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@bhooai/nexus-core": "^2.0.1",
16
+ "tsx": "^4.19.0",
17
+ "zod": "^3.23.8"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.5.0",
21
+ "typescript": "^5.6.2"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ }
26
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * nexus add — multi-app scaffolds (backend, frontend, route, module).
3
+ *
4
+ * nexus add backend <name>
5
+ * nexus add frontend <name> [--for <backend>]
6
+ * nexus add route <name> --app <backend>
7
+ * nexus add module <payments|ai|admin|uploads|graphql-subgraph>
8
+ */
9
+ import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import { resolve, join, dirname } from 'node:path';
12
+ import type { CommandContext } from '../dispatcher.js';
13
+ import { slugify } from '../util.js';
14
+ import { prompt, select, isInteractive } from '../prompts.js';
15
+ import { ensurePort, lookupPort } from '../ports.js';
16
+ import { BACKEND_FOLDERS } from './init.js';
17
+
18
+ const BACKEND_MAIN_TEMPLATE = `import { createNexusApp } from '@bhooai/nexus-core';
19
+ import { dirname, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+
24
+ const app = await createNexusApp({
25
+ name: '<%= appName %>',
26
+ srcRoot: HERE,
27
+ projectRoot: resolve(HERE, '..'),
28
+ });
29
+
30
+ await app.listen();
31
+ console.log('[<%= appName %>] http://' + app.config.server.host + ':' + app.config.server.port);
32
+ `;
33
+
34
+ const BACKEND_ROUTE_TEMPLATE = `import { defineRoutes } from '@bhooai/nexus-core';
35
+
36
+ export default defineRoutes([
37
+ {
38
+ method: 'GET',
39
+ path: '/',
40
+ handler: (ctx) => ctx.json({ ok: true, app: '<%= appName %>' }),
41
+ },
42
+ ]);
43
+ `;
44
+
45
+ const ROUTE_TEMPLATE = `import { defineRoutes } from '@bhooai/nexus-core';
46
+
47
+ export default defineRoutes([
48
+ { method: 'GET', path: '/<%= name %>', handler: list },
49
+ { method: 'POST', path: '/<%= name %>', handler: create },
50
+ { method: 'GET', path: '/<%= name %>/:id', handler: getOne },
51
+ { method: 'PATCH', path: '/<%= name %>/:id', handler: update },
52
+ { method: 'DELETE', path: '/<%= name %>/:id', handler: remove },
53
+ ]);
54
+
55
+ // Handlers -- replace with real implementations
56
+ import type { RequestContext } from '@bhooai/nexus-core';
57
+
58
+ function list(_ctx: RequestContext) { _ctx.json([]); }
59
+ function create(ctx: RequestContext) { ctx.json({ created: true }, 201); }
60
+ function getOne(ctx: RequestContext) { ctx.json({ id: ctx.params.id }); }
61
+ function update(ctx: RequestContext) { ctx.json({ id: ctx.params.id, updated: true }); }
62
+ function remove(ctx: RequestContext) { ctx.status(204); }
63
+ `;
64
+
65
+ export async function run(ctx: CommandContext): Promise<void> {
66
+ const sub = ctx.args._[0];
67
+ if (!sub) {
68
+ console.error('Usage: nexus add <backend|frontend|route|module> [args]');
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ switch (sub) {
74
+ case 'backend': return addBackend(ctx);
75
+ case 'frontend': return addFrontend(ctx);
76
+ case 'route': return addRoute(ctx);
77
+ case 'module': return addModule(ctx);
78
+ default:
79
+ console.error(`Unknown add subcommand: ${sub}`);
80
+ process.exitCode = 1;
81
+ }
82
+ }
83
+
84
+ async function addBackend(ctx: CommandContext): Promise<void> {
85
+ const name = ctx.args._[1];
86
+ if (!name) {
87
+ console.error('Usage: nexus add backend <name>');
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ const slug = slugify(name);
92
+ const appName = `backend-${slug}`;
93
+ const projectRoot = process.cwd();
94
+ const appDir = join(projectRoot, 'apps', appName);
95
+
96
+ if (existsSync(appDir)) {
97
+ console.error(`✗ ${appName} already exists`);
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+
102
+ const port = await ensurePort(projectRoot, appName);
103
+
104
+ // Create directory structure (shared with init.ts's BACKEND_FOLDERS constant)
105
+ for (const f of BACKEND_FOLDERS) {
106
+ await mkdir(join(appDir, f), { recursive: true });
107
+ }
108
+
109
+ // main.ts — pass the assigned port via env (NEXUS_PORT) so dev and prod use the same.
110
+ await writeFile(join(appDir, 'src', 'main.ts'),
111
+ BACKEND_MAIN_TEMPLATE.replace(/<%= appName %>/g, appName));
112
+
113
+ // routes/index.ts
114
+ await writeFile(join(appDir, 'src', 'routes', 'index.ts'),
115
+ BACKEND_ROUTE_TEMPLATE.replace(/<%= appName %>/g, appName));
116
+
117
+ // package.json
118
+ await writeFile(join(appDir, 'package.json'), JSON.stringify({
119
+ name: `@${slug}/${appName}`,
120
+ private: true,
121
+ version: '0.1.0',
122
+ type: 'module',
123
+ scripts: { dev: 'tsx watch src/main.ts' },
124
+ }, null, 2));
125
+
126
+ console.log(`✓ Created apps/${appName}`);
127
+ console.log(` srcRoot: apps/${appName}/src`);
128
+ console.log(` port: ${port} (persisted in .nexus-ports.json)`);
129
+ console.log(`\nAdd to apps/dev list when you want \`nexus dev\` to pick it up.`);
130
+ }
131
+
132
+ async function addFrontend(ctx: CommandContext): Promise<void> {
133
+ const name = ctx.args._[1];
134
+ if (!name) {
135
+ console.error('Usage: nexus add frontend <name> [--for <backend>]');
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ const slug = slugify(name);
140
+ const appName = `frontend-${slug}`;
141
+ const projectRoot = process.cwd();
142
+ const appDir = join(projectRoot, 'apps', appName);
143
+
144
+ if (existsSync(appDir)) {
145
+ console.error(`✗ ${appName} already exists`);
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+
150
+ // Pick backend target
151
+ let backend = ctx.args.flags.for as string | undefined;
152
+ if (!backend && isInteractive()) {
153
+ backend = await pickBackend(projectRoot);
154
+ }
155
+ backend = backend ?? 'backend-main';
156
+
157
+ const port = await ensurePort(projectRoot, appName);
158
+
159
+ await mkdir(join(appDir, 'src'), { recursive: true });
160
+
161
+ // package.json
162
+ await writeFile(join(appDir, 'package.json'), JSON.stringify({
163
+ name: `@${slug}/${appName}`,
164
+ private: true,
165
+ version: '0.1.0',
166
+ type: 'module',
167
+ scripts: {
168
+ dev: `vite --port ${port}`,
169
+ build: 'vite build',
170
+ },
171
+ }, null, 2));
172
+
173
+ // vite.config.ts — resolve the backend's port from the registry.
174
+ const backendPort = (await lookupPort(projectRoot, backend)) ?? await ensurePort(projectRoot, backend);
175
+ await writeFile(join(appDir, 'vite.config.ts'), `import { defineConfig } from 'vite';
176
+
177
+ export default defineConfig({
178
+ server: {
179
+ port: ${port},
180
+ proxy: {
181
+ '/api': { target: 'http://localhost:${backendPort}', changeOrigin: true, rewrite: (p: string) => p.replace(/^\\/api/, '') },
182
+ '/uploads': { target: 'http://localhost:${backendPort}', changeOrigin: true },
183
+ '/ws': { target: 'ws://localhost:${backendPort}', ws: true },
184
+ },
185
+ },
186
+ });
187
+ `);
188
+
189
+ // index.html + main.tsx
190
+ await writeFile(join(appDir, 'index.html'), `<!doctype html>
191
+ <html lang="en">
192
+ <head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>${name}</title></head>
193
+ <body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
194
+ </html>
195
+ `);
196
+
197
+ await writeFile(join(appDir, 'src', 'main.tsx'), `import React from 'react';
198
+ import { createRoot } from 'react-dom/client';
199
+
200
+ function App() {
201
+ return <main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
202
+ <h1>${name}</h1>
203
+ <p>Served by apps/${appName}; proxying API to ${backend}.</p>
204
+ </main>;
205
+ }
206
+
207
+ const el = document.getElementById('root');
208
+ if (el) createRoot(el).render(<App />);
209
+ `);
210
+
211
+ console.log(`✓ Created apps/${appName} (port ${port}, proxying to ${backend}:${backendPort})`);
212
+ }
213
+
214
+ async function addRoute(ctx: CommandContext): Promise<void> {
215
+ const name = ctx.args._[1];
216
+ const app = (ctx.args.flags.app as string) ?? 'backend';
217
+ if (!name) {
218
+ console.error('Usage: nexus add route <name> [--app <backend>]');
219
+ process.exitCode = 1;
220
+ return;
221
+ }
222
+ const slug = slugify(name);
223
+ const projectRoot = process.cwd();
224
+ const routeDir = join(projectRoot, 'apps', app, 'src', 'routes');
225
+
226
+ if (!existsSync(dirname(routeDir))) {
227
+ console.error(`✗ Backend "${app}" not found. Run \`nexus add backend ${app}\` first.`);
228
+ process.exitCode = 1;
229
+ return;
230
+ }
231
+
232
+ await mkdir(routeDir, { recursive: true });
233
+ const target = join(routeDir, `${slug}.ts`);
234
+ if (existsSync(target)) {
235
+ console.error(`✗ Route "${slug}" already exists`);
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+
240
+ await writeFile(target, ROUTE_TEMPLATE.replace(/<%= name %>/g, slug));
241
+ console.log(`✓ Created apps/${app}/src/routes/${slug}.ts`);
242
+ }
243
+
244
+ async function addModule(ctx: CommandContext): Promise<void> {
245
+ const name = ctx.args._[1];
246
+ if (!name) {
247
+ console.error('Usage: nexus add module <payments|ai|admin|uploads|graphql-subgraph>');
248
+ process.exitCode = 1;
249
+ return;
250
+ }
251
+ console.log(`Module "${name}" — TBD in P6. Will scaffold a prewired set of routes/services/graphql for that feature.`);
252
+ }
253
+
254
+ async function pickBackend(projectRoot: string): Promise<string> {
255
+ const apps = await listBackends(projectRoot);
256
+ if (apps.length === 0) return 'backend';
257
+ if (apps.length === 1) return apps[0]!;
258
+ return select('Pick a backend to proxy to:', apps);
259
+ }
260
+
261
+ async function listBackends(projectRoot: string): Promise<string[]> {
262
+ const appsDir = join(projectRoot, 'apps');
263
+ if (!existsSync(appsDir)) return [];
264
+ const entries = await readdir(appsDir, { withFileTypes: true });
265
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith('backend')).map((e) => e.name);
266
+ }
267
+
268
+ async function getBackendPort(_projectRoot: string, _backend: string): Promise<number> {
269
+ // Deprecrated — superseded by lookupPort/ensurePort.
270
+ return 4000;
271
+ }
272
+
273
+ export const description = 'Add backend, frontend, route, or feature module to the project';
274
+ export const usage = 'nexus add <backend|frontend|route|module> ...';
@@ -0,0 +1,91 @@
1
+ /**
2
+ * nexus build — production build for all apps or a named target.
3
+ *
4
+ * Builds backend (tsc + prune), frontend (vite build), admin (vite build),
5
+ * and produces a docker-ready dist/ tree at the project root.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
9
+ import { readdir, mkdir } from 'node:fs/promises';
10
+ import { resolve, join } from 'node:path';
11
+ import type { CommandContext } from '../dispatcher.js';
12
+
13
+ interface BuildTarget {
14
+ name: string;
15
+ path: string;
16
+ buildCmd: { cmd: string; args: string[] };
17
+ }
18
+
19
+ export async function run(ctx: CommandContext): Promise<void> {
20
+ const target = (ctx.args.flags.target as string) ?? 'all';
21
+ const projectRoot = process.cwd();
22
+
23
+ const targets: BuildTarget[] = await resolveTargets(target, projectRoot);
24
+
25
+ if (targets.length === 0) {
26
+ console.log('No buildable targets found.');
27
+ return;
28
+ }
29
+
30
+ await mkdir(join(projectRoot, 'dist'), { recursive: true });
31
+
32
+ for (const t of targets) {
33
+ console.log(`\n▶ ${t.name}`);
34
+ const code = await runCmd(t.buildCmd.cmd, t.buildCmd.args, t.path);
35
+ if (code !== 0) {
36
+ console.error(`✗ ${t.name} build failed (${code})`);
37
+ process.exitCode = code ?? 1;
38
+ return;
39
+ }
40
+ console.log(`✓ ${t.name}`);
41
+ }
42
+
43
+ console.log('\nBuild complete → dist/');
44
+ }
45
+
46
+ async function resolveTargets(target: string, root: string): Promise<BuildTarget[]> {
47
+ const appsDir = join(root, 'apps');
48
+ if (!existsSync(appsDir)) return [];
49
+ const entries = await readdir(appsDir, { withFileTypes: true });
50
+
51
+ const all: BuildTarget[] = [];
52
+ for (const e of entries) {
53
+ if (!e.isDirectory()) continue;
54
+ const appPath = join(appsDir, e.name);
55
+ const pkg = join(appPath, 'package.json');
56
+ if (!existsSync(pkg)) continue;
57
+
58
+ // Skip ai-server (Python — no build step)
59
+ if (e.name === 'ai-server') continue;
60
+
61
+ // Backend (TypeScript): no build needed (tsx at runtime), but ensure tsc passes.
62
+ if (e.name.startsWith('backend')) {
63
+ all.push({
64
+ name: e.name,
65
+ path: appPath,
66
+ buildCmd: { cmd: 'npx', args: ['tsc', '--noEmit', '-p', '../../tsconfig.json'] },
67
+ });
68
+ continue;
69
+ }
70
+
71
+ // Frontend/admin: vite build
72
+ all.push({
73
+ name: e.name,
74
+ path: appPath,
75
+ buildCmd: { cmd: 'npx', args: ['vite', 'build'] },
76
+ });
77
+ }
78
+
79
+ if (target === 'all') return all;
80
+ return all.filter((t) => t.name === target);
81
+ }
82
+
83
+ function runCmd(cmd: string, args: string[], cwd: string): Promise<number> {
84
+ return new Promise((resolve) => {
85
+ const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: true });
86
+ child.on('exit', (code) => resolve(code ?? 1));
87
+ });
88
+ }
89
+
90
+ export const description = 'Production build for all apps (or one via --target)';
91
+ export const usage = 'nexus build [--target <app>|all]';
@@ -0,0 +1,241 @@
1
+ /**
2
+ * nexus db — seed and migrate commands.
3
+ *
4
+ * nexus db:seed [--class <name>] — run seeder(s) from database/seeds/
5
+ * nexus db:migrate [--step N] — run pending migrations in order
6
+ * nexus db:rollback [--step N] — rollback last N migrations
7
+ *
8
+ * Discovery is by createNexusApp()'s auto-discovery — seeds must be in
9
+ * apps/<backend>/src/database/seeds/*.ts (matching *Seeder.ts pattern)
10
+ * apps/<backend>/src/database/migrations/ (YYYYMMDDHHMMSS_*.ts)
11
+ *
12
+ * Migrations export an `up(db)` and optionally `down(db)` async function.
13
+ * A `_migrations` collection in the app DB tracks which have run.
14
+ */
15
+ import { resolve, join } from 'node:path';
16
+ import { existsSync } from 'node:fs';
17
+ import { readdir, readFile } from 'node:fs/promises';
18
+ import { pathToFileURL } from 'node:url';
19
+ import type { CommandContext } from '../dispatcher.js';
20
+ import { MongoClient, type Db } from 'mongodb';
21
+
22
+ interface MigrationRecord {
23
+ _id: string; // filename
24
+ ranAt: Date;
25
+ }
26
+
27
+ interface MigrationModule {
28
+ up(db: Db): Promise<void>;
29
+ down?(db: Db): Promise<void>;
30
+ }
31
+
32
+ const SEEDS_SUBDIR = 'src/database/seeds';
33
+ const MIGRATIONS_SUBDIR = 'src/database/migrations';
34
+
35
+ // ----------------------------------------------------------------------
36
+ // db:seed
37
+ // ----------------------------------------------------------------------
38
+
39
+ export async function runSeed(ctx: CommandContext): Promise<void> {
40
+ const appName = (ctx.args.flags.app as string) ?? 'backend';
41
+ const className = ctx.args.flags.class as string | undefined;
42
+ const projectRoot = process.cwd();
43
+ const appDir = join(projectRoot, 'apps', appName);
44
+ const seedsDir = join(appDir, SEEDS_SUBDIR);
45
+
46
+ if (!existsSync(seedsDir)) {
47
+ console.error(`✗ No seeders directory: ${seedsDir}`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+
52
+ const files = (await readdir(seedsDir)).filter((f) => f.endsWith('Seeder.ts') || f.endsWith('Seeder.js'));
53
+ if (files.length === 0) {
54
+ console.log('(no seeders found)');
55
+ return;
56
+ }
57
+
58
+ const toRun = className ? files.filter((f) => f.replace(/\.(ts|js)$/, '') === className) : files;
59
+ if (toRun.length === 0) {
60
+ console.error(`✗ Seeder "${className}" not found`);
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+
65
+ const db = await connectDb(projectRoot);
66
+ if (!db) return;
67
+
68
+ try {
69
+ for (const file of toRun) {
70
+ const mod = await import(pathToFileURL(join(seedsDir, file)).href);
71
+ const SeederClass = (mod as any).default ?? (mod as any)[Object.keys(mod)[0]!];
72
+ if (typeof SeederClass !== 'function') continue;
73
+ const seeder = new SeederClass();
74
+ if (typeof seeder.run !== 'function') continue;
75
+ console.log(`▶ ${file}`);
76
+ await seeder.run(db);
77
+ console.log(`✓ ${file}`);
78
+ }
79
+ } finally {
80
+ await db.client.close();
81
+ }
82
+ }
83
+
84
+ // ----------------------------------------------------------------------
85
+ // db:migrate
86
+ // ----------------------------------------------------------------------
87
+
88
+ export async function runMigrate(ctx: CommandContext): Promise<void> {
89
+ const appName = (ctx.args.flags.app as string) ?? 'backend';
90
+ const step = ctx.args.flags.step ? parseInt(String(ctx.args.flags.step), 10) : null;
91
+ const projectRoot = process.cwd();
92
+ const appDir = join(projectRoot, 'apps', appName);
93
+ const migrationsDir = join(appDir, MIGRATIONS_SUBDIR);
94
+
95
+ if (!existsSync(migrationsDir)) {
96
+ console.error(`✗ No migrations directory: ${migrationsDir}`);
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+
101
+ const files = (await readdir(migrationsDir))
102
+ .filter((f) => /^\d{4}_\d{2}_\d{2}_\d{6}_.*\.(ts|js)$/.test(f))
103
+ .sort();
104
+ if (files.length === 0) {
105
+ console.log('(no migrations found)');
106
+ return;
107
+ }
108
+
109
+ const db = await connectDb(projectRoot);
110
+ if (!db) return;
111
+
112
+ try {
113
+ const applied = new Set(
114
+ (await db.collection<MigrationRecord>('_migrations').find({}).toArray()).map((r) => r._id),
115
+ );
116
+ const pending = files.filter((f) => !applied.has(f.replace(/\.(ts|js)$/, '')));
117
+ const toRun = step ? pending.slice(0, step) : pending;
118
+ if (toRun.length === 0) {
119
+ console.log('(nothing to migrate)');
120
+ return;
121
+ }
122
+ for (const file of toRun) {
123
+ const name = file.replace(/\.(ts|js)$/, '');
124
+ const mod = (await import(pathToFileURL(join(migrationsDir, file)).href)) as MigrationModule;
125
+ if (typeof mod.up !== 'function') {
126
+ console.warn(`! ${file} has no up() — skipped`);
127
+ continue;
128
+ }
129
+ console.log(`▶ ${name}`);
130
+ await mod.up(db);
131
+ await db.collection<MigrationRecord>('_migrations').updateOne(
132
+ { _id: name },
133
+ { $set: { _id: name, ranAt: new Date() } },
134
+ { upsert: true },
135
+ );
136
+ console.log(`✓ ${name}`);
137
+ }
138
+ } finally {
139
+ await db.client.close();
140
+ }
141
+ }
142
+
143
+ // ----------------------------------------------------------------------
144
+ // db:rollback
145
+ // ----------------------------------------------------------------------
146
+
147
+ export async function runRollback(ctx: CommandContext): Promise<void> {
148
+ const appName = (ctx.args.flags.app as string) ?? 'backend';
149
+ const step = ctx.args.flags.step ? parseInt(String(ctx.args.flags.step), 10) : 1;
150
+ const projectRoot = process.cwd();
151
+ const appDir = join(projectRoot, 'apps', appName);
152
+ const migrationsDir = join(appDir, MIGRATIONS_SUBDIR);
153
+
154
+ if (!existsSync(migrationsDir)) {
155
+ console.error(`✗ No migrations directory: ${migrationsDir}`);
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+
160
+ const db = await connectDb(projectRoot);
161
+ if (!db) return;
162
+
163
+ try {
164
+ const applied = await db
165
+ .collection<MigrationRecord>('_migrations')
166
+ .find({})
167
+ .sort({ ranAt: -1 })
168
+ .limit(step)
169
+ .toArray();
170
+ if (applied.length === 0) {
171
+ console.log('(nothing to rollback)');
172
+ return;
173
+ }
174
+ for (const rec of applied.reverse()) {
175
+ const file = `${rec._id}.ts`;
176
+ const fileJs = `${rec._id}.js`;
177
+ const target = existsSync(join(migrationsDir, file))
178
+ ? join(migrationsDir, file)
179
+ : existsSync(join(migrationsDir, fileJs))
180
+ ? join(migrationsDir, fileJs)
181
+ : null;
182
+ if (!target) {
183
+ console.warn(`! Migration file missing for ${rec._id} — removing record only`);
184
+ await db.collection<MigrationRecord>('_migrations').deleteOne({ _id: rec._id });
185
+ continue;
186
+ }
187
+ const mod = (await import(pathToFileURL(target).href)) as MigrationModule;
188
+ if (typeof mod.down !== 'function') {
189
+ console.warn(`! ${rec._id} has no down() — skipped`);
190
+ continue;
191
+ }
192
+ console.log(`▼ ${rec._id}`);
193
+ await mod.down(db);
194
+ await db.collection<MigrationRecord>('_migrations').deleteOne({ _id: rec._id });
195
+ console.log(`✓ ${rec._id}`);
196
+ }
197
+ } finally {
198
+ await db.client.close();
199
+ }
200
+ }
201
+
202
+ // ----------------------------------------------------------------------
203
+ // Shared
204
+ // ----------------------------------------------------------------------
205
+
206
+ async function connectDb(projectRoot: string): Promise<Db | null> {
207
+ const uri = await readMongoUri(projectRoot);
208
+ if (!uri) {
209
+ console.error('✗ Cannot determine MongoDB URI (set NEXUS_DB_URI or in nexus.config.ts)');
210
+ process.exitCode = 1;
211
+ return null;
212
+ }
213
+ const client = new MongoClient(uri);
214
+ await client.connect();
215
+ return client.db();
216
+ }
217
+
218
+ async function readMongoUri(projectRoot: string): Promise<string | null> {
219
+ // env var first
220
+ if (process.env.NEXUS_DB_URI) return process.env.NEXUS_DB_URI;
221
+ // .env
222
+ const envPath = resolve(projectRoot, '.env');
223
+ if (existsSync(envPath)) {
224
+ const raw = await readFile(envPath, 'utf-8');
225
+ for (const line of raw.split('\n')) {
226
+ const m = line.match(/^\s*NEXUS_DB_URI\s*=\s*(.+?)\s*$/);
227
+ if (m) return m[1]!;
228
+ }
229
+ }
230
+ // nexus.config.ts — last resort, parse
231
+ const cfgPath = resolve(projectRoot, 'nexus.config.ts');
232
+ if (existsSync(cfgPath)) {
233
+ const raw = await readFile(cfgPath, 'utf-8');
234
+ const m = raw.match(/uri\s*:\s*['"]([^'"]+)['"]/);
235
+ if (m) return m[1]!;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ export const description = 'Run database seeds, migrations, or rollbacks';
241
+ export const usage = 'nexus db:<seed|migrate|rollback> [--app <backend>] [--class <name>] [--step N]';