@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.
- package/CHANGELOG.md +41 -0
- package/README.md +24 -9
- package/frontend/dist/assets/{index-xxfypte7.css → index-BAzlX2nP.css} +1 -1
- package/frontend/dist/assets/index-BJdAdNOq.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +96 -3
- package/lib/cli/doctor.js +123 -11
- package/lib/cli/fleet-service.js +9 -10
- package/lib/cli/init.js +8 -1
- package/lib/cli/service.js +7 -4
- package/lib/diagnostics/store.js +2 -2
- package/lib/fleet/builtin.js +206 -17
- package/lib/fleet/causes.js +68 -0
- package/lib/fleet/definitions.js +86 -4
- package/lib/fleet/launch.js +25 -1
- package/lib/fleet/managed.js +62 -9
- package/lib/fleet/routes.js +16 -2
- package/lib/fleet/runtime.js +69 -33
- package/package.json +1 -1
- package/frontend/dist/assets/index-CWKyv6KS.js +0 -93
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-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.
|
|
1
|
+
{"version":"0.8.29"}
|
package/lib/cli/commands.js
CHANGED
|
@@ -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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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 {
|
|
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: '
|
|
88
|
-
detail: error.code === 'ENOENT' ? `
|
|
103
|
+
name: 'service cwd stabile', ok: false,
|
|
104
|
+
detail: error.code === 'ENOENT' ? `service non installato (${target})` : error.message,
|
|
89
105
|
};
|
|
90
106
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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: '
|
|
97
|
-
detail:
|
|
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
|
-
|
|
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
|
};
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -41,13 +41,16 @@ function generateFleetService(opts) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function generateFleetLinux(opts) {
|
|
44
|
-
const
|
|
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('
|
|
47
|
+
assertSystemdSafe('home', home);
|
|
48
48
|
assertSystemdSafe('nodeBin', nodeBin);
|
|
49
49
|
assertSystemdSafe('entryPath', entryPath);
|
|
50
|
-
|
|
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=${
|
|
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>${
|
|
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 -- $
|
|
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 };
|
package/lib/cli/service.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
157
|
+
cd -- "$HOME"
|
|
155
158
|
exec ${nodeQ} ${repoBinQ} serve --pidfile >> "$HOME/.nexuscrew/nexuscrew.log" 2>&1
|
|
156
159
|
`;
|
|
157
160
|
}
|
package/lib/diagnostics/store.js
CHANGED
|
@@ -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
|
}
|