@indy2kro/agentenv 1.0.1 → 1.1.0
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/dist/adapters/agent-dirs.d.ts +29 -0
- package/dist/adapters/agent-dirs.d.ts.map +1 -0
- package/dist/adapters/agent-dirs.js +46 -0
- package/dist/adapters/agent-dirs.js.map +1 -0
- package/dist/adapters/base.d.ts +7 -1
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +18 -10
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/cline.js +2 -2
- package/dist/adapters/cline.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/copilot.d.ts.map +1 -1
- package/dist/adapters/copilot.js +3 -4
- package/dist/adapters/copilot.js.map +1 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/cursor.js +7 -6
- package/dist/adapters/cursor.js.map +1 -1
- package/dist/adapters/detect.d.ts.map +1 -1
- package/dist/adapters/detect.js +1 -0
- package/dist/adapters/detect.js.map +1 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +2 -2
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/opencode.js +2 -2
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/adapters/rtk-delegation.d.ts.map +1 -1
- package/dist/adapters/rtk-delegation.js +16 -1
- package/dist/adapters/rtk-delegation.js.map +1 -1
- package/dist/adapters/vibe.d.ts.map +1 -1
- package/dist/adapters/vibe.js +2 -2
- package/dist/adapters/vibe.js.map +1 -1
- package/dist/adapters/windsurf.d.ts.map +1 -1
- package/dist/adapters/windsurf.js +2 -2
- package/dist/adapters/windsurf.js.map +1 -1
- package/dist/cli/exit.d.ts +17 -6
- package/dist/cli/exit.d.ts.map +1 -1
- package/dist/cli/exit.js +17 -7
- package/dist/cli/exit.js.map +1 -1
- package/dist/commands/apply.d.ts +77 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +202 -27
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/config-edit.d.ts +33 -0
- package/dist/commands/config-edit.d.ts.map +1 -0
- package/dist/commands/config-edit.js +216 -0
- package/dist/commands/config-edit.js.map +1 -0
- package/dist/commands/doctor.d.ts +57 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +233 -59
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/setup.d.ts +4 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +58 -5
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +40 -16
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/uninstall.d.ts +78 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +329 -31
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +51 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +172 -71
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/wizard.d.ts +27 -1
- package/dist/commands/wizard.d.ts.map +1 -1
- package/dist/commands/wizard.js +48 -31
- package/dist/commands/wizard.js.map +1 -1
- package/dist/config/schema.d.ts +28 -3
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +206 -54
- package/dist/config/schema.js.map +1 -1
- package/dist/config/scopes.d.ts +4 -2
- package/dist/config/scopes.d.ts.map +1 -1
- package/dist/config/scopes.js +23 -4
- package/dist/config/scopes.js.map +1 -1
- package/dist/generate/agentsmd.d.ts +54 -0
- package/dist/generate/agentsmd.d.ts.map +1 -1
- package/dist/generate/agentsmd.js +222 -143
- package/dist/generate/agentsmd.js.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/render.d.ts.map +1 -1
- package/dist/integrations/render.js +25 -10
- package/dist/integrations/render.js.map +1 -1
- package/dist/integrations/superpowers.d.ts.map +1 -1
- package/dist/integrations/superpowers.js +2 -0
- package/dist/integrations/superpowers.js.map +1 -1
- package/dist/shell/detector.d.ts +63 -5
- package/dist/shell/detector.d.ts.map +1 -1
- package/dist/shell/detector.js +253 -65
- package/dist/shell/detector.js.map +1 -1
- package/dist/toolchain/gh.d.ts.map +1 -1
- package/dist/toolchain/gh.js +1 -0
- package/dist/toolchain/gh.js.map +1 -1
- package/dist/toolchain/mise.d.ts +7 -0
- package/dist/toolchain/mise.d.ts.map +1 -1
- package/dist/toolchain/mise.js +20 -3
- package/dist/toolchain/mise.js.map +1 -1
- package/dist/toolchain/rtk.d.ts +71 -3
- package/dist/toolchain/rtk.d.ts.map +1 -1
- package/dist/toolchain/rtk.js +115 -7
- package/dist/toolchain/rtk.js.map +1 -1
- package/dist/ui/output.d.ts.map +1 -1
- package/dist/ui/output.js +10 -2
- package/dist/ui/output.js.map +1 -1
- package/dist/ui/spinner.d.ts +15 -3
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +52 -4
- package/dist/ui/spinner.js.map +1 -1
- package/dist/ui/theme.d.ts +27 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +73 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/utils/fs-retry.d.ts +16 -0
- package/dist/utils/fs-retry.d.ts.map +1 -1
- package/dist/utils/fs-retry.js +28 -0
- package/dist/utils/fs-retry.js.map +1 -1
- package/package.json +6 -5
package/dist/shell/detector.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from 'path';
|
|
|
7
7
|
import * as winPath from 'path/win32';
|
|
8
8
|
import * as child_process from 'child_process';
|
|
9
9
|
import { AGENT_KEYS } from '../config/schema.js';
|
|
10
|
+
import { homeDir } from '../adapters/agent-dirs.js';
|
|
10
11
|
import { getShimsDirValue, pathContainsDir, removeShimsDirValue } from '../toolchain/mise.js';
|
|
11
12
|
import { writeFileWithRetry } from '../utils/fs-retry.js';
|
|
12
13
|
import { clearShellFixState, mergeShellFixEntries, readShellFixState, shellFixStatePath, writeShellFixState, } from './shell-fix-state.js';
|
|
@@ -28,7 +29,7 @@ export const POSIX_UTILITIES = [
|
|
|
28
29
|
'mv',
|
|
29
30
|
'less',
|
|
30
31
|
];
|
|
31
|
-
// Well-known paths for Git Bash on Windows
|
|
32
|
+
// Well-known machine-wide paths for Git Bash on Windows
|
|
32
33
|
export const GIT_BASH_PATHS = [
|
|
33
34
|
'C:\\Program Files\\Git\\bin',
|
|
34
35
|
'C:\\Program Files\\Git\\usr\\bin',
|
|
@@ -36,9 +37,67 @@ export const GIT_BASH_PATHS = [
|
|
|
36
37
|
'C:\\Program Files (x86)\\Git\\usr\\bin',
|
|
37
38
|
];
|
|
38
39
|
/**
|
|
39
|
-
*
|
|
40
|
+
* Candidate Git Bash directories: the well-known machine-wide paths plus
|
|
41
|
+
* per-user (`%LOCALAPPDATA%\Programs\Git`, the default for a non-admin Git
|
|
42
|
+
* for Windows install) and scoop install locations, which vary per machine
|
|
43
|
+
* and so can't be hardcoded in GIT_BASH_PATHS.
|
|
40
44
|
*/
|
|
41
|
-
export function
|
|
45
|
+
export function gitBashCandidatePaths(env = process.env) {
|
|
46
|
+
const candidates = [...GIT_BASH_PATHS];
|
|
47
|
+
const localAppData = env.LOCALAPPDATA;
|
|
48
|
+
if (localAppData) {
|
|
49
|
+
candidates.push(path.join(localAppData, 'Programs', 'Git', 'bin'), path.join(localAppData, 'Programs', 'Git', 'usr', 'bin'));
|
|
50
|
+
}
|
|
51
|
+
const scoopRoot = env.SCOOP || (env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop') : undefined);
|
|
52
|
+
if (scoopRoot) {
|
|
53
|
+
candidates.push(path.join(scoopRoot, 'apps', 'git', 'current', 'bin'), path.join(scoopRoot, 'apps', 'git', 'current', 'usr', 'bin'));
|
|
54
|
+
}
|
|
55
|
+
return candidates;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fall back to searching PATH for git.exe when none of the well-known
|
|
59
|
+
* install locations exist — covers custom install directories the
|
|
60
|
+
* candidate list can't anticipate. Derives the Git Bash root from git.exe's
|
|
61
|
+
* location (`<root>\cmd\git.exe` or `<root>\bin\git.exe`) and checks that
|
|
62
|
+
* root's `bin` and `usr\bin` directories.
|
|
63
|
+
*/
|
|
64
|
+
export function findGitBashOnPath(spawnSync = child_process.spawnSync) {
|
|
65
|
+
try {
|
|
66
|
+
const where = spawnSync('cmd.exe', ['/c', 'where', 'git'], {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
});
|
|
70
|
+
if (where.status !== 0 || !where.stdout) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const gitExe = where.stdout
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.map((line) => line.trim())
|
|
76
|
+
.find((line) => line.length > 0);
|
|
77
|
+
if (!gitExe) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const root = path.dirname(path.dirname(gitExe));
|
|
81
|
+
for (const sub of ['bin', path.join('usr', 'bin')]) {
|
|
82
|
+
const candidate = path.join(root, sub);
|
|
83
|
+
if (fs.existsSync(candidate)) {
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Detect the current shell environment.
|
|
95
|
+
*
|
|
96
|
+
* `spawnSync` is injectable (defaults to the real `child_process.spawnSync`)
|
|
97
|
+
* purely so tests can observe — and assert nothing installs anything through
|
|
98
|
+
* — the one remaining subprocess call this function makes directly.
|
|
99
|
+
*/
|
|
100
|
+
export function detectShell(spawnSync = child_process.spawnSync) {
|
|
42
101
|
const isWindows = process.platform === 'win32';
|
|
43
102
|
const isMacOS = process.platform === 'darwin';
|
|
44
103
|
const currentShell = determineCurrentShell();
|
|
@@ -58,7 +117,7 @@ export function detectShell() {
|
|
|
58
117
|
// Find Git Bash path (used by the Tier 0 fix even when the current shell
|
|
59
118
|
// is PowerShell/cmd, so agents get pointed at a real POSIX shell).
|
|
60
119
|
// Use path.resolve() to handle paths with spaces properly.
|
|
61
|
-
for (const gitPath of
|
|
120
|
+
for (const gitPath of gitBashCandidatePaths()) {
|
|
62
121
|
try {
|
|
63
122
|
const resolvedPath = path.resolve(gitPath);
|
|
64
123
|
if (fs.existsSync(resolvedPath)) {
|
|
@@ -70,31 +129,36 @@ export function detectShell() {
|
|
|
70
129
|
// Ignore
|
|
71
130
|
}
|
|
72
131
|
}
|
|
132
|
+
// None of the well-known install locations exist: search PATH itself,
|
|
133
|
+
// which covers custom install directories.
|
|
134
|
+
if (!gitBashPath) {
|
|
135
|
+
gitBashPath = findGitBashOnPath(spawnSync);
|
|
136
|
+
}
|
|
73
137
|
}
|
|
74
138
|
else if (isMacOS) {
|
|
75
|
-
// On macOS, check for GNU coreutils
|
|
139
|
+
// On macOS, check for GNU coreutils. detectShell() must stay read-only —
|
|
140
|
+
// status/doctor call it too, and both are documented as never changing
|
|
141
|
+
// the machine — so it only ever detects, never installs. Installing is
|
|
142
|
+
// fixShellConfiguration()'s job (the explicit, reported Tier 0 apply
|
|
143
|
+
// step), which calls installGNUCoreutilsMacOS() itself when needed.
|
|
76
144
|
isPosixCompatible = checkGNUCoreutils();
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const result = child_process.spawnSync('brew', ['--prefix', 'coreutils'], {
|
|
88
|
-
encoding: 'utf-8',
|
|
89
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
-
});
|
|
145
|
+
// Read-only probe: report the gnubin path when coreutils happen to
|
|
146
|
+
// already be installed via Homebrew; do nothing (and no error) otherwise.
|
|
147
|
+
try {
|
|
148
|
+
const result = spawnSync('brew', ['--prefix', 'coreutils'], {
|
|
149
|
+
encoding: 'utf-8',
|
|
150
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
|
+
timeout: 10000,
|
|
152
|
+
});
|
|
153
|
+
if (result.status === 0) {
|
|
91
154
|
const prefix = result.stdout.trim();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
catch {
|
|
95
|
-
// coreutils not installed via Homebrew, or Homebrew not available
|
|
155
|
+
if (prefix)
|
|
156
|
+
gnubinPath = path.join(prefix, 'libexec', 'gnubin');
|
|
96
157
|
}
|
|
97
158
|
}
|
|
159
|
+
catch {
|
|
160
|
+
// coreutils not installed via Homebrew, or Homebrew not available
|
|
161
|
+
}
|
|
98
162
|
}
|
|
99
163
|
else {
|
|
100
164
|
// On Linux and other Unix-like systems, check for GNU coreutils
|
|
@@ -165,7 +229,7 @@ function checkGitBash() {
|
|
|
165
229
|
}
|
|
166
230
|
// Check PATH for Git Bash directories (spaces-safe, case-folded on win32)
|
|
167
231
|
const pathEnv = process.env.PATH || '';
|
|
168
|
-
for (const gitPath of
|
|
232
|
+
for (const gitPath of gitBashCandidatePaths()) {
|
|
169
233
|
if (pathContainsDir(pathEnv, gitPath)) {
|
|
170
234
|
return true;
|
|
171
235
|
}
|
|
@@ -192,11 +256,13 @@ export function checkGNUCoreutils() {
|
|
|
192
256
|
const grepResult = child_process.spawnSync('sh', ['-c', 'command -v grep'], {
|
|
193
257
|
encoding: 'utf-8',
|
|
194
258
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
259
|
+
timeout: 5000,
|
|
195
260
|
});
|
|
196
261
|
const grepPath = grepResult.stdout.trim();
|
|
197
262
|
if (grepPath) {
|
|
198
263
|
const grepCheck = child_process.spawnSync(grepPath, ['-P', '--version'], {
|
|
199
264
|
stdio: 'ignore',
|
|
265
|
+
timeout: 5000,
|
|
200
266
|
});
|
|
201
267
|
if (grepCheck.status === 0)
|
|
202
268
|
return true;
|
|
@@ -205,10 +271,14 @@ export function checkGNUCoreutils() {
|
|
|
205
271
|
const sedResult = child_process.spawnSync('sh', ['-c', 'command -v sed'], {
|
|
206
272
|
encoding: 'utf-8',
|
|
207
273
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
274
|
+
timeout: 5000,
|
|
208
275
|
});
|
|
209
276
|
const sedPath = sedResult.stdout.trim();
|
|
210
277
|
if (sedPath) {
|
|
211
|
-
const sedCheck = child_process.spawnSync(sedPath, ['--version'], {
|
|
278
|
+
const sedCheck = child_process.spawnSync(sedPath, ['--version'], {
|
|
279
|
+
stdio: 'ignore',
|
|
280
|
+
timeout: 5000,
|
|
281
|
+
});
|
|
212
282
|
if (sedCheck.status === 0)
|
|
213
283
|
return true;
|
|
214
284
|
}
|
|
@@ -228,7 +298,10 @@ function installGNUCoreutilsMacOS() {
|
|
|
228
298
|
}
|
|
229
299
|
try {
|
|
230
300
|
// Check if Homebrew is installed
|
|
231
|
-
const brewOk = child_process.spawnSync('sh', ['-c', 'command -v brew'], {
|
|
301
|
+
const brewOk = child_process.spawnSync('sh', ['-c', 'command -v brew'], {
|
|
302
|
+
stdio: 'ignore',
|
|
303
|
+
timeout: 5000,
|
|
304
|
+
});
|
|
232
305
|
if (brewOk.status !== 0) {
|
|
233
306
|
return undefined;
|
|
234
307
|
}
|
|
@@ -238,15 +311,22 @@ function installGNUCoreutilsMacOS() {
|
|
|
238
311
|
// Only install when the formula is not already installed
|
|
239
312
|
const listed = child_process.spawnSync('brew', ['list', '--formula', pkg], {
|
|
240
313
|
stdio: 'ignore',
|
|
314
|
+
timeout: 10000,
|
|
241
315
|
});
|
|
242
316
|
if (listed.status !== 0) {
|
|
243
|
-
|
|
317
|
+
// An actual package install (network + possibly a build), unlike the
|
|
318
|
+
// read-only probes around it — given more room before we give up.
|
|
319
|
+
child_process.spawnSync('brew', ['install', '--quiet', pkg], {
|
|
320
|
+
stdio: 'ignore',
|
|
321
|
+
timeout: 120000,
|
|
322
|
+
});
|
|
244
323
|
}
|
|
245
324
|
}
|
|
246
325
|
// Get the gnubin path from coreutils
|
|
247
326
|
const result = child_process.spawnSync('brew', ['--prefix', 'coreutils'], {
|
|
248
327
|
encoding: 'utf-8',
|
|
249
328
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
329
|
+
timeout: 10000,
|
|
250
330
|
});
|
|
251
331
|
if (result.status !== 0) {
|
|
252
332
|
return undefined;
|
|
@@ -270,11 +350,17 @@ export function checkMissingUtilities(isPosixCompatible) {
|
|
|
270
350
|
for (const util of POSIX_UTILITIES) {
|
|
271
351
|
// `where` reports "not found" via a non-zero exit code (the INFO text goes
|
|
272
352
|
// to stderr), so checking the exit status is what distinguishes "found".
|
|
273
|
-
const where = child_process.spawnSync('cmd.exe', ['/c', 'where', util], {
|
|
353
|
+
const where = child_process.spawnSync('cmd.exe', ['/c', 'where', util], {
|
|
354
|
+
stdio: 'ignore',
|
|
355
|
+
timeout: 5000,
|
|
356
|
+
});
|
|
274
357
|
if (where.status === 0) {
|
|
275
358
|
continue;
|
|
276
359
|
}
|
|
277
|
-
const which = child_process.spawnSync('sh', ['-c', `command -v ${util}`], {
|
|
360
|
+
const which = child_process.spawnSync('sh', ['-c', `command -v ${util}`], {
|
|
361
|
+
stdio: 'ignore',
|
|
362
|
+
timeout: 5000,
|
|
363
|
+
});
|
|
278
364
|
if (which.status === 0) {
|
|
279
365
|
continue;
|
|
280
366
|
}
|
|
@@ -333,15 +419,110 @@ export function getShellConfiguration(shellInfo) {
|
|
|
333
419
|
};
|
|
334
420
|
}
|
|
335
421
|
/**
|
|
336
|
-
*
|
|
422
|
+
* The Git for Windows install root for a detected Git Bash directory
|
|
423
|
+
* (`<root>\bin` or `<root>\usr\bin`), or undefined for any other shape.
|
|
424
|
+
* Uses win32 path semantics so it is correct regardless of host OS.
|
|
425
|
+
*/
|
|
426
|
+
export function gitInstallRoot(gitBashPath) {
|
|
427
|
+
if (winPath.basename(gitBashPath).toLowerCase() !== 'bin')
|
|
428
|
+
return undefined;
|
|
429
|
+
const parent = winPath.dirname(gitBashPath);
|
|
430
|
+
return winPath.basename(parent).toLowerCase() === 'usr' ? winPath.dirname(parent) : parent;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The PATH entries that expose Git for Windows' GNU tools (sed, grep, awk,
|
|
434
|
+
* xargs, ...) to native Windows shells: `<root>\mingw64\bin;<root>\usr\bin`.
|
|
435
|
+
* Returns '' when the Git root can't be derived from `gitBashPath`.
|
|
337
436
|
*/
|
|
338
437
|
export function generateGitBashPathAddition(gitBashPath) {
|
|
339
|
-
|
|
438
|
+
const root = gitInstallRoot(gitBashPath);
|
|
439
|
+
if (!root)
|
|
340
440
|
return '';
|
|
441
|
+
return `${winPath.join(root, 'mingw64', 'bin')};${winPath.join(root, 'usr', 'bin')}`;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* GNU tools agents most often reach for in shell commands. On Windows these
|
|
445
|
+
* only resolve from PowerShell/cmd when Git's `usr\bin` is on PATH, which
|
|
446
|
+
* Git's installer does not do by default.
|
|
447
|
+
*/
|
|
448
|
+
export const NATIVE_UNIX_TOOLS = [
|
|
449
|
+
'grep',
|
|
450
|
+
'sed',
|
|
451
|
+
'awk',
|
|
452
|
+
'find',
|
|
453
|
+
'xargs',
|
|
454
|
+
'diff',
|
|
455
|
+
'head',
|
|
456
|
+
'tail',
|
|
457
|
+
'wc',
|
|
458
|
+
];
|
|
459
|
+
/**
|
|
460
|
+
* Resolve `tool` the way cmd.exe/PowerShell would: first PATH entry holding
|
|
461
|
+
* `tool` + a PATHEXT extension wins. `exists` is injectable for tests.
|
|
462
|
+
*/
|
|
463
|
+
export function resolveOnWindowsPath(tool, pathVar, pathExt = '.COM;.EXE;.BAT;.CMD', exists = fs.existsSync) {
|
|
464
|
+
const extensions = pathExt.split(';').filter((ext) => ext.trim() !== '');
|
|
465
|
+
for (const rawDir of pathVar.split(';')) {
|
|
466
|
+
const dir = rawDir.trim().replace(/^"(.*)"$/, '$1');
|
|
467
|
+
if (dir === '')
|
|
468
|
+
continue;
|
|
469
|
+
for (const ext of extensions) {
|
|
470
|
+
const candidate = winPath.join(dir, `${tool}${ext.toLowerCase()}`);
|
|
471
|
+
if (exists(candidate))
|
|
472
|
+
return candidate;
|
|
473
|
+
}
|
|
341
474
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
const PERSISTENT_PATH_KEYS = [
|
|
478
|
+
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment',
|
|
479
|
+
'HKCU\\Environment',
|
|
480
|
+
];
|
|
481
|
+
/**
|
|
482
|
+
* The persistent machine + user PATH from the registry — what a freshly
|
|
483
|
+
* started PowerShell/cmd (and any agent launched from one) sees, unlike
|
|
484
|
+
* process.env.PATH, which already includes Git's dirs when agentenv itself
|
|
485
|
+
* runs inside Git Bash. `%VAR%` references are expanded from `env`.
|
|
486
|
+
* Returns undefined when neither key could be read.
|
|
487
|
+
*/
|
|
488
|
+
export function readPersistentWindowsPath(spawnSync = child_process.spawnSync, env = process.env) {
|
|
489
|
+
const parts = [];
|
|
490
|
+
for (const key of PERSISTENT_PATH_KEYS) {
|
|
491
|
+
try {
|
|
492
|
+
const result = spawnSync('reg', ['query', key, '/v', 'Path'], {
|
|
493
|
+
encoding: 'utf8',
|
|
494
|
+
timeout: 5000,
|
|
495
|
+
});
|
|
496
|
+
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
497
|
+
continue;
|
|
498
|
+
const match = /^\s*Path\s+REG_(?:EXPAND_)?SZ\s+(.*)$/im.exec(result.stdout);
|
|
499
|
+
if (match)
|
|
500
|
+
parts.push(match[1].trim());
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Key unreadable; fall through to the other one
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (parts.length === 0)
|
|
507
|
+
return undefined;
|
|
508
|
+
const lookup = new Map(Object.entries(env).map(([name, value]) => [name.toLowerCase(), value]));
|
|
509
|
+
return parts
|
|
510
|
+
.join(';')
|
|
511
|
+
.replace(/%([^%;]+)%/g, (whole, name) => lookup.get(name.toLowerCase()) ?? whole);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* When `shell` is Git's raw MSYS bash (`<root>\usr\bin\bash.exe`) and the
|
|
515
|
+
* `<root>\bin\bash.exe` launcher exists, return the launcher. The raw binary
|
|
516
|
+
* started as a plain `bash -c` gets neither MSYSTEM nor `/usr/bin` on PATH,
|
|
517
|
+
* so sed/grep/awk resolve to nothing; the launcher sets both up.
|
|
518
|
+
*/
|
|
519
|
+
export function preferredGitBashLauncher(shell, exists = fs.existsSync) {
|
|
520
|
+
const normalized = shell.replace(/\//g, '\\');
|
|
521
|
+
if (!/\\usr\\bin\\bash\.exe$/i.test(normalized))
|
|
522
|
+
return undefined;
|
|
523
|
+
const root = winPath.dirname(winPath.dirname(winPath.dirname(normalized)));
|
|
524
|
+
const launcher = winPath.join(root, 'bin', 'bash.exe');
|
|
525
|
+
return exists(launcher) ? launcher : undefined;
|
|
345
526
|
}
|
|
346
527
|
/**
|
|
347
528
|
* Generate PATH addition for GNU coreutils on macOS
|
|
@@ -360,18 +541,19 @@ function priorField(key, previous, set) {
|
|
|
360
541
|
/**
|
|
361
542
|
* Resolve the full path to bash.exe from a detected Git Bash directory.
|
|
362
543
|
* Handles both the `usr\bin` form and the plain `bin` form Git for Windows ships.
|
|
544
|
+
* Prefers the `<root>\bin\bash.exe` launcher, which sets MSYSTEM and puts
|
|
545
|
+
* `/usr/bin` + `/mingw64/bin` on PATH; the raw `<root>\usr\bin\bash.exe` does
|
|
546
|
+
* neither for a non-login `bash -c`, so GNU tools go missing. Falls back to
|
|
547
|
+
* the raw binary only when the launcher isn't there.
|
|
363
548
|
* Uses win32 path semantics so the result is correct regardless of host OS
|
|
364
549
|
* (the tests exercise this on Linux CI).
|
|
365
550
|
*/
|
|
366
|
-
export function bashExecutable(gitBashPath) {
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return winPath.join(parent, 'usr', 'bin', 'bash.exe');
|
|
373
|
-
}
|
|
374
|
-
return winPath.join(gitBashPath, 'bash.exe');
|
|
551
|
+
export function bashExecutable(gitBashPath, exists = fs.existsSync) {
|
|
552
|
+
const root = gitInstallRoot(gitBashPath);
|
|
553
|
+
if (!root)
|
|
554
|
+
return winPath.join(gitBashPath, 'bash.exe');
|
|
555
|
+
const launcher = winPath.join(root, 'bin', 'bash.exe');
|
|
556
|
+
return exists(launcher) ? launcher : winPath.join(root, 'usr', 'bin', 'bash.exe');
|
|
375
557
|
}
|
|
376
558
|
/**
|
|
377
559
|
* Apply the per-agent Tier 0 shell override for one agent, per
|
|
@@ -609,35 +791,33 @@ function recordShellFixState(results, bashExe, statePath) {
|
|
|
609
791
|
*/
|
|
610
792
|
export function fixShellConfiguration(_baseDir = '.', agents = [...AGENT_KEYS], complex = process.stdin.isTTY === true, statePath = shellFixStatePath()) {
|
|
611
793
|
if (process.platform === 'win32') {
|
|
794
|
+
// No early return when agentenv itself runs in Git Bash: the fix targets
|
|
795
|
+
// each agent's own shell config, which doesn't depend on the shell
|
|
796
|
+
// agentenv was launched from (see checkAgentShellConfiguration).
|
|
612
797
|
const shellInfo = detectShell();
|
|
613
|
-
if (shellInfo.isPosixCompatible) {
|
|
614
|
-
return {
|
|
615
|
-
success: true,
|
|
616
|
-
message: 'Shell is already POSIX-compatible',
|
|
617
|
-
gitBashPath: shellInfo.gitBashPath,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
798
|
if (!shellInfo.gitBashPath) {
|
|
621
799
|
return {
|
|
622
800
|
success: true,
|
|
623
|
-
message: 'Git Bash not found on PATH; install Git for Windows to enable Tier 0 shell fixes',
|
|
801
|
+
message: 'Git Bash not found in common install locations or on PATH; install Git for Windows to enable Tier 0 shell fixes',
|
|
624
802
|
};
|
|
625
803
|
}
|
|
626
|
-
// Non-TTY/test shell: skip file writes, just
|
|
804
|
+
// Non-TTY/test shell, or tier0.mode = "never": skip file writes, just
|
|
805
|
+
// report the state. (Callers already prefix messages with "Tier 0: "
|
|
806
|
+
// themselves, so this string must not repeat it.)
|
|
627
807
|
if (!complex) {
|
|
628
808
|
return {
|
|
629
809
|
success: true,
|
|
630
|
-
message: `
|
|
810
|
+
message: `checked; would fix ${agents.length} agent shell(s) — skipped (pass --shell-fix always, or run interactively, to apply)`,
|
|
631
811
|
gitBashPath: shellInfo.gitBashPath,
|
|
632
812
|
};
|
|
633
813
|
}
|
|
634
814
|
const bashExe = bashExecutable(shellInfo.gitBashPath);
|
|
635
|
-
const home =
|
|
815
|
+
const home = homeDir();
|
|
636
816
|
const results = agents.map((agent) => applyAgentShellFix(agent, bashExe, home));
|
|
637
817
|
const written = results.filter((result) => result.action === 'created' || result.action === 'updated');
|
|
638
818
|
recordShellFixState(written, bashExe, statePath);
|
|
639
819
|
const message = written.length > 0
|
|
640
|
-
? `
|
|
820
|
+
? `pointed ${written.length} agent shell(s) at Git Bash (${bashExe})`
|
|
641
821
|
: `Git Bash detected at ${bashExe}; agent shell config already up to date`;
|
|
642
822
|
return {
|
|
643
823
|
success: true,
|
|
@@ -692,16 +872,16 @@ export function fixShellConfiguration(_baseDir = '.', agents = [...AGENT_KEYS],
|
|
|
692
872
|
* since that's the interactive shell the user is running agentenv from, not
|
|
693
873
|
* the target agent's configuration.
|
|
694
874
|
*/
|
|
695
|
-
export function checkAgentShellConfiguration(agent, home =
|
|
875
|
+
export function checkAgentShellConfiguration(agent, home = homeDir(), exists = fs.existsSync) {
|
|
696
876
|
switch (agent) {
|
|
697
877
|
case 'claude_code':
|
|
698
|
-
return checkClaudeCodeShell(home);
|
|
878
|
+
return checkClaudeCodeShell(home, exists);
|
|
699
879
|
case 'codex_cli':
|
|
700
|
-
return checkCodexShell(home);
|
|
880
|
+
return checkCodexShell(home, exists);
|
|
701
881
|
case 'copilot':
|
|
702
882
|
return checkCopilotShell();
|
|
703
883
|
case 'opencode':
|
|
704
|
-
return checkOpenCodeShell(home);
|
|
884
|
+
return checkOpenCodeShell(home, exists);
|
|
705
885
|
case 'gemini_cli':
|
|
706
886
|
case 'cursor':
|
|
707
887
|
case 'windsurf':
|
|
@@ -713,11 +893,19 @@ export function checkAgentShellConfiguration(agent, home = process.env.HOME || p
|
|
|
713
893
|
function isBashPath(value) {
|
|
714
894
|
return typeof value === 'string' && value.length > 0;
|
|
715
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* A configured shell still needs the Tier 0 fix when it isn't bash at all, or
|
|
898
|
+
* when it is Git's raw MSYS bash and the launcher exists to replace it (see
|
|
899
|
+
* preferredGitBashLauncher) — the same value bashExecutable() would write.
|
|
900
|
+
*/
|
|
901
|
+
function shellNeedsFix(shell, exists) {
|
|
902
|
+
return (!shell.toLowerCase().includes('bash') || preferredGitBashLauncher(shell, exists) !== undefined);
|
|
903
|
+
}
|
|
716
904
|
/**
|
|
717
905
|
* Check Claude Code shell configuration: reads env.CLAUDE_CODE_GIT_BASH_PATH,
|
|
718
906
|
* the exact field patchClaudeShellFix writes.
|
|
719
907
|
*/
|
|
720
|
-
function checkClaudeCodeShell(home) {
|
|
908
|
+
function checkClaudeCodeShell(home, exists) {
|
|
721
909
|
try {
|
|
722
910
|
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
723
911
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -728,7 +916,7 @@ function checkClaudeCodeShell(home) {
|
|
|
728
916
|
return {
|
|
729
917
|
isConfigured: true,
|
|
730
918
|
shell: bashPath,
|
|
731
|
-
needsFix:
|
|
919
|
+
needsFix: shellNeedsFix(bashPath, exists),
|
|
732
920
|
};
|
|
733
921
|
}
|
|
734
922
|
}
|
|
@@ -771,7 +959,7 @@ function readTomlWindowsShellPath(content) {
|
|
|
771
959
|
* Check Codex CLI shell configuration: reads [windows] shell_path from
|
|
772
960
|
* config.toml, the exact field patchCodexShellFix writes.
|
|
773
961
|
*/
|
|
774
|
-
function checkCodexShell(home) {
|
|
962
|
+
function checkCodexShell(home, exists) {
|
|
775
963
|
try {
|
|
776
964
|
const configPath = path.join(home, '.codex', 'config.toml');
|
|
777
965
|
if (fs.existsSync(configPath)) {
|
|
@@ -780,7 +968,7 @@ function checkCodexShell(home) {
|
|
|
780
968
|
return {
|
|
781
969
|
isConfigured: true,
|
|
782
970
|
shell: shellPath,
|
|
783
|
-
needsFix:
|
|
971
|
+
needsFix: shellNeedsFix(shellPath, exists),
|
|
784
972
|
};
|
|
785
973
|
}
|
|
786
974
|
}
|
|
@@ -802,14 +990,14 @@ function checkCopilotShell() {
|
|
|
802
990
|
* Check OpenCode shell configuration: reads `shell`, the exact field
|
|
803
991
|
* patchOpenCodeShellFix writes.
|
|
804
992
|
*/
|
|
805
|
-
function checkOpenCodeShell(home) {
|
|
993
|
+
function checkOpenCodeShell(home, exists) {
|
|
806
994
|
try {
|
|
807
995
|
const configPath = path.join(home, '.config', 'opencode', 'opencode.json');
|
|
808
996
|
if (fs.existsSync(configPath)) {
|
|
809
997
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
810
998
|
const shell = config.shell;
|
|
811
999
|
if (isBashPath(shell)) {
|
|
812
|
-
return { isConfigured: true, shell, needsFix:
|
|
1000
|
+
return { isConfigured: true, shell, needsFix: shellNeedsFix(shell, exists) };
|
|
813
1001
|
}
|
|
814
1002
|
}
|
|
815
1003
|
return { isConfigured: false, needsFix: true };
|