@mmmbuto/nexuscrew 0.8.10 → 0.8.11

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.
@@ -1,4 +1,4 @@
1
- .nc-icon{display:block;flex:0 0 auto}.nc-sheet-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-launch-overlay{z-index:70}.nc-sheet{width:100%;max-width:380px;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080}.nc-power-sheet{max-width:min(620px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow-y:auto;box-sizing:border-box;min-width:0}.nc-launch-sheet{max-width:min(520px,calc(100vw - 32px));width:100%;min-width:0}.nc-launch-fields{display:flex;flex-direction:column;gap:10px;min-width:0}.nc-launch-fields .nc-field,.nc-launch-fields .nc-field input,.nc-launch-fields .nc-field select{width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-sheet-head{display:flex;align-items:center;justify-content:space-between}.nc-sheet-head b{font-size:16px;color:#d8ffe8}.nc-sheet-state{font-size:12px;opacity:.6}.nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-power-config{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;padding:10px;border:1px solid #243;border-radius:8px;background:#0a0e0a;min-width:0;overflow-wrap:anywhere}.nc-power-config span{opacity:.7}.nc-power-config small{grid-column:1 / -1;opacity:.65}.nc-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.nc-check input{accent-color:#2e7d32}.nc-note{font-size:11px;color:#e0b020;opacity:.9}.nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.8}.nc-field input,.nc-field select{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-field input:focus,.nc-field select:focus{outline:none;border-color:#2e7d32}.nc-err{color:#f88;font-size:12px}.nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;min-width:0}.nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-btn.primary{background:#2e7d32;color:#fff}.nc-btn.primary:active{background:#255028}.nc-btn:disabled{opacity:.5;cursor:default}@media(max-width:600px){.nc-power-sheet{max-width:380px}}.nc-home{display:flex;flex-direction:column;gap:12px;padding:18px 14px 10px;color:#cfe;max-width:560px;margin:0 auto;height:100dvh;min-height:0;overflow:hidden;box-sizing:border-box}.nc-home-scroll{min-height:0;flex:1 1 auto;overflow-y:auto;overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;display:flex;flex-direction:column;gap:12px;padding:0 2px 76px 0}.nc-home-head{position:relative}.nc-wordmark{font-size:26px;font-weight:700;letter-spacing:.5px;color:#d8ffe8}.nc-cursor{display:inline-block;width:12px;height:22px;margin-left:6px;background:#0c6;vertical-align:-2px;animation:nc-blink 1.1s steps(1) infinite}@keyframes nc-blink{50%{opacity:0}}@media(prefers-reduced-motion:reduce){.nc-cursor{animation:none}}.nc-home-sub{margin-top:4px;font-size:13px;opacity:.65}.nc-head-actions{position:absolute;top:0;right:0;display:inline-flex;gap:6px}.nc-refresh{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:40px;min-height:40px}.nc-filter{padding:10px 12px;font:16px/1.2 inherit;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:8px}.nc-filter:focus{outline:none;border-color:#2e7d32}.nc-session-list{display:flex;flex-direction:column;gap:8px}.nc-session{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:10px;text-align:left;min-height:56px}.nc-session:active{background:#14200f;border-color:#2e7d32}.nc-session .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-session .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-session-main b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.nc-session-main small{opacity:.55;font-size:12px}.nc-badge{flex:0 0 auto;background:#2e7d32;color:#fff;border-radius:10px;padding:1px 8px;font-size:12px;line-height:18px}.nc-err{color:#f88;font-size:13px;padding:4px 2px}.nc-empty{opacity:.5;padding:18px 4px;font-size:13px}.nc-home-foot{margin-top:auto;padding-top:14px;display:flex;justify-content:space-between;gap:8px;font-size:11px;opacity:.4}.nc-group{display:flex;flex-direction:column;gap:8px}.nc-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:6px 2px 2px}.nc-mcard{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:12px;min-height:56px}.nc-mcard-main{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;background:none;border:none;color:inherit;cursor:pointer;padding:0;text-align:left}.nc-mcard-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-mcard-text b{font-size:14px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mcard-text small{font-size:12px;opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-group-title.nc-node-title{display:flex;align-items:center;gap:6px}.nc-group-title.nc-node-title .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#355}.nc-group-title.nc-node-title .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mcard .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mcard .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-rel{flex:0 0 auto;font-size:11px;opacity:.45}.nc-power,.nc-menu{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:38px;min-height:38px;font-size:17px;cursor:pointer}.nc-power:active,.nc-menu:active{border-color:#2e7d32}.nc-fab{position:fixed;right:18px;bottom:18px;z-index:20;width:54px;height:54px;border-radius:50%;background:#2e7d32;color:#fff;border:none;font-size:28px;line-height:1;cursor:pointer;box-shadow:0 6px 18px #00000073}.nc-fab:active{background:#255028}.nc-lang{display:inline-flex;align-items:center;gap:2px}.nc-act{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;flex:0 0 auto;font-size:15px;line-height:1}.nc-act.power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-act.power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-act.pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mcard{gap:8px}/**
1
+ .nc-icon{display:block;flex:0 0 auto}.nc-sheet-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-launch-overlay{z-index:70}.nc-sheet{width:100%;max-width:380px;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080}.nc-power-sheet{max-width:min(620px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow-y:auto;box-sizing:border-box;min-width:0}.nc-launch-sheet{max-width:min(520px,calc(100vw - 32px));width:100%;min-width:0}.nc-launch-fields{display:flex;flex-direction:column;gap:10px;min-width:0}.nc-launch-fields .nc-field,.nc-launch-fields .nc-field input,.nc-launch-fields .nc-field select{width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-sheet-head{display:flex;align-items:center;justify-content:space-between}.nc-sheet-head b{font-size:16px;color:#d8ffe8}.nc-sheet-state{font-size:12px;opacity:.6}.nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-power-config{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;padding:10px;border:1px solid #243;border-radius:8px;background:#0a0e0a;min-width:0;overflow-wrap:anywhere}.nc-power-config span{opacity:.7}.nc-power-config small{grid-column:1 / -1;opacity:.65}.nc-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.nc-check input{accent-color:#2e7d32}.nc-note{font-size:11px;color:#e0b020;opacity:.9}.nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.8}.nc-field input,.nc-field select{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-field input:focus,.nc-field select:focus{outline:none;border-color:#2e7d32}.nc-err{color:#f88;font-size:12px}.nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;min-width:0}.nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-btn.primary{background:#2e7d32;color:#fff}.nc-btn.primary:active{background:#255028}.nc-btn:disabled{opacity:.5;cursor:default}@media(max-width:600px){.nc-power-sheet{max-width:380px}}.nc-home{display:flex;flex-direction:column;gap:12px;padding:18px 14px 10px;color:#cfe;max-width:560px;margin:0 auto;height:100dvh;min-height:0;overflow:hidden;box-sizing:border-box}.nc-home-scroll{min-height:0;flex:1 1 auto;overflow-y:auto;overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;display:flex;flex-direction:column;gap:12px;padding:0 2px 76px 0}.nc-home-head{position:relative}.nc-wordmark{font-size:26px;font-weight:700;letter-spacing:.5px;color:#d8ffe8}.nc-cursor{display:inline-block;width:12px;height:22px;margin-left:6px;background:#0c6;vertical-align:-2px;animation:nc-blink 1.1s steps(1) infinite}@keyframes nc-blink{50%{opacity:0}}@media(prefers-reduced-motion:reduce){.nc-cursor{animation:none}}.nc-home-sub{margin-top:4px;font-size:13px;opacity:.65}.nc-head-actions{position:absolute;top:0;right:0;display:inline-flex;gap:6px}.nc-refresh{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:40px;min-height:40px}.nc-filter{padding:10px 12px;font:16px/1.2 inherit;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:8px}.nc-filter:focus{outline:none;border-color:#2e7d32}.nc-session-list{display:flex;flex-direction:column;gap:8px}.nc-session{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:10px;text-align:left;min-height:56px}.nc-session:active{background:#14200f;border-color:#2e7d32}.nc-session .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-session .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-session-main b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.nc-session-main small{opacity:.55;font-size:12px}.nc-badge{flex:0 0 auto;background:#2e7d32;color:#fff;border-radius:10px;padding:1px 8px;font-size:12px;line-height:18px}.nc-err{color:#f88;font-size:13px;padding:4px 2px}.nc-empty{opacity:.5;padding:18px 4px;font-size:13px}.nc-home-foot{margin-top:auto;padding:14px 4px 4px;width:100%;box-sizing:border-box;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;column-gap:14px;font-size:11px}.nc-home-meta{min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;opacity:.48;line-height:1.35}.nc-home-version{flex:0 0 auto}.nc-home-endpoint{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-group{display:flex;flex-direction:column;gap:8px}.nc-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:6px 2px 2px}.nc-mcard{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:12px;min-height:56px}.nc-mcard-main{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;background:none;border:none;color:inherit;cursor:pointer;padding:0;text-align:left}.nc-mcard-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-mcard-text b{font-size:14px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mcard-text small{font-size:12px;opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-group-title.nc-node-title{display:flex;align-items:center;gap:6px}.nc-group-title.nc-node-title .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#355}.nc-group-title.nc-node-title .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mcard .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mcard .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-rel{flex:0 0 auto;font-size:11px;opacity:.45}.nc-power,.nc-menu{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:38px;min-height:38px;font-size:17px;cursor:pointer}.nc-power:active,.nc-menu:active{border-color:#2e7d32}.nc-fab{position:fixed;right:18px;bottom:18px;z-index:20;width:54px;height:54px;border-radius:50%;background:#2e7d32;color:#fff;border:none;font-size:28px;line-height:1;cursor:pointer;box-shadow:0 6px 18px #00000073}.nc-fab:active{background:#255028}.nc-lang{display:inline-flex;align-items:center;justify-content:flex-end;gap:2px;white-space:nowrap;opacity:.7}@media(max-width:380px){.nc-home-meta{flex-direction:column;align-items:flex-start;gap:0}.nc-home-endpoint{max-width:100%}}.nc-act{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;flex:0 0 auto;font-size:15px;line-height:1}.nc-act.power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-act.power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-act.pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mcard{gap:8px}/**
2
2
  * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
3
  * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
4
  * https://github.com/chjj/term.js
@@ -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-CbUkgtAz.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-ChGJawuv.css">
14
+ <script type="module" crossorigin src="/assets/index-DoUIJ4GM.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DrrRAIg6.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"0.8.10"}
1
+ {"version":"0.8.11"}
@@ -8,7 +8,7 @@ const fs = require('node:fs');
8
8
  const path = require('node:path');
9
9
  const net = require('node:net');
10
10
  const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
11
- const { installPath: serviceInstallPath } = require('./service.js');
11
+ const { installPath: serviceInstallPath, ensureLinuxTmuxSurvival } = require('./service.js');
12
12
  const { fleetInstallPath } = require('./fleet-service.js');
13
13
  const pidf = require('./pidfile.js');
14
14
  const { runInit } = require('./init.js');
@@ -263,6 +263,20 @@ function stop(opts = {}) {
263
263
  const stoppedOwners = [];
264
264
  const errors = [];
265
265
 
266
+ // The shared tmux server can live in the NexusCrew systemd cgroup. If the
267
+ // effective KillMode cannot be proven safe, stop must not mutate *any*
268
+ // runtime owner or detached tunnel: callers can then fix the unit and retry
269
+ // without ending up in a partially stopped state.
270
+ if (before.managedRunning && platform === 'linux') {
271
+ try {
272
+ (opts.ensureTmuxSurvivalImpl || ensureLinuxTmuxSurvival)({ ...opts, home: opts.home, execImpl });
273
+ } catch (error) {
274
+ const reason = String(error.message || error);
275
+ log(`stop: annullato — protezione tmux non verificata: ${reason}`);
276
+ return { platform, owner: before.owner, stopped: false, stoppedOwners, reason };
277
+ }
278
+ }
279
+
266
280
  if (before.managedRunning) {
267
281
  try {
268
282
  if (platform === 'linux') {
@@ -477,15 +491,24 @@ function restart(opts = {}) {
477
491
  // conflict, first remove only the stray portable owner, then keep managed as
478
492
  // the single authority.
479
493
  if (before.managedRunning) {
494
+ if (platform === 'linux') {
495
+ try {
496
+ (opts.ensureTmuxSurvivalImpl || ensureLinuxTmuxSurvival)({ ...opts, home: opts.home, execImpl });
497
+ } catch (error) {
498
+ return { platform, owner: before.owner, runtimeOwner: 'managed', restarted: false, reason: String(error.message || error) };
499
+ }
500
+ }
480
501
  if (before.portableRunning) {
481
502
  const portable = (opts.stopPortableImpl || stopPortableRuntime)(opts);
482
503
  if (!portable.killed) return { platform, owner: before.owner, restarted: false, reason: portable.reason };
483
504
  }
484
505
  try {
485
506
  if (platform === 'linux') {
507
+ (opts.stopTunnelsImpl || stopManagedTunnels)(opts);
486
508
  execImpl('systemctl', ['--user', 'restart', 'nexuscrew'], { stdio: 'ignore' });
487
509
  log('restart: systemctl --user restart nexuscrew');
488
510
  } else {
511
+ (opts.stopTunnelsImpl || stopManagedTunnels)(opts);
489
512
  const label = `gui/${opts.uid || uid()}/com.mmmbuto.nexuscrew`;
490
513
  execImpl('launchctl', ['kickstart', '-k', label], { stdio: 'ignore' });
491
514
  log(`restart: launchctl kickstart -k ${label}`);
@@ -709,17 +732,34 @@ function url(opts = {}) {
709
732
  function tokenRotate(opts = {}) {
710
733
  const log = opts.log || console.log;
711
734
  const platform = opts.platform || detectPlatform();
735
+ const execImpl = opts.execImpl || execFileSync;
712
736
  const { tokenPath } = urlmod.resolvePaths(opts);
713
737
  const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
714
738
  if (readonly) {
715
739
  log('token rotate: READONLY, rotazione bloccata');
716
740
  return { rotated: false, reason: 'readonly' };
717
741
  }
742
+ const runtime = resolveRuntimeOwner({ ...opts, platform, execImpl });
743
+ const running = runtime.owner !== 'stopped';
744
+ if (runtime.managedRunning && platform === 'linux') {
745
+ try {
746
+ (opts.ensureTmuxSurvivalImpl || ensureLinuxTmuxSurvival)({ ...opts, home: opts.home, execImpl });
747
+ } catch (error) {
748
+ const reason = String(error.message || error);
749
+ log(`token rotate: annullata — protezione tmux non verificata: ${reason}`);
750
+ return { rotated: false, tokenWritten: false, restarted: false, running, reason };
751
+ }
752
+ }
718
753
  rotateToken(tokenPath); // atomico (tmp+rename, 0600, no-symlink); non stampa il token
719
754
  log(`token: nuovo segreto scritto (${tokenPath})`);
720
- const running = isServiceRunning({ ...opts, platform });
721
755
  if (running) {
722
- restart({ ...opts, platform, log });
756
+ const restarted = (opts.restartImpl || restart)({ ...opts, platform, execImpl, log });
757
+ if (!restarted || restarted.restarted !== true) {
758
+ const reason = (restarted && restarted.reason) || 'esito restart non verificato';
759
+ log(`token rotate: INCOMPLETA — nuovo token scritto ma restart fallito: ${reason}`);
760
+ log('token rotate: non e\' possibile confermare l\'invalidazione del vecchio token; correggi il servizio e ripeti il restart');
761
+ return { rotated: false, tokenWritten: true, restarted: false, running, reason };
762
+ }
723
763
  log('token rotate: servizio riavviato — vecchio token invalidato (401), nuovo attivo (200)');
724
764
  } else {
725
765
  // il service manager non lo vede, ma un `serve` manuale (pidfile) puo' essere
@@ -732,7 +772,7 @@ function tokenRotate(opts = {}) {
732
772
  }
733
773
  }
734
774
  log('usa `nexuscrew show token` per il nuovo URL (il token non si stampa qui)');
735
- return { rotated: true, running };
775
+ return { rotated: true, tokenWritten: true, restarted: running, running };
736
776
  }
737
777
 
738
778
  // logs [-f]: passthrough. linux -> journalctl --user -u nexuscrew; mac/termux -> logfile.
@@ -779,12 +819,17 @@ function update(opts = {}) {
779
819
  }
780
820
  const running = isServiceRunning({ ...opts, platform });
781
821
  if (running) {
782
- restart({ ...opts, platform, log });
822
+ const restarted = (opts.restartImpl || restart)({ ...opts, platform, log });
823
+ if (!restarted || restarted.restarted !== true) {
824
+ const reason = (restarted && restarted.reason) || 'esito restart non verificato';
825
+ log(`update: pacchetto installato ma restart fallito — ${reason}`);
826
+ return { updated: true, running, restarted: false, reason, code: 1 };
827
+ }
783
828
  log('update: servizio riavviato sul nuovo codice');
784
829
  } else {
785
830
  log('update: servizio non attivo (nessun restart)');
786
831
  }
787
- return { updated: true, running, code: 0 };
832
+ return { updated: true, running, restarted: running, code: 0 };
788
833
  }
789
834
 
790
835
  // B4.3 — fleet-boot companion: avvia le celle boot:true del provider selezionato.
package/lib/cli/doctor.js CHANGED
@@ -84,6 +84,24 @@ function checkBoot(platform, home, execImpl) {
84
84
  return { name: 'boot (launchd RunAtLoad)', ok: true, warn: !ok, detail: ok ? 'plist installato' : 'plist non installato' };
85
85
  }
86
86
 
87
+ function checkTmuxSurvival(platform, execImpl) {
88
+ if (platform !== 'linux') {
89
+ return { name: 'tmux survival on service restart', ok: true, detail: `${platform}: systemd cgroup non applicabile` };
90
+ }
91
+ try {
92
+ const value = String(execImpl('systemctl', [
93
+ '--user', 'show', 'nexuscrew.service', '--property=KillMode', '--value',
94
+ ], { encoding: 'utf8' }) || '').trim();
95
+ const ok = value === 'process';
96
+ return {
97
+ name: 'tmux survival on service restart', ok,
98
+ detail: ok ? 'KillMode=process' : `KillMode=${value || 'sconosciuto'} (restart NexusCrew puo terminare tmux)`,
99
+ };
100
+ } catch (error) {
101
+ return { name: 'tmux survival on service restart', ok: false, detail: `KillMode non verificabile: ${error.message || error}` };
102
+ }
103
+ }
104
+
87
105
  // ssh client presente su PATH: prerequisito dei tunnel multi-node (design §4).
88
106
  function checkSshClient(existsImpl) {
89
107
  return existsImpl('ssh')
@@ -141,6 +159,7 @@ function doctor(opts = {}) {
141
159
  checkPty(ptyLoad),
142
160
  checkService(platform, home, execImpl, uidVal, opts.installPath),
143
161
  checkBoot(platform, home, execImpl),
162
+ checkTmuxSurvival(platform, execImpl),
144
163
  checkTokenPerms(tokenPath),
145
164
  checkSshClient(existsImpl),
146
165
  checkAutossh(existsImpl),
@@ -159,5 +178,6 @@ function doctor(opts = {}) {
159
178
  module.exports = {
160
179
  doctor, nodeMajor,
161
180
  checkNode, checkTmux, checkPty, checkService, checkBoot, checkTokenPerms,
181
+ checkTmuxSurvival,
162
182
  checkSshClient, checkAutossh, checkSshPermitlisten,
163
183
  };
@@ -4,6 +4,7 @@
4
4
  const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
+ const crypto = require('node:crypto');
7
8
  const { execFileSync } = require('node:child_process');
8
9
  const { detectPlatform, uid } = require('./platform.js');
9
10
 
@@ -69,6 +70,9 @@ After=network-online.target
69
70
 
70
71
  [Service]
71
72
  Type=simple
73
+ # The HTTP service may be the process that creates the shared tmux server.
74
+ # Restart only NexusCrew itself; tmux sessions are independent user workloads.
75
+ KillMode=process
72
76
  WorkingDirectory=${repo}
73
77
  Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
74
78
  ExecStart=${node} ${repoBin} serve
@@ -153,6 +157,53 @@ function fileMode(platform) {
153
157
  return 0o644; // systemd unit + launchd plist
154
158
  }
155
159
 
160
+ const LINUX_TMUX_SURVIVAL_DROPIN = `[Service]\n# NexusCrew owns the HTTP process, not the shared tmux server it may start.\nKillMode=process\n`;
161
+
162
+ function linuxTmuxSurvivalPath(home) {
163
+ return path.join(home, '.config', 'systemd', 'user', 'nexuscrew.service.d', '10-tmux-survival.conf');
164
+ }
165
+
166
+ // Upgrade existing Linux installations before stop/restart. Merely updating
167
+ // the npm package does not rewrite an already-installed unit, so the new CLI
168
+ // installs a narrow drop-in and reloads systemd before any destructive service
169
+ // lifecycle operation. The target is atomic and never follows a symlink.
170
+ function ensureLinuxTmuxSurvival(opts = {}) {
171
+ const home = opts.home || os.homedir();
172
+ const target = opts.dropInPath || linuxTmuxSurvivalPath(home);
173
+ const execImpl = opts.execImpl || execFileSync;
174
+ let current = '';
175
+ try {
176
+ const st = fs.lstatSync(target);
177
+ if (st.isSymbolicLink() || !st.isFile()) throw new Error(`refusing unsafe systemd drop-in: ${target}`);
178
+ current = fs.readFileSync(target, 'utf8');
179
+ } catch (error) {
180
+ if (error.code !== 'ENOENT') throw error;
181
+ }
182
+
183
+ let written = false;
184
+ if (current !== LINUX_TMUX_SURVIVAL_DROPIN) {
185
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
186
+ const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(6).toString('hex')}`;
187
+ try {
188
+ fs.writeFileSync(tmp, LINUX_TMUX_SURVIVAL_DROPIN, { mode: 0o644, flag: 'wx' });
189
+ fs.chmodSync(tmp, 0o644);
190
+ fs.renameSync(tmp, target);
191
+ written = true;
192
+ } catch (error) {
193
+ try { fs.unlinkSync(tmp); } catch (_) {}
194
+ throw error;
195
+ }
196
+ }
197
+ execImpl('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
198
+ const effective = String(execImpl('systemctl', [
199
+ '--user', 'show', 'nexuscrew.service', '--property=KillMode', '--value',
200
+ ], { encoding: 'utf8' }) || '').trim();
201
+ if (effective !== 'process') {
202
+ throw new Error(`unsafe effective systemd KillMode=${effective || 'unknown'}; refusing NexusCrew lifecycle operation`);
203
+ }
204
+ return { target, written, killMode: effective };
205
+ }
206
+
156
207
  // Install no-symlink + atomic rename. [M3]
157
208
  // - lstat target: reject symlink (no follow)
158
209
  // - write temp file nella stessa dir -> chmod mode -> atomic rename
@@ -230,6 +281,7 @@ function installCommands(platform, target, ctx) {
230
281
  module.exports = {
231
282
  generateService, generateLinux, generateMac, generateTermux,
232
283
  installService, installPath, installCommands, fileMode,
284
+ ensureLinuxTmuxSurvival, linuxTmuxSurvivalPath, LINUX_TMUX_SURVIVAL_DROPIN,
233
285
  escapeSystemdPath, escapeSystemdExec, escapeXml, shellQuote,
234
286
  assertSystemdSafe, SYSTEMD_FORBIDDEN,
235
287
  };
@@ -29,7 +29,10 @@ async function restartRuntime(opts = {}) {
29
29
  const token = opts.token || url.readToken(url.resolvePaths({ home }).tokenPath);
30
30
  let mode = 'inactive';
31
31
  if (commands.isServiceRunning({ platform, home })) {
32
- commands.restart({ platform, home, log: () => {} });
32
+ const restarted = commands.restart({ platform, home, log: () => {} });
33
+ if (!restarted || restarted.restarted !== true) {
34
+ throw new Error(`restart service fallito: ${(restarted && restarted.reason) || 'esito non verificato'}`);
35
+ }
33
36
  mode = 'service';
34
37
  } else {
35
38
  const pidPath = pidf.defaultPidfilePath(home);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
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": {