claude-coder 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/bin/cli.js +140 -0
- package/docs/ARCHITECTURE.md +319 -0
- package/docs/README.en.md +94 -0
- package/package.json +42 -0
- package/src/config.js +211 -0
- package/src/indicator.js +111 -0
- package/src/init.js +144 -0
- package/src/prompts.js +189 -0
- package/src/runner.js +348 -0
- package/src/scanner.js +31 -0
- package/src/session.js +265 -0
- package/src/setup.js +385 -0
- package/src/tasks.js +146 -0
- package/src/validator.js +131 -0
- package/templates/CLAUDE.md +257 -0
- package/templates/SCAN_PROTOCOL.md +123 -0
- package/templates/requirements.example.md +56 -0
package/src/setup.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { ensureLoopDir, paths, log, COLOR, getProjectRoot } = require('./config');
|
|
7
|
+
|
|
8
|
+
function createInterface() {
|
|
9
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ask(rl, question) {
|
|
13
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function askChoice(rl, prompt, min, max, defaultVal) {
|
|
17
|
+
while (true) {
|
|
18
|
+
const raw = await ask(rl, prompt);
|
|
19
|
+
const val = raw.trim() || String(defaultVal || '');
|
|
20
|
+
const num = parseInt(val, 10);
|
|
21
|
+
if (num >= min && num <= max) return num;
|
|
22
|
+
console.log(`请输入 ${min}-${max}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function askApiKey(rl, platform, apiUrl, existingKey) {
|
|
27
|
+
if (existingKey) {
|
|
28
|
+
console.log('保留当前 API Key 请直接回车,或输入新 Key:');
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`请输入 ${platform} 的 API Key:`);
|
|
31
|
+
}
|
|
32
|
+
if (apiUrl) {
|
|
33
|
+
console.log(` ${COLOR.blue}获取入口: ${apiUrl}${COLOR.reset}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
const key = await ask(rl, ' API Key: ');
|
|
37
|
+
if (!key.trim()) {
|
|
38
|
+
if (existingKey) return existingKey;
|
|
39
|
+
console.error('API Key 不能为空');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return key.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeConfig(filePath, lines) {
|
|
46
|
+
const dir = path.dirname(filePath);
|
|
47
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
// Backup existing
|
|
50
|
+
if (fs.existsSync(filePath)) {
|
|
51
|
+
const ts = new Date().toISOString().replace(/[:\-T]/g, '').slice(0, 14);
|
|
52
|
+
const backup = `${filePath}.bak.${ts}`;
|
|
53
|
+
fs.copyFileSync(filePath, backup);
|
|
54
|
+
log('info', `已备份旧配置到: ${backup}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureGitignore() {
|
|
61
|
+
const gitignore = path.join(getProjectRoot(), '.gitignore');
|
|
62
|
+
const patterns = ['.claude-coder/.env', '.claude-coder/.runtime/'];
|
|
63
|
+
let content = '';
|
|
64
|
+
if (fs.existsSync(gitignore)) {
|
|
65
|
+
content = fs.readFileSync(gitignore, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
const toAdd = patterns.filter(p => !content.includes(p));
|
|
68
|
+
if (toAdd.length > 0) {
|
|
69
|
+
const block = '\n# Claude Coder(含 API Key 和临时文件)\n' + toAdd.join('\n') + '\n';
|
|
70
|
+
fs.appendFileSync(gitignore, block, 'utf8');
|
|
71
|
+
log('info', '已将 .claude-coder/.env 添加到 .gitignore');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function setup() {
|
|
76
|
+
const p = paths();
|
|
77
|
+
ensureLoopDir();
|
|
78
|
+
const rl = createInterface();
|
|
79
|
+
|
|
80
|
+
// Load existing config for defaults
|
|
81
|
+
let existing = {};
|
|
82
|
+
if (fs.existsSync(p.envFile)) {
|
|
83
|
+
const { parseEnvFile } = require('./config');
|
|
84
|
+
existing = parseEnvFile(p.envFile);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log('============================================');
|
|
89
|
+
console.log(' Claude Coder 前置配置');
|
|
90
|
+
console.log('============================================');
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(' 第一步: 模型提供商配置');
|
|
93
|
+
console.log(' 第二步: MCP 工具 + 调试输出(可选)');
|
|
94
|
+
console.log('');
|
|
95
|
+
|
|
96
|
+
// Detect existing config
|
|
97
|
+
if (fs.existsSync(p.envFile)) {
|
|
98
|
+
log('warn', `检测到已有配置文件: ${p.envFile}`);
|
|
99
|
+
console.log(` 当前模型提供商: ${existing.MODEL_PROVIDER || '未知'}`);
|
|
100
|
+
console.log(` 当前 BASE_URL: ${existing.ANTHROPIC_BASE_URL || '默认'}`);
|
|
101
|
+
console.log(` Playwright MCP: ${existing.MCP_PLAYWRIGHT || '未配置'}`);
|
|
102
|
+
console.log('');
|
|
103
|
+
const reply = await ask(rl, '是否重新配置?(y/n) ');
|
|
104
|
+
if (!/^[Yy]/.test(reply.trim())) {
|
|
105
|
+
log('info', '保留现有配置,退出');
|
|
106
|
+
rl.close();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Provider selection
|
|
113
|
+
console.log('请选择模型提供商:');
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(' 1) Claude 官方');
|
|
116
|
+
console.log(` 2) GLM Coding Plan (智谱/Z.AI) ${COLOR.blue}https://open.bigmodel.cn${COLOR.reset}`);
|
|
117
|
+
console.log(` 3) 阿里云 Coding Plan (百炼) ${COLOR.blue}https://bailian.console.aliyun.com${COLOR.reset}`);
|
|
118
|
+
console.log(` 4) DeepSeek ${COLOR.blue}https://platform.deepseek.com${COLOR.reset}`);
|
|
119
|
+
console.log(' 5) 自定义 (Anthropic 兼容)');
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
const choice = await askChoice(rl, '选择 [1-5]: ', 1, 5);
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
let configLines = [];
|
|
126
|
+
|
|
127
|
+
switch (choice) {
|
|
128
|
+
case 1: {
|
|
129
|
+
configLines = [
|
|
130
|
+
'# Claude Coder 模型配置',
|
|
131
|
+
'# 提供商: Claude 官方',
|
|
132
|
+
'',
|
|
133
|
+
'MODEL_PROVIDER=claude',
|
|
134
|
+
'API_TIMEOUT_MS=3000000',
|
|
135
|
+
'MCP_TOOL_TIMEOUT=30000',
|
|
136
|
+
];
|
|
137
|
+
log('ok', '已配置为 Claude 官方模型');
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 2: {
|
|
141
|
+
// GLM platform
|
|
142
|
+
console.log('请选择 GLM 平台:');
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(' 1) 智谱开放平台 (open.bigmodel.cn) - 国内直连');
|
|
145
|
+
console.log(' 2) Z.AI (api.z.ai) - 海外节点');
|
|
146
|
+
console.log('');
|
|
147
|
+
const platChoice = await askChoice(rl, '选择 [1-2,默认 1]: ', 1, 2, 1);
|
|
148
|
+
const isBigmodel = platChoice === 1;
|
|
149
|
+
const glmProvider = isBigmodel ? 'glm-bigmodel' : 'glm-zai';
|
|
150
|
+
const glmBaseUrl = isBigmodel
|
|
151
|
+
? 'https://open.bigmodel.cn/api/anthropic'
|
|
152
|
+
: 'https://api.z.ai/api/anthropic';
|
|
153
|
+
const glmApiUrl = isBigmodel
|
|
154
|
+
? 'https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys'
|
|
155
|
+
: 'https://z.ai/manage-apikey/apikey-list';
|
|
156
|
+
|
|
157
|
+
// GLM model
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log('请选择 GLM 模型版本:');
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(' 1) GLM 4.7 - 旗舰模型,推理与代码能力强');
|
|
162
|
+
console.log(' 2) GLM 5 - 最新模型(2026),能力更强');
|
|
163
|
+
console.log('');
|
|
164
|
+
const modelChoice = await askChoice(rl, '选择 [1-2,默认 1]: ', 1, 2, 1);
|
|
165
|
+
const glmModel = modelChoice === 1 ? 'glm-4.7' : 'glm-5';
|
|
166
|
+
|
|
167
|
+
const existingKey = existing.MODEL_PROVIDER === glmProvider ? existing.ANTHROPIC_API_KEY : '';
|
|
168
|
+
const apiKey = await askApiKey(rl, glmProvider, glmApiUrl, existingKey);
|
|
169
|
+
|
|
170
|
+
configLines = [
|
|
171
|
+
'# Claude Coder 模型配置',
|
|
172
|
+
`# 提供商: GLM (${glmProvider})`,
|
|
173
|
+
`# 模型: ${glmModel}`,
|
|
174
|
+
'',
|
|
175
|
+
`MODEL_PROVIDER=${glmProvider}`,
|
|
176
|
+
`ANTHROPIC_MODEL=${glmModel}`,
|
|
177
|
+
`ANTHROPIC_BASE_URL=${glmBaseUrl}`,
|
|
178
|
+
`ANTHROPIC_API_KEY=${apiKey}`,
|
|
179
|
+
'API_TIMEOUT_MS=3000000',
|
|
180
|
+
'MCP_TOOL_TIMEOUT=30000',
|
|
181
|
+
];
|
|
182
|
+
log('ok', `已配置为 GLM 模型 (${glmProvider}, ${glmModel})`);
|
|
183
|
+
log('info', `BASE_URL: ${glmBaseUrl}`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 3: {
|
|
187
|
+
// Aliyun
|
|
188
|
+
console.log('请选择阿里云百炼区域:');
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(' 1) 国内版 (coding.dashscope.aliyuncs.com)');
|
|
191
|
+
console.log(' 2) 国际版 (coding-intl.dashscope.aliyuncs.com)');
|
|
192
|
+
console.log('');
|
|
193
|
+
const regionChoice = await askChoice(rl, '选择 [1-2,默认 1]: ', 1, 2, 1);
|
|
194
|
+
const aliyunBaseUrl = regionChoice === 1
|
|
195
|
+
? 'https://coding.dashscope.aliyuncs.com/apps/anthropic'
|
|
196
|
+
: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic';
|
|
197
|
+
|
|
198
|
+
const existingKey = existing.MODEL_PROVIDER === 'aliyun-coding' ? existing.ANTHROPIC_API_KEY : '';
|
|
199
|
+
const apiKey = await askApiKey(rl, '阿里云百炼', 'https://bailian.console.aliyun.com/?tab=model#/api-key', existingKey);
|
|
200
|
+
|
|
201
|
+
configLines = [
|
|
202
|
+
'# Claude Coder 模型配置',
|
|
203
|
+
'# 提供商: 阿里云 Coding Plan (百炼)',
|
|
204
|
+
'# Opus: glm-5 | Sonnet/Haiku: qwen3-coder-plus | Fallback: qwen3.5-plus',
|
|
205
|
+
'',
|
|
206
|
+
'MODEL_PROVIDER=aliyun-coding',
|
|
207
|
+
`ANTHROPIC_BASE_URL=${aliyunBaseUrl}`,
|
|
208
|
+
`ANTHROPIC_API_KEY=${apiKey}`,
|
|
209
|
+
'',
|
|
210
|
+
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1',
|
|
211
|
+
'# Planner (规划/推理) → glm-5',
|
|
212
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5',
|
|
213
|
+
'# Executor (写代码/编辑/工具调用) → qwen3-coder-plus',
|
|
214
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder-plus',
|
|
215
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder-plus',
|
|
216
|
+
'ANTHROPIC_SMALL_FAST_MODEL=qwen3-coder-plus',
|
|
217
|
+
'# Fallback (通用) → qwen3.5-plus',
|
|
218
|
+
'ANTHROPIC_MODEL=qwen3.5-plus',
|
|
219
|
+
'API_TIMEOUT_MS=3000000',
|
|
220
|
+
'MCP_TOOL_TIMEOUT=30000',
|
|
221
|
+
];
|
|
222
|
+
log('ok', '已配置为阿里云 Coding Plan (百炼)');
|
|
223
|
+
log('info', `BASE_URL: ${aliyunBaseUrl}`);
|
|
224
|
+
log('info', '模型映射: Opus=glm-5 / Sonnet+Haiku=qwen3-coder-plus / Fallback=qwen3.5-plus');
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 4: {
|
|
228
|
+
// DeepSeek
|
|
229
|
+
const existingKey = existing.MODEL_PROVIDER === 'deepseek' ? existing.ANTHROPIC_API_KEY : '';
|
|
230
|
+
const apiKey = await askApiKey(rl, 'DeepSeek', 'https://platform.deepseek.com/api_keys', existingKey);
|
|
231
|
+
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log('请选择 DeepSeek 模型:');
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(' 1) deepseek-chat - 通用对话 (V3),速度快成本低 [推荐日常使用]');
|
|
236
|
+
console.log(' 2) deepseek-reasoner - 纯推理模式 (R1),全链路使用 R1,成本最高 [适合攻坚]');
|
|
237
|
+
console.log(' 3) deepseek-hybrid - 混合模式 (R1 + V3),规划用 R1,执行用 V3 [性价比之选]');
|
|
238
|
+
console.log('');
|
|
239
|
+
const dsChoice = await askChoice(rl, '选择 [1-3,默认 1]: ', 1, 3, 1);
|
|
240
|
+
const dsModel = ['deepseek-chat', 'deepseek-reasoner', 'deepseek-hybrid'][dsChoice - 1];
|
|
241
|
+
|
|
242
|
+
configLines = [
|
|
243
|
+
'# Claude Coder 模型配置',
|
|
244
|
+
`# 提供商: DeepSeek`,
|
|
245
|
+
`# 模型: ${dsModel} | API_TIMEOUT_MS=600000 防止长输出超时(10分钟)`,
|
|
246
|
+
'',
|
|
247
|
+
'MODEL_PROVIDER=deepseek',
|
|
248
|
+
'ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic',
|
|
249
|
+
`ANTHROPIC_API_KEY=${apiKey}`,
|
|
250
|
+
`ANTHROPIC_AUTH_TOKEN=${apiKey}`,
|
|
251
|
+
'',
|
|
252
|
+
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1',
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
if (dsModel === 'deepseek-chat') {
|
|
256
|
+
configLines.push(
|
|
257
|
+
'# [DeepSeek Chat 降本策略]',
|
|
258
|
+
'ANTHROPIC_MODEL=deepseek-chat',
|
|
259
|
+
'ANTHROPIC_SMALL_FAST_MODEL=deepseek-chat',
|
|
260
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-chat',
|
|
261
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-chat',
|
|
262
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-chat',
|
|
263
|
+
);
|
|
264
|
+
} else if (dsModel === 'deepseek-reasoner') {
|
|
265
|
+
configLines.push(
|
|
266
|
+
'# [DeepSeek Pure Reasoner 模式]',
|
|
267
|
+
'ANTHROPIC_MODEL=deepseek-reasoner',
|
|
268
|
+
'ANTHROPIC_SMALL_FAST_MODEL=deepseek-reasoner',
|
|
269
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-reasoner',
|
|
270
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-reasoner',
|
|
271
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-reasoner',
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
configLines.push(
|
|
275
|
+
'# [DeepSeek Hybrid 混合模式]',
|
|
276
|
+
'ANTHROPIC_MODEL=deepseek-reasoner',
|
|
277
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-reasoner',
|
|
278
|
+
'ANTHROPIC_SMALL_FAST_MODEL=deepseek-chat',
|
|
279
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-chat',
|
|
280
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-chat',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
configLines.push('API_TIMEOUT_MS=600000', 'MCP_TOOL_TIMEOUT=30000');
|
|
285
|
+
log('ok', `已配置为 DeepSeek (${dsModel},Anthropic 兼容)`);
|
|
286
|
+
log('info', 'BASE_URL: https://api.deepseek.com/anthropic');
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 5: {
|
|
290
|
+
// Custom
|
|
291
|
+
const defaultUrl = existing.MODEL_PROVIDER === 'custom' ? existing.ANTHROPIC_BASE_URL || '' : '';
|
|
292
|
+
console.log(`请输入 Anthropic 兼容的 BASE_URL${defaultUrl ? `(回车保留: ${defaultUrl})` : ''}:`);
|
|
293
|
+
let baseUrl = await ask(rl, ' URL: ');
|
|
294
|
+
baseUrl = baseUrl.trim() || defaultUrl;
|
|
295
|
+
console.log('');
|
|
296
|
+
|
|
297
|
+
const existingKey = existing.MODEL_PROVIDER === 'custom' ? existing.ANTHROPIC_API_KEY : '';
|
|
298
|
+
const apiKey = await askApiKey(rl, '自定义平台', '', existingKey);
|
|
299
|
+
|
|
300
|
+
configLines = [
|
|
301
|
+
'# Claude Coder 模型配置',
|
|
302
|
+
'# 提供商: 自定义',
|
|
303
|
+
'',
|
|
304
|
+
'MODEL_PROVIDER=custom',
|
|
305
|
+
`ANTHROPIC_BASE_URL=${baseUrl}`,
|
|
306
|
+
`ANTHROPIC_API_KEY=${apiKey}`,
|
|
307
|
+
'API_TIMEOUT_MS=3000000',
|
|
308
|
+
'MCP_TOOL_TIMEOUT=30000',
|
|
309
|
+
];
|
|
310
|
+
log('ok', '已配置为自定义模型');
|
|
311
|
+
log('info', `BASE_URL: ${baseUrl}`);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// === Step 2: MCP tools ===
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log('============================================');
|
|
319
|
+
console.log(' MCP 工具配置(可选)');
|
|
320
|
+
console.log('============================================');
|
|
321
|
+
console.log('');
|
|
322
|
+
|
|
323
|
+
console.log('是否启用 Playwright MCP(浏览器自动化测试)?');
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(' Playwright MCP 由微软官方维护 (github.com/microsoft/playwright-mcp)');
|
|
326
|
+
console.log(' 提供 browser_click、browser_snapshot 等 25+ 浏览器自动化工具');
|
|
327
|
+
console.log(' 适用于有 Web 前端的项目,Agent 可用它做端到端测试');
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log(' 1) 是 - 启用 Playwright MCP(项目有 Web 前端)');
|
|
330
|
+
console.log(' 2) 否 - 跳过(纯后端 / CLI 项目)');
|
|
331
|
+
console.log('');
|
|
332
|
+
|
|
333
|
+
const mcpChoice = await askChoice(rl, '选择 [1-2]: ', 1, 2);
|
|
334
|
+
|
|
335
|
+
configLines.push('', '# MCP 工具配置');
|
|
336
|
+
if (mcpChoice === 1) {
|
|
337
|
+
configLines.push('MCP_PLAYWRIGHT=true');
|
|
338
|
+
log('ok', 'Playwright MCP 已启用');
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(' 请确保已安装 Playwright MCP:');
|
|
341
|
+
console.log(` ${COLOR.blue}npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/playwright-mcp${COLOR.reset}`);
|
|
342
|
+
console.log(` ${COLOR.blue}详见: https://github.com/microsoft/playwright-mcp${COLOR.reset}`);
|
|
343
|
+
} else {
|
|
344
|
+
configLines.push('MCP_PLAYWRIGHT=false');
|
|
345
|
+
log('info', '已跳过 Playwright MCP');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Debug output
|
|
349
|
+
console.log('');
|
|
350
|
+
console.log('是否开启 Claude 调试输出(便于排查问题,输出较多)?');
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(' 1) 否 - 静默(默认,推荐)');
|
|
353
|
+
console.log(' 2) 是 - verbose(完整每轮输出)');
|
|
354
|
+
console.log(' 3) 是 - mcp(MCP 调用,如 Playwright Click)');
|
|
355
|
+
console.log('');
|
|
356
|
+
|
|
357
|
+
const debugChoice = await askChoice(rl, '选择 [1-3,默认 1]: ', 1, 3, 1);
|
|
358
|
+
configLines.push('', '# Claude 调试(可随时修改)');
|
|
359
|
+
if (debugChoice === 2) {
|
|
360
|
+
configLines.push('CLAUDE_DEBUG=verbose');
|
|
361
|
+
log('info', '已启用 CLAUDE_DEBUG=verbose');
|
|
362
|
+
} else if (debugChoice === 3) {
|
|
363
|
+
configLines.push('CLAUDE_DEBUG=mcp');
|
|
364
|
+
log('info', '已启用 CLAUDE_DEBUG=mcp');
|
|
365
|
+
} else {
|
|
366
|
+
configLines.push('# CLAUDE_DEBUG=verbose # 取消注释可开启');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Write config
|
|
370
|
+
writeConfig(p.envFile, configLines);
|
|
371
|
+
ensureGitignore();
|
|
372
|
+
|
|
373
|
+
rl.close();
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
log('ok', '配置完成!');
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(` 配置文件: ${p.envFile}`);
|
|
379
|
+
console.log(' 使用方式: claude-coder run "你的需求"');
|
|
380
|
+
console.log(' 详细需求: 创建 requirements.md 后运行 claude-coder run');
|
|
381
|
+
console.log(' 重新配置: claude-coder setup');
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = { setup };
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { paths, log, COLOR } = require('./config');
|
|
5
|
+
|
|
6
|
+
const VALID_STATUSES = ['pending', 'in_progress', 'testing', 'done', 'failed'];
|
|
7
|
+
|
|
8
|
+
const TRANSITIONS = {
|
|
9
|
+
pending: ['in_progress'],
|
|
10
|
+
in_progress: ['testing'],
|
|
11
|
+
testing: ['done', 'failed'],
|
|
12
|
+
failed: ['in_progress'],
|
|
13
|
+
done: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function loadTasks() {
|
|
17
|
+
const p = paths();
|
|
18
|
+
if (!fs.existsSync(p.tasksFile)) return null;
|
|
19
|
+
return JSON.parse(fs.readFileSync(p.tasksFile, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveTasks(data) {
|
|
23
|
+
const p = paths();
|
|
24
|
+
fs.writeFileSync(p.tasksFile, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getFeatures(data) {
|
|
28
|
+
return data?.features || [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findNextTask(data) {
|
|
32
|
+
const features = getFeatures(data);
|
|
33
|
+
const failed = features.filter(f => f.status === 'failed')
|
|
34
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
35
|
+
if (failed.length > 0) return failed[0];
|
|
36
|
+
|
|
37
|
+
const pending = features.filter(f => f.status === 'pending')
|
|
38
|
+
.filter(f => {
|
|
39
|
+
const deps = f.depends_on || [];
|
|
40
|
+
return deps.every(depId => {
|
|
41
|
+
const dep = features.find(x => x.id === depId);
|
|
42
|
+
return dep && dep.status === 'done';
|
|
43
|
+
});
|
|
44
|
+
})
|
|
45
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
46
|
+
return pending[0] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setStatus(data, taskId, newStatus) {
|
|
50
|
+
const features = getFeatures(data);
|
|
51
|
+
const task = features.find(f => f.id === taskId);
|
|
52
|
+
if (!task) throw new Error(`任务不存在: ${taskId}`);
|
|
53
|
+
if (!VALID_STATUSES.includes(newStatus)) throw new Error(`无效状态: ${newStatus}`);
|
|
54
|
+
|
|
55
|
+
const allowed = TRANSITIONS[task.status];
|
|
56
|
+
if (!allowed || !allowed.includes(newStatus)) {
|
|
57
|
+
throw new Error(`非法状态迁移: ${task.status} → ${newStatus}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
task.status = newStatus;
|
|
61
|
+
saveTasks(data);
|
|
62
|
+
return task;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addTask(data, task) {
|
|
66
|
+
if (!data) {
|
|
67
|
+
data = { project: '', created_at: new Date().toISOString().slice(0, 10), features: [] };
|
|
68
|
+
}
|
|
69
|
+
data.features.push(task);
|
|
70
|
+
saveTasks(data);
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getStats(data) {
|
|
75
|
+
const features = getFeatures(data);
|
|
76
|
+
return {
|
|
77
|
+
total: features.length,
|
|
78
|
+
done: features.filter(f => f.status === 'done').length,
|
|
79
|
+
failed: features.filter(f => f.status === 'failed').length,
|
|
80
|
+
in_progress: features.filter(f => f.status === 'in_progress').length,
|
|
81
|
+
testing: features.filter(f => f.status === 'testing').length,
|
|
82
|
+
pending: features.filter(f => f.status === 'pending').length,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showStatus() {
|
|
87
|
+
const p = paths();
|
|
88
|
+
const data = loadTasks();
|
|
89
|
+
if (!data) {
|
|
90
|
+
log('warn', '未找到 .claude-coder/tasks.json,请先运行 claude-coder run');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stats = getStats(data);
|
|
95
|
+
const features = getFeatures(data);
|
|
96
|
+
|
|
97
|
+
console.log(`\n${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
98
|
+
console.log(` ${COLOR.blue}📋 任务状态${COLOR.reset} 项目: ${data.project || '(未命名)'}`);
|
|
99
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
100
|
+
|
|
101
|
+
const bar = stats.total > 0
|
|
102
|
+
? `[${'█'.repeat(Math.floor(stats.done / stats.total * 30))}${'░'.repeat(30 - Math.floor(stats.done / stats.total * 30))}]`
|
|
103
|
+
: '[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]';
|
|
104
|
+
console.log(` 进度: ${bar} ${stats.done}/${stats.total}`);
|
|
105
|
+
|
|
106
|
+
console.log(`\n ${COLOR.green}✔ done: ${stats.done}${COLOR.reset} ${COLOR.yellow}⏳ pending: ${stats.pending}${COLOR.reset} ${COLOR.red}✘ failed: ${stats.failed}${COLOR.reset}`);
|
|
107
|
+
|
|
108
|
+
if (stats.in_progress > 0 || stats.testing > 0) {
|
|
109
|
+
console.log(` ▸ in_progress: ${stats.in_progress} ▸ testing: ${stats.testing}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cost summary from progress.json (harness records SDK cost per session)
|
|
113
|
+
if (fs.existsSync(p.progressFile)) {
|
|
114
|
+
try {
|
|
115
|
+
const progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8'));
|
|
116
|
+
const sessions = (progress.sessions || []).filter(s => typeof s.cost === 'number');
|
|
117
|
+
if (sessions.length > 0) {
|
|
118
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
119
|
+
console.log(`\n ${COLOR.blue}💰 累计成本${COLOR.reset}: $${totalCost.toFixed(4)} (${sessions.length} sessions)`);
|
|
120
|
+
}
|
|
121
|
+
} catch { /* ignore parse errors */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Task list
|
|
125
|
+
console.log(`\n ${'─'.repeat(45)}`);
|
|
126
|
+
for (const f of features) {
|
|
127
|
+
const icon = { done: '✔', pending: '○', in_progress: '▸', testing: '⟳', failed: '✘' }[f.status] || '?';
|
|
128
|
+
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow }[f.status] || '';
|
|
129
|
+
console.log(` ${color}${icon}${COLOR.reset} [${f.id}] ${f.description} (${f.status})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`${COLOR.blue}═══════════════════════════════════════════════${COLOR.reset}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
VALID_STATUSES,
|
|
137
|
+
TRANSITIONS,
|
|
138
|
+
loadTasks,
|
|
139
|
+
saveTasks,
|
|
140
|
+
getFeatures,
|
|
141
|
+
findNextTask,
|
|
142
|
+
setStatus,
|
|
143
|
+
addTask,
|
|
144
|
+
getStats,
|
|
145
|
+
showStatus,
|
|
146
|
+
};
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { paths, log, getProjectRoot } = require('./config');
|
|
6
|
+
|
|
7
|
+
function validateSessionResult() {
|
|
8
|
+
const p = paths();
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(p.sessionResult)) {
|
|
11
|
+
log('error', 'Agent 未生成 session_result.json');
|
|
12
|
+
return { valid: false, fatal: true, reason: 'session_result.json 不存在' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let data;
|
|
16
|
+
try {
|
|
17
|
+
data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
log('error', 'session_result.json JSON 格式错误');
|
|
20
|
+
return { valid: false, fatal: true, reason: 'JSON 格式错误' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sr = data.current || data;
|
|
24
|
+
|
|
25
|
+
const required = ['session_result', 'status_after'];
|
|
26
|
+
const missing = required.filter(k => !(k in sr));
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
log('error', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
29
|
+
return { valid: false, fatal: true, reason: `缺少字段: ${missing.join(', ')}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!['success', 'failed'].includes(sr.session_result)) {
|
|
33
|
+
log('error', `session_result 必须是 success 或 failed,实际是: ${sr.session_result}`);
|
|
34
|
+
return { valid: false, fatal: true, reason: `无效 session_result: ${sr.session_result}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const validStatuses = ['pending', 'in_progress', 'testing', 'done', 'failed'];
|
|
38
|
+
if (!validStatuses.includes(sr.status_after)) {
|
|
39
|
+
log('error', `status_after 不合法: ${sr.status_after}`);
|
|
40
|
+
return { valid: false, fatal: true, reason: `无效 status_after: ${sr.status_after}` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!sr.task_id) {
|
|
44
|
+
log('warn', 'session_result.json 缺少 task_id (建议包含)');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sr.session_result === 'success') {
|
|
48
|
+
log('ok', 'session_result.json 合法 (success)');
|
|
49
|
+
} else {
|
|
50
|
+
log('warn', 'session_result.json 合法,但 Agent 报告失败 (failed)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { valid: true, fatal: false, data: sr };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkGitProgress(headBefore) {
|
|
57
|
+
if (!headBefore) {
|
|
58
|
+
log('info', '未提供 head_before,跳过 git 检查');
|
|
59
|
+
return { hasCommit: false, warning: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const projectRoot = getProjectRoot();
|
|
63
|
+
let headAfter;
|
|
64
|
+
try {
|
|
65
|
+
headAfter = execSync('git rev-parse HEAD', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
66
|
+
} catch {
|
|
67
|
+
headAfter = 'none';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (headBefore === headAfter) {
|
|
71
|
+
log('warn', '本次会话没有新的 git 提交');
|
|
72
|
+
return { hasCommit: false, warning: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
77
|
+
log('ok', `检测到新提交: ${msg}`);
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
|
|
80
|
+
return { hasCommit: true, warning: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkTestCoverage() {
|
|
84
|
+
const p = paths();
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
90
|
+
const current = sr.current || sr;
|
|
91
|
+
const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
|
|
92
|
+
|
|
93
|
+
const taskId = current.task_id || '';
|
|
94
|
+
const testCases = tests.test_cases || [];
|
|
95
|
+
|
|
96
|
+
if (current.status_after === 'done' && current.tests_passed) {
|
|
97
|
+
const taskTests = testCases.filter(t => t.feature_id === taskId);
|
|
98
|
+
if (taskTests.length > 0) {
|
|
99
|
+
const failed = taskTests.filter(t => t.last_result === 'fail');
|
|
100
|
+
if (failed.length > 0) {
|
|
101
|
+
log('warn', `tests.json 中有失败的验证记录: ${failed.map(t => t.id).join(', ')}`);
|
|
102
|
+
} else {
|
|
103
|
+
log('ok', `${taskTests.length} 条验证记录覆盖任务 ${taskId}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function validate(headBefore) {
|
|
111
|
+
log('info', '========== 开始校验 ==========');
|
|
112
|
+
|
|
113
|
+
const srResult = validateSessionResult();
|
|
114
|
+
const gitResult = checkGitProgress(headBefore);
|
|
115
|
+
checkTestCoverage();
|
|
116
|
+
|
|
117
|
+
const fatal = srResult.fatal;
|
|
118
|
+
const hasWarnings = gitResult.warning;
|
|
119
|
+
|
|
120
|
+
if (fatal) {
|
|
121
|
+
log('error', '========== 校验失败 (致命) ==========');
|
|
122
|
+
} else if (hasWarnings) {
|
|
123
|
+
log('warn', '========== 校验通过 (有警告) ==========');
|
|
124
|
+
} else {
|
|
125
|
+
log('ok', '========== 校验全部通过 ==========');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { fatal, hasWarnings, sessionData: srResult.data };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { validate, validateSessionResult, checkGitProgress };
|