@mmmbuto/nexuscrew 0.8.0 → 0.8.3
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/README.md +66 -60
- package/bin/nexuscrew.js +10 -4
- package/frontend/dist/assets/index-BFyPeZsL.js +90 -0
- package/frontend/dist/assets/index-COsoJxXQ.css +32 -0
- package/frontend/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +98 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +314 -110
- package/lib/cli/doctor.js +1 -1
- package/lib/cli/init.js +40 -10
- package/lib/cli/pidfile.js +2 -2
- package/lib/cli/service.js +0 -5
- package/lib/decks/store.js +8 -2
- package/lib/files/routes.js +3 -1
- package/lib/fleet/builtin.js +15 -5
- package/lib/fleet/managed.js +278 -107
- package/lib/mcp/server.js +24 -5
- package/lib/nodes/commands.js +27 -12
- package/lib/nodes/peering.js +116 -0
- package/lib/nodes/store.js +91 -24
- package/lib/nodes/topology-cache.js +75 -0
- package/lib/nodes/tunnel.js +22 -7
- package/lib/proxy/federation.js +263 -0
- package/lib/proxy/node-proxy.js +21 -8
- package/lib/server.js +91 -7
- package/lib/settings/routes.js +221 -5
- package/package.json +2 -2
- package/frontend/dist/assets/index-BBOgTCoO.css +0 -32
- package/frontend/dist/assets/index-DQrDBogT.js +0 -83
package/lib/cli/commands.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
const { execFileSync, spawn } = require('node:child_process');
|
|
7
7
|
const fs = require('node:fs');
|
|
8
8
|
const path = require('node:path');
|
|
9
|
+
const net = require('node:net');
|
|
9
10
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
|
|
10
11
|
const { installPath: serviceInstallPath } = require('./service.js');
|
|
12
|
+
const { fleetInstallPath } = require('./fleet-service.js');
|
|
11
13
|
const pidf = require('./pidfile.js');
|
|
12
14
|
const { runInit } = require('./init.js');
|
|
13
15
|
const { rotateToken } = require('../auth/token.js');
|
|
@@ -20,35 +22,22 @@ const nodesTunnel = require('../nodes/tunnel.js');
|
|
|
20
22
|
// Flag CLI che consumano il token successivo (forma "--k v", oltre a "--k=v").
|
|
21
23
|
// Solo flag esclusivi dei subcomandi nodes/node: non collidono con gli altri.
|
|
22
24
|
const VALUE_FLAGS = new Set([
|
|
23
|
-
'ssh', 'remote-port', 'key', 'local-port', 'node-id', 'rendezvous', 'published-port', 'token',
|
|
25
|
+
'ssh', 'ssh-port', 'remote-port', 'key', 'local-port', 'node-id', 'rendezvous', 'published-port', 'token',
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
|
-
const HELP = `NexusCrew
|
|
28
|
+
const HELP = `NexusCrew — PWA for local and remote AI workers.
|
|
27
29
|
|
|
28
30
|
Usage:
|
|
29
|
-
nexuscrew
|
|
30
|
-
nexuscrew
|
|
31
|
-
nexuscrew
|
|
32
|
-
nexuscrew
|
|
33
|
-
nexuscrew
|
|
34
|
-
nexuscrew
|
|
35
|
-
nexuscrew
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
nexuscrew mcp server MCP stdio per sessioni AI (notify/ask/file/status)
|
|
40
|
-
nexuscrew update npm i -g @mmmbuto/nexuscrew@latest + restart
|
|
41
|
-
nexuscrew fleet-boot companion di boot: avvia le celle boot:true (config-driven)
|
|
42
|
-
nexuscrew nodes add <name> --ssh u@h aggiunge un nodo (genera chiave dedicata + authorized_keys)
|
|
43
|
-
nexuscrew nodes list [--json] elenca i nodi + stato tunnel (token redatti)
|
|
44
|
-
nexuscrew nodes remove|test <name> rimuove / testa un nodo (test: tunnel/health/token remoto)
|
|
45
|
-
nexuscrew nodes up|down|restart <name> lifecycle del singolo tunnel (non tocca la config)
|
|
46
|
-
nexuscrew nodes set-token <name> aggiorna il token remoto (da stdin/env, mai argv)
|
|
47
|
-
nexuscrew node on|off ruolo "nodo raggiungibile" (reverse tunnel)
|
|
48
|
-
|
|
49
|
-
Piattaforme: linux (systemd --user), mac (launchd), termux (nohup + pidfile).
|
|
50
|
-
Bind loopback 127.0.0.1 — raggiungibile via tunnel SSH/VPN.
|
|
51
|
-
Il token appare SOLO in \`url\`/\`url --qr\` — mai in status, logs, service output.`;
|
|
31
|
+
nexuscrew start in background; show status and quick guide
|
|
32
|
+
nexuscrew show start when needed and open the authenticated PWA
|
|
33
|
+
nexuscrew show token print the clickable authenticated URL
|
|
34
|
+
nexuscrew boot enable startup at boot (use: boot off|status)
|
|
35
|
+
nexuscrew doctor run local diagnostics
|
|
36
|
+
nexuscrew help show this help
|
|
37
|
+
nexuscrew version show the installed version
|
|
38
|
+
|
|
39
|
+
Configuration, nodes, engines, providers, models and lifecycle live in the PWA.
|
|
40
|
+
NexusCrew binds only to 127.0.0.1 and automatically selects a free port.`;
|
|
52
41
|
|
|
53
42
|
// valueFlags (Set|array opzionale): flag che consumano il token successivo nella
|
|
54
43
|
// forma "--k v". Omesso -> comportamento storico (solo "--k" bool o "--k=v").
|
|
@@ -79,7 +68,7 @@ function parseFlags(argv, valueFlags) {
|
|
|
79
68
|
function serve(opts = {}) {
|
|
80
69
|
const serverStart = opts.serverStart || require('../server.js').start;
|
|
81
70
|
if (opts.pidfile) {
|
|
82
|
-
const pidPath = pidf.defaultPidfilePath();
|
|
71
|
+
const pidPath = pidf.defaultPidfilePath(opts.home);
|
|
83
72
|
// already-running check
|
|
84
73
|
const meta = pidf.readPidfile(pidPath);
|
|
85
74
|
if (meta && pidf.isAlive(meta)) {
|
|
@@ -129,8 +118,9 @@ function start(opts = {}) {
|
|
|
129
118
|
return { platform, started: true };
|
|
130
119
|
}
|
|
131
120
|
if (platform === 'termux') {
|
|
121
|
+
const home = opts.home || require('node:os').homedir();
|
|
132
122
|
// already-running?
|
|
133
|
-
const pidPath = pidf.defaultPidfilePath();
|
|
123
|
+
const pidPath = pidf.defaultPidfilePath(opts.home);
|
|
134
124
|
const meta = pidf.readPidfile(pidPath);
|
|
135
125
|
if (meta && pidf.isAlive(meta)) {
|
|
136
126
|
log(`already running (pid ${meta.pid}) — pidfile ${pidPath}`);
|
|
@@ -138,12 +128,24 @@ function start(opts = {}) {
|
|
|
138
128
|
}
|
|
139
129
|
pidf.cleanStale(pidPath);
|
|
140
130
|
const repoBin = path.join(repoRoot(), 'bin', 'nexuscrew.js');
|
|
141
|
-
const logPath = path.join(
|
|
131
|
+
const logPath = path.join(home, '.nexuscrew', 'nexuscrew.log');
|
|
142
132
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
143
133
|
const logFd = fs.openSync(logPath, 'a');
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
const resolved = urlmod.resolvePaths(opts);
|
|
135
|
+
const childEnv = {
|
|
136
|
+
...process.env,
|
|
137
|
+
HOME: home,
|
|
138
|
+
NEXUSCREW_CONFIG_FILE: resolved.configPath,
|
|
139
|
+
NEXUSCREW_TOKEN_FILE: resolved.tokenPath,
|
|
140
|
+
};
|
|
141
|
+
// config.json is authoritative after automatic port fallback.
|
|
142
|
+
delete childEnv.NEXUSCREW_PORT;
|
|
143
|
+
let child;
|
|
144
|
+
try {
|
|
145
|
+
child = spawnImpl(nodeBin(), [repoBin, 'serve', '--pidfile'], {
|
|
146
|
+
detached: true, stdio: ['ignore', logFd, logFd], env: childEnv,
|
|
147
|
+
});
|
|
148
|
+
} finally { try { fs.closeSync(logFd); } catch (_) {} }
|
|
147
149
|
if (child && typeof child.unref === 'function') child.unref();
|
|
148
150
|
log(`start: nohup ${nodeBin()} ${repoBin} serve --pidfile (>> ${logPath})`);
|
|
149
151
|
return { platform, started: true, pid: child && child.pid };
|
|
@@ -176,7 +178,7 @@ function stop(opts = {}) {
|
|
|
176
178
|
}
|
|
177
179
|
if (platform === 'termux') {
|
|
178
180
|
// kill via pidfile verificato (no broad match) + wake-lock-release
|
|
179
|
-
const pidPath = pidf.defaultPidfilePath();
|
|
181
|
+
const pidPath = pidf.defaultPidfilePath(opts.home);
|
|
180
182
|
const r = pidf.killPidfile(pidPath);
|
|
181
183
|
try { execImpl('termux-wake-lock-release', [], { stdio: 'ignore' }); } catch (_) {}
|
|
182
184
|
log(`stop: ${r.killed ? `killed pid ${r.pid}` : r.reason}`);
|
|
@@ -198,12 +200,93 @@ function isServiceRunning(opts = {}) {
|
|
|
198
200
|
catch (_) { return false; }
|
|
199
201
|
}
|
|
200
202
|
if (platform === 'termux') {
|
|
201
|
-
const meta = pidf.readPidfile(pidf.defaultPidfilePath());
|
|
203
|
+
const meta = pidf.readPidfile(pidf.defaultPidfilePath(opts.home));
|
|
202
204
|
return !!(meta && pidf.isAlive(meta));
|
|
203
205
|
}
|
|
204
206
|
return false;
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
function bootState(opts = {}) {
|
|
210
|
+
const platform = opts.platform || detectPlatform();
|
|
211
|
+
const execImpl = opts.execImpl || execFileSync;
|
|
212
|
+
const home = opts.home || require('node:os').homedir();
|
|
213
|
+
if (platform === 'linux') {
|
|
214
|
+
try { return { platform, enabled: execImpl('systemctl', ['--user', 'is-enabled', 'nexuscrew'], { encoding: 'utf8' }).trim() === 'enabled' }; }
|
|
215
|
+
catch (_) { return { platform, enabled: false }; }
|
|
216
|
+
}
|
|
217
|
+
try { return { platform, enabled: fs.lstatSync(serviceInstallPath(platform, home)).isFile() }; }
|
|
218
|
+
catch (_) { return { platform, enabled: false }; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function unlinkRegular(target) {
|
|
222
|
+
try {
|
|
223
|
+
const st = fs.lstatSync(target);
|
|
224
|
+
if (st.isSymbolicLink() || !st.isFile()) throw new Error(`refusing unsafe boot target: ${target}`);
|
|
225
|
+
fs.unlinkSync(target); return true;
|
|
226
|
+
} catch (e) { if (e.code === 'ENOENT') return false; throw e; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function bootOn(opts = {}) {
|
|
230
|
+
const platform = opts.platform || detectPlatform();
|
|
231
|
+
const quiet = opts.lifecycleLog || (() => {});
|
|
232
|
+
const { configPath, tokenPath } = urlmod.resolvePaths(opts);
|
|
233
|
+
if (!fs.existsSync(configPath) || !urlmod.readToken(tokenPath)) {
|
|
234
|
+
const selected = await findAvailablePort(opts.port || urlmod.loadPort(opts), opts);
|
|
235
|
+
(opts.runInitImpl || runInit)({ ...opts, platform, port: selected, installBoot: false, printUrl: false, log: quiet });
|
|
236
|
+
}
|
|
237
|
+
try { pidf.killPidfile(pidf.defaultPidfilePath(opts.home)); } catch (_) {}
|
|
238
|
+
(opts.runInitImpl || runInit)({ ...opts, platform, installBoot: true, printUrl: false, log: quiet });
|
|
239
|
+
const result = await smartUp({ ...opts, platform });
|
|
240
|
+
return { ...result, boot: bootState({ ...opts, platform }).enabled };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function bootOff(opts = {}) {
|
|
244
|
+
const platform = opts.platform || detectPlatform();
|
|
245
|
+
const execImpl = opts.execImpl || execFileSync;
|
|
246
|
+
const home = opts.home || require('node:os').homedir();
|
|
247
|
+
if (platform === 'linux') {
|
|
248
|
+
try { execImpl('systemctl', ['--user', 'disable', 'nexuscrew'], { stdio: 'ignore' }); } catch (_) {}
|
|
249
|
+
try { execImpl('systemctl', ['--user', 'disable', 'nexuscrew-fleet.service'], { stdio: 'ignore' }); } catch (_) {}
|
|
250
|
+
} else if (platform === 'mac') {
|
|
251
|
+
const domain = `gui/${opts.uid || uid()}`;
|
|
252
|
+
try { execImpl('launchctl', ['bootout', `${domain}/com.mmmbuto.nexuscrew`], { stdio: 'ignore' }); } catch (_) {}
|
|
253
|
+
try { execImpl('launchctl', ['bootout', `${domain}/com.mmmbuto.nexuscrew-fleet`], { stdio: 'ignore' }); } catch (_) {}
|
|
254
|
+
unlinkRegular(serviceInstallPath(platform, home));
|
|
255
|
+
try { unlinkRegular(fleetInstallPath(platform, home)); } catch (_) {}
|
|
256
|
+
const { tokenPath } = urlmod.resolvePaths(opts); const port = urlmod.loadPort(opts); const token = urlmod.readToken(tokenPath);
|
|
257
|
+
if (!(await probeNexusCrew(port, token, opts))) {
|
|
258
|
+
startPortable({ ...opts, platform });
|
|
259
|
+
await waitForNexusCrew(port, token, opts);
|
|
260
|
+
}
|
|
261
|
+
} else if (platform === 'termux') {
|
|
262
|
+
unlinkRegular(serviceInstallPath(platform, home));
|
|
263
|
+
try { unlinkRegular(fleetInstallPath(platform, home)); } catch (_) {}
|
|
264
|
+
}
|
|
265
|
+
return { platform, enabled: false, running: isServiceRunning({ ...opts, platform }) || !!pidf.readPidfile(pidf.defaultPidfilePath(home)) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function quickSummary(result, opts = {}) {
|
|
269
|
+
const log = opts.log || console.log;
|
|
270
|
+
const platform = result.platform || opts.platform || detectPlatform();
|
|
271
|
+
const home = opts.home || require('node:os').homedir();
|
|
272
|
+
const { configDir } = urlmod.resolvePaths(opts);
|
|
273
|
+
const st = nodesStore.loadStore(opts.nodesPath || path.join(configDir, 'nodes.json'));
|
|
274
|
+
let cached = [];
|
|
275
|
+
try { cached = (require('../nodes/topology-cache.js').loadCache(opts.topologyCachePath || require('../nodes/topology-cache.js').defaultPath(home)) || {}).nodes || []; } catch (_) {}
|
|
276
|
+
const direct = (st && st.nodes) || [];
|
|
277
|
+
const online = direct.filter((n) => nodesTunnel.readTunnelState(home, n.name).status === 'up').length;
|
|
278
|
+
const known = direct.length + cached.filter((n) => !direct.some((d) => d.nodeId && d.nodeId === n.instanceId)).length;
|
|
279
|
+
log(`NexusCrew ${require('../../package.json').version}`);
|
|
280
|
+
log(`server ${result.running ? '● running' : '○ stopped'} · 127.0.0.1:${result.port}`);
|
|
281
|
+
log(`boot ${bootState({ ...opts, platform }).enabled ? 'on' : 'off'} · ${platform}`);
|
|
282
|
+
log(`nodes ${known} known · ${online}/${direct.length} direct tunnels up`);
|
|
283
|
+
log('');
|
|
284
|
+
log('open nexuscrew show');
|
|
285
|
+
log('link nexuscrew show token');
|
|
286
|
+
log('boot nexuscrew boot');
|
|
287
|
+
log('check nexuscrew doctor');
|
|
288
|
+
}
|
|
289
|
+
|
|
207
290
|
// Ruoli client/node dal config.json (default entrambi off — B0 li popola dal wizard UI).
|
|
208
291
|
function readRoles(configPath) {
|
|
209
292
|
try {
|
|
@@ -238,7 +321,7 @@ function status(opts = {}) {
|
|
|
238
321
|
// boot-script installed vs server running (pidfile vivo)
|
|
239
322
|
const bootScript = path.join(home, '.termux', 'boot', 'nexuscrew.sh');
|
|
240
323
|
out.bootScriptInstalled = fs.existsSync(bootScript);
|
|
241
|
-
const meta = pidf.readPidfile(pidf.defaultPidfilePath());
|
|
324
|
+
const meta = pidf.readPidfile(pidf.defaultPidfilePath(home));
|
|
242
325
|
out.running = !!(meta && pidf.isAlive(meta));
|
|
243
326
|
out.service = out.bootScriptInstalled ? 'boot-script installed' : 'no boot-script';
|
|
244
327
|
}
|
|
@@ -304,43 +387,169 @@ function restart(opts = {}) {
|
|
|
304
387
|
throw new Error(`restart: platform ${platform} non supportata`);
|
|
305
388
|
}
|
|
306
389
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
390
|
+
function wizardComplete(configPath) {
|
|
391
|
+
try {
|
|
392
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
393
|
+
return cfg && cfg.wizardDone === true;
|
|
394
|
+
} catch (_) { return false; }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function servicePinsLegacyPort(platform, home, installPath) {
|
|
398
|
+
let target;
|
|
399
|
+
try { target = installPath || serviceInstallPath(platform, home); } catch (_) { return false; }
|
|
400
|
+
try { return /NEXUSCREW_PORT/.test(fs.readFileSync(target, 'utf8')); } catch (_) { return false; }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function portAvailable(port, host = '127.0.0.1') {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const s = net.createServer();
|
|
406
|
+
s.unref();
|
|
407
|
+
s.once('error', () => resolve(false));
|
|
408
|
+
s.listen(port, host, () => s.close(() => resolve(true)));
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function findAvailablePort(startPort, opts = {}) {
|
|
413
|
+
const available = opts.portAvailableImpl || portAvailable;
|
|
414
|
+
const first = Number(startPort);
|
|
415
|
+
if (!Number.isInteger(first) || first < 1 || first > 65535) throw new Error(`invalid port: ${startPort}`);
|
|
416
|
+
for (let port = first; port <= Math.min(65535, first + 200); port += 1) {
|
|
417
|
+
if (await available(port, '127.0.0.1')) return port;
|
|
418
|
+
}
|
|
419
|
+
throw new Error(`no free loopback port found from ${first}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function probeNexusCrew(port, token, opts = {}) {
|
|
423
|
+
if (!token) return false;
|
|
424
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
425
|
+
if (typeof fetchImpl !== 'function') return false;
|
|
426
|
+
try {
|
|
427
|
+
const r = await fetchImpl(`http://127.0.0.1:${port}/api/config`, {
|
|
428
|
+
headers: { authorization: `Bearer ${token}` },
|
|
429
|
+
signal: AbortSignal.timeout?.(700),
|
|
430
|
+
});
|
|
431
|
+
return r.status === 200;
|
|
432
|
+
} catch (_) { return false; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function waitForNexusCrew(port, token, opts = {}) {
|
|
436
|
+
const probe = opts.probeImpl || probeNexusCrew;
|
|
437
|
+
const attempts = opts.waitAttempts === undefined ? 30 : opts.waitAttempts;
|
|
438
|
+
const delay = opts.waitDelayMs === undefined ? 100 : opts.waitDelayMs;
|
|
439
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
440
|
+
if (await probe(port, token, opts)) return true;
|
|
441
|
+
if (i + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function startPortable(opts = {}) {
|
|
447
|
+
const spawnImpl = opts.spawnImpl || spawn;
|
|
448
|
+
const home = opts.home || require('node:os').homedir();
|
|
449
|
+
const logPath = path.join(home, '.nexuscrew', 'nexuscrew.log');
|
|
450
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
451
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
452
|
+
const resolved = urlmod.resolvePaths(opts);
|
|
453
|
+
const childEnv = {
|
|
454
|
+
...process.env,
|
|
455
|
+
HOME: home,
|
|
456
|
+
NEXUSCREW_CONFIG_FILE: resolved.configPath,
|
|
457
|
+
NEXUSCREW_TOKEN_FILE: resolved.tokenPath,
|
|
458
|
+
...(opts.filesRoot ? { NEXUSCREW_FILES_ROOT: opts.filesRoot } : {}),
|
|
459
|
+
};
|
|
460
|
+
// config.json is authoritative after automatic port fallback.
|
|
461
|
+
delete childEnv.NEXUSCREW_PORT;
|
|
462
|
+
let child;
|
|
463
|
+
try {
|
|
464
|
+
child = spawnImpl(nodeBin(), [path.join(repoRoot(), 'bin', 'nexuscrew.js'), 'serve', '--pidfile'], {
|
|
465
|
+
detached: true, stdio: ['ignore', logFd, logFd], env: childEnv,
|
|
466
|
+
});
|
|
467
|
+
} finally { try { fs.closeSync(logFd); } catch (_) {} }
|
|
468
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
469
|
+
return { started: true, pid: child && child.pid, portable: true };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function openPwa(fullUrl, opts = {}) {
|
|
473
|
+
if (opts.openImpl) return opts.openImpl(fullUrl);
|
|
474
|
+
const platform = opts.platform || detectPlatform();
|
|
475
|
+
const commandExists = opts.commandExists || require('./path.js').commandExists;
|
|
476
|
+
const candidates = platform === 'termux'
|
|
477
|
+
? ['termux-open-url']
|
|
478
|
+
: platform === 'mac' ? ['open'] : ['xdg-open', 'gio'];
|
|
479
|
+
const bin = candidates.find((candidate) => commandExists(candidate, opts.env || process.env));
|
|
480
|
+
if (!bin) throw new Error('no URL opener found; install termux-tools or xdg-utils');
|
|
481
|
+
const args = bin === 'gio' ? ['open', fullUrl] : [fullUrl];
|
|
482
|
+
const child = (opts.spawnImpl || spawn)(bin, args, { detached: true, stdio: 'ignore' });
|
|
483
|
+
if (child && typeof child.on === 'function') child.on('error', () => {});
|
|
484
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Normal entry point. It is deliberately quiet and returns after starting the
|
|
489
|
+
// background service. Only first-run opens the wizard; `show` always opens it.
|
|
490
|
+
async function smartUp(opts = {}) {
|
|
491
|
+
const quiet = opts.lifecycleLog || (() => {});
|
|
312
492
|
const platform = opts.platform || detectPlatform();
|
|
313
|
-
const { configPath, tokenPath } = urlmod.resolvePaths(opts);
|
|
314
493
|
const runInitImpl = opts.runInitImpl || runInit;
|
|
315
494
|
const startImpl = opts.startImpl || start;
|
|
495
|
+
const restartImpl = opts.restartImpl || restart;
|
|
496
|
+
const portableStart = opts.startPortableImpl || startPortable;
|
|
497
|
+
const probe = opts.probeImpl || probeNexusCrew;
|
|
498
|
+
const { configPath, tokenPath } = urlmod.resolvePaths(opts);
|
|
499
|
+
let initialized = fs.existsSync(configPath) && !!urlmod.readToken(tokenPath);
|
|
500
|
+
let port;
|
|
316
501
|
|
|
317
|
-
const initialized = fs.existsSync(configPath) && !!urlmod.readToken(tokenPath);
|
|
318
502
|
if (!initialized) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
runInitImpl({ ...opts, log, platform, printUrl: false });
|
|
503
|
+
const requested = opts.port || urlmod.loadPort(opts);
|
|
504
|
+
const selected = await findAvailablePort(requested, opts);
|
|
505
|
+
runInitImpl({ ...opts, port: selected, log: quiet, platform, installBoot: false, printUrl: false });
|
|
506
|
+
port = selected;
|
|
507
|
+
initialized = true;
|
|
322
508
|
}
|
|
323
509
|
|
|
324
|
-
|
|
510
|
+
// 0.8.0 services embedded NEXUSCREW_PORT in their environment, overriding
|
|
511
|
+
// config.json forever. Regenerate once so config.json becomes authoritative.
|
|
512
|
+
const home = opts.home || require('node:os').homedir();
|
|
513
|
+
const persistent = bootState({ ...opts, platform }).enabled;
|
|
514
|
+
if (persistent && servicePinsLegacyPort(platform, home, opts.installPath)) {
|
|
515
|
+
runInitImpl({ ...opts, log: quiet, platform, installBoot: true, printUrl: false });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!port) port = urlmod.loadPort(opts);
|
|
519
|
+
let token = urlmod.readToken(tokenPath);
|
|
520
|
+
let running = await probe(port, token, opts);
|
|
521
|
+
let portableAttempted = false;
|
|
522
|
+
const managedRunning = isServiceRunning({ ...opts, platform });
|
|
523
|
+
if (!running && managedRunning) running = await waitForNexusCrew(port, token, opts);
|
|
524
|
+
|
|
325
525
|
if (!running) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
526
|
+
if (!(await (opts.portAvailableImpl || portAvailable)(port, '127.0.0.1'))) {
|
|
527
|
+
port = await findAvailablePort(port + 1, opts);
|
|
528
|
+
runInitImpl({ ...opts, port, log: quiet, platform, installBoot: persistent, printUrl: false });
|
|
529
|
+
token = urlmod.readToken(tokenPath);
|
|
530
|
+
} else {
|
|
531
|
+
try {
|
|
532
|
+
const started = persistent
|
|
533
|
+
? (managedRunning ? restartImpl({ ...opts, platform, log: quiet }) : startImpl({ ...opts, platform, log: quiet }))
|
|
534
|
+
: portableStart({ ...opts, platform });
|
|
535
|
+
if (started && started.started === false) {
|
|
536
|
+
portableStart({ ...opts, platform }); portableAttempted = true;
|
|
537
|
+
}
|
|
538
|
+
} catch (_) {
|
|
539
|
+
portableStart({ ...opts, platform }); portableAttempted = true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
running = await waitForNexusCrew(port, token, opts);
|
|
543
|
+
if (!running && !portableAttempted) {
|
|
544
|
+
try { portableStart({ ...opts, platform }); portableAttempted = true; } catch (_) {}
|
|
545
|
+
running = await waitForNexusCrew(port, token, opts);
|
|
546
|
+
}
|
|
547
|
+
if (!running) throw new Error(`server did not become ready on 127.0.0.1:${port}`);
|
|
329
548
|
}
|
|
330
549
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const port = urlmod.loadPort(opts);
|
|
335
|
-
const token = urlmod.readToken(tokenPath);
|
|
336
|
-
const baseUrl = urlmod.buildUrl(port, null, { withToken: false });
|
|
337
|
-
const fullUrl = urlmod.buildUrl(port, token, { withToken: true });
|
|
338
|
-
log(baseUrl);
|
|
339
|
-
if (token) {
|
|
340
|
-
log(urlmod.renderQr(fullUrl, { qrcode: opts.qrcode }));
|
|
341
|
-
log('scansiona il QR (contiene il token) o usa `nexuscrew url` per l\'URL completo');
|
|
342
|
-
}
|
|
343
|
-
return { platform, initialized, running, url: baseUrl, port };
|
|
550
|
+
const shouldOpen = !opts.noOpen && (!!opts.forceOpen || !wizardComplete(configPath));
|
|
551
|
+
if (shouldOpen) openPwa(urlmod.buildUrl(port, token, { withToken: true }), { ...opts, platform });
|
|
552
|
+
return { platform, initialized, running, opened: shouldOpen, port, url: urlmod.buildUrl(port, null) };
|
|
344
553
|
}
|
|
345
554
|
|
|
346
555
|
// url [--qr]: ristampa URL completo con #token — UNICO comando che mostra il token.
|
|
@@ -378,7 +587,7 @@ function tokenRotate(opts = {}) {
|
|
|
378
587
|
} else {
|
|
379
588
|
// il service manager non lo vede, ma un `serve` manuale (pidfile) puo' essere
|
|
380
589
|
// vivo col VECCHIO token cachato allo startup: avvisa esplicitamente.
|
|
381
|
-
const meta = pidf.readPidfile(pidf.defaultPidfilePath());
|
|
590
|
+
const meta = pidf.readPidfile(pidf.defaultPidfilePath(opts.home));
|
|
382
591
|
if (meta && pidf.isAlive(meta)) {
|
|
383
592
|
log(`token rotate: ATTENZIONE — server manuale attivo (pid ${meta.pid}): il vecchio token resta valido finche' non lo riavvii`);
|
|
384
593
|
} else {
|
|
@@ -485,66 +694,64 @@ function dispatchFleetBoot(opts) {
|
|
|
485
694
|
}
|
|
486
695
|
|
|
487
696
|
function dispatch(argv, opts = {}) {
|
|
697
|
+
const log = opts.log || console.log;
|
|
698
|
+
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
699
|
+
log(HELP);
|
|
700
|
+
return { code: 0 };
|
|
701
|
+
}
|
|
702
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) {
|
|
703
|
+
log(require('../../package.json').version);
|
|
704
|
+
return { code: 0 };
|
|
705
|
+
}
|
|
488
706
|
const { flags, rest } = parseFlags(argv, VALUE_FLAGS);
|
|
489
707
|
const cmd = rest[0];
|
|
490
|
-
const log = opts.log || console.log;
|
|
491
708
|
|
|
492
709
|
// help esplicito
|
|
493
710
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
494
711
|
log(HELP);
|
|
495
712
|
return { code: 0 };
|
|
496
713
|
}
|
|
497
|
-
//
|
|
714
|
+
// Public surface: normal start, show, boot, doctor, help, version.
|
|
498
715
|
if (!cmd) {
|
|
499
|
-
|
|
500
|
-
|
|
716
|
+
if (Object.keys(flags).length) {
|
|
717
|
+
log(`unknown option: --${Object.keys(flags)[0]}\n\n${HELP}`);
|
|
718
|
+
return { code: 1 };
|
|
719
|
+
}
|
|
720
|
+
return smartUp({ ...opts, log: opts.lifecycleLog || (() => {}) }).then((r) => { quickSummary(r, { ...opts, log }); return { code: 0 }; });
|
|
721
|
+
}
|
|
722
|
+
if (cmd === 'show') {
|
|
723
|
+
if (rest[1] === 'token') {
|
|
724
|
+
return smartUp({ ...opts, noOpen: true, log: opts.lifecycleLog || (() => {}) }).then((r) => {
|
|
725
|
+
const { tokenPath } = urlmod.resolvePaths(opts); const token = urlmod.readToken(tokenPath);
|
|
726
|
+
log(urlmod.buildUrl(r.port, token, { withToken: true })); return { code: 0 };
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (rest[1]) { log('usage: nexuscrew show [token]'); return { code: 1 }; }
|
|
730
|
+
return smartUp({ ...opts, forceOpen: true, log: opts.lifecycleLog || (() => {}) }).then(() => ({ code: 0 }));
|
|
731
|
+
}
|
|
732
|
+
if (cmd === 'boot') {
|
|
733
|
+
const sub = rest[1] || 'on';
|
|
734
|
+
if (sub === 'status') {
|
|
735
|
+
const b = bootState(opts); log(`boot: ${b.enabled ? 'on' : 'off'} · ${b.platform}`); return { code: 0 };
|
|
736
|
+
}
|
|
737
|
+
if (sub === 'off') return bootOff(opts).then((r) => { log(`boot: off · ${r.platform}`); return { code: 0 }; });
|
|
738
|
+
if (sub === 'on') return bootOn(opts).then((r) => { quickSummary(r, { ...opts, log }); return { code: r.boot ? 0 : 1 }; });
|
|
739
|
+
log('usage: nexuscrew boot [on|off|status]'); return { code: 1 };
|
|
501
740
|
}
|
|
502
|
-
if (cmd === '
|
|
503
|
-
|
|
741
|
+
if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
|
|
742
|
+
log(require('../../package.json').version);
|
|
504
743
|
return { code: 0 };
|
|
505
744
|
}
|
|
745
|
+
// Internal runtime commands used by service managers and MCP clients. They
|
|
746
|
+
// are intentionally omitted from HELP and are not configuration surfaces.
|
|
506
747
|
if (cmd === 'serve') {
|
|
507
748
|
serve({ pidfile: flags.pidfile, serverStart: opts.serverStart });
|
|
508
749
|
return { code: 0, keepAlive: true }; // server.listen tiene il processo vivo; non exit
|
|
509
750
|
}
|
|
510
|
-
if (cmd === 'start' || cmd === 'up') {
|
|
511
|
-
const r = start({ execImpl: opts.execImpl, spawnImpl: opts.spawnImpl, log,
|
|
512
|
-
platform: opts.platform, uid: opts.uid, home: opts.home, installPath: opts.installPath });
|
|
513
|
-
return { code: r.started === false ? 1 : 0 };
|
|
514
|
-
}
|
|
515
|
-
if (cmd === 'stop' || cmd === 'down') {
|
|
516
|
-
const r = stop({ execImpl: opts.execImpl, log, platform: opts.platform, uid: opts.uid });
|
|
517
|
-
return { code: r.stopped === false ? 1 : 0 };
|
|
518
|
-
}
|
|
519
|
-
if (cmd === 'status') {
|
|
520
|
-
status({ ...opts, json: !!flags.json, log });
|
|
521
|
-
return { code: 0 };
|
|
522
|
-
}
|
|
523
|
-
if (cmd === 'url') {
|
|
524
|
-
url({ ...opts, qr: !!flags.qr, log });
|
|
525
|
-
return { code: 0 };
|
|
526
|
-
}
|
|
527
|
-
if (cmd === 'token') {
|
|
528
|
-
if (rest[1] === 'rotate') {
|
|
529
|
-
tokenRotate({ ...opts, log });
|
|
530
|
-
return { code: 0 };
|
|
531
|
-
}
|
|
532
|
-
log('usage: nexuscrew token rotate');
|
|
533
|
-
return { code: 1 };
|
|
534
|
-
}
|
|
535
|
-
if (cmd === 'logs') {
|
|
536
|
-
const follow = !!(flags.f || flags.follow) || rest.includes('-f');
|
|
537
|
-
const r = logs({ ...opts, follow, log });
|
|
538
|
-
return { code: 0, keepAlive: !!r.keepAlive };
|
|
539
|
-
}
|
|
540
751
|
if (cmd === 'doctor') {
|
|
541
752
|
const r = doctor({ ...opts, log });
|
|
542
753
|
return { code: r.code };
|
|
543
754
|
}
|
|
544
|
-
if (cmd === 'update') {
|
|
545
|
-
const r = update({ ...opts, log });
|
|
546
|
-
return { code: r.code || 0 };
|
|
547
|
-
}
|
|
548
755
|
if (cmd === 'mcp') {
|
|
549
756
|
// Server MCP stdio (bridge cella→operatore): stdout e' il canale JSON-RPC, quindi
|
|
550
757
|
// NESSUN log qui. keepAlive: resta vivo finche' il client tiene aperto stdin.
|
|
@@ -555,13 +762,7 @@ function dispatch(argv, opts = {}) {
|
|
|
555
762
|
if (cmd === 'fleet-boot') {
|
|
556
763
|
return dispatchFleetBoot({ ...opts, log });
|
|
557
764
|
}
|
|
558
|
-
|
|
559
|
-
return dispatchNodes(rest, flags, { ...opts, log });
|
|
560
|
-
}
|
|
561
|
-
if (cmd === 'node') {
|
|
562
|
-
return dispatchNode(rest, flags, { ...opts, log });
|
|
563
|
-
}
|
|
564
|
-
log(`unknown command: ${cmd}\n\n${HELP}`);
|
|
765
|
+
log(`"${cmd}" is not a public CLI command. Open the PWA with: nexuscrew show\n\n${HELP}`);
|
|
565
766
|
return { code: 1 };
|
|
566
767
|
}
|
|
567
768
|
|
|
@@ -574,7 +775,7 @@ function dispatchNodes(rest, flags, opts) {
|
|
|
574
775
|
switch (sub) {
|
|
575
776
|
case 'add':
|
|
576
777
|
return { code: nodesCmds.nodesAdd({
|
|
577
|
-
...common, ssh: flags.ssh, remotePort: flags['remote-port'],
|
|
778
|
+
...common, ssh: flags.ssh, sshPort: flags['ssh-port'], remotePort: flags['remote-port'],
|
|
578
779
|
key: flags.key, localPort: flags['local-port'], nodeId: flags['node-id'],
|
|
579
780
|
}).code };
|
|
580
781
|
case undefined:
|
|
@@ -624,6 +825,9 @@ function dispatchNode(rest, flags, opts) {
|
|
|
624
825
|
module.exports = {
|
|
625
826
|
dispatch, serve, start, stop, status, parseFlags, HELP,
|
|
626
827
|
smartUp, url, tokenRotate, logs, doctor, update, restart,
|
|
828
|
+
portAvailable, findAvailablePort, probeNexusCrew, waitForNexusCrew, openPwa, startPortable, wizardComplete,
|
|
829
|
+
servicePinsLegacyPort,
|
|
627
830
|
isServiceRunning, readRoles,
|
|
831
|
+
bootState, bootOn, bootOff, quickSummary,
|
|
628
832
|
runFleetBoot, dispatchFleetBoot,
|
|
629
833
|
};
|
package/lib/cli/doctor.js
CHANGED
|
@@ -51,7 +51,7 @@ function checkService(platform, home, execImpl, uidVal, installPathOverride) {
|
|
|
51
51
|
active = true;
|
|
52
52
|
} else if (platform === 'termux') {
|
|
53
53
|
const pidf = require('./pidfile.js');
|
|
54
|
-
const meta = pidf.readPidfile(pidf.defaultPidfilePath());
|
|
54
|
+
const meta = pidf.readPidfile(pidf.defaultPidfilePath(home));
|
|
55
55
|
active = !!(meta && pidf.isAlive(meta));
|
|
56
56
|
}
|
|
57
57
|
} catch (_) { active = false; }
|