@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/src/index.ts CHANGED
@@ -1,94 +1,36 @@
1
- import { doctor } from './commands/doctor.js';
2
- import { init } from './commands/init.js';
3
- import { dev } from './commands/dev.js';
4
- import { pysetup } from './commands/pysetup.js';
5
- import { cluster } from './commands/cluster.js';
6
- import { node } from './commands/node.js';
7
- import { syncCommand } from './commands/sync.js';
8
- import { uninstall } from './commands/uninstall.js';
9
- import { loadDotEnv } from './dotenv.js';
1
+ /**
2
+ * nexus CLI entry — registers all commands and dispatches.
3
+ */
4
+ import { registerCommand, run as dispatch } from './dispatcher.js';
5
+ import * as initCmd from './commands/init.js';
6
+ import * as devCmd from './commands/dev.js';
7
+ import * as doctorCmd from './commands/doctor.js';
8
+ import * as buildCmd from './commands/build.js';
9
+ import * as testCmd from './commands/test.js';
10
+ import * as addCmd from './commands/add.js';
11
+ import * as pluginCmd from './commands/plugin.js';
12
+ import * as dbCmd from './commands/db.js';
13
+ import * as queueCmd from './commands/queue.js';
14
+ import { downCommand, upCommand } from './commands/maintenance-commands.js';
15
+ import { registerMakeCommands } from './make/generators.js';
10
16
 
11
- /** Dispatch a CLI command. Returns the exit code. */
12
- export async function run(cmd: string, args: string[]): Promise<number> {
13
- // Load the project .env once so NEXUS_* keys reach the config loader AND the
14
- // supervised child processes (they inherit process.env).
15
- loadDotEnv(process.cwd());
17
+ registerCommand({ name: 'init', description: initCmd.description, usage: initCmd.usage, run: initCmd.run });
18
+ registerCommand({ name: 'dev', description: devCmd.description, usage: devCmd.usage, run: devCmd.run });
19
+ registerCommand({ name: 'doctor', description: doctorCmd.description, usage: doctorCmd.usage, run: doctorCmd.run });
20
+ registerCommand({ name: 'build', description: buildCmd.description, usage: buildCmd.usage, run: buildCmd.run });
21
+ registerCommand({ name: 'test', description: testCmd.description, usage: testCmd.usage, run: testCmd.run });
22
+ registerCommand({ name: 'add', description: addCmd.description, usage: addCmd.usage, run: addCmd.run });
23
+ registerCommand({ name: 'plugin', description: pluginCmd.description, usage: pluginCmd.usage, run: pluginCmd.run });
24
+ registerCommand({ name: 'db:seed', description: dbCmd.description, usage: dbCmd.usage, run: dbCmd.runSeed });
25
+ registerCommand({ name: 'db:migrate', description: dbCmd.description, usage: dbCmd.usage, run: dbCmd.runMigrate });
26
+ registerCommand({ name: 'db:rollback', description: dbCmd.description, usage: dbCmd.usage, run: dbCmd.runRollback });
27
+ registerCommand({ name: 'queue:work', description: queueCmd.description, usage: queueCmd.usage, run: queueCmd.runWork });
28
+ registerCommand({ name: 'queue:retry', description: queueCmd.description, usage: queueCmd.usage, run: queueCmd.runRetry });
29
+ registerCommand(downCommand);
30
+ registerCommand(upCommand);
16
31
 
17
- switch (cmd) {
18
- case 'doctor':
19
- return doctor();
20
- case 'init':
21
- return init({}, args);
22
- case 'dev':
23
- return dev(args);
24
- case 'build':
25
- console.log('nexus build - implemented in a later phase (tsc across workspaces).');
26
- return 0;
27
- case 'test':
28
- console.log('nexus test - run `npm test` (vitest + pytest) for now.');
29
- return 0;
30
- case 'plugin':
31
- console.log('nexus plugin <new|install> - implemented in Phase 8.');
32
- return 0;
33
- case 'add':
34
- console.log('nexus add <subgraph|module> - implemented in Phase 5/6.');
35
- return 0;
36
- case 'pysetup':
37
- return pysetup(args);
38
- case 'uninstall':
39
- return uninstall(args);
40
- case 'cluster':
41
- return cluster(args);
42
- case 'node':
43
- return node(args);
44
- case 'sync':
45
- return syncCommand(args);
46
- case 'help':
47
- case '--help':
48
- case '-h':
49
- printHelp();
50
- return 0;
51
- default:
52
- console.error(`Unknown command: ${cmd}`);
53
- printHelp();
54
- return 1;
55
- }
56
- }
57
-
58
- function printHelp(): void {
59
- console.log(`
60
- BhooAI Nexus CLI
61
-
62
- Usage: nexus <command> [args]
32
+ registerMakeCommands(registerCommand);
63
33
 
64
- Commands:
65
- init [target] [--as=root|node] [--role=R] [--name=N] [--mongo-uri=URI]
66
- [--redis-url=URL] [--ai-providers=a,b] [--ai-key id=val]
67
- [--cluster-token=T] [--venv|--no-venv] [--no-interactive] [--no-install] [--skip-mongo-check]
68
- Scaffold a project; one-command wizard sets up
69
- Node + frontend + admin + Python AI + Mongo/Redis
70
- dev [--only a,b] Start the four terminals under the supervisor
71
- build Build all TypeScript workspaces
72
- test Run all test suites
73
- doctor Verify the environment (node, python, mongo, redis)
74
- plugin <new|install> Manage plugins
75
- add <subgraph|module> Scaffold a new subgraph or module
76
- pysetup [pkgs...] [--venv] [--interactive]
77
- Install the AI-server Python deps (requirements.txt) + extras
78
- uninstall [--target <path>] [--purge] [--force] [--dry-run]
79
- Drop the project DB, delete its nexus_projects record,
80
- optionally remove the project directory (--purge)
81
- node <id|serve> Run this machine as a cluster node (agent + role service)
82
- cluster <sub> Root-side mesh: status, link, scale, serve, etc.
83
- sync [--check] [--no-db] Rewrite Dockerfiles, docker.*, serve-all.mjs & DB from nexus.config.ts
84
-
85
- Options:
86
- -h, --help Show this help
87
- `);
34
+ export async function run(cmd: string, argv: string[]): Promise<void> {
35
+ await dispatch(cmd, argv);
88
36
  }
89
-
90
- // Re-exports for programmatic use.
91
- export { doctor, init, dev, pysetup, cluster, node, syncCommand };
92
- export { syncConfig, configChangedSinceSync, deriveValues, fingerprintOf } from './config-sync.js';
93
- export { Supervisor } from './supervisor.js';
94
- export type { LogEntry } from './supervisor.js';
@@ -0,0 +1,431 @@
1
+ /**
2
+ * All make:* generators — registered as commands under the `make:` prefix.
3
+ */
4
+ import type { Command } from '../dispatcher.js';
5
+ import { runGenerator, type GeneratorSpec } from '../generators.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Templates for each artifact
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const GENERATORS: Record<string, GeneratorSpec> = {
12
+ route: {
13
+ folder: 'routes',
14
+ suffix: '.ts',
15
+ render: ({ slug }) => `import { defineRoutes } from '@bhooai/nexus-core';
16
+ import type { RequestContext } from '@bhooai/nexus-core';
17
+
18
+ export default defineRoutes([
19
+ { method: 'GET', path: '/${slug}', handler: list },
20
+ { method: 'POST', path: '/${slug}', handler: create },
21
+ { method: 'GET', path: '/${slug}/:id', handler: getOne },
22
+ { method: 'PATCH', path: '/${slug}/:id', handler: update },
23
+ { method: 'DELETE', path: '/${slug}/:id', handler: remove },
24
+ ]);
25
+
26
+ function list(_ctx: RequestContext) { _ctx.json([]); }
27
+ function create(ctx: RequestContext) { ctx.json({ created: true }, 201); }
28
+ function getOne(ctx: RequestContext) { ctx.json({ id: ctx.params.id }); }
29
+ function update(ctx: RequestContext) { ctx.json({ id: ctx.params.id, updated: true }); }
30
+ function remove(ctx: RequestContext) { ctx.status(204); }
31
+ `,
32
+ },
33
+
34
+ controller: {
35
+ folder: 'controllers',
36
+ suffix: 'Controller.ts',
37
+ render: ({ className, slug }) => `/**
38
+ * ${className}Controller — actions for the ${slug} resource.
39
+ * Routes reference these from ../routes/${slug}.ts.
40
+ */
41
+ export class ${className}Controller {
42
+ async list() { return []; }
43
+ async create(body: unknown) { return { created: true, body }; }
44
+ async getOne(id: string) { return { id }; }
45
+ async update(id: string, body: unknown) { return { id, body }; }
46
+ async remove(_id: string) { /* no-op */ }
47
+ }
48
+ `,
49
+ },
50
+
51
+ model: {
52
+ folder: 'models',
53
+ suffix: '.ts',
54
+ render: ({ className, slug }) => `import { model, Schema } from '@bhooai/nexus-data';
55
+
56
+ const ${slug}Schema = new Schema({
57
+ name: { type: String, required: true },
58
+ createdAt: { type: Date, default: () => new Date() },
59
+ updatedAt: { type: Date, default: () => new Date() },
60
+ });
61
+
62
+ export const ${className} = model('${slug}s', ${slug}Schema);
63
+ export type ${className}Doc = typeof ${className} extends ((...args: never[]) => infer R) ? R : never;
64
+ `,
65
+ },
66
+
67
+ service: {
68
+ folder: 'services',
69
+ suffix: 'Service.ts',
70
+ render: ({ className }) => `/**
71
+ * ${className}Service — business logic for the resource.
72
+ * Callers: routes/*, graphql/*.graph.ts, ws/*.room.ts.
73
+ */
74
+ export class ${className}Service {
75
+ async list() { return []; }
76
+ async findById(id: string) { return { id }; }
77
+ }
78
+ `,
79
+ },
80
+
81
+ repository: {
82
+ folder: 'repositories',
83
+ suffix: 'Repository.ts',
84
+ render: ({ className, slug }) => `/**
85
+ * ${className}Repository — query layer over the ${slug} collection.
86
+ * Keeps Mongo concerns out of services.
87
+ */
88
+ import type { Db } from 'mongodb';
89
+
90
+ export class ${className}Repository {
91
+ constructor(private db: Db) {}
92
+
93
+ async findAll() {
94
+ return this.db.collection('${slug}s').find({}).toArray();
95
+ }
96
+
97
+ async findById(id: string) {
98
+ return this.db.collection('${slug}s').findOne({ _id: id as never });
99
+ }
100
+ }
101
+ `,
102
+ },
103
+
104
+ middleware: {
105
+ folder: 'middleware',
106
+ suffix: '.ts',
107
+ render: ({ slug, className }) => `import type { Middleware } from '@bhooai/nexus-core';
108
+
109
+ /**
110
+ * ${className} — route middleware. Attach via routes/*.ts middleware: [...].
111
+ */
112
+ export const ${className}: Middleware = async (ctx, next) => {
113
+ // pre-handler work
114
+ await next();
115
+ // post-handler work
116
+ };
117
+ `,
118
+ },
119
+
120
+ validator: {
121
+ folder: 'validators',
122
+ suffix: '.schema.ts',
123
+ render: ({ slug, className }) => `import { z } from 'zod';
124
+
125
+ export const ${className}Schema = z.object({
126
+ name: z.string().min(2),
127
+ });
128
+
129
+ export type ${className}Input = z.infer<typeof ${className}Schema>;
130
+ `,
131
+ },
132
+
133
+ job: {
134
+ folder: 'jobs',
135
+ suffix: 'Job.ts',
136
+ render: ({ className }) => `import { Job } from '@bhooai/nexus-core';
137
+
138
+ export default class ${className}Job extends Job {
139
+ queue = 'default';
140
+ retries = 3;
141
+ backoff = 'exponential' as const;
142
+
143
+ async handle(...args: unknown[]): Promise<void> {
144
+ // Job logic here. Throw to trigger retry.
145
+ void args;
146
+ }
147
+ }
148
+ `,
149
+ },
150
+
151
+ event: {
152
+ folder: 'events',
153
+ suffix: '.ts',
154
+ render: ({ className }) => `import { DomainEvent } from '@bhooai/nexus-core';
155
+
156
+ export class ${className} extends DomainEvent {
157
+ // Payload fields as constructor args.
158
+ // constructor(public readonly userId: string) { super(); }
159
+ }
160
+ `,
161
+ },
162
+
163
+ listener: {
164
+ folder: 'listeners',
165
+ suffix: '.ts',
166
+ render: ({ className }) => `import { Listener } from '@bhooai/nexus-core';
167
+
168
+ // Convention: file name "On<Foo>.ts" auto-wires to event "<Foo>".
169
+ // Adjust imports / class target if your event lives elsewhere.
170
+ export default class ${className} extends Listener {
171
+ async handle(event: unknown): Promise<void> {
172
+ // event is the dispatched DomainEvent instance
173
+ void event;
174
+ }
175
+ }
176
+ `,
177
+ },
178
+
179
+ policy: {
180
+ folder: 'policies',
181
+ suffix: 'Policy.ts',
182
+ render: ({ className, slug }) => `import { Policy } from '@bhooai/nexus-core';
183
+
184
+ /**
185
+ * ${className}Policy — authorization rules for ${slug}.
186
+ *
187
+ * Methods on this class correspond to abilities: view, create, update, delete, ...
188
+ * Return true to allow, false to deny. Async allowed.
189
+ */
190
+ export default class ${className}Policy extends Policy {
191
+ viewAny(_actor: unknown) { return true; }
192
+ view(_actor: unknown, _resource: unknown) { return true; }
193
+ create(_actor: unknown) { return true; }
194
+ update(_actor: unknown, _resource: unknown) { return true; }
195
+ delete(_actor: unknown, _resource: unknown) { return true; }
196
+ }
197
+ `,
198
+ },
199
+
200
+ resource: {
201
+ folder: 'resources',
202
+ suffix: 'Resource.ts',
203
+ render: ({ className }) => `import { Resource } from '@bhooai/nexus-core';
204
+
205
+ interface ${className}Shape {
206
+ id: string;
207
+ name: string;
208
+ // ...
209
+ }
210
+
211
+ export class ${className}Resource extends Resource<${className}Shape> {
212
+ toJSON(value: ${className}Shape) {
213
+ return {
214
+ id: value.id,
215
+ name: value.name,
216
+ };
217
+ }
218
+ }
219
+ `,
220
+ },
221
+
222
+ request: {
223
+ folder: 'requests',
224
+ suffix: 'Request.ts',
225
+ render: ({ className }) => `import { FormRequest } from '@bhooai/nexus-core';
226
+ import { z } from 'zod';
227
+
228
+ export class ${className}Request extends FormRequest {
229
+ schema = z.object({
230
+ // define body schema
231
+ });
232
+
233
+ // authorize(ctx) { return !!ctx.state.user; }
234
+ }
235
+ `,
236
+ },
237
+
238
+ mail: {
239
+ folder: 'mail/mailables',
240
+ suffix: 'Mail.ts',
241
+ render: ({ className, slug }) => `import { Mailable } from '@bhooai/nexus-core';
242
+
243
+ export default class ${className}Mail extends Mailable {
244
+ subject = '${className}';
245
+ template = '${slug}';
246
+
247
+ data() {
248
+ return {};
249
+ }
250
+ }
251
+ `,
252
+ companions: [{
253
+ relativePath: ({ slug }) => `mail/templates/${slug}.ejs`,
254
+ content: () => `<!doctype html>
255
+ <html>
256
+ <body>
257
+ <p>Hello,</p>
258
+ <p>This is the auto-generated template. Edit me.</p>
259
+ </body>
260
+ </html>
261
+ `,
262
+ }],
263
+ },
264
+
265
+ room: {
266
+ folder: 'ws',
267
+ suffix: '.room.ts',
268
+ render: ({ slug }) => `import type { RequestContext } from '@bhooai/nexus-core';
269
+
270
+ /**
271
+ * WebSocket room "${slug}". Auto-mounted on /ws.
272
+ *
273
+ * Real wiring awaits the realtime integration in createNexusApp (P2.5).
274
+ * For now this file simply exports a manifest.
275
+ */
276
+ export default {
277
+ name: '${slug}',
278
+ onJoin: async (_socket: unknown, _ctx: RequestContext) => {},
279
+ onLeave: async (_socket: unknown, _ctx: RequestContext) => {},
280
+ onMessage: {
281
+ // 'msg:send': async (socket, payload) => { ... },
282
+ },
283
+ };
284
+ `,
285
+ },
286
+
287
+ subgraph: {
288
+ folder: 'graphql',
289
+ suffix: '.graph.ts',
290
+ render: ({ slug, className }) => `/**
291
+ * ${className} GraphQL subgraph — picked up by the federation composer.
292
+ * typeDefs use federation directives (@key, @extends).
293
+ */
294
+ export const typeDefs = /* GraphQL */ \`
295
+ type ${className} @key(fields: "id") {
296
+ id: ID!
297
+ name: String!
298
+ }
299
+
300
+ extend type Query {
301
+ ${slug}: [${className}!]!
302
+ }
303
+ \`;
304
+
305
+ export const resolvers = {
306
+ Query: {
307
+ ${slug}: () => [],
308
+ },
309
+ };
310
+ `,
311
+ },
312
+
313
+ seeder: {
314
+ folder: 'database/seeds',
315
+ suffix: 'Seeder.ts',
316
+ render: ({ className }) => `import { Seeder } from '@bhooai/nexus-core';
317
+ import type { Db } from 'mongodb';
318
+
319
+ export default class ${className}Seeder extends Seeder {
320
+ async run(db?: Db): Promise<void> {
321
+ void db;
322
+ // Example: await db?.collection('users').insertMany([...]);
323
+ }
324
+ }
325
+ `,
326
+ },
327
+
328
+ migration: {
329
+ folder: 'database/migrations',
330
+ // Migrations get a timestamped filename — handled specially below (make:migration below).
331
+ // This entry is here only as a template holder; registration skips it.
332
+ suffix: '.ts',
333
+ render: ({ slug }) => `import type { Db } from 'mongodb';
334
+
335
+ export async function up(db: Db): Promise<void> {
336
+ // await db.createCollection('${slug}s');
337
+ }
338
+
339
+ export async function down(db: Db): Promise<void> {
340
+ // await db.dropCollection('${slug}s');
341
+ }
342
+ `,
343
+ },
344
+
345
+ provider: {
346
+ folder: 'providers',
347
+ suffix: 'ServiceProvider.ts',
348
+ render: ({ className }) => `import { ServiceProvider } from '@bhooai/nexus-core';
349
+ import type { Container } from '@bhooai/nexus-core';
350
+
351
+ export default class ${className}ServiceProvider extends ServiceProvider {
352
+ register(container: Container): void {
353
+ // container.singleton('name', () => new Thing());
354
+ void container;
355
+ }
356
+
357
+ async boot(_container: Container): Promise<void> {
358
+ // post-registration boot
359
+ }
360
+ }
361
+ `,
362
+ },
363
+
364
+ plugin: {
365
+ folder: 'plugins',
366
+ suffix: '/index.ts',
367
+ render: ({ slug, className }) => `/**
368
+ * ${className} plugin entry.
369
+ */
370
+ import type { Container } from '@bhooai/nexus-core';
371
+
372
+ export function register(_container: Container): void {}
373
+
374
+ export async function boot(): Promise<void> {}
375
+ `,
376
+ },
377
+ };
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // Registration
381
+ // ---------------------------------------------------------------------------
382
+
383
+ export function registerMakeCommands(register: (cmd: Command) => void): void {
384
+ for (const [name, spec] of Object.entries(GENERATORS)) {
385
+ if (name === 'migration') continue; // handled below with timestamp logic
386
+ register({
387
+ name: `make:${name}`,
388
+ description: `Generate a new ${name} under apps/<backend>/src/${spec.folder}`,
389
+ usage: `nexus make:${name} <name> [--app <backend>]`,
390
+ run: async (ctx) => {
391
+ await runGenerator(ctx, spec);
392
+ },
393
+ });
394
+ }
395
+
396
+ // migrations need a separate code-path (timestamped filename).
397
+ register({
398
+ name: 'make:migration',
399
+ description: 'Generate a new timestamped migration',
400
+ usage: 'nexus make:migration <name> [--app <backend>]',
401
+ run: async (ctx) => {
402
+ const rawName = ctx.args._[0];
403
+ if (!rawName) {
404
+ console.error('Usage: nexus make:migration <name> [--app <backend>]');
405
+ process.exitCode = 1;
406
+ return;
407
+ }
408
+ const { resolve, join } = await import('node:path');
409
+ const { mkdir, writeFile } = await import('node:fs/promises');
410
+ const { existsSync } = await import('node:fs');
411
+ const { slugFromName } = await import('../generators.js');
412
+
413
+ const slug = slugFromName(rawName);
414
+ const app = (ctx.args.flags.app as string) ?? 'backend';
415
+ const projectRoot = process.cwd();
416
+ const ts = new Date();
417
+ const stamp = `${ts.getUTCFullYear()}_${String(ts.getUTCMonth() + 1).padStart(2, '0')}_${String(ts.getUTCDate()).padStart(2, '0')}_${String(ts.getUTCHours()).padStart(2, '0')}${String(ts.getUTCMinutes()).padStart(2, '0')}${String(ts.getUTCSeconds()).padStart(2, '0')}`;
418
+ const fileName = `${stamp}_${slug}.ts`;
419
+ const dir = resolve(projectRoot, 'apps', app, 'src', 'database', 'migrations');
420
+ await mkdir(dir, { recursive: true });
421
+ const target = join(dir, fileName);
422
+ if (existsSync(target)) {
423
+ console.error(`✗ already exists: ${target}`);
424
+ process.exitCode = 1;
425
+ return;
426
+ }
427
+ await writeFile(target, GENERATORS.migration!.render({ slug, className: '', name: rawName }));
428
+ console.log(`✓ Created ${target.replace(projectRoot + '\\', '')}`);
429
+ },
430
+ });
431
+ }
package/src/ports.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Per-app port registry.
3
+ *
4
+ * Maintains `.nexus-ports.json` at the project root so each backend /
5
+ * frontend / admin has a stable port across runs. Both `nexus add` and
6
+ * `nexus dev` consult + update this file. `createNexusApp()` reads via the
7
+ * NEXUS_PORT env var (set by `nexus dev`) or by looking up its app name.
8
+ *
9
+ * Format:
10
+ * { "backend": 4000, "backend-shop": 4001, "frontend": 3000, ... }
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { readFile, writeFile } from 'node:fs/promises';
14
+ import { resolve, join } from 'node:path';
15
+ import { nextFreePort } from './util.js';
16
+
17
+ const REGISTRY_FILENAME = '.nexus-ports.json';
18
+
19
+ export interface PortRegistry {
20
+ [appName: string]: number;
21
+ }
22
+
23
+ const DEFAULTS: Record<string, number> = {
24
+ backend: 4000,
25
+ frontend: 3000,
26
+ admin: 3300,
27
+ 'ai-server': 8000,
28
+ };
29
+
30
+ export function registryPath(projectRoot: string): string {
31
+ return join(resolve(projectRoot), REGISTRY_FILENAME);
32
+ }
33
+
34
+ export async function readRegistry(projectRoot: string): Promise<PortRegistry> {
35
+ const p = registryPath(projectRoot);
36
+ if (!existsSync(p)) return {};
37
+ try {
38
+ const raw = await readFile(p, 'utf-8');
39
+ return JSON.parse(raw) as PortRegistry;
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ export async function writeRegistry(projectRoot: string, registry: PortRegistry): Promise<void> {
46
+ await writeFile(registryPath(projectRoot), JSON.stringify(registry, null, 2) + '\n', 'utf-8');
47
+ }
48
+
49
+ /**
50
+ * Get the port assigned to an app, allocating + persisting one if missing.
51
+ *
52
+ * Default port logic:
53
+ * - If app is a known type (backend / frontend / admin / ai-server), use its canonical default
54
+ * - Otherwise, derive from the type prefix: backend-* → 4000+, frontend-* → 3000+, admin-* → 3300+
55
+ * - Bump until free + not already in registry
56
+ */
57
+ export async function ensurePort(projectRoot: string, appName: string): Promise<number> {
58
+ const reg = await readRegistry(projectRoot);
59
+ if (reg[appName]) return reg[appName]!;
60
+
61
+ const defaultPort = defaultPortFor(appName);
62
+ const used = new Set(Object.values(reg));
63
+ let candidate = defaultPort;
64
+ while (used.has(candidate)) candidate++;
65
+ // Verify the candidate is actually free on the box right now
66
+ candidate = await nextFreePort(candidate);
67
+
68
+ reg[appName] = candidate;
69
+ await writeRegistry(projectRoot, reg);
70
+ return candidate;
71
+ }
72
+
73
+ export function defaultPortFor(appName: string): number {
74
+ if (DEFAULTS[appName]) return DEFAULTS[appName]!;
75
+ if (appName.startsWith('backend-')) return 4000;
76
+ if (appName.startsWith('frontend-')) return 3000;
77
+ if (appName.startsWith('admin-')) return 3300;
78
+ return 5000;
79
+ }
80
+
81
+ /** Look up the port for an app without allocating one (returns null if unset). */
82
+ export async function lookupPort(projectRoot: string, appName: string): Promise<number | null> {
83
+ const reg = await readRegistry(projectRoot);
84
+ return reg[appName] ?? null;
85
+ }