@bhooai/nexus-cli 0.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -12
- package/src/commands/add.ts +274 -0
- package/src/commands/build.ts +91 -0
- package/src/commands/db.ts +241 -0
- package/src/commands/dev.ts +149 -186
- package/src/commands/doctor.ts +102 -184
- package/src/commands/init.ts +204 -958
- package/src/commands/maintenance-commands.ts +40 -0
- package/src/commands/plugin.ts +233 -0
- package/src/commands/queue.ts +145 -0
- package/src/commands/test.ts +34 -0
- package/src/dispatcher.ts +98 -0
- package/src/generators.ts +102 -0
- package/src/index.ts +32 -90
- package/src/make/generators.ts +431 -0
- package/src/ports.ts +85 -0
- package/src/prompts.ts +68 -0
- package/src/templating/render.ts +109 -0
- package/src/util.ts +74 -112
- package/templates/base/Dockerfile.ejs +68 -0
- package/templates/base/README.md.ejs +62 -0
- package/templates/base/apps/admin/index.html.ejs +12 -0
- package/templates/base/apps/admin/nginx.conf.ejs +16 -0
- package/templates/base/apps/admin/package.json.ejs +15 -0
- package/templates/base/apps/admin/src/main.tsx.ejs +7 -0
- package/templates/base/apps/admin/vite.config.ts.ejs +22 -0
- package/templates/base/apps/ai-server/main.py.ejs +30 -0
- package/templates/base/apps/ai-server/requirements.txt +4 -0
- package/templates/base/apps/backend/src/main.ts +14 -0
- package/templates/base/apps/backend/src/routes/index.ts.ejs +11 -0
- package/templates/base/apps/frontend/index.html.ejs +12 -0
- package/templates/base/apps/frontend/nginx.conf.ejs +33 -0
- package/templates/base/apps/frontend/package.json.ejs +11 -0
- package/templates/base/apps/frontend/src/main.tsx.ejs +16 -0
- package/templates/base/apps/frontend/vite.config.ts.ejs +22 -0
- package/templates/base/docker-compose.yml.ejs +93 -0
- package/templates/base/dockerignore +21 -0
- package/templates/base/gitignore +43 -0
- package/templates/base/nexus.config.ts.ejs +97 -0
- package/templates/base/package.json.ejs +40 -0
- package/templates/{tsconfig.json → base/tsconfig.json} +2 -3
- package/tsconfig.json +5 -4
- package/PLAN.md +0 -141
- package/README.md +0 -34
- package/src/commands/cluster.ts +0 -133
- package/src/commands/node.ts +0 -106
- package/src/commands/pysetup.ts +0 -142
- package/src/commands/sync.ts +0 -116
- package/src/commands/uninstall.ts +0 -287
- package/src/config-sync.ts +0 -384
- package/src/dotenv.ts +0 -39
- package/src/supervisor.ts +0 -394
- package/src/wizard.ts +0 -149
- package/templates/Dockerfile +0 -60
- package/templates/README.md +0 -69
- package/templates/apps/admin/index.html +0 -12
- package/templates/apps/admin/package.json +0 -22
- package/templates/apps/admin/postcss.config.js +0 -3
- package/templates/apps/admin/src/main.tsx +0 -10
- package/templates/apps/admin/src/vite-env.d.ts +0 -18
- package/templates/apps/admin/tailwind.config.js +0 -9
- package/templates/apps/admin/tsconfig.json +0 -17
- package/templates/apps/admin/vite.config.ts +0 -64
- package/templates/apps/ai-server/main.py +0 -43
- package/templates/apps/ai-server/providers/__init__.py +0 -3
- package/templates/apps/ai-server/providers/base.py +0 -111
- package/templates/apps/ai-server/requirements.txt +0 -3
- package/templates/apps/ai-server/routers/__init__.py +0 -3
- package/templates/apps/ai-server/routers/chat.py +0 -47
- package/templates/apps/ai-server/routers/embeddings.py +0 -30
- package/templates/apps/ai-server/routers/lint.py +0 -167
- package/templates/apps/ai-server/routers/models.py +0 -23
- package/templates/apps/ai-server/routers/preflight.py +0 -169
- package/templates/apps/ai-server/settings.py +0 -48
- package/templates/apps/backend/package.json +0 -34
- package/templates/apps/backend/src/main.ts +0 -375
- package/templates/apps/backend/src/modules/admin/adminRoutes.ts +0 -732
- package/templates/apps/backend/src/modules/admin/clusterRoutes.ts +0 -391
- package/templates/apps/backend/src/modules/admin/databaseRoutes.ts +0 -161
- package/templates/apps/backend/src/modules/admin/lintProxy.ts +0 -89
- package/templates/apps/backend/src/modules/admin/preflightProxy.ts +0 -242
- package/templates/apps/backend/src/modules/admin/roleCatalog.ts +0 -78
- package/templates/apps/backend/src/modules/admin/schemaRoutes.ts +0 -449
- package/templates/apps/backend/src/modules/ai/aiProxy.ts +0 -265
- package/templates/apps/backend/src/modules/auth/authRoutes.ts +0 -220
- package/templates/apps/backend/src/modules/payments/paymentRoutes.ts +0 -100
- package/templates/apps/backend/src/modules/payments/paymentStore.ts +0 -172
- package/templates/apps/backend/src/modules/requests/requestLog.ts +0 -175
- package/templates/apps/backend/src/modules/users/userGraph.ts +0 -83
- package/templates/apps/backend/src/modules/users/userModel.ts +0 -88
- package/templates/apps/backend/src/plugins/CronScheduler.ts +0 -69
- package/templates/apps/backend/src/plugins/loadPlugins.ts +0 -107
- package/templates/apps/backend/tsconfig.json +0 -14
- package/templates/apps/frontend/index.html +0 -12
- package/templates/apps/frontend/package.json +0 -20
- package/templates/apps/frontend/postcss.config.js +0 -3
- package/templates/apps/frontend/src/index.css +0 -44
- package/templates/apps/frontend/src/main.tsx +0 -65
- package/templates/apps/frontend/tailwind.config.js +0 -9
- package/templates/apps/frontend/vite.config.ts +0 -72
- package/templates/bin/nexus.js +0 -35
- package/templates/bin/serve-all.mjs +0 -45
- package/templates/dockerignore +0 -15
- package/templates/gitignore +0 -12
- package/templates/nexus.config.ts +0 -69
- package/templates/package.json +0 -47
- package/tests/cli.test.ts +0 -52
- package/tests/config-sync.test.ts +0 -201
- package/tests/dotenv.test.ts +0 -51
- package/vitest.config.ts +0 -9
- package/vitest.config.ts.timestamp-1786095205351-444061f6ff5c58.mjs +0 -13
- /package/templates/{uploads → base/apps/backend/storage}/.gitkeep +0 -0
package/src/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, '&')
|
|
88
|
+
.replace(/</g, '<')
|
|
89
|
+
.replace(/>/g, '>')
|
|
90
|
+
.replace(/"/g, '"')
|
|
91
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
6
|
+
import { resolve } from 'node:path';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
export interface ParsedArgs {
|
|
9
|
+
_: string[];
|
|
10
|
+
flags: Record<string, string | boolean>;
|
|
11
|
+
}
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
/**
|
|
16
|
-
export function
|
|
17
|
-
return new Promise((
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
/**
|
|
32
|
-
export async function
|
|
33
|
-
|
|
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
|
-
/**
|
|
37
|
-
export function
|
|
38
|
-
|
|
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
|
-
|
|
68
|
+
/** Check directory existence. */
|
|
69
|
+
export function dirExists(p: string): boolean {
|
|
47
70
|
try {
|
|
48
|
-
|
|
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
|
-
/**
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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,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,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}`);
|