@mmmbuto/nexuscrew 0.8.7 → 0.8.9

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.
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+
7
+ const PACKAGE_NAME = require('../../package.json').name;
8
+ const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
9
+
10
+ function parseVersion(value) {
11
+ const m = VERSION_RE.exec(String(value || '').trim());
12
+ if (!m) return null;
13
+ return {
14
+ raw: m[0],
15
+ major: Number(m[1]),
16
+ minor: Number(m[2]),
17
+ patch: Number(m[3]),
18
+ prerelease: m[4] ? m[4].split('.') : [],
19
+ };
20
+ }
21
+
22
+ function compareIdentifier(a, b) {
23
+ const an = /^[0-9]+$/.test(a);
24
+ const bn = /^[0-9]+$/.test(b);
25
+ if (an && bn) return Number(a) === Number(b) ? 0 : Number(a) > Number(b) ? 1 : -1;
26
+ if (an !== bn) return an ? -1 : 1;
27
+ return a === b ? 0 : a > b ? 1 : -1;
28
+ }
29
+
30
+ function compareVersions(a, b) {
31
+ const av = parseVersion(a);
32
+ const bv = parseVersion(b);
33
+ if (!av || !bv) throw new Error('versione semver non valida');
34
+ for (const key of ['major', 'minor', 'patch']) {
35
+ if (av[key] !== bv[key]) return av[key] > bv[key] ? 1 : -1;
36
+ }
37
+ if (!av.prerelease.length && !bv.prerelease.length) return 0;
38
+ if (!av.prerelease.length) return 1;
39
+ if (!bv.prerelease.length) return -1;
40
+ const n = Math.max(av.prerelease.length, bv.prerelease.length);
41
+ for (let i = 0; i < n; i += 1) {
42
+ if (av.prerelease[i] === undefined) return -1;
43
+ if (bv.prerelease[i] === undefined) return 1;
44
+ const c = compareIdentifier(av.prerelease[i], bv.prerelease[i]);
45
+ if (c) return c;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ function registryVersion(stdout) {
51
+ const raw = String(stdout || '').trim();
52
+ let value = raw;
53
+ try {
54
+ const decoded = JSON.parse(raw);
55
+ if (typeof decoded === 'string') value = decoded;
56
+ } catch (_) { /* npm senza --json o output gia' plain */ }
57
+ const parsed = parseVersion(value);
58
+ if (!parsed) throw new Error('npm ha restituito una versione non valida');
59
+ return parsed.raw;
60
+ }
61
+
62
+ function scrubError(error) {
63
+ return String((error && error.message) || error || 'errore sconosciuto')
64
+ .replace(/[\r\n\t]+/g, ' ')
65
+ .replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@/gi, 'https://***@')
66
+ .replace(/\/(?:home|Users|data\/data\/com\.termux\/files\/home)\/[^\s:]+/g, '<local-path>')
67
+ .replace(/(?:Bearer\s+)?[A-Za-z0-9_-]{40,}/gi, '***')
68
+ .slice(0, 300);
69
+ }
70
+
71
+ function pidAlive(pid) {
72
+ if (!Number.isInteger(pid) || pid < 1) return false;
73
+ try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
74
+ }
75
+
76
+ function readLock(file) {
77
+ try {
78
+ const st = fs.lstatSync(file);
79
+ if (!st.isFile() || st.isSymbolicLink()) return null;
80
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
81
+ return value && Number.isInteger(value.pid) && typeof value.token === 'string' ? value : null;
82
+ } catch (_) { return null; }
83
+ }
84
+
85
+ function writeLock(file, value, flags = 'wx') {
86
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
87
+ fs.writeFileSync(file, `${JSON.stringify(value)}\n`, { flag: flags, mode: 0o600 });
88
+ fs.chmodSync(file, 0o600);
89
+ }
90
+
91
+ function acquireUpdateLock(file, pid = process.pid, token = crypto.randomBytes(16).toString('hex')) {
92
+ for (let attempt = 0; attempt < 2; attempt += 1) {
93
+ try { writeLock(file, { pid, token, createdAt: Date.now() }); return { ok: true, token }; }
94
+ catch (e) {
95
+ if (e.code !== 'EEXIST') throw e;
96
+ const current = readLock(file);
97
+ if (current && pidAlive(current.pid)) return { ok: false, current };
98
+ try { fs.unlinkSync(file); } catch (unlinkError) { if (unlinkError.code !== 'ENOENT') throw unlinkError; }
99
+ }
100
+ }
101
+ return { ok: false, current: readLock(file) };
102
+ }
103
+
104
+ function adoptUpdateLock(file, token, pid = process.pid) {
105
+ const current = readLock(file);
106
+ if (!current || current.token !== token) return false;
107
+ const tmp = `${file}.${pid}.tmp`;
108
+ try {
109
+ writeLock(tmp, { ...current, pid, adoptedAt: Date.now() });
110
+ fs.renameSync(tmp, file);
111
+ return true;
112
+ } catch (e) {
113
+ try { fs.unlinkSync(tmp); } catch (_) {}
114
+ throw e;
115
+ }
116
+ }
117
+
118
+ function releaseUpdateLock(file, token) {
119
+ const current = readLock(file);
120
+ if (!current || current.token !== token) return false;
121
+ try { fs.unlinkSync(file); return true; } catch (_) { return false; }
122
+ }
123
+
124
+ function readState(file) {
125
+ try {
126
+ const st = fs.lstatSync(file);
127
+ if (!st.isFile() || st.isSymbolicLink()) return {};
128
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
129
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
130
+ } catch (_) { return {}; }
131
+ }
132
+
133
+ function writeState(file, value) {
134
+ try {
135
+ const st = fs.lstatSync(file);
136
+ if (st.isSymbolicLink() || !st.isFile()) throw new Error('update state target non sicuro');
137
+ } catch (e) {
138
+ if (e.code !== 'ENOENT') throw e;
139
+ }
140
+ const dir = path.dirname(file);
141
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
142
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
143
+ try {
144
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
145
+ fs.chmodSync(tmp, 0o600);
146
+ fs.renameSync(tmp, file);
147
+ } catch (e) {
148
+ try { fs.unlinkSync(tmp); } catch (_) {}
149
+ throw e;
150
+ }
151
+ }
152
+
153
+ module.exports = {
154
+ PACKAGE_NAME, VERSION_RE, parseVersion, compareVersions, registryVersion,
155
+ scrubError, pidAlive, readLock, acquireUpdateLock, adoptUpdateLock, releaseUpdateLock,
156
+ readState, writeState,
157
+ };
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { execFile, spawn } = require('node:child_process');
7
+ const {
8
+ PACKAGE_NAME, compareVersions, parseVersion, registryVersion, scrubError, pidAlive, readLock,
9
+ acquireUpdateLock, releaseUpdateLock, readState, writeState,
10
+ } = require('./core.js');
11
+
12
+ const DEFAULT_INITIAL_DELAY_MS = 60 * 1000;
13
+ const DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000;
14
+ const DEFAULT_MAX_LOG_BYTES = 1024 * 1024;
15
+
16
+ function packageRoot() { return path.resolve(__dirname, '..', '..'); }
17
+
18
+ function isGlobalInstall(root = packageRoot()) {
19
+ const normalized = path.resolve(root).split(path.sep).join('/');
20
+ return normalized.includes('/node_modules/@mmmbuto/nexuscrew');
21
+ }
22
+
23
+ function isNewer(candidate, current) {
24
+ try { return compareVersions(candidate, current) > 0; } catch (_) { return false; }
25
+ }
26
+
27
+ function lookupLatestNpm({ execFileImpl = execFile, timeoutMs = 20_000 } = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ execFileImpl('npm', ['view', `${PACKAGE_NAME}@latest`, 'version', '--json'], {
30
+ encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024,
31
+ }, (error, stdout) => {
32
+ if (error) return reject(error);
33
+ try { resolve(registryVersion(stdout)); } catch (e) { reject(e); }
34
+ });
35
+ });
36
+ }
37
+
38
+ function createNpmUpdater(opts = {}) {
39
+ const currentVersion = String(opts.currentVersion || require('../../package.json').version);
40
+ const home = opts.home || os.homedir();
41
+ const statusPath = opts.statusPath || path.join(home, '.nexuscrew', 'npm-update.json');
42
+ const logPath = opts.logPath || path.join(home, '.nexuscrew', 'npm-update.log');
43
+ const lockPath = opts.lockPath || path.join(home, '.nexuscrew', 'npm-update.lock');
44
+ const runnerPath = opts.runnerPath || path.join(__dirname, 'runner.js');
45
+ const supported = opts.supported === undefined ? isGlobalInstall(opts.packageRoot) : !!opts.supported;
46
+ const readonly = opts.readonly === true;
47
+ const lookupLatest = opts.lookupLatest || (() => lookupLatestNpm(opts));
48
+ const spawnImpl = opts.spawnImpl || spawn;
49
+ const useSystemdRun = opts.useSystemdRun === undefined
50
+ ? (process.platform === 'linux' && !!process.env.INVOCATION_ID)
51
+ : opts.useSystemdRun === true;
52
+ const initialDelayMs = opts.initialDelayMs === undefined ? DEFAULT_INITIAL_DELAY_MS : opts.initialDelayMs;
53
+ const intervalMs = opts.intervalMs === undefined ? DEFAULT_INTERVAL_MS : opts.intervalMs;
54
+ const maxLogBytes = opts.maxLogBytes === undefined ? DEFAULT_MAX_LOG_BYTES : opts.maxLogBytes;
55
+ let enabled = opts.enabled !== false && !readonly;
56
+ let checking = null;
57
+ let timer = null;
58
+ let state = readState(statusPath);
59
+
60
+ // Only stale temp files are reaped. A fresh temp may belong to another live
61
+ // NexusCrew process writing the same shared state.
62
+ try {
63
+ const dir = path.dirname(statusPath); const prefix = `${path.basename(statusPath)}.`;
64
+ for (const name of fs.readdirSync(dir)) {
65
+ if (!name.startsWith(prefix) || !name.endsWith('.tmp')) continue;
66
+ const file = path.join(dir, name);
67
+ if (Date.now() - fs.statSync(file).mtimeMs > 24 * 60 * 60 * 1000) fs.unlinkSync(file);
68
+ }
69
+ } catch (_) {}
70
+
71
+ if (state.phase === 'installed' && state.targetVersion === currentVersion) {
72
+ state = { ...state, phase: 'idle', available: false, latest: currentVersion, lastError: '' };
73
+ writeState(statusPath, state);
74
+ }
75
+ const activeLock = () => { const lock = readLock(lockPath); return !!(lock && pidAlive(lock.pid)); };
76
+ if ((state.phase === 'installing' || state.phase === 'restarting') && !pidAlive(state.updaterPid) && !activeLock()) {
77
+ state = { ...state, phase: 'error', lastError: 'aggiornamento precedente interrotto' };
78
+ writeState(statusPath, state);
79
+ }
80
+
81
+ const persist = (patch) => {
82
+ state = { ...state, ...patch };
83
+ writeState(statusPath, state);
84
+ return status();
85
+ };
86
+
87
+ const status = () => {
88
+ state = { ...state, ...readState(statusPath) };
89
+ return {
90
+ supported, enabled, current: currentVersion,
91
+ phase: state.phase || 'idle', latest: state.latest || '',
92
+ available: state.available === true && isNewer(state.latest, currentVersion),
93
+ lastCheckedAt: state.lastCheckedAt || '', lastUpdatedAt: state.lastUpdatedAt || '',
94
+ lastError: state.lastError || '', blockedVersion: state.blockedVersion || '',
95
+ };
96
+ };
97
+
98
+ const launch = (version) => {
99
+ if (!supported) throw new Error('auto-update disponibile solo nell’installazione npm globale');
100
+ if (readonly) throw new Error('READONLY: auto-update bloccato');
101
+ if (!isNewer(version, currentVersion)) return status();
102
+ status();
103
+ if (state.phase === 'installing' || state.phase === 'restarting' || activeLock()) {
104
+ const error = new Error('aggiornamento già in corso'); error.status = 409; error.code = 'update-busy'; throw error;
105
+ }
106
+ if (state.blockedVersion === version) {
107
+ const error = new Error(`versione ${version} bloccata dopo un rollback fallito/necessario`); error.status = 409; error.code = 'update-blocked'; throw error;
108
+ }
109
+ const reservation = acquireUpdateLock(lockPath);
110
+ if (!reservation.ok) {
111
+ const error = new Error('aggiornamento già in corso'); error.status = 409; error.code = 'update-busy'; throw error;
112
+ }
113
+ fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 0o700 });
114
+ try {
115
+ const logStat = fs.lstatSync(logPath);
116
+ if (!logStat.isFile() || logStat.isSymbolicLink()) throw new Error('update log target non sicuro');
117
+ if (logStat.size > maxLogBytes) fs.truncateSync(logPath, 0);
118
+ } catch (error) {
119
+ if (error.code !== 'ENOENT') {
120
+ releaseUpdateLock(lockPath, reservation.token);
121
+ persist({ phase: 'error', available: true, lastError: scrubError(error) });
122
+ throw error;
123
+ }
124
+ }
125
+ const fd = fs.openSync(logPath, 'a', 0o600);
126
+ let child;
127
+ try {
128
+ persist({ phase: 'installing', targetVersion: version, latest: version, available: true, lastError: '' });
129
+ const runnerArgs = [runnerPath, '--version', version, '--status', statusPath, '--home', home,
130
+ '--lock', lockPath, '--lock-token', reservation.token];
131
+ const bin = useSystemdRun ? 'systemd-run' : process.execPath;
132
+ const argv = useSystemdRun
133
+ ? ['--user', '--quiet', '--collect', `--unit=nexuscrew-update-${process.pid}-${Date.now()}`,
134
+ `--property=StandardOutput=append:${logPath}`, `--property=StandardError=append:${logPath}`,
135
+ process.execPath, ...runnerArgs]
136
+ : runnerArgs;
137
+ child = spawnImpl(bin, argv, {
138
+ detached: true, stdio: ['ignore', fd, fd], env: { ...process.env, NEXUSCREW_UPDATE_RUNNER: '1' },
139
+ });
140
+ } catch (e) {
141
+ releaseUpdateLock(lockPath, reservation.token);
142
+ persist({ phase: 'error', available: true, lastError: scrubError(e) });
143
+ throw e;
144
+ } finally {
145
+ try { fs.closeSync(fd); } catch (_) {}
146
+ }
147
+ if (child && typeof child.once === 'function') {
148
+ child.once('error', (error) => {
149
+ if (releaseUpdateLock(lockPath, reservation.token)) {
150
+ persist({ phase: 'error', available: true, lastError: scrubError(error) });
151
+ }
152
+ });
153
+ if (useSystemdRun) child.once('exit', (code) => {
154
+ if (code && releaseUpdateLock(lockPath, reservation.token)) {
155
+ persist({ phase: 'error', available: true, lastError: `systemd-run terminato con codice ${code}` });
156
+ return;
157
+ }
158
+ // systemd-run returns after scheduling the transient unit. The runner
159
+ // must adopt the reservation promptly; otherwise release a dead lock.
160
+ const timer = setTimeout(() => {
161
+ const lock = readLock(lockPath);
162
+ if (lock?.token === reservation.token && lock.pid === process.pid
163
+ && releaseUpdateLock(lockPath, reservation.token)) {
164
+ persist({ phase: 'error', available: true, lastError: 'runner aggiornamento non avviato' });
165
+ }
166
+ }, 15_000);
167
+ if (typeof timer.unref === 'function') timer.unref();
168
+ });
169
+ }
170
+ if (!child || !Number.isInteger(child.pid)) {
171
+ releaseUpdateLock(lockPath, reservation.token);
172
+ const error = new Error('impossibile avviare il processo di aggiornamento');
173
+ persist({ phase: 'error', available: true, lastError: error.message });
174
+ throw error;
175
+ }
176
+ persist({ updaterPid: child.pid });
177
+ if (typeof child.unref === 'function') child.unref();
178
+ return status();
179
+ };
180
+
181
+ const check = async ({ autoApply = false } = {}) => {
182
+ if (!supported) return status();
183
+ if (checking) return checking;
184
+ checking = (async () => {
185
+ status();
186
+ if (state.phase === 'installing' || state.phase === 'restarting' || activeLock()) return status();
187
+ persist({ phase: 'checking', lastError: '' });
188
+ try {
189
+ const latest = await lookupLatest();
190
+ if (parseVersion(latest)?.prerelease.length) throw new Error('npm latest punta a una prerelease: aggiornamento rifiutato');
191
+ const available = isNewer(latest, currentVersion);
192
+ const blocked = state.blockedVersion === latest;
193
+ persist({
194
+ phase: blocked ? 'error' : available ? 'available' : 'idle', latest, available,
195
+ lastCheckedAt: new Date().toISOString(),
196
+ lastError: blocked ? (state.lastError || `versione ${latest} bloccata dopo rollback`) : '',
197
+ });
198
+ if (available && !blocked && autoApply && enabled) return launch(latest);
199
+ return status();
200
+ } catch (e) {
201
+ return persist({ phase: 'error', lastCheckedAt: new Date().toISOString(), lastError: scrubError(e) });
202
+ } finally { checking = null; }
203
+ })();
204
+ return checking;
205
+ };
206
+
207
+ const apply = async () => {
208
+ if (!supported) throw new Error('auto-update disponibile solo nell’installazione npm globale');
209
+ if (readonly) throw new Error('READONLY: auto-update bloccato');
210
+ status();
211
+ if (state.phase === 'installing' || state.phase === 'restarting' || activeLock()) {
212
+ const error = new Error('aggiornamento già in corso'); error.status = 409; error.code = 'update-busy'; throw error;
213
+ }
214
+ const latest = state.latest && isNewer(state.latest, currentVersion)
215
+ ? state.latest : (await check()).latest;
216
+ if (!latest || !isNewer(latest, currentVersion)) return status();
217
+ return launch(latest);
218
+ };
219
+
220
+ const schedule = (delay) => {
221
+ if (!enabled || !supported || readonly || timer) return;
222
+ timer = setTimeout(async () => {
223
+ timer = null;
224
+ await check({ autoApply: true });
225
+ schedule(intervalMs);
226
+ }, Math.max(0, delay));
227
+ if (typeof timer.unref === 'function') timer.unref();
228
+ };
229
+
230
+ const start = () => schedule(initialDelayMs);
231
+ const close = () => { if (timer) clearTimeout(timer); timer = null; };
232
+ const setEnabled = (value) => {
233
+ enabled = value === true && !readonly;
234
+ close();
235
+ if (enabled) schedule(250);
236
+ return status();
237
+ };
238
+
239
+ return { status, check, apply, start, close, setEnabled, supported };
240
+ }
241
+
242
+ module.exports = {
243
+ DEFAULT_INITIAL_DELAY_MS, DEFAULT_INTERVAL_MS, DEFAULT_MAX_LOG_BYTES, packageRoot, isGlobalInstall,
244
+ isNewer, pidAlive, lookupLatestNpm, createNpmUpdater,
245
+ };
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { execFileSync } = require('node:child_process');
8
+ const {
9
+ PACKAGE_NAME, parseVersion, scrubError, pidAlive, adoptUpdateLock, releaseUpdateLock, readState, writeState,
10
+ } = require('./core.js');
11
+
12
+ function args(argv) {
13
+ const out = {};
14
+ for (let i = 0; i < argv.length; i += 2) {
15
+ const key = argv[i];
16
+ if (!key || !key.startsWith('--') || argv[i + 1] === undefined) return null;
17
+ out[key.slice(2)] = argv[i + 1];
18
+ }
19
+ return out;
20
+ }
21
+
22
+ async function restartRuntime(opts = {}) {
23
+ const home = opts.home || os.homedir();
24
+ const commands = opts.commands || require('../cli/commands.js');
25
+ const pidf = opts.pidfile || require('../cli/pidfile.js');
26
+ const platform = opts.platform || require('../cli/platform.js').detectPlatform();
27
+ const url = opts.url || require('../cli/url.js');
28
+ const port = opts.port || url.loadPort({ home });
29
+ const token = opts.token || url.readToken(url.resolvePaths({ home }).tokenPath);
30
+ let mode = 'inactive';
31
+ if (commands.isServiceRunning({ platform, home })) {
32
+ commands.restart({ platform, home, log: () => {} });
33
+ mode = 'service';
34
+ } else {
35
+ const pidPath = pidf.defaultPidfilePath(home);
36
+ const meta = pidf.readPidfile(pidPath);
37
+ if (meta && pidf.isAlive(meta)) {
38
+ const stopped = pidf.killPidfile(pidPath);
39
+ if (!stopped.killed) throw new Error(`restart portatile fallito: ${stopped.reason || 'processo non arrestato'}`);
40
+ const wait = opts.waitImpl || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
41
+ let released = false;
42
+ for (let i = 0; i < 60; i += 1) {
43
+ const dead = !(opts.pidAliveImpl || pidAlive)(stopped.pid);
44
+ const free = await (opts.portAvailableImpl || commands.portAvailable)(port);
45
+ if (dead && free) { released = true; break; }
46
+ await wait(100);
47
+ }
48
+ if (!released) throw new Error(`restart portatile fallito: porta ${port} non liberata`);
49
+ commands.startPortable({ platform, home });
50
+ mode = 'portable';
51
+ }
52
+ }
53
+ if (mode !== 'inactive') {
54
+ const waitFor = opts.waitForRuntimeImpl || commands.waitForNexusCrew;
55
+ const healthy = await waitFor(port, token, {
56
+ waitAttempts: opts.healthAttempts || 60, waitDelayMs: opts.healthDelayMs || 250,
57
+ ...(opts.healthProbeImpl ? { probeImpl: opts.healthProbeImpl } : {}),
58
+ });
59
+ if (!healthy) throw new Error(`NexusCrew ${mode} non healthy su 127.0.0.1:${port} dopo il restart`);
60
+ }
61
+ return mode;
62
+ }
63
+
64
+ async function runUpdate(opts = {}) {
65
+ const version = String(opts.version || '');
66
+ if (!parseVersion(version)) throw new Error('versione update non valida');
67
+ const home = opts.home || os.homedir();
68
+ const statusPath = opts.statusPath || path.join(home, '.nexuscrew', 'npm-update.json');
69
+ const execImpl = opts.execImpl || execFileSync;
70
+ const readInstalledVersion = opts.readInstalledVersion || (() => {
71
+ const p = path.resolve(__dirname, '..', '..', 'package.json');
72
+ return JSON.parse(fs.readFileSync(p, 'utf8')).version;
73
+ });
74
+ const preflightImpl = opts.preflightImpl || (({ version: expectedVersion = version } = {}) => {
75
+ const bin = path.resolve(__dirname, '..', '..', 'bin', 'nexuscrew.js');
76
+ const output = execFileSync(process.execPath, [bin, 'version'], { encoding: 'utf8', timeout: 20_000, stdio: ['ignore', 'pipe', 'pipe'] });
77
+ if (String(output || '').trim() !== expectedVersion) throw new Error(`preflight CLI fallito: attesa ${expectedVersion}`);
78
+ return true;
79
+ });
80
+ const restartImpl = opts.restartImpl || restartRuntime;
81
+ const lockPath = opts.lockPath || '';
82
+ const lockToken = opts.lockToken || '';
83
+ let ownsLock = false;
84
+ if (lockPath || lockToken) {
85
+ if (!lockPath || !lockToken || !adoptUpdateLock(lockPath, lockToken, process.pid)) {
86
+ const error = new Error('lock aggiornamento non posseduto'); error.status = 409; throw error;
87
+ }
88
+ ownsLock = true;
89
+ }
90
+ const previous = readState(statusPath);
91
+ const previousVersion = String(readInstalledVersion() || '');
92
+ let installedNew = false;
93
+ try {
94
+ writeState(statusPath, { ...previous, phase: 'installing', targetVersion: version, updaterPid: process.pid, lastError: '' });
95
+ execImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${version}`, '--no-audit', '--no-fund'], {
96
+ stdio: 'inherit', timeout: 5 * 60 * 1000,
97
+ });
98
+ const installed = String(readInstalledVersion() || '');
99
+ if (installed !== version) throw new Error(`verifica installazione fallita: attesa ${version}, trovata ${installed || 'sconosciuta'}`);
100
+ installedNew = true;
101
+ await preflightImpl({ version, home });
102
+ writeState(statusPath, { ...readState(statusPath), phase: 'restarting', updaterPid: process.pid, lastError: '' });
103
+ const restartMode = await restartImpl({ home, ...(opts.runtimeSeams || {}) });
104
+ writeState(statusPath, {
105
+ ...readState(statusPath), phase: 'installed', current: version, latest: version,
106
+ available: false, blockedVersion: '', lastUpdatedAt: new Date().toISOString(), lastError: '',
107
+ });
108
+ return { updated: true, version, restartMode };
109
+ } catch (e) {
110
+ let rollbackError = null; let rolledBack = false;
111
+ if (installedNew && parseVersion(previousVersion) && previousVersion !== version) {
112
+ try {
113
+ execImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${previousVersion}`, '--no-audit', '--no-fund'], {
114
+ stdio: 'inherit', timeout: 5 * 60 * 1000,
115
+ });
116
+ if (String(readInstalledVersion() || '') !== previousVersion) throw new Error(`rollback verify: attesa ${previousVersion}`);
117
+ await preflightImpl({ version: previousVersion, home, rollback: true });
118
+ await restartImpl({ home, ...(opts.runtimeSeams || {}) });
119
+ rolledBack = true;
120
+ } catch (rollbackFailure) { rollbackError = rollbackFailure; }
121
+ }
122
+ const detail = rollbackError
123
+ ? `${scrubError(e)}; rollback ${previousVersion || '?'} fallito: ${scrubError(rollbackError)}`
124
+ : rolledBack ? `${scrubError(e)}; rollback a ${previousVersion} completato` : scrubError(e);
125
+ writeState(statusPath, {
126
+ ...readState(statusPath), phase: 'error', current: rolledBack ? previousVersion : readState(statusPath).current,
127
+ available: true, blockedVersion: installedNew ? version : '', rolledBackTo: rolledBack ? previousVersion : '',
128
+ lastError: detail,
129
+ });
130
+ throw e;
131
+ } finally {
132
+ if (ownsLock) releaseUpdateLock(lockPath, lockToken);
133
+ }
134
+ }
135
+
136
+ if (require.main === module) {
137
+ const parsed = args(process.argv.slice(2));
138
+ if (!parsed || !parsed.version || !parsed.status) process.exitCode = 2;
139
+ else runUpdate({ version: parsed.version, statusPath: parsed.status, home: parsed.home,
140
+ lockPath: parsed.lock, lockToken: parsed['lock-token'] })
141
+ .catch(() => { process.exitCode = 1; });
142
+ }
143
+
144
+ module.exports = { args, restartRuntime, runUpdate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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": {