@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.
- package/CHANGELOG.md +40 -0
- package/README.md +29 -1
- package/frontend/dist/assets/{index-CrggFFKZ.css → index-5PMZjYKR.css} +1 -1
- package/frontend/dist/assets/index-DTYcphMi.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/fleet-service.js +22 -7
- package/lib/cli/service.js +107 -7
- package/lib/settings/routes.js +5 -7
- package/package.json +1 -1
- package/frontend/dist/assets/index-cNTOIj7e.js +0 -93
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-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.
|
|
1
|
+
{"version":"0.8.36"}
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
package/lib/cli/service.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
};
|
package/lib/settings/routes.js
CHANGED
|
@@ -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).
|
|
753
|
-
//
|
|
754
|
-
//
|
|
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
|
|
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:
|
|
775
|
+
skippedActivation: out.skippedActivation,
|
|
778
776
|
});
|
|
779
777
|
} catch (e) { send(res, 500, { error: String(e.message || e) }); }
|
|
780
778
|
});
|