@mmmbuto/nexuscrew 0.8.11 → 0.8.12

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: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}/**
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-mobile-position-head{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:8px;min-height:48px;padding:2px 0;border-bottom:1px solid #1c2b20}.nc-mobile-position-toggle{min-width:0;min-height:44px;padding:4px 2px;display:flex;align-items:center;gap:8px;border:0;background:none;color:inherit;text-align:left;cursor:pointer}.nc-mobile-position-toggle .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mobile-position-toggle .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mobile-chevron{flex:0 0 14px;font-size:18px;color:#8fc;text-align:center}.nc-mobile-position-copy{min-width:0;display:flex;flex-direction:column;gap:1px}.nc-mobile-position-copy b,.nc-mobile-position-copy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mobile-position-copy b{font-size:13px;color:#d8ffe8}.nc-mobile-position-copy small{font-size:11px;opacity:.55}.nc-mobile-position-filter{min-height:44px;max-width:104px;padding:0 26px 0 9px;border:1px solid #294433;border-radius:8px;background:#0e140e;color:#bfe8cf;font:12px/1 inherit}.nc-mobile-position-head>.nc-act{width:44px;height:44px}.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:44px;min-height:44px;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:44px;height:44px;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}@media(max-width:390px){.nc-mobile-position-head{gap:5px}.nc-mobile-position-filter{max-width:92px;padding-left:7px}.nc-mcard{padding:10px 9px;gap:5px}}/**
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-DoUIJ4GM.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DrrRAIg6.css">
14
+ <script type="module" crossorigin src="/assets/index-DrroT7uq.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-PkKeNfT7.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"0.8.11"}
1
+ {"version":"0.8.12"}
package/lib/cli/doctor.js CHANGED
@@ -67,7 +67,12 @@ function checkBoot(platform, home, execImpl) {
67
67
  if (platform === 'termux') {
68
68
  const p = path.join(home, '.termux', 'boot', 'nexuscrew.sh');
69
69
  const ok = fs.existsSync(p);
70
- return { name: 'boot script', ok, warn: !ok, detail: ok ? p : 'nessun Termux:boot script' };
70
+ return {
71
+ name: 'boot script', ok, warn: ok,
72
+ detail: ok
73
+ ? `${p} · app Termux:Boot non verificabile da CLI: installala e avviala una volta`
74
+ : 'nessun Termux:boot script',
75
+ };
71
76
  }
72
77
  if (platform === 'linux') {
73
78
  try {
@@ -84,6 +89,24 @@ function checkBoot(platform, home, execImpl) {
84
89
  return { name: 'boot (launchd RunAtLoad)', ok: true, warn: !ok, detail: ok ? 'plist installato' : 'plist non installato' };
85
90
  }
86
91
 
92
+ function checkUserLinger(platform, execImpl, uidVal) {
93
+ if (platform !== 'linux') {
94
+ return { name: 'user linger', ok: true, detail: `${platform}: non applicabile` };
95
+ }
96
+ try {
97
+ const value = String(execImpl('loginctl', [
98
+ 'show-user', String(uidVal), '--property=Linger', '--value',
99
+ ], { encoding: 'utf8' }) || '').trim().toLowerCase();
100
+ const enabled = value === 'yes';
101
+ return {
102
+ name: 'user linger', ok: true, warn: !enabled,
103
+ detail: enabled ? 'enabled · il servizio user puo partire senza login' : 'disabled · abilita con loginctl enable-linger per il boot senza login',
104
+ };
105
+ } catch (_) {
106
+ return { name: 'user linger', ok: true, warn: true, detail: 'non verificabile; il boot senza login potrebbe non partire' };
107
+ }
108
+ }
109
+
87
110
  function checkTmuxSurvival(platform, execImpl) {
88
111
  if (platform !== 'linux') {
89
112
  return { name: 'tmux survival on service restart', ok: true, detail: `${platform}: systemd cgroup non applicabile` };
@@ -159,6 +182,7 @@ function doctor(opts = {}) {
159
182
  checkPty(ptyLoad),
160
183
  checkService(platform, home, execImpl, uidVal, opts.installPath),
161
184
  checkBoot(platform, home, execImpl),
185
+ checkUserLinger(platform, execImpl, uidVal),
162
186
  checkTmuxSurvival(platform, execImpl),
163
187
  checkTokenPerms(tokenPath),
164
188
  checkSshClient(existsImpl),
@@ -178,6 +202,6 @@ function doctor(opts = {}) {
178
202
  module.exports = {
179
203
  doctor, nodeMajor,
180
204
  checkNode, checkTmux, checkPty, checkService, checkBoot, checkTokenPerms,
181
- checkTmuxSurvival,
205
+ checkTmuxSurvival, checkUserLinger,
182
206
  checkSshClient, checkAutossh, checkSshPermitlisten,
183
207
  };
@@ -66,6 +66,7 @@ function generateLinux(ctx) {
66
66
  return `# NexusCrew service (systemd --user, loopback, solo tunnel SSH/VPN)
67
67
  [Unit]
68
68
  Description=NexusCrew - browser tmux client (loopback, solo tunnel SSH/VPN)
69
+ Wants=network-online.target
69
70
  After=network-online.target
70
71
 
71
72
  [Service]
@@ -14,6 +14,9 @@ const pidPath = process.env.NEXUSCREW_TUNNEL_PIDFILE;
14
14
  const runId = process.env.NEXUSCREW_TUNNEL_RUN_ID;
15
15
  const stableMsRaw = Number(process.env.NEXUSCREW_TUNNEL_STABLE_MS || 3000);
16
16
  const stableMs = Number.isFinite(stableMsRaw) && stableMsRaw >= 100 ? Math.min(stableMsRaw, 30000) : 3000;
17
+ const ownershipGraceRaw = Number(process.env.NEXUSCREW_TUNNEL_OWNERSHIP_GRACE_MS || 2000);
18
+ const ownershipGraceMs = Number.isFinite(ownershipGraceRaw) && ownershipGraceRaw >= 100
19
+ ? Math.min(ownershipGraceRaw, 10000) : 2000;
17
20
  if (!sshBin || !statePath || !pidPath || !runId) process.exit(2);
18
21
 
19
22
  let child = null;
@@ -21,6 +24,8 @@ let stopping = false;
21
24
  let attempt = 0;
22
25
  let retryTimer = null;
23
26
  let upTimer = null;
27
+ let ownershipWaitTimer = null;
28
+ let ownershipTimer = null;
24
29
 
25
30
  function ownsGeneration() {
26
31
  try {
@@ -48,14 +53,14 @@ function writeState(status, extra = {}) {
48
53
  function scheduleRetry(detail) {
49
54
  if (stopping) return finish();
50
55
  const delayMs = backoffDelay(attempt, { baseMs: 1000, capMs: 60000 });
51
- writeState('retrying', { delayMs, detail });
56
+ if (!writeState('retrying', { delayMs, detail })) return stop();
52
57
  attempt += 1;
53
58
  retryTimer = setTimeout(run, delayMs);
54
59
  }
55
60
 
56
61
  function run() {
57
62
  if (stopping) return finish();
58
- writeState('starting');
63
+ if (!writeState('starting')) return stop();
59
64
  try {
60
65
  child = spawn(sshBin, sshArgs, { stdio: 'inherit' });
61
66
  } catch (e) {
@@ -78,7 +83,7 @@ function run() {
78
83
  upTimer = setTimeout(() => {
79
84
  if (!stopping && child && child.exitCode == null) {
80
85
  attempt = 0;
81
- writeState('transport-ready', { sshPid: child.pid, stableMs });
86
+ if (!writeState('transport-ready', { sshPid: child.pid, stableMs })) stop();
82
87
  }
83
88
  }, stableMs);
84
89
  });
@@ -94,6 +99,8 @@ function run() {
94
99
  function finish() {
95
100
  clearTimeout(retryTimer);
96
101
  clearTimeout(upTimer);
102
+ clearTimeout(ownershipWaitTimer);
103
+ clearInterval(ownershipTimer);
97
104
  if (ownsGeneration()) {
98
105
  writeState('down');
99
106
  try {
@@ -119,4 +126,19 @@ function stop() {
119
126
 
120
127
  process.on('SIGTERM', stop);
121
128
  process.on('SIGINT', stop);
122
- run();
129
+
130
+ // The parent can only write our PID after spawn returns. Give that narrow race
131
+ // a bounded grace window, then enforce generation ownership continuously. A
132
+ // replaced/removed pidfile must stop both supervisor and ssh instead of leaving
133
+ // an invisible retrying orphan behind.
134
+ const ownershipDeadline = Date.now() + ownershipGraceMs;
135
+ function acquireGeneration() {
136
+ if (stopping) return finish();
137
+ if (ownsGeneration()) {
138
+ ownershipTimer = setInterval(() => { if (!ownsGeneration()) stop(); }, 500);
139
+ return run();
140
+ }
141
+ if (Date.now() >= ownershipDeadline) return finish();
142
+ ownershipWaitTimer = setTimeout(acquireGeneration, 20);
143
+ }
144
+ acquireGeneration();
@@ -37,6 +37,11 @@ function assertForwardSpec(node) {
37
37
  if (node.sshPort !== undefined && !store.isPort(node.sshPort)) throw new Error('tunnel: sshPort non valida');
38
38
  if (node.identityFile !== undefined && !store.isAbsPath(node.identityFile)) throw new Error('tunnel: identityFile non valido');
39
39
  if (node.keyPath !== undefined && !store.isAbsPath(node.keyPath)) throw new Error('tunnel: keyPath non valido');
40
+ if (node.reversePort !== undefined && !store.isPort(node.reversePort)) throw new Error('tunnel: reversePort non valida');
41
+ if (node.localAppPort !== undefined && !store.isPort(node.localAppPort)) throw new Error('tunnel: localAppPort non valida');
42
+ if (node.shared === true && node.reversePort !== undefined && !store.isPort(node.localAppPort)) {
43
+ throw new Error('tunnel: Share richiede localAppPort esplicita');
44
+ }
40
45
  if (!store.parseSshTarget(node.ssh)) throw new Error('tunnel: target ssh non valido');
41
46
  }
42
47
 
@@ -52,7 +57,7 @@ function buildForwardArgs(node) {
52
57
  // The reverse channel publishes this device back through the hub, so it is
53
58
  // opt-in only. A negotiated reversePort alone must never imply consent.
54
59
  const reverse = node.shared === true && node.reversePort !== undefined ? [
55
- '-R', `127.0.0.1:${node.reversePort}:127.0.0.1:${node.localAppPort || node.remotePort}`,
60
+ '-R', `127.0.0.1:${node.reversePort}:127.0.0.1:${node.localAppPort}`,
56
61
  ] : [];
57
62
  return SSH_BASE_OPTS.concat(transport, identity ? ['-i', identity] : [], [
58
63
  '-L', `127.0.0.1:${node.localPort}:127.0.0.1:${node.remotePort}`,
@@ -246,13 +251,23 @@ function removeStateIfOwned(home, name, meta) {
246
251
  return false;
247
252
  }
248
253
 
249
- function supervisorExited(pid, timeoutMs = 2500) {
254
+ function supervisorExited(pid, timeoutMs = 2500, impl = {}) {
250
255
  const deadline = Date.now() + timeoutMs;
251
256
  const sleeper = new Int32Array(new SharedArrayBuffer(4));
257
+ const pidExistsImpl = impl.pidExistsImpl || pidf.pidExists;
258
+ const procReadImpl = impl.procReadImpl || fs.readFileSync;
259
+ const spawnSyncImpl = impl.spawnSyncImpl || spawnSync;
252
260
  const exited = () => {
253
- if (!pidf.pidExists(pid)) return true;
254
- try { return fs.readFileSync(`/proc/${pid}/stat`, 'utf8').split(' ')[2] === 'Z'; }
255
- catch (_) { return false; }
261
+ if (!pidExistsImpl(pid)) return true;
262
+ try { return procReadImpl(`/proc/${pid}/stat`, 'utf8').split(' ')[2] === 'Z'; }
263
+ catch (_) {
264
+ // macOS has no /proc. `ps` is argv-only and its STAT starts with Z for a
265
+ // zombie, which is no longer a usable supervisor even before wait/reap.
266
+ try {
267
+ const result = spawnSyncImpl('ps', ['-p', String(pid), '-o', 'stat='], { encoding: 'utf8' });
268
+ return !result?.error && /^Z/.test(String(result?.stdout || '').trim());
269
+ } catch (_error) { return false; }
270
+ }
256
271
  };
257
272
  while (!exited() && Date.now() < deadline) Atomics.wait(sleeper, 0, 0, 25);
258
273
  return exited();
@@ -418,7 +433,8 @@ function restartTunnel(opts) {
418
433
  // Helper di alto livello: costruisce args dal nodo e avvia il forward.
419
434
  function startForward(opts) {
420
435
  const node = opts.node;
421
- const args = buildForwardArgs({ ...node, localAppPort: opts.localAppPort });
436
+ const localAppPort = opts.localAppPort === undefined ? node.localAppPort : opts.localAppPort;
437
+ const args = buildForwardArgs({ ...node, localAppPort });
422
438
  // NexusCrew already owns retry/backoff in tunnel-supervisor.js. Nesting
423
439
  // autossh inside that supervisor made liveness dishonest: autossh could stay
424
440
  // alive while every SSH child exited 255, so the UI reported "tunnel up".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
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": {
@@ -20,7 +20,7 @@
20
20
  "start": "node bin/nexuscrew.js serve",
21
21
  "dev": "node bin/nexuscrew.js serve",
22
22
  "build": "cd frontend && npm install && npm run build && cd ..",
23
- "test": "node tests/run-isolated.js"
23
+ "test": "node tests/run-isolated.js && npm --prefix frontend test"
24
24
  },
25
25
  "dependencies": {
26
26
  "express": "^4.21.0",