@mmmbuto/nexuscrew 0.8.58 → 0.9.0
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 +137 -2
- package/README.md +1 -0
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-zjL6kZ7J.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/adapters.js +50 -6
- package/lib/cli/commands.js +40 -2
- package/lib/cli/doctor.js +95 -12
- package/lib/cli/init.js +25 -3
- package/lib/cli/path.js +43 -10
- package/lib/cli/pidfile.js +23 -2
- package/lib/config.js +15 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +50 -4
- package/lib/fleet/cell-exec.js +87 -9
- package/lib/fleet/cell-lease-server.js +719 -0
- package/lib/fleet/cell-lease.js +112 -0
- package/lib/fleet/definitions.js +101 -7
- package/lib/fleet/launch-broker.js +115 -3
- package/lib/fleet/lease-client.js +191 -0
- package/lib/fleet/lease-routes.js +92 -0
- package/lib/fleet/lease-verifier.js +230 -0
- package/lib/fleet/managed.js +366 -48
- package/lib/fleet/prompt-delivery.js +50 -2
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +53 -6
- package/lib/live-host/bridge.js +369 -0
- package/lib/live-host/routes.js +184 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +9 -2
- package/lib/nodes/store.js +14 -0
- package/lib/nodes/tunnel.js +4 -1
- package/lib/proxy/federation.js +106 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +307 -0
- package/lib/proxy/panel-proxy.js +305 -0
- package/lib/server.js +127 -4
- package/package.json +1 -1
- package/skills/alibaba-token-media/SKILL.md +19 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/fill-forms/SKILL.md +23 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-BEGNtmx2.js +0 -93
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
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-zjL6kZ7J.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0vuhL1YP.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.9.0"}
|
package/lib/audio/adapters.js
CHANGED
|
@@ -31,13 +31,25 @@ const UTTERANCE_TIMEOUT_MS = 30 * 1000;
|
|
|
31
31
|
// errore, la sintesi non e' partita davvero.
|
|
32
32
|
const START_GRACE_MS = 250;
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Stessa forma corretta in lib/cli/path.js: il discriminante e' CHI ha
|
|
35
|
+
// fallito, non "c'e' stata un'eccezione". ENOENT ("il path non c'e' qui")
|
|
36
|
+
// e' legittimo, si continua a cercare; ogni altro errore (EACCES — sul file
|
|
37
|
+
// O sulla directory che lo contiene, ELOOP, ENOTDIR) significa "non sono
|
|
38
|
+
// riuscito a verificarlo", non "non c'e'".
|
|
39
|
+
function probeBin(fsImpl, file) {
|
|
35
40
|
try {
|
|
36
41
|
const st = fsImpl.statSync(file);
|
|
37
|
-
if (!st.isFile()) return
|
|
42
|
+
if (!st.isFile()) return { status: 'absent' };
|
|
38
43
|
fsImpl.accessSync(file, fsDefault.constants.X_OK);
|
|
39
|
-
return
|
|
40
|
-
} catch (
|
|
44
|
+
return { status: 'found', path: file };
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e.code === 'ENOENT') return { status: 'absent' };
|
|
47
|
+
return { status: 'blocked', path: file, code: e.code || e.constructor.name };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isExecutable(fsImpl, file) {
|
|
52
|
+
return probeBin(fsImpl, file).status === 'found';
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
// Ricerca bounded nel PATH: nessuna shell, nessun glob, nessun fallback su cwd.
|
|
@@ -52,6 +64,24 @@ function lookupBin(name, { env = process.env, fsImpl = fsDefault } = {}) {
|
|
|
52
64
|
return null;
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
// Come lookupBin, ma riporta anche le entry del PATH dove la verifica e'
|
|
68
|
+
// fallita per un motivo diverso da "non c'e' qui" — usata da detectAdapter
|
|
69
|
+
// per non scegliere in silenzio un adapter con proprieta' diverse (es.
|
|
70
|
+
// testo in argv invece che stdin, vedi 'spd-say') quando il preferito era
|
|
71
|
+
// presente ma irraggiungibile, non assente.
|
|
72
|
+
function resolveBin(name, { env = process.env, fsImpl = fsDefault } = {}) {
|
|
73
|
+
if (!name || name.includes('/')) return { path: null, blocked: [] };
|
|
74
|
+
const raw = String(env.PATH || '');
|
|
75
|
+
if (!raw) return { path: null, blocked: [] };
|
|
76
|
+
const blocked = [];
|
|
77
|
+
for (const dir of raw.split(path.delimiter).filter(Boolean).slice(0, 64)) {
|
|
78
|
+
const r = probeBin(fsImpl, path.join(dir, name));
|
|
79
|
+
if (r.status === 'found') return { path: r.path, blocked };
|
|
80
|
+
if (r.status === 'blocked') blocked.push(r);
|
|
81
|
+
}
|
|
82
|
+
return { path: null, blocked };
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
// Descrittori di piattaforma. `stdin:true` = il testo non tocca argv.
|
|
56
86
|
// L'ordine dentro ogni piattaforma e' una preferenza dichiarata, non casuale:
|
|
57
87
|
// prima chi accetta stdin.
|
|
@@ -100,10 +130,24 @@ function detectAdapter({ platform = process.platform, env = process.env, fsImpl
|
|
|
100
130
|
// secondo caso e' quello reale su Android, quindi va riconosciuto.
|
|
101
131
|
const termux = typeof env.PREFIX === 'string' && env.PREFIX.includes('com.termux');
|
|
102
132
|
const key = termux ? 'android' : platform;
|
|
133
|
+
// Un candidato precedente "bloccato" (presente ma irraggiungibile: permessi,
|
|
134
|
+
// symlink rotto) non e' un'assenza: se il descrittore scelto e' un fallback
|
|
135
|
+
// con proprieta' diverse (es. spd-say: testo in argv invece che stdin), chi
|
|
136
|
+
// legge il risultato deve poterlo sapere invece di credere a un'assenza
|
|
137
|
+
// genuina. Il caso "nessun descrittore trovato affatto" resta `null`
|
|
138
|
+
// com'era: cambiare quella shape romperebbe createAdapter(null) altrove
|
|
139
|
+
// per un guadagno che oggi nessun chiamante consumerebbe (nessuno dei due
|
|
140
|
+
// call site ha un canale di log in questo punto).
|
|
141
|
+
const precededByBlocked = [];
|
|
103
142
|
for (const d of descriptors) {
|
|
104
143
|
if (!d.platforms.includes(key)) continue;
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
144
|
+
const r = resolveBin(d.bin, { env, fsImpl });
|
|
145
|
+
if (r.path) {
|
|
146
|
+
return precededByBlocked.length
|
|
147
|
+
? { ...d, bin: r.path, platform: key, installed: true, precededByBlocked }
|
|
148
|
+
: { ...d, bin: r.path, platform: key, installed: true };
|
|
149
|
+
}
|
|
150
|
+
if (r.blocked.length) precededByBlocked.push({ id: d.id, blocked: r.blocked });
|
|
107
151
|
}
|
|
108
152
|
return null;
|
|
109
153
|
}
|
package/lib/cli/commands.js
CHANGED
|
@@ -607,12 +607,38 @@ function startPortable(opts = {}) {
|
|
|
607
607
|
function openPwa(fullUrl, opts = {}) {
|
|
608
608
|
if (opts.openImpl) return opts.openImpl(fullUrl);
|
|
609
609
|
const platform = opts.platform || detectPlatform();
|
|
610
|
-
const
|
|
610
|
+
const env = opts.env || process.env;
|
|
611
|
+
const log = opts.log || console.log;
|
|
611
612
|
const candidates = platform === 'termux'
|
|
612
613
|
? ['termux-open-url']
|
|
613
614
|
: platform === 'mac' ? ['open'] : ['xdg-open', 'gio'];
|
|
614
|
-
|
|
615
|
+
|
|
616
|
+
let bin = null;
|
|
617
|
+
const blockedBefore = [];
|
|
618
|
+
if (opts.commandExists) {
|
|
619
|
+
// Seam booleano esplicito (test): nessuna informazione su "bloccato" vs
|
|
620
|
+
// "assente" disponibile — comportamento storico, nessun warning possibile.
|
|
621
|
+
bin = candidates.find((candidate) => opts.commandExists(candidate, env)) || null;
|
|
622
|
+
} else {
|
|
623
|
+
const resolveCmd = opts.resolveCommand || require('./path.js').resolveCommand;
|
|
624
|
+
for (const candidate of candidates) {
|
|
625
|
+
const r = resolveCmd(candidate, env);
|
|
626
|
+
if (r.found) { bin = candidate; break; }
|
|
627
|
+
if (r.blocked && r.blocked.length) blockedBefore.push({ candidate, blocked: r.blocked });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
615
630
|
if (!bin) throw new Error('no URL opener found; install termux-tools or xdg-utils');
|
|
631
|
+
if (blockedBefore.length) {
|
|
632
|
+
// Un candidato preferito non era verificabile (permessi, symlink rotto),
|
|
633
|
+
// non genuinamente assente: `find` sceglierebbe il successivo IN
|
|
634
|
+
// SILENZIO. Continuiamo best-effort (non blocchiamo l'apertura del PWA
|
|
635
|
+
// per questo), ma lo diciamo — altrimenti si esegue un binario diverso
|
|
636
|
+
// da quello inteso senza che nessuno lo sappia.
|
|
637
|
+
const detail = blockedBefore
|
|
638
|
+
.map((b) => `${b.candidate}: ${b.blocked.map((x) => `${x.path} (${x.code})`).join('; ')}`)
|
|
639
|
+
.join(' | ');
|
|
640
|
+
log(`WARN: apertura PWA con "${bin}" — un candidato preferito non era verificabile, non assente (${detail})`);
|
|
641
|
+
}
|
|
616
642
|
const args = bin === 'gio' ? ['open', fullUrl] : [fullUrl];
|
|
617
643
|
const child = (opts.spawnImpl || spawn)(bin, args, { detached: true, stdio: 'ignore' });
|
|
618
644
|
if (child && typeof child.on === 'function') child.on('error', () => {});
|
|
@@ -1129,6 +1155,18 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1129
1155
|
if (!cells.length) { log('nodes cells: elenco vuoto — usa `none` se intendi nessuna cella'); return { code: 1 }; }
|
|
1130
1156
|
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { cellVisibility: 'selected', cells } }).code };
|
|
1131
1157
|
}
|
|
1158
|
+
// Accesso ai pannelli (D8): dietro un pannello c'e' un browser con sessioni
|
|
1159
|
+
// gia' autenticate, e quell'accesso non si revoca cambiando una chiave. Il
|
|
1160
|
+
// resto del modello tratta un peer pairato come l'operatore stesso; qui no:
|
|
1161
|
+
// default negato, si concede un nodo per volta.
|
|
1162
|
+
if (sub === 'panel') {
|
|
1163
|
+
const arg = rest[3];
|
|
1164
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
1165
|
+
log('nodes panel: uso `nodes panel <nodo> on|off`');
|
|
1166
|
+
return { code: 1 };
|
|
1167
|
+
}
|
|
1168
|
+
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { panelAccess: arg === 'on' } }).code };
|
|
1169
|
+
}
|
|
1132
1170
|
if (sub === 'remove') {
|
|
1133
1171
|
if (flags.yes !== true) { log('nodes remove: conferma richiesta con --yes'); return { code: 1 }; }
|
|
1134
1172
|
return { code: nodesCmds.nodesRemove({ ...opts, log, ref }).code };
|
package/lib/cli/doctor.js
CHANGED
|
@@ -13,7 +13,7 @@ const { detectPlatform, uid } = require('./platform.js');
|
|
|
13
13
|
const { installPath } = require('./service.js');
|
|
14
14
|
const { fleetInstallPath } = require('./fleet-service.js');
|
|
15
15
|
const { resolvePaths } = require('./url.js');
|
|
16
|
-
const { commandExists } = require('./path.js');
|
|
16
|
+
const { commandExists, resolveCommand } = require('./path.js');
|
|
17
17
|
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
18
18
|
const { loadConfig } = require('../config.js');
|
|
19
19
|
const {
|
|
@@ -29,10 +29,20 @@ function checkNode() {
|
|
|
29
29
|
return { name: 'node >= 18', ok: maj >= 18, detail: `v${process.versions.node}` };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function checkTmux(existsImpl, tmuxBin) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
function checkTmux(existsImpl, tmuxBin, resolveImpl) {
|
|
33
|
+
if (existsImpl(tmuxBin || 'tmux')) return { name: 'tmux presente', ok: true };
|
|
34
|
+
// existsImpl e' un booleano (spesso un seam di test): non dice se e' una
|
|
35
|
+
// vera assenza o una verifica impossibile. Quando disponibile, resolveImpl
|
|
36
|
+
// arricchisce SOLO il messaggio — mai la decisione ok/fail, che resta
|
|
37
|
+
// quella di existsImpl.
|
|
38
|
+
if (resolveImpl) {
|
|
39
|
+
const r = resolveImpl(tmuxBin || 'tmux');
|
|
40
|
+
if (r && r.blocked && r.blocked.length) {
|
|
41
|
+
const detail = r.blocked.map((b) => `${b.path} (${b.code})`).join('; ');
|
|
42
|
+
return { name: 'tmux presente', ok: false, detail: `non verificabile su PATH, non "non installato" (${detail})` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { name: 'tmux presente', ok: false, detail: 'non trovato su PATH (installa tmux)' };
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function checkPty(ptyLoad) {
|
|
@@ -48,6 +58,7 @@ function checkService(platform, home, execImpl, uidVal, installPathOverride) {
|
|
|
48
58
|
const target = installPathOverride || installPath(platform, home);
|
|
49
59
|
const installed = fs.existsSync(target);
|
|
50
60
|
let active = false;
|
|
61
|
+
let activeUnverifiable = null; // null = stato verificato (attivo o no); string = non ho potuto verificare
|
|
51
62
|
try {
|
|
52
63
|
if (platform === 'linux') {
|
|
53
64
|
const s = execImpl('systemctl', ['--user', 'is-active', 'nexuscrew'], { encoding: 'utf8' });
|
|
@@ -60,12 +71,27 @@ function checkService(platform, home, execImpl, uidVal, installPathOverride) {
|
|
|
60
71
|
const meta = pidf.readPidfile(pidf.defaultPidfilePath(home));
|
|
61
72
|
active = !!(meta && pidf.isAlive(meta));
|
|
62
73
|
}
|
|
63
|
-
} catch (
|
|
74
|
+
} catch (e) {
|
|
75
|
+
active = false;
|
|
76
|
+
// Linux: systemctl assente (ENOENT) o bus/dbus down = non ho potuto VERIFICARE
|
|
77
|
+
// se e' attivo; systemctl che gira e risponde 'inactive' (altri throw) =
|
|
78
|
+
// legittimo "non attivo", VERIFICATO. Verdetto invariato (active resta false,
|
|
79
|
+
// ok/warn non cambiano); il messaggio distingue, come checkTmuxSurvival nello
|
|
80
|
+
// stesso file. Il discriminante e' CHI ha fallito, non che e' ci sia stata
|
|
81
|
+
// un'eccezione. mac/termux non toccati: il collasso nominato e' linux/systemctl.
|
|
82
|
+
if (platform === 'linux' && systemctlUnverifiable(e)) activeUnverifiable = e.code || e.message || e.constructor.name;
|
|
83
|
+
}
|
|
64
84
|
return {
|
|
65
85
|
name: 'service installato/attivo',
|
|
66
86
|
ok: installed,
|
|
67
87
|
warn: installed && !active, // installato ma non attivo = warning, non fail
|
|
68
|
-
detail: installed
|
|
88
|
+
detail: installed
|
|
89
|
+
? (active
|
|
90
|
+
? 'attivo'
|
|
91
|
+
: (activeUnverifiable
|
|
92
|
+
? `installato, stato attivita' non verificabile (systemctl/dbus: ${activeUnverifiable}), non "non attivo"`
|
|
93
|
+
: 'installato ma non attivo'))
|
|
94
|
+
: `non installato (${target})`,
|
|
69
95
|
};
|
|
70
96
|
}
|
|
71
97
|
|
|
@@ -164,8 +190,15 @@ function checkBoot(platform, home, execImpl) {
|
|
|
164
190
|
const s = execImpl('systemctl', ['--user', 'is-enabled', 'nexuscrew'], { encoding: 'utf8' });
|
|
165
191
|
const enabled = String(s).trim() === 'enabled';
|
|
166
192
|
return { name: 'boot (systemd enabled)', ok: true, warn: !enabled, detail: enabled ? 'enabled' : 'non enabled (non parte al boot)' };
|
|
167
|
-
} catch (
|
|
168
|
-
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// systemctl assente (ENOENT) o bus/dbus down = non ho potuto verificare se
|
|
195
|
+
// e' enabled; systemctl che gira e risponde 'disabled'/'masked' (altri
|
|
196
|
+
// throw) = legittimo "non enabled", VERIFICATO. Verdetto invariato
|
|
197
|
+
// (ok:true, warn:true); il messaggio distingue, come checkTmuxSurvival.
|
|
198
|
+
if (systemctlUnverifiable(e)) {
|
|
199
|
+
return { name: 'boot (systemd enabled)', ok: true, warn: true, detail: `non verificabile (systemctl/dbus non raggiungibile: ${e.code || e.message || e.constructor.name}): l'unita' potrebbe essere enabled, non ho potuto guardare` };
|
|
200
|
+
}
|
|
201
|
+
return { name: 'boot (systemd enabled)', ok: true, warn: true, detail: 'non enabled (non parte al boot)' };
|
|
169
202
|
}
|
|
170
203
|
}
|
|
171
204
|
// mac: RunAtLoad nel plist installato
|
|
@@ -192,6 +225,19 @@ function checkUserLinger(platform, execImpl, uidVal) {
|
|
|
192
225
|
}
|
|
193
226
|
}
|
|
194
227
|
|
|
228
|
+
// systemctl assente (ENOENT: il binario non e' sul PATH) o bus/dbus non
|
|
229
|
+
// raggiungibile = non ho potuto VERIFICARE lo stato, distinto da systemctl che
|
|
230
|
+
// ha GIRATO e ha risposto 'inactive'/'disabled' (legittimo "non attivo"/"non
|
|
231
|
+
// enabled"). Stesso principio di checkTmuxSurvival (sotto): il discriminante e'
|
|
232
|
+
// CHI ha fallito, non che ci sia stata un'eccezione. Function declaration: hoisted,
|
|
233
|
+
// quindi disponibile a checkService/checkBoot che la precedono nel sorgente.
|
|
234
|
+
function systemctlUnverifiable(e) {
|
|
235
|
+
if (!e) return false;
|
|
236
|
+
if (e.code === 'ENOENT') return true; // systemctl non installato su PATH
|
|
237
|
+
const msg = String((e && e.message) || e);
|
|
238
|
+
return /Failed to connect to bus|Unable to connect to bus|D-Bus|dbus|Could not connect|Connection refused|Failed to get (D-?bus|the bus)/i.test(msg);
|
|
239
|
+
}
|
|
240
|
+
|
|
195
241
|
function checkTmuxSurvival(platform, execImpl) {
|
|
196
242
|
if (platform !== 'linux') {
|
|
197
243
|
return { name: 'tmux survival on service restart', ok: true, detail: `${platform}: systemd cgroup non applicabile` };
|
|
@@ -289,12 +335,36 @@ function checkTermuxExec(runtimeEnv, opts = {}) {
|
|
|
289
335
|
const libDir = path.join(termux.prefix, 'lib');
|
|
290
336
|
let present = '';
|
|
291
337
|
let candidates = [];
|
|
338
|
+
// Stessa forma gia' corretta altrove: ENOENT ("PREFIX/lib non c'e'") e'
|
|
339
|
+
// legittimo, ma EACCES/ELOOP/ENOTDIR ("non sono riuscito a leggere la
|
|
340
|
+
// directory") non e' un'assenza — e' un fallimento della verifica. Il
|
|
341
|
+
// verdetto resta ok:false in entrambi i casi (in nessuno dei due possiamo
|
|
342
|
+
// CONFERMARE che la libreria trusted esista): solo il messaggio distingue.
|
|
343
|
+
let libDirBlocked = null;
|
|
292
344
|
try {
|
|
293
345
|
candidates = fs.readdirSync(libDir).filter((name) => TERMUX_EXEC_BASENAME_RE.test(name)).sort();
|
|
294
|
-
} catch (
|
|
346
|
+
} catch (e) {
|
|
347
|
+
if (e.code !== 'ENOENT') libDirBlocked = `${e.code || e.constructor.name}: ${e.message}`;
|
|
348
|
+
}
|
|
349
|
+
// Riconsegna: il fix sopra copriva readdirSync sulla DIRECTORY, ma il
|
|
350
|
+
// difetto era tornato un passo piu' avanti, nella stessa funzione —
|
|
351
|
+
// statSync su ogni CANDIDATO ricollassava EACCES/ELOOP in "prossimo",
|
|
352
|
+
// indistinguibile da "questo nome non e' un file valido". Misurato: un
|
|
353
|
+
// candidato che e' un symlink circolare (o rotto in modo diverso da
|
|
354
|
+
// ENOENT) fa fallire statSync con ELOOP — la libreria potrebbe essere
|
|
355
|
+
// davvero li' dietro, solo irraggiungibile in questo modo specifico. Se
|
|
356
|
+
// TUTTI i candidati finiscono bloccati, il loop esce con present='' e,
|
|
357
|
+
// senza questo tracciamento, il ramo finale direbbe "non trovata" —
|
|
358
|
+
// esattamente il messaggio fuorviante gia' chiuso una volta, tornato un
|
|
359
|
+
// passo piu' avanti nella stessa funzione.
|
|
360
|
+
const candidateBlocked = [];
|
|
295
361
|
for (const name of candidates) {
|
|
296
362
|
const candidate = path.join(libDir, name);
|
|
297
|
-
try {
|
|
363
|
+
try {
|
|
364
|
+
if (fs.statSync(candidate).isFile()) { present = candidate; break; }
|
|
365
|
+
} catch (e) {
|
|
366
|
+
if (e.code !== 'ENOENT') candidateBlocked.push(`${name} (${e.code || e.constructor.name})`);
|
|
367
|
+
}
|
|
298
368
|
}
|
|
299
369
|
if (trusted) {
|
|
300
370
|
return { name: 'termux-exec preload', ok: true, detail: `preload trusted: ${path.basename(trusted)}` };
|
|
@@ -305,6 +375,18 @@ function checkTermuxExec(runtimeEnv, opts = {}) {
|
|
|
305
375
|
detail: `libreria presente (${path.basename(present)}) ma LD_PRELOAD non valido nell'env del doctor: avvia il servizio da una shell Termux di login o via termux-exec preload`,
|
|
306
376
|
};
|
|
307
377
|
}
|
|
378
|
+
if (libDirBlocked) {
|
|
379
|
+
return {
|
|
380
|
+
name: 'termux-exec preload', ok: false,
|
|
381
|
+
detail: `non ho potuto verificare PREFIX/lib (${libDirBlocked}), non "assente": sulla build Google Play celle e shell non possono eseguire comandi se manca davvero`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (candidateBlocked.length) {
|
|
385
|
+
return {
|
|
386
|
+
name: 'termux-exec preload', ok: false,
|
|
387
|
+
detail: `non ho potuto verificare ${candidateBlocked.length === 1 ? 'un candidato' : 'alcuni candidati'} sotto PREFIX/lib (${candidateBlocked.join('; ')}), non "assente": sulla build Google Play celle e shell non possono eseguire comandi se manca davvero`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
308
390
|
return {
|
|
309
391
|
name: 'termux-exec preload', ok: false,
|
|
310
392
|
detail: 'libtermux-exec non trovata sotto PREFIX/lib: sulla build Google Play celle e shell non possono eseguire comandi',
|
|
@@ -469,6 +551,7 @@ function doctor(opts = {}) {
|
|
|
469
551
|
const log = opts.log || console.log;
|
|
470
552
|
const ptyLoad = opts.ptyLoad || (() => require('../pty/provider.js').loadPty());
|
|
471
553
|
const existsImpl = opts.commandExists || commandExists;
|
|
554
|
+
const resolveImpl = opts.resolveCommand || resolveCommand;
|
|
472
555
|
const { tokenPath } = resolvePaths(opts);
|
|
473
556
|
const fleetEnabled = opts.fleetEnabled !== false
|
|
474
557
|
&& opts.builtinEnabled !== false
|
|
@@ -476,7 +559,7 @@ function doctor(opts = {}) {
|
|
|
476
559
|
|
|
477
560
|
const checks = [
|
|
478
561
|
checkNode(),
|
|
479
|
-
checkTmux(existsImpl, opts.tmuxBin),
|
|
562
|
+
checkTmux(existsImpl, opts.tmuxBin, resolveImpl),
|
|
480
563
|
checkPty(ptyLoad),
|
|
481
564
|
checkService(platform, home, execImpl, uidVal, opts.installPath),
|
|
482
565
|
checkServiceWorkingDirectory(platform, home, opts.installPath),
|
package/lib/cli/init.js
CHANGED
|
@@ -19,12 +19,28 @@ const {
|
|
|
19
19
|
} = require('./fleet-service.js');
|
|
20
20
|
const { atomicWrite: writeFleet } = require('../fleet/definitions.js');
|
|
21
21
|
const { defaultDefinitions } = require('../fleet/managed.js');
|
|
22
|
-
const { commandExists } = require('./path.js');
|
|
22
|
+
const { commandExists, resolveCommand } = require('./path.js');
|
|
23
23
|
|
|
24
24
|
function haveTmux(tmuxBin, env = process.env) {
|
|
25
25
|
return commandExists(tmuxBin, env);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Messaggio per l'utente quando tmux risulta assente. SOLO quando siamo NOI
|
|
29
|
+
// a fare la verifica (nessun tmuxOk imposto dal chiamante) proviamo ad
|
|
30
|
+
// arricchirlo: se resolveCommand trova entry del PATH dove non e' stato
|
|
31
|
+
// possibile guardare (permessi, symlink rotto, directory non
|
|
32
|
+
// attraversabile), il messaggio deve dire "non verificabile", non "non
|
|
33
|
+
// trovato" — altrimenti manda l'operatore a installare un pacchetto che
|
|
34
|
+
// magari e' gia' li'.
|
|
35
|
+
function tmuxUnavailableMessage(tmuxBin, env) {
|
|
36
|
+
const r = resolveCommand(tmuxBin || 'tmux', env);
|
|
37
|
+
if (r.blocked && r.blocked.length) {
|
|
38
|
+
const detail = r.blocked.map((b) => `${b.path} (${b.code})`).join('; ');
|
|
39
|
+
return `tmux non verificabile su PATH, non "non trovato" (${detail})`;
|
|
40
|
+
}
|
|
41
|
+
return 'tmux non trovato su PATH (installa tmux)';
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
function nodeMajor() {
|
|
29
45
|
return parseInt(String(process.versions.node).split('.')[0], 10);
|
|
30
46
|
}
|
|
@@ -101,7 +117,7 @@ function runInit(opts = {}) {
|
|
|
101
117
|
const dryRun = !!opts.dryRun;
|
|
102
118
|
const installBoot = opts.installBoot !== false;
|
|
103
119
|
const log = opts.log || (() => {});
|
|
104
|
-
const tmuxOk = opts.tmuxOk !== undefined ? opts.tmuxOk : haveTmux(opts.tmuxBin || 'tmux');
|
|
120
|
+
const tmuxOk = opts.tmuxOk !== undefined ? opts.tmuxOk : haveTmux(opts.tmuxBin || 'tmux', opts.env || process.env);
|
|
105
121
|
|
|
106
122
|
// prereq Node (abort before any write) [M8]
|
|
107
123
|
if (nodeMajor() < 18) {
|
|
@@ -211,7 +227,13 @@ function runInit(opts = {}) {
|
|
|
211
227
|
actions.push(`DRY-RUN service (${platform}) generato, NON installato`);
|
|
212
228
|
} else if (!tmuxOk) {
|
|
213
229
|
// tmux mancante: abort before service install (config/token gia' creati) [M8]
|
|
214
|
-
|
|
230
|
+
// Se il chiamante ha imposto tmuxOk direttamente (test/seam), non
|
|
231
|
+
// abbiamo l'informazione per distinguere assenza da verifica bloccata:
|
|
232
|
+
// il messaggio resta quello storico.
|
|
233
|
+
const reason = opts.tmuxOk === undefined
|
|
234
|
+
? tmuxUnavailableMessage(opts.tmuxBin || 'tmux', opts.env || process.env)
|
|
235
|
+
: 'tmux non trovato su PATH (installa tmux)';
|
|
236
|
+
actions.push(`WARN: ${reason} -> service NON installato (ri-runna init)`);
|
|
215
237
|
} else {
|
|
216
238
|
try {
|
|
217
239
|
const r = installService(platform, content, svcCtx, { execImpl: opts.execImpl });
|
package/lib/cli/path.js
CHANGED
|
@@ -4,21 +4,54 @@
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Il discriminante e' CHI ha fallito, non "c'e' stata un'eccezione" (stessa
|
|
8
|
+
// forma gia' corretta in tests/helpers/pi-real-consumer.js, qui nel codice di
|
|
9
|
+
// prodotto): ENOENT su statSync/accessSync significa "il path non c'e' qui",
|
|
10
|
+
// legittimo, si continua a cercare altrove sul PATH. Ogni altro errore
|
|
11
|
+
// (EACCES — permessi sul file O sulla directory che lo contiene, ELOOP —
|
|
12
|
+
// symlink circolare, ENOTDIR, EIO) significa "non sono riuscito a
|
|
13
|
+
// verificarlo", non "non c'e'".
|
|
14
|
+
function probe(p) {
|
|
8
15
|
try {
|
|
9
16
|
const st = fs.statSync(p);
|
|
10
|
-
if (!st.isFile()) return
|
|
17
|
+
if (!st.isFile()) return { status: 'absent' };
|
|
11
18
|
fs.accessSync(p, fs.constants.X_OK);
|
|
12
|
-
return
|
|
13
|
-
} catch (
|
|
19
|
+
return { status: 'found', path: p };
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (e.code === 'ENOENT') return { status: 'absent' };
|
|
22
|
+
return { status: 'blocked', path: p, code: e.code || e.constructor.name, message: e.message };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function executable(p) {
|
|
27
|
+
return probe(p).status === 'found';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Risolve `bin` sul PATH riportando anche le entry dove la verifica e'
|
|
31
|
+
// fallita per un motivo diverso da "non c'e' qui" — cosi' un chiamante che
|
|
32
|
+
// deve spiegare un esito negativo puo' distinguere un'assenza genuina da un
|
|
33
|
+
// controllo impossibile (permessi, symlink rotto, directory non
|
|
34
|
+
// attraversabile), invece di trattarli come lo stesso "non trovato".
|
|
35
|
+
function resolveCommand(bin, env = process.env) {
|
|
36
|
+
if (typeof bin !== 'string' || !bin || bin.includes('\0')) return { found: false, path: null, blocked: [] };
|
|
37
|
+
if (path.isAbsolute(bin) || bin.includes('/') || bin.includes('\\')) {
|
|
38
|
+
const r = probe(bin);
|
|
39
|
+
return r.status === 'found'
|
|
40
|
+
? { found: true, path: r.path, blocked: [] }
|
|
41
|
+
: { found: false, path: null, blocked: r.status === 'blocked' ? [r] : [] };
|
|
42
|
+
}
|
|
43
|
+
const dirs = String((env && env.PATH) || '').split(path.delimiter).filter(Boolean);
|
|
44
|
+
const blocked = [];
|
|
45
|
+
for (const dir of dirs) {
|
|
46
|
+
const r = probe(path.join(dir, bin));
|
|
47
|
+
if (r.status === 'found') return { found: true, path: r.path, blocked };
|
|
48
|
+
if (r.status === 'blocked') blocked.push(r);
|
|
49
|
+
}
|
|
50
|
+
return { found: false, path: null, blocked };
|
|
14
51
|
}
|
|
15
52
|
|
|
16
53
|
function commandExists(bin, env = process.env) {
|
|
17
|
-
|
|
18
|
-
if (path.isAbsolute(bin) || bin.includes('/') || bin.includes('\\')) return executable(bin);
|
|
19
|
-
return String((env && env.PATH) || '').split(path.delimiter)
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.some((dir) => executable(path.join(dir, bin)));
|
|
54
|
+
return resolveCommand(bin, env).found;
|
|
22
55
|
}
|
|
23
56
|
|
|
24
|
-
module.exports = { commandExists };
|
|
57
|
+
module.exports = { commandExists, resolveCommand };
|
package/lib/cli/pidfile.js
CHANGED
|
@@ -55,8 +55,26 @@ function writePidfile(p, pid, cmd, extra = {}) {
|
|
|
55
55
|
fs.writeFileSync(p, meta + '\n', { flag: 'wx', mode: 0o600 });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// La rimozione NON e' un unlink nudo. Il pidfile e' la prova che un processo
|
|
59
|
+
// e' vivo: togliere quello di un VIVO che non siamo noi significa cancellarla
|
|
60
|
+
// — chi governa quel processo (stop, doctor, supervisor) lo crederebbe morto,
|
|
61
|
+
// o peggio adotterebbe uno slot libero che e' occupato. La rimozione e'
|
|
62
|
+
// legittima in tre casi, verificati QUI:
|
|
63
|
+
// 1. il pidfile e' il NOSTRO (meta.pid === process.pid): self-cleanup;
|
|
64
|
+
// 2. e' STALE: il pid e' morto o non e' piu' attribuibile al cmd registrato;
|
|
65
|
+
// 3. non e' leggibile come pidfile: garbage, non il pidfile di nessuno.
|
|
66
|
+
// `allowLive: true` e' la garanzia del CHIAMANTE, non un bypass: killPidfile la
|
|
67
|
+
// usa DOPO un kill verificato del pid esatto del file; il supervisor dei tunnel
|
|
68
|
+
// dopo il match pid+runId del proprio spawn. Quelle vie verificano il soggetto
|
|
69
|
+
// per conto loro prima di dichiararlo. Ritorna false (e non tocca il file) se
|
|
70
|
+
// il pidfile appartiene a un vivo che non siamo noi e non c'e' garanzia.
|
|
71
|
+
function removePidfile(p, { allowLive = false, impl = {} } = {}) {
|
|
72
|
+
const meta = readPidfile(p);
|
|
73
|
+
if (!allowLive && meta && meta.pid !== process.pid && isAlive(meta, impl)) {
|
|
74
|
+
return false; // pidfile di un processo vivo che non siamo noi: resta
|
|
75
|
+
}
|
|
59
76
|
try { fs.unlinkSync(p); } catch (_) {}
|
|
77
|
+
return true;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
// A PID can exist without belonging to this UID. Android commonly reuses PIDs
|
|
@@ -155,7 +173,10 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
|
|
|
155
173
|
}
|
|
156
174
|
try {
|
|
157
175
|
killImpl(meta.pid, signal);
|
|
158
|
-
|
|
176
|
+
// allowLive: il segnale e' partito verso il pid VERIFICATO del file (cmd
|
|
177
|
+
// matchato sopra): la rimozione e' giusta anche se il processo non e' ancora
|
|
178
|
+
// sparito da /proc quando unlink gira.
|
|
179
|
+
removePidfile(p, { allowLive: true });
|
|
159
180
|
return { killed: true, pid: meta.pid };
|
|
160
181
|
} catch (e) {
|
|
161
182
|
return { killed: false, reason: e.message };
|
package/lib/config.js
CHANGED
|
@@ -49,6 +49,16 @@ function baseDefaults() {
|
|
|
49
49
|
// Il manager aggiorna solo verso una semver superiore: mai downgrade.
|
|
50
50
|
autoUpdate: true,
|
|
51
51
|
sessionPresets: {},
|
|
52
|
+
// Ponte Live (fetta 3, rev5 MC0): isolabile — a false il ponte non si
|
|
53
|
+
// connette mai e ogni avvio di Live resta sul comportamento standard.
|
|
54
|
+
liveBridgeEnabled: true,
|
|
55
|
+
// Socket di controllo dell'app-server (misurato 2026-08-15:
|
|
56
|
+
// $CODEX_HOME/app-server-control/app-server-control.sock, WebSocket sopra
|
|
57
|
+
// unix socket). Non cablare mai una porta al posto di questo path.
|
|
58
|
+
liveBridgeSocketPath: path.join(os.homedir(), '.codex', 'app-server-control', 'app-server-control.sock'),
|
|
59
|
+
// MC1.5: limite dichiarato per OGNI fase del ponte (GET designazione e
|
|
60
|
+
// sessione sul socket). Oltre questo la Live parte senza puntamento.
|
|
61
|
+
liveBridgeTimeoutMs: 1500,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -95,6 +105,11 @@ function envOverrides() {
|
|
|
95
105
|
if (process.env.NEXUSCREW_AUTO_UPDATE !== undefined) {
|
|
96
106
|
e.autoUpdate = !['', '0', 'false', 'no', 'off'].includes(String(process.env.NEXUSCREW_AUTO_UPDATE).toLowerCase());
|
|
97
107
|
}
|
|
108
|
+
if (process.env.NEXUSCREW_LIVE_BRIDGE !== undefined) {
|
|
109
|
+
e.liveBridgeEnabled = !['', '0', 'false', 'no', 'off'].includes(String(process.env.NEXUSCREW_LIVE_BRIDGE).toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
if (process.env.NEXUSCREW_LIVE_BRIDGE_SOCKET) e.liveBridgeSocketPath = process.env.NEXUSCREW_LIVE_BRIDGE_SOCKET;
|
|
112
|
+
if (process.env.NEXUSCREW_LIVE_BRIDGE_TIMEOUT_MS) e.liveBridgeTimeoutMs = Number(process.env.NEXUSCREW_LIVE_BRIDGE_TIMEOUT_MS);
|
|
98
113
|
return e;
|
|
99
114
|
}
|
|
100
115
|
|