@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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -0
  3. package/dist/adapters/base.d.ts +58 -0
  4. package/dist/adapters/base.d.ts.map +1 -0
  5. package/dist/adapters/base.js +16 -0
  6. package/dist/adapters/base.js.map +1 -0
  7. package/dist/adapters/claude.d.ts +21 -0
  8. package/dist/adapters/claude.d.ts.map +1 -0
  9. package/dist/adapters/claude.js +198 -0
  10. package/dist/adapters/claude.js.map +1 -0
  11. package/dist/adapters/codex.d.ts +23 -0
  12. package/dist/adapters/codex.d.ts.map +1 -0
  13. package/dist/adapters/codex.js +134 -0
  14. package/dist/adapters/codex.js.map +1 -0
  15. package/dist/adapters/copilot.d.ts +21 -0
  16. package/dist/adapters/copilot.d.ts.map +1 -0
  17. package/dist/adapters/copilot.js +111 -0
  18. package/dist/adapters/copilot.js.map +1 -0
  19. package/dist/adapters/detect.d.ts +23 -0
  20. package/dist/adapters/detect.d.ts.map +1 -0
  21. package/dist/adapters/detect.js +80 -0
  22. package/dist/adapters/detect.js.map +1 -0
  23. package/dist/adapters/index.d.ts +12 -0
  24. package/dist/adapters/index.d.ts.map +1 -0
  25. package/dist/adapters/index.js +11 -0
  26. package/dist/adapters/index.js.map +1 -0
  27. package/dist/adapters/opencode.d.ts +19 -0
  28. package/dist/adapters/opencode.d.ts.map +1 -0
  29. package/dist/adapters/opencode.js +107 -0
  30. package/dist/adapters/opencode.js.map +1 -0
  31. package/dist/commands/apply.d.ts +21 -0
  32. package/dist/commands/apply.d.ts.map +1 -0
  33. package/dist/commands/apply.js +113 -0
  34. package/dist/commands/apply.js.map +1 -0
  35. package/dist/commands/configure.d.ts +13 -0
  36. package/dist/commands/configure.d.ts.map +1 -0
  37. package/dist/commands/configure.js +151 -0
  38. package/dist/commands/configure.js.map +1 -0
  39. package/dist/commands/setup.d.ts +11 -0
  40. package/dist/commands/setup.d.ts.map +1 -0
  41. package/dist/commands/setup.js +78 -0
  42. package/dist/commands/setup.js.map +1 -0
  43. package/dist/commands/status.d.ts +3 -0
  44. package/dist/commands/status.d.ts.map +1 -0
  45. package/dist/commands/status.js +152 -0
  46. package/dist/commands/status.js.map +1 -0
  47. package/dist/config/schema.d.ts +142 -0
  48. package/dist/config/schema.d.ts.map +1 -0
  49. package/dist/config/schema.js +778 -0
  50. package/dist/config/schema.js.map +1 -0
  51. package/dist/config/scopes.d.ts +14 -0
  52. package/dist/config/scopes.d.ts.map +1 -0
  53. package/dist/config/scopes.js +32 -0
  54. package/dist/config/scopes.js.map +1 -0
  55. package/dist/generate/agentsmd.d.ts +44 -0
  56. package/dist/generate/agentsmd.d.ts.map +1 -0
  57. package/dist/generate/agentsmd.js +340 -0
  58. package/dist/generate/agentsmd.js.map +1 -0
  59. package/dist/index.d.ts +7 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +24 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/integrations/base.d.ts +33 -0
  64. package/dist/integrations/base.d.ts.map +1 -0
  65. package/dist/integrations/base.js +9 -0
  66. package/dist/integrations/base.js.map +1 -0
  67. package/dist/integrations/index.d.ts +7 -0
  68. package/dist/integrations/index.d.ts.map +1 -0
  69. package/dist/integrations/index.js +5 -0
  70. package/dist/integrations/index.js.map +1 -0
  71. package/dist/integrations/superpowers.d.ts +48 -0
  72. package/dist/integrations/superpowers.d.ts.map +1 -0
  73. package/dist/integrations/superpowers.js +191 -0
  74. package/dist/integrations/superpowers.js.map +1 -0
  75. package/dist/shell/detector.d.ts +92 -0
  76. package/dist/shell/detector.d.ts.map +1 -0
  77. package/dist/shell/detector.js +714 -0
  78. package/dist/shell/detector.js.map +1 -0
  79. package/dist/toolchain/fallbacks.d.ts +77 -0
  80. package/dist/toolchain/fallbacks.d.ts.map +1 -0
  81. package/dist/toolchain/fallbacks.js +238 -0
  82. package/dist/toolchain/fallbacks.js.map +1 -0
  83. package/dist/toolchain/gh.d.ts +20 -0
  84. package/dist/toolchain/gh.d.ts.map +1 -0
  85. package/dist/toolchain/gh.js +36 -0
  86. package/dist/toolchain/gh.js.map +1 -0
  87. package/dist/toolchain/mise.d.ts +92 -0
  88. package/dist/toolchain/mise.d.ts.map +1 -0
  89. package/dist/toolchain/mise.js +346 -0
  90. package/dist/toolchain/mise.js.map +1 -0
  91. package/dist/toolchain/rtk.d.ts +27 -0
  92. package/dist/toolchain/rtk.d.ts.map +1 -0
  93. package/dist/toolchain/rtk.js +76 -0
  94. package/dist/toolchain/rtk.js.map +1 -0
  95. package/dist/wizard/build.d.ts +24 -0
  96. package/dist/wizard/build.d.ts.map +1 -0
  97. package/dist/wizard/build.js +64 -0
  98. package/dist/wizard/build.js.map +1 -0
  99. 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