@mmmbuto/nexuscrew 0.8.21 → 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.
@@ -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-DaFQbMHq.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DQbVJLji.css">
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.21"}
1
+ {"version":"0.8.22"}
@@ -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
- // Provider external legacy may not expose `tmux`; an explicit false still
21
- // wins, while the historical `active:true` contract remains compatible.
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}`,
@@ -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, nodes, engines, providers, models and lifecycle live in the PWA.
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,
@@ -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 (systemctl --user disable "cloud-cell@*") o usa il fleet esterno; il companion nexuscrew-fleet.service non va installato per evitare doppio boot',
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 selectProvider (lib/fleet/provider.js) ma SENZA spawn/await.
175
- // Necessario: runInit e' sync (bin/nexuscrew.js chiama process.exit subito dopo dispatch
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 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
- const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase() || null;
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 = cfg.builtinEnabled !== false ? !!loadDefinitions(defsPath) : false;
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
- // auto: external fidato vince (no-doppio-boot), poi builtin, poi disabled.
221
- if (extTrusted) return { mode: 'external', reason: `auto: external fidato configurato (${trustedExternal}; skip companion)` };
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;
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
- // B4.2 — Fleet built-in. FACADE: implementa lo STESSO contratto di
3
- // lib/fleet/index.js (createFleet: available / status / up / down / engine /
4
- // boot / isCellSession) leggendo le definizioni da ~/.nexuscrew/fleet.json
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) — async per parita' di contratto con createFleet.
96
+ // createBuiltinFleet(cfg) — unica implementazione Fleet di NexusCrew.
98
97
  // cfg: { fleetDefsPath?, tmuxBin?, home?, builtinEnabled?, readonlyDefault?,
99
98
  // sendKeysReadyMs?, fleetProviderReason?, launchBroker?, launchReadyMs? }
100
99
  // ---------------------------------------------------------------------------