groove-dev 0.27.156 → 0.27.159
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +61 -16
- package/node_modules/@groove-dev/daemon/src/process.js +130 -2
- package/node_modules/@groove-dev/daemon/src/rotator.js +2 -1
- package/node_modules/@groove-dev/daemon/src/routes/files.js +28 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +89 -71
- package/node_modules/@groove-dev/gui/dist/assets/index-Bij9o_dc.js +1020 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -2
- package/node_modules/@groove-dev/gui/src/app.css +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +11 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +8 -1
- package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +18 -6
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +69 -38
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +121 -49
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/journalist.js +61 -16
- package/packages/daemon/src/process.js +130 -2
- package/packages/daemon/src/rotator.js +2 -1
- package/packages/daemon/src/routes/files.js +28 -6
- package/packages/daemon/src/tunnel-manager.js +89 -71
- package/packages/gui/dist/assets/index-Bij9o_dc.js +1020 -0
- package/packages/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -2
- package/packages/gui/src/app.css +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +8 -8
- package/packages/gui/src/components/agents/diff-viewer.jsx +2 -2
- package/packages/gui/src/components/agents/workspace-mode.jsx +11 -2
- package/packages/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/packages/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/packages/gui/src/components/editor/terminal.jsx +1 -1
- package/packages/gui/src/components/layout/welcome-splash.jsx +8 -1
- package/packages/gui/src/components/network/activity-chart.jsx +4 -4
- package/packages/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +18 -6
- package/packages/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/packages/gui/src/stores/groove.js +9 -1
- package/packages/gui/src/stores/slices/agents-slice.js +69 -38
- package/packages/gui/src/views/memory.jsx +121 -49
- package/ssh/error.png +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/CHANGELOG.md +0 -2
- package/node_modules/@fontsource-variable/jetbrains-mono/LICENSE +0 -93
- package/node_modules/@fontsource-variable/jetbrains-mono/README.md +0 -48
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/index.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/metadata.json +0 -29
- package/node_modules/@fontsource-variable/jetbrains-mono/package.json +0 -47
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/metadata.scss +0 -46
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/mixins.scss +0 -193
- package/node_modules/@fontsource-variable/jetbrains-mono/unicode.json +0 -8
- package/node_modules/@fontsource-variable/jetbrains-mono/wght-italic.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/wght.css +0 -59
- package/node_modules/@groove-dev/gui/dist/assets/index-COQYX12F.js +0 -1015
- package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/packages/gui/dist/assets/index-COQYX12F.js +0 -1015
- package/packages/gui/dist/assets/index-Diw6wDPU.css +0 -1
- package/packages/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
|
@@ -37,6 +37,18 @@ function sshCmd(cmd) {
|
|
|
37
37
|
return `bash -lc '${nvmProbe}${cmd}'`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function npmGlobalInstall(pkg, user) {
|
|
41
|
+
const base = `npm i -g --prefer-online ${pkg}`;
|
|
42
|
+
if (user === 'root') return base;
|
|
43
|
+
return `${base} || sudo -n ${base}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPermissionError(output) {
|
|
47
|
+
return /EACCES|permission denied|sudo.*password/i.test(output);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PERMISSION_HINT = 'npm global install requires write access. Either install Node via nvm (recommended) or configure passwordless sudo for npm on the remote server.';
|
|
51
|
+
|
|
40
52
|
export class TunnelManager {
|
|
41
53
|
constructor(daemon) {
|
|
42
54
|
this.daemon = daemon;
|
|
@@ -213,6 +225,14 @@ export class TunnelManager {
|
|
|
213
225
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
214
226
|
|
|
215
227
|
try {
|
|
228
|
+
const probeCmd = [
|
|
229
|
+
`NV=$(node --version 2>/dev/null || echo "");`,
|
|
230
|
+
`echo "__NODE__${`$\{NV\}`}__NODE_END__";`,
|
|
231
|
+
`S=$(curl -sf http://localhost:${REMOTE_PORT}/api/status 2>/dev/null);`,
|
|
232
|
+
`if [ -n "$S" ]; then echo "__GROOVE_RUNNING__$S__GROOVE_END__";`,
|
|
233
|
+
`else which groove >/dev/null 2>&1 && echo __GROOVE_VER__$(groove --version 2>/dev/null || echo unknown)__GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__; fi`,
|
|
234
|
+
].join(' ');
|
|
235
|
+
|
|
216
236
|
const result = execFileSync('ssh', [
|
|
217
237
|
...keyArgs,
|
|
218
238
|
'-p', String(config.port || 22),
|
|
@@ -220,27 +240,32 @@ export class TunnelManager {
|
|
|
220
240
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
221
241
|
'-o', 'BatchMode=yes',
|
|
222
242
|
target,
|
|
223
|
-
sshCmd(
|
|
243
|
+
sshCmd(probeCmd),
|
|
224
244
|
], {
|
|
225
245
|
encoding: 'utf8',
|
|
226
246
|
timeout: 15000,
|
|
227
247
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
228
248
|
});
|
|
229
249
|
|
|
250
|
+
const nodeMatch = result.match(/__NODE__(.+?)__NODE_END__/);
|
|
251
|
+
const nodeVersionRaw = nodeMatch ? nodeMatch[1].trim() : '';
|
|
252
|
+
const nodeInstalled = nodeVersionRaw.startsWith('v');
|
|
253
|
+
const nodeVersion = nodeInstalled ? nodeVersionRaw : null;
|
|
254
|
+
|
|
230
255
|
if (result.includes('__GROOVE_NOT_INSTALLED__')) {
|
|
231
|
-
return { reachable: true, daemonRunning: false, grooveInstalled: false };
|
|
256
|
+
return { reachable: true, daemonRunning: false, grooveInstalled: false, nodeInstalled, nodeVersion };
|
|
232
257
|
}
|
|
233
258
|
if (result.includes('__GROOVE_STOPPED__')) {
|
|
234
259
|
const verMatch = result.match(/__GROOVE_VER__(.+?)__GROOVE_STOPPED__/);
|
|
235
260
|
const remoteVersion = verMatch ? verMatch[1].trim() : null;
|
|
236
|
-
return { reachable: true, daemonRunning: false, grooveInstalled: true, remoteVersion };
|
|
261
|
+
return { reachable: true, daemonRunning: false, grooveInstalled: true, remoteVersion, nodeInstalled, nodeVersion };
|
|
237
262
|
}
|
|
238
263
|
const runMatch = result.match(/__GROOVE_RUNNING__(.+?)__GROOVE_END__/);
|
|
239
264
|
let remoteVersion = null;
|
|
240
265
|
if (runMatch) {
|
|
241
266
|
try { remoteVersion = JSON.parse(runMatch[1]).version || null; } catch { /* ignore */ }
|
|
242
267
|
}
|
|
243
|
-
return { reachable: true, daemonRunning: true, grooveInstalled: true, remoteVersion };
|
|
268
|
+
return { reachable: true, daemonRunning: true, grooveInstalled: true, remoteVersion, nodeInstalled, nodeVersion };
|
|
244
269
|
} catch (err) {
|
|
245
270
|
const stderr = err.stderr?.toString() || '';
|
|
246
271
|
if (stderr.includes('Permission denied')) {
|
|
@@ -259,14 +284,17 @@ export class TunnelManager {
|
|
|
259
284
|
|
|
260
285
|
if (this.active.has(id)) {
|
|
261
286
|
const existing = this.active.get(id);
|
|
262
|
-
return { localPort: existing.localPort, pid: existing.pid };
|
|
287
|
+
return { localPort: existing.localPort, pid: existing.pid, name: config.name };
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'testing' } });
|
|
266
291
|
|
|
292
|
+
// For known servers, skip the full test — tunnel first, check version after
|
|
267
293
|
let testResult;
|
|
268
294
|
if (opts.skipTest && opts.testResult) {
|
|
269
295
|
testResult = opts.testResult;
|
|
296
|
+
} else if (config.lastConnected && opts.skipTest !== false) {
|
|
297
|
+
testResult = { reachable: true, daemonRunning: true, grooveInstalled: true, remoteVersion: null };
|
|
270
298
|
} else {
|
|
271
299
|
testResult = await this.test(id);
|
|
272
300
|
}
|
|
@@ -274,22 +302,19 @@ export class TunnelManager {
|
|
|
274
302
|
throw new Error(testResult.error || 'Host unreachable');
|
|
275
303
|
}
|
|
276
304
|
|
|
305
|
+
// First-time only: install groove if missing, start daemon if not running
|
|
277
306
|
let preConnectHandled = false;
|
|
278
307
|
if (!testResult.daemonRunning && !testResult.grooveInstalled) {
|
|
279
308
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'installing' } });
|
|
280
309
|
await this.remoteInstall(id);
|
|
281
310
|
preConnectHandled = true;
|
|
282
311
|
} else if (!testResult.daemonRunning && testResult.grooveInstalled) {
|
|
283
|
-
const localVer = getLocalVersion();
|
|
284
|
-
if (testResult.remoteVersion && testResult.remoteVersion !== localVer) {
|
|
285
|
-
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: testResult.remoteVersion, to: localVer } });
|
|
286
|
-
await this._remoteUpgrade(id, config);
|
|
287
|
-
}
|
|
288
312
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
289
313
|
await this.autoStart(id);
|
|
290
314
|
preConnectHandled = true;
|
|
291
315
|
}
|
|
292
316
|
|
|
317
|
+
// Establish SSH tunnel
|
|
293
318
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'connecting' } });
|
|
294
319
|
|
|
295
320
|
const localPort = await this._findAvailablePort();
|
|
@@ -345,9 +370,7 @@ export class TunnelManager {
|
|
|
345
370
|
failCount: 0,
|
|
346
371
|
});
|
|
347
372
|
|
|
348
|
-
// Verify
|
|
349
|
-
// The cached test result (line 270) assumes daemonRunning=true based on
|
|
350
|
-
// lastConnected, but the daemon may have stopped since then.
|
|
373
|
+
// Verify daemon is reachable through tunnel, start if needed
|
|
351
374
|
let remoteAlive = false;
|
|
352
375
|
try {
|
|
353
376
|
const probe = await fetch(`http://localhost:${localPort}/api/health`, {
|
|
@@ -359,7 +382,6 @@ export class TunnelManager {
|
|
|
359
382
|
if (!remoteAlive && config.autoStart) {
|
|
360
383
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
361
384
|
await this.autoStart(id);
|
|
362
|
-
// Give the daemon a moment to accept connections through the tunnel
|
|
363
385
|
for (let i = 0; i < 5; i++) {
|
|
364
386
|
await new Promise(r => setTimeout(r, 1000));
|
|
365
387
|
try {
|
|
@@ -373,9 +395,9 @@ export class TunnelManager {
|
|
|
373
395
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'waiting', message: 'Remote daemon not running. Start it manually or enable auto-start.' } });
|
|
374
396
|
}
|
|
375
397
|
|
|
376
|
-
|
|
377
|
-
if (remoteAlive && !preConnectHandled
|
|
378
|
-
|
|
398
|
+
// Auto-upgrade: check version through tunnel, upgrade if behind (non-blocking)
|
|
399
|
+
if (remoteAlive && !preConnectHandled) {
|
|
400
|
+
this._checkAndUpgradeRunning(id, config, localPort).catch(() => {});
|
|
379
401
|
}
|
|
380
402
|
|
|
381
403
|
const remoteVer = testResult?.remoteVersion || null;
|
|
@@ -428,98 +450,93 @@ export class TunnelManager {
|
|
|
428
450
|
}
|
|
429
451
|
|
|
430
452
|
async _checkAndUpgradeRunning(id, config, localPort) {
|
|
431
|
-
const localVer = getLocalVersion();
|
|
432
|
-
if (localVer === '0.0.0') return;
|
|
433
|
-
|
|
434
453
|
try {
|
|
454
|
+
// Get remote daemon version
|
|
435
455
|
const resp = await fetch(`http://localhost:${localPort}/api/status`, {
|
|
436
456
|
signal: AbortSignal.timeout(5000),
|
|
437
457
|
});
|
|
438
458
|
if (!resp.ok) return;
|
|
439
459
|
const status = await resp.json();
|
|
440
|
-
const
|
|
441
|
-
if (!
|
|
442
|
-
|
|
443
|
-
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: oldVersion, to: localVer } });
|
|
460
|
+
const remoteVer = status.version;
|
|
461
|
+
if (!remoteVer) return;
|
|
444
462
|
|
|
463
|
+
// Check latest version on npm (from the remote server)
|
|
445
464
|
const target = `${config.user}@${config.host}`;
|
|
446
465
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
447
466
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
448
|
-
const pinnedPkg = `groove-dev@${localVer}`;
|
|
449
|
-
const installCmd = config.user === 'root' ? `npm i -g --prefer-online ${pinnedPkg}` : `sudo npm i -g --prefer-online ${pinnedPkg}`;
|
|
450
467
|
|
|
468
|
+
let npmVer;
|
|
451
469
|
try {
|
|
452
|
-
execFileSync('ssh', [...sshBase, sshCmd(
|
|
453
|
-
encoding: 'utf8',
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
encoding: 'utf8',
|
|
462
|
-
timeout: 120000,
|
|
463
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
464
|
-
});
|
|
470
|
+
npmVer = execFileSync('ssh', [...sshBase, sshCmd('npm view groove-dev version 2>/dev/null')], {
|
|
471
|
+
encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
472
|
+
}).trim();
|
|
473
|
+
} catch { return; }
|
|
474
|
+
|
|
475
|
+
if (!npmVer || npmVer === remoteVer) {
|
|
476
|
+
const localVer = getLocalVersion();
|
|
477
|
+
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: remoteVer, match: remoteVer === localVer } });
|
|
478
|
+
return;
|
|
465
479
|
}
|
|
466
480
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const installedVer = verOutput.replace(/[^0-9.]/g, '') || verOutput.trim();
|
|
481
|
+
// Remote is behind npm — upgrade
|
|
482
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: remoteVer, to: npmVer } });
|
|
483
|
+
|
|
484
|
+
const installCmd = npmGlobalInstall(`groove-dev@${npmVer}`, config.user);
|
|
485
|
+
const cleanupCmd = 'rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true';
|
|
473
486
|
|
|
474
|
-
|
|
475
|
-
|
|
487
|
+
try {
|
|
488
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
489
|
+
encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
490
|
+
});
|
|
491
|
+
} catch (err) {
|
|
492
|
+
const errOutput = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
493
|
+
if (errOutput.includes('ENOTEMPTY')) {
|
|
494
|
+
execFileSync('ssh', [...sshBase, sshCmd(cleanupCmd)], { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
495
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], { encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
496
|
+
} else {
|
|
497
|
+
throw err;
|
|
498
|
+
}
|
|
476
499
|
}
|
|
477
500
|
|
|
501
|
+
// Restart remote daemon
|
|
478
502
|
const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
|
|
479
503
|
const setProjectDir = config.projectDir
|
|
480
504
|
? `curl -sf -X POST -H 'Content-Type: application/json' --data '{"path":"${config.projectDir}"}' http://localhost:${REMOTE_PORT}/api/project-dir > /dev/null 2>&1 || true; `
|
|
481
505
|
: '';
|
|
482
506
|
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/status && (${setProjectDir}true) || true`;
|
|
483
|
-
|
|
484
|
-
encoding: 'utf8',
|
|
485
|
-
timeout: 60000,
|
|
486
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
507
|
+
execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
|
|
508
|
+
encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
487
509
|
});
|
|
488
510
|
|
|
511
|
+
// Verify through tunnel
|
|
489
512
|
let daemonVer = null;
|
|
490
|
-
try { daemonVer = JSON.parse(restartResult.trim()).version || null; } catch { /* parse failed */ }
|
|
491
|
-
|
|
492
513
|
for (let i = 0; i < 3; i++) {
|
|
493
514
|
try {
|
|
494
515
|
const check = await fetch(`http://localhost:${localPort}/api/status`, {
|
|
495
516
|
signal: AbortSignal.timeout(3000),
|
|
496
517
|
});
|
|
497
518
|
if (check.ok) {
|
|
498
|
-
|
|
499
|
-
daemonVer = checkData.version || daemonVer;
|
|
519
|
+
daemonVer = (await check.json()).version || null;
|
|
500
520
|
break;
|
|
501
521
|
}
|
|
502
522
|
} catch { /* retry */ }
|
|
503
523
|
await new Promise(r => setTimeout(r, 2000));
|
|
504
524
|
}
|
|
505
525
|
|
|
526
|
+
const localVer = getLocalVersion();
|
|
506
527
|
if (daemonVer) {
|
|
507
528
|
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: daemonVer, match: daemonVer === localVer } });
|
|
508
529
|
} else {
|
|
509
|
-
this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: 'Daemon did not respond after restart', from:
|
|
530
|
+
this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: 'Daemon did not respond after restart', from: remoteVer, attempted: npmVer } });
|
|
510
531
|
}
|
|
511
532
|
|
|
512
|
-
this.daemon.audit.log('tunnel.upgrade', { id, from:
|
|
533
|
+
this.daemon.audit.log('tunnel.upgrade', { id, from: remoteVer, to: daemonVer || npmVer });
|
|
513
534
|
} catch (err) {
|
|
514
535
|
try {
|
|
515
536
|
const verify = await fetch(`http://localhost:${localPort}/api/status`, { signal: AbortSignal.timeout(5000) });
|
|
516
537
|
if (verify.ok) {
|
|
517
538
|
const verifyData = await verify.json();
|
|
518
|
-
|
|
519
|
-
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: verifyData.version, match: true } });
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
this.daemon.broadcast({ type: 'tunnel.version-mismatch', data: { id, localVersion: localVer, remoteVersion: verifyData.version, message: 'Upgrade timed out but remote is reachable' } });
|
|
539
|
+
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: getLocalVersion(), remoteVersion: verifyData.version, match: false } });
|
|
523
540
|
return;
|
|
524
541
|
}
|
|
525
542
|
} catch { /* tunnel verification failed */ }
|
|
@@ -533,7 +550,7 @@ export class TunnelManager {
|
|
|
533
550
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
534
551
|
const localVer = getLocalVersion();
|
|
535
552
|
const pkg = localVer !== '0.0.0' ? `groove-dev@${localVer}` : 'groove-dev';
|
|
536
|
-
const installCmd = config.user
|
|
553
|
+
const installCmd = npmGlobalInstall(pkg, config.user);
|
|
537
554
|
|
|
538
555
|
let usedFallback = false;
|
|
539
556
|
try {
|
|
@@ -554,7 +571,7 @@ export class TunnelManager {
|
|
|
554
571
|
}
|
|
555
572
|
} else {
|
|
556
573
|
if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
557
|
-
const fallbackCmd =
|
|
574
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
558
575
|
try {
|
|
559
576
|
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
560
577
|
encoding: 'utf8',
|
|
@@ -565,6 +582,7 @@ export class TunnelManager {
|
|
|
565
582
|
} catch { /* fall through to original error */ }
|
|
566
583
|
}
|
|
567
584
|
if (!usedFallback) {
|
|
585
|
+
if (isPermissionError(errOutput)) throw new Error(PERMISSION_HINT);
|
|
568
586
|
throw new Error(`Remote upgrade failed: ${errOutput.slice(-400)}`);
|
|
569
587
|
}
|
|
570
588
|
}
|
|
@@ -670,12 +688,10 @@ export class TunnelManager {
|
|
|
670
688
|
throw new Error(`Failed to check remote environment: ${err.message}`);
|
|
671
689
|
}
|
|
672
690
|
|
|
673
|
-
// Step 2: Install groove-dev globally (
|
|
691
|
+
// Step 2: Install groove-dev globally (try user-space first, sudo fallback)
|
|
674
692
|
const localVer = getLocalVersion();
|
|
675
693
|
const pkg = localVer !== '0.0.0' ? `groove-dev@${localVer}` : 'groove-dev';
|
|
676
|
-
const installCmd = config.user
|
|
677
|
-
? `npm i -g --prefer-online ${pkg}`
|
|
678
|
-
: `sudo npm i -g --prefer-online ${pkg}`;
|
|
694
|
+
const installCmd = npmGlobalInstall(pkg, config.user);
|
|
679
695
|
|
|
680
696
|
try {
|
|
681
697
|
execFileSync('ssh', [
|
|
@@ -697,7 +713,7 @@ export class TunnelManager {
|
|
|
697
713
|
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
698
714
|
}
|
|
699
715
|
} else if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
700
|
-
const fallbackCmd =
|
|
716
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
701
717
|
try {
|
|
702
718
|
execFileSync('ssh', [...sshBase, remoteCmd(fallbackCmd)], {
|
|
703
719
|
encoding: 'utf8',
|
|
@@ -706,9 +722,11 @@ export class TunnelManager {
|
|
|
706
722
|
});
|
|
707
723
|
} catch (err2) {
|
|
708
724
|
const output = err2.stdout?.toString() || err2.stderr?.toString() || err2.message;
|
|
725
|
+
if (isPermissionError(output)) throw new Error(PERMISSION_HINT);
|
|
709
726
|
throw new Error(`npm install failed: ${output.slice(-400)}`);
|
|
710
727
|
}
|
|
711
728
|
} else {
|
|
729
|
+
if (isPermissionError(errOutput)) throw new Error(PERMISSION_HINT);
|
|
712
730
|
throw new Error(`npm install failed: ${errOutput.slice(-400)}`);
|
|
713
731
|
}
|
|
714
732
|
}
|
|
@@ -752,7 +770,7 @@ export class TunnelManager {
|
|
|
752
770
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
753
771
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
754
772
|
const pinnedPkg = `groove-dev@${localVer}`;
|
|
755
|
-
const installCmd = config.user
|
|
773
|
+
const installCmd = npmGlobalInstall(pinnedPkg, config.user);
|
|
756
774
|
|
|
757
775
|
try {
|
|
758
776
|
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
@@ -771,7 +789,7 @@ export class TunnelManager {
|
|
|
771
789
|
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
772
790
|
}
|
|
773
791
|
} else {
|
|
774
|
-
const fallbackCmd =
|
|
792
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
775
793
|
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
776
794
|
encoding: 'utf8',
|
|
777
795
|
timeout: 120000,
|