@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/package.json
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bhooai/nexus-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
},
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "BhooAI Nexus v2 CLI — init, dev, build, test, scaffolding, plugins, queue, db.",
|
|
5
|
+
"license": "MIT",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"main": "./src/index.ts",
|
|
9
|
-
"types": "./src/index.ts",
|
|
10
8
|
"bin": {
|
|
11
9
|
"nexus": "./bin/nexus.js"
|
|
12
10
|
},
|
|
13
11
|
"scripts": {
|
|
14
|
-
"
|
|
15
|
-
"test": "vitest run"
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
16
13
|
},
|
|
17
14
|
"dependencies": {
|
|
18
|
-
"@bhooai/nexus-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
15
|
+
"@bhooai/nexus-core": "^2.0.0",
|
|
16
|
+
"tsx": "^4.19.0",
|
|
17
|
+
"zod": "^3.23.8"
|
|
21
18
|
},
|
|
22
19
|
"devDependencies": {
|
|
23
20
|
"@types/node": "^22.5.0",
|
|
24
|
-
"typescript": "^5.6.2"
|
|
25
|
-
"vitest": "^2.1.1"
|
|
21
|
+
"typescript": "^5.6.2"
|
|
26
22
|
}
|
|
27
23
|
}
|
|
@@ -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]';
|