@bhooai/nexus-cli 0.1.4 → 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.
Files changed (112) hide show
  1. package/package.json +8 -12
  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/prompts.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Minimal wizard prompts for the CLI. Uses node:readline/promises so deps
3
+ * stay tiny. Provides prompt + select with arrow-key navigation.
4
+ */
5
+ import { createInterface, type Interface } from 'node:readline/promises';
6
+ import { stdin as input, stdout as output } from 'node:process';
7
+
8
+ let rl: Interface | null = null;
9
+
10
+ function getRl(): Interface {
11
+ if (!rl) rl = createInterface({ input, output });
12
+ return rl;
13
+ }
14
+
15
+ export function closePrompts(): void {
16
+ if (rl) {
17
+ rl.close();
18
+ rl = null;
19
+ }
20
+ }
21
+
22
+ export function isInteractive(): boolean {
23
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
24
+ }
25
+
26
+ /** Free-form prompt. */
27
+ export async function prompt(question: string, defaultValue?: string): Promise<string> {
28
+ const r = getRl();
29
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
30
+ const answer = await r.question(`${question}${suffix}: `);
31
+ return answer.trim() || defaultValue || '';
32
+ }
33
+
34
+ /** Yes/No confirm. */
35
+ export async function confirm(question: string, defaultValue: boolean = false): Promise<boolean> {
36
+ const hint = defaultValue ? 'Y/n' : 'y/N';
37
+ const answer = await prompt(`${question} [${hint}]`);
38
+ if (!answer) return defaultValue;
39
+ return /^y(es)?$/i.test(answer);
40
+ }
41
+
42
+ /** Single-select from a list. */
43
+ export async function select<T extends string>(question: string, choices: readonly T[]): Promise<T> {
44
+ console.log(question);
45
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c}`));
46
+ const answer = await prompt(`Choice [1-${choices.length}]`, '1');
47
+ const idx = parseInt(answer, 10) - 1;
48
+ if (idx < 0 || idx >= choices.length) return choices[0]!;
49
+ return choices[idx]!;
50
+ }
51
+
52
+ /** Multi-select (comma-separated). */
53
+ export async function multiSelect<T extends string>(question: string, choices: readonly T[]): Promise<T[]> {
54
+ console.log(question);
55
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c}`));
56
+ const answer = await prompt(`Choices [comma-separated], empty=none`, '');
57
+ if (!answer.trim()) return [];
58
+ return answer
59
+ .split(',')
60
+ .map((s) => parseInt(s.trim(), 10) - 1)
61
+ .filter((i) => i >= 0 && i < choices.length)
62
+ .map((i) => choices[i]!);
63
+ }
64
+
65
+ /** Hidden-input prompt (for secrets). Note: readline/promises doesn't mask input — caller beware. */
66
+ export async function promptHidden(question: string): Promise<string> {
67
+ return prompt(question);
68
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * EJS-style template renderer. Supports `<%= expr %>` and `<%- expr %>` (unescaped),
3
+ * plus `<% if (...) { %>...<% } %>` control flow via Function constructor.
4
+ *
5
+ * Deliberately tiny — no external deps. Templates live in
6
+ * packages/nexus-cli/templates/ and use the `data` object passed in.
7
+ */
8
+
9
+ export type TemplateVars = Record<string, unknown>;
10
+
11
+ const CACHE = new Map<string, (vars: TemplateVars) => string>();
12
+
13
+ /** Render a template string with vars. */
14
+ export function renderString(template: string, vars: TemplateVars): string {
15
+ const fn = compile(template);
16
+ return fn(vars);
17
+ }
18
+
19
+ /** Pre-compile a template string into a render function. */
20
+ export function compile(template: string): (vars: TemplateVars) => string {
21
+ const cached = CACHE.get(template);
22
+ if (cached) return cached;
23
+
24
+ // Build a function body that pieces together plain text + expressions.
25
+ // Escape backslashes + backticks in literal text segments, then emit as
26
+ // template literal parts. Expressions are pushed into an output array.
27
+ const body: string[] = ['const __out=[];'];
28
+ body.push('with(__data){'); // expose vars as bare identifiers
29
+
30
+ let i = 0;
31
+ let buf = '';
32
+ const flushLiteral = () => {
33
+ if (buf) {
34
+ body.push(`__out.push(${JSON.stringify(buf)});`);
35
+ buf = '';
36
+ }
37
+ };
38
+
39
+ while (i < template.length) {
40
+ const start = template.indexOf('<%', i);
41
+ if (start === -1) {
42
+ buf += template.slice(i);
43
+ break;
44
+ }
45
+ buf += template.slice(i, start);
46
+
47
+ const end = template.indexOf('%>', start);
48
+ if (end === -1) {
49
+ buf += template.slice(start);
50
+ break;
51
+ }
52
+
53
+ const tag = template.slice(start, end + 2);
54
+ const inner = tag.slice(2, -2);
55
+ flushLiteral();
56
+
57
+ if (inner.startsWith('=')) {
58
+ // Escaped interpolation
59
+ const expr = inner.slice(1).trim();
60
+ body.push(`__out.push(__esc(String(${expr})));`);
61
+ } else if (inner.startsWith('-')) {
62
+ // Raw interpolation
63
+ const expr = inner.slice(1).trim();
64
+ body.push(`__out.push(String(${expr}));`);
65
+ } else if (inner.startsWith('#')) {
66
+ // Comment — drop
67
+ } else {
68
+ // Control flow
69
+ body.push(inner);
70
+ }
71
+
72
+ i = end + 2;
73
+ }
74
+ flushLiteral();
75
+ body.push('}');
76
+ body.push('return __out.join("");');
77
+
78
+ const src = body.join('\n');
79
+ const fn = new Function('__data', '__esc', src) as (vars: TemplateVars, esc: (s: string) => string) => string;
80
+ const wrapped = (vars: TemplateVars) => fn(vars, escapeHtml);
81
+ CACHE.set(template, wrapped);
82
+ return wrapped;
83
+ }
84
+
85
+ function escapeHtml(s: string): string {
86
+ return s
87
+ .replace(/&/g, '&amp;')
88
+ .replace(/</g, '&lt;')
89
+ .replace(/>/g, '&gt;')
90
+ .replace(/"/g, '&quot;')
91
+ .replace(/'/g, '&#39;');
92
+ }
93
+
94
+ /** Render a file (assumed UTF-8). */
95
+ export async function renderFile(path: string, vars: TemplateVars): Promise<string> {
96
+ const { readFile } = await import('node:fs/promises');
97
+ const raw = await readFile(path, 'utf-8');
98
+ return renderString(raw, vars);
99
+ }
100
+
101
+ /** Whether a filename looks like a template (render) vs verbatim copy. */
102
+ export function isTemplateName(name: string): boolean {
103
+ return name.endsWith('.ejs') || name.endsWith('.tmpl') || name.endsWith('.hbs');
104
+ }
105
+
106
+ /** Strip template suffix: foo.ejs → foo. */
107
+ export function stripTemplateSuffix(name: string): string {
108
+ return name.replace(/\.(ejs|tmpl|hbs)$/, '');
109
+ }
package/src/util.ts CHANGED
@@ -1,132 +1,94 @@
1
- import { createConnection } from 'node:net';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
1
+ /**
2
+ * CLI utilities argument parsing, port probing, path helpers.
3
+ */
4
+ import { createServer } from 'node:net';
4
5
  import { existsSync } from 'node:fs';
5
- import { join } from 'node:path';
6
+ import { resolve } from 'node:path';
6
7
 
7
- const execFileAsync = promisify(execFile);
8
+ export interface ParsedArgs {
9
+ _: string[];
10
+ flags: Record<string, string | boolean>;
11
+ }
8
12
 
9
- export interface CheckResult {
10
- name: string;
11
- ok: boolean;
12
- detail: string;
13
+ /** Parse argv like `--foo bar`, `--foo=bar`, `--flag` → { _: [...rest], flags: {...} }. */
14
+ export function parseArgs(argv: string[]): ParsedArgs {
15
+ const _: string[] = [];
16
+ const flags: Record<string, string | boolean> = {};
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i]!;
19
+ if (a.startsWith('--')) {
20
+ const eq = a.indexOf('=');
21
+ if (eq === -1) {
22
+ const key = a.slice(2);
23
+ const next = argv[i + 1];
24
+ if (next !== undefined && !next.startsWith('-')) {
25
+ flags[key] = next;
26
+ i++;
27
+ } else {
28
+ flags[key] = true;
29
+ }
30
+ } else {
31
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
32
+ }
33
+ } else if (a.startsWith('-') && a.length > 1) {
34
+ // short flags: -abc → { a: true, b: true, c: true }
35
+ for (const ch of a.slice(1)) flags[ch] = true;
36
+ } else {
37
+ _.push(a);
38
+ }
39
+ }
40
+ return { _, flags };
13
41
  }
14
42
 
15
- /** Try a TCP connect to host:port within a timeout. */
16
- export function tcpReachable(host: string, port: number, timeoutMs = 1500): Promise<boolean> {
17
- return new Promise((resolve) => {
18
- const socket = createConnection({ host, port }, () => {
19
- socket.destroy();
20
- resolve(true);
21
- });
22
- socket.setTimeout(timeoutMs);
23
- socket.on('error', () => resolve(false));
24
- socket.on('timeout', () => {
25
- socket.destroy();
26
- resolve(false);
27
- });
43
+ /** Test whether a port is free. */
44
+ export async function isPortFree(port: number, host: string = '127.0.0.1'): Promise<boolean> {
45
+ return new Promise((resolveFree) => {
46
+ const srv = createServer();
47
+ srv.once('error', () => resolveFree(false));
48
+ srv.once('listening', () => srv.close(() => resolveFree(true)));
49
+ srv.listen(port, host);
28
50
  });
29
51
  }
30
52
 
31
- /** True if nothing is listening on `port` (quick TCP connect check). */
32
- export async function isPortFree(host: string, port: number, timeoutMs = 700): Promise<boolean> {
33
- return !(await tcpReachable(host === '0.0.0.0' ? '127.0.0.1' : host, port, timeoutMs));
53
+ /** Find a free port by scanning from `start`. */
54
+ export async function nextFreePort(start: number, host: string = '127.0.0.1'): Promise<number> {
55
+ let p = start;
56
+ while (!(await isPortFree(p, host))) {
57
+ p++;
58
+ if (p > 65535) throw new Error('No free port found');
59
+ }
60
+ return p;
34
61
  }
35
62
 
36
- /** Parse host/port from common connection-string formats. */
37
- export function parseHostPort(url: string, defaultPort: number): { host: string; port: number } {
38
- try {
39
- const u = new URL(url);
40
- return { host: u.hostname || 'localhost', port: u.port ? Number(u.port) : defaultPort };
41
- } catch {
42
- return { host: 'localhost', port: defaultPort };
43
- }
63
+ /** Resolve a path that may be relative to cwd. */
64
+ export function resolvePath(p: string): string {
65
+ return resolve(process.cwd(), p);
44
66
  }
45
67
 
46
- export async function versionOf(cmd: string, args: string[] = ['--version']): Promise<string> {
68
+ /** Check directory existence. */
69
+ export function dirExists(p: string): boolean {
47
70
  try {
48
- // Windows: runtimes like `npm` are .cmd shims requiring a shell. Passing the
49
- // full command line (no separate args array) avoids Node's DEP0190 warning.
50
- const win = process.platform === 'win32';
51
- const { stdout } = win
52
- ? await execFileAsync(
53
- [cmd, ...args].map((t) => (/\s/.test(t) ? `"${t}"` : t)).join(' '),
54
- [],
55
- { shell: true },
56
- )
57
- : await execFileAsync(cmd, args, {});
58
- return stdout.trim();
71
+ return existsSync(p);
59
72
  } catch {
60
- return '';
73
+ return false;
61
74
  }
62
75
  }
63
76
 
64
- /** A prerequisite check (runtime or service). */
65
- export interface Prereq extends CheckResult {
66
- /** Critical = the wizard should refuse to proceed. Warnings ask to continue. */
67
- critical?: boolean;
68
- }
69
-
70
- /**
71
- * Detect installed runtimes: node, npm, python, git. Returns a list of checks
72
- * suitable for printing. node/npm/python are critical; git is a warning.
73
- * Shared by `nexus doctor` and `nexus init` step 1.
74
- */
75
- export async function scanRuntimes(): Promise<Prereq[]> {
76
- const out: Prereq[] = [];
77
- out.push({ name: 'node', ok: !!process.versions.node, detail: `v${process.versions.node}`, critical: true });
78
- const npmVer = await versionOf(process.platform === 'win32' ? 'npm' : 'npm');
79
- out.push({ name: 'npm', ok: !!npmVer, detail: npmVer ? `v${npmVer}` : 'not found', critical: true });
80
- const pyVer = await versionOf(process.platform === 'win32' ? 'python' : 'python3');
81
- out.push({ name: 'python', ok: !!pyVer, detail: pyVer || 'not found (ai-server disabled)', critical: true });
82
- const gitVer = await versionOf('git', ['--version']);
83
- out.push({ name: 'git', ok: !!gitVer, detail: gitVer ? gitVer.replace(/^git version /, 'v') : 'not found' });
84
- return out;
85
- }
86
-
87
- /** Lightweight config shape used by `scanServices` (avoids importing nexus-core here). */
88
- export interface ServiceConfig {
89
- db: { uri: string };
90
- redis: { url: string };
91
- ai: { serverUrl: string };
92
- server: { host: string; port: number };
93
- frontend: { port: number };
94
- admin: { port: number };
77
+ /** Slugify a project name (lowercase, no spaces). */
78
+ export function slugify(name: string): string {
79
+ return name
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9]+/g, '-')
82
+ .replace(/^-+|-+$/g, '');
95
83
  }
96
84
 
97
- /**
98
- * Probe Mongo, Redis, and the AI server (TCP reachability) plus check that the
99
- * backend/frontend/admin ports are free. Returns a list of checks.
100
- * Shared by `nexus doctor` and `nexus init` step 1 / step 18 (verify).
101
- */
102
- export async function scanServices(cfg: ServiceConfig): Promise<Prereq[]> {
103
- const out: Prereq[] = [];
104
- const db = parseHostPort(cfg.db.uri, 27017);
105
- const dbOk = await tcpReachable(db.host, db.port);
106
- out.push({ name: 'mongodb', ok: dbOk, detail: `${db.host}:${db.port} ${dbOk ? 'reachable' : 'unreachable'}` });
107
-
108
- const redis = parseHostPort(cfg.redis.url, 6379);
109
- const redisOk = await tcpReachable(redis.host, redis.port);
110
- out.push({ name: 'redis', ok: redisOk, detail: `${redis.host}:${redis.port} ${redisOk ? 'reachable' : 'unreachable'}` });
111
-
112
- const ai = parseHostPort(cfg.ai.serverUrl, 8000);
113
- const aiOk = await tcpReachable(ai.host, ai.port);
114
- out.push({ name: 'ai server', ok: aiOk, detail: `${ai.host}:${ai.port} ${aiOk ? 'reachable' : 'unreachable'}` });
115
-
116
- const backendFree = await isPortFree(cfg.server.host, cfg.server.port, 500);
117
- out.push({ name: 'backend port', ok: backendFree, detail: `:${cfg.server.port} ${backendFree ? 'free' : 'IN USE'}` });
118
-
119
- const frontendFree = await isPortFree('127.0.0.1', cfg.frontend.port, 500);
120
- out.push({ name: 'frontend port', ok: frontendFree, detail: `:${cfg.frontend.port} ${frontendFree ? 'free' : 'in use'}` });
121
-
122
- const adminFree = await isPortFree('127.0.0.1', cfg.admin.port, 500);
123
- out.push({ name: 'admin port', ok: adminFree, detail: `:${cfg.admin.port} ${adminFree ? 'free' : 'in use'}` });
124
-
125
- return out;
85
+ /** Generate a random base64url secret. */
86
+ export function randomSecret(bytes: number = 32): string {
87
+ const buf = new Uint8Array(bytes);
88
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
89
+ globalThis.crypto.getRandomValues(buf);
90
+ } else {
91
+ for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256);
92
+ }
93
+ return Buffer.from(buf).toString('base64url');
126
94
  }
127
-
128
- /** Returns true if `path` exists and its directory looks like a nexus project root. */
129
- export function looksLikeProject(root: string): boolean {
130
- return existsSync(join(root, 'package.json')) &&
131
- ['ts', 'js', 'mjs', 'cjs'].some((ext) => existsSync(join(root, `nexus.config.${ext}`)));
132
- }
@@ -0,0 +1,68 @@
1
+ # Multi-stage Docker build for <%= name %>.
2
+ # Targets: backend-<name>, frontend-<name>, admin, ai-server.
3
+ # Build: docker compose build
4
+ # Run: docker compose up
5
+ # Scale: docker compose up --scale backend-main=3
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Backend base — Node runtime + workspace deps installed.
9
+ # ---------------------------------------------------------------------------
10
+ FROM node:22-alpine AS backend-base
11
+ WORKDIR /app
12
+ RUN corepack enable
13
+ COPY package.json package-lock.json* ./
14
+ COPY apps/backend/package.json apps/backend/package.json
15
+ RUN npm install --omit=dev --workspace=apps/backend --include-workspace-root=false
16
+ COPY apps/backend apps/backend
17
+ COPY tsconfig.json ./
18
+
19
+ EXPOSE <%= backendPort %>
20
+ ENV NODE_ENV=production
21
+ CMD ["npx", "tsx", "apps/backend/src/main.ts"]
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Frontend — React/Vite → static assets served by nginx
25
+ # ---------------------------------------------------------------------------
26
+ FROM node:22-alpine AS frontend-build
27
+ WORKDIR /app
28
+ RUN corepack enable
29
+ COPY package.json package-lock.json* ./
30
+ COPY apps/frontend apps/frontend
31
+ RUN npm install --workspace=apps/frontend --include-workspace-root=false
32
+ WORKDIR /app/apps/frontend
33
+ RUN npx vite build
34
+
35
+ FROM nginx:1.27-alpine AS frontend
36
+ COPY --from=frontend-build /app/apps/frontend/dist /usr/share/nginx/html
37
+ COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf
38
+ EXPOSE 80
39
+ CMD ["nginx", "-g", "daemon off;"]
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Admin SPA — same as frontend but separate stage for independent deploys
43
+ # ---------------------------------------------------------------------------
44
+ FROM node:22-alpine AS admin-build
45
+ WORKDIR /app
46
+ RUN corepack enable
47
+ COPY package.json package-lock.json* ./
48
+ COPY apps/admin apps/admin
49
+ RUN npm install --workspace=apps/admin --include-workspace-root=false
50
+ WORKDIR /app/apps/admin
51
+ RUN npx vite build
52
+
53
+ FROM nginx:1.27-alpine AS admin
54
+ COPY --from=admin-build /app/apps/admin/dist /usr/share/nginx/html
55
+ COPY apps/admin/nginx.conf /etc/nginx/conf.d/default.conf
56
+ EXPOSE 80
57
+ CMD ["nginx", "-g", "daemon off;"]
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # AI server — Python FastAPI
61
+ # ---------------------------------------------------------------------------
62
+ FROM python:3.13-slim AS ai-server
63
+ WORKDIR /app
64
+ COPY apps/ai-server/requirements.txt ./
65
+ RUN pip install --no-cache-dir -r requirements.txt
66
+ COPY apps/ai-server ./
67
+ EXPOSE <%= aiPort %>
68
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "<%= aiPort %>"]
@@ -0,0 +1,62 @@
1
+ # <%= name %>
2
+
3
+ Built with [BhooAI Nexus](https://github.com/bhooai/nexus).
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Default services:
13
+
14
+ - Backend API → http://localhost:<%= backendPort %>
15
+ - Frontend → http://localhost:<%= frontendPort %>
16
+ - Admin panel → http://localhost:<%= adminPort %>
17
+ - AI server → http://localhost:<%= aiPort %>
18
+
19
+ ## Project layout
20
+
21
+ ```
22
+ apps/
23
+ backend/ # Node.js backend — auto-discovers routes/, graphql/, ws/, mail/, errors/, …
24
+ src/
25
+ routes/ # HTTP endpoints
26
+ graphql/ # *.graph.ts (federation)
27
+ ws/ # *.room.ts (WebSocket rooms)
28
+ mail/ # templates/ + mailables/
29
+ errors/ # pages/ + Handler.ts
30
+ models/ services/ repositories/ controllers/ middleware/ validators/
31
+ jobs/ listeners/ events/ policies/ resources/ requests/
32
+ helpers/ config/ database/ providers/ plugins/ types/ constants/
33
+ storage/
34
+ uploads/ private/ temp/ cache/ logs/
35
+ frontend/ # React + Vite
36
+ admin/ # Admin console (wraps @bhooai/nexus-admin)
37
+ ai-server/ # Python FastAPI (optional AI features)
38
+ ```
39
+
40
+ ## Common commands
41
+
42
+ ```bash
43
+ npm run dev # start all services
44
+ npm run build # production build
45
+ npm run test # run tests
46
+ npm run doctor # environment check
47
+ npm run down # enable maintenance mode
48
+ npm run up # disable maintenance mode
49
+ ```
50
+
51
+ Generate code:
52
+
53
+ ```bash
54
+ nexus make:route users
55
+ nexus make:model Post
56
+ nexus make:job SendWelcomeEmail
57
+ nexus make:mail PasswordReset
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ Edit `nexus.config.ts`. Secrets go in `.env`. See [PLAN.md](../../../PLAN.md) for the architecture.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title><%= name %> — Admin</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,16 @@
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ location / {
8
+ try_files $uri $uri/ /index.html;
9
+ }
10
+ location /admin/ {
11
+ proxy_pass http://backend:<%= backendPort %>/admin/;
12
+ proxy_http_version 1.1;
13
+ proxy_set_header Host $host;
14
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@<%= nameSlug %>/admin",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --port <%= adminPort %>",
8
+ "build": "vite build"
9
+ },
10
+ "dependencies": {
11
+ "@bhooai/nexus-admin": "^2.0.0",
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ import '@bhooai/nexus-admin/style.css';
2
+ import NexusAdmin from '@bhooai/nexus-admin';
3
+ import React from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+
6
+ const el = document.getElementById('root');
7
+ if (el) createRoot(el).render(<NexusAdmin />);
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ port: <%= adminPort %>,
6
+ proxy: {
7
+ '/admin': {
8
+ target: 'http://localhost:<%= backendPort %>',
9
+ changeOrigin: true,
10
+ },
11
+ '/api': {
12
+ target: 'http://localhost:<%= backendPort %>',
13
+ changeOrigin: true,
14
+ rewrite: (p: string) => p.replace(/^\/api/, ''),
15
+ },
16
+ '/health': {
17
+ target: 'http://localhost:<%= backendPort %>',
18
+ changeOrigin: true,
19
+ },
20
+ },
21
+ },
22
+ });
@@ -0,0 +1,30 @@
1
+ """AI server (FastAPI) — provides /health and OpenAI-compatible /v1/chat/completions stub.
2
+
3
+ Extend with real provider integrations (Ollama, OpenAI, Anthropic, etc.).
4
+ """
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ app = FastAPI(title="<%= name %> AI server")
9
+
10
+ app.add_middleware(
11
+ CORSMiddleware,
12
+ allow_origins=["*"],
13
+ allow_methods=["*"],
14
+ allow_headers=["*"],
15
+ )
16
+
17
+
18
+ @app.get("/health")
19
+ async def health():
20
+ return {"ok": True, "service": "ai", "app": "<%= nameSlug %>"}
21
+
22
+
23
+ @app.get("/v1/models")
24
+ async def models():
25
+ return {"data": [{"id": "default", "object": "model"}]}
26
+
27
+
28
+ if __name__ == "__main__":
29
+ import uvicorn
30
+ uvicorn.run(app, host="127.0.0.1", port=<%= aiPort %>)
@@ -0,0 +1,4 @@
1
+ fastapi>=0.115
2
+ uvicorn[standard]>=0.30
3
+ httpx>=0.27
4
+ pydantic>=2.0
@@ -0,0 +1,14 @@
1
+ import { createNexusApp } from '@bhooai/nexus-core';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const HERE = dirname(fileURLToPath(import.meta.url));
6
+
7
+ const app = await createNexusApp({
8
+ name: 'backend',
9
+ srcRoot: HERE, // apps/backend/src
10
+ projectRoot: resolve(HERE, '..', '..', '..'), // workspace root
11
+ });
12
+
13
+ await app.listen();
14
+ console.log(`[backend] http://${app.config.server.host}:${app.config.server.port}`);
@@ -0,0 +1,11 @@
1
+ import { defineRoutes } from '@bhooai/nexus-core';
2
+
3
+ export default defineRoutes([
4
+ {
5
+ method: 'GET',
6
+ path: '/',
7
+ handler: (ctx) => {
8
+ ctx.json({ ok: true, name: '<%= nameSlug %>', docs: 'https://github.com/bhooai/nexus' });
9
+ },
10
+ },
11
+ ]);