@mmmbuto/nexuscrew 0.8.34 → 0.8.36

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.
@@ -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-cNTOIj7e.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-CrggFFKZ.css">
14
+ <script type="module" crossorigin src="/assets/index-DTYcphMi.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-5PMZjYKR.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"0.8.34"}
1
+ {"version":"0.8.36"}
@@ -24,6 +24,7 @@ const path = require('node:path');
24
24
  const { execFileSync } = require('node:child_process');
25
25
  const {
26
26
  escapeSystemdPath, escapeSystemdExec, escapeXml, shellQuote, assertSystemdSafe,
27
+ reinstallLaunchAgent,
27
28
  } = require('./service.js');
28
29
  const { uid } = require('./platform.js');
29
30
 
@@ -237,7 +238,13 @@ function fleetInstallCommands(platform, target, ctx) {
237
238
  // Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
238
239
  // default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
239
240
  // e' PRESERVATO e le failure raccolte per diagnosi (M1: non si ingoia, non si rollback).
240
- function installFleetService(platform, content, ctx, { dryRun = false, execImpl = execFileSync } = {}) {
241
+ function installFleetService(platform, content, ctx, {
242
+ dryRun = false,
243
+ execImpl = execFileSync,
244
+ sleepImpl,
245
+ launchdWaitAttempts,
246
+ launchdWaitMs,
247
+ } = {}) {
241
248
  const home = ctx.home || os.homedir();
242
249
  const target = ctx.installPath || fleetInstallPath(platform, home);
243
250
  const mode = fleetFileMode(platform);
@@ -268,13 +275,21 @@ function installFleetService(platform, content, ctx, { dryRun = false, execImpl
268
275
  }
269
276
 
270
277
  // exec service manager — raccogli failure per diagnosi (NON ingoiare, M1)
271
- const cmds = fleetInstallCommands(platform, target, ctx);
272
278
  const failures = [];
273
- for (const [bin, args] of cmds) {
274
- try { execImpl(bin, args, { stdio: 'ignore' }); }
275
- catch (e) {
276
- if (platform === 'mac' && bin === 'launchctl' && args[0] === 'bootout') continue;
277
- failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message });
279
+ if (platform === 'mac') {
280
+ failures.push(...reinstallLaunchAgent(target, 'com.mmmbuto.nexuscrew-fleet', ctx, {
281
+ execImpl,
282
+ sleepImpl,
283
+ waitAttempts: launchdWaitAttempts,
284
+ waitMs: launchdWaitMs,
285
+ }));
286
+ } else {
287
+ const cmds = fleetInstallCommands(platform, target, ctx);
288
+ for (const [bin, args] of cmds) {
289
+ try { execImpl(bin, args, { stdio: 'ignore' }); }
290
+ catch (e) {
291
+ failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message });
292
+ }
278
293
  }
279
294
  }
280
295
 
@@ -8,6 +8,10 @@ const crypto = require('node:crypto');
8
8
  const { execFileSync } = require('node:child_process');
9
9
  const { detectPlatform, uid } = require('./platform.js');
10
10
 
11
+ const LAUNCHD_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
12
+ const DEFAULT_LAUNCHD_WAIT_ATTEMPTS = 100;
13
+ const DEFAULT_LAUNCHD_WAIT_MS = 50;
14
+
11
15
  // --- Escaping helpers ---
12
16
 
13
17
  // systemd WorkingDirectory: escape % (percent specifier). Spazi ok.
@@ -220,12 +224,91 @@ function ensureLinuxTmuxSurvival(opts = {}) {
220
224
  return { target, written, killMode: effective };
221
225
  }
222
226
 
227
+ function sleepSync(ms) {
228
+ if (ms > 0) Atomics.wait(LAUNCHD_SLEEP_BUFFER, 0, 0, ms);
229
+ }
230
+
231
+ function normalizedWaitAttempts(value) {
232
+ const parsed = Number(value);
233
+ return Number.isFinite(parsed) && parsed >= 1
234
+ ? Math.floor(parsed)
235
+ : DEFAULT_LAUNCHD_WAIT_ATTEMPTS;
236
+ }
237
+
238
+ function normalizedWaitMs(value) {
239
+ const parsed = Number(value);
240
+ return Number.isFinite(parsed) && parsed >= 0
241
+ ? parsed
242
+ : DEFAULT_LAUNCHD_WAIT_MS;
243
+ }
244
+
245
+ // `launchctl bootout` can return while a running KeepAlive job is still being
246
+ // removed. An immediate bootstrap then fails and leaves the service unloaded.
247
+ // Poll the launchd namespace with a bounded wait before loading the new plist.
248
+ function waitForLaunchdUnload(label, opts = {}) {
249
+ const execImpl = opts.execImpl || execFileSync;
250
+ const sleepImpl = opts.sleepImpl || sleepSync;
251
+ const attempts = normalizedWaitAttempts(opts.attempts);
252
+ const delayMs = normalizedWaitMs(opts.delayMs);
253
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
254
+ try {
255
+ execImpl('launchctl', ['print', label], { stdio: 'ignore' });
256
+ } catch (_) {
257
+ return true;
258
+ }
259
+ if (attempt + 1 < attempts) sleepImpl(delayMs);
260
+ }
261
+ return false;
262
+ }
263
+
264
+ function reinstallLaunchAgent(target, serviceName, ctx, opts = {}) {
265
+ const execImpl = opts.execImpl || execFileSync;
266
+ const domain = `gui/${ctx.uid || uid()}`;
267
+ const label = `${domain}/${serviceName}`;
268
+ const waitAttempts = normalizedWaitAttempts(opts.waitAttempts);
269
+ const waitMs = normalizedWaitMs(opts.waitMs);
270
+ let bootoutSucceeded = false;
271
+ try {
272
+ execImpl('launchctl', ['bootout', label], { stdio: 'ignore' });
273
+ bootoutSucceeded = true;
274
+ } catch (_) {
275
+ // Idempotent when the job is not loaded; bootstrap below remains authoritative.
276
+ }
277
+ if (bootoutSucceeded && !waitForLaunchdUnload(label, {
278
+ execImpl,
279
+ sleepImpl: opts.sleepImpl,
280
+ attempts: waitAttempts,
281
+ delayMs: waitMs,
282
+ })) {
283
+ return [{
284
+ cmd: `launchctl print ${label}`,
285
+ error: `launchd job still loaded after ${waitAttempts} checks following successful bootout`,
286
+ }];
287
+ }
288
+ try {
289
+ execImpl('launchctl', ['bootstrap', domain, target], { stdio: 'ignore' });
290
+ return [];
291
+ } catch (error) {
292
+ return [{
293
+ cmd: `launchctl bootstrap ${domain} ${target}`,
294
+ error: error.message,
295
+ }];
296
+ }
297
+ }
298
+
223
299
  // Install no-symlink + atomic rename. [M3]
224
300
  // - lstat target: reject symlink (no follow)
225
301
  // - write temp file nella stessa dir -> chmod mode -> atomic rename
226
302
  // - exec service manager (systemctl/launchctl); skip in dryRun
227
303
  // execImpl per test (default execFileSync).
228
- function installService(platform, content, ctx, { dryRun = false, execImpl = execFileSync } = {}) {
304
+ function installService(platform, content, ctx, {
305
+ dryRun = false,
306
+ execImpl = execFileSync,
307
+ sleepImpl,
308
+ launchdWaitAttempts,
309
+ launchdWaitMs,
310
+ activate = true,
311
+ } = {}) {
229
312
  const home = ctx.home || os.homedir();
230
313
  const target = ctx.installPath || installPath(platform, home);
231
314
  const mode = fileMode(platform);
@@ -263,13 +346,29 @@ function installService(platform, content, ctx, { dryRun = false, execImpl = exe
263
346
 
264
347
  // exec service manager — NON ingoiare failure (M1): raccogli per diagnosi
265
348
  const cmds = installCommands(platform, target, ctx);
349
+ if (!activate) {
350
+ return {
351
+ target,
352
+ mode,
353
+ written: true,
354
+ failures: [],
355
+ skippedActivation: cmds.map(([bin, args]) => `${bin} ${args.join(' ')}`),
356
+ };
357
+ }
266
358
  const failures = [];
267
- for (const [bin, args] of cmds) {
268
- try { execImpl(bin, args, { stdio: 'ignore' }); }
269
- catch (e) {
270
- // bootout e' idempotente: un job non ancora caricato non e' un errore di installazione.
271
- if (platform === 'mac' && bin === 'launchctl' && args[0] === 'bootout') continue;
272
- failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message });
359
+ if (platform === 'mac') {
360
+ failures.push(...reinstallLaunchAgent(target, 'com.mmmbuto.nexuscrew', ctx, {
361
+ execImpl,
362
+ sleepImpl,
363
+ waitAttempts: launchdWaitAttempts,
364
+ waitMs: launchdWaitMs,
365
+ }));
366
+ } else {
367
+ for (const [bin, args] of cmds) {
368
+ try { execImpl(bin, args, { stdio: 'ignore' }); }
369
+ catch (e) {
370
+ failures.push({ cmd: `${bin} ${args.join(' ')}`, error: e.message });
371
+ }
273
372
  }
274
373
  }
275
374
 
@@ -303,6 +402,7 @@ module.exports = {
303
402
  generateService, generateLinux, generateMac, generateTermux,
304
403
  installService, installPath, installCommands, fileMode,
305
404
  ensureLinuxTmuxSurvival, linuxTmuxSurvivalPath, LINUX_TMUX_SURVIVAL_DROPIN,
405
+ reinstallLaunchAgent, waitForLaunchdUnload,
306
406
  escapeSystemdPath, escapeSystemdExec, escapeXml, shellQuote,
307
407
  assertSystemdSafe, SYSTEMD_FORBIDDEN,
308
408
  };
@@ -749,9 +749,9 @@ function settingsRoutes(deps = {}) {
749
749
 
750
750
  // --- POST /service/regenerate — rigenera l'unit service --------------------
751
751
  // Riusa generateService/installService (lib/cli/service.js: escaping per-platform,
752
- // no-symlink, tmp+rename atomico). L'execImpl e' un NO-OP che registra i comandi
753
- // di attivazione SENZA eseguirli: il contratto B2 vieta il restart automatico
754
- // dall'API (la UI avvisa di riavviare a mano).
752
+ // no-symlink, tmp+rename atomico). activate:false registra il piano di attivazione
753
+ // SENZA eseguirlo: il contratto B2 vieta il restart automatico dall'API
754
+ // (la UI avvisa di riavviare a mano).
755
755
  r.post('/service/regenerate', mutGate, (_req, res) => {
756
756
  try {
757
757
  if (!bootState({ platform, execImpl: seams.execImpl, uid: seams.uid, home }).enabled) {
@@ -767,14 +767,12 @@ function settingsRoutes(deps = {}) {
767
767
  installPath: seams.serviceInstallPath,
768
768
  };
769
769
  const content = generateService(platform, ctx);
770
- const skipped = [];
771
- const noExec = (bin, args) => { skipped.push(`${bin} ${(args || []).join(' ')}`); };
772
- const out = installService(platform, content, ctx, { execImpl: noExec });
770
+ const out = installService(platform, content, ctx, { activate: false });
773
771
  send(res, 200, {
774
772
  regenerated: true,
775
773
  target: out.target,
776
774
  note: 'unit rigenerata; nessun restart automatico — riavvia il service per applicarla',
777
- skippedActivation: skipped,
775
+ skippedActivation: out.skippedActivation,
778
776
  });
779
777
  } catch (e) { send(res, 500, { error: String(e.message || e) }); }
780
778
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.34",
3
+ "version": "0.8.36",
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": {