@indy2kro/agentenv 0.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/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/adapters/base.d.ts +58 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +16 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +21 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +198 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +23 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +134 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/copilot.d.ts +21 -0
- package/dist/adapters/copilot.d.ts.map +1 -0
- package/dist/adapters/copilot.js +111 -0
- package/dist/adapters/copilot.js.map +1 -0
- package/dist/adapters/detect.d.ts +23 -0
- package/dist/adapters/detect.d.ts.map +1 -0
- package/dist/adapters/detect.js +80 -0
- package/dist/adapters/detect.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +19 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/opencode.js +107 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/commands/apply.d.ts +21 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js +113 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/configure.d.ts +13 -0
- package/dist/commands/configure.d.ts.map +1 -0
- package/dist/commands/configure.js +151 -0
- package/dist/commands/configure.js.map +1 -0
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +78 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +152 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config/schema.d.ts +142 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +778 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/scopes.d.ts +14 -0
- package/dist/config/scopes.d.ts.map +1 -0
- package/dist/config/scopes.js +32 -0
- package/dist/config/scopes.js.map +1 -0
- package/dist/generate/agentsmd.d.ts +44 -0
- package/dist/generate/agentsmd.d.ts.map +1 -0
- package/dist/generate/agentsmd.js +340 -0
- package/dist/generate/agentsmd.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/base.d.ts +33 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +9 -0
- package/dist/integrations/base.js.map +1 -0
- package/dist/integrations/index.d.ts +7 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +5 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/superpowers.d.ts +48 -0
- package/dist/integrations/superpowers.d.ts.map +1 -0
- package/dist/integrations/superpowers.js +191 -0
- package/dist/integrations/superpowers.js.map +1 -0
- package/dist/shell/detector.d.ts +92 -0
- package/dist/shell/detector.d.ts.map +1 -0
- package/dist/shell/detector.js +714 -0
- package/dist/shell/detector.js.map +1 -0
- package/dist/toolchain/fallbacks.d.ts +77 -0
- package/dist/toolchain/fallbacks.d.ts.map +1 -0
- package/dist/toolchain/fallbacks.js +238 -0
- package/dist/toolchain/fallbacks.js.map +1 -0
- package/dist/toolchain/gh.d.ts +20 -0
- package/dist/toolchain/gh.d.ts.map +1 -0
- package/dist/toolchain/gh.js +36 -0
- package/dist/toolchain/gh.js.map +1 -0
- package/dist/toolchain/mise.d.ts +92 -0
- package/dist/toolchain/mise.d.ts.map +1 -0
- package/dist/toolchain/mise.js +346 -0
- package/dist/toolchain/mise.js.map +1 -0
- package/dist/toolchain/rtk.d.ts +27 -0
- package/dist/toolchain/rtk.d.ts.map +1 -0
- package/dist/toolchain/rtk.js +76 -0
- package/dist/toolchain/rtk.js.map +1 -0
- package/dist/wizard/build.d.ts +24 -0
- package/dist/wizard/build.d.ts.map +1 -0
- package/dist/wizard/build.js +64 -0
- package/dist/wizard/build.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 Shell Detection and Configuration
|
|
3
|
+
* Detects the shell environment and ensures POSIX compatibility on Windows
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as winPath from 'path/win32';
|
|
8
|
+
import * as child_process from 'child_process';
|
|
9
|
+
import { AGENT_KEYS } from '../config/schema.js';
|
|
10
|
+
// Well-known POSIX utilities that should be available
|
|
11
|
+
export const POSIX_UTILITIES = [
|
|
12
|
+
'grep',
|
|
13
|
+
'sed',
|
|
14
|
+
'awk',
|
|
15
|
+
'find',
|
|
16
|
+
'diff',
|
|
17
|
+
'tar',
|
|
18
|
+
'gzip',
|
|
19
|
+
'curl',
|
|
20
|
+
'cat',
|
|
21
|
+
'ls',
|
|
22
|
+
'mkdir',
|
|
23
|
+
'rm',
|
|
24
|
+
'cp',
|
|
25
|
+
'mv',
|
|
26
|
+
'less',
|
|
27
|
+
];
|
|
28
|
+
// Well-known paths for Git Bash on Windows
|
|
29
|
+
export const GIT_BASH_PATHS = [
|
|
30
|
+
'C:\\Program Files\\Git\\bin',
|
|
31
|
+
'C:\\Program Files\\Git\\usr\\bin',
|
|
32
|
+
'C:\\Program Files (x86)\\Git\\bin',
|
|
33
|
+
'C:\\Program Files (x86)\\Git\\usr\\bin',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Detect the current shell environment
|
|
37
|
+
*/
|
|
38
|
+
export function detectShell() {
|
|
39
|
+
const isWindows = process.platform === 'win32';
|
|
40
|
+
const isMacOS = process.platform === 'darwin';
|
|
41
|
+
const currentShell = determineCurrentShell();
|
|
42
|
+
// Check if we're already in a POSIX-compatible environment
|
|
43
|
+
let isPosixCompatible;
|
|
44
|
+
let isGitBash = false;
|
|
45
|
+
let gitBashPath;
|
|
46
|
+
let gnubinPath;
|
|
47
|
+
const pathEnvironment = process.env.PATH || '';
|
|
48
|
+
if (isWindows) {
|
|
49
|
+
// Check if we're in Git Bash (MSYS2)
|
|
50
|
+
isGitBash = checkGitBash();
|
|
51
|
+
isPosixCompatible = isGitBash;
|
|
52
|
+
// Find Git Bash path
|
|
53
|
+
for (const gitPath of GIT_BASH_PATHS) {
|
|
54
|
+
try {
|
|
55
|
+
if (fs.existsSync(gitPath)) {
|
|
56
|
+
gitBashPath = gitPath;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Check PATH for Git Bash directories
|
|
65
|
+
if (!isGitBash && gitBashPath) {
|
|
66
|
+
// Check if Git Bash binaries are in PATH
|
|
67
|
+
const pathParts = pathEnvironment.split(path.delimiter);
|
|
68
|
+
const hasGitInPath = pathParts.some((p) => p.includes('Git') && (p.includes('bin') || p.includes('usr\\bin')));
|
|
69
|
+
if (hasGitInPath) {
|
|
70
|
+
isPosixCompatible = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (isMacOS) {
|
|
75
|
+
// On macOS, check for GNU coreutils
|
|
76
|
+
isPosixCompatible = checkGNUCoreutils();
|
|
77
|
+
// If GNU coreutils are not available, we may need to install them
|
|
78
|
+
if (!isPosixCompatible) {
|
|
79
|
+
gnubinPath = installGNUCoreutilsMacOS();
|
|
80
|
+
if (gnubinPath) {
|
|
81
|
+
isPosixCompatible = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Try to get the gnubin path if coreutils are installed
|
|
86
|
+
try {
|
|
87
|
+
const prefix = child_process
|
|
88
|
+
.execSync('brew --prefix coreutils', { encoding: 'utf-8' })
|
|
89
|
+
.trim();
|
|
90
|
+
gnubinPath = path.join(prefix, 'libexec', 'gnubin');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// coreutils not installed via Homebrew, or Homebrew not available
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// On Linux and other Unix-like systems, check for GNU coreutils
|
|
99
|
+
// Most Linux distros ship GNU tools by default
|
|
100
|
+
isPosixCompatible = checkGNUCoreutils();
|
|
101
|
+
}
|
|
102
|
+
// Check for missing POSIX utilities
|
|
103
|
+
const missingUtilities = isWindows ? checkMissingUtilities(isPosixCompatible) : [];
|
|
104
|
+
return {
|
|
105
|
+
currentShell,
|
|
106
|
+
isPosixCompatible,
|
|
107
|
+
isWindows: isWindows,
|
|
108
|
+
isMacOS: isMacOS,
|
|
109
|
+
isGitBash,
|
|
110
|
+
gitBashPath,
|
|
111
|
+
gnubinPath,
|
|
112
|
+
missingUtilities,
|
|
113
|
+
pathEnvironment,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Determine the current shell from environment
|
|
118
|
+
*/
|
|
119
|
+
function determineCurrentShell() {
|
|
120
|
+
// On Windows
|
|
121
|
+
if (process.platform === 'win32') {
|
|
122
|
+
const comspec = process.env.COMSPEC;
|
|
123
|
+
const shell = process.env.SHELL;
|
|
124
|
+
if (shell) {
|
|
125
|
+
// In Git Bash, SHELL points to bash
|
|
126
|
+
if (shell.includes('bash')) {
|
|
127
|
+
return shell;
|
|
128
|
+
}
|
|
129
|
+
return shell;
|
|
130
|
+
}
|
|
131
|
+
if (comspec) {
|
|
132
|
+
return comspec;
|
|
133
|
+
}
|
|
134
|
+
return 'Unknown Windows shell';
|
|
135
|
+
}
|
|
136
|
+
// On Unix-like systems
|
|
137
|
+
const shell = process.env.SHELL;
|
|
138
|
+
if (shell) {
|
|
139
|
+
return shell;
|
|
140
|
+
}
|
|
141
|
+
return '/bin/sh';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if we're running in Git Bash
|
|
145
|
+
*/
|
|
146
|
+
function checkGitBash() {
|
|
147
|
+
if (process.platform !== 'win32') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
// Check for MSYS2 environment variable
|
|
152
|
+
if (process.env.MSYSTEM) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
// Check for MINGW environment
|
|
156
|
+
if (process.env.MSYS2_ARG_CONV_EXCL) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
// Check if bash.exe is in the current process path
|
|
160
|
+
const shell = process.env.SHELL || '';
|
|
161
|
+
if (shell.toLowerCase().includes('bash')) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
// Check PATH for Git Bash directories
|
|
165
|
+
const pathEnv = process.env.PATH || '';
|
|
166
|
+
const pathParts = pathEnv.split(';');
|
|
167
|
+
for (const gitPath of GIT_BASH_PATHS) {
|
|
168
|
+
if (pathParts.some((p) => p.toLowerCase() === gitPath.toLowerCase())) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check if GNU coreutils are available (for macOS/Linux)
|
|
180
|
+
* Uses `command -v` to get the resolved path, then checks version output.
|
|
181
|
+
* This handles shell function shadowing (e.g., Claude Code's ugrep/bfs).
|
|
182
|
+
*/
|
|
183
|
+
function checkGNUCoreutils() {
|
|
184
|
+
if (process.platform === 'win32') {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
// Check if we have GNU versions of tools
|
|
189
|
+
// Use `command -v` to get the actual resolved path (works through shell functions)
|
|
190
|
+
// BSD vs GNU grep: GNU grep has -P flag for PCRE
|
|
191
|
+
try {
|
|
192
|
+
// First, get the resolved path - this will follow shell function aliases.
|
|
193
|
+
// Then invoke the binary directly rather than building a shell string so
|
|
194
|
+
// paths containing spaces keep working reliably.
|
|
195
|
+
const grepPath = child_process.execSync('command -v grep', { encoding: 'utf-8' }).trim();
|
|
196
|
+
child_process.spawnSync(grepPath, ['-P', '--version'], { stdio: 'ignore' });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Try other indicators
|
|
201
|
+
}
|
|
202
|
+
// Check for GNU sed via resolved path
|
|
203
|
+
try {
|
|
204
|
+
const sedPath = child_process.execSync('command -v sed', { encoding: 'utf-8' }).trim();
|
|
205
|
+
// GNU sed supports --version; BSD sed does not
|
|
206
|
+
child_process.spawnSync(sedPath, ['--version'], { stdio: 'ignore' });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Install GNU coreutils on macOS via Homebrew.
|
|
220
|
+
* Returns the gnubin path that needs to be prepended to PATH, or undefined if installation fails.
|
|
221
|
+
*/
|
|
222
|
+
function installGNUCoreutilsMacOS() {
|
|
223
|
+
if (process.platform !== 'darwin') {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
// Check if Homebrew is installed
|
|
228
|
+
child_process.execSync('command -v brew', { stdio: 'ignore' });
|
|
229
|
+
// Install coreutils, gnu-sed, grep, findutils, gawk if not already installed
|
|
230
|
+
const packages = ['coreutils', 'gnu-sed', 'grep', 'findutils', 'gawk'];
|
|
231
|
+
for (const pkg of packages) {
|
|
232
|
+
try {
|
|
233
|
+
// Check if already installed
|
|
234
|
+
child_process.execSync(`brew list --formula ${pkg}`, { stdio: 'ignore' });
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Not installed, install it
|
|
238
|
+
child_process.execSync(`brew install --quiet ${pkg}`, { stdio: 'ignore' });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Get the gnubin path from coreutils
|
|
242
|
+
const prefix = child_process.execSync('brew --prefix coreutils', { encoding: 'utf-8' }).trim();
|
|
243
|
+
const gnubinPath = path.join(prefix, 'libexec', 'gnubin');
|
|
244
|
+
return gnubinPath;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check which POSIX utilities are missing on Windows
|
|
252
|
+
*/
|
|
253
|
+
function checkMissingUtilities(isPosixCompatible) {
|
|
254
|
+
if (process.platform !== 'win32' || isPosixCompatible) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const missing = [];
|
|
258
|
+
for (const util of POSIX_UTILITIES) {
|
|
259
|
+
try {
|
|
260
|
+
// Try to find the utility
|
|
261
|
+
child_process.execSync(`where ${util}`, { stdio: 'ignore' });
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
try {
|
|
265
|
+
child_process.execSync(`which ${util}`, { stdio: 'ignore' });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
missing.push(util);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return missing;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Generate shell configuration for an agent
|
|
276
|
+
* Returns the shell configuration that should be used
|
|
277
|
+
*/
|
|
278
|
+
export function getShellConfiguration(shellInfo) {
|
|
279
|
+
if (shellInfo.isWindows) {
|
|
280
|
+
// On Windows
|
|
281
|
+
if (shellInfo.isPosixCompatible) {
|
|
282
|
+
return {
|
|
283
|
+
shell: shellInfo.currentShell,
|
|
284
|
+
posixPath: shellInfo.gitBashPath,
|
|
285
|
+
needsConfiguration: false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Need to configure Git Bash
|
|
289
|
+
if (shellInfo.gitBashPath) {
|
|
290
|
+
return {
|
|
291
|
+
shell: 'bash.exe',
|
|
292
|
+
posixPath: shellInfo.gitBashPath,
|
|
293
|
+
needsConfiguration: true,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Git Bash not found
|
|
297
|
+
return {
|
|
298
|
+
shell: shellInfo.currentShell,
|
|
299
|
+
needsConfiguration: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// On macOS and Linux
|
|
303
|
+
if (shellInfo.isPosixCompatible) {
|
|
304
|
+
return {
|
|
305
|
+
shell: shellInfo.currentShell,
|
|
306
|
+
gnuPath: shellInfo.gnubinPath,
|
|
307
|
+
needsConfiguration: false,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// Need to configure GNU coreutils on macOS
|
|
311
|
+
if (shellInfo.isMacOS && shellInfo.gnubinPath) {
|
|
312
|
+
return {
|
|
313
|
+
shell: shellInfo.currentShell,
|
|
314
|
+
gnuPath: shellInfo.gnubinPath,
|
|
315
|
+
needsConfiguration: true,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Cannot configure
|
|
319
|
+
return {
|
|
320
|
+
shell: shellInfo.currentShell,
|
|
321
|
+
needsConfiguration: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Generate PATH addition for Git Bash on Windows
|
|
326
|
+
*/
|
|
327
|
+
export function generateGitBashPathAddition(gitBashPath) {
|
|
328
|
+
if (process.platform !== 'win32') {
|
|
329
|
+
return '';
|
|
330
|
+
}
|
|
331
|
+
const binPath = path.join(gitBashPath, '..', 'mingw64', 'bin');
|
|
332
|
+
const usrBinPath = path.join(gitBashPath, '..', 'usr', 'bin');
|
|
333
|
+
return `${binPath};${usrBinPath}`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generate PATH addition for GNU coreutils on macOS
|
|
337
|
+
*/
|
|
338
|
+
export function generateMacOSGNUPathAddition(gnubinPath) {
|
|
339
|
+
if (process.platform !== 'darwin') {
|
|
340
|
+
return '';
|
|
341
|
+
}
|
|
342
|
+
return gnubinPath;
|
|
343
|
+
}
|
|
344
|
+
// Export the installation function for use by other modules
|
|
345
|
+
export { installGNUCoreutilsMacOS };
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the full path to bash.exe from a detected Git Bash directory.
|
|
348
|
+
* Handles both the `usr\bin` form and the plain `bin` form Git for Windows ships.
|
|
349
|
+
* Uses win32 path semantics so the result is correct regardless of host OS
|
|
350
|
+
* (the tests exercise this on Linux CI).
|
|
351
|
+
*/
|
|
352
|
+
export function bashExecutable(gitBashPath) {
|
|
353
|
+
const parent = winPath.dirname(gitBashPath);
|
|
354
|
+
if (winPath.basename(gitBashPath).toLowerCase() === 'bin') {
|
|
355
|
+
if (winPath.basename(parent).toLowerCase() === 'usr') {
|
|
356
|
+
return winPath.join(gitBashPath, 'bash.exe');
|
|
357
|
+
}
|
|
358
|
+
return winPath.join(parent, 'usr', 'bin', 'bash.exe');
|
|
359
|
+
}
|
|
360
|
+
return winPath.join(gitBashPath, 'bash.exe');
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Apply the per-agent Tier 0 shell override for one agent, per
|
|
364
|
+
* docs/research/tier0-shell-fix.md:
|
|
365
|
+
* - claude_code: env.CLAUDE_CODE_GIT_BASH_PATH in ~/.claude/settings.json
|
|
366
|
+
* - codex_cli: [windows] shell_path in ~/.codex/config.toml
|
|
367
|
+
* - opencode: shell + defaultShell in ~/.config/opencode/opencode.json
|
|
368
|
+
* - copilot: no per-file override exists (SHELL env var); skipped
|
|
369
|
+
*
|
|
370
|
+
* Existing user content is preserved; files are only written when a fix is
|
|
371
|
+
* actually needed, so repeated runs degrade to "unchanged".
|
|
372
|
+
*/
|
|
373
|
+
export function applyAgentShellFix(agent, bashExe, home) {
|
|
374
|
+
switch (agent) {
|
|
375
|
+
case 'claude_code':
|
|
376
|
+
return patchClaudeShellFix(bashExe, home);
|
|
377
|
+
case 'codex_cli':
|
|
378
|
+
return patchCodexShellFix(bashExe, home);
|
|
379
|
+
case 'opencode':
|
|
380
|
+
return patchOpenCodeShellFix(bashExe, home);
|
|
381
|
+
case 'copilot':
|
|
382
|
+
return {
|
|
383
|
+
agent,
|
|
384
|
+
file: '',
|
|
385
|
+
action: 'skipped',
|
|
386
|
+
message: 'GitHub Copilot follows the SHELL env var; Git Bash bin dirs on PATH cover this',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function patchClaudeShellFix(bashExe, home) {
|
|
391
|
+
const file = path.join(home, '.claude', 'settings.json');
|
|
392
|
+
let settings;
|
|
393
|
+
if (!fs.existsSync(file)) {
|
|
394
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
395
|
+
fs.writeFileSync(file, JSON.stringify({ env: { CLAUDE_CODE_GIT_BASH_PATH: bashExe } }, null, 2));
|
|
396
|
+
return {
|
|
397
|
+
agent: 'claude_code',
|
|
398
|
+
file,
|
|
399
|
+
action: 'created',
|
|
400
|
+
message: `Set CLAUDE_CODE_GIT_BASH_PATH in ${file}`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
settings = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return {
|
|
408
|
+
agent: 'claude_code',
|
|
409
|
+
file,
|
|
410
|
+
action: 'skipped',
|
|
411
|
+
message: `${file} is not valid JSON; refusing to modify it`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const env = (settings.env ?? {});
|
|
415
|
+
const bashKey = 'CLAUDE_CODE_GIT_BASH_PATH';
|
|
416
|
+
if (env[bashKey] === bashExe) {
|
|
417
|
+
return { agent: 'claude_code', file, action: 'unchanged', message: `Already set in ${file}` };
|
|
418
|
+
}
|
|
419
|
+
env[bashKey] = bashExe;
|
|
420
|
+
settings.env = env;
|
|
421
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2));
|
|
422
|
+
return {
|
|
423
|
+
agent: 'claude_code',
|
|
424
|
+
file,
|
|
425
|
+
action: 'updated',
|
|
426
|
+
message: `Set CLAUDE_CODE_GIT_BASH_PATH in ${file}`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function patchCodexShellFix(bashExe, home) {
|
|
430
|
+
const file = path.join(home, '.codex', 'config.toml');
|
|
431
|
+
const valueLine = `shell_path = ${JSON.stringify(bashExe)}`;
|
|
432
|
+
if (!fs.existsSync(file)) {
|
|
433
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
434
|
+
fs.writeFileSync(file, `[windows]\n${valueLine}\n`);
|
|
435
|
+
return {
|
|
436
|
+
agent: 'codex_cli',
|
|
437
|
+
file,
|
|
438
|
+
action: 'created',
|
|
439
|
+
message: `Set [windows] shell_path in ${file}`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const original = fs.readFileSync(file, 'utf-8');
|
|
443
|
+
const updated = setTomlWindowsShellPath(original, valueLine);
|
|
444
|
+
if (updated === original) {
|
|
445
|
+
return { agent: 'codex_cli', file, action: 'unchanged', message: `Already set in ${file}` };
|
|
446
|
+
}
|
|
447
|
+
fs.writeFileSync(file, updated);
|
|
448
|
+
return {
|
|
449
|
+
agent: 'codex_cli',
|
|
450
|
+
file,
|
|
451
|
+
action: 'updated',
|
|
452
|
+
message: `Set [windows] shell_path in ${file}`,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function patchOpenCodeShellFix(bashExe, home) {
|
|
456
|
+
const file = path.join(home, '.config', 'opencode', 'opencode.json');
|
|
457
|
+
if (!fs.existsSync(file)) {
|
|
458
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
459
|
+
fs.writeFileSync(file, JSON.stringify({ shell: bashExe, defaultShell: bashExe }, null, 2));
|
|
460
|
+
return {
|
|
461
|
+
agent: 'opencode',
|
|
462
|
+
file,
|
|
463
|
+
action: 'created',
|
|
464
|
+
message: `Set shell/defaultShell in ${file}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
let config;
|
|
468
|
+
try {
|
|
469
|
+
config = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return {
|
|
473
|
+
agent: 'opencode',
|
|
474
|
+
file,
|
|
475
|
+
action: 'skipped',
|
|
476
|
+
message: `${file} is not valid JSON; refusing to modify it`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (config.shell === bashExe && config.defaultShell === bashExe) {
|
|
480
|
+
return { agent: 'opencode', file, action: 'unchanged', message: `Already set in ${file}` };
|
|
481
|
+
}
|
|
482
|
+
config.shell = bashExe;
|
|
483
|
+
config.defaultShell = bashExe;
|
|
484
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
485
|
+
return {
|
|
486
|
+
agent: 'opencode',
|
|
487
|
+
file,
|
|
488
|
+
action: 'updated',
|
|
489
|
+
message: `Set shell/defaultShell in ${file}`,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Rewrite a Codex config.toml so [windows] contains shell_path = valueLine.
|
|
494
|
+
* Returns the original content when no change is needed.
|
|
495
|
+
*/
|
|
496
|
+
function setTomlWindowsShellPath(content, valueLine) {
|
|
497
|
+
const lines = content.split(/\r?\n/);
|
|
498
|
+
let windowsSectionIndex = -1;
|
|
499
|
+
let shellPathIndex = -1;
|
|
500
|
+
for (let i = 0; i < lines.length; i++) {
|
|
501
|
+
const trimmed = lines[i].trim();
|
|
502
|
+
if (trimmed === '[windows]')
|
|
503
|
+
windowsSectionIndex = i;
|
|
504
|
+
if (/^shell_path\s*=/.test(trimmed))
|
|
505
|
+
shellPathIndex = i;
|
|
506
|
+
}
|
|
507
|
+
if (windowsSectionIndex === -1) {
|
|
508
|
+
return `${content.replace(/\s+$/, '')}\n\n[windows]\n${valueLine}\n`;
|
|
509
|
+
}
|
|
510
|
+
if (shellPathIndex !== -1) {
|
|
511
|
+
if (lines[shellPathIndex].trim() === valueLine)
|
|
512
|
+
return content;
|
|
513
|
+
lines[shellPathIndex] = valueLine;
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
lines.splice(windowsSectionIndex + 1, 0, valueLine);
|
|
517
|
+
return lines.join('\n');
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Fix shell configuration for Windows and macOS
|
|
521
|
+
* This is a Tier 0 fix - ensures POSIX utilities are available
|
|
522
|
+
*/
|
|
523
|
+
export function fixShellConfiguration(_baseDir = '.', agents = [...AGENT_KEYS]) {
|
|
524
|
+
if (process.platform === 'win32') {
|
|
525
|
+
const shellInfo = detectShell();
|
|
526
|
+
if (shellInfo.isPosixCompatible) {
|
|
527
|
+
return {
|
|
528
|
+
success: true,
|
|
529
|
+
message: 'Shell is already POSIX-compatible',
|
|
530
|
+
gitBashPath: shellInfo.gitBashPath,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (!shellInfo.gitBashPath) {
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
message: 'Git Bash not found. Please install Git for Windows first.',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const bashExe = bashExecutable(shellInfo.gitBashPath);
|
|
540
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
541
|
+
const results = agents.map((agent) => applyAgentShellFix(agent, bashExe, home));
|
|
542
|
+
const written = results.filter((result) => result.action === 'created' || result.action === 'updated');
|
|
543
|
+
const message = written.length > 0
|
|
544
|
+
? `Configured agent shell overrides (${written.map((result) => result.agent).join(', ')}) pointing at ${bashExe}`
|
|
545
|
+
: `Git Bash detected at ${bashExe}; agent shell config already up to date`;
|
|
546
|
+
return {
|
|
547
|
+
success: true,
|
|
548
|
+
message,
|
|
549
|
+
gitBashPath: shellInfo.gitBashPath,
|
|
550
|
+
results,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// macOS and Linux: ensure GNU coreutils are available
|
|
554
|
+
if (process.platform === 'darwin') {
|
|
555
|
+
const shellInfo = detectShell();
|
|
556
|
+
if (shellInfo.isPosixCompatible) {
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
message: 'Shell is already POSIX-compatible',
|
|
560
|
+
gnubinPath: shellInfo.gnubinPath,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
// Try to install GNU coreutils via Homebrew
|
|
564
|
+
const gnubinPath = installGNUCoreutilsMacOS();
|
|
565
|
+
if (!gnubinPath) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
message: 'GNU coreutils not found and could not be installed. Please install Homebrew and run: brew install coreutils gnu-sed grep findutils gawk',
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
success: true,
|
|
573
|
+
message: `GNU coreutils installed via Homebrew. Prepend ${gnubinPath} to PATH for GNU tools.`,
|
|
574
|
+
gnubinPath,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// Linux and other platforms
|
|
578
|
+
const shellInfo = detectShell();
|
|
579
|
+
if (shellInfo.isPosixCompatible) {
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
message: 'Shell is already POSIX-compatible',
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
message: 'POSIX-compatible shell not detected. Most Linux distributions ship with GNU coreutils by default.',
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Check if a specific agent is configured to use a POSIX shell
|
|
592
|
+
*/
|
|
593
|
+
export function checkAgentShellConfiguration(agent) {
|
|
594
|
+
if (process.platform !== 'win32') {
|
|
595
|
+
return {
|
|
596
|
+
isConfigured: true,
|
|
597
|
+
needsFix: false,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// Agent-specific shell configuration check
|
|
601
|
+
switch (agent) {
|
|
602
|
+
case 'claude_code':
|
|
603
|
+
// Claude Code uses SHELL environment variable or config
|
|
604
|
+
return checkClaudeCodeShell();
|
|
605
|
+
case 'codex_cli':
|
|
606
|
+
return checkCodexShell();
|
|
607
|
+
case 'copilot':
|
|
608
|
+
return checkCopilotShell();
|
|
609
|
+
case 'opencode':
|
|
610
|
+
return checkOpenCodeShell();
|
|
611
|
+
default:
|
|
612
|
+
return {
|
|
613
|
+
isConfigured: false,
|
|
614
|
+
needsFix: true,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Check Claude Code shell configuration
|
|
620
|
+
*/
|
|
621
|
+
function checkClaudeCodeShell() {
|
|
622
|
+
try {
|
|
623
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
624
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
625
|
+
if (fs.existsSync(settingsPath)) {
|
|
626
|
+
const data = fs.readFileSync(settingsPath, 'utf-8');
|
|
627
|
+
const settings = JSON.parse(data);
|
|
628
|
+
if (settings.shell) {
|
|
629
|
+
return {
|
|
630
|
+
isConfigured: true,
|
|
631
|
+
shell: settings.shell,
|
|
632
|
+
needsFix: !settings.shell.toLowerCase().includes('bash'),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
isConfigured: false,
|
|
638
|
+
needsFix: true,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return {
|
|
643
|
+
isConfigured: false,
|
|
644
|
+
needsFix: true,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Check Codex CLI shell configuration
|
|
650
|
+
*/
|
|
651
|
+
function checkCodexShell() {
|
|
652
|
+
// Codex CLI respects the SHELL environment variable
|
|
653
|
+
const shell = process.env.SHELL;
|
|
654
|
+
if (shell && shell.toLowerCase().includes('bash')) {
|
|
655
|
+
return {
|
|
656
|
+
isConfigured: true,
|
|
657
|
+
shell,
|
|
658
|
+
needsFix: false,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
isConfigured: false,
|
|
663
|
+
needsFix: true,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Check Copilot shell configuration
|
|
668
|
+
*/
|
|
669
|
+
function checkCopilotShell() {
|
|
670
|
+
// GitHub Copilot CLI respects the SHELL environment variable
|
|
671
|
+
const shell = process.env.SHELL;
|
|
672
|
+
if (shell && shell.toLowerCase().includes('bash')) {
|
|
673
|
+
return {
|
|
674
|
+
isConfigured: true,
|
|
675
|
+
shell,
|
|
676
|
+
needsFix: false,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
isConfigured: false,
|
|
681
|
+
needsFix: true,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Check OpenCode shell configuration
|
|
686
|
+
*/
|
|
687
|
+
function checkOpenCodeShell() {
|
|
688
|
+
try {
|
|
689
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
690
|
+
const configPath = path.join(home, '.config', 'opencode', 'opencode.json');
|
|
691
|
+
if (fs.existsSync(configPath)) {
|
|
692
|
+
const data = fs.readFileSync(configPath, 'utf-8');
|
|
693
|
+
const config = JSON.parse(data);
|
|
694
|
+
if (config.shell) {
|
|
695
|
+
return {
|
|
696
|
+
isConfigured: true,
|
|
697
|
+
shell: config.shell,
|
|
698
|
+
needsFix: !config.shell.toLowerCase().includes('bash'),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
isConfigured: false,
|
|
704
|
+
needsFix: true,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return {
|
|
709
|
+
isConfigured: false,
|
|
710
|
+
needsFix: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=detector.js.map
|