@mmmbuto/nexuscrew 0.9.6 → 0.9.8

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.
@@ -10,6 +10,219 @@ const {
10
10
  stableRuntimeDir,
11
11
  } = require('./core.js');
12
12
 
13
+ // R28 — rigenerazione delle definizioni di boot dopo l'installazione.
14
+ //
15
+ // L'aggiornamento del pacchetto sposta i path versionati che le unit/plist
16
+ // scrivono LETTERALMENTE (node del Cellar su Homebrew, pacchetto in
17
+ // lib/node_modules): senza questo passo il servizio riparte sulle definizioni
18
+ // vecchie e la companion di boot — che nessun altro percorso sana, se non
19
+ // `nexuscrew init` — resta puntata a un binario che non esiste più, con exit
20
+ // 78 EX_CONFIG e le celle boot:true mute (misurato sul campo 2026-08-18).
21
+ //
22
+ // Il criterio R23 resta intatto («alias stabile che realpath allo stesso
23
+ // file», verificato alla SCRITTURA, mai a ogni avvio) con una estensione
24
+ // dichiarata per il caso del runner: il processo dell'update può girare con
25
+ // un node già unlinkato su disco, e in quel caso si sceglie il primo percorso
26
+ // stabile VIVO (resolveLiveBootPaths).
27
+ //
28
+ // Contratto: sana SOLO le definizioni che ESISTONO già (il boot è opt-in:
29
+ // l'update non installa un companion mai chiesto); attivazione differita
30
+ // (activate:false) perché il companion RunAtLoad/oneshot non va eseguito a
31
+ // metà update — R28-rimedio: l'attivazione la APPLICA healBootDefinitions
32
+ // nel percorso di update (daemon-reload / bootout+bootstrap), non un restart
33
+ // che non carica le definizioni; un fallimento NON blocca l'aggiornamento
34
+ // (il comportamento senza questo passo era proprio quello) ma viene LOGGATO
35
+ // e riportato: il silenzio era il difetto, non il fallimento.
36
+ function regenBootDefinitions(opts = {}) {
37
+ const readFileImpl = opts.readFileImpl || fs.readFileSync;
38
+ const serviceMod = opts.serviceMod || require('../cli/service.js');
39
+ const fleetMod = opts.fleetMod || require('../cli/fleet-service.js');
40
+ const aliasMod = opts.aliasMod || require('../cli/stable-alias.js');
41
+ const platformMod = opts.platformMod || require('../cli/platform.js');
42
+ const home = opts.home || os.homedir();
43
+ const platform = opts.platform || platformMod.detectPlatform();
44
+ const log = opts.log || console.log;
45
+ const out = { regenerated: [], skipped: [], warnings: [], errors: [] };
46
+
47
+ const revive = aliasMod.resolveLiveBootPaths({
48
+ nodeBin: platformMod.nodeBin(),
49
+ entryPath: path.resolve(__dirname, '..', '..', 'bin', 'nexuscrew.js'),
50
+ ...(opts.realpath ? { realpath: opts.realpath } : {}),
51
+ ...(opts.exists ? { exists: opts.exists } : {}),
52
+ });
53
+ out.warnings.push(...revive.warnings);
54
+
55
+ const targets = [
56
+ {
57
+ component: 'service',
58
+ path: () => serviceMod.installPath(platform, home),
59
+ isOurs: (content) => serviceMod.isOurService(platform, content),
60
+ exists: () => {
61
+ try { return fs.lstatSync(serviceMod.installPath(platform, home)).isFile(); } catch (_) { return false; }
62
+ },
63
+ install: () => {
64
+ const content = serviceMod.generateService(platform, {
65
+ repoRoot: platformMod.repoRoot(),
66
+ nodeBin: revive.nodeBin,
67
+ entryPath: revive.entryPath,
68
+ port: opts.port,
69
+ home,
70
+ uid: platformMod.uid(),
71
+ });
72
+ return serviceMod.installService(platform, content, {
73
+ repoRoot: platformMod.repoRoot(),
74
+ nodeBin: revive.nodeBin,
75
+ entryPath: revive.entryPath,
76
+ port: opts.port,
77
+ home,
78
+ uid: platformMod.uid(),
79
+ }, { activate: false });
80
+ },
81
+ },
82
+ {
83
+ component: 'fleet-companion',
84
+ path: () => fleetMod.fleetInstallPath(platform, home),
85
+ isOurs: (content) => fleetMod.isOurFleetService(platform, content),
86
+ exists: () => {
87
+ try { return fs.lstatSync(fleetMod.fleetInstallPath(platform, home)).isFile(); } catch (_) { return false; }
88
+ },
89
+ install: () => fleetMod.installFleetService(platform, fleetMod.generateFleetService({
90
+ platform,
91
+ nodeBin: revive.nodeBin,
92
+ entryPath: revive.entryPath,
93
+ repoRoot: platformMod.repoRoot(),
94
+ home,
95
+ }), { home, uid: platformMod.uid() }, { activate: false }),
96
+ },
97
+ ];
98
+
99
+ for (const target of targets) {
100
+ let present;
101
+ try { present = target.exists(); } catch (e) { present = false; }
102
+ if (!present) {
103
+ out.skipped.push(`${target.component}: nessuna definizione installata, nulla da sanare`);
104
+ continue;
105
+ }
106
+ // R28-rimedio, difetto 3 (audit): symlink e directory al posto del target
107
+ // erano già saltati, ma un FILE REGOLARE DI TERZI veniva sovrascritto. Le
108
+ // definizioni che generiamo portano tutte il nome del progetto nell'header
109
+ // (systemd: «# NexusCrew service», plist: Label com.mmmbuto.nexuscrew):
110
+ // se il contenuto non lo contiene, il file non è nostro e non lo tocchiamo
111
+ // — lo skip è dichiarato, non silenzioso. Un file illeggibile non blocca
112
+ // la sanazione (si presume nostro: il caso terzi è il contenuto leggibile
113
+ // che dichiara altro).
114
+ // R28 (audit rev2): la proprieta' si riconosce da un'ancora STRUTTURALE del
115
+ // nostro generatore (isOurService/isOurFleetService), non dalla parola
116
+ // «nexuscrew» presente nel file: un unit di terzi che ci nomina in un
117
+ // commento veniva sovrascritto. E cio' che NON si riesce a leggere non si
118
+ // presume nostro: si salta e si dichiara — l'unica direzione sicura, perche'
119
+ // l'errore costa una sanazione mancata invece del file di qualcun altro.
120
+ let content = null;
121
+ let why = null;
122
+ try { content = readFileImpl(target.path(), 'utf8'); }
123
+ catch (e) {
124
+ why = `${target.component}: definizione esistente non leggibile (${target.path()}, ${(e && e.code) || 'errore'}) — non identificabile, non la tocco; rilancia nexuscrew init`;
125
+ }
126
+ if (!why && !target.isOurs(content)) {
127
+ why = `${target.component}: file esistente non nostro al target (${target.path()}) — nessuna ancora delle nostre definizioni, possibile file di terzi, non lo tocco`;
128
+ }
129
+ if (why) {
130
+ out.skipped.push(why);
131
+ log(`WARN boot definitions: ${why}`);
132
+ continue;
133
+ }
134
+ try {
135
+ const result = target.install();
136
+ out.regenerated.push(target.component);
137
+ out.warnings.push(...(Array.isArray(result.skippedActivation)
138
+ ? result.skippedActivation.map((cmd) => `${target.component}: attivazione differita (${cmd}) — il passo di attivazione dell'update la applica`)
139
+ : []));
140
+ log(`boot definitions: ${target.component} rigenerata su ${result.target}`);
141
+ } catch (e) {
142
+ out.errors.push(`${target.component}: ${String((e && e.message) || e)}`);
143
+ log(`WARN boot definitions: rigenerazione di ${target.component} fallita: ${(e && e.message) || e} (aggiornamento proseguito; rilancia nexuscrew init)`);
144
+ }
145
+ }
146
+ for (const w of out.warnings) log(`WARN boot definitions: ${w}`);
147
+ return out;
148
+ }
149
+
150
+ // R28-rimedio, difetto 2 (audit): l'attivazione differita dichiarata dalla
151
+ // regen era una promessa FALSA in silenzio. `restart` fa `systemctl --user
152
+ // restart` SENZA daemon-reload (systemd riavvia con la definizione VECCHIA
153
+ // in memoria) e `launchctl kickstart` riusa la definizione già caricata; col
154
+ // runtime spento non c'è restart affatto. L'attivazione va APPLICATA qui,
155
+ // nel percorso di update, subito dopo la regen. È UN SOLO CERVELLO per
156
+ // «reinstalla ⇒ sana le definizioni ⇒ rendile note al service manager»:
157
+ // la usa il runner dell'auto-update e la usa `update()` manuale (difetto 1:
158
+ // la via manuale bypassava la sanazione).
159
+ //
160
+ // Cosa applica, per piattaforma, dopo la regen:
161
+ // - linux: `systemctl --user daemon-reload` — rende note le unit riscritte
162
+ // SENZA avviare nulla (il companion oneshot resta fermo; il restart
163
+ // dell'update carica davvero la definizione nuova).
164
+ // - mac, runtime attivo: bootout+bootstrap del principale (RunAtLoad lo
165
+ // riparte subito sul nuovo codice: coincide col restart che l'update farebbe
166
+ // comunque) e POI della companion (le celle boot:true partono col servizio
167
+ // già nuovo). Il kickstart del restart successivo è ridondante e innocuo.
168
+ // - mac, runtime SPENTO: NESSUN bootstrap — non si accende ciò che l'utente
169
+ // ha spento, e start/restart non caricano definizioni (kickstart). Limite
170
+ // DICHIARATO in activation.skipped: launchd prende le definizioni scritte
171
+ // al prossimo boot della macchina o con `nexuscrew init`.
172
+ // - termux: nessun service manager — skipped dichiarato.
173
+ //
174
+ // Best-effort come la regen: un fallimento non blocca l'aggiornamento, viene
175
+ // loggato e riportato in activation.errors.
176
+ function healBootDefinitions(opts = {}) {
177
+ const platformMod = opts.platformMod || require('../cli/platform.js');
178
+ const platform = opts.platform || platformMod.detectPlatform();
179
+ const home = opts.home || os.homedir();
180
+ const execImpl = opts.execImpl || execFileSync;
181
+ const log = opts.log || console.log;
182
+ const regen = (opts.regenImpl || regenBootDefinitions)({ ...opts });
183
+ const out = { ...regen, activation: { applied: [], skipped: [], errors: [] } };
184
+ if (!Array.isArray(regen.regenerated) || regen.regenerated.length === 0) {
185
+ out.activation.skipped.push('nessuna definizione rigenerata: nulla da attivare');
186
+ return out;
187
+ }
188
+ const run = (bin, args) => {
189
+ const cmd = `${bin} ${args.join(' ')}`;
190
+ try { execImpl(bin, args, { stdio: 'ignore' }); out.activation.applied.push(cmd); }
191
+ catch (e) { out.activation.errors.push(`${cmd}: ${(e && e.message) || e}`); }
192
+ };
193
+ if (platform === 'linux') {
194
+ run('systemctl', ['--user', 'daemon-reload']);
195
+ } else if (platform === 'mac') {
196
+ if (opts.running !== true) {
197
+ const limit = 'runtime spento: definizioni scritte e NON attivate — launchd le carica al prossimo boot della macchina o con nexuscrew init (start/restart usano kickstart sulla definizione già in memoria)';
198
+ out.activation.skipped.push(limit);
199
+ out.warnings.push(`attivazione non applicata: ${limit}`);
200
+ } else {
201
+ const uid = opts.uid !== undefined ? opts.uid : platformMod.uid();
202
+ const domain = `gui/${uid}`;
203
+ const serviceMod = opts.serviceMod || require('../cli/service.js');
204
+ const fleetMod = opts.fleetMod || require('../cli/fleet-service.js');
205
+ const boot = (label, target) => {
206
+ // bootout best-effort: non caricato è l'esito atteso del primo giro
207
+ try { execImpl('launchctl', ['bootout', `${domain}/${label}`], { stdio: 'ignore' }); } catch (_) {}
208
+ run('launchctl', ['bootstrap', domain, target]);
209
+ };
210
+ if (regen.regenerated.includes('service')) {
211
+ boot('com.mmmbuto.nexuscrew', serviceMod.installPath(platform, home));
212
+ }
213
+ if (regen.regenerated.includes('fleet-companion')) {
214
+ boot('com.mmmbuto.nexuscrew-fleet', fleetMod.fleetInstallPath(platform, home));
215
+ }
216
+ }
217
+ } else {
218
+ out.activation.skipped.push(`platform ${platform}: nessun service manager, attivazione non applicabile`);
219
+ }
220
+ for (const a of out.activation.applied) log(`boot definitions: attivazione applicata (${a})`);
221
+ for (const s of out.activation.skipped) log(`WARN boot definitions: ${s}`);
222
+ for (const e of out.activation.errors) log(`WARN boot definitions: attivazione fallita: ${e}`);
223
+ return out;
224
+ }
225
+
13
226
  function args(argv) {
14
227
  const out = {};
15
228
  for (let i = 0; i < argv.length; i += 2) {
@@ -132,6 +345,16 @@ async function runUpdate(opts = {}) {
132
345
  const previous = readState(statusPath);
133
346
  const previousVersion = String(readInstalledVersion() || '');
134
347
  let installedNew = false;
348
+ // wasRunning serve solo a mac (decide se bootout+bootstrap è lecito: col
349
+ // runtime spento non si accende nulla): su linux daemon-reload non esegue
350
+ // servizi e non serve saperlo. Calcolato PRIMA del try perché vale per il
351
+ // percorso di aggiornamento e per quello di rollback.
352
+ const regenPlatform = opts.regenSeams && opts.regenSeams.platformMod
353
+ ? opts.regenSeams.platformMod.detectPlatform()
354
+ : require('../cli/platform.js').detectPlatform();
355
+ const wasRunning = regenPlatform === 'mac'
356
+ ? ((opts.isRunningImpl || ((o) => require('../cli/commands.js').isServiceRunning(o)))({ home }))
357
+ : undefined;
135
358
  try {
136
359
  writeState(statusPath, { ...previous, phase: 'installing', targetVersion: version, updaterPid: process.pid, lastError: '' });
137
360
  execImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${version}`, '--no-audit', '--no-fund'], {
@@ -142,12 +365,23 @@ async function runUpdate(opts = {}) {
142
365
  installedNew = true;
143
366
  await preflightImpl({ version, home });
144
367
  writeState(statusPath, { ...readState(statusPath), phase: 'restarting', updaterPid: process.pid, lastError: '' });
368
+ // R28: sana le definizioni di boot PRIMA del riavvio — è il passo che
369
+ // manca al percorso di upgrade e che lasciava la companion puntata a un
370
+ // binario morto (exit 78, silenzio). R28-rimedio: la sanazione ora
371
+ // APPLICA anche l'attivazione differita (healBootDefinitions) — il
372
+ // restart da solo non caricava le definizioni riscritte. Best-effort
373
+ // DICHIARATO: un errore qui non blocca l'aggiornamento, viene loggato e
374
+ // riportato nel risultato.
375
+ const bootDefinitions = (opts.healBootImpl || healBootDefinitions)({
376
+ home, running: wasRunning, execImpl, ...(opts.regenSeams || {}),
377
+ ...(opts.regenBootImpl ? { regenImpl: opts.regenBootImpl } : {}),
378
+ });
145
379
  const restartMode = await restartImpl({ home, ...(opts.runtimeSeams || {}) });
146
380
  writeState(statusPath, {
147
381
  ...readState(statusPath), phase: 'installed', current: version, latest: version,
148
382
  available: false, blockedVersion: '', lastUpdatedAt: new Date().toISOString(), lastError: '',
149
383
  });
150
- return { updated: true, version, restartMode };
384
+ return { updated: true, version, restartMode, bootDefinitions };
151
385
  } catch (e) {
152
386
  let rollbackError = null; let rolledBack = false;
153
387
  if (installedNew && parseVersion(previousVersion) && previousVersion !== version) {
@@ -157,6 +391,14 @@ async function runUpdate(opts = {}) {
157
391
  });
158
392
  if (String(readInstalledVersion() || '') !== previousVersion) throw new Error(`rollback verify: attesa ${previousVersion}`);
159
393
  await preflightImpl({ version: previousVersion, home, rollback: true });
394
+ // R28: il rollback riparte dalle stesse definizioni sane — il
395
+ // downgrade reinstalla il pacchetto nel prefix attuale e i path
396
+ // vivi valgono anche per la versione precedente. R28-rimedio:
397
+ // anche qui sanazione+attivazione (heal), non la sola regen.
398
+ (opts.healBootImpl || healBootDefinitions)({
399
+ home, running: wasRunning, execImpl, ...(opts.regenSeams || {}),
400
+ ...(opts.regenBootImpl ? { regenImpl: opts.regenBootImpl } : {}),
401
+ });
160
402
  await restartImpl({ home, ...(opts.runtimeSeams || {}) });
161
403
  rolledBack = true;
162
404
  } catch (rollbackFailure) { rollbackError = rollbackFailure; }
@@ -183,4 +425,4 @@ if (require.main === module) {
183
425
  .catch(() => { process.exitCode = 1; });
184
426
  }
185
427
 
186
- module.exports = { args, restartRuntime, runUpdate };
428
+ module.exports = { args, restartRuntime, runUpdate, regenBootDefinitions, healBootDefinitions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {