@mmmbuto/nexuscrew 0.7.5 → 0.7.7
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/README.md +22 -3
- package/frontend/dist/assets/index-DUbtTZMj.css +32 -0
- package/frontend/dist/assets/index-EVa9bnAC.js +81 -0
- package/frontend/dist/index.html +2 -2
- package/lib/cli/commands.js +48 -1
- package/lib/cli/fleet-service.js +306 -0
- package/lib/cli/init.js +68 -1
- package/lib/files/routes.js +4 -1
- package/lib/fleet/boot.js +62 -0
- package/lib/fleet/builtin.js +477 -0
- package/lib/fleet/definitions.js +384 -0
- package/lib/fleet/index.js +49 -10
- package/lib/fleet/provider.js +102 -0
- package/lib/fleet/routes.js +77 -8
- package/lib/fs/routes.js +56 -0
- package/lib/server.js +8 -3
- package/package.json +2 -2
- package/frontend/dist/assets/index-BWhSqR3J.css +0 -32
- package/frontend/dist/assets/index-Bx8vdLZY.css +0 -32
- package/frontend/dist/assets/index-CLb2xmyu.js +0 -81
- package/frontend/dist/assets/index-CgxfkmRo.js +0 -81
- package/frontend/dist/assets/index-DQMx2p4x.js +0 -81
- package/frontend/dist/assets/index-DWhfVwKW.css +0 -32
- package/frontend/dist/assets/index-DoPZ_3-h.js +0 -81
- package/frontend/dist/assets/index-o9l7pPAf.css +0 -32
- package/frontend/dist/assets/index-q9PUrGO0.js +0 -81
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-EVa9bnAC.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DUbtTZMj.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/lib/cli/commands.js
CHANGED
|
@@ -18,6 +18,7 @@ Usage:
|
|
|
18
18
|
nexuscrew start avvia il servizio (systemctl / launchctl / nohup+pidfile)
|
|
19
19
|
nexuscrew stop stop del servizio (service manager / pidfile verificato)
|
|
20
20
|
nexuscrew status stato: platform + service + porta + URL
|
|
21
|
+
nexuscrew fleet-boot companion di boot: avvia le celle boot:true (config-driven)
|
|
21
22
|
|
|
22
23
|
Piattaforme: linux (systemd --user), mac (launchd), termux (nohup + pidfile).
|
|
23
24
|
Bind loopback 127.0.0.1 — raggiungibile via tunnel SSH/VPN.`;
|
|
@@ -162,6 +163,49 @@ function status(opts = {}) {
|
|
|
162
163
|
return out;
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// B4.3 — fleet-boot companion: avvia le celle boot:true del provider selezionato.
|
|
167
|
+
// dispatch e' sync (bin/nexuscrew.js non fa await), quindi runFleetBoot (async)
|
|
168
|
+
// viene lanciato e il processo tenuto vivo (keepAlive) finche' non termina, poi
|
|
169
|
+
// esce col code risultante. runFleetBoot e' la unit testabile (no process.exit);
|
|
170
|
+
// dispatchFleetBoot e' il thin glue. Seam iniettabili: selectProvider / loadConfig
|
|
171
|
+
// / cfg / exit (per test, no process.exit che uccide il runner).
|
|
172
|
+
async function runFleetBoot(opts = {}) {
|
|
173
|
+
const log = opts.log || console.log;
|
|
174
|
+
const loadConfig = opts.loadConfig || require('../config.js').loadConfig;
|
|
175
|
+
const selectProvider = opts.selectProvider || require('../fleet/provider.js').selectProvider;
|
|
176
|
+
const { bootCells } = require('../fleet/boot.js');
|
|
177
|
+
const cfg = opts.cfg || loadConfig();
|
|
178
|
+
|
|
179
|
+
const sel = await selectProvider(cfg);
|
|
180
|
+
if (sel.mode === 'external') {
|
|
181
|
+
log('fleet-boot: boot gestito dal fleet esterno (nessuna azione del companion)');
|
|
182
|
+
return { code: 0, mode: 'external' };
|
|
183
|
+
}
|
|
184
|
+
if (sel.mode !== 'builtin' || !sel.fleet || !sel.fleet.available) {
|
|
185
|
+
// disabled (o builtin unavailable): niente da avviare, non e' un errore.
|
|
186
|
+
log(`fleet-boot: nessun boot (provider ${sel.mode}${sel.reason ? ' — ' + sel.reason : ''})`);
|
|
187
|
+
return { code: 0, mode: sel.mode };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// builtin: up() per ogni cella boot:true. READONLY fa fallire le up con 403 ->
|
|
191
|
+
// raccolte in failed[] -> exit 1 (nessun short-circuit speciale, design §9d).
|
|
192
|
+
const res = await bootCells(sel.fleet, { log });
|
|
193
|
+
log(`fleet-boot: ${res.started.length} started, ${res.skipped.length} skipped, ${res.failed.length} failed`);
|
|
194
|
+
for (const f of res.failed) log(` failed: ${f.cell} — ${f.reason}`);
|
|
195
|
+
return { code: res.failed.length ? 1 : 0, mode: 'builtin', summary: res };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dispatchFleetBoot(opts) {
|
|
199
|
+
const exit = typeof opts.exit === 'function' ? opts.exit : ((c) => process.exit(c));
|
|
200
|
+
runFleetBoot(opts)
|
|
201
|
+
.then((r) => exit(r.code))
|
|
202
|
+
.catch((e) => {
|
|
203
|
+
(opts.log || console.log)(`fleet-boot: error — ${e && e.message ? e.message : e}`);
|
|
204
|
+
exit(1);
|
|
205
|
+
});
|
|
206
|
+
return { code: 0, keepAlive: true }; // il processo resta vivo finche' runFleetBoot non chiama exit()
|
|
207
|
+
}
|
|
208
|
+
|
|
165
209
|
function dispatch(argv, opts = {}) {
|
|
166
210
|
const { flags, rest } = parseFlags(argv);
|
|
167
211
|
const cmd = rest[0];
|
|
@@ -191,8 +235,11 @@ function dispatch(argv, opts = {}) {
|
|
|
191
235
|
status({ execImpl: opts.execImpl, log, platform: opts.platform, uid: opts.uid, home: opts.home });
|
|
192
236
|
return { code: 0 };
|
|
193
237
|
}
|
|
238
|
+
if (cmd === 'fleet-boot') {
|
|
239
|
+
return dispatchFleetBoot({ ...opts, log });
|
|
240
|
+
}
|
|
194
241
|
log(`unknown command: ${cmd}\n\n${HELP}`);
|
|
195
242
|
return { code: 1 };
|
|
196
243
|
}
|
|
197
244
|
|
|
198
|
-
module.exports = { dispatch, serve, start, stop, status, parseFlags, HELP };
|
|
245
|
+
module.exports = { dispatch, serve, start, stop, status, parseFlags, HELP, runFleetBoot, dispatchFleetBoot };
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// B4.3 — Service companion (boot) generation + migration gate. Design §4c / §9b.
|
|
3
|
+
//
|
|
4
|
+
// generateFleetService({platform, nodeBin, entryPath}) genera il contenuto del
|
|
5
|
+
// service UNICO di boot (NON una unit per cella) che al boot esegue
|
|
6
|
+
// <node> <entry> fleet-boot
|
|
7
|
+
// seguendo ESATTAMENTE i pattern/escaping di lib/cli/service.js:
|
|
8
|
+
// linux -> systemd --user 'nexuscrew-fleet.service' (Type=oneshot)
|
|
9
|
+
// mac -> launchd plist 'com.mmmbuto.nexuscrew-fleet.plist' (RunAtLoad)
|
|
10
|
+
// termux -> script '~/.termux/boot/nexuscrew-fleet.sh'
|
|
11
|
+
//
|
|
12
|
+
// migrationGate({exec, platform}) rileva unit legacy 'cloud-cell@*.service'
|
|
13
|
+
// abilitate (systemctl --user list-unit-files --state=enabled, via execFile —
|
|
14
|
+
// MAI shell string). Se presenti -> {blocked:true, units, remediation}: il
|
|
15
|
+
// companion NON va installato (design 9b: mai doppio boot silenzioso). Su
|
|
16
|
+
// piattaforme senza systemd il gate passa. `exec` e' iniettabile per testabilita'.
|
|
17
|
+
//
|
|
18
|
+
// Nessun side-effect all'import e NESSUNA installazione reale qui (solo generazione
|
|
19
|
+
// contenuto + gate). L'installazione e' demandata al flusso init (separato).
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
const {
|
|
26
|
+
escapeSystemdPath, escapeSystemdExec, escapeXml, shellQuote, assertSystemdSafe,
|
|
27
|
+
} = require('./service.js');
|
|
28
|
+
const { uid } = require('./platform.js');
|
|
29
|
+
|
|
30
|
+
// entryPath e' tipicamente <repoRoot>/bin/nexuscrew.js -> repoRoot = dirname x2.
|
|
31
|
+
function deriveRepoRoot(entryPath) {
|
|
32
|
+
return path.dirname(path.dirname(entryPath));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generateFleetService(opts) {
|
|
36
|
+
const platform = opts && opts.platform;
|
|
37
|
+
if (platform === 'linux') return generateFleetLinux(opts);
|
|
38
|
+
if (platform === 'mac') return generateFleetMac(opts);
|
|
39
|
+
if (platform === 'termux') return generateFleetTermux(opts);
|
|
40
|
+
throw new Error(`unsupported platform: ${platform}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generateFleetLinux(opts) {
|
|
44
|
+
const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
|
|
45
|
+
const { nodeBin, entryPath } = opts;
|
|
46
|
+
// Parita' di hardening con service.js (M3): reject char non gestibili in systemd.
|
|
47
|
+
assertSystemdSafe('repoRoot', repoRoot);
|
|
48
|
+
assertSystemdSafe('nodeBin', nodeBin);
|
|
49
|
+
assertSystemdSafe('entryPath', entryPath);
|
|
50
|
+
const repo = escapeSystemdPath(repoRoot);
|
|
51
|
+
const node = escapeSystemdExec(nodeBin);
|
|
52
|
+
const entry = escapeSystemdExec(entryPath);
|
|
53
|
+
const nodeDir = escapeSystemdPath(path.dirname(nodeBin));
|
|
54
|
+
return `# NexusCrew fleet boot companion (systemd --user) - avvia le celle boot:true
|
|
55
|
+
[Unit]
|
|
56
|
+
Description=NexusCrew fleet boot companion (avvia le celle boot:true)
|
|
57
|
+
After=network-online.target
|
|
58
|
+
|
|
59
|
+
[Service]
|
|
60
|
+
Type=oneshot
|
|
61
|
+
WorkingDirectory=${repo}
|
|
62
|
+
Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
|
|
63
|
+
ExecStart=${node} ${entry} fleet-boot
|
|
64
|
+
|
|
65
|
+
[Install]
|
|
66
|
+
WantedBy=default.target
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function generateFleetMac(opts) {
|
|
71
|
+
const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
|
|
72
|
+
const home = opts.home || os.homedir();
|
|
73
|
+
const nodeXml = escapeXml(opts.nodeBin);
|
|
74
|
+
const entryXml = escapeXml(opts.entryPath);
|
|
75
|
+
const repoXml = escapeXml(repoRoot);
|
|
76
|
+
const homeXml = escapeXml(home);
|
|
77
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
78
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
79
|
+
<plist version="1.0">
|
|
80
|
+
<dict>
|
|
81
|
+
<key>Label</key>
|
|
82
|
+
<string>com.mmmbuto.nexuscrew-fleet</string>
|
|
83
|
+
<key>ProgramArguments</key>
|
|
84
|
+
<array>
|
|
85
|
+
<string>${nodeXml}</string>
|
|
86
|
+
<string>${entryXml}</string>
|
|
87
|
+
<string>fleet-boot</string>
|
|
88
|
+
</array>
|
|
89
|
+
<key>WorkingDirectory</key>
|
|
90
|
+
<string>${repoXml}</string>
|
|
91
|
+
<key>RunAtLoad</key>
|
|
92
|
+
<true/>
|
|
93
|
+
<key>StandardOutPath</key>
|
|
94
|
+
<string>${homeXml}/.nexuscrew/fleet-boot.log</string>
|
|
95
|
+
<key>StandardErrorPath</key>
|
|
96
|
+
<string>${homeXml}/.nexuscrew/fleet-boot.log</string>
|
|
97
|
+
</dict>
|
|
98
|
+
</plist>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateFleetTermux(opts) {
|
|
103
|
+
const repoRoot = opts.repoRoot || deriveRepoRoot(opts.entryPath);
|
|
104
|
+
const nodeQ = shellQuote(opts.nodeBin);
|
|
105
|
+
const entryQ = shellQuote(opts.entryPath);
|
|
106
|
+
const repoQ = shellQuote(repoRoot);
|
|
107
|
+
return `#!/data/data/com.termux/files/usr/bin/sh
|
|
108
|
+
# NexusCrew fleet boot companion (Termux) - avvia le celle boot:true
|
|
109
|
+
export PATH=/data/data/com.termux/files/usr/bin:$PATH
|
|
110
|
+
export HOME=/data/data/com.termux/files/home
|
|
111
|
+
cd -- ${repoQ}
|
|
112
|
+
exec ${nodeQ} ${entryQ} fleet-boot >> "$HOME/.nexuscrew/fleet-boot.log" 2>&1
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default exec per il gate: execFileSync (argv diretto, MAI shell string).
|
|
117
|
+
function defaultListEnabled(bin, args, opts) {
|
|
118
|
+
return execFileSync(bin, args, opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Migration gate (design 9b): blocca l'installazione del companion se ci sono
|
|
122
|
+
// unit legacy cloud-cell@*.service abilitate (mai doppio boot silenzioso).
|
|
123
|
+
// opts.exec — funzione iniettabile con firma execFileSync(bin, args, opts)
|
|
124
|
+
// opts.platform — se !== 'linux' il gate passa (no systemd -> no rischio)
|
|
125
|
+
// Ritorna {blocked, units[], remediation?|reason?}.
|
|
126
|
+
function migrationGate(opts = {}) {
|
|
127
|
+
const { exec, platform } = opts;
|
|
128
|
+
if (platform && platform !== 'linux') {
|
|
129
|
+
return { blocked: false, units: [], reason: `platform ${platform}: no systemd, gate skipped` };
|
|
130
|
+
}
|
|
131
|
+
const execImpl = typeof exec === 'function' ? exec : defaultListEnabled;
|
|
132
|
+
|
|
133
|
+
let stdout;
|
|
134
|
+
try {
|
|
135
|
+
stdout = execImpl('systemctl',
|
|
136
|
+
['--user', 'list-unit-files', '--state=enabled', 'cloud-cell@*'],
|
|
137
|
+
{ encoding: 'utf8' });
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// systemctl assente / ambiente non systemd-user: nessuna evidenza di unit
|
|
140
|
+
// legacy -> il gate passa (non blocca su assenza di informazioni).
|
|
141
|
+
return { blocked: false, units: [], reason: `gate skipped: ${e && e.message ? e.message : 'command error'}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const units = [];
|
|
145
|
+
for (const line of String(stdout || '').split('\n')) {
|
|
146
|
+
const tok = line.trim().split(/\s+/)[0];
|
|
147
|
+
if (tok && /^cloud-cell@.+\.service$/.test(tok)) units.push(tok);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (units.length) {
|
|
151
|
+
return {
|
|
152
|
+
blocked: true,
|
|
153
|
+
units,
|
|
154
|
+
remediation: 'disabilita le unit legacy cloud-cell@*.service (systemctl --user disable "cloud-cell@*") o usa il fleet esterno; il companion nexuscrew-fleet.service non va installato per evitare doppio boot',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { blocked: false, units: [], reason: 'nessuna unit cloud-cell@*.service abilitata' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- selectProviderModeSync — resolver SINCRONO del mode del provider ---
|
|
161
|
+
// Speculare alla logica di selectProvider (lib/fleet/provider.js) ma SENZA spawn/await.
|
|
162
|
+
// Necessario: runInit e' sync (bin/nexuscrew.js chiama process.exit subito dopo dispatch
|
|
163
|
+
// per 'init'; selectProvider e' async e non sopravvive al process.exit). E' il default
|
|
164
|
+
// del seam opts.selectProvider del companion (iniettabile per i test).
|
|
165
|
+
//
|
|
166
|
+
// Divergenza documentata vs selectProvider: la responsivita' del fleet esterno
|
|
167
|
+
// (status --json) richiederebbe spawn async; qui un fleetBin FIDATO (binTrusted, sync)
|
|
168
|
+
// -> external. Scelta CONSERVATIVA sicura (no-doppio-boot, §9b): se c'e' un fleet
|
|
169
|
+
// esterno fidato configurato, NON installiamo il companion builtin.
|
|
170
|
+
// Ritorna {mode, reason} con mode in 'external' | 'builtin' | 'disabled'.
|
|
171
|
+
function selectProviderModeSync(cfg = {}) {
|
|
172
|
+
if (cfg.fleetEnabled === false) {
|
|
173
|
+
return { mode: 'disabled', reason: 'fleet disabilitato (fleetEnabled=false)' };
|
|
174
|
+
}
|
|
175
|
+
const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase() || null;
|
|
176
|
+
|
|
177
|
+
// external: binario fidato (regular file, exec, non world-writable, no symlink).
|
|
178
|
+
// lazy-require: niente costo/accoppiamento all'import del modulo (usato solo dal companion).
|
|
179
|
+
const { binTrusted } = require('../fleet/index.js'); // sync (audit F3)
|
|
180
|
+
const extTrusted = !!(cfg.fleetBin && binTrusted(cfg.fleetBin));
|
|
181
|
+
|
|
182
|
+
// builtin: fleet.json valido (loadDefinitions e' sync; garbage -> null = fail-closed).
|
|
183
|
+
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
184
|
+
const home = cfg.home || os.homedir();
|
|
185
|
+
const defsPath = cfg.fleetDefsPath || path.join(home, '.nexuscrew', 'fleet.json');
|
|
186
|
+
const builtinAvail = cfg.builtinEnabled !== false ? !!loadDefinitions(defsPath) : false;
|
|
187
|
+
|
|
188
|
+
// forced: fail-closed se il richiesto non e' disponibile (§9g: niente auto-fallback).
|
|
189
|
+
if (forced === 'external') {
|
|
190
|
+
return extTrusted
|
|
191
|
+
? { mode: 'external', reason: 'forced external (binario fidato)' }
|
|
192
|
+
: { mode: 'disabled', reason: 'fail-closed: forced external non fidato (§9g)' };
|
|
193
|
+
}
|
|
194
|
+
if (forced === 'builtin') {
|
|
195
|
+
return builtinAvail
|
|
196
|
+
? { mode: 'builtin', reason: 'forced builtin (fleet.json valido)' }
|
|
197
|
+
: { mode: 'disabled', reason: 'fail-closed: forced builtin ma fleet.json invalido/mancante (§9g)' };
|
|
198
|
+
}
|
|
199
|
+
if (forced === 'disabled') return { mode: 'disabled', reason: 'forced disabled' };
|
|
200
|
+
if (forced) return { mode: 'disabled', reason: `fail-closed: provider forzato "${forced}" non riconosciuto` };
|
|
201
|
+
|
|
202
|
+
// auto: external fidato vince (no-doppio-boot), poi builtin, poi disabled.
|
|
203
|
+
if (extTrusted) return { mode: 'external', reason: 'auto: external fidato configurato (skip companion)' };
|
|
204
|
+
if (builtinAvail) return { mode: 'builtin', reason: 'auto: builtin (fleet.json valido)' };
|
|
205
|
+
return { mode: 'disabled', reason: 'auto: nessun provider disponibile (no external fidato, no fleet.json)' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Install (speculare a installService di lib/cli/service.js) ---
|
|
209
|
+
// Stesso hardening di service.js: no-symlink atomic (lstat + tmp+rename), failure
|
|
210
|
+
// collection (NON ingoiare activation — M1), execImpl iniettabile (test). Differenze
|
|
211
|
+
// companion:
|
|
212
|
+
// - nomi 'nexuscrew-fleet.*' + mode termux 0o755 (boot script eseguibile)
|
|
213
|
+
// - linux: daemon-reload + enable, NESSUN restart (Type=oneshot -> parte al boot)
|
|
214
|
+
// - mac: bootout + bootstrap (idempotente come service.js)
|
|
215
|
+
// Nessuna installazione reale all'import: il companion e' installato dal flusso init.
|
|
216
|
+
|
|
217
|
+
function fleetInstallPath(platform, home) {
|
|
218
|
+
if (platform === 'linux') return path.join(home, '.config', 'systemd', 'user', 'nexuscrew-fleet.service');
|
|
219
|
+
if (platform === 'mac') return path.join(home, 'Library', 'LaunchAgents', 'com.mmmbuto.nexuscrew-fleet.plist');
|
|
220
|
+
if (platform === 'termux') return path.join(home, '.termux', 'boot', 'nexuscrew-fleet.sh');
|
|
221
|
+
throw new Error(`unsupported platform: ${platform}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function fleetFileMode(platform) {
|
|
225
|
+
if (platform === 'termux') return 0o755; // boot script eseguibile (design §4c: chmod 755)
|
|
226
|
+
return 0o644; // systemd unit + launchd plist (come service.js)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fleetInstallCommands(platform, target, ctx) {
|
|
230
|
+
if (platform === 'linux') {
|
|
231
|
+
return [
|
|
232
|
+
['systemctl', ['--user', 'daemon-reload']],
|
|
233
|
+
['systemctl', ['--user', 'enable', 'nexuscrew-fleet.service']],
|
|
234
|
+
// NESSUN restart: Type=oneshot — il companion avvia le celle boot:true al boot.
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
if (platform === 'mac') {
|
|
238
|
+
const label = `gui/${ctx.uid || uid()}/com.mmmbuto.nexuscrew-fleet`;
|
|
239
|
+
return [
|
|
240
|
+
['launchctl', ['bootout', label]], // idempotente: ignorato se non caricato
|
|
241
|
+
['launchctl', ['bootstrap', label, target]],
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
if (platform === 'termux') {
|
|
245
|
+
return []; // nessun service manager; boot script + app Termux:Boot (start manuale)
|
|
246
|
+
}
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
|
|
251
|
+
// default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
|
|
252
|
+
// e' PRESERVATO e le failure raccolte per diagnosi (M1: non si ingoia, non si rollback).
|
|
253
|
+
function installFleetService(platform, content, ctx, { dryRun = false, execImpl = execFileSync } = {}) {
|
|
254
|
+
const home = ctx.home || os.homedir();
|
|
255
|
+
const target = ctx.installPath || fleetInstallPath(platform, home);
|
|
256
|
+
const mode = fleetFileMode(platform);
|
|
257
|
+
|
|
258
|
+
// lstat: reject symlink preesistente (no-symlink atomic, parita' service.js [M3])
|
|
259
|
+
try {
|
|
260
|
+
const st = fs.lstatSync(target);
|
|
261
|
+
if (st.isSymbolicLink()) {
|
|
262
|
+
throw new Error(`refusing symlink install target: ${target}`);
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
if (e.code !== 'ENOENT') throw e;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (dryRun) {
|
|
269
|
+
return { target, mode, written: false, failures: [], note: 'dry-run: nessuna scrittura' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
273
|
+
const tmp = target + '.tmp.' + process.pid; // stessa dir -> atomic rename su stesso FS
|
|
274
|
+
try {
|
|
275
|
+
fs.writeFileSync(tmp, content, { mode });
|
|
276
|
+
fs.chmodSync(tmp, mode);
|
|
277
|
+
fs.renameSync(tmp, target); // atomic
|
|
278
|
+
} catch (e) {
|
|
279
|
+
try { fs.unlinkSync(tmp); } catch (_) {} // cleanup temp su failure [m1]
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// exec service manager — raccogli failure per diagnosi (NON ingoiare, M1)
|
|
284
|
+
const cmds = fleetInstallCommands(platform, target, ctx);
|
|
285
|
+
const failures = [];
|
|
286
|
+
for (const [bin, args] of cmds) {
|
|
287
|
+
try { execImpl(bin, args, { stdio: 'ignore' }); }
|
|
288
|
+
catch (e) { failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message }); }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { target, mode, written: true, failures };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
generateFleetService,
|
|
296
|
+
generateFleetLinux,
|
|
297
|
+
generateFleetMac,
|
|
298
|
+
generateFleetTermux,
|
|
299
|
+
migrationGate,
|
|
300
|
+
deriveRepoRoot,
|
|
301
|
+
installFleetService,
|
|
302
|
+
fleetInstallPath,
|
|
303
|
+
fleetFileMode,
|
|
304
|
+
fleetInstallCommands,
|
|
305
|
+
selectProviderModeSync,
|
|
306
|
+
};
|
package/lib/cli/init.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
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
|
-
//
|
|
6
|
+
// [B4.3] fleet companion (solo se provider builtin + gate ok; READONLY/dry-run skip;
|
|
7
|
+
// mai fa fallire l'init principale) -> print URL #token. Termux:boot best-effort.
|
|
7
8
|
const fs = require('node:fs');
|
|
8
9
|
const os = require('node:os');
|
|
9
10
|
const path = require('node:path');
|
|
@@ -11,6 +12,11 @@ const { execFileSync } = require('node:child_process');
|
|
|
11
12
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
|
|
12
13
|
const { loadOrCreateToken } = require('../auth/token.js');
|
|
13
14
|
const { generateService, installService, fileMode, installPath: svcInstallPath } = require('./service.js');
|
|
15
|
+
// B4.3 — companion di boot del fleet (design §4c/§9b/§9d). Seam iniettabili per test.
|
|
16
|
+
const {
|
|
17
|
+
generateFleetService, installFleetService, migrationGate,
|
|
18
|
+
selectProviderModeSync, fleetFileMode,
|
|
19
|
+
} = require('./fleet-service.js');
|
|
14
20
|
|
|
15
21
|
function haveTmux(tmuxBin) {
|
|
16
22
|
try { execFileSync('command', ['-v', tmuxBin], { stdio: 'ignore', shell: true }); return true; }
|
|
@@ -127,6 +133,67 @@ function runInit(opts = {}) {
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
// --- B4.3 companion: service di boot del fleet (design §4c/§9b/§9d) ---
|
|
137
|
+
// Il companion si installa SOLO se il provider e' builtin (§9b) e il migration
|
|
138
|
+
// gate non rileva unit legacy cloud-cell@*.service abilitate (no doppio boot).
|
|
139
|
+
// READONLY blocca ogni azione del companion (§9d). MAI far fallire l'init
|
|
140
|
+
// principale: ogni errore del companion -> WARN action + return normale.
|
|
141
|
+
try {
|
|
142
|
+
const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
|
|
143
|
+
if (readonly) {
|
|
144
|
+
actions.push('fleet companion: READONLY, non installato');
|
|
145
|
+
} else {
|
|
146
|
+
// provider mode: companion SOLO se builtin (§9b). selectProviderModeSync e'
|
|
147
|
+
// il default SINCRONO (runInit e' sync: bin chiama process.exit dopo dispatch,
|
|
148
|
+
// selectProvider async non sopravvive). Seam opts.selectProvider per i test.
|
|
149
|
+
const selectProvider = opts.selectProvider || selectProviderModeSync;
|
|
150
|
+
const fleetCfg = opts.fleetCfg || {
|
|
151
|
+
home,
|
|
152
|
+
fleetEnabled: opts.fleetEnabled !== undefined ? opts.fleetEnabled : true,
|
|
153
|
+
fleetBin: opts.fleetBin || path.join(home, '.local', 'bin', 'fleet'),
|
|
154
|
+
fleetProvider: opts.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER,
|
|
155
|
+
builtinEnabled: opts.builtinEnabled,
|
|
156
|
+
fleetDefsPath: opts.fleetDefsPath || path.join(configDir, 'fleet.json'),
|
|
157
|
+
};
|
|
158
|
+
const sel = selectProvider(fleetCfg) || {};
|
|
159
|
+
if (sel.mode !== 'builtin') {
|
|
160
|
+
actions.push(`fleet companion: non installato (provider ${sel.mode})`);
|
|
161
|
+
} else {
|
|
162
|
+
// migration gate (§9b): seam opts.migrationGate iniettabile; default quello
|
|
163
|
+
// di fleet-service.js (exec iniettabile via opts.execImpl). blocked -> NON
|
|
164
|
+
// installare (mai doppio boot silenzioso).
|
|
165
|
+
const gate = (opts.migrationGate || migrationGate)({ exec: opts.execImpl, platform });
|
|
166
|
+
if (gate.blocked) {
|
|
167
|
+
actions.push(`WARN: fleet companion NON installato — migration gate bloccato: ${(gate.units || []).join(', ')} (${gate.remediation || 'disabilita le unit legacy cloud-cell@*.service'})`);
|
|
168
|
+
} else if (dryRun) {
|
|
169
|
+
actions.push('DRY-RUN fleet companion generato, NON installato');
|
|
170
|
+
} else {
|
|
171
|
+
const fleetContent = generateFleetService({
|
|
172
|
+
platform,
|
|
173
|
+
nodeBin: svcCtx.nodeBin,
|
|
174
|
+
entryPath: path.join(svcCtx.repoRoot, 'bin', 'nexuscrew.js'),
|
|
175
|
+
repoRoot: svcCtx.repoRoot,
|
|
176
|
+
home,
|
|
177
|
+
});
|
|
178
|
+
const fr = installFleetService(
|
|
179
|
+
platform, fleetContent,
|
|
180
|
+
{ home, uid: uid(), installPath: opts.fleetInstallPath },
|
|
181
|
+
{ execImpl: opts.execImpl },
|
|
182
|
+
);
|
|
183
|
+
if (fr.failures && fr.failures.length) {
|
|
184
|
+
// file installato MA activation (systemctl/launchctl) fallita [M1]
|
|
185
|
+
actions.push(`WARN: fleet companion file installato ${fr.target} MA activation fallita: ${fr.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
|
|
186
|
+
} else {
|
|
187
|
+
actions.push(`fleet companion installed ${fr.target} (mode 0${fleetFileMode(platform).toString(8)})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// qualunque errore del companion: WARN + init principale prosegue intatto.
|
|
194
|
+
actions.push(`WARN: fleet companion fallito: ${e.message} (init principale prosegue)`);
|
|
195
|
+
}
|
|
196
|
+
|
|
130
197
|
// Termux:boot best-effort detection (R4)
|
|
131
198
|
if (platform === 'termux') {
|
|
132
199
|
const bootDir = path.join(home, '.termux', 'boot');
|
package/lib/files/routes.js
CHANGED
|
@@ -23,7 +23,10 @@ function filesRoutes({ cfg, sessionExists, paste }) {
|
|
|
23
23
|
return res.status(404).json({ error: 'sessione tmux inesistente' });
|
|
24
24
|
}
|
|
25
25
|
const saved = store.saveUpload(cfg.filesRoot, session, req.file.buffer, req.file.originalname);
|
|
26
|
-
|
|
26
|
+
// paste=false (tasto allegati del composer): il client appende il path al
|
|
27
|
+
// testo del composer — niente scrittura PTY. Default = incolla (FilesPanel).
|
|
28
|
+
const wantPaste = String((req.body && req.body.paste) || '') !== 'false';
|
|
29
|
+
const pasted = wantPaste ? await paste(session, saved.path) : false;
|
|
27
30
|
res.json({ ...saved, pasted });
|
|
28
31
|
});
|
|
29
32
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// B4.3 — Service companion boot. bootCells(fleet, {log}) avvia tutte le celle
|
|
3
|
+
// boot:true di un fleet provider (builtin) chiamandone up(). Una cella gia' in
|
|
4
|
+
// esecuzione (up -> httpError 409 / "duplicate session") = SKIP non fatale;
|
|
5
|
+
// ogni altro errore viene raccolto in failed[] e NON ferma le altre celle.
|
|
6
|
+
// Nessun side-effect all'import. Design §4c / §9b.
|
|
7
|
+
//
|
|
8
|
+
// Agnostico: usa solo il contratto fleet {available, status(), up()} — non legge
|
|
9
|
+
// internals delle definizioni. La lista delle celle boot:true arriva da status()
|
|
10
|
+
// (campo `boot` derivato dalle definitions), cosi' un fleet esterno compatibile
|
|
11
|
+
// potra' riusare la stessa funzione.
|
|
12
|
+
|
|
13
|
+
const SKIP_STATUS = 409; // up() del builtin lancia httpError(409) su sessione duplicata
|
|
14
|
+
|
|
15
|
+
async function bootCells(fleet, { log = () => {} } = {}) {
|
|
16
|
+
const started = [];
|
|
17
|
+
const skipped = [];
|
|
18
|
+
const failed = [];
|
|
19
|
+
|
|
20
|
+
if (!fleet || fleet.available === false) {
|
|
21
|
+
// provider non disponibile: niente da avviare. Il caller (runFleetBoot)
|
|
22
|
+
// filtra gia' mode==='builtin' (= fleet.available); qui e' solo defense-in-depth.
|
|
23
|
+
return { started, skipped, failed };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let cells = [];
|
|
27
|
+
try {
|
|
28
|
+
const st = await fleet.status();
|
|
29
|
+
cells = (st && Array.isArray(st.cells)) ? st.cells : [];
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// status e' una lettura: se fallisce non possiamo sapere cosa avviare.
|
|
32
|
+
// Riportiamo come fallimento globale senza processare alcuna cella.
|
|
33
|
+
failed.push({ cell: '*', reason: `status() failed: ${e && e.message ? e.message : e}` });
|
|
34
|
+
return { started, skipped, failed };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const c of cells) {
|
|
38
|
+
if (!c || !c.boot) continue; // solo boot:true
|
|
39
|
+
const id = c.cell;
|
|
40
|
+
try {
|
|
41
|
+
await fleet.up(id);
|
|
42
|
+
started.push(id);
|
|
43
|
+
log(`fleet-boot: started ${id}`);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
const status = e && typeof e.status === 'number' ? e.status : null;
|
|
46
|
+
const reason = (e && e.message) ? e.message : String(e);
|
|
47
|
+
// gia' in esecuzione: skip non fatale (contratto builtin: 409/duplicate).
|
|
48
|
+
// Il regex copre anche fleet esterni che lanciano un Error generico.
|
|
49
|
+
if (status === SKIP_STATUS || /duplicate session|già in esecuzione/i.test(reason)) {
|
|
50
|
+
skipped.push(id);
|
|
51
|
+
log(`fleet-boot: skipped ${id} (already running)`);
|
|
52
|
+
} else {
|
|
53
|
+
failed.push({ cell: id, reason });
|
|
54
|
+
log(`fleet-boot: failed ${id} — ${reason}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { started, skipped, failed };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { bootCells };
|