stigmergy 1.3.2-beta.3 → 1.3.2-beta.4

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.
@@ -0,0 +1,252 @@
1
+ # 错误教训记录 (LESSONS LEARNED)
2
+
3
+ 本文档记录 Stigmergy 项目开发过程中遇到的重大错误和经验教训,**必须仔细阅读并避免重复犯错**。
4
+
5
+ ---
6
+
7
+ ## 🔴 致命错误 #1: 模板字符串中的正则表达式转义
8
+
9
+ **日期**: 2025-12-25
10
+ **影响**: 生产环境语法错误,所有 CLI 工具的 ResumeSession 集成失效
11
+ **修复版本**: v1.3.2-beta.3
12
+ **调试时间**: 约 2 小时
13
+ **失败尝试**: 3 次
14
+
15
+ ### 问题描述
16
+
17
+ 生成的 `resumesession-history.js` 文件包含语法错误:
18
+
19
+ ```javascript
20
+ SyntaxError: Invalid or unexpected token
21
+ at resumesession-history.js:515
22
+ const cleanInput = input.replace(/^\\/?stigmergy-resume\s*/i, '').trim();
23
+ ^^^^^^^
24
+ ```
25
+
26
+ ### 错误代码
27
+
28
+ **文件**: `src/core/coordination/nodejs/generators/ResumeSessionGenerator.js`
29
+ **位置**: Line 537, 627
30
+
31
+ ```javascript
32
+ // ❌ 错误写法
33
+ const cleanInput = input.replace(/^\\\\/?\${commandName}\\s*/i, '').trim();
34
+ const parts = cleanInput.split(/\\\\s+/).filter(p => p.length > 0);
35
+ ```
36
+
37
+ ### 根本原因分析
38
+
39
+ #### 错误 #1: 阻止了模板插值
40
+
41
+ ```javascript
42
+ \${commandName} // ❌ 这会生成字面量 "${commandName}",而不是插值后的值!
43
+ ${commandName} // ✅ 这才会插值生成 "/stigmergy-resume"
44
+ ```
45
+
46
+ **错误后果**:生成的代码中包含 `${commandName}` 字面量,导致语法错误。
47
+
48
+ #### 错误 #2: 正则字面量中的 `/` 冲突
49
+
50
+ ```javascript
51
+ // 当 commandName = "/stigmergy-resume" 时
52
+ // 生成的代码:
53
+ const cleanInput = input.replace(/^\\/?\/stigmergy-resume\s*/i, '').trim();
54
+ // ↑ 第一个 / 关闭了正则字面量
55
+ // stigmergy-resume\s*/i ← 这部分变成了无关代码
56
+ ```
57
+
58
+ **错误后果**:正则表达式被提前关闭,后续代码变成语法错误。
59
+
60
+ #### 错误 #3: 反斜杠转义计算错误
61
+
62
+ ```
63
+ 模板字符串转义层级:
64
+ 源文件 → 模板字符串 → 生成代码 → 正则表达式
65
+
66
+ 错误的计算:
67
+ 源文件写:/^\\\\/?\${commandName}\\s*/i
68
+ 期望生成:/^\\/?stigmergy-resume\s*/i
69
+ 实际生成:/^\\/?${commandName}\s*/i ← 未插值 + 正则破坏
70
+ ```
71
+
72
+ ### 正确的解决方案
73
+
74
+ ```javascript
75
+ // ✅ 正确写法
76
+ const cleanInput = input.replace(
77
+ new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'),
78
+ ''
79
+ ).trim();
80
+ const parts = cleanInput.split(/\\\s+/).filter(p => p.length > 0);
81
+ ```
82
+
83
+ ### 转义计算公式
84
+
85
+ ```
86
+ 源文件中的反斜杠数 = 目标反斜杠数 × 2^(嵌套层数)
87
+ ```
88
+
89
+ **实际应用**:
90
+
91
+ | 目标 | 在模板字符串中写 | 生成后 | 解释 |
92
+ |------|------------------|--------|------|
93
+ | `\s` | `\\s` | `\s` | 模板转义 1 次 |
94
+ | `\\s` | `\\\\s` | `\\s` | 模板转义 1 次 |
95
+ | `\\\s` | `\\\\\\s` | `\\s` | 字符串内容为 `\\s` |
96
+
97
+ ### 为什么使用 RegExp 构造函数?
98
+
99
+ ```javascript
100
+ // ❌ 危险:正则字面量
101
+ /^\\\\/?\/stigmergy-resume\s*/i
102
+ // ↑ 与命令名中的 / 冲突
103
+
104
+ // ✅ 安全:RegExp 构造函数
105
+ new RegExp('^\\\\\\\\/?' + '/stigmergy-resume' + '\\\\\s*', 'i')
106
+ // ↑ 通过字符串拼接避免 / 冲突
107
+ ```
108
+
109
+ ### 经验教训
110
+
111
+ #### 1. 永远记住的规则
112
+
113
+ > **在模板字符串中生成包含正则的代码时,使用 RegExp 构造函数 + 字符串拼接,永远比尝试计算正确的正则字面量转义更安全!**
114
+
115
+ ```javascript
116
+ // ✅ 安全、清晰、可维护
117
+ new RegExp('pattern' + variable + 'pattern', 'flags')
118
+
119
+ // ❌ 危险、复杂、易出错
120
+ /pattern${variable}pattern/flags
121
+ ```
122
+
123
+ #### 2. 必须遵守的规则
124
+
125
+ - [ ] **永远不要阻止插值**:使用 `${variable}`,而不是 `\${variable}`
126
+ - [ ] **动态模式 → RegExp 构造函数**:避免正则字面量中的 `/` 冲突
127
+ - [ ] **反斜杠数量必须计算**:使用公式,不能猜测
128
+ - [ ] **必须添加语法验证**:生成代码后立即测试语法
129
+ - [ ] **必须注释说明转义**:让其他人理解你的计算
130
+
131
+ #### 3. 代码审查检查清单
132
+
133
+ 涉及 **模板字符串 + 正则表达式** 的代码必须检查:
134
+
135
+ ```javascript
136
+ // ✅ 检查项
137
+ - [ ] 所有 ${variable} 是否真的需要插值(不是 \${variable})
138
+ - [ ] 如果模式包含动态的 /,是否使用了 RegExp 构造函数
139
+ - [ ] 反斜杠数量是否经过计算(而非猜测)
140
+ - [ ] 是否有语法验证测试
141
+ - [ ] 是否注释说明了转义的计算过程
142
+ ```
143
+
144
+ ### 验证工具
145
+
146
+ ```javascript
147
+ /**
148
+ * 验证生成的代码是否有语法错误
149
+ */
150
+ function validateGeneratedCode(code) {
151
+ try {
152
+ new Function(code);
153
+ return true;
154
+ } catch (e) {
155
+ console.error('❌ 语法错误:', e.message);
156
+ console.error('代码:', code);
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * 计算需要的反斜杠数量
163
+ */
164
+ function calculateBackslashes(targetCount, nestingDepth = 1) {
165
+ return '\\'.repeat(targetCount * Math.pow(2, nestingDepth));
166
+ }
167
+
168
+ // 使用示例
169
+ const generatedCode = generator.generateForCLI('claude');
170
+ if (!validateGeneratedCode(generatedCode)) {
171
+ throw new Error('生成的代码有语法错误!');
172
+ }
173
+ ```
174
+
175
+ ### 相关文档
176
+
177
+ - [开发规范 - 代码生成规范](./development_guidelines.md#10-代码生成规范)
178
+ - [ResumeSessionGenerator 源代码](../src/core/coordination/nodejs/generators/ResumeSessionGenerator.js)
179
+ - [修复提交](https://github.com/your-repo/commit/6a2ee2c6)
180
+
181
+ ---
182
+
183
+ ## 📋 错误教训快速参考
184
+
185
+ ### 正则表达式生成
186
+
187
+ | 场景 | ✅ 正确 | ❌ 错误 |
188
+ |------|--------|--------|
189
+ | 动态模式 | `new RegExp(pattern + var)` | `/pattern${var}/` |
190
+ | 模板插值 | `${variable}` | `\${variable}` |
191
+ | 反斜杠转义 | 通过公式计算 | 猜测数量 |
192
+ | 语法验证 | 必须添加 | 跳过测试 |
193
+
194
+ ### 转义速查表
195
+
196
+ | 生成目标 | 模板字符串中写 | 说明 |
197
+ |----------|----------------|------|
198
+ | `\s` | `\\s` | 匹配空白字符 |
199
+ | `\\s` | `\\\\s` | 字符串 `"\s"` |
200
+ | `\\\s` | `\\\\\\s` | 字符串 `"\\s"` |
201
+ | `/stigmergy-resume` | `${commandName}` | 插值命令名 |
202
+ | `RegExp('/stigmergy-resume')` | `new RegExp('${commandName}')` | 动态正则 |
203
+
204
+ ---
205
+
206
+ ## 🔄 如何添加新的错误教训
207
+
208
+ 当遇到重大错误时,请按照以下格式记录:
209
+
210
+ ```markdown
211
+ ## 🔴 致命错误 #N: [错误标题]
212
+
213
+ **日期**: YYYY-MM-DD
214
+ **影响**: [影响范围]
215
+ **修复版本**: vX.X.X
216
+ **调试时间**: 约 X 小时
217
+ **失败尝试**: X 次
218
+
219
+ ### 问题描述
220
+ [描述问题的现象和错误信息]
221
+
222
+ ### 错误代码
223
+ **文件**: [文件路径]
224
+ **位置**: Line XXX
225
+
226
+ ```javascript
227
+ // ❌ 错误写法
228
+ [错误代码]
229
+ ```
230
+
231
+ ### 根本原因分析
232
+ [分析为什么会出现这个错误]
233
+
234
+ ### 正确的解决方案
235
+ ```javascript
236
+ // ✅ 正确写法
237
+ [正确代码]
238
+ ```
239
+
240
+ ### 经验教训
241
+ [总结规则和检查清单]
242
+
243
+ ### 验证工具
244
+ [提供验证和测试方法]
245
+ ```
246
+
247
+ ---
248
+
249
+ **最后更新**: 2025-12-25
250
+ **维护者**: Stigmergy 开发团队
251
+
252
+ **记住这些教训,避免重复犯错!**
@@ -389,4 +389,280 @@ class CacheManager {
389
389
  }
390
390
  ```
391
391
 
392
+ ## 10. 代码生成规范
393
+
394
+ ### 10.1 模板字符串中的正则表达式 ⚠️ 极其重要!
395
+
396
+ #### 🔴 致命陷阱:正则表达式字面量 vs RegExp 构造函数
397
+
398
+ **问题场景**:在模板字符串中生成包含正则表达式的代码
399
+
400
+ ```javascript
401
+ // ❌ 错误示例
402
+ const code = `
403
+ const cleanInput = input.replace(/^\\\\/?${commandName}\\s*/i, '').trim();
404
+ `;
405
+
406
+ // 当 commandName = "/stigmergy-resume" 时
407
+ // 生成:/^\\/?\/stigmergy-resume\s*/i
408
+ // ↑ 正则被 "/stigmergy-resume" 中的 / 提前关闭!
409
+ ```
410
+
411
+ **错误原因分析**:
412
+
413
+ 1. **正则字面量语法冲突**:`/pattern/` 首尾的 `/` 是定界符
414
+ 2. **命令名中的 `/` 破坏语法**:`/^\\/?\/stigmergy-resume` 中第一个 `/` 关闭了正则
415
+ 3. **模板字符串插值错误**:使用 `\${variable}` 会阻止插值!
416
+
417
+ #### ✅ 正确做法:使用 RegExp 构造函数
418
+
419
+ ```javascript
420
+ // ✅ 正确示例
421
+ const code = `
422
+ const cleanInput = input.replace(
423
+ new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'),
424
+ ''
425
+ ).trim();
426
+ `;
427
+ ```
428
+
429
+ **转义计算公式**:
430
+
431
+ ```
432
+ 源文件中的反斜杠数 = 目标反斜杠数 × 2^(嵌套层数)
433
+ ```
434
+
435
+ | 目标 | 在模板字符串中写 | 生成后 | 说明 |
436
+ |------|------------------|--------|------|
437
+ | `\s` | `\\s` | `\s` | 模板转义一次 |
438
+ | `\\s` | `\\\\s` | `\\s` | 模板转义一次 |
439
+ | `\\\s` | `\\\\\\s` | `\\s` | 字符串中为 `\\s` |
440
+
441
+ #### 📋 必须遵守的规则
442
+
443
+ 1. **动态模式 → 用 RegExp 构造函数**
444
+ ```javascript
445
+ new RegExp(pattern + variable + pattern, flags)
446
+ ```
447
+
448
+ 2. **静态模式 → 可以用正则字面量**
449
+ ```javascript
450
+ const staticRegex = /\s+/; // 简单清晰
451
+ ```
452
+
453
+ 3. **永远不要阻止插值**
454
+ ```javascript
455
+ // ❌ 错误:阻止插值
456
+ \${commandName} // 生成字面量 "${commandName}"
457
+
458
+ // ✅ 正确:允许插值
459
+ ${commandName} // 生成实际值 "/stigmergy-resume"
460
+ ```
461
+
462
+ 4. **不要在字符串中转义 `/`**
463
+ ```javascript
464
+ // ❌ 不必要
465
+ '\/path' // 在字符串中不需要转义 /
466
+
467
+ // ✅ 正确
468
+ '/path' // 字符串中的 / 不需要转义
469
+ ```
470
+
471
+ #### 🛠️ 调试工具
472
+
473
+ ```javascript
474
+ /**
475
+ * 验证生成的代码是否有语法错误
476
+ */
477
+ function validateGeneratedCode(code) {
478
+ try {
479
+ new Function(code);
480
+ return true;
481
+ } catch (e) {
482
+ console.error('语法错误:', e.message);
483
+ return false;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * 计算需要的反斜杠数量
489
+ */
490
+ function calculateBackslashes(targetCount, nestingDepth = 1) {
491
+ return '\\'.repeat(targetCount * Math.pow(2, nestingDepth));
492
+ }
493
+ ```
494
+
495
+ #### 📝 代码审查检查清单
496
+
497
+ 涉及 **模板字符串 + 正则表达式** 的代码必须检查:
498
+
499
+ - [ ] 所有 `${variable}` 是否真的需要插值(不是 `\${variable}`)
500
+ - [ ] 如果模式包含动态的 `/`,是否使用了 RegExp 构造函数
501
+ - [ ] 反斜杠数量是否经过计算(而非猜测)
502
+ - [ ] 是否有语法验证测试
503
+ - [ ] 是否注释说明了转义的计算过程
504
+
505
+ #### 💡 永远记住
506
+
507
+ > **在模板字符串中生成包含正则的代码时,使用 RegExp 构造函数 + 字符串拼接,永远比尝试计算正确的正则字面量转义更安全!**
508
+
509
+ ```javascript
510
+ // ✅ 安全、清晰、可维护
511
+ new RegExp('pattern' + variable + 'pattern', 'flags')
512
+
513
+ // ❌ 危险、复杂、易出错
514
+ /pattern${variable}pattern/flags
515
+ ```
516
+
517
+ ### 10.2 代码生成最佳实践
518
+
519
+ #### 10.2.1 生成器模式
520
+
521
+ ```javascript
522
+ class CodeGenerator {
523
+ /**
524
+ * 生成CLI集成代码
525
+ * @param {string} cliName - CLI工具名称
526
+ * @returns {string} 生成的代码
527
+ */
528
+ generateForCLI(cliName) {
529
+ const commandName = this.getCommandName(cliName);
530
+
531
+ return `
532
+ // Auto-generated by Stigmergy - DO NOT EDIT
533
+ const handler = {
534
+ commandName: '${commandName}',
535
+
536
+ async handle(input) {
537
+ const cleanInput = input.replace(
538
+ new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'),
539
+ ''
540
+ ).trim();
541
+
542
+ // 处理逻辑...
543
+ return { response: 'OK' };
544
+ }
545
+ };
546
+
547
+ module.exports = handler;
548
+ `;
549
+ }
550
+
551
+ /**
552
+ * 获取命令名称
553
+ */
554
+ getCommandName(cliName) {
555
+ const needsSlash = ['claude', 'codebuddy'].includes(cliName.toLowerCase());
556
+ return needsSlash ? `/stigmergy-resume` : 'stigmergy-resume';
557
+ }
558
+ }
559
+ ```
560
+
561
+ #### 10.2.2 测试生成代码
562
+
563
+ ```javascript
564
+ const { CodeGenerator } = require('./generators');
565
+
566
+ describe('CodeGenerator', () => {
567
+ test('should generate valid JavaScript syntax', () => {
568
+ const generator = new CodeGenerator();
569
+ const code = generator.generateForCLI('claude');
570
+
571
+ // 验证语法
572
+ expect(() => new Function(code)).not.toThrow();
573
+
574
+ // 验证包含关键部分
575
+ expect(code).toContain('new RegExp');
576
+ expect(code).toContain('/stigmergy-resume');
577
+ });
578
+
579
+ test('should escape regex correctly', () => {
580
+ const generator = new CodeGenerator();
581
+ const code = generator.generateForCLI('claude');
582
+
583
+ // 提取并验证正则表达式
584
+ const regexMatch = code.match(/new RegExp\('([^']+)',\s*'i'\)/);
585
+ expect(regexMatch).toBeTruthy();
586
+
587
+ // 测试生成的正则是否工作
588
+ const pattern = regexMatch[1];
589
+ const regex = new RegExp(pattern, 'i');
590
+ expect(regex.test('/stigmergy-resume')).toBe(true);
591
+ expect(regex.test('\\/stigmergy-resume')).toBe(true);
592
+ });
593
+ });
594
+ ```
595
+
596
+ ### 10.3 常见错误案例
597
+
598
+ #### 案例1:阻止插值
599
+
600
+ ```javascript
601
+ // ❌ 错误
602
+ const commandName = '/stigmergy-resume';
603
+ const code = `input.replace(/^\\\\/?\${commandName}\\s*/i, '')`;
604
+ // 生成:input.replace(/^\\\\/?${commandName}\s*/i, '')
605
+ // ↑ 未插值!
606
+ ```
607
+
608
+ ```javascript
609
+ // ✅ 正确
610
+ const commandName = '/stigmergy-resume';
611
+ const code = `input.replace(new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'), '')`;
612
+ // 生成:input.replace(new RegExp('^\\\\/?' + '/stigmergy-resume' + '\s*', 'i'), '')
613
+ ```
614
+
615
+ #### 案例2:正则字面量中的 `/` 冲突
616
+
617
+ ```javascript
618
+ // ❌ 错误
619
+ const commandName = '/stigmergy-resume';
620
+ const code = `input.replace(/\\/?${commandName}\\s*/i, '')`;
621
+ // 生成:input.replace(/\/?\/stigmergy-resume\s*/i, '')
622
+ // ↑ 第一个 / 关闭了正则
623
+ ```
624
+
625
+ ```javascript
626
+ // ✅ 正确
627
+ const commandName = '/stigmergy-resume';
628
+ const code = `input.replace(new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'), '')`;
629
+ ```
630
+
631
+ ### 10.4 历史教训记录
632
+
633
+ #### 2025-12-25: ResumeSessionGenerator 正则转义错误
634
+
635
+ **问题描述**:
636
+ 生成的 `resumesession-history.js` 文件包含语法错误,导致无法加载。
637
+
638
+ **错误代码**(Line 537, 627):
639
+ ```javascript
640
+ const cleanInput = input.replace(/^\\\\/?\${commandName}\\s*/i, '').trim();
641
+ ```
642
+
643
+ **错误原因**:
644
+ 1. `\${commandName}` 阻止了模板插值
645
+ 2. 正则字面量中的 `/` 与命令名中的 `/` 冲突
646
+ 3. 反斜杠转义计算错误
647
+
648
+ **修复方案**:
649
+ ```javascript
650
+ const cleanInput = input.replace(
651
+ new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'),
652
+ ''
653
+ ).trim();
654
+ ```
655
+
656
+ **影响范围**:
657
+ - 影响:所有CLI工具的ResumeSession集成
658
+ - 修复版本:v1.3.2-beta.3
659
+ - 调试时间:约2小时
660
+ - 失败尝试:3次
661
+
662
+ **经验总结**:
663
+ - 永远使用 RegExp 构造函数处理动态正则
664
+ - 永远不要在模板字符串中用 `\${` 阻止插值(除非真的需要字面量)
665
+ - 反斜杠数量必须通过公式计算,不能猜测
666
+ - 必须添加语法验证测试
667
+
392
668
  遵循这些开发规范可以确保代码质量、可维护性和团队协作效率。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stigmergy",
3
- "version": "1.3.2-beta.3",
3
+ "version": "1.3.2-beta.4",
4
4
  "description": "Stigmergy CLI - Multi-Agents Cross-AI CLI Tools Collaboration System",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -373,10 +373,19 @@ class CLIPathDetector {
373
373
  if (await fs.access(this.pathCacheFile).then(() => true).catch(() => false)) {
374
374
  const data = await fs.readFile(this.pathCacheFile, 'utf8');
375
375
  const cacheData = JSON.parse(data);
376
- this.detectedPaths = cacheData.detectedPaths || {};
377
376
 
378
- console.log(`[DETECTOR] Loaded ${Object.keys(this.detectedPaths).length} paths from cache`);
379
- return this.detectedPaths;
377
+ // Check if cache is too old (older than 1 hour) and skip loading if so
378
+ const cacheAge = Date.now() - new Date(cacheData.timestamp).getTime();
379
+ const maxCacheAge = 60 * 60 * 1000; // 1 hour in milliseconds
380
+
381
+ if (cacheAge < maxCacheAge) {
382
+ this.detectedPaths = cacheData.detectedPaths || {};
383
+ console.log(`[DETECTOR] Loaded ${Object.keys(this.detectedPaths).length} paths from cache (age: ${Math.floor(cacheAge/1000)}s)`);
384
+ return this.detectedPaths;
385
+ } else {
386
+ console.log(`[DETECTOR] Cache is too old (${Math.floor(cacheAge/1000)}s), skipping cache`);
387
+ return {};
388
+ }
380
389
  }
381
390
  } catch (error) {
382
391
  console.log(`[DETECTOR] Warning: Could not load path cache: ${error.message}`);
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const os = require('os');
3
3
  const { errorHandler, ERROR_TYPES } = require('./error_handler');
4
4
  const CLIPathDetector = require('./cli_path_detector');
5
+ const StigmergyInstaller = require('./installer');
5
6
 
6
7
  // AI CLI Tools Configuration
7
8
  const CLI_TOOLS = {
@@ -140,6 +141,189 @@ async function getCLIPath(toolName) {
140
141
  return toolPath;
141
142
  }
142
143
 
144
+ /**
145
+ * Check if a CLI tool is actually executable
146
+ * @param {string} toolName - Name of the tool to check
147
+ * @returns {Promise<boolean>} Whether the tool is executable
148
+ */
149
+ async function checkIfCLIExecutable(toolName) {
150
+ const { spawnSync } = require('child_process');
151
+ const os = require('os');
152
+
153
+ // Special handling for codex - check if the JS file is valid
154
+ if (toolName === 'codex') {
155
+ try {
156
+ // First check if codex command exists
157
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
158
+ const whichResult = spawnSync(whichCmd, [toolName], {
159
+ encoding: 'utf8',
160
+ timeout: 10000,
161
+ stdio: ['pipe', 'pipe', 'pipe'],
162
+ shell: true,
163
+ });
164
+
165
+ if (whichResult.status === 0 && whichResult.stdout.trim()) {
166
+ const codexPath = whichResult.stdout.trim().split('\n')[0]; // Get first match
167
+
168
+ // If it's a shell script, check the target JS file
169
+ if (
170
+ codexPath.endsWith('.sh') ||
171
+ codexPath.endsWith('.cmd') ||
172
+ codexPath.endsWith('/codex') ||
173
+ codexPath.endsWith('\\codex')
174
+ ) {
175
+ // Try to verify JS file, but don't fail if we can't
176
+ // The actual --version test below is more reliable
177
+ try {
178
+ const fsSync = require('fs');
179
+ const scriptContent = fsSync.readFileSync(codexPath, 'utf8');
180
+
181
+ // Look for JS file reference in the script
182
+ // Match node_modules/@openai/codex/bin/codex.js pattern
183
+ const jsFileMatch = scriptContent.match(/node_modules\/@openai\/codex\/bin\/codex\.js/);
184
+ if (jsFileMatch) {
185
+ // Construct actual path based on npm global directory
186
+ const npmGlobalDir = require('path').dirname(codexPath);
187
+ const jsFilePath = require('path').join(npmGlobalDir, jsFileMatch[0]);
188
+
189
+ if (fsSync.existsSync(jsFilePath)) {
190
+ const stats = fsSync.statSync(jsFilePath);
191
+ if (stats.size === 0) {
192
+ console.log('[DEBUG] Codex JS file is empty, marking as unavailable');
193
+ return false;
194
+ }
195
+ // File exists and has content - continue to version check
196
+ } else {
197
+ console.log('[DEBUG] Codex JS file not found at expected path, will try version check');
198
+ }
199
+ }
200
+ } catch (scriptError) {
201
+ console.log(`[DEBUG] Could not verify codex script: ${scriptError.message}`);
202
+ // Continue anyway - the version check below is more reliable
203
+ }
204
+ }
205
+
206
+ // If we got here, the codex command exists - continue with normal checks below
207
+ } else {
208
+ // Codex command doesn't exist
209
+ return false;
210
+ }
211
+ } catch (error) {
212
+ console.log(`[DEBUG] Error checking codex: ${error.message}`);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ // First try to find the executable using which/where command (more reliable)
218
+ try {
219
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
220
+ const whichResult = spawnSync(whichCmd, [toolName], {
221
+ encoding: 'utf8',
222
+ timeout: 10000,
223
+ stdio: ['pipe', 'pipe', 'pipe'], // Use pipes to avoid file opening
224
+ shell: true,
225
+ });
226
+
227
+ if (whichResult.status === 0 && whichResult.stdout.trim()) {
228
+ // Found executable, now test it safely
229
+ const testArgs = ['--help'];
230
+ const testOptions = {
231
+ encoding: 'utf8',
232
+ timeout: 5000,
233
+ stdio: ['pipe', 'pipe', 'pipe'], // Don't inherit from parent to avoid opening UI
234
+ shell: true,
235
+ };
236
+
237
+ // Additional protection for codex
238
+ // Note: codex requires shell=true on Windows to work properly
239
+ if (toolName === 'codex') {
240
+ // Keep shell=true for codex (don't override)
241
+ testOptions.windowsHide = true;
242
+ testOptions.detached = false;
243
+ }
244
+
245
+ const testResult = spawnSync(toolName, testArgs, testOptions);
246
+
247
+ // If command runs successfully or at least returns something (not command not found)
248
+ if (testResult.status === 0 || testResult.status === 1) {
249
+ return true;
250
+ }
251
+ }
252
+ } catch (error) {
253
+ // which/where command probably failed, continue with other checks
254
+ console.log(`[DEBUG] which/where check failed for ${toolName}: ${error.message}`);
255
+ }
256
+
257
+ // Special handling for codex to avoid opening files
258
+ if (toolName === 'codex') {
259
+ // For codex, only try --help and --version with extra precautions
260
+ // Note: codex requires shell=true on Windows
261
+ const codexChecks = [
262
+ { args: ['--help'], expected: 0 },
263
+ { args: ['--version'], expected: 0 },
264
+ ];
265
+
266
+ for (const check of codexChecks) {
267
+ try {
268
+ const result = spawnSync(toolName, check.args, {
269
+ encoding: 'utf8',
270
+ timeout: 10000,
271
+ stdio: ['pipe', 'pipe', 'pipe'], // Ensure all IO is piped
272
+ shell: true, // Use shell for codex compatibility
273
+ windowsHide: true, // Hide console window on Windows
274
+ detached: false, // Don't detach process
275
+ });
276
+
277
+ if (result.status === 0 || result.status === 1) {
278
+ return true;
279
+ }
280
+ } catch (error) {
281
+ // Continue to next check
282
+ }
283
+ }
284
+ return false; // If all codex checks fail
285
+ }
286
+
287
+ // Fallback: Try multiple ways to check if CLI is available but more safely
288
+ const checks = [
289
+ // Method 1: Try help command (most common and safe)
290
+ { args: ['--help'], expected: 0 },
291
+ // Method 2: Try help command with -h
292
+ { args: ['-h'], expected: 0 },
293
+ // Method 3: Try version command
294
+ { args: ['--version'], expected: 0 },
295
+ // Method 4: Try just the command (help case)
296
+ { args: [], expected: 0 },
297
+ ];
298
+
299
+ for (const check of checks) {
300
+ try {
301
+ const result = spawnSync(toolName, check.args, {
302
+ encoding: 'utf8',
303
+ timeout: 5000,
304
+ stdio: ['pipe', 'pipe', 'pipe'], // Use pipe instead of inherit to avoid opening files
305
+ shell: true,
306
+ });
307
+
308
+ // Check if command executed successfully or at least didn't fail with "command not found"
309
+ if (
310
+ result.status === check.expected ||
311
+ (result.status !== 127 &&
312
+ result.status !== 9009 &&
313
+ result.status !== 1) // Also avoid status 1 (general error)
314
+ ) {
315
+ // 127 = command not found on Unix, 9009 = command not found on Windows
316
+ return true;
317
+ }
318
+ } catch (error) {
319
+ // Continue to next check method
320
+ continue;
321
+ }
322
+ }
323
+
324
+ return false;
325
+ }
326
+
143
327
  /**
144
328
  * Update PATH configuration and run path detection
145
329
  */
@@ -157,13 +341,211 @@ async function setupCLIPaths() {
157
341
  // 3. Check and update PATH if needed
158
342
  const pathStatus = await detector.updatePATHIfMissing();
159
343
 
344
+ // 4. Create an enhanced report using StigmergyInstaller detection logic
345
+ const installer = new StigmergyInstaller();
346
+ const scanResult = await installer.scanCLI();
347
+
348
+ const enhancedReport = {
349
+ platform: detector.platform,
350
+ npmGlobalPaths: detector.npmGlobalPaths,
351
+ detectedPaths: detector.detectedPaths,
352
+ summary: {
353
+ total: Object.keys(detector.cliNameMap).length,
354
+ found: Object.keys(scanResult.available).length,
355
+ missing: Object.keys(scanResult.missing).length
356
+ }
357
+ };
358
+
160
359
  return {
161
360
  detectedPaths,
162
361
  pathStatus,
163
- report: detector.getPathStatusReport()
362
+ report: enhancedReport
164
363
  };
165
364
  }
166
365
 
366
+ /**
367
+ * Check if a CLI tool is actually executable
368
+ * @param {string} toolName - Name of the tool to check
369
+ * @returns {Promise<boolean>} Whether the tool is executable
370
+ */
371
+ async function checkIfCLIExecutable(toolName) {
372
+ const { spawnSync } = require('child_process');
373
+ const os = require('os');
374
+
375
+ // Special handling for codex - check if the JS file is valid
376
+ if (toolName === 'codex') {
377
+ try {
378
+ // First check if codex command exists
379
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
380
+ const whichResult = spawnSync(whichCmd, [toolName], {
381
+ encoding: 'utf8',
382
+ timeout: 10000,
383
+ stdio: ['pipe', 'pipe', 'pipe'],
384
+ shell: true,
385
+ });
386
+
387
+ if (whichResult.status === 0 && whichResult.stdout.trim()) {
388
+ const codexPath = whichResult.stdout.trim().split('\n')[0]; // Get first match
389
+
390
+ // If it's a shell script, check the target JS file
391
+ if (
392
+ codexPath.endsWith('.sh') ||
393
+ codexPath.endsWith('.cmd') ||
394
+ codexPath.endsWith('/codex') ||
395
+ codexPath.endsWith('\\codex')
396
+ ) {
397
+ // Try to verify JS file, but don't fail if we can't
398
+ // The actual --version test below is more reliable
399
+ try {
400
+ const fsSync = require('fs');
401
+ const scriptContent = fsSync.readFileSync(codexPath, 'utf8');
402
+
403
+ // Look for JS file reference in the script
404
+ // Match node_modules/@openai/codex/bin/codex.js pattern
405
+ const jsFileMatch = scriptContent.match(/node_modules\/@openai\/codex\/bin\/codex\.js/);
406
+ if (jsFileMatch) {
407
+ // Construct actual path based on npm global directory
408
+ const npmGlobalDir = require('path').dirname(codexPath);
409
+ const jsFilePath = require('path').join(npmGlobalDir, jsFileMatch[0]);
410
+
411
+ if (fsSync.existsSync(jsFilePath)) {
412
+ const stats = fsSync.statSync(jsFilePath);
413
+ if (stats.size === 0) {
414
+ console.log('[DEBUG] Codex JS file is empty, marking as unavailable');
415
+ return false;
416
+ }
417
+ // File exists and has content - continue to version check
418
+ } else {
419
+ console.log('[DEBUG] Codex JS file not found at expected path, will try version check');
420
+ }
421
+ }
422
+ } catch (scriptError) {
423
+ console.log(`[DEBUG] Could not verify codex script: ${scriptError.message}`);
424
+ // Continue anyway - the version check below is more reliable
425
+ }
426
+ }
427
+
428
+ // If we got here, the codex command exists - continue with normal checks below
429
+ } else {
430
+ // Codex command doesn't exist
431
+ return false;
432
+ }
433
+ } catch (error) {
434
+ console.log(`[DEBUG] Error checking codex: ${error.message}`);
435
+ return false;
436
+ }
437
+ }
438
+
439
+ // First try to find the executable using which/where command (more reliable)
440
+ try {
441
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
442
+ const whichResult = spawnSync(whichCmd, [toolName], {
443
+ encoding: 'utf8',
444
+ timeout: 10000,
445
+ stdio: ['pipe', 'pipe', 'pipe'], // Use pipes to avoid file opening
446
+ shell: true,
447
+ });
448
+
449
+ if (whichResult.status === 0 && whichResult.stdout.trim()) {
450
+ // Found executable, now test it safely
451
+ const testArgs = ['--help'];
452
+ const testOptions = {
453
+ encoding: 'utf8',
454
+ timeout: 5000,
455
+ stdio: ['pipe', 'pipe', 'pipe'], // Don't inherit from parent to avoid opening UI
456
+ shell: true,
457
+ };
458
+
459
+ // Additional protection for codex
460
+ // Note: codex requires shell=true on Windows to work properly
461
+ if (toolName === 'codex') {
462
+ // Keep shell=true for codex (don't override)
463
+ testOptions.windowsHide = true;
464
+ testOptions.detached = false;
465
+ }
466
+
467
+ const testResult = spawnSync(toolName, testArgs, testOptions);
468
+
469
+ // If command runs successfully or at least returns something (not command not found)
470
+ if (testResult.status === 0 || testResult.status === 1) {
471
+ return true;
472
+ }
473
+ }
474
+ } catch (error) {
475
+ // which/where command probably failed, continue with other checks
476
+ console.log(`[DEBUG] which/where check failed for ${toolName}: ${error.message}`);
477
+ }
478
+
479
+ // Special handling for codex to avoid opening files
480
+ if (toolName === 'codex') {
481
+ // For codex, only try --help and --version with extra precautions
482
+ // Note: codex requires shell=true on Windows
483
+ const codexChecks = [
484
+ { args: ['--help'], expected: 0 },
485
+ { args: ['--version'], expected: 0 },
486
+ ];
487
+
488
+ for (const check of codexChecks) {
489
+ try {
490
+ const result = spawnSync(toolName, check.args, {
491
+ encoding: 'utf8',
492
+ timeout: 10000,
493
+ stdio: ['pipe', 'pipe', 'pipe'], // Ensure all IO is piped
494
+ shell: true, // Use shell for codex compatibility
495
+ windowsHide: true, // Hide console window on Windows
496
+ detached: false, // Don't detach process
497
+ });
498
+
499
+ if (result.status === 0 || result.status === 1) {
500
+ return true;
501
+ }
502
+ } catch (error) {
503
+ // Continue to next check
504
+ }
505
+ }
506
+ return false; // If all codex checks fail
507
+ }
508
+
509
+ // Fallback: Try multiple ways to check if CLI is available but more safely
510
+ const checks = [
511
+ // Method 1: Try help command (most common and safe)
512
+ { args: ['--help'], expected: 0 },
513
+ // Method 2: Try help command with -h
514
+ { args: ['-h'], expected: 0 },
515
+ // Method 3: Try version command
516
+ { args: ['--version'], expected: 0 },
517
+ // Method 4: Try just the command (help case)
518
+ { args: [], expected: 0 },
519
+ ];
520
+
521
+ for (const check of checks) {
522
+ try {
523
+ const result = spawnSync(toolName, check.args, {
524
+ encoding: 'utf8',
525
+ timeout: 5000,
526
+ stdio: ['pipe', 'pipe', 'pipe'], // Use pipe instead of inherit to avoid opening files
527
+ shell: true,
528
+ });
529
+
530
+ // Check if command executed successfully or at least didn't fail with "command not found"
531
+ if (
532
+ result.status === check.expected ||
533
+ (result.status !== 127 &&
534
+ result.status !== 9009 &&
535
+ result.status !== 1) // Also avoid status 1 (general error)
536
+ ) {
537
+ // 127 = command not found on Unix, 9009 = command not found on Windows
538
+ return true;
539
+ }
540
+ } catch (error) {
541
+ // Continue to next check method
542
+ continue;
543
+ }
544
+ }
545
+
546
+ return false;
547
+ }
548
+
167
549
  /**
168
550
  * Scan for available CLI tools
169
551
  * @param {Object} options - Scan options
@@ -172,29 +554,26 @@ async function setupCLIPaths() {
172
554
  * @returns {Promise<Object>} Scan results
173
555
  */
174
556
  async function scanForTools(options = {}) {
175
- const detector = getPathDetector();
176
-
177
- // Load cached paths first
178
- await detector.loadDetectedPaths();
179
-
180
- // Detect all CLI paths
181
- const detectedPaths = await detector.detectAllCLIPaths();
557
+ // Use StigmergyInstaller detection logic directly
558
+ const installer = new StigmergyInstaller();
559
+ const scanResult = await installer.scanCLI();
182
560
 
183
561
  const found = [];
184
562
  const missing = [];
185
563
 
186
- for (const [toolName, toolPath] of Object.entries(detectedPaths)) {
187
- if (toolPath) {
188
- found.push({
189
- name: toolName,
190
- path: toolPath,
191
- type: 'cli',
192
- status: 'installed',
193
- description: CLI_TOOLS[toolName]?.name || toolName
194
- });
195
- } else {
196
- missing.push(toolName);
197
- }
564
+ // Convert scanResult to the expected format
565
+ for (const [toolName, toolInfo] of Object.entries(scanResult.available)) {
566
+ found.push({
567
+ name: toolName,
568
+ path: null, // Path is not provided by scanCLI, but we can potentially add it
569
+ type: 'cli',
570
+ status: 'installed',
571
+ description: CLI_TOOLS[toolName]?.name || toolName
572
+ });
573
+ }
574
+
575
+ for (const [toolName, toolInfo] of Object.entries(scanResult.missing)) {
576
+ missing.push(toolName);
198
577
  }
199
578
 
200
579
  return {
@@ -212,20 +591,25 @@ async function scanForTools(options = {}) {
212
591
  async function checkInstallation(toolName) {
213
592
  validateCLITool(toolName);
214
593
 
215
- const detector = getPathDetector();
216
- await detector.loadDetectedPaths();
594
+ // Use StigmergyInstaller detection logic directly
595
+ const installer = new StigmergyInstaller();
596
+ const isAvailable = await installer.checkCLI(toolName);
217
597
 
218
- let toolPath = detector.getDetectedPath(toolName);
598
+ // If the tool is available, try to get its path and version
599
+ let toolPath = null;
600
+ let version = null;
219
601
 
220
- // If not cached, detect it
221
- if (!toolPath) {
222
- toolPath = await detector.detectCLIPath(toolName);
223
- }
602
+ if (isAvailable) {
603
+ // Get path using the detector
604
+ const detector = getPathDetector();
605
+ await detector.loadDetectedPaths();
606
+ toolPath = detector.getDetectedPath(toolName);
224
607
 
225
- const installed = !!toolPath;
608
+ if (!toolPath) {
609
+ toolPath = await detector.detectCLIPath(toolName);
610
+ }
226
611
 
227
- let version = null;
228
- if (installed) {
612
+ // Get version
229
613
  try {
230
614
  const { spawnSync } = require('child_process');
231
615
  const toolConfig = CLI_TOOLS[toolName];
@@ -246,7 +630,7 @@ async function checkInstallation(toolName) {
246
630
  }
247
631
 
248
632
  return {
249
- installed,
633
+ installed: isAvailable,
250
634
  path: toolPath,
251
635
  version,
252
636
  lastChecked: new Date().toISOString()
@@ -106,6 +106,15 @@ class EnhancedCLIInstaller {
106
106
  return { success: true, mode: 'elevated' };
107
107
  }
108
108
 
109
+ // Check if we're in a container environment (common indicators)
110
+ const inContainer = await this.checkContainerEnvironment();
111
+ if (inContainer) {
112
+ this.log('info', 'Detected container environment, using user-space installation');
113
+ this.permissionMode = 'user-space';
114
+ this.permissionConfigured = true;
115
+ return { success: true, mode: 'user-space' };
116
+ }
117
+
109
118
  // Attempt standard installation first
110
119
  const testResult = await this.attemptTestInstallation();
111
120
 
@@ -128,9 +137,10 @@ class EnhancedCLIInstaller {
128
137
  return { success: true, mode: 'elevated' };
129
138
  } else {
130
139
  this.log('error', 'Failed to set up elevated permissions');
131
- this.permissionMode = 'failed';
140
+ this.log('info', 'Falling back to user-space installation');
141
+ this.permissionMode = 'user-space';
132
142
  this.permissionConfigured = true;
133
- return { success: false, mode: 'failed', error: elevatedSetup.error };
143
+ return { success: true, mode: 'user-space' };
134
144
  }
135
145
  }
136
146
 
@@ -142,9 +152,10 @@ class EnhancedCLIInstaller {
142
152
 
143
153
  } catch (error) {
144
154
  this.log('error', `Permission setup failed: ${error.message}`);
145
- this.permissionMode = 'failed';
155
+ this.log('info', 'Falling back to user-space installation');
156
+ this.permissionMode = 'user-space';
146
157
  this.permissionConfigured = true;
147
- return { success: false, mode: 'failed', error: error.message };
158
+ return { success: true, mode: 'user-space', error: error.message };
148
159
  }
149
160
  }
150
161
 
@@ -169,6 +180,59 @@ class EnhancedCLIInstaller {
169
180
  }
170
181
  }
171
182
 
183
+ /**
184
+ * Check if we're running in a container environment
185
+ */
186
+ async checkContainerEnvironment() {
187
+ try {
188
+ // Check for container indicators
189
+ const fs = require('fs');
190
+
191
+ // Check for .dockerenv file
192
+ if (fs.existsSync('/.dockerenv')) {
193
+ return true;
194
+ }
195
+
196
+ // Check for container environment variables
197
+ if (process.env.container || process.env.DOCKER_CONTAINER) {
198
+ return true;
199
+ }
200
+
201
+ // Check cgroup for container indicators
202
+ try {
203
+ if (fs.existsSync('/proc/1/cgroup')) {
204
+ const cgroupContent = fs.readFileSync('/proc/1/cgroup', 'utf8');
205
+ if (cgroupContent.includes('docker') || cgroupContent.includes('containerd')) {
206
+ return true;
207
+ }
208
+ }
209
+ } catch (e) {
210
+ // Ignore errors reading cgroup
211
+ }
212
+
213
+ // Check for container-specific files
214
+ const containerIndicators = [
215
+ '/run/.containerenv', // Podman/Docker
216
+ '/sys/fs/cgroup/cpu/cpu.cfs_quota_us', // Common in containers
217
+ ];
218
+
219
+ for (const indicator of containerIndicators) {
220
+ try {
221
+ if (fs.existsSync(indicator)) {
222
+ return true;
223
+ }
224
+ } catch (e) {
225
+ // Ignore errors
226
+ }
227
+ }
228
+
229
+ return false;
230
+ } catch (error) {
231
+ // If we can't determine, assume not in container
232
+ return false;
233
+ }
234
+ }
235
+
172
236
  /**
173
237
  * Attempt a test installation to check permissions
174
238
  */
@@ -304,6 +368,15 @@ class EnhancedCLIInstaller {
304
368
  await this.setupPermissions();
305
369
  }
306
370
 
371
+ // Check if we're in a container environment and force user-space mode if needed
372
+ if (this.permissionMode !== 'user-space') {
373
+ const inContainer = await this.checkContainerEnvironment();
374
+ if (inContainer) {
375
+ this.log('info', 'Detected container environment, switching to user-space installation');
376
+ this.permissionMode = 'user-space';
377
+ }
378
+ }
379
+
307
380
  this.log('info', `Installing ${toolInfo.name} (${toolName})...`);
308
381
 
309
382
  try {
@@ -358,6 +431,8 @@ class EnhancedCLIInstaller {
358
431
  return await this.executeStandardInstallation(toolInfo);
359
432
  case 'elevated':
360
433
  return await this.executeElevatedInstallation(toolInfo);
434
+ case 'user-space':
435
+ return await this.executeUserSpaceInstallation(toolInfo);
361
436
  case 'failed':
362
437
  return await this.executeFallbackInstallation(toolInfo);
363
438
  default:
@@ -412,10 +487,25 @@ class EnhancedCLIInstaller {
412
487
  return { success: true, error: null };
413
488
  } else {
414
489
  const errorMessage = result.stderr || result.stdout || `Exit code ${result.status}`;
490
+
491
+ // Check if this is a permission error and switch to user-space if needed
492
+ if (this.isPermissionError(errorMessage)) {
493
+ this.log('warn', `Standard installation failed due to permission error, switching to user-space installation...`);
494
+ this.permissionMode = 'user-space';
495
+ return await this.executeUserSpaceInstallation(toolInfo);
496
+ }
497
+
415
498
  return { success: false, error: errorMessage };
416
499
  }
417
500
 
418
501
  } catch (error) {
502
+ // Check if this is a permission error and switch to user-space if needed
503
+ if (this.isPermissionError(error.message)) {
504
+ this.log('warn', `Standard installation failed due to permission error, switching to user-space installation...`);
505
+ this.permissionMode = 'user-space';
506
+ return await this.executeUserSpaceInstallation(toolInfo);
507
+ }
508
+
419
509
  return { success: false, error: error.message };
420
510
  }
421
511
  }
@@ -477,12 +567,22 @@ class EnhancedCLIInstaller {
477
567
  this.log('warn', `Could not clean up temp script: ${cleanupError.message}`);
478
568
  }
479
569
 
480
- return {
481
- success: result.status === 0,
482
- error: result.status !== 0 ? 'Windows elevated installation failed' : null
483
- };
570
+ if (result.status === 0) {
571
+ return {
572
+ success: true,
573
+ error: null
574
+ };
575
+ } else {
576
+ // If elevated installation failed, try user-space installation
577
+ this.log('warn', `Elevated installation failed, trying user-space installation...`);
578
+ this.permissionMode = 'user-space';
579
+ return await this.executeUserSpaceInstallation(toolInfo);
580
+ }
484
581
  } catch (error) {
485
- return { success: false, error: error.message };
582
+ // If elevated installation failed, try user-space installation
583
+ this.log('warn', `Elevated installation failed (${error.message}), trying user-space installation...`);
584
+ this.permissionMode = 'user-space';
585
+ return await this.executeUserSpaceInstallation(toolInfo);
486
586
  }
487
587
  }
488
588
 
@@ -490,15 +590,21 @@ class EnhancedCLIInstaller {
490
590
  * Execute Unix elevated installation
491
591
  */
492
592
  async executeUnixElevatedInstallation(toolInfo) {
493
- const permissionSetup = await this.setupPermissions();
593
+ // Use the detected privilege escalation tool
594
+ const privilegeSetup = await this.setupUnixElevatedContext();
595
+
596
+ if (!privilegeSetup.success) {
597
+ this.log('warn', 'No privilege escalation tool available, using user-space installation...');
598
+ return await this.executeUserSpaceInstallation(toolInfo);
599
+ }
494
600
 
495
601
  // If no privilege escalation tool is available, use user-space installation
496
- if (permissionSetup.userSpaceOnly) {
602
+ if (privilegeSetup.userSpaceOnly) {
497
603
  return await this.executeUserSpaceInstallation(toolInfo);
498
604
  }
499
605
 
500
606
  // Use the detected privilege escalation tool
501
- const privilegeTool = permissionSetup.privilegeTool || 'sudo';
607
+ const privilegeTool = privilegeSetup.privilegeTool || 'sudo';
502
608
  const command = `${privilegeTool} ${toolInfo.install}`;
503
609
 
504
610
  try {
@@ -660,9 +766,22 @@ class EnhancedCLIInstaller {
660
766
  if (result.status === 0) {
661
767
  return { success: true, error: null };
662
768
  } else {
663
- return { success: false, error: `Fallback failed: ${result.stderr}` };
769
+ const errorMessage = result.stderr || `Fallback failed with exit code ${result.status}`;
770
+ // If fallback failed due to permissions, try user-space installation
771
+ if (this.isPermissionError(errorMessage)) {
772
+ this.log('warn', `Fallback installation failed due to permission error, switching to user-space installation...`);
773
+ this.permissionMode = 'user-space';
774
+ return await this.executeUserSpaceInstallation(toolInfo);
775
+ }
776
+ return { success: false, error: errorMessage };
664
777
  }
665
778
  } catch (error) {
779
+ // If fallback failed due to permissions, try user-space installation
780
+ if (this.isPermissionError(error.message)) {
781
+ this.log('warn', `Fallback installation failed due to permission error, switching to user-space installation...`);
782
+ this.permissionMode = 'user-space';
783
+ return await this.executeUserSpaceInstallation(toolInfo);
784
+ }
666
785
  return { success: false, error: error.message };
667
786
  }
668
787
  }
@@ -769,9 +888,21 @@ class EnhancedCLIInstaller {
769
888
  continue;
770
889
  }
771
890
 
891
+ // Determine the appropriate upgrade command based on permission mode
892
+ let upgradeCommand;
893
+ if (this.permissionMode === 'user-space') {
894
+ // For user-space installations, upgrade to user directory
895
+ const os = require('os');
896
+ const path = require('path');
897
+ let userNpmDir = process.env.NPM_CONFIG_PREFIX || path.join(os.homedir(), '.npm-global');
898
+ upgradeCommand = `npm install -g --prefix "${userNpmDir}" ${toolName}`;
899
+ } else {
900
+ upgradeCommand = `npm upgrade -g ${toolName}`;
901
+ }
902
+
772
903
  const toolInfo = {
773
904
  ...originalInfo,
774
- install: `npm upgrade -g ${toolName}`,
905
+ install: upgradeCommand,
775
906
  name: `${originalInfo.name} (Upgrade)`
776
907
  };
777
908
 
@@ -896,7 +1027,17 @@ class EnhancedCLIInstaller {
896
1027
  return false;
897
1028
  }
898
1029
 
899
- const upgradeCommand = `npm upgrade -g ${toolName}`;
1030
+ // Determine the appropriate upgrade command based on permission mode
1031
+ let upgradeCommand;
1032
+ if (this.permissionMode === 'user-space') {
1033
+ // For user-space installations, upgrade to user directory
1034
+ const os = require('os');
1035
+ const path = require('path');
1036
+ let userNpmDir = process.env.NPM_CONFIG_PREFIX || path.join(os.homedir(), '.npm-global');
1037
+ upgradeCommand = `npm install -g --prefix "${userNpmDir}" ${toolName}`;
1038
+ } else {
1039
+ upgradeCommand = `npm upgrade -g ${toolName}`;
1040
+ }
900
1041
 
901
1042
  this.results.installations[toolName] = {
902
1043
  startTime: Date.now(),