@mmmbuto/nexuscrew 0.9.29 → 0.9.30
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 +9 -0
- package/docs/LIVE_PROMPT.md +1 -1
- package/docs/NOTIFICATIONS.md +37 -0
- package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
- package/frontend/dist/assets/{index-CywSos2e.css → index-DqCfZFGi.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +91 -10
- package/frontend/dist/version.json +1 -1
- package/lib/audio/groups.js +1 -1
- package/lib/auth/token.js +4 -4
- package/lib/cli/commands.js +59 -12
- package/lib/cli/doctor.js +5 -5
- package/lib/cli/fleet-service.js +7 -7
- package/lib/cli/init.js +27 -27
- package/lib/cli/pidfile.js +1 -1
- package/lib/cli/service.js +6 -6
- package/lib/cli/stable-alias.js +4 -4
- package/lib/cli/url.js +1 -1
- package/lib/config.js +36 -6
- package/lib/files/routes.js +1 -1
- package/lib/fleet/boot.js +2 -2
- package/lib/fleet/builtin.js +15 -15
- package/lib/fleet/causes.js +1 -1
- package/lib/fleet/cell-exec.js +23 -23
- package/lib/fleet/cell-hold.js +6 -6
- package/lib/fleet/cell-lease-server.js +106 -106
- package/lib/fleet/cell-lease.js +21 -21
- package/lib/fleet/definitions.js +10 -10
- package/lib/fleet/identity-authority.js +1 -1
- package/lib/fleet/launch-broker.js +13 -13
- package/lib/fleet/launch.js +15 -15
- package/lib/fleet/lease-client.js +23 -23
- package/lib/fleet/lease-routes.js +3 -3
- package/lib/fleet/lease-verifier.js +22 -22
- package/lib/fleet/managed.js +35 -35
- package/lib/fleet/prompt-delivery.js +13 -13
- package/lib/fleet/routes.js +15 -10
- package/lib/fleet/runtime.js +24 -24
- package/lib/live-host/bridge.js +36 -36
- package/lib/live-host/routes.js +2 -2
- package/lib/live-host/store.js +4 -4
- package/lib/mcp/server.js +14 -14
- package/lib/mcp/tools.js +5 -5
- package/lib/nodes/access-presets.js +258 -0
- package/lib/nodes/commands.js +198 -4
- package/lib/nodes/store.js +152 -9
- package/lib/nodes/tunnel.js +7 -7
- package/lib/notify/ask-answer-service.js +184 -0
- package/lib/notify/ask-receipts.js +197 -0
- package/lib/notify/ask-relay.js +152 -0
- package/lib/notify/asks.js +28 -3
- package/lib/notify/event-feed-acl.js +95 -0
- package/lib/notify/event-feed-asks-routes.js +144 -0
- package/lib/notify/event-feed-client.js +434 -0
- package/lib/notify/event-feed-history.js +115 -0
- package/lib/notify/event-feed-producers.js +175 -0
- package/lib/notify/event-feed-routes.js +254 -0
- package/lib/notify/event-feed.js +328 -0
- package/lib/notify/events.js +1 -1
- package/lib/notify/notifier.js +3 -3
- package/lib/notify/push-relay.js +240 -0
- package/lib/notify/push.js +8 -8
- package/lib/notify/routes.js +62 -23
- package/lib/proxy/federation.js +74 -3
- package/lib/proxy/node-proxy.js +13 -13
- package/lib/proxy/panel-auth.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/proxy/resource-acl.js +145 -0
- package/lib/server.js +372 -39
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +45 -32
- package/lib/update/manager.js +1 -1
- package/lib/update/runner.js +1 -1
- package/lib/voice/transcribe.js +1 -1
- package/lib/ws/bridge.js +3 -3
- package/package.json +1 -1
- package/frontend/dist/assets/index-B_THfiL5.js +0 -167
package/lib/cli/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Minimal public CLI plus internal service/MCP entry points.
|
|
2
|
+
// Minimal public CLI plus internal service/MCP entry points.
|
|
3
3
|
// serve = foreground HTTP (+ --pidfile lifecycle su Termux/manuale).
|
|
4
4
|
// start/stop/status = per-platform: linux (systemctl --user), mac (launchctl),
|
|
5
5
|
// termux (nohup serve --pidfile + pidfile verificato; status boot-script vs running).
|
|
@@ -69,6 +69,10 @@ Usage:
|
|
|
69
69
|
nexuscrew nodes share <name|nodeId> on|off [--json]
|
|
70
70
|
nexuscrew nodes panel <name|nodeId> on|off
|
|
71
71
|
nexuscrew nodes live-host <name|nodeId> on|off
|
|
72
|
+
nexuscrew nodes edit <name|nodeId> --access-role admin|user|nexushost
|
|
73
|
+
nexuscrew nodes edit <name|nodeId> --events-access on|off [...grant flags]
|
|
74
|
+
nexuscrew nodes edit <name|nodeId> --events-receive on|off
|
|
75
|
+
nexuscrew nodes access [--json]
|
|
72
76
|
nexuscrew nodes invite --ssh TARGET [--ssh-port PORT] [--name SLUG]
|
|
73
77
|
[--label TEXT] [--json]
|
|
74
78
|
printf '%s' "$PAIRING_URL" | nexuscrew nodes pair|join
|
|
@@ -87,7 +91,7 @@ Lifecycle semantics:
|
|
|
87
91
|
|
|
88
92
|
const CLI_VALUE_FLAGS = new Set([
|
|
89
93
|
'label', 'ssh', 'ssh-port', 'autostart', 'visibility', 'selected',
|
|
90
|
-
'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir',
|
|
94
|
+
'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir', 'events-receive',
|
|
91
95
|
]);
|
|
92
96
|
|
|
93
97
|
// valueFlags (Set|array opzionale): flag che consumano il token successivo nella
|
|
@@ -221,7 +225,7 @@ function quickSummary(result, opts = {}) {
|
|
|
221
225
|
log('check nexuscrew doctor');
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
// Ruoli client/node dal config.json (default entrambi off —
|
|
228
|
+
// Ruoli client/node dal config.json (default entrambi off — li popola dal wizard UI).
|
|
225
229
|
function readRoles(configPath) {
|
|
226
230
|
try {
|
|
227
231
|
const c = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
@@ -791,8 +795,8 @@ function url(opts = {}) {
|
|
|
791
795
|
return { url: full, port, hasToken: !!token };
|
|
792
796
|
}
|
|
793
797
|
|
|
794
|
-
//
|
|
795
|
-
//
|
|
798
|
+
// Token rotation: atomic write of the new token plus real invalidation of the
|
|
799
|
+
// active sessions (service restart when running). The new token is never printed.
|
|
796
800
|
function tokenRotate(opts = {}) {
|
|
797
801
|
const log = opts.log || console.log;
|
|
798
802
|
const platform = opts.platform || detectPlatform();
|
|
@@ -1017,7 +1021,7 @@ function update(opts = {}) {
|
|
|
1017
1021
|
return { updated: true, running, restarted: running, bootDefinitions: healed, code: 0 };
|
|
1018
1022
|
}
|
|
1019
1023
|
|
|
1020
|
-
//
|
|
1024
|
+
// Fleet boot companion: starts the boot:true cells of the selected provider.
|
|
1021
1025
|
// dispatch e' sync (bin/nexuscrew.js non fa await), quindi runFleetBoot (async)
|
|
1022
1026
|
// viene lanciato e il processo tenuto vivo (keepAlive) finche' non termina, poi
|
|
1023
1027
|
// esce col code risultante. runFleetBoot e' la unit testabile (no process.exit);
|
|
@@ -1039,7 +1043,7 @@ async function runFleetBoot(opts = {}) {
|
|
|
1039
1043
|
}
|
|
1040
1044
|
|
|
1041
1045
|
// builtin: up() per ogni cella boot:true. READONLY fa fallire le up con 403 ->
|
|
1042
|
-
//
|
|
1046
|
+
// collected in failed[] -> exit 1 (no special short-circuit by design).
|
|
1043
1047
|
const res = await bootCells(sel.fleet, { log });
|
|
1044
1048
|
log(`fleet-boot: ${res.started.length} started, ${res.skipped.length} skipped, ${res.failed.length} failed`);
|
|
1045
1049
|
for (const f of res.failed) log(` failed: ${f.cell} — ${f.reason}`);
|
|
@@ -1105,12 +1109,16 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1105
1109
|
if (sub === 'list') return { code: nodesCmds.nodesList({
|
|
1106
1110
|
...opts, log, json: flags.json === true, direct: flags.direct === true,
|
|
1107
1111
|
}).code };
|
|
1108
|
-
if (sub === 'inspect' || sub === 'show')
|
|
1112
|
+
if (sub === 'inspect' || sub === 'show') {
|
|
1113
|
+
const feedDiagnostics = await nodesCmds.fetchFeedDiagnostics(opts);
|
|
1114
|
+
return { code: (await nodesCmds.nodesInspect({ ...opts, log, ref, json: flags.json === true, feedDiagnostics })).code };
|
|
1115
|
+
}
|
|
1109
1116
|
if (sub === 'doctor') {
|
|
1110
1117
|
const view = nodesCmds.loadPeerInventory(opts);
|
|
1111
1118
|
let peers = view.nodes;
|
|
1119
|
+
const feedDiagnostics = await nodesCmds.fetchFeedDiagnostics(opts);
|
|
1112
1120
|
if (ref) {
|
|
1113
|
-
const inspected = nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
|
|
1121
|
+
const inspected = await nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
|
|
1114
1122
|
if (inspected.code !== 0) { log(`nodes doctor: nodo sconosciuto "${ref}"`); return { code: 1 }; }
|
|
1115
1123
|
peers = [inspected.peer];
|
|
1116
1124
|
}
|
|
@@ -1130,7 +1138,15 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1130
1138
|
}
|
|
1131
1139
|
if (flags.json === true) log(JSON.stringify({ nodeId: view.nodeId, checks }, null, 2));
|
|
1132
1140
|
else if (checks.length === 0) log('nodes doctor: nessun peer diretto');
|
|
1133
|
-
else for (const check of checks)
|
|
1141
|
+
else for (const check of checks) {
|
|
1142
|
+
log(`${check.name}: ${check.ok ? 'OK' : 'KO'} — ${check.result}${check.detail ? ` · ${check.detail}` : ''}`);
|
|
1143
|
+
// Feed section per direct peer: kill-switch, cursor, last failure,
|
|
1144
|
+
// counters for observability. Read-only, no secrets.
|
|
1145
|
+
const peer = peers.find((p) => (p.nodeId || null) === (check.nodeId || null) && p.name === check.name);
|
|
1146
|
+
if (peer && peer.kind === 'direct') {
|
|
1147
|
+
for (const line of nodesCmds.feedLinesFor(feedDiagnostics, peer.nodeId)) log(` ${line}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1134
1150
|
return { code: checks.some((check) => !check.ok) ? 1 : 0 };
|
|
1135
1151
|
}
|
|
1136
1152
|
if (sub === 'identity') {
|
|
@@ -1151,7 +1167,32 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1151
1167
|
}
|
|
1152
1168
|
if (flags.visibility !== undefined) patch.visibility = flags.visibility;
|
|
1153
1169
|
if (flags.selected !== undefined) patch.selected = String(flags.selected).split(',').map((x) => x.trim()).filter(Boolean);
|
|
1154
|
-
|
|
1170
|
+
// Federated access: a preset sets the whole vector, the granular flags
|
|
1171
|
+
// refine one that was already set explicitly.
|
|
1172
|
+
if (flags['access-role'] !== undefined) patch.accessRole = String(flags['access-role']);
|
|
1173
|
+
for (const [flag, key] of [
|
|
1174
|
+
['events-access', 'eventsAccess'],
|
|
1175
|
+
['node-events-access', 'nodeEventsAccess'],
|
|
1176
|
+
['ask-reply-access', 'askReplyAccess'],
|
|
1177
|
+
['files-read-access', 'filesReadAccess'],
|
|
1178
|
+
['peer-operator-access', 'peerOperatorAccess'],
|
|
1179
|
+
]) {
|
|
1180
|
+
if (flags[flag] === undefined) continue;
|
|
1181
|
+
try { patch[key] = boolFlag(flags[flag], `--${flag}`); }
|
|
1182
|
+
catch (e) { log(e.message); return { code: 1 }; }
|
|
1183
|
+
}
|
|
1184
|
+
// The client's OWN choice to import a peer's feed (never an owner grant):
|
|
1185
|
+
// on|off, same shape as the other boolean switches.
|
|
1186
|
+
if (flags['events-receive'] !== undefined) {
|
|
1187
|
+
try { patch.eventsReceive = boolFlag(flags['events-receive'], '--events-receive'); }
|
|
1188
|
+
catch (e) { log(e.message); return { code: 1 }; }
|
|
1189
|
+
}
|
|
1190
|
+
const expectedRevision = flags['access-revision'] === undefined ? undefined : Number(flags['access-revision']);
|
|
1191
|
+
if (expectedRevision !== undefined && !Number.isInteger(expectedRevision)) {
|
|
1192
|
+
log('nodes edit: --access-revision vuole un intero (la revision letta da `nodes access`)');
|
|
1193
|
+
return { code: 1 };
|
|
1194
|
+
}
|
|
1195
|
+
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch, expectedRevision }).code };
|
|
1155
1196
|
}
|
|
1156
1197
|
if (sub === 'rename') {
|
|
1157
1198
|
if (flags.label === undefined) { log('nodes rename: --label TEXT obbligatorio'); return { code: 1 }; }
|
|
@@ -1202,6 +1243,12 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1202
1243
|
}
|
|
1203
1244
|
return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { liveHostAccess: arg === 'on' } }).code };
|
|
1204
1245
|
}
|
|
1246
|
+
// Inventario read-only delle concessioni: chi puo' fare cosa su QUESTO nodo,
|
|
1247
|
+
// con la revisione della policy. E' il punto di partenza della migrazione
|
|
1248
|
+
// esplicita: i peer non configurati restano negati finche' non lo sono.
|
|
1249
|
+
if (sub === 'access') {
|
|
1250
|
+
return { code: nodesCmds.nodesAccess({ ...opts, log, json: flags.json === true }).code };
|
|
1251
|
+
}
|
|
1205
1252
|
if (sub === 'remove') {
|
|
1206
1253
|
if (flags.yes !== true) { log('nodes remove: conferma richiesta con --yes'); return { code: 1 }; }
|
|
1207
1254
|
return { code: nodesCmds.nodesRemove({ ...opts, log, ref }).code };
|
|
@@ -1303,7 +1350,7 @@ function dispatch(argv, opts = {}) {
|
|
|
1303
1350
|
const { flags, rest } = parseFlags(argv, CLI_VALUE_FLAGS);
|
|
1304
1351
|
const cmd = rest[0];
|
|
1305
1352
|
|
|
1306
|
-
//
|
|
1353
|
+
// Help-as-flag handling come flag, deciso qui e solo qui: dopo parseFlags `--help`
|
|
1307
1354
|
// finisce in flags e `-h` resta come posizionale dopo il sottocomando; in
|
|
1308
1355
|
// entrambi i casi il sottocomando NON deve essere eseguito. Prima di questa
|
|
1309
1356
|
// guardia `nexuscrew init --help` eseguiva init davvero: rigenerava i plist,
|
package/lib/cli/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// nexuscrew doctor:
|
|
2
|
+
// nexuscrew doctor: self-diagnosis entry point.
|
|
3
3
|
// Struttura ESTENSIBILE: una lista di check-fn, ognuna ritorna
|
|
4
4
|
// { name, ok, warn?, detail? }. SSH è un requisito locale; la policy del
|
|
5
5
|
// server remoto si prova soltanto tentando il forwarding reale.
|
|
@@ -276,7 +276,7 @@ function checkTmuxSurvival(platform, execImpl) {
|
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
// ssh client
|
|
279
|
+
// ssh client on PATH: prerequisite for the multi-node tunnels.
|
|
280
280
|
function checkSshClient(existsImpl) {
|
|
281
281
|
return existsImpl('ssh')
|
|
282
282
|
? { name: 'OpenSSH transport', ok: true, detail: 'ssh presente · USATO dal supervisor NexusCrew' }
|
|
@@ -424,7 +424,7 @@ function checkTmuxServerCwd(platform, execImpl, opts = {}) {
|
|
|
424
424
|
|
|
425
425
|
// Inspect only the single tmux global environment key required by
|
|
426
426
|
// termux-exec. The value is validated and then discarded; it is never logged.
|
|
427
|
-
// This distinguishes a healthy server from
|
|
427
|
+
// This distinguishes a healthy server from a stale run (no preload) and one
|
|
428
428
|
// (present but rejected by the trust boundary) without killing any session.
|
|
429
429
|
function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
|
|
430
430
|
const env = runtimeEnv || process.env;
|
|
@@ -448,13 +448,13 @@ function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
|
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
// Check MCP identity
|
|
451
|
+
// Check MCP identity. NON-FAILING per costrutto: `ok` è sempre true, così
|
|
452
452
|
// chi usa NexusCrew solo come PWA (nessuna integrazione MCP) non vede mai il
|
|
453
453
|
// doctor andare in FAIL solo per env MCP assente. Il check osserva SOLO la
|
|
454
454
|
// presence delle env var di identità nel processo che lancia `doctor` (nessuna
|
|
455
455
|
// lettura di ~/.codex/config.toml, cache private o config MCP).
|
|
456
456
|
//
|
|
457
|
-
// LIMITE DOCUMENTATO
|
|
457
|
+
// LIMITE DOCUMENTATO: `doctor` non gira dentro il server MCP stdio e non può
|
|
458
458
|
// distinguere in modo portatile "MCP configurato" da "PWA-only". Il WARN è quindi
|
|
459
459
|
// conservativo e informativo: segnala l'assenza di identità osservabile a chi sta
|
|
460
460
|
// configurando l'MCP, senza rompere l'utente PWA-only (per il quale ok resta true).
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Service companion (boot) generation and migration gate.
|
|
3
3
|
//
|
|
4
4
|
// generateFleetService({platform, nodeBin, entryPath}) genera il contenuto del
|
|
5
5
|
// service UNICO di boot (NON una unit per cella) che al boot esegue
|
|
@@ -44,7 +44,7 @@ function generateFleetService(opts) {
|
|
|
44
44
|
function generateFleetLinux(opts) {
|
|
45
45
|
const home = opts.home || os.homedir();
|
|
46
46
|
const { nodeBin, entryPath } = opts;
|
|
47
|
-
// Parita' di hardening con service.js
|
|
47
|
+
// Parita' di hardening con service.js: reject char non gestibili in systemd.
|
|
48
48
|
assertSystemdSafe('home', home);
|
|
49
49
|
assertSystemdSafe('nodeBin', nodeBin);
|
|
50
50
|
assertSystemdSafe('entryPath', entryPath);
|
|
@@ -194,7 +194,7 @@ function selectProviderModeSync(cfg = {}) {
|
|
|
194
194
|
|
|
195
195
|
// --- Install (speculare a installService di lib/cli/service.js) ---
|
|
196
196
|
// Stesso hardening di service.js: no-symlink atomic (lstat + tmp+rename), failure
|
|
197
|
-
//
|
|
197
|
+
// Collection (NON ingoiare activation —), execImpl iniettabile (test). Differenze
|
|
198
198
|
// companion:
|
|
199
199
|
// - nomi 'nexuscrew-fleet.*' + mode termux 0o755 (boot script eseguibile)
|
|
200
200
|
// - linux: daemon-reload + enable, NESSUN restart (Type=oneshot -> parte al boot)
|
|
@@ -209,7 +209,7 @@ function fleetInstallPath(platform, home) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
function fleetFileMode(platform) {
|
|
212
|
-
if (platform === 'termux') return 0o755; // boot script
|
|
212
|
+
if (platform === 'termux') return 0o755; // boot script must stay executable (chmod 755)
|
|
213
213
|
return 0o644; // systemd unit + launchd plist (come service.js)
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -237,7 +237,7 @@ function fleetInstallCommands(platform, target, ctx) {
|
|
|
237
237
|
|
|
238
238
|
// Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
|
|
239
239
|
// default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
|
|
240
|
-
//
|
|
240
|
+
// E' PRESERVATO e le failure raccolte per diagnosi (it is never swallowed, non si rollback).
|
|
241
241
|
// da revisione: `activate: false` (speculare a installService) scrive la definizione SENZA
|
|
242
242
|
// toccare il service manager — per chi rigenera a meta' aggiornamento: il companion
|
|
243
243
|
// e' RunAtLoad/oneshot, un bootstrap adesso eseguirebbe le celle boot:true mentre
|
|
@@ -255,7 +255,7 @@ function installFleetService(platform, content, ctx, {
|
|
|
255
255
|
const target = ctx.installPath || fleetInstallPath(platform, home);
|
|
256
256
|
const mode = fleetFileMode(platform);
|
|
257
257
|
|
|
258
|
-
//
|
|
258
|
+
// Lstat: reject symlink preesistente (no-symlink atomic, parita' service.js)
|
|
259
259
|
try {
|
|
260
260
|
const st = fs.lstatSync(target);
|
|
261
261
|
if (st.isSymbolicLink()) {
|
|
@@ -288,7 +288,7 @@ function installFleetService(platform, content, ctx, {
|
|
|
288
288
|
return { target, mode, written: true, failures: [], skippedActivation: skipped };
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
//
|
|
291
|
+
// Exec service manager — raccogli failure per diagnosi (NON ingoiare)
|
|
292
292
|
const failures = [];
|
|
293
293
|
if (platform === 'mac') {
|
|
294
294
|
failures.push(...reinstallLaunchAgent(target, 'com.mmmbuto.nexuscrew-fleet', ctx, {
|
package/lib/cli/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Nexuscrew init: orchestrazione setup portatile.
|
|
3
3
|
// detectPlatform -> prereq (Node>=18 abort, tmux abort-before-service) ->
|
|
4
4
|
// migration rule (porta da service esistente) -> config.json (preserva) ->
|
|
5
5
|
// token (preserva) -> NexusFiles -> generateService -> installService (skip dry-run) ->
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// fleet companion (only when the provider is builtin and the gate passes;
|
|
7
|
+
// never fails the main init) -> print URL #token. Termux boot: best-effort.
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
@@ -12,7 +12,7 @@ const crypto = require('node:crypto');
|
|
|
12
12
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
|
|
13
13
|
const { loadOrCreateToken } = require('../auth/token.js');
|
|
14
14
|
const { generateService, installService, fileMode, installPath: svcInstallPath } = require('./service.js');
|
|
15
|
-
//
|
|
15
|
+
// Fleet boot companion. Injectable seams for tests.
|
|
16
16
|
const {
|
|
17
17
|
generateFleetService, installFleetService, migrationGate,
|
|
18
18
|
selectProviderModeSync, fleetFileMode,
|
|
@@ -103,7 +103,7 @@ function ensureFleetDefaults(opts = {}) {
|
|
|
103
103
|
return { path: fleetDefsPath, created: Boolean(creato), enabled: true };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// Migration rule
|
|
106
|
+
// Migration rule: se non c'è config.json, parse la porta dal service file esistente.
|
|
107
107
|
function readExistingPort(platform, home, installPathOverride) {
|
|
108
108
|
const p = installPathOverride || svcInstallPath(platform, home);
|
|
109
109
|
let raw;
|
|
@@ -139,7 +139,7 @@ function runInit(opts = {}) {
|
|
|
139
139
|
const log = opts.log || (() => {});
|
|
140
140
|
const tmuxOk = opts.tmuxOk !== undefined ? opts.tmuxOk : haveTmux(opts.tmuxBin || 'tmux', opts.env || process.env);
|
|
141
141
|
|
|
142
|
-
//
|
|
142
|
+
// Prereq Node (abort before any write)
|
|
143
143
|
if (nodeMajor() < 18) {
|
|
144
144
|
throw new Error(`Node >= 18 richiesto (attuale ${process.versions.node}). Aggiorna Node prima di init.`);
|
|
145
145
|
}
|
|
@@ -161,7 +161,7 @@ function runInit(opts = {}) {
|
|
|
161
161
|
if (migrated) { port = migrated; actions.push(`migration: porta ${port} letta dal service esistente`); }
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
164
|
+
// Config.json (scrivi se non esiste; preserva se esiste)
|
|
165
165
|
if (!dryRun) {
|
|
166
166
|
fs.mkdirSync(configDir, { recursive: true });
|
|
167
167
|
if (!fs.existsSync(configPath)) {
|
|
@@ -198,7 +198,7 @@ function runInit(opts = {}) {
|
|
|
198
198
|
}
|
|
199
199
|
if (!port) port = 41820;
|
|
200
200
|
|
|
201
|
-
//
|
|
201
|
+
// Token (preserva esistente)
|
|
202
202
|
let token;
|
|
203
203
|
if (!dryRun) {
|
|
204
204
|
token = loadOrCreateToken(tokenPath);
|
|
@@ -213,7 +213,7 @@ function runInit(opts = {}) {
|
|
|
213
213
|
actions.push(`files root ${filesRoot}`);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Nodes.json: nodeId STABILE per installazione (generato qui se manca) +
|
|
217
217
|
// migrazione esplicita guarded dei nodes legacy da config.json. READONLY/dry-run
|
|
218
218
|
// saltano; ogni errore -> WARN, MAI far fallire l'init.
|
|
219
219
|
if (!dryRun && process.env.NEXUSCREW_READONLY !== '1' && !opts.readonly) {
|
|
@@ -244,7 +244,7 @@ function runInit(opts = {}) {
|
|
|
244
244
|
port, home, uid: uid(),
|
|
245
245
|
installPath: opts.installPath,
|
|
246
246
|
};
|
|
247
|
-
// 1-bis
|
|
247
|
+
// 1-bis: la STESSA classe di difetto del companion di boot — nodeBin
|
|
248
248
|
// e entry finiscono versionati nell'unit/plist del servizio per-nodo. Si
|
|
249
249
|
// risolve qui, alla scrittura, e si dichiara cio' che non ha alias stabile:
|
|
250
250
|
// i due punti di scrittura non devono divergere (correggerne uno solo
|
|
@@ -262,7 +262,7 @@ function runInit(opts = {}) {
|
|
|
262
262
|
} else if (dryRun) {
|
|
263
263
|
actions.push(`DRY-RUN service (${platform}) generato, NON installato`);
|
|
264
264
|
} else if (!tmuxOk) {
|
|
265
|
-
//
|
|
265
|
+
// Tmux mancante: abort before service install (config/token gia' creati)
|
|
266
266
|
// Se il chiamante ha imposto tmuxOk direttamente (test/seam), non
|
|
267
267
|
// abbiamo l'informazione per distinguere assenza da verifica bloccata:
|
|
268
268
|
// il messaggio resta quello storico.
|
|
@@ -285,24 +285,24 @@ function runInit(opts = {}) {
|
|
|
285
285
|
actions.push(skip);
|
|
286
286
|
}
|
|
287
287
|
if (r.failures && r.failures.length) {
|
|
288
|
-
//
|
|
288
|
+
// File installato MA activation (systemctl/launchctl) fallita
|
|
289
289
|
installFailures.push({ component: 'service', phase: 'activation' });
|
|
290
290
|
actions.push(`WARN: service file installato ${r.target} MA activation fallita: ${r.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
|
|
291
291
|
} else {
|
|
292
292
|
actions.push(`service installed ${r.target} (mode 0${fileMode(platform).toString(8)})`);
|
|
293
293
|
}
|
|
294
294
|
} catch (e) {
|
|
295
|
-
//
|
|
295
|
+
// Failure: preserve file + diagnosi (no rollback)
|
|
296
296
|
installFailures.push({ component: 'service', phase: 'install' });
|
|
297
297
|
actions.push(`WARN: service install fallito: ${e.message} (file generati preservati)`);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
// ---
|
|
302
|
-
//
|
|
303
|
-
// gate
|
|
304
|
-
// READONLY
|
|
305
|
-
//
|
|
301
|
+
// --- Fleet boot companion service ---
|
|
302
|
+
// The companion installs ONLY when the provider is builtin and the migration
|
|
303
|
+
// gate finds no enabled legacy cloud-cell@*.service units (no double boot).
|
|
304
|
+
// READONLY blocks every companion action. Never let it fail the main init:
|
|
305
|
+
// any companion error -> WARN action + normal return.
|
|
306
306
|
try {
|
|
307
307
|
const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
|
|
308
308
|
if (!installBoot) {
|
|
@@ -310,9 +310,9 @@ function runInit(opts = {}) {
|
|
|
310
310
|
} else if (readonly) {
|
|
311
311
|
actions.push('fleet companion: READONLY, non installato');
|
|
312
312
|
} else {
|
|
313
|
-
// provider mode: companion
|
|
314
|
-
//
|
|
315
|
-
//
|
|
313
|
+
// provider mode: companion ONLY when builtin. selectProviderModeSync is
|
|
314
|
+
// the synchronous default (runInit is sync: the bin calls process.exit after
|
|
315
|
+
// dispatch, an async selectProvider would not survive). Seam opts.selectProvider for tests.
|
|
316
316
|
const selectProvider = opts.selectProvider || selectProviderModeSync;
|
|
317
317
|
const fleetCfg = opts.fleetCfg || {
|
|
318
318
|
home,
|
|
@@ -324,16 +324,16 @@ function runInit(opts = {}) {
|
|
|
324
324
|
if (sel.mode !== 'builtin') {
|
|
325
325
|
actions.push(`fleet companion: non installato (provider ${sel.mode})`);
|
|
326
326
|
} else {
|
|
327
|
-
// migration gate
|
|
328
|
-
//
|
|
329
|
-
//
|
|
327
|
+
// migration gate: seam opts.migrationGate injectable; the fleet-service.js
|
|
328
|
+
// one (injectable exec via opts.execImpl). Blocked -> do NOT
|
|
329
|
+
// install (never a silent double boot).
|
|
330
330
|
const gate = (opts.migrationGate || migrationGate)({ exec: opts.execImpl, platform });
|
|
331
331
|
if (gate.blocked) {
|
|
332
332
|
actions.push(`WARN: fleet companion NON installato — migration gate bloccato: ${(gate.units || []).join(', ')} (${gate.remediation || 'disabilita le unit legacy cloud-cell@*.service'})`);
|
|
333
333
|
} else if (dryRun) {
|
|
334
334
|
actions.push('DRY-RUN fleet companion generato, NON installato');
|
|
335
335
|
} else {
|
|
336
|
-
//
|
|
336
|
+
// I path del boot si risolvono alla SCRITTURA del servizio (mai
|
|
337
337
|
// a ogni avvio): un alias stabile che risolva allo stesso file, se
|
|
338
338
|
// c'e'; altrimenti il path attuale, DICHIARATO — un avviso adesso e'
|
|
339
339
|
// meglio di un path che scade in silenzio (vedi stable-alias.js).
|
|
@@ -355,7 +355,7 @@ function runInit(opts = {}) {
|
|
|
355
355
|
if (fr.skippedActivation) {
|
|
356
356
|
actions.push(`fleet companion ${fr.target}: activation skipped (--no-activate)`);
|
|
357
357
|
} else if (fr.failures && fr.failures.length) {
|
|
358
|
-
//
|
|
358
|
+
// File installato MA activation (systemctl/launchctl) fallita
|
|
359
359
|
installFailures.push({ component: 'fleet-companion', phase: 'activation' });
|
|
360
360
|
actions.push(`WARN: fleet companion file installato ${fr.target} MA activation fallita: ${fr.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
|
|
361
361
|
} else {
|
|
@@ -370,7 +370,7 @@ function runInit(opts = {}) {
|
|
|
370
370
|
actions.push(`WARN: fleet companion fallito: ${e.message} (init principale prosegue)`);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// Termux:boot best-effort detection
|
|
373
|
+
// Termux:boot best-effort detection
|
|
374
374
|
if (platform === 'termux' && installBoot) {
|
|
375
375
|
const bootDir = path.join(home, '.termux', 'boot');
|
|
376
376
|
const bootOk = fs.existsSync(bootDir);
|
package/lib/cli/pidfile.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Pidfile con verified kill: metadata {pid, cmd, startTs}; kill verifica cmd+pid
|
|
3
3
|
// prima di signalare (no broad match by name). Il controllo pid+nascita riduce
|
|
4
4
|
// il PID reuse a una finestra residua fra verifica e segnale — non lo elimina;
|
|
5
|
-
//
|
|
5
|
+
// Dettaglio e motivo nel commento sopra il kill in killPidfile.
|
|
6
6
|
// Primario su Termux (serve --pidfile); opzionale --manual su linux/mac.
|
|
7
7
|
const fs = require('node:fs');
|
|
8
8
|
const os = require('node:os');
|
package/lib/cli/service.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Service generation per-platform: systemd --user (linux), launchd plist (mac),
|
|
3
|
-
// Termux:boot script. Escaping per-platform + install no-symlink atomic.
|
|
3
|
+
// Termux:boot script. Escaping per-platform + install no-symlink atomic.
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
@@ -42,7 +42,7 @@ function shellQuote(s) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// systemd ExecStart non gestisce bene alcuni char (", $, ;, backtick, newline, apice)
|
|
45
|
-
//
|
|
45
|
+
// Nemmeno con escaping ad hoc: reject esplicito con errore chiaro.
|
|
46
46
|
// space e % sono ok (escaped). Path repo normali non hanno questi char.
|
|
47
47
|
const SYSTEMD_FORBIDDEN = /[";$`\n']/;
|
|
48
48
|
function assertSystemdSafe(label, p) {
|
|
@@ -54,7 +54,7 @@ function assertSystemdSafe(label, p) {
|
|
|
54
54
|
// --- Templates ---
|
|
55
55
|
|
|
56
56
|
// Entry del servizio: di norma derivato da repoRoot; i punti di scrittura
|
|
57
|
-
//
|
|
57
|
+
// Che risolvono l'alias stabile (/1-bis, stable-alias.js) passano
|
|
58
58
|
// l'entryPath gia' risolto. Contratto vecchio intatto: senza entryPath si
|
|
59
59
|
// deriva da repoRoot come prima.
|
|
60
60
|
function serviceEntryPath(ctx) {
|
|
@@ -69,7 +69,7 @@ function generateService(platform, ctx) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function generateLinux(ctx) {
|
|
72
|
-
assertSystemdSafe('repoRoot', ctx.repoRoot); //
|
|
72
|
+
assertSystemdSafe('repoRoot', ctx.repoRoot); // Reject char non gestibili
|
|
73
73
|
assertSystemdSafe('nodeBin', ctx.nodeBin);
|
|
74
74
|
// HOME is stable across init/repair/update. The runtime directory may be
|
|
75
75
|
// atomically replaced; keeping it as the service cwd leaves the HTTP process
|
|
@@ -304,7 +304,7 @@ function reinstallLaunchAgent(target, serviceName, ctx, opts = {}) {
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
// Install no-symlink + atomic rename.
|
|
307
|
+
// Install no-symlink + atomic rename.
|
|
308
308
|
// - lstat target: reject symlink (no follow)
|
|
309
309
|
// - write temp file nella stessa dir -> chmod mode -> atomic rename
|
|
310
310
|
// - exec service manager (systemctl/launchctl); skip in dryRun
|
|
@@ -352,7 +352,7 @@ function installService(platform, content, ctx, {
|
|
|
352
352
|
throw e;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
//
|
|
355
|
+
// Exec service manager — NON ingoiare failure: raccogli per diagnosi
|
|
356
356
|
const cmds = installCommands(platform, target, ctx);
|
|
357
357
|
const activation = planActivation(platform, target, ctx, { execImpl, activate });
|
|
358
358
|
if (!activation.activate) {
|
package/lib/cli/stable-alias.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Alias stabili per i path del servizio di boot.
|
|
3
3
|
//
|
|
4
4
|
// Il companion di boot scrive nodeBin e entryPath LETTERALMENTE nel plist
|
|
5
5
|
// launchd (ProgramArguments[0]) e nell'unit systemd (ExecStart=). Quei path
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Se c'e', scrivere quello; se no, scrivere il path attuale e DICHIARARLO:
|
|
18
18
|
// un avviso alla scrittura e' meglio di un path che scade in silenzio.
|
|
19
19
|
//
|
|
20
|
-
// Tre vincoli non negoziabili (briefing
|
|
20
|
+
// Tre vincoli non negoziabili (briefing):
|
|
21
21
|
// 1. La verifica «e' lo stesso file» si fa alla SCRITTURA del servizio, MAI a
|
|
22
22
|
// ogni avvio: dopo l'upgrade il symlink DEVE puntare a un node diverso —
|
|
23
23
|
// e' l'effetto voluto. Un controllo di identita' permanente si
|
|
@@ -116,7 +116,7 @@ function resolveBootPaths({ nodeBin, entryPath, env = process.env, realpath } =
|
|
|
116
116
|
// morto: il suo criterio e' «alias che realpath allo STESSO file», e un file
|
|
117
117
|
// morto non e' lo stesso file di nessuno. Il criterio qui e' diverso e
|
|
118
118
|
// dichiarato: un path VIVO — l'alias stabile dello stesso file quando
|
|
119
|
-
//
|
|
119
|
+
// L'attuale vive (intatto), altrimenti il primo candidato stabile che
|
|
120
120
|
// ESISTE, con la dichiarazione; se non c'e' niente di vivo, il path resta e
|
|
121
121
|
// la dichiarazione dice di rilanciare init. `env`, `realpath` e `exists`
|
|
122
122
|
// iniettabili per test.
|
|
@@ -127,7 +127,7 @@ function resolveLiveBootPaths({ nodeBin, entryPath, env = process.env, realpath,
|
|
|
127
127
|
const revive = (current, candidates, what) => {
|
|
128
128
|
try {
|
|
129
129
|
realpathImpl(current);
|
|
130
|
-
return null; //
|
|
130
|
+
return null; // Vivo: decide il criterio di resolveBootPaths
|
|
131
131
|
} catch (_) { /* morto su disco: cerca una alternativa viva */ }
|
|
132
132
|
for (const candidate of Array.isArray(candidates) ? candidates : []) {
|
|
133
133
|
if (existsImpl(candidate)) {
|
package/lib/cli/url.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// URL/token helpers
|
|
2
|
+
// URL/token helpers for the unified CLI.
|
|
3
3
|
// - resolvePaths: config/token path home-aware (seam per test con HOME temporanea).
|
|
4
4
|
// - readToken: legge il token esistente SENZA crearlo (no-follow symlink), null se assente.
|
|
5
5
|
// - loadPort: porta corrente (opts > env > config.json > default 41820).
|
package/lib/config.js
CHANGED
|
@@ -17,7 +17,7 @@ function baseDefaults() {
|
|
|
17
17
|
return {
|
|
18
18
|
bind: '127.0.0.1',
|
|
19
19
|
port: 41820,
|
|
20
|
-
// Seconda porta loopback, SOLO per /panel/* (
|
|
20
|
+
// Seconda porta loopback, SOLO per /panel/* (sicurezza 2026-08-16):
|
|
21
21
|
// un pannello vive su un'origin diversa dal control plane — la porta fa
|
|
22
22
|
// parte dell'origin, quindi un iframe cross-porta non legge il
|
|
23
23
|
// localStorage del padre, qualunque JS esegua il container. Nessuna API
|
|
@@ -57,14 +57,14 @@ function baseDefaults() {
|
|
|
57
57
|
// Il manager aggiorna solo verso una semver superiore: mai downgrade.
|
|
58
58
|
autoUpdate: true,
|
|
59
59
|
sessionPresets: {},
|
|
60
|
-
// Ponte Live (fetta 3, rev5
|
|
60
|
+
// Ponte Live (fetta 3, rev5): isolabile — a false il ponte non si
|
|
61
61
|
// connette mai e ogni avvio di Live resta sul comportamento standard.
|
|
62
62
|
liveBridgeEnabled: true,
|
|
63
63
|
// Socket di controllo dell'app-server (misurato 2026-08-15:
|
|
64
64
|
// $CODEX_HOME/app-server-control/app-server-control.sock, WebSocket sopra
|
|
65
65
|
// unix socket). Non cablare mai una porta al posto di questo path.
|
|
66
66
|
liveBridgeSocketPath: path.join(os.homedir(), '.codex', 'app-server-control', 'app-server-control.sock'),
|
|
67
|
-
//
|
|
67
|
+
// Limite dichiarato per OGNI fase del ponte (GET designazione e
|
|
68
68
|
// sessione sul socket). Oltre questo la Live parte senza puntamento.
|
|
69
69
|
liveBridgeTimeoutMs: 1500,
|
|
70
70
|
// PTY grace is finite and configurable for mobile handovers. The caps keep
|
|
@@ -82,7 +82,37 @@ function configJsonPath() {
|
|
|
82
82
|
return process.env.NEXUSCREW_CONFIG_FILE || path.join(os.homedir(), '.nexuscrew', 'config.json');
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Legge ~/.nexuscrew/config.json se esiste (nuovo
|
|
85
|
+
// Legge ~/.nexuscrew/config.json se esiste (nuovo). {} se assente/malformato.
|
|
86
|
+
// federation.events.enabled — the node-level kill switch for the event feed.
|
|
87
|
+
// STRICT boolean: only the literal false disables. A truthy string or any
|
|
88
|
+
// other type never enables anything beyond the default, and never disables by
|
|
89
|
+
// accident: "false" (string) is not a decision, it is noise, and the default
|
|
90
|
+
// holds. Without explicit grants nothing is enabled either way — the switch
|
|
91
|
+
// only stops what grants already allow.
|
|
92
|
+
function eventsEnabledFlag(raw, log = console.warn) {
|
|
93
|
+
// Fail-closed strictness: only the literal true/false is a decision. A
|
|
94
|
+
// missing value keeps the default (enabled); ANY other type — "false" as a
|
|
95
|
+
// string, 0, null, an object — is invalid config, is treated as DISABLED and
|
|
96
|
+
// is reported: silently enabling on garbage is how switches lie.
|
|
97
|
+
if (raw === undefined) return true;
|
|
98
|
+
if (raw === true) return true;
|
|
99
|
+
if (raw === false) return false;
|
|
100
|
+
try { log('config: federation.events.enabled must be a boolean; invalid value treated as disabled'); } catch (_) {}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readFederationEventsEnabled(opts = {}) {
|
|
105
|
+
if (typeof opts.federationEventsEnabled === 'boolean') return opts.federationEventsEnabled;
|
|
106
|
+
let cfg = {};
|
|
107
|
+
try {
|
|
108
|
+
const raw = fs.readFileSync(opts.configJsonPath || configJsonPath(), 'utf8');
|
|
109
|
+
cfg = JSON.parse(raw) || {};
|
|
110
|
+
} catch (_) { cfg = {}; }
|
|
111
|
+
const fed = cfg && typeof cfg.federation === 'object' && cfg.federation ? cfg.federation : null;
|
|
112
|
+
const events = fed && typeof fed.events === 'object' && fed.events ? fed.events : null;
|
|
113
|
+
return eventsEnabledFlag(events ? events.enabled : undefined);
|
|
114
|
+
}
|
|
115
|
+
|
|
86
116
|
function readConfigJson() {
|
|
87
117
|
try {
|
|
88
118
|
const raw = fs.readFileSync(configJsonPath(), 'utf8');
|
|
@@ -134,7 +164,7 @@ function envOverrides() {
|
|
|
134
164
|
return e;
|
|
135
165
|
}
|
|
136
166
|
|
|
137
|
-
// Precedence: baseDefaults < config.json < env < opts.
|
|
167
|
+
// Precedence: baseDefaults < config.json < env < opts.
|
|
138
168
|
// Le credenziali authority vivono in due file 0600 nella dir
|
|
139
169
|
// dell'authority (default ~/.nexuscrew/identity-authority). Se esistono vengono
|
|
140
170
|
// caricate qui, senza MAI loggare i valori: builtin costruisce l'authority
|
|
@@ -195,4 +225,4 @@ function defaults() {
|
|
|
195
225
|
return { ...baseDefaults(), ...envOverrides() };
|
|
196
226
|
}
|
|
197
227
|
|
|
198
|
-
module.exports = { assertLoopback, baseDefaults, readConfigJson, loadConfig, defaults, LOOPBACK, configJsonPath };
|
|
228
|
+
module.exports = { assertLoopback, baseDefaults, readConfigJson, loadConfig, defaults, LOOPBACK, configJsonPath, eventsEnabledFlag, readFederationEventsEnabled };
|
package/lib/files/routes.js
CHANGED
|
@@ -55,7 +55,7 @@ function filesRoutes({
|
|
|
55
55
|
// paste=false (tasto allegati del composer): il client appende il path al
|
|
56
56
|
// testo del composer — niente scrittura PTY. Default = incolla (FilesPanel).
|
|
57
57
|
const wantPaste = String((req.body && req.body.paste) || '') !== 'false';
|
|
58
|
-
//
|
|
58
|
+
// : paste RICHIESTO ma non riuscito non e' un 200 con pasted:false —
|
|
59
59
|
// tre cause collassavano in quell'unica etichetta muta (non richiesto /
|
|
60
60
|
// tmux non ha ricevuto / testo respinto) e nessuno, risposta o UI, poteva
|
|
61
61
|
// accorgersene. Il vocabolario e' quello del ramo answer degli ask
|
package/lib/fleet/boot.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Fleet boot companion: bootCells(fleet, {log}) starts the boot:true cells
|
|
3
3
|
// boot:true di un fleet provider (builtin) chiamandone up(). Una cella gia' in
|
|
4
4
|
// esecuzione (up -> httpError 409 / "duplicate session") = SKIP non fatale;
|
|
5
5
|
// ogni altro errore viene raccolto in failed[] e NON ferma le altre celle.
|
|
6
|
-
//
|
|
6
|
+
// No side effects on import.
|
|
7
7
|
//
|
|
8
8
|
// Agnostico: usa solo il contratto fleet {available, status(), up()} — non legge
|
|
9
9
|
// internals delle definizioni. La lista delle celle boot:true arriva da status()
|