bhooai-nexus 3.0.6 → 3.0.7
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 +2 -1
- package/packages/nexus-admin/package.json +1 -1
- package/packages/nexus-ads/package.json +1 -1
- package/packages/nexus-ai-client/package.json +1 -1
- package/packages/nexus-auth/package.json +1 -1
- package/packages/nexus-cache/package.json +1 -1
- package/packages/nexus-cli/package.json +2 -1
- package/packages/nexus-cli/src/browser.ts +4 -2
- package/packages/nexus-cli/src/commands/dev.ts +15 -3
- package/packages/nexus-cli/src/commands/doctor.ts +140 -29
- package/packages/nexus-cli/src/commands/init.ts +22 -4
- package/packages/nexus-cli/src/commands/license.ts +115 -31
- package/packages/nexus-cli/src/commands/sync.ts +310 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +1002 -0
- package/packages/nexus-cli/src/devPanel.ts +88 -7
- package/packages/nexus-cli/src/devServiceManager.ts +1 -1
- package/packages/nexus-cli/src/dispatcher.ts +16 -50
- package/packages/nexus-cli/src/helpDoc.ts +207 -0
- package/packages/nexus-cli/src/index.ts +13 -11
- package/packages/nexus-cli/src/launcher.ts +31 -32
- package/packages/nexus-cli/src/wizard.ts +68 -11
- package/packages/nexus-cli/templates/base/dockerignore +1 -0
- package/packages/nexus-cli/templates/base/gitignore +2 -1
- package/packages/nexus-cli/templates/base/nexus/licence/.gitkeep +0 -0
- package/packages/nexus-cli/templates/base/nexus.config.ts.ejs +18 -9
- package/packages/nexus-core/package.json +1 -1
- package/packages/nexus-core/src/app/createNexusApp.ts +1 -1
- package/packages/nexus-core/src/config/defaults.ts +2 -2
- package/packages/nexus-core/src/config/schema.ts +3 -3
- package/packages/nexus-core/src/config/types.ts +2 -2
- package/packages/nexus-crypto/package.json +1 -1
- package/packages/nexus-crypto/src/license.ts +46 -16
- package/packages/nexus-data/package.json +1 -1
- package/packages/nexus-email/package.json +1 -1
- package/packages/nexus-examples/examples/ai-chat/apps/frontend/src/App.tsx +10 -1
- package/packages/nexus-examples/package.json +1 -1
- package/packages/nexus-fusion/package.json +1 -1
- package/packages/nexus-fusion-core/package.json +1 -1
- package/packages/nexus-future/package.json +1 -1
- package/packages/nexus-graphql/package.json +1 -1
- package/packages/nexus-payments/package.json +1 -1
- package/packages/nexus-plugins/package.json +1 -1
- package/packages/nexus-postcss/package.json +1 -1
- package/packages/nexus-realtime/package.json +1 -1
- package/packages/nexus-safe-goto/package.json +1 -1
- package/packages/nexus-telemetry/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bhooai-nexus",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"description": "BhooAI Nexus - a Node-based full-stack framework (websockets, GraphQL federation, inbuilt security, custom ODM, OAuth, payments, WebRTC, AI, plugins). v2: multi-app, Laravel-style folders, Docker-based scaling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BhooAI - oravianando",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"init": "node bin/nexus.js init",
|
|
98
98
|
"doctor": "node bin/nexus.js doctor",
|
|
99
99
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
100
|
+
"publish:packages": "node scripts/publish.mjs",
|
|
100
101
|
"lint": "eslint \"packages/**/*.ts\" \"apps/**/*.{ts,tsx}\"",
|
|
101
102
|
"format": "prettier --write ."
|
|
102
103
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bhooai/nexus-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
4
4
|
"description": "BhooAI Nexus v2 CLI — init, dev, build, test, scaffolding, plugins, queue, db.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@bhooai/nexus-core": "^2.0.1",
|
|
22
22
|
"@bhooai/nexus-crypto": "^2.0.1",
|
|
23
23
|
"@bhooai/nexus-examples": "^2.0.1",
|
|
24
|
+
"mongodb": "^6.9.0",
|
|
24
25
|
"tsx": "^4.19.0",
|
|
25
26
|
"zod": "^3.23.8"
|
|
26
27
|
},
|
|
@@ -7,8 +7,10 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
export function openBrowser(url: string): void {
|
|
8
8
|
try {
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
|
-
// `
|
|
11
|
-
|
|
10
|
+
// `rundll32` receives the URL as a plain argv (no shell), so `&` in the
|
|
11
|
+
// query string is NOT treated as a command separator. `cmd /c start`
|
|
12
|
+
// would truncate at the first `&`, dropping state/code_challenge.
|
|
13
|
+
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore', detached: true }).unref();
|
|
12
14
|
} else if (process.platform === 'darwin') {
|
|
13
15
|
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
14
16
|
} else {
|
|
@@ -12,7 +12,7 @@ import { join, dirname } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import type { CommandContext } from '../dispatcher.js';
|
|
14
14
|
import { loadConfigAuto } from '@bhooai/nexus-core';
|
|
15
|
-
import { ensureLicense } from '@bhooai/nexus-crypto';
|
|
15
|
+
import { ensureLicense, resolveLicenseToken } from '@bhooai/nexus-crypto';
|
|
16
16
|
import { resolveConfiguredPorts, findFreeShift, describePortHolder } from '../ports.js';
|
|
17
17
|
import { ServiceManager, type ServiceSpec } from '../devServiceManager.js';
|
|
18
18
|
import { startDevPanel } from '../devPanel.js';
|
|
@@ -31,12 +31,19 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
31
31
|
// check fails we still start the services: the backend boots in a
|
|
32
32
|
// license-error state and answers requests with 402 LICENSE_REQUIRED, so the
|
|
33
33
|
// UI can show the error instead of a proxy 500.
|
|
34
|
+
// The console's "Add license" banner is offered ONLY when no license token
|
|
35
|
+
// exists at all (missing nexus/licence/licence.jwt and no NEXUS_LICENSE_KEY) —
|
|
36
|
+
// transient verify failures (offline authority, expired token, …) just warn.
|
|
37
|
+
let licenseError: string | null = null;
|
|
34
38
|
try {
|
|
35
39
|
await ensureLicense({ projectDir: projectRoot });
|
|
36
40
|
} catch (err) {
|
|
37
|
-
|
|
41
|
+
licenseError = (err as Error).message;
|
|
42
|
+
console.warn(`⚠ License check failed: ${licenseError}`);
|
|
43
|
+
console.warn(' Run `nexus license add` to authorize this project.');
|
|
38
44
|
console.warn(' Starting anyway — API requests will return 402 LICENSE_REQUIRED.');
|
|
39
45
|
}
|
|
46
|
+
const licenseMissing = !resolveLicenseToken({ projectDir: projectRoot });
|
|
40
47
|
|
|
41
48
|
const noPanel = !!ctx.args.flags['no-panel'];
|
|
42
49
|
const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -47,6 +54,11 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
47
54
|
return;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
// Expose the project root to every service so license resolution finds
|
|
58
|
+
// `<project>/nexus/licence/licence.jwt` even when an app's own projectRoot is a
|
|
59
|
+
// subfolder (e.g. apps/backend).
|
|
60
|
+
for (const s of specs) s.env = { ...(s.env ?? {}), NEXUS_PROJECT_ROOT: projectRoot };
|
|
61
|
+
|
|
50
62
|
// Coordinated step-up: if any configured port is busy, shift the whole set
|
|
51
63
|
// by the smallest delta that frees every port. nexus.config.ts stays
|
|
52
64
|
// canonical — the shift is ephemeral for this run (NEXUS_PORT_STEP).
|
|
@@ -99,7 +111,7 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
99
111
|
if (isTty && !noPanel) {
|
|
100
112
|
// Full-screen Nexus Console.
|
|
101
113
|
console.log(`nexus dev — ${specs.length} service(s). Opening console...\n`);
|
|
102
|
-
await startDevPanel({ services: manager, onQuit: teardown });
|
|
114
|
+
await startDevPanel({ services: manager, onQuit: teardown, licenseError: licenseMissing ? licenseError : null });
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nexus doctor — environment health check. Never hard-fails; reports issues.
|
|
3
|
+
* Renders a full-screen TUI on a TTY (with a live spinner) and plain text when
|
|
4
|
+
* piped. `nexus doctor` and the welcome launcher's "Environment check" both use it.
|
|
3
5
|
*/
|
|
4
6
|
import { exec } from 'node:child_process';
|
|
5
7
|
import { promisify } from 'node:util';
|
|
@@ -7,11 +9,22 @@ import { existsSync } from 'node:fs';
|
|
|
7
9
|
import { resolve } from 'node:path';
|
|
8
10
|
import { createConnection } from 'node:net';
|
|
9
11
|
import type { CommandContext } from '../dispatcher.js';
|
|
12
|
+
import { ANSI, Tui, isTty, getTermSize, type KeyInfo } from '../tui.js';
|
|
13
|
+
import { fitCell } from '../layout.js';
|
|
10
14
|
|
|
11
15
|
const execAsync = promisify(exec);
|
|
12
16
|
|
|
17
|
+
export interface CheckResult {
|
|
18
|
+
name: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
detail?: string;
|
|
21
|
+
/** Optional checks warn (•) but never count as failures. */
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
interface Check {
|
|
14
26
|
name: string;
|
|
27
|
+
optional?: boolean;
|
|
15
28
|
run(): Promise<{ ok: boolean; detail?: string }>;
|
|
16
29
|
}
|
|
17
30
|
|
|
@@ -39,11 +52,8 @@ async function tcpReachable(host: string, port: number, timeoutMs: number = 1000
|
|
|
39
52
|
});
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const projectRoot = process.cwd();
|
|
46
|
-
const checks: Check[] = [
|
|
55
|
+
function buildChecks(projectRoot: string): Check[] {
|
|
56
|
+
return [
|
|
47
57
|
{
|
|
48
58
|
name: 'Node.js ≥ 20.10',
|
|
49
59
|
run: async () => {
|
|
@@ -66,50 +76,151 @@ export async function run(_ctx: CommandContext): Promise<void> {
|
|
|
66
76
|
},
|
|
67
77
|
{
|
|
68
78
|
name: 'Python (optional — for AI server)',
|
|
79
|
+
optional: true,
|
|
69
80
|
run: async () => {
|
|
70
81
|
const v = await cmdVersion('python', ['--version']);
|
|
71
|
-
return v ? { ok: true, detail: v } : { ok: false, detail: 'python not found
|
|
82
|
+
return v ? { ok: true, detail: v } : { ok: false, detail: 'python not found' };
|
|
72
83
|
},
|
|
73
84
|
},
|
|
74
85
|
{
|
|
75
86
|
name: 'Project layout (apps/backend)',
|
|
76
|
-
run: async () =>
|
|
77
|
-
ok
|
|
78
|
-
detail:
|
|
79
|
-
}
|
|
87
|
+
run: async () => {
|
|
88
|
+
const ok = existsSync(resolve(projectRoot, 'apps/backend'));
|
|
89
|
+
return { ok, detail: ok ? 'found' : 'apps/backend missing' };
|
|
90
|
+
},
|
|
80
91
|
},
|
|
81
92
|
{
|
|
82
93
|
name: 'nexus.config.ts',
|
|
83
|
-
run: async () =>
|
|
84
|
-
ok
|
|
85
|
-
detail:
|
|
86
|
-
}
|
|
94
|
+
run: async () => {
|
|
95
|
+
const ok = existsSync(resolve(projectRoot, 'nexus.config.ts'));
|
|
96
|
+
return { ok, detail: ok ? 'found' : 'missing' };
|
|
97
|
+
},
|
|
87
98
|
},
|
|
88
99
|
{
|
|
89
100
|
name: 'MongoDB reachable (localhost:27017)',
|
|
90
|
-
run: async () =>
|
|
91
|
-
ok
|
|
92
|
-
|
|
93
|
-
}
|
|
101
|
+
run: async () => {
|
|
102
|
+
const ok = await tcpReachable('127.0.0.1', 27017);
|
|
103
|
+
return { ok, detail: ok ? 'ok' : 'not reachable' };
|
|
104
|
+
},
|
|
94
105
|
},
|
|
95
106
|
{
|
|
96
107
|
name: 'Redis reachable (localhost:6379)',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
optional: true,
|
|
109
|
+
run: async () => {
|
|
110
|
+
const ok = await tcpReachable('127.0.0.1', 6379);
|
|
111
|
+
return { ok, detail: ok ? 'ok' : 'not reachable' };
|
|
112
|
+
},
|
|
101
113
|
},
|
|
102
114
|
];
|
|
115
|
+
}
|
|
103
116
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (!result.ok) failed++;
|
|
117
|
+
/** Run every check and return structured results. */
|
|
118
|
+
export async function runChecks(projectRoot: string): Promise<CheckResult[]> {
|
|
119
|
+
const out: CheckResult[] = [];
|
|
120
|
+
for (const check of buildChecks(projectRoot)) {
|
|
121
|
+
const r = await check.run().catch((e) => ({ ok: false, detail: String(e) }));
|
|
122
|
+
out.push({ name: check.name, ok: r.ok, detail: r.detail, optional: check.optional });
|
|
111
123
|
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
128
|
+
|
|
129
|
+
function statusIcon(r: CheckResult): string {
|
|
130
|
+
if (r.ok) return `${ANSI.green}✓${ANSI.reset}`;
|
|
131
|
+
return r.optional ? `${ANSI.yellow}•${ANSI.reset}` : `${ANSI.red}✗${ANSI.reset}`;
|
|
132
|
+
}
|
|
112
133
|
|
|
134
|
+
function renderResults(results: CheckResult[]): string[] {
|
|
135
|
+
const W = Math.min(getTermSize().cols - 4, 84);
|
|
136
|
+
const nameW = Math.min(Math.max(20, ...results.map((r) => r.name.length)) + 2, 46);
|
|
137
|
+
const lines: string[] = [
|
|
138
|
+
`${ANSI.bold}${ANSI.cyan}nexus doctor${ANSI.reset} ${ANSI.gray}— environment check${ANSI.reset}`,
|
|
139
|
+
`${ANSI.gray}${'─'.repeat(Math.max(W, 10))}${ANSI.reset}`,
|
|
140
|
+
'',
|
|
141
|
+
];
|
|
142
|
+
for (const r of results) {
|
|
143
|
+
const detail = r.detail ? ` ${ANSI.gray}(${r.detail})${ANSI.reset}` : '';
|
|
144
|
+
lines.push(` ${statusIcon(r)} ${fitCell(r.name, nameW)}${detail}`);
|
|
145
|
+
}
|
|
146
|
+
const failed = results.filter((r) => !r.ok && !r.optional).length;
|
|
147
|
+
const warned = results.filter((r) => !r.ok && r.optional).length;
|
|
148
|
+
lines.push('');
|
|
149
|
+
if (failed === 0) {
|
|
150
|
+
const extra = warned > 0 ? ` ${ANSI.gray}(${warned} optional unavailable)${ANSI.reset}` : '';
|
|
151
|
+
lines.push(` ${ANSI.green}All checks passed.${ANSI.reset}${extra}`);
|
|
152
|
+
} else {
|
|
153
|
+
lines.push(` ${ANSI.red}${failed} check(s) need attention.${ANSI.reset}`);
|
|
154
|
+
}
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Full-screen doctor TUI: spinner while running, `r` to rerun, `q` to close. */
|
|
159
|
+
async function runDoctorTui(projectRoot: string): Promise<void> {
|
|
160
|
+
let results: CheckResult[] = [];
|
|
161
|
+
let running = true;
|
|
162
|
+
let spin = 0;
|
|
163
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
164
|
+
|
|
165
|
+
const render = (tui: Tui): void => {
|
|
166
|
+
const lines: string[] = [
|
|
167
|
+
`${ANSI.bold}${ANSI.cyan}nexus doctor${ANSI.reset} ${ANSI.gray}— environment check${ANSI.reset}`,
|
|
168
|
+
`${ANSI.gray}${'─'.repeat(Math.max(Math.min(getTermSize().cols - 4, 84), 10))}${ANSI.reset}`,
|
|
169
|
+
'',
|
|
170
|
+
];
|
|
171
|
+
if (running) {
|
|
172
|
+
lines.push(` ${ANSI.cyan}${SPINNER[spin % SPINNER.length]}${ANSI.reset} ${ANSI.gray}running checks…${ANSI.reset}`);
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(...renderResults(results).slice(3));
|
|
175
|
+
}
|
|
176
|
+
const footer = `${ANSI.gray}${running ? 'please wait…' : 'r rerun · q close'}${ANSI.reset}`;
|
|
177
|
+
tui.draw(lines, footer);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const tui: Tui = new Tui((_str: string, key: KeyInfo) => {
|
|
181
|
+
if (key.name === 'q' || key.name === 'escape' || (key.name === 'c' && key.ctrl)) {
|
|
182
|
+
tui.quit = true;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (key.name === 'r' && !running) void refresh();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const refresh = async (): Promise<void> => {
|
|
189
|
+
running = true;
|
|
190
|
+
if (!timer) timer = setInterval(() => { spin++; render(tui); }, 90);
|
|
191
|
+
render(tui);
|
|
192
|
+
results = await runChecks(projectRoot);
|
|
193
|
+
running = false;
|
|
194
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
195
|
+
render(tui);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
tui.enter();
|
|
199
|
+
const onResize = (): void => render(tui);
|
|
200
|
+
process.stdout.on('resize', onResize);
|
|
201
|
+
await refresh();
|
|
202
|
+
await tui.wait();
|
|
203
|
+
process.stdout.off('resize', onResize);
|
|
204
|
+
if (timer) clearInterval(timer);
|
|
205
|
+
tui.exit();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function run(_ctx: CommandContext): Promise<void> {
|
|
209
|
+
const projectRoot = process.cwd();
|
|
210
|
+
|
|
211
|
+
if (isTty()) {
|
|
212
|
+
await runDoctorTui(projectRoot);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log('nexus doctor — environment check\n');
|
|
217
|
+
const results = await runChecks(projectRoot);
|
|
218
|
+
for (const r of results) {
|
|
219
|
+
const icon = r.ok ? '✓' : r.optional ? '•' : '✗';
|
|
220
|
+
const detail = r.detail ? ` (${r.detail})` : '';
|
|
221
|
+
console.log(`${icon} ${r.name}${detail}`);
|
|
222
|
+
}
|
|
223
|
+
const failed = results.filter((r) => !r.ok && !r.optional).length;
|
|
113
224
|
console.log(failed === 0 ? '\nAll checks passed.' : `\n${failed} check(s) need attention.`);
|
|
114
225
|
}
|
|
115
226
|
|
|
@@ -32,6 +32,7 @@ interface InitVars {
|
|
|
32
32
|
name: string;
|
|
33
33
|
nameSlug: string;
|
|
34
34
|
jwtSecret: string;
|
|
35
|
+
dbActive: 'fusion' | 'mongodb';
|
|
35
36
|
mongoUri: string;
|
|
36
37
|
redisUrl: string;
|
|
37
38
|
example: string;
|
|
@@ -41,6 +42,7 @@ interface InitVars {
|
|
|
41
42
|
adminPort: number;
|
|
42
43
|
aiPort: number;
|
|
43
44
|
pyserverPort: number;
|
|
45
|
+
fusionPort: number;
|
|
44
46
|
aiProviders: string[];
|
|
45
47
|
/** Rendered `apps: [...]` block body (port layout) for nexus.config.ts. */
|
|
46
48
|
appsBlock: string;
|
|
@@ -84,6 +86,7 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
84
86
|
name,
|
|
85
87
|
nameSlug: name,
|
|
86
88
|
jwtSecret: randomSecret(32),
|
|
89
|
+
dbActive: (result as { dbActive?: 'fusion' | 'mongodb' }).dbActive ?? 'fusion',
|
|
87
90
|
mongoUri: result.mongoUri,
|
|
88
91
|
redisUrl: result.redisUrl,
|
|
89
92
|
example: result.example,
|
|
@@ -93,6 +96,7 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
93
96
|
adminPort: result.ports.admin,
|
|
94
97
|
aiPort: result.ports.ai,
|
|
95
98
|
pyserverPort: result.ports.pyserver,
|
|
99
|
+
fusionPort: result.ports.fusion,
|
|
96
100
|
aiProviders: result.aiProviders,
|
|
97
101
|
appsBlock: [
|
|
98
102
|
` { name: 'backend', port: ${result.ports.backend} },`,
|
|
@@ -100,6 +104,7 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
100
104
|
...(includeAdmin ? [` { name: 'admin', port: ${result.ports.admin} },`] : []),
|
|
101
105
|
...(includeAi ? [` { name: 'ai-server', port: ${result.ports.ai} },`] : []),
|
|
102
106
|
` { name: 'pyserver', port: ${result.ports.pyserver} },`,
|
|
107
|
+
` { name: 'fusion', port: ${result.ports.fusion} },`,
|
|
103
108
|
].join('\n'),
|
|
104
109
|
};
|
|
105
110
|
|
|
@@ -199,7 +204,7 @@ export async function run(ctx: CommandContext): Promise<void> {
|
|
|
199
204
|
// 4. .env
|
|
200
205
|
const envPath = join(targetDir, '.env');
|
|
201
206
|
if (!existsSync(envPath)) {
|
|
202
|
-
await writeFile(envPath, buildEnvFile(vars), 'utf-8');
|
|
207
|
+
await writeFile(envPath, buildEnvFile(vars, devMode), 'utf-8');
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
// 4b. --dev: rewrite bhooai-nexus + @bhooai/* to local file: + live symlink
|
|
@@ -253,7 +258,9 @@ Ports allocated:
|
|
|
253
258
|
${frontendLine}
|
|
254
259
|
Admin : ${includeAdmin ? vars.adminPort : '(not included)'}
|
|
255
260
|
AI server : ${includeAi ? vars.aiPort : '(not included)'}
|
|
256
|
-
|
|
261
|
+
PyServer : ${vars.pyserverPort}
|
|
262
|
+
Fusion DB : ${vars.fusionPort}
|
|
263
|
+
`);
|
|
257
264
|
|
|
258
265
|
// Ensure stdin is paused so Node can drain naturally after TUI exit (even on npm failure → exit to shell).
|
|
259
266
|
try { process.stdin.pause(); } catch {}
|
|
@@ -439,7 +446,7 @@ async function ensureBackendFolderTree(backendDir: string): Promise<void> {
|
|
|
439
446
|
}
|
|
440
447
|
}
|
|
441
448
|
|
|
442
|
-
function buildEnvFile(vars: InitVars): string {
|
|
449
|
+
function buildEnvFile(vars: InitVars, dev = false): string {
|
|
443
450
|
return `# BhooAI Nexus — generated .env
|
|
444
451
|
# Secrets live here. Do not commit.
|
|
445
452
|
|
|
@@ -451,13 +458,20 @@ AI_PORT=${vars.aiPort}
|
|
|
451
458
|
# Set this to the backend host when deploying to a VPS (default: localhost)
|
|
452
459
|
# NEXUS_BACKEND_HOST=
|
|
453
460
|
|
|
454
|
-
# Database
|
|
461
|
+
# Database — Fusion is the default (embedded, zero-dep).
|
|
462
|
+
# Set NEXUS_DB_ACTIVE=mongodb + NEXUS_DB_MONGODB_ENABLED=true to use Mongo.
|
|
463
|
+
NEXUS_DB_ACTIVE=${vars.dbActive}
|
|
455
464
|
NEXUS_DB_URI=${vars.mongoUri}
|
|
456
465
|
NEXUS_REDIS_URL=${vars.redisUrl}
|
|
457
466
|
|
|
458
467
|
# Auth
|
|
459
468
|
NEXUS_AUTH_JWT_SECRET=${vars.jwtSecret}
|
|
460
469
|
|
|
470
|
+
# License authority
|
|
471
|
+
${dev
|
|
472
|
+
? '# --dev: verify against the LOCAL authority (nexus-bhooai-com on :3000).\nNEXUS_LICENSE_SERVER=http://localhost:3000'
|
|
473
|
+
: '# Defaults to https://nexus.bhooai.com — override with NEXUS_LICENSE_SERVER.'}
|
|
474
|
+
|
|
461
475
|
# AI (ollama is local by default; uncomment others as needed)
|
|
462
476
|
NEXUS_AI_PROVIDER=ollama
|
|
463
477
|
NEXUS_AI_MODEL=llama3.1:8b
|
|
@@ -469,6 +483,10 @@ ${vars.aiProviders.filter((p) => p !== 'ollama').map((p) => `# NEXUS_AI_${p.toUp
|
|
|
469
483
|
PY_PORT=${vars.pyserverPort}
|
|
470
484
|
NEXUS_IMAGES_URL=http://127.0.0.1:${vars.pyserverPort}
|
|
471
485
|
|
|
486
|
+
# Fusion DB server (nexus dev service "fusion", port from nexus.config.ts apps[])
|
|
487
|
+
FUSION_PORT=${vars.fusionPort}
|
|
488
|
+
NEXUS_FUSION_URL=http://127.0.0.1:${vars.fusionPort}
|
|
489
|
+
|
|
472
490
|
# Storage (uncomment for S3)
|
|
473
491
|
# NEXUS_STORAGE_S3_BUCKET=
|
|
474
492
|
# NEXUS_STORAGE_S3_REGION=
|