@mmmbuto/nexuscrew 0.8.52 → 0.8.53
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 +97 -0
- package/frontend/dist/assets/{index-Cxwpa2Y_.js → index-BJ-_5bxw.js} +29 -29
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +256 -3
- package/lib/mcp/server.js +35 -1
- package/lib/proxy/federation.js +29 -2
- package/lib/tmux/actions.js +33 -1
- package/package.json +1 -1
package/frontend/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
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-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BJ-_5bxw.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-CYi_lhCg.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.53"}
|
package/lib/cli/commands.js
CHANGED
|
@@ -7,6 +7,7 @@ const { execFileSync, spawn } = require('node:child_process');
|
|
|
7
7
|
const fs = require('node:fs');
|
|
8
8
|
const path = require('node:path');
|
|
9
9
|
const net = require('node:net');
|
|
10
|
+
const crypto = require('node:crypto');
|
|
10
11
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
|
|
11
12
|
const { installPath: serviceInstallPath, ensureLinuxTmuxSurvival } = require('./service.js');
|
|
12
13
|
const { fleetInstallPath } = require('./fleet-service.js');
|
|
@@ -37,7 +38,8 @@ Usage:
|
|
|
37
38
|
nexuscrew boot enable startup at boot (use: boot off|status)
|
|
38
39
|
nexuscrew status show service, port, roles and node status
|
|
39
40
|
nexuscrew stop stop the background service
|
|
40
|
-
nexuscrew restart restart the background service
|
|
41
|
+
nexuscrew restart restart the background service (verifies it came back)
|
|
42
|
+
nexuscrew autoupdate turn automatic updates on or off (use: autoupdate on|off|status)
|
|
41
43
|
nexuscrew doctor run local diagnostics
|
|
42
44
|
nexuscrew nodes list and manage connected peers (use: nodes help)
|
|
43
45
|
nexuscrew help show this help
|
|
@@ -544,6 +546,25 @@ async function probeNexusCrew(port, token, opts = {}) {
|
|
|
544
546
|
} catch (_) { return false; }
|
|
545
547
|
}
|
|
546
548
|
|
|
549
|
+
// probeNexusCrew restituisce un BOOLEANO e collassa ogni non-200 su `false`:
|
|
550
|
+
// per i suoi molti chiamanti va bene, e cambiarne il contratto ripercuoterebbe
|
|
551
|
+
// ovunque. Ma «non risponde» e «risponde 401» sono due cose opposte — un 401
|
|
552
|
+
// dice che il servizio E' IN PIEDI e che il token locale non vale — e chi deve
|
|
553
|
+
// spiegare un riavvio ha bisogno di distinguerle, altrimenti dichiara fallito
|
|
554
|
+
// un riavvio riuscito e manda a cercare un processo morto che e' vivo.
|
|
555
|
+
// Rilievo dell'audit: il caso token-assente era gia' coperto, questo no.
|
|
556
|
+
async function probeNexusCrewStatus(port, token, opts = {}) {
|
|
557
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
558
|
+
if (typeof fetchImpl !== 'function') return null;
|
|
559
|
+
try {
|
|
560
|
+
const r = await fetchImpl(`http://127.0.0.1:${port}/api/config`, {
|
|
561
|
+
headers: { authorization: `Bearer ${token || ''}` },
|
|
562
|
+
signal: AbortSignal.timeout?.(700),
|
|
563
|
+
});
|
|
564
|
+
return r.status;
|
|
565
|
+
} catch (_) { return null; } // nessuna risposta: e' un altro guasto
|
|
566
|
+
}
|
|
567
|
+
|
|
547
568
|
async function waitForNexusCrew(port, token, opts = {}) {
|
|
548
569
|
const probe = opts.probeImpl || probeNexusCrew;
|
|
549
570
|
const attempts = opts.waitAttempts === undefined ? 30 : opts.waitAttempts;
|
|
@@ -811,6 +832,113 @@ function logs(opts = {}) {
|
|
|
811
832
|
return { platform, bin, args, follow, keepAlive: true };
|
|
812
833
|
}
|
|
813
834
|
|
|
835
|
+
// autoupdate on|off|status — l'aggiornamento automatico si spegne anche da qui.
|
|
836
|
+
//
|
|
837
|
+
// PERCHE' NEL CLI, visto che la casella esiste gia' in Settings. Perche' il
|
|
838
|
+
// momento in cui serve spegnerlo e' quello in cui la PWA non si apre: il nodo
|
|
839
|
+
// si e' aggiornato, il servizio non e' tornato su, e la riga di comando e'
|
|
840
|
+
// l'unica superficie rimasta. Un controllo che vive solo dove il guasto lo
|
|
841
|
+
// rende irraggiungibile e' mezzo assente.
|
|
842
|
+
//
|
|
843
|
+
// A SERVIZIO ACCESO PASSA DALL'API, e non e' un dettaglio: scrivere il file
|
|
844
|
+
// mentre il processo vive lascerebbe il manager in memoria con il vecchio
|
|
845
|
+
// valore, e continuerebbe ad aggiornare all'ora prevista. Un interruttore che
|
|
846
|
+
// risulta spento e non spegne e' peggio di un interruttore che manca. La route
|
|
847
|
+
// invece persiste E chiama `setEnabled`, quindi vale subito.
|
|
848
|
+
function autoUpdateCommand(args, opts = {}) {
|
|
849
|
+
const log = opts.log || console.log;
|
|
850
|
+
const azione = String(args[0] || 'status').toLowerCase();
|
|
851
|
+
if (!['on', 'off', 'status'].includes(azione)) {
|
|
852
|
+
log('uso: nexuscrew autoupdate on|off|status');
|
|
853
|
+
return { code: 1 };
|
|
854
|
+
}
|
|
855
|
+
const { configPath, tokenPath } = urlmod.resolvePaths(opts);
|
|
856
|
+
const port = urlmod.loadPort(opts);
|
|
857
|
+
const token = urlmod.readToken(tokenPath);
|
|
858
|
+
const attivo = (opts.isServiceRunningImpl || isServiceRunning)({ ...opts });
|
|
859
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
860
|
+
|
|
861
|
+
const daFile = () => {
|
|
862
|
+
try {
|
|
863
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
864
|
+
return cfg.autoUpdate !== false;
|
|
865
|
+
} catch (_) { return true; } // default ON, come la configurazione
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
if (azione === 'status') {
|
|
869
|
+
// A servizio acceso vince cio' che il processo sta DAVVERO facendo, non
|
|
870
|
+
// cio' che il file dice: se i due divergono, e' il primo a spegnere o
|
|
871
|
+
// accendere gli aggiornamenti.
|
|
872
|
+
if (!attivo) {
|
|
873
|
+
log(`autoupdate: ${daFile() ? 'on' : 'off'} (da configurazione; servizio non attivo)`);
|
|
874
|
+
return { code: 0 };
|
|
875
|
+
}
|
|
876
|
+
return fetchImpl(`http://127.0.0.1:${port}/api/settings`, {
|
|
877
|
+
headers: { authorization: `Bearer ${token}` },
|
|
878
|
+
}).then(async (r) => {
|
|
879
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
880
|
+
const j = await r.json();
|
|
881
|
+
const acceso = j.autoUpdate !== false;
|
|
882
|
+
log(`autoupdate: ${acceso ? 'on' : 'off'}`);
|
|
883
|
+
if (j.update && j.update.latest) log(` ultima versione vista su npm latest: ${j.update.latest}`);
|
|
884
|
+
return { code: 0 };
|
|
885
|
+
}).catch((e) => {
|
|
886
|
+
log(`autoupdate: ${daFile() ? 'on' : 'off'} (da configurazione; servizio non interrogabile: ${e.message})`);
|
|
887
|
+
return { code: 0 };
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const acceso = azione === 'on';
|
|
892
|
+
if (!attivo) {
|
|
893
|
+
// Servizio spento: si scrive il file, e si DICE che vale al prossimo avvio.
|
|
894
|
+
// Tacerlo lascerebbe credere che abbia gia' effetto.
|
|
895
|
+
let cfg = {};
|
|
896
|
+
try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (_) { /* file nuovo */ }
|
|
897
|
+
// Scrittura atomica e rifiuto dei symlink, come fa la route che scrive lo
|
|
898
|
+
// stesso file: tmp nella stessa directory, 0600, rename. Un `writeFileSync`
|
|
899
|
+
// diretto puo' lasciare un config.json troncato se il processo muore a
|
|
900
|
+
// meta' — e un config.json illeggibile e' un nodo che non riparte, cioe'
|
|
901
|
+
// proprio il guasto che questo comando serve a evitare. Rilievo dell'audit.
|
|
902
|
+
try {
|
|
903
|
+
if (fs.lstatSync(configPath).isSymbolicLink()) {
|
|
904
|
+
log('autoupdate: config.json e\' un symlink, non lo scrivo');
|
|
905
|
+
return { code: 1 };
|
|
906
|
+
}
|
|
907
|
+
} catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
908
|
+
const dir = path.dirname(configPath);
|
|
909
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
910
|
+
const tmp = path.join(dir, `.${path.basename(configPath)}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
911
|
+
try {
|
|
912
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ ...cfg, autoUpdate: acceso }, null, 2)}\n`, { mode: 0o600 });
|
|
913
|
+
fs.chmodSync(tmp, 0o600);
|
|
914
|
+
fs.renameSync(tmp, configPath);
|
|
915
|
+
} catch (e) {
|
|
916
|
+
try { fs.unlinkSync(tmp); } catch (_) { /* best-effort */ }
|
|
917
|
+
throw e;
|
|
918
|
+
}
|
|
919
|
+
log(`autoupdate: ${azione} (scritto in configurazione; vale al prossimo avvio del servizio)`);
|
|
920
|
+
return { code: 0 };
|
|
921
|
+
}
|
|
922
|
+
return fetchImpl(`http://127.0.0.1:${port}/api/settings/config`, {
|
|
923
|
+
method: 'POST',
|
|
924
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
925
|
+
body: JSON.stringify({ autoUpdate: acceso }),
|
|
926
|
+
}).then(async (r) => {
|
|
927
|
+
if (!r.ok) {
|
|
928
|
+
const j = await r.json().catch(() => ({}));
|
|
929
|
+
log(`autoupdate: non applicato — ${j.error || `HTTP ${r.status}`}`);
|
|
930
|
+
return { code: 1 };
|
|
931
|
+
}
|
|
932
|
+
log(`autoupdate: ${azione} (applicato subito e salvato)`);
|
|
933
|
+
return { code: 0 };
|
|
934
|
+
}).catch((e) => {
|
|
935
|
+
log(`autoupdate: non applicato — ${e.message}`);
|
|
936
|
+
log(' il servizio risulta attivo ma non risponde: non scrivo il file, perche\' resterebbe');
|
|
937
|
+
log(' un valore che il processo vivo non conosce.');
|
|
938
|
+
return { code: 1 };
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
814
942
|
// update: npm i -g @latest + restart se attivo. Fallimento npm -> messaggio chiaro, code 1.
|
|
815
943
|
function update(opts = {}) {
|
|
816
944
|
const execImpl = opts.execImpl || execFileSync;
|
|
@@ -1158,9 +1286,134 @@ function dispatch(argv, opts = {}) {
|
|
|
1158
1286
|
const result = stop({ ...opts, log });
|
|
1159
1287
|
return { code: result.stopped || ['not running', 'stale pidfile'].includes(result.reason) ? 0 : 1 };
|
|
1160
1288
|
}
|
|
1289
|
+
if (cmd === 'autoupdate') {
|
|
1290
|
+
return autoUpdateCommand(rest.slice(1), { ...opts, log });
|
|
1291
|
+
}
|
|
1161
1292
|
if (cmd === 'restart') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1293
|
+
// Seam come negli altri punti del file (`opts.restartImpl || restart`):
|
|
1294
|
+
// serve a poter dichiarare in prova QUALE runtime e' in piedi, perche' il
|
|
1295
|
+
// ritentativo si comporta in modo diverso fra gestito e portatile — e senza
|
|
1296
|
+
// il seam un test finisce per esercitare il ramo che non intendeva.
|
|
1297
|
+
const result = (opts.restartImpl || restart)({ ...opts, log });
|
|
1298
|
+
if (!result.restarted) return { code: 1 };
|
|
1299
|
+
// IL RIAVVIO ORA VERIFICA DI ESSERE TORNATO SU, e prima no. `restart`
|
|
1300
|
+
// dichiarava successo appena `systemctl restart` (o launchctl, o l'avvio
|
|
1301
|
+
// portatile) ritornava: quella e' la conferma che il COMANDO e' partito,
|
|
1302
|
+
// non che il servizio risponda. Su Termux, il 2026-08-07, ha restituito 0
|
|
1303
|
+
// con il servizio morto, e il nodo e' rimasto giu' oltre quattro ore senza
|
|
1304
|
+
// che nessuno lo sapesse — da fuori si vedeva solo un KO generico.
|
|
1305
|
+
//
|
|
1306
|
+
// La verifica esisteva gia' e veniva usata dal percorso di auto-update e
|
|
1307
|
+
// dal bootstrap Fleet («serve un restart verificato»); mancava proprio sul
|
|
1308
|
+
// comando che una persona digita a mano. Con l'auto-update acceso questo
|
|
1309
|
+
// riavvio avviene DA SOLO su ogni nodo, quindi un esito non verificato si
|
|
1310
|
+
// moltiplica per la flotta.
|
|
1311
|
+
//
|
|
1312
|
+
// Il ramo restituisce una PROMESSA e `dispatch` resta sincrona: e' il
|
|
1313
|
+
// disegno che il file ha gia' (dispatchNodes fa lo stesso) e il chiamante
|
|
1314
|
+
// avvolge in `Promise.resolve`. Cambiare la firma di dispatch avrebbe
|
|
1315
|
+
// rotto ottanta test che la chiamano in modo sincrono, per una verifica
|
|
1316
|
+
// che riguarda un comando solo.
|
|
1317
|
+
const { tokenPath } = urlmod.resolvePaths(opts);
|
|
1318
|
+
const port = urlmod.loadPort(opts);
|
|
1319
|
+
const token = urlmod.readToken(tokenPath);
|
|
1320
|
+
// SENZA TOKEN NON SI PUO' DIRE NIENTE SULLA SALUTE, e soprattutto non si
|
|
1321
|
+
// deve dire qualcosa di sbagliato: la sonda fallirebbe per autenticazione e
|
|
1322
|
+
// il messaggio incolperebbe la porta o il processo, mandando a cercare dove
|
|
1323
|
+
// il problema non e'. Rilievo dell'audit. Il riavvio e' comunque partito,
|
|
1324
|
+
// quindi non e' un fallimento: e' una verifica che non si e' potuta fare, e
|
|
1325
|
+
// si dice cosi'.
|
|
1326
|
+
if (!token) {
|
|
1327
|
+
log('restart: comando eseguito, ma la salute NON e\' verificabile senza token locale.');
|
|
1328
|
+
log(' Controlla a mano che il servizio risponda.');
|
|
1329
|
+
return { code: 0 };
|
|
1330
|
+
}
|
|
1331
|
+
const attesa = (sano) => {
|
|
1332
|
+
if (sano) {
|
|
1333
|
+
log(`restart: servizio verificato su 127.0.0.1:${port}`);
|
|
1334
|
+
return { code: 0 };
|
|
1335
|
+
}
|
|
1336
|
+
// NON E' ANCORA UN FALLIMENTO: si prova a capire PERCHE' e, se e' la
|
|
1337
|
+
// causa nota, si rimedia una volta sola.
|
|
1338
|
+
//
|
|
1339
|
+
// LA CAUSA NOTA. Sul percorso portatile — quello di Termux, dove non c'e'
|
|
1340
|
+
// un gestore di servizi che rialzi il processo — `restart` avvia il
|
|
1341
|
+
// nuovo processo SUBITO dopo aver fermato il vecchio, senza aspettare che
|
|
1342
|
+
// muoia ne' che la porta si liberi. Il nuovo non riesce ad ascoltare,
|
|
1343
|
+
// esce, e nessuno se ne accorge: servizio giu', tunnel su (e' un
|
|
1344
|
+
// processo SSH separato), nessun auto-recupero. Misurato il 2026-08-07:
|
|
1345
|
+
// oltre venti minuti in quello stato.
|
|
1346
|
+
//
|
|
1347
|
+
// Il percorso dell'AUTO-UPDATE fa gia' la cosa giusta — aspetta che il
|
|
1348
|
+
// processo sia morto E che la porta sia libera, fino a sei secondi, e
|
|
1349
|
+
// solo allora avvia. Le due strade erano divergenti, e quella che una
|
|
1350
|
+
// persona digita a mano era la meno prudente.
|
|
1351
|
+
//
|
|
1352
|
+
// PRIMA DI DARE LA COLPA A QUALCUNO, si guarda se il servizio risponde
|
|
1353
|
+
// affatto. Un 401 significa che E' IN PIEDI e che il token locale non
|
|
1354
|
+
// vale: il riavvio e' riuscito, e dichiararlo fallito manderebbe a
|
|
1355
|
+
// cercare un processo morto che invece e' vivo.
|
|
1356
|
+
return (opts.probeStatusImpl || probeNexusCrewStatus)(port, token, opts).then((stato) => {
|
|
1357
|
+
if (stato === 401 || stato === 403) {
|
|
1358
|
+
log(`restart: il servizio RISPONDE su 127.0.0.1:${port}, ma il token locale non e' valido.`);
|
|
1359
|
+
log(' Il riavvio e\' riuscito; e\' la credenziale a non funzionare.');
|
|
1360
|
+
log(' Rigenera il token locale, poi riprova a collegarti.');
|
|
1361
|
+
return { code: 1 };
|
|
1362
|
+
}
|
|
1363
|
+
return continua();
|
|
1364
|
+
});
|
|
1365
|
+
};
|
|
1366
|
+
const continua = () => {
|
|
1367
|
+
// SOLO SUL RUNTIME PORTATILE. E' li' che manca un supervisore — ed e'
|
|
1368
|
+
// esattamente la ragione per cui il difetto esiste: su Termux nessuno
|
|
1369
|
+
// rialza il processo. Dove il servizio e' gestito (systemd, launchd) il
|
|
1370
|
+
// supervisore c'e' ed e' suo il compito: avviare noi un processo
|
|
1371
|
+
// portatile accanto significherebbe metterne in piedi uno che il gestore
|
|
1372
|
+
// non conosce, mentre il gestore puo' rialzare la propria unita' — due
|
|
1373
|
+
// processi sulla stessa porta, e il nostro sopravvivrebbe allo stop del
|
|
1374
|
+
// servizio. Li' si riferisce e basta.
|
|
1375
|
+
//
|
|
1376
|
+
// Difetto mio, trovato rileggendo prima dell'audit: la prima stesura
|
|
1377
|
+
// chiamava `startPortable` in ogni caso.
|
|
1378
|
+
if (result.runtimeOwner === 'managed') {
|
|
1379
|
+
log(`restart: il servizio NON risponde su 127.0.0.1:${port} dopo il riavvio.`);
|
|
1380
|
+
log(' Il runtime e\' gestito dal servizio di sistema: non avvio un processo accanto.');
|
|
1381
|
+
log(' Controlla lo stato e i log dell\'unita\' di sistema.');
|
|
1382
|
+
return { code: 1 };
|
|
1383
|
+
}
|
|
1384
|
+
// UN SOLO RITENTATIVO, e solo a porta libera. Se la porta e' ancora
|
|
1385
|
+
// occupata il problema e' un altro (un processo che non muore) e
|
|
1386
|
+
// riprovare lo nasconderebbe; se e' libera e il servizio non c'e', il
|
|
1387
|
+
// nuovo processo e' uscito e riavviarlo e' esattamente il rimedio.
|
|
1388
|
+
// Ripetere all'infinito trasformerebbe un guasto in un ciclo.
|
|
1389
|
+
return (opts.portAvailableImpl || portAvailable)(port, '127.0.0.1').then((libera) => {
|
|
1390
|
+
if (!libera) {
|
|
1391
|
+
log(`restart: il servizio NON risponde su 127.0.0.1:${port} e la porta e' ancora occupata.`);
|
|
1392
|
+
log(' Qualcosa tiene la porta senza servire: non riavvio a vuoto.');
|
|
1393
|
+
log(' Controlla i log del servizio e i processi rimasti.');
|
|
1394
|
+
return { code: 1 };
|
|
1395
|
+
}
|
|
1396
|
+
log(`restart: nessun servizio su ${port} e porta libera — il processo e' uscito. Riprovo una volta.`);
|
|
1397
|
+
(opts.startPortableImpl || startPortable)({ ...opts, spawnImpl: opts.spawnImpl });
|
|
1398
|
+
return (opts.waitForRuntimeImpl || waitForNexusCrew)(port, token, {
|
|
1399
|
+
...opts, waitAttempts: opts.waitAttempts === undefined ? 60 : opts.waitAttempts,
|
|
1400
|
+
waitDelayMs: opts.waitDelayMs === undefined ? 250 : opts.waitDelayMs,
|
|
1401
|
+
}).then((sanoOra) => {
|
|
1402
|
+
if (sanoOra) {
|
|
1403
|
+
log(`restart: servizio verificato su 127.0.0.1:${port} al secondo tentativo`);
|
|
1404
|
+
return { code: 0 };
|
|
1405
|
+
}
|
|
1406
|
+
log(`restart: il servizio NON risponde su 127.0.0.1:${port} nemmeno dopo un secondo avvio.`);
|
|
1407
|
+
log(' Il comando di riavvio e\' partito, il processo non resta su.');
|
|
1408
|
+
log(' Controlla i log del servizio; su Termux puo\' servire riavviare il dispositivo.');
|
|
1409
|
+
return { code: 1 };
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
};
|
|
1413
|
+
return (opts.waitForRuntimeImpl || waitForNexusCrew)(port, token, {
|
|
1414
|
+
...opts, waitAttempts: opts.waitAttempts === undefined ? 60 : opts.waitAttempts,
|
|
1415
|
+
waitDelayMs: opts.waitDelayMs === undefined ? 250 : opts.waitDelayMs,
|
|
1416
|
+
}).then(attesa);
|
|
1164
1417
|
}
|
|
1165
1418
|
// Internal runtime commands used by service managers and MCP clients. They
|
|
1166
1419
|
// are intentionally omitted from HELP and are not configuration surfaces.
|
package/lib/mcp/server.js
CHANGED
|
@@ -215,10 +215,44 @@ function createMcpServer(opts = {}) {
|
|
|
215
215
|
});
|
|
216
216
|
} catch (e) { throw transportError(baseUrl, e); }
|
|
217
217
|
const j = await r.json().catch(() => ({}));
|
|
218
|
-
if (!r.ok)
|
|
218
|
+
if (!r.ok) {
|
|
219
|
+
const base = j.error ? `API ${r.status}: ${j.error}` : `API ${r.status}`;
|
|
220
|
+
throw new Error(base + await disallineamentoDiVersione());
|
|
221
|
+
}
|
|
219
222
|
return j;
|
|
220
223
|
}
|
|
221
224
|
|
|
225
|
+
// NC-R. Aggiornare NexusCrew NON aggiorna il bridge MCP delle celle gia' in
|
|
226
|
+
// piedi: quel processo e' partito col codice di prima e ci resta fino al
|
|
227
|
+
// riavvio della cella. Il sintomo e' crudele — si installa una correzione, si
|
|
228
|
+
// riprova, e si riceve l'errore VECCHIO — e chi lo subisce conclude che la
|
|
229
|
+
// correzione non funziona. E' successo il 2026-08-07 su rc.26, a me, e ci ho
|
|
230
|
+
// messo un giro intero a capirlo.
|
|
231
|
+
//
|
|
232
|
+
// Il momento in cui serve saperlo e' esattamente quello in cui qualcosa
|
|
233
|
+
// fallisce, quindi la verifica sta SOLO sul ramo d'errore: a regime non costa
|
|
234
|
+
// niente, e non si puo' nemmeno mettere in cache all'avvio — la versione che
|
|
235
|
+
// cambia e' quella dell'hub, e cambia proprio mentre questo processo vive.
|
|
236
|
+
//
|
|
237
|
+
// Non trasforma mai un errore in un altro: se la verifica fallisce, l'errore
|
|
238
|
+
// originale esce come sarebbe uscito comunque.
|
|
239
|
+
async function disallineamentoDiVersione() {
|
|
240
|
+
try {
|
|
241
|
+
const r = await fetchImpl(`${baseUrl}/api/config`, {
|
|
242
|
+
headers: { authorization: `Bearer ${readToken()}` },
|
|
243
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
244
|
+
});
|
|
245
|
+
if (!r.ok) return '';
|
|
246
|
+
const cfg = await r.json();
|
|
247
|
+
if (typeof cfg.version !== 'string' || cfg.version === VERSION) return '';
|
|
248
|
+
return `\n\nNOTA: questo bridge MCP e' la versione ${VERSION}, l'hub e' la ${cfg.version}.`
|
|
249
|
+
+ ' Aggiornare NexusCrew non aggiorna il bridge di una cella gia\' avviata:'
|
|
250
|
+
+ ' riavvia questa cella se ti aspettavi un comportamento diverso.';
|
|
251
|
+
} catch (_) {
|
|
252
|
+
return '';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
222
256
|
const ctx = {
|
|
223
257
|
session,
|
|
224
258
|
identity,
|
package/lib/proxy/federation.js
CHANGED
|
@@ -351,8 +351,35 @@ async function probeHealth({ port, token, expectedInstanceId = null, fetchImpl =
|
|
|
351
351
|
} catch (e) {
|
|
352
352
|
out.transport = 'down';
|
|
353
353
|
out.status = 'down';
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
// DUE GUASTI DIVERSI, e prima avevano lo stesso messaggio. Con un canale
|
|
355
|
+
// inverso SSH il listener sull'hub e' sshd: se il dispositivo non e'
|
|
356
|
+
// connesso non c'e' nessun listener e la connessione viene RIFIUTATA; se
|
|
357
|
+
// invece il tunnel regge ma NexusCrew sul dispositivo e' morto, sshd
|
|
358
|
+
// accetta, inoltra, e la connessione viene AZZERATA dall'altro capo.
|
|
359
|
+
//
|
|
360
|
+
// MISURATO il 2026-08-07 sui tre casi reali contemporaneamente presenti:
|
|
361
|
+
// tunnel su + servizio giu' -> ECONNRESET
|
|
362
|
+
// nessun listener -> ECONNREFUSED
|
|
363
|
+
// tutto su -> HTTP (401)
|
|
364
|
+
//
|
|
365
|
+
// Perche' vale la pena distinguerli: «peer non raggiungibile» ha mandato
|
|
366
|
+
// l'indagine nella federazione e nel pairing per quattro ore, mentre il
|
|
367
|
+
// difetto era un servizio che non era ripartito sul dispositivo. Sono due
|
|
368
|
+
// guasti con due rimedi diversi — uno si risolve sulla rete, l'altro
|
|
369
|
+
// andando sul dispositivo — e un messaggio unico li fa cercare nel posto
|
|
370
|
+
// sbagliato la meta' delle volte.
|
|
371
|
+
const codice = (e && (e.code || (e.cause && e.cause.code))) || '';
|
|
372
|
+
if (e && (e.name === 'AbortError' || codice === 'ETIMEDOUT')) {
|
|
373
|
+
out.detail = `peer non raggiungibile (timeout ${timeoutMs}ms)`;
|
|
374
|
+
} else if (codice === 'ECONNREFUSED') {
|
|
375
|
+
out.detail = `canale inverso non attivo sulla porta ${port}: il dispositivo non e' connesso`;
|
|
376
|
+
out.layer = 'tunnel';
|
|
377
|
+
} else if (codice === 'ECONNRESET' || codice === 'ECONNABORTED' || codice === 'EPIPE') {
|
|
378
|
+
out.detail = `canale inverso attivo sulla porta ${port}, ma NexusCrew non risponde sul dispositivo`;
|
|
379
|
+
out.layer = 'service';
|
|
380
|
+
} else {
|
|
381
|
+
out.detail = 'peer non raggiungibile (tcp refused/down)';
|
|
382
|
+
}
|
|
356
383
|
return out;
|
|
357
384
|
} finally {
|
|
358
385
|
if (timer) clearTimeout(timer);
|
package/lib/tmux/actions.js
CHANGED
|
@@ -157,7 +157,39 @@ async function submitToSession(tmuxBin, session, text, opts = {}) {
|
|
|
157
157
|
// inside the composer. C-e is sent as its own tmux command after the paste,
|
|
158
158
|
// then the pane is revalidated before the separate Enter. Other clients
|
|
159
159
|
// still receive a short separation between paste and submit.
|
|
160
|
-
|
|
160
|
+
//
|
|
161
|
+
// NC-Q: L'ATTESA SCALA COL PAYLOAD, e prima era una costante. Il tempo che
|
|
162
|
+
// un TUI impiega a ingerire un bracketed paste cresce con la lunghezza —
|
|
163
|
+
// Claude Code sopra una certa taglia lo COLLASSA («paste again to expand»)
|
|
164
|
+
// — mentre l'attesa era fissa. Oltre quella soglia l'Enter arriva mentre il
|
|
165
|
+
// client sta ancora digerendo e viene mangiato: il testo resta nel
|
|
166
|
+
// composer, la cella non si muove, e il mittente riceve `submitted`. E'
|
|
167
|
+
// una perdita silenziosa, ed e' la peggiore: chi manda crede di aver
|
|
168
|
+
// consegnato.
|
|
169
|
+
//
|
|
170
|
+
// MISURATO il 2026-08-07, stesso bersaglio a 11 minuti di distanza: ~2900
|
|
171
|
+
// caratteri -> mai elaborato per NOVE ORE; ~60 caratteri -> in lavorazione
|
|
172
|
+
// dopo dodici secondi. Lo stesso era successo con `claude.zai-a` a 150 ms e
|
|
173
|
+
// con `codex-vl` a 400: la costante piu' alta non bastava, perche' il
|
|
174
|
+
// problema non e' quale costante ma che sia una costante.
|
|
175
|
+
//
|
|
176
|
+
// I numeri sono un'EURISTICA, non una misura del tempo di ingestione: 150
|
|
177
|
+
// ms ogni 500 caratteri oltre i primi 500, col vecchio valore come minimo.
|
|
178
|
+
// Il gradino usa `floor` di proposito: sotto i 500 caratteri non cambia
|
|
179
|
+
// NIENTE, perche' quei messaggi erano gia' affidabili e rallentarli sarebbe
|
|
180
|
+
// stato un costo pagato da tutti per un difetto che non li riguarda.
|
|
181
|
+
//
|
|
182
|
+
// Nessun tetto esplicito: il testo e' gia' limitato a MAX_SUBMIT, quindi
|
|
183
|
+
// l'attesa non puo' superare i ~2,5 s per costruzione. Un `Math.min` qui
|
|
184
|
+
// sarebbe un ramo che non si esegue mai — e un ramo irraggiungibile e' un
|
|
185
|
+
// pezzo di codice che promette di proteggere da qualcosa che non puo'
|
|
186
|
+
// accadere.
|
|
187
|
+
//
|
|
188
|
+
// Riduce di molto la finestra; non la chiude. `submitted` continua a
|
|
189
|
+
// significare paste+Enter, non accettazione — e resta vero che dopo un
|
|
190
|
+
// messaggio lungo conviene verificare che la cella si sia mossa.
|
|
191
|
+
const attesaBase = codexComposer ? 400 : 150;
|
|
192
|
+
await delay(attesaBase + Math.floor(text.length / 500) * 150);
|
|
161
193
|
if (!(await paneAlive())) return { submitted: false, reason: 'sessione terminata durante la consegna' };
|
|
162
194
|
if (codexComposer) {
|
|
163
195
|
const flush = await execTmux(execFileImpl, tmuxBin, ['send-keys', '-t', paneId, 'C-e']);
|