@mmmbuto/nexuscrew 0.8.27 → 0.8.29

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-CWKyv6KS.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-xxfypte7.css">
14
+ <script type="module" crossorigin src="/assets/index-BJdAdNOq.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BAzlX2nP.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"0.8.27"}
1
+ {"version":"0.8.29"}
@@ -14,7 +14,7 @@ const pidf = require('./pidfile.js');
14
14
  const { runInit, ensureFleetDefaults } = require('./init.js');
15
15
  const { rotateToken } = require('../auth/token.js');
16
16
  const urlmod = require('./url.js');
17
- const { doctor } = require('./doctor.js');
17
+ const { doctor, checkServiceWorkingDirectory } = require('./doctor.js');
18
18
  const nodesStore = require('../nodes/store.js');
19
19
  const nodesTunnel = require('../nodes/tunnel.js');
20
20
  const nodesCmds = require('../nodes/commands.js');
@@ -346,6 +346,66 @@ function servicePinsLegacyPort(platform, home, installPath) {
346
346
  try { return /NEXUSCREW_PORT/.test(fs.readFileSync(target, 'utf8')); } catch (_) { return false; }
347
347
  }
348
348
 
349
+ function fleetServiceDefinitionNeedsRefresh(platform, home, installPath) {
350
+ let target;
351
+ try { target = installPath || fleetInstallPath(platform, home); } catch (_) { return false; }
352
+ try { fs.lstatSync(target); }
353
+ catch (error) { return error.code !== 'ENOENT'; }
354
+ return !checkServiceWorkingDirectory(platform, home, target).ok;
355
+ }
356
+
357
+ function serviceDefinitionNeedsRefresh(platform, home, installPath, fleetInstallPathOverride) {
358
+ if (servicePinsLegacyPort(platform, home, installPath)) return true;
359
+ if (!checkServiceWorkingDirectory(platform, home, installPath).ok) return true;
360
+ return fleetServiceDefinitionNeedsRefresh(platform, home, fleetInstallPathOverride);
361
+ }
362
+
363
+ function serviceMigrationMarkerPath(home, override) {
364
+ return override || path.join(home, '.nexuscrew', 'service-cwd-migration.pending');
365
+ }
366
+
367
+ function serviceMigrationPending(home, override) {
368
+ const target = serviceMigrationMarkerPath(home, override);
369
+ try {
370
+ const st = fs.lstatSync(target);
371
+ if (st.isSymbolicLink() || !st.isFile()) throw new Error(`unsafe service migration marker: ${target}`);
372
+ return true;
373
+ } catch (error) {
374
+ if (error.code === 'ENOENT') return false;
375
+ throw error;
376
+ }
377
+ }
378
+
379
+ function ensureServiceMigrationMarker(home, override) {
380
+ const target = serviceMigrationMarkerPath(home, override);
381
+ if (serviceMigrationPending(home, override)) return target;
382
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
383
+ let fd;
384
+ try {
385
+ fd = fs.openSync(target, 'wx', 0o600);
386
+ fs.writeFileSync(fd, 'pending\n');
387
+ fs.fsyncSync(fd);
388
+ fs.fchmodSync(fd, 0o600);
389
+ } catch (error) {
390
+ if (error.code === 'EEXIST' && serviceMigrationPending(home, override)) return target;
391
+ throw error;
392
+ } finally {
393
+ if (fd !== undefined) fs.closeSync(fd);
394
+ }
395
+ return target;
396
+ }
397
+
398
+ function clearServiceMigrationMarker(home, override) {
399
+ const target = serviceMigrationMarkerPath(home, override);
400
+ try {
401
+ const st = fs.lstatSync(target);
402
+ if (st.isSymbolicLink() || !st.isFile()) throw new Error(`unsafe service migration marker: ${target}`);
403
+ fs.unlinkSync(target);
404
+ } catch (error) {
405
+ if (error.code !== 'ENOENT') throw error;
406
+ }
407
+ }
408
+
349
409
  function portAvailable(port, host = '127.0.0.1') {
350
410
  return new Promise((resolve) => {
351
411
  const s = net.createServer();
@@ -465,8 +525,38 @@ async function smartUp(opts = {}) {
465
525
  // config.json forever. Regenerate once so config.json becomes authoritative.
466
526
  const home = opts.home || require('node:os').homedir();
467
527
  const persistent = bootState({ ...opts, platform }).enabled;
468
- if (persistent && servicePinsLegacyPort(platform, home, opts.installPath)) {
469
- runInitImpl({ ...opts, log: quiet, platform, installBoot: true, printUrl: false });
528
+ let serviceRefreshed = false;
529
+ const migrationPending = persistent
530
+ ? serviceMigrationPending(home, opts.serviceMigrationMarkerPath) : false;
531
+ if (persistent && (migrationPending || serviceDefinitionNeedsRefresh(
532
+ platform, home, opts.installPath, opts.fleetInstallPath,
533
+ ))) {
534
+ ensureServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
535
+ const initResult = runInitImpl({ ...opts, log: quiet, platform, installBoot: true, printUrl: false });
536
+ serviceRefreshed = !serviceDefinitionNeedsRefresh(
537
+ platform, home, opts.installPath, opts.fleetInstallPath,
538
+ );
539
+ if (!serviceRefreshed) {
540
+ throw new Error('service cwd migration failed verification; refusing to continue with a replaceable working directory');
541
+ }
542
+ const installFailures = Array.isArray(initResult?.installFailures) ? initResult.installFailures : [];
543
+ if (installFailures.length) {
544
+ const components = [...new Set(installFailures.map((failure) => failure?.component)
545
+ .filter((component) => component === 'service' || component === 'fleet-companion'))];
546
+ throw new Error(`service cwd migration activation failed verification${components.length ? ` (${components.join(', ')})` : ''}; refusing to continue with an unverified service runtime`);
547
+ }
548
+ // Writing and activating a definition does not prove that an already-active
549
+ // Linux unit adopted it (`systemctl enable --now` is a no-op for that
550
+ // process). Restart every persistent runtime exactly once after the files
551
+ // and activation outcomes are verified. The lifecycle helper preserves the
552
+ // shared tmux server on Linux and uses PID-pinned portable restart on Termux.
553
+ const migrated = restartImpl({ ...opts, platform, log: quiet });
554
+ if (!migrated || migrated.restarted !== true) {
555
+ throw new Error(`service cwd migration restart failed verification (${platform}); refusing to continue with an unverified service runtime`);
556
+ }
557
+ // Only a verified file migration plus successful service activation (and,
558
+ // on Termux, a verified runtime restart) clears the durable retry marker.
559
+ clearServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
470
560
  }
471
561
 
472
562
  if (!port) port = urlmod.loadPort(opts);
@@ -972,6 +1062,9 @@ function dispatch(argv, opts = {}) {
972
1062
  module.exports = {
973
1063
  dispatch, dispatchNodes, serve, start, stop, status, parseFlags, HELP, NODES_HELP,
974
1064
  smartUp, url, tokenRotate, logs, doctor, update, restart,
1065
+ serviceDefinitionNeedsRefresh,
1066
+ fleetServiceDefinitionNeedsRefresh,
1067
+ serviceMigrationPending,
975
1068
  portAvailable, findAvailablePort, probeNexusCrew, waitForNexusCrew, openPwa, startPortable, wizardComplete,
976
1069
  servicePinsLegacyPort,
977
1070
  isServiceRunning, readRoles,
package/lib/cli/doctor.js CHANGED
@@ -11,6 +11,7 @@ const path = require('node:path');
11
11
  const { execFileSync } = require('node:child_process');
12
12
  const { detectPlatform, uid } = require('./platform.js');
13
13
  const { installPath } = require('./service.js');
14
+ const { fleetInstallPath } = require('./fleet-service.js');
14
15
  const { resolvePaths } = require('./url.js');
15
16
  const { commandExists } = require('./path.js');
16
17
  const { loadDefinitions } = require('../fleet/definitions.js');
@@ -79,25 +80,73 @@ function checkMacServiceWorkingDirectory(platform, home, installPathOverride) {
79
80
  if (platform !== 'mac') {
80
81
  return { name: 'launchd cwd stabile', ok: true, detail: `${platform}: non applicabile` };
81
82
  }
82
- const target = installPathOverride || installPath('mac', home);
83
+ return checkServiceWorkingDirectory(platform, home, installPathOverride);
84
+ }
85
+
86
+ // The service cwd is inherited by a shared tmux server and every future pane.
87
+ // It must therefore be HOME, never the replaceable runtime directory. This
88
+ // check reads only the installed definition and fails closed on missing,
89
+ // symlinked, malformed, or legacy service files.
90
+ function checkServiceWorkingDirectory(platform, home, installPathOverride) {
91
+ if (!['linux', 'mac', 'termux'].includes(platform)) {
92
+ return { name: 'service cwd stabile', ok: true, detail: `${platform}: non applicabile` };
93
+ }
94
+ const target = installPathOverride || installPath(platform, home);
83
95
  let raw;
84
- try { raw = fs.readFileSync(target, 'utf8'); }
96
+ try {
97
+ const st = fs.lstatSync(target);
98
+ if (st.isSymbolicLink() || !st.isFile()) throw Object.assign(new Error('definizione service non regolare'), { code: 'UNSAFE' });
99
+ raw = fs.readFileSync(target, 'utf8');
100
+ }
85
101
  catch (error) {
86
102
  return {
87
- name: 'launchd cwd stabile', ok: false,
88
- detail: error.code === 'ENOENT' ? `plist non installato (${target})` : error.message,
103
+ name: 'service cwd stabile', ok: false,
104
+ detail: error.code === 'ENOENT' ? `service non installato (${target})` : error.message,
89
105
  };
90
106
  }
91
- const match = raw.match(/<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/);
92
- if (!match) return { name: 'launchd cwd stabile', ok: false, detail: 'WorkingDirectory assente dal plist' };
93
- const actual = decodeXmlText(match[1]);
94
- const expected = String(home);
107
+ let actual = '';
108
+ let ok = false;
109
+ if (platform === 'mac') {
110
+ const match = raw.match(/<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/);
111
+ if (!match) return { name: 'service cwd stabile', ok: false, detail: 'WorkingDirectory assente dal plist' };
112
+ actual = decodeXmlText(match[1]);
113
+ ok = actual === String(home);
114
+ } else if (platform === 'linux') {
115
+ const match = raw.match(/^WorkingDirectory=(.+)$/m);
116
+ if (!match) return { name: 'service cwd stabile', ok: false, detail: 'WorkingDirectory assente dalla unit' };
117
+ actual = match[1].replace(/%%/g, '%');
118
+ ok = actual === String(home);
119
+ } else {
120
+ const stable = /^\s*cd -- "\$HOME"\s*$/m.test(raw);
121
+ actual = stable ? '$HOME' : (/^\s*cd\s+--\s+(.+)$/m.exec(raw)?.[1] || 'cd assente');
122
+ ok = stable;
123
+ }
95
124
  return {
96
- name: 'launchd cwd stabile', ok: actual === expected,
97
- detail: actual === expected ? expected : `${actual} (atteso HOME stabile: ${expected})`,
125
+ name: 'service cwd stabile', ok,
126
+ detail: ok ? (platform === 'termux' ? '$HOME' : String(home)) : `${actual} (atteso HOME stabile)`,
98
127
  };
99
128
  }
100
129
 
130
+ // The Fleet boot companion can win the boot race and create the shared tmux
131
+ // server before the HTTP service. Apply the same stable-HOME invariant to it.
132
+ // A missing companion is non-fatal because Fleet boot is optional; an installed
133
+ // but stale/unsafe definition is a real failure.
134
+ function checkFleetServiceWorkingDirectory(platform, home, installPathOverride) {
135
+ if (!['linux', 'mac', 'termux'].includes(platform)) {
136
+ return { name: 'fleet service cwd stabile', ok: true, detail: `${platform}: non applicabile` };
137
+ }
138
+ const target = installPathOverride || fleetInstallPath(platform, home);
139
+ try {
140
+ fs.lstatSync(target);
141
+ } catch (error) {
142
+ if (error.code === 'ENOENT') {
143
+ return { name: 'fleet service cwd stabile', ok: true, warn: true, detail: `companion non installato (${target})` };
144
+ }
145
+ }
146
+ const result = checkServiceWorkingDirectory(platform, home, target);
147
+ return { ...result, name: 'fleet service cwd stabile' };
148
+ }
149
+
101
150
  function checkBoot(platform, home, execImpl) {
102
151
  if (platform === 'termux') {
103
152
  const p = path.join(home, '.termux', 'boot', 'nexuscrew.sh');
@@ -261,6 +310,61 @@ function checkTermuxExec(runtimeEnv, opts = {}) {
261
310
  };
262
311
  }
263
312
 
313
+ // Read-only probe of the long-lived tmux server cwd. A server that retained an
314
+ // unlinked runtime directory keeps accepting clients but makes later children
315
+ // fail getcwd(3). Never reports the path itself; only stable state.
316
+ function checkTmuxServerCwd(platform, execImpl, opts = {}) {
317
+ if (!['linux', 'termux'].includes(platform)) {
318
+ return { name: 'tmux server cwd', ok: true, detail: `${platform}: non applicabile` };
319
+ }
320
+ let rawPid = '';
321
+ try {
322
+ rawPid = String(execImpl(opts.tmuxBin || 'tmux', ['display-message', '-p', '#{pid}'], { encoding: 'utf8' }) || '').trim();
323
+ } catch (_) {
324
+ return { name: 'tmux server cwd', ok: true, warn: true, detail: 'server tmux non attivo; probe rinviato al primo avvio' };
325
+ }
326
+ if (!/^\d+$/.test(rawPid)) {
327
+ return { name: 'tmux server cwd', ok: true, warn: true, detail: 'server tmux non rilevato' };
328
+ }
329
+ try {
330
+ const resolveImpl = opts.procCwdImpl || ((pid) => fs.realpathSync(`/proc/${pid}/cwd`));
331
+ const cwd = resolveImpl(Number(rawPid));
332
+ if (typeof cwd !== 'string' || !cwd) throw new Error('cwd vuota');
333
+ return { name: 'tmux server cwd', ok: true, detail: 'cwd risolvibile' };
334
+ } catch (_) {
335
+ return {
336
+ name: 'tmux server cwd', ok: false,
337
+ detail: 'cwd del server tmux non risolvibile (directory sostituita); termina le sessioni in modo esplicito e riavvia tmux',
338
+ };
339
+ }
340
+ }
341
+
342
+ // Inspect only the single tmux global environment key required by
343
+ // termux-exec. The value is validated and then discarded; it is never logged.
344
+ // This distinguishes a healthy server from RC-2 (stale/no preload) and RC-7
345
+ // (present but rejected by the trust boundary) without killing any session.
346
+ function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
347
+ const env = runtimeEnv || process.env;
348
+ const platform = opts.platform || detectPlatform();
349
+ if (!termuxRuntimePaths(env, { platform, home: opts.home })) {
350
+ return { name: 'tmux server termux-exec', ok: true, detail: `${platform}: non applicabile` };
351
+ }
352
+ let raw = '';
353
+ try {
354
+ raw = String(execImpl(opts.tmuxBin || 'tmux', ['show-environment', '-g', 'LD_PRELOAD'], { encoding: 'utf8' }) || '').trim();
355
+ } catch (_) {
356
+ return { name: 'tmux server termux-exec', ok: true, warn: true, detail: 'server tmux non attivo; probe rinviato al primo avvio' };
357
+ }
358
+ const match = raw.match(/^LD_PRELOAD=(.+)$/);
359
+ const trusted = match && trustedTermuxPreload({ ...env, LD_PRELOAD: match[1] }, { platform, home: opts.home });
360
+ return trusted
361
+ ? { name: 'tmux server termux-exec', ok: true, detail: 'preload trusted presente nel server tmux' }
362
+ : {
363
+ name: 'tmux server termux-exec', ok: false,
364
+ detail: 'LD_PRELOAD assente o non trusted nel server tmux; server stale o formato non compatibile (nessun kill automatico)',
365
+ };
366
+ }
367
+
264
368
  // Check MCP identity (P0). NON-FAILING per costrutto: `ok` è sempre true, così
265
369
  // chi usa NexusCrew solo come PWA (nessuna integrazione MCP) non vede mai il
266
370
  // doctor andare in FAIL solo per env MCP assente. Il check osserva SOLO la
@@ -341,7 +445,10 @@ function doctor(opts = {}) {
341
445
  checkTmux(existsImpl, opts.tmuxBin),
342
446
  checkPty(ptyLoad),
343
447
  checkService(platform, home, execImpl, uidVal, opts.installPath),
344
- checkMacServiceWorkingDirectory(platform, home, opts.installPath),
448
+ checkServiceWorkingDirectory(platform, home, opts.installPath),
449
+ fleetEnabled
450
+ ? checkFleetServiceWorkingDirectory(platform, home, opts.fleetInstallPath)
451
+ : { name: 'fleet service cwd stabile', ok: true, warn: true, detail: 'Fleet disabilitata' },
345
452
  checkBoot(platform, home, execImpl),
346
453
  checkUserLinger(platform, execImpl, uidVal),
347
454
  checkTmuxSurvival(platform, execImpl),
@@ -352,6 +459,8 @@ function doctor(opts = {}) {
352
459
  checkAutossh(existsImpl),
353
460
  checkSshPermitlisten(opts.sshVersion),
354
461
  checkMcpIdentity(opts.env),
462
+ checkTmuxServerCwd(platform, execImpl, { tmuxBin: opts.tmuxBin, procCwdImpl: opts.procCwdImpl }),
463
+ checkTmuxServerTermuxPreload(opts.env, execImpl, { platform, home, tmuxBin: opts.tmuxBin }),
355
464
  ];
356
465
 
357
466
  for (const c of checks) {
@@ -368,7 +477,10 @@ module.exports = {
368
477
  checkNode, checkTmux, checkPty, checkService, checkBoot, checkTokenPerms,
369
478
  checkMacServiceWorkingDirectory,
370
479
  checkFleetDefinitions,
480
+ checkServiceWorkingDirectory,
481
+ checkFleetServiceWorkingDirectory,
371
482
  checkTmuxSurvival, checkUserLinger,
372
483
  checkSshClient, checkAutossh, checkSshPermitlisten,
373
484
  checkMcpIdentity, checkTermuxExec,
485
+ checkTmuxServerCwd, checkTmuxServerTermuxPreload,
374
486
  };
@@ -41,13 +41,16 @@ function generateFleetService(opts) {
41
41
  }
42
42
 
43
43
  function generateFleetLinux(opts) {
44
- const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
44
+ const home = opts.home || os.homedir();
45
45
  const { nodeBin, entryPath } = opts;
46
46
  // Parita' di hardening con service.js (M3): reject char non gestibili in systemd.
47
- assertSystemdSafe('repoRoot', repoRoot);
47
+ assertSystemdSafe('home', home);
48
48
  assertSystemdSafe('nodeBin', nodeBin);
49
49
  assertSystemdSafe('entryPath', entryPath);
50
- const repo = escapeSystemdPath(repoRoot);
50
+ // fleet-boot can create the shared tmux server before the HTTP service. Its
51
+ // cwd must therefore be the stable HOME, never the replaceable npm package
52
+ // directory containing entryPath.
53
+ const stableCwd = escapeSystemdPath(home);
51
54
  const node = escapeSystemdExec(nodeBin);
52
55
  const entry = escapeSystemdExec(entryPath);
53
56
  const nodeDir = escapeSystemdPath(path.dirname(nodeBin));
@@ -61,7 +64,7 @@ Type=oneshot
61
64
  # fleet-boot may be the process that creates the shared tmux server. Keep
62
65
  # systemd from reaping that server and its cells when this oneshot exits.
63
66
  KillMode=process
64
- WorkingDirectory=${repo}
67
+ WorkingDirectory=${stableCwd}
65
68
  Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
66
69
  ExecStart=${node} ${entry} fleet-boot
67
70
 
@@ -71,11 +74,9 @@ WantedBy=default.target
71
74
  }
72
75
 
73
76
  function generateFleetMac(opts) {
74
- const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
75
77
  const home = opts.home || os.homedir();
76
78
  const nodeXml = escapeXml(opts.nodeBin);
77
79
  const entryXml = escapeXml(opts.entryPath);
78
- const repoXml = escapeXml(repoRoot);
79
80
  const homeXml = escapeXml(home);
80
81
  const launchPath = [...new Set([
81
82
  path.dirname(opts.nodeBin), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin',
@@ -94,7 +95,7 @@ function generateFleetMac(opts) {
94
95
  <string>fleet-boot</string>
95
96
  </array>
96
97
  <key>WorkingDirectory</key>
97
- <string>${repoXml}</string>
98
+ <string>${homeXml}</string>
98
99
  <key>EnvironmentVariables</key>
99
100
  <dict>
100
101
  <key>PATH</key>
@@ -112,10 +113,8 @@ function generateFleetMac(opts) {
112
113
  }
113
114
 
114
115
  function generateFleetTermux(opts) {
115
- const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
116
116
  const nodeQ = shellQuote(opts.nodeBin);
117
117
  const entryQ = shellQuote(opts.entryPath);
118
- const repoQ = shellQuote(repoRoot);
119
118
  return `#!/data/data/com.termux/files/usr/bin/sh
120
119
  # NexusCrew fleet boot companion (Termux) - avvia le celle boot:true
121
120
  export PATH=/data/data/com.termux/files/usr/bin:$PATH
@@ -124,7 +123,7 @@ export PREFIX=/data/data/com.termux/files/usr
124
123
  export TMPDIR=$PREFIX/tmp
125
124
  export TMUX_TMPDIR=$PREFIX/var/run
126
125
  mkdir -p "$TMPDIR" "$TMUX_TMPDIR"
127
- cd -- ${repoQ}
126
+ cd -- "$HOME"
128
127
  exec ${nodeQ} ${entryQ} fleet-boot >> "$HOME/.nexuscrew/fleet-boot.log" 2>&1
129
128
  `;
130
129
  }
package/lib/cli/init.js CHANGED
@@ -109,6 +109,9 @@ function runInit(opts = {}) {
109
109
  }
110
110
 
111
111
  const actions = [];
112
+ // Structured, value-free install outcomes for callers that must fail closed
113
+ // (notably smart-up migrations). Human-readable actions remain compatible.
114
+ const installFailures = [];
112
115
 
113
116
  // porta: opts.port > migration rule > config esistente > default 41820
114
117
  let port = opts.port;
@@ -214,12 +217,14 @@ function runInit(opts = {}) {
214
217
  const r = installService(platform, content, svcCtx, { execImpl: opts.execImpl });
215
218
  if (r.failures && r.failures.length) {
216
219
  // file installato MA activation (systemctl/launchctl) fallita [M1]
220
+ installFailures.push({ component: 'service', phase: 'activation' });
217
221
  actions.push(`WARN: service file installato ${r.target} MA activation fallita: ${r.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
218
222
  } else {
219
223
  actions.push(`service installed ${r.target} (mode 0${fileMode(platform).toString(8)})`);
220
224
  }
221
225
  } catch (e) {
222
226
  // failure: preserve file + diagnosi (no rollback) [M8]
227
+ installFailures.push({ component: 'service', phase: 'install' });
223
228
  actions.push(`WARN: service install fallito: ${e.message} (file generati preservati)`);
224
229
  }
225
230
  }
@@ -273,6 +278,7 @@ function runInit(opts = {}) {
273
278
  );
274
279
  if (fr.failures && fr.failures.length) {
275
280
  // file installato MA activation (systemctl/launchctl) fallita [M1]
281
+ installFailures.push({ component: 'fleet-companion', phase: 'activation' });
276
282
  actions.push(`WARN: fleet companion file installato ${fr.target} MA activation fallita: ${fr.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
277
283
  } else {
278
284
  actions.push(`fleet companion installed ${fr.target} (mode 0${fleetFileMode(platform).toString(8)})`);
@@ -282,6 +288,7 @@ function runInit(opts = {}) {
282
288
  }
283
289
  } catch (e) {
284
290
  // qualunque errore del companion: WARN + init principale prosegue intatto.
291
+ installFailures.push({ component: 'fleet-companion', phase: 'install' });
285
292
  actions.push(`WARN: fleet companion fallito: ${e.message} (init principale prosegue)`);
286
293
  }
287
294
 
@@ -303,7 +310,7 @@ function runInit(opts = {}) {
303
310
 
304
311
  for (const a of actions) log(a);
305
312
 
306
- return { platform, port, token, url: urlWithToken, actions, tmuxOk, dryRun };
313
+ return { platform, port, token, url: urlWithToken, actions, installFailures, tmuxOk, dryRun };
307
314
  }
308
315
 
309
316
  module.exports = { runInit, readExistingPort, haveTmux, nodeMajor, writeConfigAtomic, ensureFleetDefaults };
@@ -59,9 +59,12 @@ function generateService(platform, ctx) {
59
59
  function generateLinux(ctx) {
60
60
  assertSystemdSafe('repoRoot', ctx.repoRoot); // [M3] reject char non gestibili
61
61
  assertSystemdSafe('nodeBin', ctx.nodeBin);
62
- const workDir = ctx.runtimeDir || path.join(ctx.home, '.nexuscrew');
63
- assertSystemdSafe('runtimeDir', workDir);
64
- const runtime = escapeSystemdPath(workDir);
62
+ // HOME is stable across init/repair/update. The runtime directory may be
63
+ // atomically replaced; keeping it as the service cwd leaves the HTTP process
64
+ // and a shared tmux server holding an orphaned inode, so every later pane can
65
+ // fail getcwd(3). Runtime state still lives in runtimeDir; only cwd changes.
66
+ assertSystemdSafe('home', ctx.home);
67
+ const runtime = escapeSystemdPath(ctx.home);
65
68
  const node = escapeSystemdExec(ctx.nodeBin);
66
69
  const repoBin = escapeSystemdExec(path.join(ctx.repoRoot, 'bin', 'nexuscrew.js'));
67
70
  const nodeDir = escapeSystemdPath(path.dirname(ctx.nodeBin));
@@ -151,7 +154,7 @@ export LANG=\${LANG:-en_US.UTF-8}
151
154
  export LC_CTYPE=\${LC_CTYPE:-en_US.UTF-8}
152
155
  termux-wake-lock 2>/dev/null || true
153
156
  mkdir -p "$HOME/.nexuscrew" "$TMPDIR" "$TMUX_TMPDIR"
154
- cd -- "$HOME/.nexuscrew"
157
+ cd -- "$HOME"
155
158
  exec ${nodeQ} ${repoBinQ} serve --pidfile >> "$HOME/.nexuscrew/nexuscrew.log" 2>&1
156
159
  `;
157
160
  }
@@ -7,7 +7,7 @@ const ALLOWED_DURATIONS = new Set([300, 900, 1800, 3600]);
7
7
  const LEVELS = new Set(['debug', 'info', 'warn', 'error']);
8
8
  const META_KEYS = new Set([
9
9
  'port', 'platform', 'reason', 'durationSeconds', 'count', 'errno', 'client',
10
- 'cell', 'engine', 'action', 'state', 'transport', 'phase', 'version', 'status', 'node',
10
+ 'cell', 'engine', 'action', 'state', 'transport', 'phase', 'version', 'status', 'node', 'code',
11
11
  ]);
12
12
  const DENIED_KEY = /(authorization|cookie|token|secret|credential|password|prompt|terminal|argv|command|env|content|payload|file|path|endpoint|url)/i;
13
13
 
@@ -18,7 +18,7 @@ function redactText(value, max = 160) {
18
18
  .replace(/\bBearer\s+\S+/gi, 'Bearer [redacted]')
19
19
  .replace(/\b(?:sk|xox[baprs]|gh[pousr]|npm)_[A-Za-z0-9_-]{8,}\b/g, '[redacted]')
20
20
  .replace(/\b(?:token|password|secret|api[_-]?key)\s*[=:]\s*\S+/gi, '[redacted]')
21
- .replace(/(?:\/home\/[^/\s]+|\/data\/(?:data|user\/\d+)\/[^/\s]+)(?:\/[^\s]*)?/g, '[path]')
21
+ .replace(/(?:\/home\/[^/\s]+|\/Users\/[^/\s]+|\/data\/(?:data|user\/\d+)\/[^/\s]+)(?:\/[^\s]*)?/g, '[path]')
22
22
  .replace(/\b[A-Z][A-Z0-9_]{2,}\s*=\s*\S+/g, '[env]');
23
23
  return text.slice(0, max);
24
24
  }