@mmmbuto/nexuscrew 0.8.20 → 0.8.22
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 +33 -21
- package/frontend/dist/assets/{index-DQbVJLji.css → index-77r8nzQf.css} +1 -1
- package/frontend/dist/assets/index-ClJP2j6k.js +91 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cells/routes.js +2 -2
- package/lib/cli/commands.js +233 -8
- package/lib/cli/fleet-service.js +7 -41
- package/lib/cli/init.js +0 -2
- package/lib/config.js +0 -2
- package/lib/fleet/builtin.js +4 -5
- package/lib/fleet/cell-exec.js +165 -11
- package/lib/fleet/definitions.js +1 -1
- package/lib/fleet/provider.js +10 -90
- package/lib/fleet/routes.js +7 -14
- package/lib/fleet/runtime.js +39 -20
- package/lib/nodes/commands.js +160 -19
- package/lib/nodes/inventory.js +81 -0
- package/lib/nodes/tunnel.js +9 -1
- package/lib/server.js +31 -3
- package/lib/settings/routes.js +68 -32
- package/package.json +1 -1
- package/frontend/dist/assets/index-CQnOyaXz.js +0 -93
- package/lib/fleet/exec.js +0 -32
- package/lib/fleet/index.js +0 -180
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-ClJP2j6k.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-77r8nzQf.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.22"}
|
package/lib/cells/routes.js
CHANGED
|
@@ -17,8 +17,8 @@ function publicCells(status, instanceId, now = Date.now()) {
|
|
|
17
17
|
if (!raw || !CELL_ID_RE.test(String(raw.cell || ''))
|
|
18
18
|
|| !isValidSession(raw.tmuxSession) || seen.has(raw.cell)) continue;
|
|
19
19
|
seen.add(raw.cell);
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// Un `tmux:false` esplicito prevale su active:true: la directory globale non
|
|
21
|
+
// deve dichiarare ricevibile una cella senza sessione viva.
|
|
22
22
|
const active = raw.active === true && raw.tmux !== false;
|
|
23
23
|
cells.push({
|
|
24
24
|
id: `${instanceId}:${raw.cell}`,
|
package/lib/cli/commands.js
CHANGED
|
@@ -17,6 +17,8 @@ const urlmod = require('./url.js');
|
|
|
17
17
|
const { doctor } = require('./doctor.js');
|
|
18
18
|
const nodesStore = require('../nodes/store.js');
|
|
19
19
|
const nodesTunnel = require('../nodes/tunnel.js');
|
|
20
|
+
const nodesCmds = require('../nodes/commands.js');
|
|
21
|
+
const peering = require('../nodes/peering.js');
|
|
20
22
|
// Service/portable runtime ownership + start/stop lifecycle (Phase 3 extraction).
|
|
21
23
|
const runtimeLifecycle = require('./runtime-lifecycle.js');
|
|
22
24
|
const {
|
|
@@ -36,12 +38,50 @@ Usage:
|
|
|
36
38
|
nexuscrew stop stop the background service
|
|
37
39
|
nexuscrew restart restart the background service
|
|
38
40
|
nexuscrew doctor run local diagnostics
|
|
41
|
+
nexuscrew nodes list and manage connected peers (use: nodes help)
|
|
39
42
|
nexuscrew help show this help
|
|
40
43
|
nexuscrew version show the installed version
|
|
41
44
|
|
|
42
|
-
Configuration,
|
|
45
|
+
Configuration, engines, models and advanced lifecycle also live in the PWA.
|
|
43
46
|
NexusCrew binds only to 127.0.0.1 and automatically selects a free port.`;
|
|
44
47
|
|
|
48
|
+
const NODES_HELP = `NexusCrew nodes — headless peer management.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
nexuscrew nodes list [--direct|--network] [--json]
|
|
52
|
+
nexuscrew nodes inspect|show <name|nodeId> [--json]
|
|
53
|
+
nexuscrew nodes doctor [name|nodeId] [--json]
|
|
54
|
+
nexuscrew nodes edit <name|nodeId> [--label TEXT] [--ssh TARGET]
|
|
55
|
+
[--ssh-port PORT] [--autostart on|off]
|
|
56
|
+
[--visibility network|relay-only|selected]
|
|
57
|
+
[--selected NODE_ID,...]
|
|
58
|
+
nexuscrew nodes remove <name|nodeId> --yes
|
|
59
|
+
nexuscrew nodes test|up|down|connect|disconnect|restart|reconnect <name|nodeId>
|
|
60
|
+
[--persist]
|
|
61
|
+
nexuscrew nodes rename <name|nodeId> --label TEXT
|
|
62
|
+
nexuscrew nodes visibility <name|nodeId> network|relay-only|selected
|
|
63
|
+
[--selected NODE_ID,...]
|
|
64
|
+
nexuscrew nodes share <name|nodeId> on|off [--json]
|
|
65
|
+
nexuscrew nodes invite --ssh TARGET [--ssh-port PORT] [--name SLUG]
|
|
66
|
+
[--label TEXT] [--json]
|
|
67
|
+
printf '%s' "$PAIRING_URL" | nexuscrew nodes pair|join
|
|
68
|
+
[--ssh TARGET] [--name SLUG] [--label TEXT]
|
|
69
|
+
nexuscrew nodes identity [--json]
|
|
70
|
+
|
|
71
|
+
Pairing links are read from stdin, never from argv. Mutations honor
|
|
72
|
+
NEXUSCREW_READONLY=1. Routed peers are inspect-only.
|
|
73
|
+
|
|
74
|
+
Lifecycle semantics:
|
|
75
|
+
up connect now and enable autostart
|
|
76
|
+
down disconnect and disable autostart
|
|
77
|
+
disconnect disconnect now without changing autostart
|
|
78
|
+
restart reconnect now without changing autostart`;
|
|
79
|
+
|
|
80
|
+
const CLI_VALUE_FLAGS = new Set([
|
|
81
|
+
'label', 'ssh', 'ssh-port', 'autostart', 'visibility', 'selected',
|
|
82
|
+
'name', 'local-label', 'identity-file',
|
|
83
|
+
]);
|
|
84
|
+
|
|
45
85
|
// valueFlags (Set|array opzionale): flag che consumano il token successivo nella
|
|
46
86
|
// forma "--k v". Omesso -> comportamento storico (solo "--k" bool o "--k=v").
|
|
47
87
|
function parseFlags(argv, valueFlags) {
|
|
@@ -594,10 +634,6 @@ async function runFleetBoot(opts = {}) {
|
|
|
594
634
|
const cfg = opts.cfg || loadConfig();
|
|
595
635
|
|
|
596
636
|
const sel = await selectProvider(cfg);
|
|
597
|
-
if (sel.mode === 'external') {
|
|
598
|
-
log('fleet-boot: boot gestito dal fleet esterno (nessuna azione del companion)');
|
|
599
|
-
return { code: 0, mode: 'external' };
|
|
600
|
-
}
|
|
601
637
|
if (sel.mode !== 'builtin' || !sel.fleet || !sel.fleet.available) {
|
|
602
638
|
// disabled (o builtin unavailable): niente da avviare, non e' un errore.
|
|
603
639
|
log(`fleet-boot: nessun boot (provider ${sel.mode}${sel.reason ? ' — ' + sel.reason : ''})`);
|
|
@@ -623,6 +659,193 @@ function dispatchFleetBoot(opts) {
|
|
|
623
659
|
return { code: 0, keepAlive: true }; // il processo resta vivo finche' runFleetBoot non chiama exit()
|
|
624
660
|
}
|
|
625
661
|
|
|
662
|
+
function boolFlag(value, name) {
|
|
663
|
+
if (value === true || value === 'on' || value === 'true' || value === '1') return true;
|
|
664
|
+
if (value === 'off' || value === 'false' || value === '0') return false;
|
|
665
|
+
throw new Error(`${name}: atteso on|off`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function readPairingStdin(opts = {}) {
|
|
669
|
+
const clean = (value) => String(value || '').trim().replace(/[\r\n]/g, '');
|
|
670
|
+
if (typeof opts.stdin === 'function') return clean(opts.stdin());
|
|
671
|
+
if (typeof opts.stdin === 'string') return clean(opts.stdin);
|
|
672
|
+
if (process.stdin.isTTY) return '';
|
|
673
|
+
try { return clean(fs.readFileSync(0, 'utf8')); } catch (_) { return ''; }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function localSettingsRequest(pathname, body, opts = {}) {
|
|
677
|
+
const method = opts.method || 'POST';
|
|
678
|
+
if (typeof opts.localApiImpl === 'function') return opts.localApiImpl(pathname, body, { method });
|
|
679
|
+
const up = await (opts.smartUpImpl || smartUp)({
|
|
680
|
+
...opts, noOpen: true, log: opts.lifecycleLog || (() => {}),
|
|
681
|
+
});
|
|
682
|
+
const { tokenPath } = urlmod.resolvePaths(opts);
|
|
683
|
+
const token = urlmod.readToken(tokenPath);
|
|
684
|
+
if (!token) throw new Error('token locale assente: inizializza NexusCrew');
|
|
685
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
686
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch non disponibile');
|
|
687
|
+
const response = await fetchImpl(`http://127.0.0.1:${up.port || urlmod.loadPort(opts)}${pathname}`, {
|
|
688
|
+
method,
|
|
689
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
690
|
+
body: JSON.stringify(body || {}),
|
|
691
|
+
});
|
|
692
|
+
const value = await response.json().catch(() => ({}));
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
const error = new Error(value.error || `HTTP ${response.status}`);
|
|
695
|
+
error.status = response.status;
|
|
696
|
+
error.data = value;
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
return value;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function dispatchNodes(rest, flags, opts = {}) {
|
|
703
|
+
const log = opts.log || console.log;
|
|
704
|
+
const sub = rest[1] || 'list';
|
|
705
|
+
const ref = rest[2];
|
|
706
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') { log(NODES_HELP); return { code: 0 }; }
|
|
707
|
+
if (sub === 'list') return { code: nodesCmds.nodesList({
|
|
708
|
+
...opts, log, json: flags.json === true, direct: flags.direct === true,
|
|
709
|
+
}).code };
|
|
710
|
+
if (sub === 'inspect' || sub === 'show') return { code: nodesCmds.nodesInspect({ ...opts, log, ref, json: flags.json === true }).code };
|
|
711
|
+
if (sub === 'doctor') {
|
|
712
|
+
const view = nodesCmds.loadPeerInventory(opts);
|
|
713
|
+
let peers = view.nodes;
|
|
714
|
+
if (ref) {
|
|
715
|
+
const inspected = nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
|
|
716
|
+
if (inspected.code !== 0) { log(`nodes doctor: nodo sconosciuto "${ref}"`); return { code: 1 }; }
|
|
717
|
+
peers = [inspected.peer];
|
|
718
|
+
}
|
|
719
|
+
const checks = [];
|
|
720
|
+
for (const peer of peers) {
|
|
721
|
+
if (peer.kind === 'transitive') {
|
|
722
|
+
checks.push({ name: peer.name, nodeId: peer.nodeId, ok: true, result: 'inspect-only', route: peer.route });
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const lines = [];
|
|
726
|
+
const result = await nodesCmds.nodesTest({ ...opts, log: (line) => lines.push(String(line)), ref: peer.nodeId || peer.name });
|
|
727
|
+
checks.push({
|
|
728
|
+
name: peer.name, nodeId: peer.nodeId || null, ok: result.code === 0,
|
|
729
|
+
result: result.result || (result.code === 0 ? 'ok' : 'failed'),
|
|
730
|
+
detail: lines[lines.length - 1] || '',
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (flags.json === true) log(JSON.stringify({ nodeId: view.nodeId, checks }, null, 2));
|
|
734
|
+
else if (checks.length === 0) log('nodes doctor: nessun peer diretto');
|
|
735
|
+
else for (const check of checks) log(`${check.name}: ${check.ok ? 'OK' : 'KO'} — ${check.result}${check.detail ? ` · ${check.detail}` : ''}`);
|
|
736
|
+
return { code: checks.some((check) => !check.ok) ? 1 : 0 };
|
|
737
|
+
}
|
|
738
|
+
if (sub === 'identity') {
|
|
739
|
+
const view = nodesCmds.loadPeerInventory(opts);
|
|
740
|
+
const value = { nodeId: view.nodeId };
|
|
741
|
+
if (flags.json === true) log(JSON.stringify(value, null, 2));
|
|
742
|
+
else log(`nodeId: ${value.nodeId || '(store non inizializzato)'}`);
|
|
743
|
+
return { code: value.nodeId ? 0 : 1 };
|
|
744
|
+
}
|
|
745
|
+
if (sub === 'edit') {
|
|
746
|
+
const patch = {};
|
|
747
|
+
if (flags.label !== undefined) patch.label = flags.label;
|
|
748
|
+
if (flags.ssh !== undefined) patch.ssh = flags.ssh;
|
|
749
|
+
if (flags['ssh-port'] !== undefined) patch.sshPort = Number(flags['ssh-port']);
|
|
750
|
+
if (flags.autostart !== undefined) {
|
|
751
|
+
try { patch.autostart = boolFlag(flags.autostart, '--autostart'); }
|
|
752
|
+
catch (e) { log(e.message); return { code: 1 }; }
|
|
753
|
+
}
|
|
754
|
+
if (flags.visibility !== undefined) patch.visibility = flags.visibility;
|
|
755
|
+
if (flags.selected !== undefined) patch.selected = String(flags.selected).split(',').map((x) => x.trim()).filter(Boolean);
|
|
756
|
+
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch }).code };
|
|
757
|
+
}
|
|
758
|
+
if (sub === 'rename') {
|
|
759
|
+
if (flags.label === undefined) { log('nodes rename: --label TEXT obbligatorio'); return { code: 1 }; }
|
|
760
|
+
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { label: flags.label } }).code };
|
|
761
|
+
}
|
|
762
|
+
if (sub === 'visibility') {
|
|
763
|
+
const visibility = rest[3] || flags.visibility;
|
|
764
|
+
const selected = flags.selected === undefined ? []
|
|
765
|
+
: String(flags.selected).split(',').map((value) => value.trim()).filter(Boolean);
|
|
766
|
+
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { visibility, selected } }).code };
|
|
767
|
+
}
|
|
768
|
+
if (sub === 'remove') {
|
|
769
|
+
if (flags.yes !== true) { log('nodes remove: conferma richiesta con --yes'); return { code: 1 }; }
|
|
770
|
+
return { code: nodesCmds.nodesRemove({ ...opts, log, ref }).code };
|
|
771
|
+
}
|
|
772
|
+
if (sub === 'test') return { code: (await nodesCmds.nodesTest({ ...opts, log, ref })).code };
|
|
773
|
+
if (['up', 'down', 'connect', 'disconnect', 'restart', 'reconnect'].includes(sub)) {
|
|
774
|
+
const fn = sub === 'up' || sub === 'connect' ? nodesCmds.nodesUp
|
|
775
|
+
: sub === 'restart' || sub === 'reconnect' ? nodesCmds.nodesRestart : nodesCmds.nodesDown;
|
|
776
|
+
const persistAutostart = sub === 'up' || sub === 'down' || flags.persist === true;
|
|
777
|
+
return { code: fn({ ...opts, log, ref, persistAutostart }).code };
|
|
778
|
+
}
|
|
779
|
+
if (sub === 'share') {
|
|
780
|
+
const inspected = nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
|
|
781
|
+
if (inspected.code !== 0 || inspected.peer.kind !== 'direct') {
|
|
782
|
+
log(`nodes share: peer diretto sconosciuto "${ref || ''}"`); return { code: 1 };
|
|
783
|
+
}
|
|
784
|
+
let shared;
|
|
785
|
+
try { shared = boolFlag(rest[3], 'nodes share'); }
|
|
786
|
+
catch (error) { log(error.message); return { code: 1 }; }
|
|
787
|
+
try {
|
|
788
|
+
const result = await localSettingsRequest(
|
|
789
|
+
`/api/settings/nodes/${encodeURIComponent(inspected.peer.name)}/share`,
|
|
790
|
+
{ shared }, { ...opts, method: 'PATCH' },
|
|
791
|
+
);
|
|
792
|
+
if (flags.json === true) log(JSON.stringify(result, null, 2));
|
|
793
|
+
else log(`nodes share [${inspected.peer.name}]: ${shared ? 'on' : 'off'}`);
|
|
794
|
+
return { code: 0 };
|
|
795
|
+
} catch (error) { log(`nodes share: ${error.message}`); return { code: 1 }; }
|
|
796
|
+
}
|
|
797
|
+
if (sub === 'invite') {
|
|
798
|
+
const body = {
|
|
799
|
+
...(flags.ssh !== undefined ? { ssh: flags.ssh } : {}),
|
|
800
|
+
...(flags['ssh-port'] !== undefined ? { sshPort: Number(flags['ssh-port']) } : {}),
|
|
801
|
+
...(flags.name !== undefined ? { name: flags.name } : {}),
|
|
802
|
+
...(flags.label !== undefined ? { label: flags.label } : {}),
|
|
803
|
+
};
|
|
804
|
+
if (!body.ssh) { log('nodes invite: --ssh TARGET obbligatorio'); return { code: 1 }; }
|
|
805
|
+
try {
|
|
806
|
+
const result = await localSettingsRequest('/api/settings/peering/invite', body, opts);
|
|
807
|
+
if (flags.json === true) log(JSON.stringify(result, null, 2));
|
|
808
|
+
else { log(result.pairingUrl); log(`scade: ${new Date(result.expiresAt).toISOString()}`); }
|
|
809
|
+
return { code: 0 };
|
|
810
|
+
} catch (e) { log(`nodes invite: ${e.message}`); return { code: 1 }; }
|
|
811
|
+
}
|
|
812
|
+
if (sub === 'pair' || sub === 'join') {
|
|
813
|
+
const pairingUrl = readPairingStdin(opts);
|
|
814
|
+
if (!pairingUrl) {
|
|
815
|
+
log("nodes pair: passa il link via stdin (es. printf '%s' \"$PAIRING_URL\" | nexuscrew nodes pair)");
|
|
816
|
+
return { code: 1 };
|
|
817
|
+
}
|
|
818
|
+
const decoded = peering.parsePairingUrl(pairingUrl);
|
|
819
|
+
if (!decoded) { log('nodes pair: link non valido o corrotto'); return { code: 1 }; }
|
|
820
|
+
const body = {
|
|
821
|
+
pairingUrl,
|
|
822
|
+
name: flags.name || decoded.name,
|
|
823
|
+
ssh: flags.ssh || decoded.ssh,
|
|
824
|
+
...(flags.label !== undefined || decoded.label ? { label: flags.label || decoded.label } : {}),
|
|
825
|
+
...(flags['ssh-port'] !== undefined || decoded.sshPort ? { sshPort: Number(flags['ssh-port'] || decoded.sshPort) } : {}),
|
|
826
|
+
...(flags['local-label'] !== undefined ? { localLabel: flags['local-label'] } : {}),
|
|
827
|
+
...(flags['identity-file'] !== undefined ? { identityFile: flags['identity-file'] } : {}),
|
|
828
|
+
};
|
|
829
|
+
if (!body.name || !body.ssh) {
|
|
830
|
+
log('nodes pair: il link non contiene name/ssh; fornisci --name e --ssh');
|
|
831
|
+
return { code: 1 };
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const result = await localSettingsRequest('/api/settings/nodes/pair', body, opts);
|
|
835
|
+
if (flags.json === true) log(JSON.stringify(result, null, 2));
|
|
836
|
+
else log(`nodes pair: collegato "${result.name}" (${result.instanceId})`);
|
|
837
|
+
return { code: 0 };
|
|
838
|
+
} catch (e) {
|
|
839
|
+
const stage = e.data && e.data.stage ? ` [${e.data.stage}]` : '';
|
|
840
|
+
const hint = e.data && e.data.hint ? ` · ${e.data.hint}` : '';
|
|
841
|
+
log(`nodes pair${stage}: ${e.message}${hint}`);
|
|
842
|
+
return { code: 1 };
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
log(`comando nodes sconosciuto: ${sub}\n\n${NODES_HELP}`);
|
|
846
|
+
return { code: 1 };
|
|
847
|
+
}
|
|
848
|
+
|
|
626
849
|
function dispatch(argv, opts = {}) {
|
|
627
850
|
const log = opts.log || console.log;
|
|
628
851
|
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
@@ -633,7 +856,7 @@ function dispatch(argv, opts = {}) {
|
|
|
633
856
|
log(require('../../package.json').version);
|
|
634
857
|
return { code: 0 };
|
|
635
858
|
}
|
|
636
|
-
const { flags, rest } = parseFlags(argv);
|
|
859
|
+
const { flags, rest } = parseFlags(argv, CLI_VALUE_FLAGS);
|
|
637
860
|
const cmd = rest[0];
|
|
638
861
|
|
|
639
862
|
// help esplicito
|
|
@@ -673,9 +896,10 @@ function dispatch(argv, opts = {}) {
|
|
|
673
896
|
return { code: 0 };
|
|
674
897
|
}
|
|
675
898
|
if (cmd === 'status') {
|
|
676
|
-
status({ ...opts, log });
|
|
899
|
+
status({ ...opts, log, json: flags.json === true });
|
|
677
900
|
return { code: 0 };
|
|
678
901
|
}
|
|
902
|
+
if (cmd === 'nodes' || cmd === 'peers') return dispatchNodes(rest, flags, opts);
|
|
679
903
|
if (cmd === 'stop') {
|
|
680
904
|
const result = stop({ ...opts, log });
|
|
681
905
|
return { code: result.stopped || ['not running', 'stale pidfile'].includes(result.reason) ? 0 : 1 };
|
|
@@ -691,6 +915,7 @@ function dispatch(argv, opts = {}) {
|
|
|
691
915
|
return { code: 0, keepAlive: true }; // server.listen tiene il processo vivo; non exit
|
|
692
916
|
}
|
|
693
917
|
if (cmd === 'doctor') {
|
|
918
|
+
if (flags.peers === true) return dispatchNodes(['nodes', 'doctor'], flags, opts);
|
|
694
919
|
const r = doctor({ ...opts, log });
|
|
695
920
|
return { code: r.code };
|
|
696
921
|
}
|
|
@@ -709,7 +934,7 @@ function dispatch(argv, opts = {}) {
|
|
|
709
934
|
}
|
|
710
935
|
|
|
711
936
|
module.exports = {
|
|
712
|
-
dispatch, serve, start, stop, status, parseFlags, HELP,
|
|
937
|
+
dispatch, dispatchNodes, serve, start, stop, status, parseFlags, HELP, NODES_HELP,
|
|
713
938
|
smartUp, url, tokenRotate, logs, doctor, update, restart,
|
|
714
939
|
portAvailable, findAvailablePort, probeNexusCrew, waitForNexusCrew, openPwa, startPortable, wizardComplete,
|
|
715
940
|
servicePinsLegacyPort,
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -164,63 +164,29 @@ function migrationGate(opts = {}) {
|
|
|
164
164
|
return {
|
|
165
165
|
blocked: true,
|
|
166
166
|
units,
|
|
167
|
-
remediation: 'disabilita le unit legacy cloud-cell@*.service
|
|
167
|
+
remediation: 'esegui il backup, poi disabilita le unit legacy cloud-cell@*.service prima di abilitare nexuscrew-fleet.service; non devono esistere due proprietari del boot',
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
return { blocked: false, units: [], reason: 'nessuna unit cloud-cell@*.service abilitata' };
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// --- selectProviderModeSync — resolver SINCRONO del mode del provider ---
|
|
174
|
-
// Speculare alla logica di
|
|
175
|
-
//
|
|
176
|
-
// per 'init'; selectProvider e' async e non sopravvive al process.exit). E' il default
|
|
177
|
-
// del seam opts.selectProvider del companion (iniettabile per i test).
|
|
178
|
-
//
|
|
179
|
-
// Divergenza documentata vs selectProvider: la responsivita' del fleet esterno
|
|
180
|
-
// (status --json) richiederebbe spawn async; qui un fleetBin FIDATO (binTrusted, sync)
|
|
181
|
-
// -> external. Scelta CONSERVATIVA sicura (no-doppio-boot, §9b): se c'e' un fleet
|
|
182
|
-
// esterno fidato configurato, NON installiamo il companion builtin.
|
|
183
|
-
// Ritorna {mode, reason} con mode in 'external' | 'builtin' | 'disabled'.
|
|
174
|
+
// Speculare alla logica builtin-only di lib/fleet/provider.js ma sincrono per
|
|
175
|
+
// runInit. Il binario legacy `fleet` non viene scoperto né eseguito.
|
|
184
176
|
function selectProviderModeSync(cfg = {}) {
|
|
185
177
|
if (cfg.fleetEnabled === false) {
|
|
186
178
|
return { mode: 'disabled', reason: 'fleet disabilitato (fleetEnabled=false)' };
|
|
187
179
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// external: binario fidato (regular file, exec, non world-writable, no symlink).
|
|
191
|
-
// lazy-require: niente costo/accoppiamento all'import del modulo (usato solo dal companion).
|
|
192
|
-
const { binTrusted, externalFleetCandidates } = require('../fleet/index.js'); // sync (audit F3)
|
|
193
|
-
// Deve usare lo stesso insieme di path del resolver runtime asincrono. In
|
|
194
|
-
// particolare Termux installa normalmente `fleet` in $PREFIX/bin: se init
|
|
195
|
-
// guardasse soltanto cfg.fleetBin potrebbe installare il companion builtin e
|
|
196
|
-
// il runtime scegliere poi l'external, creando due proprietari del boot.
|
|
197
|
-
const trustedExternal = externalFleetCandidates(cfg).find((candidate) => binTrusted(candidate)) || null;
|
|
198
|
-
const extTrusted = !!trustedExternal;
|
|
180
|
+
if (cfg.builtinEnabled === false) return { mode: 'disabled', reason: 'fleet builtin disabilitata' };
|
|
199
181
|
|
|
200
182
|
// builtin: fleet.json valido (loadDefinitions e' sync; garbage -> null = fail-closed).
|
|
201
183
|
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
202
184
|
const home = cfg.home || os.homedir();
|
|
203
185
|
const defsPath = cfg.fleetDefsPath || path.join(home, '.nexuscrew', 'fleet.json');
|
|
204
|
-
const builtinAvail =
|
|
205
|
-
|
|
206
|
-
// forced: fail-closed se il richiesto non e' disponibile (§9g: niente auto-fallback).
|
|
207
|
-
if (forced === 'external') {
|
|
208
|
-
return extTrusted
|
|
209
|
-
? { mode: 'external', reason: 'forced external (binario fidato)' }
|
|
210
|
-
: { mode: 'disabled', reason: 'fail-closed: forced external non fidato (§9g)' };
|
|
211
|
-
}
|
|
212
|
-
if (forced === 'builtin') {
|
|
213
|
-
return builtinAvail
|
|
214
|
-
? { mode: 'builtin', reason: 'forced builtin (fleet.json valido)' }
|
|
215
|
-
: { mode: 'disabled', reason: 'fail-closed: forced builtin ma fleet.json invalido/mancante (§9g)' };
|
|
216
|
-
}
|
|
217
|
-
if (forced === 'disabled') return { mode: 'disabled', reason: 'forced disabled' };
|
|
218
|
-
if (forced) return { mode: 'disabled', reason: `fail-closed: provider forzato "${forced}" non riconosciuto` };
|
|
186
|
+
const builtinAvail = !!loadDefinitions(defsPath);
|
|
219
187
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (builtinAvail) return { mode: 'builtin', reason: 'auto: builtin (fleet.json valido)' };
|
|
223
|
-
return { mode: 'disabled', reason: 'auto: nessun provider disponibile (no external fidato, no fleet.json)' };
|
|
188
|
+
if (builtinAvail) return { mode: 'builtin', reason: 'NexusCrew builtin (fleet.json valido)' };
|
|
189
|
+
return { mode: 'disabled', reason: 'fleet.json invalido o mancante (fail-closed)' };
|
|
224
190
|
}
|
|
225
191
|
|
|
226
192
|
// --- Install (speculare a installService di lib/cli/service.js) ---
|
package/lib/cli/init.js
CHANGED
|
@@ -219,8 +219,6 @@ function runInit(opts = {}) {
|
|
|
219
219
|
const fleetCfg = opts.fleetCfg || {
|
|
220
220
|
home,
|
|
221
221
|
fleetEnabled: opts.fleetEnabled !== undefined ? opts.fleetEnabled : true,
|
|
222
|
-
fleetBin: opts.fleetBin || path.join(home, '.local', 'bin', 'fleet'),
|
|
223
|
-
fleetProvider: opts.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER,
|
|
224
222
|
builtinEnabled: opts.builtinEnabled,
|
|
225
223
|
fleetDefsPath,
|
|
226
224
|
};
|
package/lib/config.js
CHANGED
|
@@ -28,7 +28,6 @@ function baseDefaults() {
|
|
|
28
28
|
voiceToken: '',
|
|
29
29
|
voiceTokenFile: null,
|
|
30
30
|
fleetEnabled: true,
|
|
31
|
-
fleetBin: path.join(os.homedir(), '.local', 'bin', 'fleet'),
|
|
32
31
|
providerSecretsPath: path.join(os.homedir(), '.nexuscrew', 'providers.env'),
|
|
33
32
|
// Existing user-owned shell exports. NexusCrew parses simple assignments
|
|
34
33
|
// as data; it never executes/sources this file and never copies values.
|
|
@@ -74,7 +73,6 @@ function envOverrides() {
|
|
|
74
73
|
if (process.env.NEXUSCREW_VOICE_TOKEN) e.voiceToken = process.env.NEXUSCREW_VOICE_TOKEN;
|
|
75
74
|
if (process.env.NEXUSCREW_VOICE_TOKEN_FILE) e.voiceTokenFile = process.env.NEXUSCREW_VOICE_TOKEN_FILE;
|
|
76
75
|
if (process.env.NEXUSCREW_FLEET) e.fleetEnabled = process.env.NEXUSCREW_FLEET !== '0';
|
|
77
|
-
if (process.env.NEXUSCREW_FLEET_BIN) e.fleetBin = process.env.NEXUSCREW_FLEET_BIN;
|
|
78
76
|
if (process.env.NEXUSCREW_PROVIDER_SECRETS) e.providerSecretsPath = process.env.NEXUSCREW_PROVIDER_SECRETS;
|
|
79
77
|
if (process.env.NEXUSCREW_PROVIDER_SHELL) e.providerShellPath = process.env.NEXUSCREW_PROVIDER_SHELL;
|
|
80
78
|
if (process.env.NEXUSCREW_PROVIDER_KEYS) e.providerKeysPath = process.env.NEXUSCREW_PROVIDER_KEYS;
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// tramite lib/fleet/definitions.js, e lo ESTENDE con define*/edit*/remove* +
|
|
2
|
+
// Fleet built-in. FACADE: espone available/status/up/down/engine/boot e
|
|
3
|
+
// isCellSession leggendo le definizioni da ~/.nexuscrew/fleet.json tramite
|
|
4
|
+
// lib/fleet/definitions.js, insieme a define*/edit*/remove* +
|
|
6
5
|
// schema + capabilities (design §4b/§9c).
|
|
7
6
|
//
|
|
8
7
|
// La responsabilita' runtime/launch (status/up/down/restart + launch/readiness
|
|
@@ -94,7 +93,7 @@ function applyCellTransition(target, engineId, { model, hasModel, policy, hasPol
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
// ---------------------------------------------------------------------------
|
|
97
|
-
// createBuiltinFleet(cfg) —
|
|
96
|
+
// createBuiltinFleet(cfg) — unica implementazione Fleet di NexusCrew.
|
|
98
97
|
// cfg: { fleetDefsPath?, tmuxBin?, home?, builtinEnabled?, readonlyDefault?,
|
|
99
98
|
// sendKeysReadyMs?, fleetProviderReason?, launchBroker?, launchReadyMs? }
|
|
100
99
|
// ---------------------------------------------------------------------------
|