stigmergy 1.3.2-beta.2 → 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.
- package/docs/LESSONS_LEARNED.md +252 -0
- package/docs/development_guidelines.md +276 -0
- package/package.json +1 -1
- package/src/cli/commands/project.js +1 -1
- package/src/cli/router-beta.js +1 -1
- package/src/core/cli_path_detector.js +12 -3
- package/src/core/cli_tools.js +415 -31
- package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +4 -4
- package/src/core/enhanced_cli_installer.js +156 -15
|
@@ -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
package/src/cli/router-beta.js
CHANGED
|
@@ -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
|
-
|
|
379
|
-
|
|
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}`);
|
package/src/core/cli_tools.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
216
|
-
|
|
594
|
+
// Use StigmergyInstaller detection logic directly
|
|
595
|
+
const installer = new StigmergyInstaller();
|
|
596
|
+
const isAvailable = await installer.checkCLI(toolName);
|
|
217
597
|
|
|
218
|
-
|
|
598
|
+
// If the tool is available, try to get its path and version
|
|
599
|
+
let toolPath = null;
|
|
600
|
+
let version = null;
|
|
219
601
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
608
|
+
if (!toolPath) {
|
|
609
|
+
toolPath = await detector.detectCLIPath(toolName);
|
|
610
|
+
}
|
|
226
611
|
|
|
227
|
-
|
|
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()
|
|
@@ -534,8 +534,8 @@ function buildQuery(input) {
|
|
|
534
534
|
search: null
|
|
535
535
|
};
|
|
536
536
|
|
|
537
|
-
const cleanInput = input.replace(
|
|
538
|
-
const parts = cleanInput.split(
|
|
537
|
+
const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'), '').trim();
|
|
538
|
+
const parts = cleanInput.split(/\\\s+/).filter(p => p.length > 0);
|
|
539
539
|
|
|
540
540
|
for (let i = 0; i < parts.length; i++) {
|
|
541
541
|
const part = parts[i].toLowerCase();
|
|
@@ -624,8 +624,8 @@ class GeminiHistoryHandler {
|
|
|
624
624
|
search: null
|
|
625
625
|
};
|
|
626
626
|
|
|
627
|
-
const cleanInput = input.replace(
|
|
628
|
-
const parts = cleanInput.split(
|
|
627
|
+
const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + this.commandName + '\\\\\s*', 'i'), '').trim();
|
|
628
|
+
const parts = cleanInput.split(/\\\s+/).filter(p => p.length > 0);
|
|
629
629
|
|
|
630
630
|
for (let i = 0; i < parts.length; i++) {
|
|
631
631
|
const part = parts[i].toLowerCase();
|
|
@@ -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.
|
|
140
|
+
this.log('info', 'Falling back to user-space installation');
|
|
141
|
+
this.permissionMode = 'user-space';
|
|
132
142
|
this.permissionConfigured = true;
|
|
133
|
-
return { success:
|
|
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.
|
|
155
|
+
this.log('info', 'Falling back to user-space installation');
|
|
156
|
+
this.permissionMode = 'user-space';
|
|
146
157
|
this.permissionConfigured = true;
|
|
147
|
-
return { success:
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(),
|