groove-dev 0.27.155 → 0.27.157
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/CLAUDE.md +0 -7
- 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/rotator.js +2 -1
- package/node_modules/@groove-dev/daemon/src/routes/files.js +54 -10
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -12
- package/node_modules/@groove-dev/gui/dist/assets/{index-BTLb6zTD.js → index-B6taUF7J.js} +42 -42
- package/node_modules/@groove-dev/gui/dist/assets/index-BAM0QzR0.css +1 -0
- 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/agent-file-tree.jsx +12 -14
- 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/editor/file-tree.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +46 -19
- 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/rotator.js +2 -1
- package/packages/daemon/src/routes/files.js +54 -10
- package/packages/daemon/src/tunnel-manager.js +24 -12
- package/packages/gui/dist/assets/{index-BTLb6zTD.js → index-B6taUF7J.js} +42 -42
- package/packages/gui/dist/assets/index-BAM0QzR0.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -14
- 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/editor/file-tree.jsx +6 -8
- package/packages/gui/src/views/memory.jsx +46 -19
- package/ssh/error.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +0 -1
- package/packages/gui/dist/assets/index-Diw6wDPU.css +0 -1
package/CLAUDE.md
CHANGED
|
@@ -295,10 +295,3 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
|
|
|
295
295
|
- Dashboard: routing donut, cache panel, context health gauges
|
|
296
296
|
- Monitor/QC agent mode (stay active, loop)
|
|
297
297
|
- Distribution: demo video, HN launch, Twitter content
|
|
298
|
-
|
|
299
|
-
<!-- GROOVE:START -->
|
|
300
|
-
## GROOVE Orchestration (auto-injected)
|
|
301
|
-
Active agents: 0
|
|
302
|
-
See AGENTS_REGISTRY.md for full agent state.
|
|
303
|
-
**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
|
|
304
|
-
<!-- GROOVE:END -->
|
|
@@ -483,6 +483,9 @@ export class Journalist {
|
|
|
483
483
|
'Be specific. Name files, functions, and line numbers. Do not summarize vaguely.',
|
|
484
484
|
'Preserve the investigation narrative — the next agent needs to understand the',
|
|
485
485
|
'journey, not just the destination.',
|
|
486
|
+
'IMPORTANT: Focus on what the user ASKED the agent to do (the original task).',
|
|
487
|
+
'Do NOT include observations about unrelated code issues, potential improvements,',
|
|
488
|
+
'or things the agent noticed but did not act on. The next agent must stay on task.',
|
|
486
489
|
'Keep your response under 4000 characters.',
|
|
487
490
|
'',
|
|
488
491
|
'---',
|
|
@@ -1024,7 +1027,13 @@ export class Journalist {
|
|
|
1024
1027
|
originalTask ? `## Original Task\n\n${originalTask}\n` : '',
|
|
1025
1028
|
``,
|
|
1026
1029
|
agent.role === 'planner' ? 'CRITICAL: You are a PLANNING ONLY agent. Do NOT implement code. Route all work to your team via .groove/recommended-team.json.\n' : '',
|
|
1027
|
-
|
|
1030
|
+
`## Instructions`,
|
|
1031
|
+
``,
|
|
1032
|
+
`Continue and finish the in-progress task — deliver the output. Stay focused on that specific task only.`,
|
|
1033
|
+
`- Do NOT explore the codebase looking for other things to fix or improve`,
|
|
1034
|
+
`- Do NOT start new work outside the original task scope`,
|
|
1035
|
+
`- Do NOT act on TODO comments, code quality issues, or improvements you notice in passing`,
|
|
1036
|
+
`- If the task is already complete, report what was accomplished and STOP — await new instructions from the user`,
|
|
1028
1037
|
].filter(Boolean).join('\n');
|
|
1029
1038
|
|
|
1030
1039
|
// Hard cap: 16000 chars — investigation context needs room to preserve the full narrative
|
|
@@ -1142,34 +1151,60 @@ export class Journalist {
|
|
|
1142
1151
|
* Build a full context-resume prompt that preserves the conversation
|
|
1143
1152
|
* thread so a fresh agent picks up where the previous session left off.
|
|
1144
1153
|
*/
|
|
1145
|
-
buildConversationResumePrompt(agent, userMessage) {
|
|
1154
|
+
buildConversationResumePrompt(agent, userMessage, { isRotation = false, reason } = {}) {
|
|
1146
1155
|
const thread = this.extractConversationThread(agent);
|
|
1147
1156
|
if (!thread) return null;
|
|
1148
1157
|
|
|
1149
1158
|
const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
|
|
1150
1159
|
const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000, agent.scope, agent.teamId) || '';
|
|
1151
1160
|
|
|
1161
|
+
// Extract the user's original task from the conversation — the first substantial
|
|
1162
|
+
// user message is almost always the task assignment. This anchors the new agent.
|
|
1163
|
+
const originalTask = this._extractOriginalTask(thread);
|
|
1164
|
+
|
|
1165
|
+
// Rotation and idle-resume need very different framing. During rotation the agent
|
|
1166
|
+
// has no new user message — it must continue the exact in-progress task without
|
|
1167
|
+
// drifting. During idle-resume the user explicitly sent a message to continue.
|
|
1168
|
+
const isIdleResume = !isRotation && userMessage && userMessage.trim().length > 0;
|
|
1169
|
+
|
|
1170
|
+
const taskFocusBlock = isRotation ? [
|
|
1171
|
+
`## CRITICAL: Task Focus`,
|
|
1172
|
+
``,
|
|
1173
|
+
`You were auto-rotated (reason: ${reason || 'context_management'}) — this is a routine context refresh, NOT a new assignment.`,
|
|
1174
|
+
originalTask ? `Your task: ${originalTask}` : '',
|
|
1175
|
+
``,
|
|
1176
|
+
`Rules:`,
|
|
1177
|
+
`- Continue ONLY the task described in the conversation below`,
|
|
1178
|
+
`- Do NOT explore the codebase looking for other things to fix or improve`,
|
|
1179
|
+
`- Do NOT start new work that was not part of the original task`,
|
|
1180
|
+
`- Do NOT act on TODO comments, code quality issues, or improvements you notice in passing`,
|
|
1181
|
+
`- If the task is complete, report what was done and STOP — await new instructions from the user`,
|
|
1182
|
+
`- If the task is in progress, pick up exactly where the previous session left off`,
|
|
1183
|
+
``,
|
|
1184
|
+
] : [];
|
|
1185
|
+
|
|
1152
1186
|
let prompt = [
|
|
1153
1187
|
`# Session Context Resume`,
|
|
1154
1188
|
``,
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1189
|
+
isRotation
|
|
1190
|
+
? `You are continuing a session after an automatic context rotation. Your context was refreshed but your task has NOT changed. The conversation below is your previous session — continue the same work.`
|
|
1191
|
+
: `You are continuing a session that went idle. Below is the full conversation from your previous session — your actual exchanges with the user. Pick up exactly where you left off. The user's new message follows at the end.`,
|
|
1158
1192
|
``,
|
|
1159
1193
|
`Role: ${agent.role} | Provider: ${agent.provider} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1160
1194
|
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1161
1195
|
``,
|
|
1196
|
+
...taskFocusBlock,
|
|
1162
1197
|
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1163
1198
|
discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
|
|
1164
1199
|
`## Previous Conversation\n\n${thread}`,
|
|
1165
1200
|
``,
|
|
1166
1201
|
`---`,
|
|
1167
1202
|
``,
|
|
1168
|
-
`## New Message From User
|
|
1169
|
-
``,
|
|
1170
|
-
userMessage,
|
|
1203
|
+
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1171
1204
|
``,
|
|
1172
|
-
|
|
1205
|
+
isRotation
|
|
1206
|
+
? `Continue the in-progress task from the conversation above. Stay focused on that task only. Do not ask the user to repeat anything. If the task was already completed, state that and wait for new instructions.`
|
|
1207
|
+
: `Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything.`,
|
|
1173
1208
|
].filter(Boolean).join('\n');
|
|
1174
1209
|
|
|
1175
1210
|
// Hard cap at 80K chars (~20K tokens) to leave plenty of room in context window
|
|
@@ -1180,23 +1215,24 @@ export class Journalist {
|
|
|
1180
1215
|
prompt = [
|
|
1181
1216
|
`# Session Context Resume`,
|
|
1182
1217
|
``,
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1218
|
+
isRotation
|
|
1219
|
+
? `You are continuing after an automatic context rotation. Your task has NOT changed.`
|
|
1220
|
+
: `You are continuing a session that went idle (older turns summarized to fit). Pick up exactly where you left off.`,
|
|
1186
1221
|
``,
|
|
1187
1222
|
`Role: ${agent.role} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1188
1223
|
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1189
1224
|
``,
|
|
1225
|
+
...taskFocusBlock,
|
|
1190
1226
|
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1191
1227
|
`## Previous Conversation\n\n${smallerThread}`,
|
|
1192
1228
|
``,
|
|
1193
1229
|
`---`,
|
|
1194
1230
|
``,
|
|
1195
|
-
`## New Message From User
|
|
1196
|
-
``,
|
|
1197
|
-
userMessage,
|
|
1231
|
+
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1198
1232
|
``,
|
|
1199
|
-
|
|
1233
|
+
isRotation
|
|
1234
|
+
? `Continue the in-progress task only. Do not explore or start new work. If done, state that and wait.`
|
|
1235
|
+
: `Continue seamlessly. Do not ask the user to repeat anything.`,
|
|
1200
1236
|
].filter(Boolean).join('\n');
|
|
1201
1237
|
}
|
|
1202
1238
|
}
|
|
@@ -1204,6 +1240,15 @@ export class Journalist {
|
|
|
1204
1240
|
return prompt;
|
|
1205
1241
|
}
|
|
1206
1242
|
|
|
1243
|
+
_extractOriginalTask(thread) {
|
|
1244
|
+
// Find the first substantial USER message in the thread — that's the task.
|
|
1245
|
+
const match = thread.match(/\[USER\]:\n([\s\S]*?)(?=\n\n---|\n\[CLAUDE\]:|$)/);
|
|
1246
|
+
if (!match) return '';
|
|
1247
|
+
const firstMsg = match[1].trim();
|
|
1248
|
+
if (firstMsg.length < 10) return '';
|
|
1249
|
+
return firstMsg.length > 500 ? firstMsg.slice(0, 500) + '...' : firstMsg;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1207
1252
|
// --- Workspace Grouping ---
|
|
1208
1253
|
|
|
1209
1254
|
/**
|
|
@@ -367,7 +367,8 @@ export class Rotator extends EventEmitter {
|
|
|
367
367
|
if (typeof journalist.buildConversationResumePrompt === 'function') {
|
|
368
368
|
const conversationPrompt = journalist.buildConversationResumePrompt(
|
|
369
369
|
agent,
|
|
370
|
-
options.additionalPrompt || ''
|
|
370
|
+
options.additionalPrompt || '',
|
|
371
|
+
{ isRotation: true, reason: options.reason || 'auto' }
|
|
371
372
|
);
|
|
372
373
|
if (conversationPrompt && conversationPrompt.length > 500) {
|
|
373
374
|
brief = conversationPrompt;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { resolve, sep, isAbsolute, basename } from 'path';
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, realpathSync } from 'fs';
|
|
4
|
-
import { execFile, execFileSync } from 'child_process';
|
|
4
|
+
import { execFile, execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { lookup as mimeLookup } from '../mimetypes.js';
|
|
7
7
|
|
|
@@ -331,15 +331,37 @@ export function registerFileRoutes(app, daemon) {
|
|
|
331
331
|
}
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
-
// Download a file (
|
|
334
|
+
// Download a file or folder (folders are streamed as zip)
|
|
335
335
|
app.get('/api/files/download', (req, res) => {
|
|
336
336
|
const relPath = req.query.path;
|
|
337
337
|
const result = validateFilePath(relPath, getEditorRoot(daemon));
|
|
338
338
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
339
|
-
if (!existsSync(result.fullPath)) return res.status(404).json({ error: '
|
|
339
|
+
if (!existsSync(result.fullPath)) return res.status(404).json({ error: 'Not found' });
|
|
340
340
|
|
|
341
341
|
const stat = statSync(result.fullPath);
|
|
342
|
-
|
|
342
|
+
|
|
343
|
+
if (stat.isDirectory()) {
|
|
344
|
+
const folderName = basename(result.fullPath);
|
|
345
|
+
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(folderName)}.zip"`);
|
|
346
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
347
|
+
|
|
348
|
+
const zipProc = spawn('zip', [
|
|
349
|
+
'-r', '-q', '-',
|
|
350
|
+
relPath,
|
|
351
|
+
'-x', `${relPath}/.git/*`,
|
|
352
|
+
'-x', `${relPath}/node_modules/*`,
|
|
353
|
+
], {
|
|
354
|
+
cwd: getEditorRoot(daemon),
|
|
355
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
zipProc.stdout.pipe(res);
|
|
359
|
+
zipProc.stderr.on('data', () => {});
|
|
360
|
+
zipProc.on('error', () => {
|
|
361
|
+
if (!res.headersSent) res.status(500).json({ error: 'Failed to create zip' });
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
343
365
|
|
|
344
366
|
const name = basename(result.fullPath);
|
|
345
367
|
const mime = mimeLookup(name) || 'application/octet-stream';
|
|
@@ -643,24 +665,46 @@ export function registerFileRoutes(app, daemon) {
|
|
|
643
665
|
const rawFiles = daemon.registry.getFilesTouched(req.params.id);
|
|
644
666
|
const rootDir = agent.workingDir || daemon.projectDir;
|
|
645
667
|
|
|
646
|
-
// Build git diff numstat for line-level +/- counts
|
|
668
|
+
// Build git diff numstat for line-level +/- counts (unstaged + staged + untracked)
|
|
647
669
|
let numstatMap = {};
|
|
648
670
|
const writtenPaths = rawFiles.filter(f => f.writes > 0).map(f => f.path);
|
|
649
671
|
if (writtenPaths.length > 0) {
|
|
650
|
-
|
|
651
|
-
const out = execFileSync('git', ['diff', '--numstat', '--', ...writtenPaths], {
|
|
652
|
-
cwd: rootDir, timeout: 10000, maxBuffer: 2 * 1024 * 1024,
|
|
653
|
-
}).toString();
|
|
672
|
+
const parseNumstat = (out) => {
|
|
654
673
|
for (const line of out.split('\n')) {
|
|
655
674
|
const m = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
656
|
-
if (m) {
|
|
675
|
+
if (m && !numstatMap[m[3]]) {
|
|
657
676
|
numstatMap[m[3]] = {
|
|
658
677
|
additions: m[1] === '-' ? 0 : Number(m[1]),
|
|
659
678
|
deletions: m[2] === '-' ? 0 : Number(m[2]),
|
|
660
679
|
};
|
|
661
680
|
}
|
|
662
681
|
}
|
|
682
|
+
};
|
|
683
|
+
try {
|
|
684
|
+
const unstaged = execFileSync('git', ['diff', '--numstat', '--', ...writtenPaths], {
|
|
685
|
+
cwd: rootDir, timeout: 10000, maxBuffer: 2 * 1024 * 1024,
|
|
686
|
+
}).toString();
|
|
687
|
+
parseNumstat(unstaged);
|
|
663
688
|
} catch { /* git not available or not a repo */ }
|
|
689
|
+
try {
|
|
690
|
+
const staged = execFileSync('git', ['diff', '--cached', '--numstat', '--', ...writtenPaths], {
|
|
691
|
+
cwd: rootDir, timeout: 10000, maxBuffer: 2 * 1024 * 1024,
|
|
692
|
+
}).toString();
|
|
693
|
+
parseNumstat(staged);
|
|
694
|
+
} catch { /* ignore */ }
|
|
695
|
+
// For untracked files not covered by diff, count lines as all additions
|
|
696
|
+
for (const p of writtenPaths) {
|
|
697
|
+
if (numstatMap[p]) continue;
|
|
698
|
+
const full = isAbsolute(p) ? p : resolve(rootDir, p);
|
|
699
|
+
try {
|
|
700
|
+
const stat = statSync(full);
|
|
701
|
+
if (stat.isFile()) {
|
|
702
|
+
const content = readFileSync(full, 'utf8');
|
|
703
|
+
const lineCount = content.split('\n').length;
|
|
704
|
+
numstatMap[p] = { additions: lineCount, deletions: 0 };
|
|
705
|
+
}
|
|
706
|
+
} catch { /* file may not exist */ }
|
|
707
|
+
}
|
|
664
708
|
}
|
|
665
709
|
|
|
666
710
|
const files = rawFiles.map(f => {
|
|
@@ -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;
|
|
@@ -446,7 +458,7 @@ export class TunnelManager {
|
|
|
446
458
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
447
459
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
448
460
|
const pinnedPkg = `groove-dev@${localVer}`;
|
|
449
|
-
const installCmd = config.user
|
|
461
|
+
const installCmd = npmGlobalInstall(pinnedPkg, config.user);
|
|
450
462
|
|
|
451
463
|
try {
|
|
452
464
|
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
@@ -455,8 +467,7 @@ export class TunnelManager {
|
|
|
455
467
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
456
468
|
});
|
|
457
469
|
} catch {
|
|
458
|
-
const
|
|
459
|
-
const fallbackCmd = config.user === 'root' ? `npm i -g --prefer-online ${fallbackPkg}` : `sudo npm i -g --prefer-online ${fallbackPkg}`;
|
|
470
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
460
471
|
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
461
472
|
encoding: 'utf8',
|
|
462
473
|
timeout: 120000,
|
|
@@ -533,7 +544,7 @@ export class TunnelManager {
|
|
|
533
544
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
534
545
|
const localVer = getLocalVersion();
|
|
535
546
|
const pkg = localVer !== '0.0.0' ? `groove-dev@${localVer}` : 'groove-dev';
|
|
536
|
-
const installCmd = config.user
|
|
547
|
+
const installCmd = npmGlobalInstall(pkg, config.user);
|
|
537
548
|
|
|
538
549
|
let usedFallback = false;
|
|
539
550
|
try {
|
|
@@ -554,7 +565,7 @@ export class TunnelManager {
|
|
|
554
565
|
}
|
|
555
566
|
} else {
|
|
556
567
|
if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
557
|
-
const fallbackCmd =
|
|
568
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
558
569
|
try {
|
|
559
570
|
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
560
571
|
encoding: 'utf8',
|
|
@@ -565,6 +576,7 @@ export class TunnelManager {
|
|
|
565
576
|
} catch { /* fall through to original error */ }
|
|
566
577
|
}
|
|
567
578
|
if (!usedFallback) {
|
|
579
|
+
if (isPermissionError(errOutput)) throw new Error(PERMISSION_HINT);
|
|
568
580
|
throw new Error(`Remote upgrade failed: ${errOutput.slice(-400)}`);
|
|
569
581
|
}
|
|
570
582
|
}
|
|
@@ -670,12 +682,10 @@ export class TunnelManager {
|
|
|
670
682
|
throw new Error(`Failed to check remote environment: ${err.message}`);
|
|
671
683
|
}
|
|
672
684
|
|
|
673
|
-
// Step 2: Install groove-dev globally (
|
|
685
|
+
// Step 2: Install groove-dev globally (try user-space first, sudo fallback)
|
|
674
686
|
const localVer = getLocalVersion();
|
|
675
687
|
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}`;
|
|
688
|
+
const installCmd = npmGlobalInstall(pkg, config.user);
|
|
679
689
|
|
|
680
690
|
try {
|
|
681
691
|
execFileSync('ssh', [
|
|
@@ -697,7 +707,7 @@ export class TunnelManager {
|
|
|
697
707
|
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
698
708
|
}
|
|
699
709
|
} else if (localVer !== '0.0.0' && pkg.includes('@')) {
|
|
700
|
-
const fallbackCmd =
|
|
710
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
701
711
|
try {
|
|
702
712
|
execFileSync('ssh', [...sshBase, remoteCmd(fallbackCmd)], {
|
|
703
713
|
encoding: 'utf8',
|
|
@@ -706,9 +716,11 @@ export class TunnelManager {
|
|
|
706
716
|
});
|
|
707
717
|
} catch (err2) {
|
|
708
718
|
const output = err2.stdout?.toString() || err2.stderr?.toString() || err2.message;
|
|
719
|
+
if (isPermissionError(output)) throw new Error(PERMISSION_HINT);
|
|
709
720
|
throw new Error(`npm install failed: ${output.slice(-400)}`);
|
|
710
721
|
}
|
|
711
722
|
} else {
|
|
723
|
+
if (isPermissionError(errOutput)) throw new Error(PERMISSION_HINT);
|
|
712
724
|
throw new Error(`npm install failed: ${errOutput.slice(-400)}`);
|
|
713
725
|
}
|
|
714
726
|
}
|
|
@@ -752,7 +764,7 @@ export class TunnelManager {
|
|
|
752
764
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
753
765
|
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
754
766
|
const pinnedPkg = `groove-dev@${localVer}`;
|
|
755
|
-
const installCmd = config.user
|
|
767
|
+
const installCmd = npmGlobalInstall(pinnedPkg, config.user);
|
|
756
768
|
|
|
757
769
|
try {
|
|
758
770
|
execFileSync('ssh', [...sshBase, sshCmd(installCmd)], {
|
|
@@ -771,7 +783,7 @@ export class TunnelManager {
|
|
|
771
783
|
throw new Error(`npm install failed after cleanup: ${retryOutput.slice(-400)}`);
|
|
772
784
|
}
|
|
773
785
|
} else {
|
|
774
|
-
const fallbackCmd =
|
|
786
|
+
const fallbackCmd = npmGlobalInstall('groove-dev', config.user);
|
|
775
787
|
execFileSync('ssh', [...sshBase, sshCmd(fallbackCmd)], {
|
|
776
788
|
encoding: 'utf8',
|
|
777
789
|
timeout: 120000,
|