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.
@@ -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-DWao9glo.js"></script>
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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.61",
3
+ "version": "0.27.63",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- <div className="bg-surface-1 border-b border-border px-4 py-2.5">
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
- get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
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 sub = [
1341
- result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
1342
- result.projectDir ? `→ ${result.projectDir}/` : '',
1343
- ].filter(Boolean).join(' · ');
1344
- get().addToast('success', `Launched ${(result.launched || 0) + (result.reused || 0)} agents`, sub || undefined);
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.61",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.61",
3
+ "version": "0.27.63",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.61",
3
+ "version": "0.27.63",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- return spawn('cmd.exe', ['/c', 'bash setup.sh --json'], {
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 installed = existsSync(resolve(installPath, 'setup.sh'));
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
- path: installed ? installPath : null,
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
- // Refuse to clone over an existing directory avoids surprising merges.
4752
+ // If directory exists from a previous failed install, clean it up automatically.
4706
4753
  if (existsSync(installPath)) {
4707
- return res.status(400).json({ error: 'Install path already exists; uninstall first' });
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
- const hint = stripCredentials(cloneErr.trim().split('\n').slice(-1)[0] || 'git clone failed');
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
- const setup = spawnSetupSh(installPath);
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
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
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: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
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
- const setup = spawnSetupSh(installPath);
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
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
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