groove-dev 0.27.61 → 0.27.63
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 +7 -0
- 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/api.js +117 -16
- package/node_modules/@groove-dev/gui/dist/assets/{index-DWao9glo.js → index-Zb6PcuaY.js} +12 -12
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +98 -13
- package/node_modules/@groove-dev/gui/src/stores/groove.js +25 -6
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +117 -16
- package/packages/gui/dist/assets/{index-DWao9glo.js → index-Zb6PcuaY.js} +12 -12
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/network/compute-header.jsx +98 -13
- package/packages/gui/src/stores/groove.js +25 -6
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Zb6PcuaY.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
@@ -107,6 +107,103 @@ const MAX_VRAM_MB = 128 * 1024;
|
|
|
107
107
|
const MAX_CPU = 128;
|
|
108
108
|
const MAX_LOAD = 4.0;
|
|
109
109
|
|
|
110
|
+
const SPARKLINE_ROWS = [
|
|
111
|
+
{ key: 'globalSessions', label: 'Sessions', color: HEX.accent },
|
|
112
|
+
{ key: 'mySessions', label: 'My Sessions', color: HEX.info },
|
|
113
|
+
{ key: 'nodeCount', label: 'Nodes', color: HEX.purple },
|
|
114
|
+
{ key: 'avgLoad', label: 'Load', color: HEX.warning },
|
|
115
|
+
{ key: 'myLoad', label: 'My Load', color: HEX.success },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function TrendsColumn({ snapshots }) {
|
|
119
|
+
const hasData = snapshots && snapshots.length >= 2;
|
|
120
|
+
return (
|
|
121
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
122
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Trends</div>
|
|
123
|
+
{!hasData ? (
|
|
124
|
+
<div className="text-2xs font-mono text-text-4">Collecting data…</div>
|
|
125
|
+
) : (
|
|
126
|
+
SPARKLINE_ROWS.map((row) => {
|
|
127
|
+
const data = snapshots.map((s) => ({ v: s[row.key] ?? 0 }));
|
|
128
|
+
const current = data[data.length - 1].v;
|
|
129
|
+
const display = Number.isInteger(current) ? current : current.toFixed(2);
|
|
130
|
+
return (
|
|
131
|
+
<div key={row.key} className="flex items-center gap-2">
|
|
132
|
+
<span className="w-[72px] text-2xs font-mono text-text-3 uppercase truncate flex-shrink-0">{row.label}</span>
|
|
133
|
+
<MiniSparkline data={data} color={row.color} width={140} height={24} />
|
|
134
|
+
<span className="text-2xs font-mono text-text-1 tabular-nums flex-shrink-0">{display}</span>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function YourNodeColumn({ node, compute }) {
|
|
144
|
+
if (!node || !node.active) {
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
147
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Your Node</div>
|
|
148
|
+
<div className="text-2xs font-mono text-text-4">Node idle</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const layersLabel = node.layers ? `Layers ${node.layers} / 36` : 'Unassigned';
|
|
154
|
+
const modelLabel = node.model || 'Qwen/Qwen3-4B';
|
|
155
|
+
const bw = compute.totalBandwidthMbps ? `${Math.round(compute.totalBandwidthMbps)} Mbps` : '— Mbps';
|
|
156
|
+
const nodeRam = node.hardware?.memory;
|
|
157
|
+
const ramPct = nodeRam && compute.totalRamMb > 0
|
|
158
|
+
? `${((nodeRam / compute.totalRamMb) * 100).toFixed(1)}%`
|
|
159
|
+
: '—';
|
|
160
|
+
|
|
161
|
+
const metrics = [
|
|
162
|
+
{ label: 'Layers', value: layersLabel },
|
|
163
|
+
{ label: 'Model', value: modelLabel },
|
|
164
|
+
{ label: 'Sessions', value: node.sessions ?? 0 },
|
|
165
|
+
{ label: 'Bandwidth', value: bw },
|
|
166
|
+
{ label: 'RAM Share', value: ramPct },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex flex-col gap-1.5 min-w-0">
|
|
171
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Your Node</div>
|
|
172
|
+
{metrics.map((m) => (
|
|
173
|
+
<div key={m.label} className="min-w-0">
|
|
174
|
+
<div className="text-2xs font-mono text-text-4 uppercase tracking-wider leading-none">{m.label}</div>
|
|
175
|
+
<div className="text-xs font-mono text-text-1 truncate leading-tight">{m.value}</div>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function BarsTrendsNode({ compute, allZero, avgGpuUtil }) {
|
|
183
|
+
const snapshots = useGrooveStore((s) => s.networkSnapshots);
|
|
184
|
+
const node = useGrooveStore((s) => s.networkNode);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="bg-surface-1 border-b border-border px-4 py-2.5" style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr 1fr', gap: '1.5rem' }}>
|
|
188
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
189
|
+
{allZero ? (
|
|
190
|
+
<div className="text-2xs font-mono text-text-4">Waiting for network data...</div>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<AsciiBar label="RAM" value={compute.totalRamMb} max={MAX_RAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
194
|
+
<AsciiBar label="VRAM" value={compute.totalVramMb} max={MAX_VRAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
195
|
+
<AsciiBar label="CPU" value={compute.totalCpuCores} max={MAX_CPU} unit="cores" />
|
|
196
|
+
<AsciiBar label="GPU%" value={avgGpuUtil} max={100} unit="%" />
|
|
197
|
+
<AsciiBar label="LOAD" value={compute.avgLoad} max={MAX_LOAD} unit="" />
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
<TrendsColumn snapshots={snapshots} />
|
|
202
|
+
<YourNodeColumn node={node} compute={compute} />
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
110
207
|
export const ComputeHeader = memo(function ComputeHeader() {
|
|
111
208
|
const compute = useGrooveStore((s) => s.networkCompute);
|
|
112
209
|
const nodes = useGrooveStore((s) => s.networkStatus.nodes || []);
|
|
@@ -148,19 +245,7 @@ export const ComputeHeader = memo(function ComputeHeader() {
|
|
|
148
245
|
))}
|
|
149
246
|
</div>
|
|
150
247
|
|
|
151
|
-
<
|
|
152
|
-
{allZero ? (
|
|
153
|
-
<div className="text-2xs font-mono text-text-4">Waiting for network data...</div>
|
|
154
|
-
) : (
|
|
155
|
-
<div className="flex flex-col gap-0.5">
|
|
156
|
-
<AsciiBar label="RAM" value={compute.totalRamMb} max={MAX_RAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
157
|
-
<AsciiBar label="VRAM" value={compute.totalVramMb} max={MAX_VRAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
158
|
-
<AsciiBar label="CPU" value={compute.totalCpuCores} max={MAX_CPU} unit="cores" />
|
|
159
|
-
<AsciiBar label="GPU%" value={avgGpuUtil} max={100} unit="%" />
|
|
160
|
-
<AsciiBar label="LOAD" value={compute.avgLoad} max={MAX_LOAD} unit="" />
|
|
161
|
-
</div>
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
248
|
+
<BarsTrendsNode compute={compute} allZero={allZero} avgGpuUtil={avgGpuUtil} />
|
|
164
249
|
</div>
|
|
165
250
|
);
|
|
166
251
|
});
|
|
@@ -1305,8 +1305,17 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1305
1305
|
try {
|
|
1306
1306
|
const result = await api.post('/recommended-team/launch', { teamId });
|
|
1307
1307
|
const agents = result.agents || [];
|
|
1308
|
+
const failures = result.failed || [];
|
|
1308
1309
|
const names = agents.map((a) => a.name).join(', ') || '';
|
|
1309
|
-
|
|
1310
|
+
|
|
1311
|
+
if (agents.length === 0 && failures.length > 0) {
|
|
1312
|
+
get().addToast('error', 'Delegation failed', failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1313
|
+
} else {
|
|
1314
|
+
get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
|
|
1315
|
+
if (failures.length > 0) {
|
|
1316
|
+
get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1310
1319
|
if (agents.length > 0) {
|
|
1311
1320
|
set((s) => ({
|
|
1312
1321
|
thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
|
|
@@ -1337,11 +1346,21 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1337
1346
|
get().addToast('info', 'Launching team...');
|
|
1338
1347
|
const body = { ...(modifiedAgents && { agents: modifiedAgents }), ...(teamId && { teamId }) };
|
|
1339
1348
|
const result = await api.post('/recommended-team/launch', body);
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1349
|
+
const totalOk = (result.launched || 0) + (result.reused || 0);
|
|
1350
|
+
const failures = result.failed || [];
|
|
1351
|
+
|
|
1352
|
+
if (totalOk === 0 && failures.length > 0) {
|
|
1353
|
+
get().addToast('error', 'Team launch failed', failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1354
|
+
} else {
|
|
1355
|
+
const sub = [
|
|
1356
|
+
result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
|
|
1357
|
+
result.projectDir ? `→ ${result.projectDir}/` : '',
|
|
1358
|
+
].filter(Boolean).join(' · ');
|
|
1359
|
+
get().addToast('success', `Launched ${totalOk} agents`, sub || undefined);
|
|
1360
|
+
if (failures.length > 0) {
|
|
1361
|
+
get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1345
1364
|
// Set thinking indicator for all launched/reused agents
|
|
1346
1365
|
const launchedAgents = result.agents || [];
|
|
1347
1366
|
if (launchedAgents.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.63",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import express from 'express';
|
|
5
|
-
import { resolve, dirname, join, sep } from 'path';
|
|
5
|
+
import { resolve, dirname, join, sep, relative } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync, realpathSync } from 'fs';
|
|
8
|
-
import { spawn, execFile } from 'child_process';
|
|
8
|
+
import { spawn, execFile, execFileSync } from 'child_process';
|
|
9
9
|
import { createHash, randomUUID } from 'crypto';
|
|
10
10
|
import { hostname, networkInterfaces, homedir } from 'os';
|
|
11
11
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
@@ -2914,6 +2914,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2914
2914
|
console.log(`[Groove] Project directory: ${projectWorkingDir}`);
|
|
2915
2915
|
}
|
|
2916
2916
|
|
|
2917
|
+
function normalizeScope(patterns, baseDir) {
|
|
2918
|
+
if (!patterns || !Array.isArray(patterns)) return patterns;
|
|
2919
|
+
return patterns.map((p) => {
|
|
2920
|
+
if (typeof p === 'string' && p.startsWith('/')) {
|
|
2921
|
+
const rel = relative(baseDir, p);
|
|
2922
|
+
if (!rel.startsWith('..')) return rel;
|
|
2923
|
+
return p.slice(1);
|
|
2924
|
+
}
|
|
2925
|
+
return p;
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2917
2929
|
// Separate phase 1 (builders) and phase 2 (QC/finisher)
|
|
2918
2930
|
const phase1 = agentConfigs.filter((a) => !a.phase || a.phase === 1);
|
|
2919
2931
|
let phase2 = agentConfigs.filter((a) => a.phase === 2);
|
|
@@ -2973,7 +2985,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2973
2985
|
// Spawn fresh with the same name/team but new prompt + full context
|
|
2974
2986
|
const validated = validateAgentConfig({
|
|
2975
2987
|
role: existing.role,
|
|
2976
|
-
scope: config.scope || existing.scope || [],
|
|
2988
|
+
scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
|
|
2977
2989
|
prompt,
|
|
2978
2990
|
provider: config.provider || existing.provider || undefined,
|
|
2979
2991
|
model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
|
|
@@ -2995,7 +3007,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2995
3007
|
try {
|
|
2996
3008
|
const validated = validateAgentConfig({
|
|
2997
3009
|
role: config.role,
|
|
2998
|
-
scope: config.scope || [],
|
|
3010
|
+
scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
|
|
2999
3011
|
prompt,
|
|
3000
3012
|
provider: config.provider || undefined,
|
|
3001
3013
|
model: config.model || daemon.config?.defaultModel || 'auto',
|
|
@@ -3015,6 +3027,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3015
3027
|
}
|
|
3016
3028
|
}
|
|
3017
3029
|
|
|
3030
|
+
if (failed.length > 0) {
|
|
3031
|
+
console.warn(`[Groove] Team launch had ${failed.length} failure(s):`, failed.map((f) => `${f.role}: ${f.error}`).join(', '));
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3018
3034
|
// Phase 2 agents also scoped to projectWorkingDir
|
|
3019
3035
|
if (phase2.length > 0 && phase1Ids.length > 0) {
|
|
3020
3036
|
// Dedup: if a running idle fullstack already exists in this team,
|
|
@@ -4619,9 +4635,37 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4619
4635
|
: join(base, 'venv', 'bin', 'python3');
|
|
4620
4636
|
}
|
|
4621
4637
|
|
|
4638
|
+
let _cachedGitBash = undefined;
|
|
4639
|
+
function findGitBash() {
|
|
4640
|
+
if (_cachedGitBash !== undefined) return _cachedGitBash;
|
|
4641
|
+
try {
|
|
4642
|
+
const gitPath = execFileSync('where', ['git'], { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
4643
|
+
.toString().trim().split('\n')[0].trim();
|
|
4644
|
+
// git.exe is typically at <Git>\cmd\git.exe — navigate up to Git root
|
|
4645
|
+
const gitDir = dirname(dirname(gitPath));
|
|
4646
|
+
const candidate = join(gitDir, 'bin', 'bash.exe');
|
|
4647
|
+
if (existsSync(candidate)) { _cachedGitBash = candidate; return _cachedGitBash; }
|
|
4648
|
+
} catch { /* where failed — try common paths */ }
|
|
4649
|
+
const fallbacks = [
|
|
4650
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
4651
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
4652
|
+
];
|
|
4653
|
+
for (const p of fallbacks) {
|
|
4654
|
+
if (existsSync(p)) { _cachedGitBash = p; return _cachedGitBash; }
|
|
4655
|
+
}
|
|
4656
|
+
_cachedGitBash = null;
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4622
4660
|
function spawnSetupSh(cwd) {
|
|
4623
4661
|
if (IS_WIN) {
|
|
4624
|
-
|
|
4662
|
+
const bashPath = findGitBash();
|
|
4663
|
+
if (!bashPath) {
|
|
4664
|
+
const err = new Error('Could not find bash. Ensure Git for Windows is installed from https://git-scm.com');
|
|
4665
|
+
err.code = 'BASH_NOT_FOUND';
|
|
4666
|
+
throw err;
|
|
4667
|
+
}
|
|
4668
|
+
return spawn(bashPath, ['setup.sh', '--json'], {
|
|
4625
4669
|
cwd,
|
|
4626
4670
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4627
4671
|
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
@@ -4681,10 +4725,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4681
4725
|
|
|
4682
4726
|
app.get('/api/network/install/status', networkGate, (req, res) => {
|
|
4683
4727
|
const installPath = networkRoot();
|
|
4684
|
-
const
|
|
4728
|
+
const dirExists = existsSync(installPath);
|
|
4729
|
+
const installed = dirExists && existsSync(resolve(installPath, 'setup.sh'));
|
|
4730
|
+
const stale = dirExists && !installed;
|
|
4685
4731
|
res.json({
|
|
4686
4732
|
installed,
|
|
4687
|
-
|
|
4733
|
+
stale,
|
|
4734
|
+
path: dirExists ? installPath : null,
|
|
4688
4735
|
version: installed ? getInstalledNetworkVersion() : null,
|
|
4689
4736
|
});
|
|
4690
4737
|
});
|
|
@@ -4702,9 +4749,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4702
4749
|
return res.status(500).json({ error: 'Invalid install path' });
|
|
4703
4750
|
}
|
|
4704
4751
|
|
|
4705
|
-
//
|
|
4752
|
+
// If directory exists from a previous failed install, clean it up automatically.
|
|
4706
4753
|
if (existsSync(installPath)) {
|
|
4707
|
-
|
|
4754
|
+
if (daemon.config?.networkBeta?.installed) {
|
|
4755
|
+
return res.status(400).json({ error: 'Install path already exists; uninstall first' });
|
|
4756
|
+
}
|
|
4757
|
+
try {
|
|
4758
|
+
rmSync(installPath, { recursive: true, force: true });
|
|
4759
|
+
daemon.audit?.log?.('network.install.stale-cleanup', { path: installPath });
|
|
4760
|
+
} catch (cleanupErr) {
|
|
4761
|
+
return res.status(500).json({ error: `Failed to clean stale install directory: ${cleanupErr.message}` });
|
|
4762
|
+
}
|
|
4708
4763
|
}
|
|
4709
4764
|
|
|
4710
4765
|
daemon.networkInstall = { running: true, startedAt: Date.now() };
|
|
@@ -4728,7 +4783,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4728
4783
|
};
|
|
4729
4784
|
|
|
4730
4785
|
try {
|
|
4731
|
-
const pat = daemon.credentials?.getKey?.('github-pat') || null;
|
|
4786
|
+
const pat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
|
|
4732
4787
|
|
|
4733
4788
|
let installVersion;
|
|
4734
4789
|
try {
|
|
@@ -4739,6 +4794,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4739
4794
|
|
|
4740
4795
|
broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
|
|
4741
4796
|
|
|
4797
|
+
// Pre-flight: verify git is installed before attempting clone.
|
|
4798
|
+
const gitInstalled = await new Promise((resolveGit) => {
|
|
4799
|
+
execFile('git', ['--version'], { timeout: 5000 }, (err) => resolveGit(!err));
|
|
4800
|
+
});
|
|
4801
|
+
if (!gitInstalled) {
|
|
4802
|
+
return fail('Git is not installed. Install Git from https://git-scm.com and restart Groove.');
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4742
4805
|
const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', NETWORK_REPO_URL, installPath];
|
|
4743
4806
|
const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
4744
4807
|
if (pat) {
|
|
@@ -4768,14 +4831,30 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4768
4831
|
});
|
|
4769
4832
|
|
|
4770
4833
|
if (cloneCode.code !== 0) {
|
|
4771
|
-
|
|
4834
|
+
let hint;
|
|
4835
|
+
const errMsg = cloneCode.err || '';
|
|
4836
|
+
const lastLine = cloneErr.trim().split('\n').slice(-1)[0] || '';
|
|
4837
|
+
if (errMsg.includes('ENOENT')) {
|
|
4838
|
+
hint = 'Git is not installed. Install Git from https://git-scm.com and restart Groove.';
|
|
4839
|
+
} else if (/Authentication failed|could not read Username/i.test(cloneErr)) {
|
|
4840
|
+
hint = 'Authentication failed — run "groove set-key github-pat <token>" to set a GitHub PAT.';
|
|
4841
|
+
} else if (/not found/i.test(cloneErr)) {
|
|
4842
|
+
hint = `Repository or tag not found (${installVersion}). Check NETWORK_REPO_URL and tag.`;
|
|
4843
|
+
} else {
|
|
4844
|
+
hint = stripCredentials(lastLine || errMsg || 'git clone failed');
|
|
4845
|
+
}
|
|
4772
4846
|
return fail(`Clone failed: ${hint}`);
|
|
4773
4847
|
}
|
|
4774
4848
|
|
|
4775
4849
|
broadcastInstallProgress('cloned', 'Repository cloned', 10);
|
|
4776
4850
|
|
|
4777
4851
|
// Run setup.sh --json from the install directory
|
|
4778
|
-
|
|
4852
|
+
let setup;
|
|
4853
|
+
try {
|
|
4854
|
+
setup = spawnSetupSh(installPath);
|
|
4855
|
+
} catch (spawnErr) {
|
|
4856
|
+
return fail(`Setup failed: ${spawnErr.message}`);
|
|
4857
|
+
}
|
|
4779
4858
|
|
|
4780
4859
|
daemon.networkInstall.proc = setup;
|
|
4781
4860
|
|
|
@@ -4809,7 +4888,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4809
4888
|
});
|
|
4810
4889
|
|
|
4811
4890
|
if (setupResult.code !== 0) {
|
|
4812
|
-
|
|
4891
|
+
let hint;
|
|
4892
|
+
if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
|
|
4893
|
+
hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
|
|
4894
|
+
} else {
|
|
4895
|
+
hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
|
|
4896
|
+
}
|
|
4813
4897
|
return fail(`Setup failed: ${hint}`);
|
|
4814
4898
|
}
|
|
4815
4899
|
|
|
@@ -4883,9 +4967,16 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4883
4967
|
// surface that. Uses spawn with array args — no shell interpolation.
|
|
4884
4968
|
function fetchLatestNetworkTag() {
|
|
4885
4969
|
return new Promise((resolvePromise) => {
|
|
4970
|
+
const tagEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
4971
|
+
const tagPat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
|
|
4972
|
+
if (tagPat) {
|
|
4973
|
+
tagEnv.GIT_CONFIG_COUNT = '1';
|
|
4974
|
+
tagEnv.GIT_CONFIG_KEY_0 = 'http.extraHeader';
|
|
4975
|
+
tagEnv.GIT_CONFIG_VALUE_0 = `Authorization: token ${tagPat}`;
|
|
4976
|
+
}
|
|
4886
4977
|
const proc = spawn('git', ['ls-remote', '--tags', NETWORK_REPO_URL], {
|
|
4887
4978
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4888
|
-
env:
|
|
4979
|
+
env: tagEnv,
|
|
4889
4980
|
});
|
|
4890
4981
|
let stdout = '';
|
|
4891
4982
|
let stderr = '';
|
|
@@ -5023,7 +5114,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
5023
5114
|
|
|
5024
5115
|
broadcastUpdateProgress('deps', 'Updating dependencies...', 30);
|
|
5025
5116
|
|
|
5026
|
-
|
|
5117
|
+
let setup;
|
|
5118
|
+
try {
|
|
5119
|
+
setup = spawnSetupSh(installPath);
|
|
5120
|
+
} catch (spawnErr) {
|
|
5121
|
+
return fail(`Setup failed: ${spawnErr.message}`);
|
|
5122
|
+
}
|
|
5027
5123
|
|
|
5028
5124
|
daemon.networkInstall.proc = setup;
|
|
5029
5125
|
|
|
@@ -5054,7 +5150,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
5054
5150
|
});
|
|
5055
5151
|
|
|
5056
5152
|
if (setupResult.code !== 0) {
|
|
5057
|
-
|
|
5153
|
+
let hint;
|
|
5154
|
+
if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
|
|
5155
|
+
hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
|
|
5156
|
+
} else {
|
|
5157
|
+
hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
|
|
5158
|
+
}
|
|
5058
5159
|
return fail(`Setup failed: ${hint}`);
|
|
5059
5160
|
}
|
|
5060
5161
|
|