@mmmbuto/nexuscrew 0.7.5 → 0.7.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.
@@ -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-q9PUrGO0.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DWhfVwKW.css">
14
+ <script type="module" crossorigin src="/assets/index-EVa9bnAC.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DUbtTZMj.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -18,6 +18,7 @@ Usage:
18
18
  nexuscrew start avvia il servizio (systemctl / launchctl / nohup+pidfile)
19
19
  nexuscrew stop stop del servizio (service manager / pidfile verificato)
20
20
  nexuscrew status stato: platform + service + porta + URL
21
+ nexuscrew fleet-boot companion di boot: avvia le celle boot:true (config-driven)
21
22
 
22
23
  Piattaforme: linux (systemd --user), mac (launchd), termux (nohup + pidfile).
23
24
  Bind loopback 127.0.0.1 — raggiungibile via tunnel SSH/VPN.`;
@@ -162,6 +163,49 @@ function status(opts = {}) {
162
163
  return out;
163
164
  }
164
165
 
166
+ // B4.3 — fleet-boot companion: avvia le celle boot:true del provider selezionato.
167
+ // dispatch e' sync (bin/nexuscrew.js non fa await), quindi runFleetBoot (async)
168
+ // viene lanciato e il processo tenuto vivo (keepAlive) finche' non termina, poi
169
+ // esce col code risultante. runFleetBoot e' la unit testabile (no process.exit);
170
+ // dispatchFleetBoot e' il thin glue. Seam iniettabili: selectProvider / loadConfig
171
+ // / cfg / exit (per test, no process.exit che uccide il runner).
172
+ async function runFleetBoot(opts = {}) {
173
+ const log = opts.log || console.log;
174
+ const loadConfig = opts.loadConfig || require('../config.js').loadConfig;
175
+ const selectProvider = opts.selectProvider || require('../fleet/provider.js').selectProvider;
176
+ const { bootCells } = require('../fleet/boot.js');
177
+ const cfg = opts.cfg || loadConfig();
178
+
179
+ const sel = await selectProvider(cfg);
180
+ if (sel.mode === 'external') {
181
+ log('fleet-boot: boot gestito dal fleet esterno (nessuna azione del companion)');
182
+ return { code: 0, mode: 'external' };
183
+ }
184
+ if (sel.mode !== 'builtin' || !sel.fleet || !sel.fleet.available) {
185
+ // disabled (o builtin unavailable): niente da avviare, non e' un errore.
186
+ log(`fleet-boot: nessun boot (provider ${sel.mode}${sel.reason ? ' — ' + sel.reason : ''})`);
187
+ return { code: 0, mode: sel.mode };
188
+ }
189
+
190
+ // builtin: up() per ogni cella boot:true. READONLY fa fallire le up con 403 ->
191
+ // raccolte in failed[] -> exit 1 (nessun short-circuit speciale, design §9d).
192
+ const res = await bootCells(sel.fleet, { log });
193
+ log(`fleet-boot: ${res.started.length} started, ${res.skipped.length} skipped, ${res.failed.length} failed`);
194
+ for (const f of res.failed) log(` failed: ${f.cell} — ${f.reason}`);
195
+ return { code: res.failed.length ? 1 : 0, mode: 'builtin', summary: res };
196
+ }
197
+
198
+ function dispatchFleetBoot(opts) {
199
+ const exit = typeof opts.exit === 'function' ? opts.exit : ((c) => process.exit(c));
200
+ runFleetBoot(opts)
201
+ .then((r) => exit(r.code))
202
+ .catch((e) => {
203
+ (opts.log || console.log)(`fleet-boot: error — ${e && e.message ? e.message : e}`);
204
+ exit(1);
205
+ });
206
+ return { code: 0, keepAlive: true }; // il processo resta vivo finche' runFleetBoot non chiama exit()
207
+ }
208
+
165
209
  function dispatch(argv, opts = {}) {
166
210
  const { flags, rest } = parseFlags(argv);
167
211
  const cmd = rest[0];
@@ -191,8 +235,11 @@ function dispatch(argv, opts = {}) {
191
235
  status({ execImpl: opts.execImpl, log, platform: opts.platform, uid: opts.uid, home: opts.home });
192
236
  return { code: 0 };
193
237
  }
238
+ if (cmd === 'fleet-boot') {
239
+ return dispatchFleetBoot({ ...opts, log });
240
+ }
194
241
  log(`unknown command: ${cmd}\n\n${HELP}`);
195
242
  return { code: 1 };
196
243
  }
197
244
 
198
- module.exports = { dispatch, serve, start, stop, status, parseFlags, HELP };
245
+ module.exports = { dispatch, serve, start, stop, status, parseFlags, HELP, runFleetBoot, dispatchFleetBoot };
@@ -0,0 +1,306 @@
1
+ 'use strict';
2
+ // B4.3 — Service companion (boot) generation + migration gate. Design §4c / §9b.
3
+ //
4
+ // generateFleetService({platform, nodeBin, entryPath}) genera il contenuto del
5
+ // service UNICO di boot (NON una unit per cella) che al boot esegue
6
+ // <node> <entry> fleet-boot
7
+ // seguendo ESATTAMENTE i pattern/escaping di lib/cli/service.js:
8
+ // linux -> systemd --user 'nexuscrew-fleet.service' (Type=oneshot)
9
+ // mac -> launchd plist 'com.mmmbuto.nexuscrew-fleet.plist' (RunAtLoad)
10
+ // termux -> script '~/.termux/boot/nexuscrew-fleet.sh'
11
+ //
12
+ // migrationGate({exec, platform}) rileva unit legacy 'cloud-cell@*.service'
13
+ // abilitate (systemctl --user list-unit-files --state=enabled, via execFile —
14
+ // MAI shell string). Se presenti -> {blocked:true, units, remediation}: il
15
+ // companion NON va installato (design 9b: mai doppio boot silenzioso). Su
16
+ // piattaforme senza systemd il gate passa. `exec` e' iniettabile per testabilita'.
17
+ //
18
+ // Nessun side-effect all'import e NESSUNA installazione reale qui (solo generazione
19
+ // contenuto + gate). L'installazione e' demandata al flusso init (separato).
20
+
21
+ const fs = require('node:fs');
22
+ const os = require('node:os');
23
+ const path = require('node:path');
24
+ const { execFileSync } = require('node:child_process');
25
+ const {
26
+ escapeSystemdPath, escapeSystemdExec, escapeXml, shellQuote, assertSystemdSafe,
27
+ } = require('./service.js');
28
+ const { uid } = require('./platform.js');
29
+
30
+ // entryPath e' tipicamente <repoRoot>/bin/nexuscrew.js -> repoRoot = dirname x2.
31
+ function deriveRepoRoot(entryPath) {
32
+ return path.dirname(path.dirname(entryPath));
33
+ }
34
+
35
+ function generateFleetService(opts) {
36
+ const platform = opts && opts.platform;
37
+ if (platform === 'linux') return generateFleetLinux(opts);
38
+ if (platform === 'mac') return generateFleetMac(opts);
39
+ if (platform === 'termux') return generateFleetTermux(opts);
40
+ throw new Error(`unsupported platform: ${platform}`);
41
+ }
42
+
43
+ function generateFleetLinux(opts) {
44
+ const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
45
+ const { nodeBin, entryPath } = opts;
46
+ // Parita' di hardening con service.js (M3): reject char non gestibili in systemd.
47
+ assertSystemdSafe('repoRoot', repoRoot);
48
+ assertSystemdSafe('nodeBin', nodeBin);
49
+ assertSystemdSafe('entryPath', entryPath);
50
+ const repo = escapeSystemdPath(repoRoot);
51
+ const node = escapeSystemdExec(nodeBin);
52
+ const entry = escapeSystemdExec(entryPath);
53
+ const nodeDir = escapeSystemdPath(path.dirname(nodeBin));
54
+ return `# NexusCrew fleet boot companion (systemd --user) - avvia le celle boot:true
55
+ [Unit]
56
+ Description=NexusCrew fleet boot companion (avvia le celle boot:true)
57
+ After=network-online.target
58
+
59
+ [Service]
60
+ Type=oneshot
61
+ WorkingDirectory=${repo}
62
+ Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
63
+ ExecStart=${node} ${entry} fleet-boot
64
+
65
+ [Install]
66
+ WantedBy=default.target
67
+ `;
68
+ }
69
+
70
+ function generateFleetMac(opts) {
71
+ const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
72
+ const home = opts.home || os.homedir();
73
+ const nodeXml = escapeXml(opts.nodeBin);
74
+ const entryXml = escapeXml(opts.entryPath);
75
+ const repoXml = escapeXml(repoRoot);
76
+ const homeXml = escapeXml(home);
77
+ return `<?xml version="1.0" encoding="UTF-8"?>
78
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
79
+ <plist version="1.0">
80
+ <dict>
81
+ <key>Label</key>
82
+ <string>com.mmmbuto.nexuscrew-fleet</string>
83
+ <key>ProgramArguments</key>
84
+ <array>
85
+ <string>${nodeXml}</string>
86
+ <string>${entryXml}</string>
87
+ <string>fleet-boot</string>
88
+ </array>
89
+ <key>WorkingDirectory</key>
90
+ <string>${repoXml}</string>
91
+ <key>RunAtLoad</key>
92
+ <true/>
93
+ <key>StandardOutPath</key>
94
+ <string>${homeXml}/.nexuscrew/fleet-boot.log</string>
95
+ <key>StandardErrorPath</key>
96
+ <string>${homeXml}/.nexuscrew/fleet-boot.log</string>
97
+ </dict>
98
+ </plist>
99
+ `;
100
+ }
101
+
102
+ function generateFleetTermux(opts) {
103
+ const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
104
+ const nodeQ = shellQuote(opts.nodeBin);
105
+ const entryQ = shellQuote(opts.entryPath);
106
+ const repoQ = shellQuote(repoRoot);
107
+ return `#!/data/data/com.termux/files/usr/bin/sh
108
+ # NexusCrew fleet boot companion (Termux) - avvia le celle boot:true
109
+ export PATH=/data/data/com.termux/files/usr/bin:$PATH
110
+ export HOME=/data/data/com.termux/files/home
111
+ cd -- ${repoQ}
112
+ exec ${nodeQ} ${entryQ} fleet-boot >> "$HOME/.nexuscrew/fleet-boot.log" 2>&1
113
+ `;
114
+ }
115
+
116
+ // Default exec per il gate: execFileSync (argv diretto, MAI shell string).
117
+ function defaultListEnabled(bin, args, opts) {
118
+ return execFileSync(bin, args, opts);
119
+ }
120
+
121
+ // Migration gate (design 9b): blocca l'installazione del companion se ci sono
122
+ // unit legacy cloud-cell@*.service abilitate (mai doppio boot silenzioso).
123
+ // opts.exec — funzione iniettabile con firma execFileSync(bin, args, opts)
124
+ // opts.platform — se !== 'linux' il gate passa (no systemd -> no rischio)
125
+ // Ritorna {blocked, units[], remediation?|reason?}.
126
+ function migrationGate(opts = {}) {
127
+ const { exec, platform } = opts;
128
+ if (platform && platform !== 'linux') {
129
+ return { blocked: false, units: [], reason: `platform ${platform}: no systemd, gate skipped` };
130
+ }
131
+ const execImpl = typeof exec === 'function' ? exec : defaultListEnabled;
132
+
133
+ let stdout;
134
+ try {
135
+ stdout = execImpl('systemctl',
136
+ ['--user', 'list-unit-files', '--state=enabled', 'cloud-cell@*'],
137
+ { encoding: 'utf8' });
138
+ } catch (e) {
139
+ // systemctl assente / ambiente non systemd-user: nessuna evidenza di unit
140
+ // legacy -> il gate passa (non blocca su assenza di informazioni).
141
+ return { blocked: false, units: [], reason: `gate skipped: ${e && e.message ? e.message : 'command error'}` };
142
+ }
143
+
144
+ const units = [];
145
+ for (const line of String(stdout || '').split('\n')) {
146
+ const tok = line.trim().split(/\s+/)[0];
147
+ if (tok && /^cloud-cell@.+\.service$/.test(tok)) units.push(tok);
148
+ }
149
+
150
+ if (units.length) {
151
+ return {
152
+ blocked: true,
153
+ units,
154
+ 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',
155
+ };
156
+ }
157
+ return { blocked: false, units: [], reason: 'nessuna unit cloud-cell@*.service abilitata' };
158
+ }
159
+
160
+ // --- selectProviderModeSync — resolver SINCRONO del mode del provider ---
161
+ // Speculare alla logica di selectProvider (lib/fleet/provider.js) ma SENZA spawn/await.
162
+ // Necessario: runInit e' sync (bin/nexuscrew.js chiama process.exit subito dopo dispatch
163
+ // per 'init'; selectProvider e' async e non sopravvive al process.exit). E' il default
164
+ // del seam opts.selectProvider del companion (iniettabile per i test).
165
+ //
166
+ // Divergenza documentata vs selectProvider: la responsivita' del fleet esterno
167
+ // (status --json) richiederebbe spawn async; qui un fleetBin FIDATO (binTrusted, sync)
168
+ // -> external. Scelta CONSERVATIVA sicura (no-doppio-boot, §9b): se c'e' un fleet
169
+ // esterno fidato configurato, NON installiamo il companion builtin.
170
+ // Ritorna {mode, reason} con mode in 'external' | 'builtin' | 'disabled'.
171
+ function selectProviderModeSync(cfg = {}) {
172
+ if (cfg.fleetEnabled === false) {
173
+ return { mode: 'disabled', reason: 'fleet disabilitato (fleetEnabled=false)' };
174
+ }
175
+ const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase() || null;
176
+
177
+ // external: binario fidato (regular file, exec, non world-writable, no symlink).
178
+ // lazy-require: niente costo/accoppiamento all'import del modulo (usato solo dal companion).
179
+ const { binTrusted } = require('../fleet/index.js'); // sync (audit F3)
180
+ const extTrusted = !!(cfg.fleetBin && binTrusted(cfg.fleetBin));
181
+
182
+ // builtin: fleet.json valido (loadDefinitions e' sync; garbage -> null = fail-closed).
183
+ const { loadDefinitions } = require('../fleet/definitions.js');
184
+ const home = cfg.home || os.homedir();
185
+ const defsPath = cfg.fleetDefsPath || path.join(home, '.nexuscrew', 'fleet.json');
186
+ const builtinAvail = cfg.builtinEnabled !== false ? !!loadDefinitions(defsPath) : false;
187
+
188
+ // forced: fail-closed se il richiesto non e' disponibile (§9g: niente auto-fallback).
189
+ if (forced === 'external') {
190
+ return extTrusted
191
+ ? { mode: 'external', reason: 'forced external (binario fidato)' }
192
+ : { mode: 'disabled', reason: 'fail-closed: forced external non fidato (§9g)' };
193
+ }
194
+ if (forced === 'builtin') {
195
+ return builtinAvail
196
+ ? { mode: 'builtin', reason: 'forced builtin (fleet.json valido)' }
197
+ : { mode: 'disabled', reason: 'fail-closed: forced builtin ma fleet.json invalido/mancante (§9g)' };
198
+ }
199
+ if (forced === 'disabled') return { mode: 'disabled', reason: 'forced disabled' };
200
+ if (forced) return { mode: 'disabled', reason: `fail-closed: provider forzato "${forced}" non riconosciuto` };
201
+
202
+ // auto: external fidato vince (no-doppio-boot), poi builtin, poi disabled.
203
+ if (extTrusted) return { mode: 'external', reason: 'auto: external fidato configurato (skip companion)' };
204
+ if (builtinAvail) return { mode: 'builtin', reason: 'auto: builtin (fleet.json valido)' };
205
+ return { mode: 'disabled', reason: 'auto: nessun provider disponibile (no external fidato, no fleet.json)' };
206
+ }
207
+
208
+ // --- Install (speculare a installService di lib/cli/service.js) ---
209
+ // Stesso hardening di service.js: no-symlink atomic (lstat + tmp+rename), failure
210
+ // collection (NON ingoiare activation — M1), execImpl iniettabile (test). Differenze
211
+ // companion:
212
+ // - nomi 'nexuscrew-fleet.*' + mode termux 0o755 (boot script eseguibile)
213
+ // - linux: daemon-reload + enable, NESSUN restart (Type=oneshot -> parte al boot)
214
+ // - mac: bootout + bootstrap (idempotente come service.js)
215
+ // Nessuna installazione reale all'import: il companion e' installato dal flusso init.
216
+
217
+ function fleetInstallPath(platform, home) {
218
+ if (platform === 'linux') return path.join(home, '.config', 'systemd', 'user', 'nexuscrew-fleet.service');
219
+ if (platform === 'mac') return path.join(home, 'Library', 'LaunchAgents', 'com.mmmbuto.nexuscrew-fleet.plist');
220
+ if (platform === 'termux') return path.join(home, '.termux', 'boot', 'nexuscrew-fleet.sh');
221
+ throw new Error(`unsupported platform: ${platform}`);
222
+ }
223
+
224
+ function fleetFileMode(platform) {
225
+ if (platform === 'termux') return 0o755; // boot script eseguibile (design §4c: chmod 755)
226
+ return 0o644; // systemd unit + launchd plist (come service.js)
227
+ }
228
+
229
+ function fleetInstallCommands(platform, target, ctx) {
230
+ if (platform === 'linux') {
231
+ return [
232
+ ['systemctl', ['--user', 'daemon-reload']],
233
+ ['systemctl', ['--user', 'enable', 'nexuscrew-fleet.service']],
234
+ // NESSUN restart: Type=oneshot — il companion avvia le celle boot:true al boot.
235
+ ];
236
+ }
237
+ if (platform === 'mac') {
238
+ const label = `gui/${ctx.uid || uid()}/com.mmmbuto.nexuscrew-fleet`;
239
+ return [
240
+ ['launchctl', ['bootout', label]], // idempotente: ignorato se non caricato
241
+ ['launchctl', ['bootstrap', label, target]],
242
+ ];
243
+ }
244
+ if (platform === 'termux') {
245
+ return []; // nessun service manager; boot script + app Termux:Boot (start manuale)
246
+ }
247
+ return [];
248
+ }
249
+
250
+ // Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
251
+ // default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
252
+ // e' PRESERVATO e le failure raccolte per diagnosi (M1: non si ingoia, non si rollback).
253
+ function installFleetService(platform, content, ctx, { dryRun = false, execImpl = execFileSync } = {}) {
254
+ const home = ctx.home || os.homedir();
255
+ const target = ctx.installPath || fleetInstallPath(platform, home);
256
+ const mode = fleetFileMode(platform);
257
+
258
+ // lstat: reject symlink preesistente (no-symlink atomic, parita' service.js [M3])
259
+ try {
260
+ const st = fs.lstatSync(target);
261
+ if (st.isSymbolicLink()) {
262
+ throw new Error(`refusing symlink install target: ${target}`);
263
+ }
264
+ } catch (e) {
265
+ if (e.code !== 'ENOENT') throw e;
266
+ }
267
+
268
+ if (dryRun) {
269
+ return { target, mode, written: false, failures: [], note: 'dry-run: nessuna scrittura' };
270
+ }
271
+
272
+ fs.mkdirSync(path.dirname(target), { recursive: true });
273
+ const tmp = target + '.tmp.' + process.pid; // stessa dir -> atomic rename su stesso FS
274
+ try {
275
+ fs.writeFileSync(tmp, content, { mode });
276
+ fs.chmodSync(tmp, mode);
277
+ fs.renameSync(tmp, target); // atomic
278
+ } catch (e) {
279
+ try { fs.unlinkSync(tmp); } catch (_) {} // cleanup temp su failure [m1]
280
+ throw e;
281
+ }
282
+
283
+ // exec service manager — raccogli failure per diagnosi (NON ingoiare, M1)
284
+ const cmds = fleetInstallCommands(platform, target, ctx);
285
+ const failures = [];
286
+ for (const [bin, args] of cmds) {
287
+ try { execImpl(bin, args, { stdio: 'ignore' }); }
288
+ catch (e) { failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message }); }
289
+ }
290
+
291
+ return { target, mode, written: true, failures };
292
+ }
293
+
294
+ module.exports = {
295
+ generateFleetService,
296
+ generateFleetLinux,
297
+ generateFleetMac,
298
+ generateFleetTermux,
299
+ migrationGate,
300
+ deriveRepoRoot,
301
+ installFleetService,
302
+ fleetInstallPath,
303
+ fleetFileMode,
304
+ fleetInstallCommands,
305
+ selectProviderModeSync,
306
+ };
package/lib/cli/init.js CHANGED
@@ -3,7 +3,8 @@
3
3
  // detectPlatform -> prereq (Node>=18 abort, tmux abort-before-service) ->
4
4
  // migration rule (porta da service esistente) -> config.json (preserva) ->
5
5
  // token (preserva) -> NexusFiles -> generateService -> installService (skip dry-run) ->
6
- // print URL #token. Termux:boot detection best-effort.
6
+ // [B4.3] fleet companion (solo se provider builtin + gate ok; READONLY/dry-run skip;
7
+ // mai fa fallire l'init principale) -> print URL #token. Termux:boot best-effort.
7
8
  const fs = require('node:fs');
8
9
  const os = require('node:os');
9
10
  const path = require('node:path');
@@ -11,6 +12,11 @@ const { execFileSync } = require('node:child_process');
11
12
  const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
12
13
  const { loadOrCreateToken } = require('../auth/token.js');
13
14
  const { generateService, installService, fileMode, installPath: svcInstallPath } = require('./service.js');
15
+ // B4.3 — companion di boot del fleet (design §4c/§9b/§9d). Seam iniettabili per test.
16
+ const {
17
+ generateFleetService, installFleetService, migrationGate,
18
+ selectProviderModeSync, fleetFileMode,
19
+ } = require('./fleet-service.js');
14
20
 
15
21
  function haveTmux(tmuxBin) {
16
22
  try { execFileSync('command', ['-v', tmuxBin], { stdio: 'ignore', shell: true }); return true; }
@@ -127,6 +133,67 @@ function runInit(opts = {}) {
127
133
  }
128
134
  }
129
135
 
136
+ // --- B4.3 companion: service di boot del fleet (design §4c/§9b/§9d) ---
137
+ // Il companion si installa SOLO se il provider e' builtin (§9b) e il migration
138
+ // gate non rileva unit legacy cloud-cell@*.service abilitate (no doppio boot).
139
+ // READONLY blocca ogni azione del companion (§9d). MAI far fallire l'init
140
+ // principale: ogni errore del companion -> WARN action + return normale.
141
+ try {
142
+ const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
143
+ if (readonly) {
144
+ actions.push('fleet companion: READONLY, non installato');
145
+ } else {
146
+ // provider mode: companion SOLO se builtin (§9b). selectProviderModeSync e'
147
+ // il default SINCRONO (runInit e' sync: bin chiama process.exit dopo dispatch,
148
+ // selectProvider async non sopravvive). Seam opts.selectProvider per i test.
149
+ const selectProvider = opts.selectProvider || selectProviderModeSync;
150
+ const fleetCfg = opts.fleetCfg || {
151
+ home,
152
+ fleetEnabled: opts.fleetEnabled !== undefined ? opts.fleetEnabled : true,
153
+ fleetBin: opts.fleetBin || path.join(home, '.local', 'bin', 'fleet'),
154
+ fleetProvider: opts.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER,
155
+ builtinEnabled: opts.builtinEnabled,
156
+ fleetDefsPath: opts.fleetDefsPath || path.join(configDir, 'fleet.json'),
157
+ };
158
+ const sel = selectProvider(fleetCfg) || {};
159
+ if (sel.mode !== 'builtin') {
160
+ actions.push(`fleet companion: non installato (provider ${sel.mode})`);
161
+ } else {
162
+ // migration gate (§9b): seam opts.migrationGate iniettabile; default quello
163
+ // di fleet-service.js (exec iniettabile via opts.execImpl). blocked -> NON
164
+ // installare (mai doppio boot silenzioso).
165
+ const gate = (opts.migrationGate || migrationGate)({ exec: opts.execImpl, platform });
166
+ if (gate.blocked) {
167
+ actions.push(`WARN: fleet companion NON installato — migration gate bloccato: ${(gate.units || []).join(', ')} (${gate.remediation || 'disabilita le unit legacy cloud-cell@*.service'})`);
168
+ } else if (dryRun) {
169
+ actions.push('DRY-RUN fleet companion generato, NON installato');
170
+ } else {
171
+ const fleetContent = generateFleetService({
172
+ platform,
173
+ nodeBin: svcCtx.nodeBin,
174
+ entryPath: path.join(svcCtx.repoRoot, 'bin', 'nexuscrew.js'),
175
+ repoRoot: svcCtx.repoRoot,
176
+ home,
177
+ });
178
+ const fr = installFleetService(
179
+ platform, fleetContent,
180
+ { home, uid: uid(), installPath: opts.fleetInstallPath },
181
+ { execImpl: opts.execImpl },
182
+ );
183
+ if (fr.failures && fr.failures.length) {
184
+ // file installato MA activation (systemctl/launchctl) fallita [M1]
185
+ actions.push(`WARN: fleet companion file installato ${fr.target} MA activation fallita: ${fr.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
186
+ } else {
187
+ actions.push(`fleet companion installed ${fr.target} (mode 0${fleetFileMode(platform).toString(8)})`);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ } catch (e) {
193
+ // qualunque errore del companion: WARN + init principale prosegue intatto.
194
+ actions.push(`WARN: fleet companion fallito: ${e.message} (init principale prosegue)`);
195
+ }
196
+
130
197
  // Termux:boot best-effort detection (R4)
131
198
  if (platform === 'termux') {
132
199
  const bootDir = path.join(home, '.termux', 'boot');
@@ -23,7 +23,10 @@ function filesRoutes({ cfg, sessionExists, paste }) {
23
23
  return res.status(404).json({ error: 'sessione tmux inesistente' });
24
24
  }
25
25
  const saved = store.saveUpload(cfg.filesRoot, session, req.file.buffer, req.file.originalname);
26
- const pasted = await paste(session, saved.path);
26
+ // paste=false (tasto allegati del composer): il client appende il path al
27
+ // testo del composer — niente scrittura PTY. Default = incolla (FilesPanel).
28
+ const wantPaste = String((req.body && req.body.paste) || '') !== 'false';
29
+ const pasted = wantPaste ? await paste(session, saved.path) : false;
27
30
  res.json({ ...saved, pasted });
28
31
  });
29
32
  });
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+ // B4.3 — Service companion boot. bootCells(fleet, {log}) avvia tutte le celle
3
+ // boot:true di un fleet provider (builtin) chiamandone up(). Una cella gia' in
4
+ // esecuzione (up -> httpError 409 / "duplicate session") = SKIP non fatale;
5
+ // ogni altro errore viene raccolto in failed[] e NON ferma le altre celle.
6
+ // Nessun side-effect all'import. Design §4c / §9b.
7
+ //
8
+ // Agnostico: usa solo il contratto fleet {available, status(), up()} — non legge
9
+ // internals delle definizioni. La lista delle celle boot:true arriva da status()
10
+ // (campo `boot` derivato dalle definitions), cosi' un fleet esterno compatibile
11
+ // potra' riusare la stessa funzione.
12
+
13
+ const SKIP_STATUS = 409; // up() del builtin lancia httpError(409) su sessione duplicata
14
+
15
+ async function bootCells(fleet, { log = () => {} } = {}) {
16
+ const started = [];
17
+ const skipped = [];
18
+ const failed = [];
19
+
20
+ if (!fleet || fleet.available === false) {
21
+ // provider non disponibile: niente da avviare. Il caller (runFleetBoot)
22
+ // filtra gia' mode==='builtin' (= fleet.available); qui e' solo defense-in-depth.
23
+ return { started, skipped, failed };
24
+ }
25
+
26
+ let cells = [];
27
+ try {
28
+ const st = await fleet.status();
29
+ cells = (st && Array.isArray(st.cells)) ? st.cells : [];
30
+ } catch (e) {
31
+ // status e' una lettura: se fallisce non possiamo sapere cosa avviare.
32
+ // Riportiamo come fallimento globale senza processare alcuna cella.
33
+ failed.push({ cell: '*', reason: `status() failed: ${e && e.message ? e.message : e}` });
34
+ return { started, skipped, failed };
35
+ }
36
+
37
+ for (const c of cells) {
38
+ if (!c || !c.boot) continue; // solo boot:true
39
+ const id = c.cell;
40
+ try {
41
+ await fleet.up(id);
42
+ started.push(id);
43
+ log(`fleet-boot: started ${id}`);
44
+ } catch (e) {
45
+ const status = e && typeof e.status === 'number' ? e.status : null;
46
+ const reason = (e && e.message) ? e.message : String(e);
47
+ // gia' in esecuzione: skip non fatale (contratto builtin: 409/duplicate).
48
+ // Il regex copre anche fleet esterni che lanciano un Error generico.
49
+ if (status === SKIP_STATUS || /duplicate session|già in esecuzione/i.test(reason)) {
50
+ skipped.push(id);
51
+ log(`fleet-boot: skipped ${id} (already running)`);
52
+ } else {
53
+ failed.push({ cell: id, reason });
54
+ log(`fleet-boot: failed ${id} — ${reason}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ return { started, skipped, failed };
60
+ }
61
+
62
+ module.exports = { bootCells };