groove-dev 0.27.61 → 0.27.62

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.61",
3
+ "version": "0.27.62",
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.62",
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' },
@@ -4728,7 +4772,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4728
4772
  };
4729
4773
 
4730
4774
  try {
4731
- const pat = daemon.credentials?.getKey?.('github-pat') || null;
4775
+ const pat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
4732
4776
 
4733
4777
  let installVersion;
4734
4778
  try {
@@ -4739,6 +4783,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4739
4783
 
4740
4784
  broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
4741
4785
 
4786
+ // Pre-flight: verify git is installed before attempting clone.
4787
+ const gitInstalled = await new Promise((resolveGit) => {
4788
+ execFile('git', ['--version'], { timeout: 5000 }, (err) => resolveGit(!err));
4789
+ });
4790
+ if (!gitInstalled) {
4791
+ return fail('Git is not installed. Install Git from https://git-scm.com and restart Groove.');
4792
+ }
4793
+
4742
4794
  const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', NETWORK_REPO_URL, installPath];
4743
4795
  const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
4744
4796
  if (pat) {
@@ -4768,14 +4820,30 @@ Keep responses concise. Help them think, don't lecture them about the system the
4768
4820
  });
4769
4821
 
4770
4822
  if (cloneCode.code !== 0) {
4771
- const hint = stripCredentials(cloneErr.trim().split('\n').slice(-1)[0] || 'git clone failed');
4823
+ let hint;
4824
+ const errMsg = cloneCode.err || '';
4825
+ const lastLine = cloneErr.trim().split('\n').slice(-1)[0] || '';
4826
+ if (errMsg.includes('ENOENT')) {
4827
+ hint = 'Git is not installed. Install Git from https://git-scm.com and restart Groove.';
4828
+ } else if (/Authentication failed|could not read Username/i.test(cloneErr)) {
4829
+ hint = 'Authentication failed — run "groove set-key github-pat <token>" to set a GitHub PAT.';
4830
+ } else if (/not found/i.test(cloneErr)) {
4831
+ hint = `Repository or tag not found (${installVersion}). Check NETWORK_REPO_URL and tag.`;
4832
+ } else {
4833
+ hint = stripCredentials(lastLine || errMsg || 'git clone failed');
4834
+ }
4772
4835
  return fail(`Clone failed: ${hint}`);
4773
4836
  }
4774
4837
 
4775
4838
  broadcastInstallProgress('cloned', 'Repository cloned', 10);
4776
4839
 
4777
4840
  // Run setup.sh --json from the install directory
4778
- const setup = spawnSetupSh(installPath);
4841
+ let setup;
4842
+ try {
4843
+ setup = spawnSetupSh(installPath);
4844
+ } catch (spawnErr) {
4845
+ return fail(`Setup failed: ${spawnErr.message}`);
4846
+ }
4779
4847
 
4780
4848
  daemon.networkInstall.proc = setup;
4781
4849
 
@@ -4809,7 +4877,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
4809
4877
  });
4810
4878
 
4811
4879
  if (setupResult.code !== 0) {
4812
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
4880
+ let hint;
4881
+ if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
4882
+ hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
4883
+ } else {
4884
+ hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
4885
+ }
4813
4886
  return fail(`Setup failed: ${hint}`);
4814
4887
  }
4815
4888
 
@@ -4883,9 +4956,16 @@ Keep responses concise. Help them think, don't lecture them about the system the
4883
4956
  // surface that. Uses spawn with array args — no shell interpolation.
4884
4957
  function fetchLatestNetworkTag() {
4885
4958
  return new Promise((resolvePromise) => {
4959
+ const tagEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
4960
+ const tagPat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
4961
+ if (tagPat) {
4962
+ tagEnv.GIT_CONFIG_COUNT = '1';
4963
+ tagEnv.GIT_CONFIG_KEY_0 = 'http.extraHeader';
4964
+ tagEnv.GIT_CONFIG_VALUE_0 = `Authorization: token ${tagPat}`;
4965
+ }
4886
4966
  const proc = spawn('git', ['ls-remote', '--tags', NETWORK_REPO_URL], {
4887
4967
  stdio: ['ignore', 'pipe', 'pipe'],
4888
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
4968
+ env: tagEnv,
4889
4969
  });
4890
4970
  let stdout = '';
4891
4971
  let stderr = '';
@@ -5023,7 +5103,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
5023
5103
 
5024
5104
  broadcastUpdateProgress('deps', 'Updating dependencies...', 30);
5025
5105
 
5026
- const setup = spawnSetupSh(installPath);
5106
+ let setup;
5107
+ try {
5108
+ setup = spawnSetupSh(installPath);
5109
+ } catch (spawnErr) {
5110
+ return fail(`Setup failed: ${spawnErr.message}`);
5111
+ }
5027
5112
 
5028
5113
  daemon.networkInstall.proc = setup;
5029
5114
 
@@ -5054,7 +5139,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
5054
5139
  });
5055
5140
 
5056
5141
  if (setupResult.code !== 0) {
5057
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
5142
+ let hint;
5143
+ if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
5144
+ hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
5145
+ } else {
5146
+ hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
5147
+ }
5058
5148
  return fail(`Setup failed: ${hint}`);
5059
5149
  }
5060
5150