groove-dev 0.27.113 → 0.27.116
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +6 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +27 -2
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/process.js +254 -208
- package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
- package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +59 -2
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +27 -2
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/process.js +254 -208
- package/packages/daemon/src/teams.js +143 -20
- package/packages/daemon/src/tunnel-manager.js +78 -45
- package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/packages/gui/src/stores/groove.js +57 -12
- package/packages/gui/src/views/agents.jsx +23 -4
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +84 -5
- package/TRAINING_DATA_v3.md +0 -11
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
- package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
- package/codex-test/offroad-nitro-racer/index.html +0 -21
- package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
- package/codex-test/offroad-nitro-racer/package.json +0 -15
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
- package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
- package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
- package/codex-test/offroad-nitro-racer/src/style.css +0 -291
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
- package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
- package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// GROOVE — Teams (Live Agent Groups)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from 'fs';
|
|
5
|
-
import { resolve } from 'path';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
|
|
5
|
+
import { resolve, basename } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { validateTeamName } from './validate.js';
|
|
8
8
|
|
|
@@ -141,29 +141,67 @@ export class Teams {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* can wipe accumulated state and keep working without restarting the daemon.
|
|
144
|
+
* Archive a team — kills its agents, moves its directory to archived-teams/,
|
|
145
|
+
* stores metadata.json for later restore.
|
|
147
146
|
*/
|
|
148
|
-
|
|
147
|
+
archive(id) {
|
|
149
148
|
const team = this.teams.get(id);
|
|
150
149
|
if (!team) throw new Error('Team not found');
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
const agents = this._killAndRemoveAgents(id);
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
team.workingDir &&
|
|
155
|
+
team.workingDir !== this.daemon.projectDir &&
|
|
156
|
+
existsSync(team.workingDir)
|
|
157
|
+
) {
|
|
158
|
+
try {
|
|
159
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
160
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
161
|
+
const slug = basename(team.workingDir);
|
|
162
|
+
const archiveName = `${slug}-${Date.now()}`;
|
|
163
|
+
const archivePath = resolve(archiveDir, archiveName);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
renameSync(team.workingDir, archivePath);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.code === 'EXDEV') {
|
|
169
|
+
cpSync(team.workingDir, archivePath, { recursive: true });
|
|
170
|
+
rmSync(team.workingDir, { recursive: true, force: true });
|
|
171
|
+
} else {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const metadata = {
|
|
177
|
+
originalName: team.name,
|
|
178
|
+
originalId: team.id,
|
|
179
|
+
deletedAt: new Date().toISOString(),
|
|
180
|
+
agentCount: agents.length,
|
|
181
|
+
originalWorkingDir: team.workingDir,
|
|
182
|
+
};
|
|
183
|
+
writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
|
|
157
186
|
}
|
|
158
187
|
}
|
|
159
188
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
189
|
+
this._removeTeamAndCleanup(team, id);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete a team — kills its agents, removes its directory permanently.
|
|
195
|
+
* If permanent is false (default), delegates to archive() instead.
|
|
196
|
+
*/
|
|
197
|
+
delete(id, { permanent = false } = {}) {
|
|
198
|
+
if (!permanent) return this.archive(id);
|
|
199
|
+
|
|
200
|
+
const team = this.teams.get(id);
|
|
201
|
+
if (!team) throw new Error('Team not found');
|
|
202
|
+
|
|
203
|
+
this._killAndRemoveAgents(id);
|
|
164
204
|
|
|
165
|
-
// Remove the team's working directory — refuse to nuke the project root
|
|
166
|
-
// (legacy default teams that were never migrated point there).
|
|
167
205
|
if (
|
|
168
206
|
team.workingDir &&
|
|
169
207
|
team.workingDir !== this.daemon.projectDir &&
|
|
@@ -172,24 +210,109 @@ export class Teams {
|
|
|
172
210
|
try {
|
|
173
211
|
rmSync(team.workingDir, { recursive: true, force: true });
|
|
174
212
|
} catch (err) {
|
|
175
|
-
console.log(`[Groove:Teams] Failed to
|
|
213
|
+
console.log(`[Groove:Teams] Failed to delete directory: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this._removeTeamAndCleanup(team, id);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_killAndRemoveAgents(teamId) {
|
|
222
|
+
const agents = this.daemon.registry.getAll().filter((a) => a.teamId === teamId);
|
|
223
|
+
for (const agent of agents) {
|
|
224
|
+
if (agent.status === 'running' || agent.status === 'starting') {
|
|
225
|
+
try { this.daemon.processes.kill(agent.id); } catch { /* ignore */ }
|
|
176
226
|
}
|
|
177
227
|
}
|
|
228
|
+
for (const agent of agents) {
|
|
229
|
+
this.daemon.registry.remove(agent.id);
|
|
230
|
+
}
|
|
231
|
+
return agents;
|
|
232
|
+
}
|
|
178
233
|
|
|
234
|
+
_removeTeamAndCleanup(team, id) {
|
|
179
235
|
this.teams.delete(id);
|
|
180
236
|
this._save();
|
|
181
237
|
this.daemon.broadcast({ type: 'team:deleted', teamId: id });
|
|
182
238
|
|
|
183
|
-
// Always keep a default team available — regenerate one with a clean folder
|
|
184
239
|
if (team.isDefault) {
|
|
185
240
|
this._ensureDefault();
|
|
186
241
|
const fresh = this.getDefault();
|
|
187
242
|
if (fresh) this.daemon.broadcast({ type: 'team:created', team: fresh });
|
|
188
243
|
}
|
|
189
244
|
|
|
190
|
-
// Clean up orphaned logs immediately — don't wait for the 24h GC cycle
|
|
191
245
|
try { this.daemon._gc(); } catch { /* gc should never block deletion */ }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
listArchived() {
|
|
249
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
250
|
+
if (!existsSync(archiveDir)) return [];
|
|
251
|
+
const entries = readdirSync(archiveDir, { withFileTypes: true });
|
|
252
|
+
const result = [];
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!entry.isDirectory()) continue;
|
|
255
|
+
const metaPath = resolve(archiveDir, entry.name, 'metadata.json');
|
|
256
|
+
try {
|
|
257
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
258
|
+
result.push({ id: entry.name, ...meta });
|
|
259
|
+
} catch {
|
|
260
|
+
result.push({ id: entry.name, originalName: entry.name, deletedAt: null, agentCount: 0 });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
restore(archivedId) {
|
|
267
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
268
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
269
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
270
|
+
|
|
271
|
+
let meta = {};
|
|
272
|
+
const metaPath = resolve(archivePath, 'metadata.json');
|
|
273
|
+
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
|
|
274
|
+
|
|
275
|
+
const name = meta.originalName || archivedId;
|
|
276
|
+
let workingDir = meta.originalWorkingDir || resolve(this.daemon.projectDir, slugify(name));
|
|
277
|
+
|
|
278
|
+
if (existsSync(workingDir)) {
|
|
279
|
+
workingDir = resolve(this.daemon.projectDir, `${slugify(name)}-${Date.now()}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
renameSync(archivePath, workingDir);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (err.code === 'EXDEV') {
|
|
286
|
+
cpSync(archivePath, workingDir, { recursive: true });
|
|
287
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
288
|
+
} else {
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Remove the metadata file from the restored directory
|
|
294
|
+
const restoredMetaPath = resolve(workingDir, 'metadata.json');
|
|
295
|
+
try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
|
|
296
|
+
|
|
297
|
+
const id = randomUUID().slice(0, 8);
|
|
298
|
+
const team = {
|
|
299
|
+
id,
|
|
300
|
+
name,
|
|
301
|
+
isDefault: false,
|
|
302
|
+
workingDir,
|
|
303
|
+
createdAt: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
this.teams.set(id, team);
|
|
306
|
+
this._save();
|
|
307
|
+
this.daemon.broadcast({ type: 'team:created', team });
|
|
308
|
+
return team;
|
|
309
|
+
}
|
|
192
310
|
|
|
311
|
+
purge(archivedId) {
|
|
312
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
313
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
314
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
315
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
193
316
|
return true;
|
|
194
317
|
}
|
|
195
318
|
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
|
|
6
|
-
import { resolve
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
import { resolve } from 'path';
|
|
8
7
|
import { createConnection } from 'net';
|
|
9
8
|
import crypto from 'crypto';
|
|
10
9
|
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
10
|
function getLocalVersion() {
|
|
13
11
|
try {
|
|
14
|
-
const pkg = JSON.parse(readFileSync(
|
|
12
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
13
|
return pkg.version || '0.0.0';
|
|
16
14
|
} catch { return '0.0.0'; }
|
|
17
15
|
}
|
|
@@ -34,6 +32,11 @@ function validateField(value, name) {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
function sshCmd(cmd) {
|
|
36
|
+
const nvmProbe = 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; ';
|
|
37
|
+
return `bash -lc '${nvmProbe}${cmd}'`;
|
|
38
|
+
}
|
|
39
|
+
|
|
37
40
|
export class TunnelManager {
|
|
38
41
|
constructor(daemon) {
|
|
39
42
|
this.daemon = daemon;
|
|
@@ -217,7 +220,7 @@ export class TunnelManager {
|
|
|
217
220
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
218
221
|
'-o', 'BatchMode=yes',
|
|
219
222
|
target,
|
|
220
|
-
`
|
|
223
|
+
sshCmd(`S=$(curl -sf http://localhost:${REMOTE_PORT}/api/status 2>/dev/null); if [ -n "$S" ]; then echo "__GROOVE_RUNNING__$S__GROOVE_END__"; else which groove >/dev/null 2>&1 && echo __GROOVE_VER__$(groove --version 2>/dev/null || echo unknown)__GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__; fi`),
|
|
221
224
|
], {
|
|
222
225
|
encoding: 'utf8',
|
|
223
226
|
timeout: 20000,
|
|
@@ -412,7 +415,7 @@ export class TunnelManager {
|
|
|
412
415
|
const installCmd = config.user === 'root' ? `npm i -g --prefer-online ${pinnedPkg}` : `sudo npm i -g --prefer-online ${pinnedPkg}`;
|
|
413
416
|
|
|
414
417
|
try {
|
|
415
|
-
execFileSync('ssh', [...sshBase,
|
|
418
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
416
419
|
encoding: 'utf8',
|
|
417
420
|
timeout: 120000,
|
|
418
421
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -420,14 +423,14 @@ export class TunnelManager {
|
|
|
420
423
|
} catch {
|
|
421
424
|
const fallbackPkg = 'groove-dev';
|
|
422
425
|
const fallbackCmd = config.user === 'root' ? `npm i -g --prefer-online ${fallbackPkg}` : `sudo npm i -g --prefer-online ${fallbackPkg}`;
|
|
423
|
-
execFileSync('ssh', [...sshBase,
|
|
426
|
+
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
424
427
|
encoding: 'utf8',
|
|
425
428
|
timeout: 120000,
|
|
426
429
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
427
430
|
});
|
|
428
431
|
}
|
|
429
432
|
|
|
430
|
-
const verOutput = execFileSync('ssh', [...sshBase,
|
|
433
|
+
const verOutput = execFileSync('ssh', [...sshBase, sshCmd('groove --version')], {
|
|
431
434
|
encoding: 'utf8',
|
|
432
435
|
timeout: 10000,
|
|
433
436
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -438,8 +441,8 @@ export class TunnelManager {
|
|
|
438
441
|
this.daemon.broadcast({ type: 'tunnel.version-mismatch', data: { id, localVersion: localVer, remoteVersion: installedVer, message: 'Pinned version not available on npm, installed latest' } });
|
|
439
442
|
}
|
|
440
443
|
|
|
441
|
-
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2;
|
|
442
|
-
const restartResult = execFileSync('ssh', [...sshBase,
|
|
444
|
+
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; 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`;
|
|
445
|
+
const restartResult = execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
|
|
443
446
|
encoding: 'utf8',
|
|
444
447
|
timeout: 60000,
|
|
445
448
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -484,30 +487,40 @@ export class TunnelManager {
|
|
|
484
487
|
|
|
485
488
|
let usedFallback = false;
|
|
486
489
|
try {
|
|
487
|
-
execFileSync('ssh', [...sshBase,
|
|
490
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
488
491
|
encoding: 'utf8',
|
|
489
492
|
timeout: 120000,
|
|
490
493
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
491
494
|
});
|
|
492
495
|
} catch (err) {
|
|
493
|
-
|
|
494
|
-
|
|
496
|
+
const errOutput = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
497
|
+
if (errOutput.includes('ENOTEMPTY')) {
|
|
495
498
|
try {
|
|
496
|
-
execFileSync('ssh', [...sshBase,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
499
|
+
execFileSync('ssh', [...sshBase, sshCmd('rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true')], { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
500
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], { encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
501
|
+
} catch (retryErr) {
|
|
502
|
+
const retryOutput = retryErr.stdout?.toString() || retryErr.stderr?.toString() || retryErr.message;
|
|
503
|
+
throw new Error(`Remote upgrade failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
507
|
+
const fallbackCmd = config.user === 'root' ? 'npm i -g --prefer-online groove-dev' : 'sudo npm i -g --prefer-online groove-dev';
|
|
508
|
+
try {
|
|
509
|
+
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
510
|
+
encoding: 'utf8',
|
|
511
|
+
timeout: 120000,
|
|
512
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
513
|
+
});
|
|
514
|
+
usedFallback = true;
|
|
515
|
+
} catch { /* fall through to original error */ }
|
|
516
|
+
}
|
|
517
|
+
if (!usedFallback) {
|
|
518
|
+
throw new Error(`Remote upgrade failed: ${errOutput.slice(-400)}`);
|
|
519
|
+
}
|
|
507
520
|
}
|
|
508
521
|
}
|
|
509
522
|
|
|
510
|
-
const verOutput = execFileSync('ssh', [...sshBase,
|
|
523
|
+
const verOutput = execFileSync('ssh', [...sshBase, sshCmd('groove --version')], {
|
|
511
524
|
encoding: 'utf8',
|
|
512
525
|
timeout: 10000,
|
|
513
526
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -537,7 +550,7 @@ export class TunnelManager {
|
|
|
537
550
|
? `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; `
|
|
538
551
|
: '';
|
|
539
552
|
const remoteCmd =
|
|
540
|
-
`${cdPrefix}
|
|
553
|
+
`${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; ` +
|
|
541
554
|
`sleep 5; ` +
|
|
542
555
|
`curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null ` +
|
|
543
556
|
`&& (${setProjectDir}echo __DAEMON_OK__) ` +
|
|
@@ -550,7 +563,7 @@ export class TunnelManager {
|
|
|
550
563
|
'-o', 'ConnectTimeout=10',
|
|
551
564
|
'-o', 'BatchMode=yes',
|
|
552
565
|
target,
|
|
553
|
-
|
|
566
|
+
sshCmd(remoteCmd),
|
|
554
567
|
], {
|
|
555
568
|
encoding: 'utf8',
|
|
556
569
|
timeout: 45000,
|
|
@@ -564,8 +577,9 @@ export class TunnelManager {
|
|
|
564
577
|
}
|
|
565
578
|
} catch (err) {
|
|
566
579
|
if (err.message.includes('Remote daemon failed')) throw err;
|
|
567
|
-
const output = err.stdout?.toString() || err.stderr?.toString() ||
|
|
568
|
-
|
|
580
|
+
const output = err.stdout?.toString() || err.stderr?.toString() || '';
|
|
581
|
+
if (output.includes('__DAEMON_OK__')) return;
|
|
582
|
+
throw new Error(`Failed to start remote daemon: ${(output || err.message).slice(-300)}`);
|
|
569
583
|
}
|
|
570
584
|
}
|
|
571
585
|
|
|
@@ -585,7 +599,7 @@ export class TunnelManager {
|
|
|
585
599
|
|
|
586
600
|
// Non-interactive SSH doesn't source shell profiles, so npm/node may not be on PATH.
|
|
587
601
|
// Use a login shell (-l) to get the user's full environment.
|
|
588
|
-
const remoteCmd = (cmd) =>
|
|
602
|
+
const remoteCmd = (cmd) => sshCmd(cmd);
|
|
589
603
|
|
|
590
604
|
// Step 1: Check if node/npm are available
|
|
591
605
|
try {
|
|
@@ -623,7 +637,16 @@ export class TunnelManager {
|
|
|
623
637
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
624
638
|
});
|
|
625
639
|
} catch (err) {
|
|
626
|
-
|
|
640
|
+
const errOutput = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
641
|
+
if (errOutput.includes('ENOTEMPTY')) {
|
|
642
|
+
try {
|
|
643
|
+
execFileSync('ssh', [...sshBase, remoteCmd('rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true')], { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
644
|
+
execFileSync('ssh', [...sshBase, remoteCmd(installCmd)], { encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
645
|
+
} catch (retryErr) {
|
|
646
|
+
const retryOutput = retryErr.stdout?.toString() || retryErr.stderr?.toString() || retryErr.message;
|
|
647
|
+
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
648
|
+
}
|
|
649
|
+
} else if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
627
650
|
const fallbackCmd = config.user === 'root' ? 'npm i -g --prefer-online groove-dev' : 'sudo npm i -g --prefer-online groove-dev';
|
|
628
651
|
try {
|
|
629
652
|
execFileSync('ssh', [...sshBase, remoteCmd(fallbackCmd)], {
|
|
@@ -636,8 +659,7 @@ export class TunnelManager {
|
|
|
636
659
|
throw new Error(`npm install failed: ${output.slice(-400)}`);
|
|
637
660
|
}
|
|
638
661
|
} else {
|
|
639
|
-
|
|
640
|
-
throw new Error(`npm install failed: ${output.slice(-400)}`);
|
|
662
|
+
throw new Error(`npm install failed: ${errOutput.slice(-400)}`);
|
|
641
663
|
}
|
|
642
664
|
}
|
|
643
665
|
|
|
@@ -645,7 +667,7 @@ export class TunnelManager {
|
|
|
645
667
|
try {
|
|
646
668
|
const result = execFileSync('ssh', [
|
|
647
669
|
...sshBase,
|
|
648
|
-
remoteCmd(`
|
|
670
|
+
remoteCmd(`GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)`),
|
|
649
671
|
], {
|
|
650
672
|
encoding: 'utf8',
|
|
651
673
|
timeout: 45000,
|
|
@@ -683,29 +705,40 @@ export class TunnelManager {
|
|
|
683
705
|
const installCmd = config.user === 'root' ? `npm i -g --prefer-online ${pinnedPkg}` : `sudo npm i -g --prefer-online ${pinnedPkg}`;
|
|
684
706
|
|
|
685
707
|
try {
|
|
686
|
-
execFileSync('ssh', [...sshBase,
|
|
687
|
-
encoding: 'utf8',
|
|
688
|
-
timeout: 120000,
|
|
689
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
690
|
-
});
|
|
691
|
-
} catch {
|
|
692
|
-
const fallbackCmd = config.user === 'root' ? 'npm i -g --prefer-online groove-dev' : 'sudo npm i -g --prefer-online groove-dev';
|
|
693
|
-
execFileSync('ssh', [...sshBase, `bash -lc '${fallbackCmd}'`], {
|
|
708
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
694
709
|
encoding: 'utf8',
|
|
695
710
|
timeout: 120000,
|
|
696
711
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
697
712
|
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const errOutput = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
715
|
+
if (errOutput.includes('ENOTEMPTY')) {
|
|
716
|
+
try {
|
|
717
|
+
execFileSync('ssh', [...sshBase, sshCmd('rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true')], { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
718
|
+
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], { encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
719
|
+
} catch (retryErr) {
|
|
720
|
+
const retryOutput = retryErr.stdout?.toString() || retryErr.stderr?.toString() || retryErr.message;
|
|
721
|
+
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
const fallbackCmd = config.user === 'root' ? 'npm i -g --prefer-online groove-dev' : 'sudo npm i -g --prefer-online groove-dev';
|
|
725
|
+
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
726
|
+
encoding: 'utf8',
|
|
727
|
+
timeout: 120000,
|
|
728
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
729
|
+
});
|
|
730
|
+
}
|
|
698
731
|
}
|
|
699
732
|
|
|
700
|
-
const verOutput = execFileSync('ssh', [...sshBase,
|
|
733
|
+
const verOutput = execFileSync('ssh', [...sshBase, sshCmd('groove --version')], {
|
|
701
734
|
encoding: 'utf8',
|
|
702
735
|
timeout: 10000,
|
|
703
736
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
704
737
|
}).trim();
|
|
705
738
|
const installedVer = verOutput.replace(/[^0-9.]/g, '') || verOutput.trim();
|
|
706
739
|
|
|
707
|
-
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2;
|
|
708
|
-
const restartResult = execFileSync('ssh', [...sshBase,
|
|
740
|
+
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; 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`;
|
|
741
|
+
const restartResult = execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
|
|
709
742
|
encoding: 'utf8',
|
|
710
743
|
timeout: 60000,
|
|
711
744
|
stdio: ['pipe', 'pipe', 'pipe'],
|