claude-coder 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +177 -125
  2. package/bin/cli.js +159 -161
  3. package/package.json +52 -47
  4. package/src/commands/auth.js +294 -0
  5. package/src/commands/setup-modules/helpers.js +105 -0
  6. package/src/commands/setup-modules/index.js +26 -0
  7. package/src/commands/setup-modules/mcp.js +95 -0
  8. package/src/commands/setup-modules/provider.js +261 -0
  9. package/src/commands/setup-modules/safety.js +62 -0
  10. package/src/commands/setup-modules/simplify.js +53 -0
  11. package/src/commands/setup.js +172 -0
  12. package/src/common/assets.js +192 -0
  13. package/src/{config.js → common/config.js} +138 -201
  14. package/src/common/constants.js +57 -0
  15. package/src/{indicator.js → common/indicator.js} +222 -217
  16. package/src/common/interaction.js +170 -0
  17. package/src/common/logging.js +77 -0
  18. package/src/common/sdk.js +51 -0
  19. package/src/{tasks.js → common/tasks.js} +157 -172
  20. package/src/common/utils.js +147 -0
  21. package/src/core/base.js +54 -0
  22. package/src/core/coding.js +55 -0
  23. package/src/core/context.js +132 -0
  24. package/src/core/hooks.js +529 -0
  25. package/src/{init.js → core/init.js} +163 -144
  26. package/src/core/plan.js +318 -0
  27. package/src/core/prompts.js +253 -0
  28. package/src/core/query.js +48 -0
  29. package/src/core/repair.js +58 -0
  30. package/src/{runner.js → core/runner.js} +352 -420
  31. package/src/core/scan.js +89 -0
  32. package/src/core/simplify.js +59 -0
  33. package/src/core/validator.js +138 -0
  34. package/{prompts/ADD_GUIDE.md → templates/addGuide.md} +98 -98
  35. package/templates/addUser.md +26 -0
  36. package/{prompts/CLAUDE.md → templates/agentProtocol.md} +195 -199
  37. package/templates/bash-process.md +5 -0
  38. package/{prompts/coding_user.md → templates/codingUser.md} +31 -23
  39. package/templates/guidance.json +35 -0
  40. package/templates/playwright.md +17 -0
  41. package/templates/requirements.example.md +56 -56
  42. package/{prompts/SCAN_PROTOCOL.md → templates/scanProtocol.md} +118 -118
  43. package/{prompts/scan_user.md → templates/scanUser.md} +17 -17
  44. package/templates/test_rule.md +194 -194
  45. package/prompts/add_user.md +0 -24
  46. package/src/auth.js +0 -245
  47. package/src/hooks.js +0 -160
  48. package/src/prompts.js +0 -295
  49. package/src/scanner.js +0 -62
  50. package/src/session.js +0 -352
  51. package/src/setup.js +0 -579
  52. package/src/validator.js +0 -181
@@ -1,144 +1,163 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const net = require('net');
5
- const http = require('http');
6
- const { spawn, execSync } = require('child_process');
7
- const { paths, log, getProjectRoot } = require('./config');
8
-
9
- function loadProfile() {
10
- const p = paths();
11
- if (!fs.existsSync(p.profile)) {
12
- log('error', '未找到 project_profile.json,请先运行 claude-coder run 完成项目扫描');
13
- process.exit(1);
14
- }
15
- return JSON.parse(fs.readFileSync(p.profile, 'utf8'));
16
- }
17
-
18
- function isPortFree(port) {
19
- return new Promise(resolve => {
20
- const server = net.createServer();
21
- server.once('error', () => resolve(false));
22
- server.once('listening', () => { server.close(); resolve(true); });
23
- server.listen(port, '127.0.0.1');
24
- });
25
- }
26
-
27
- function waitForHealth(url, timeoutMs = 15000) {
28
- const start = Date.now();
29
- return new Promise(resolve => {
30
- const check = () => {
31
- if (Date.now() - start > timeoutMs) { resolve(false); return; }
32
- const req = http.get(url, res => {
33
- resolve(res.statusCode < 500);
34
- });
35
- req.on('error', () => setTimeout(check, 1000));
36
- req.setTimeout(3000, () => { req.destroy(); setTimeout(check, 1000); });
37
- };
38
- check();
39
- });
40
- }
41
-
42
- function runCmd(cmd, cwd) {
43
- try {
44
- execSync(cmd, { cwd: cwd || getProjectRoot(), stdio: 'inherit', shell: true });
45
- return true;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- async function init() {
52
- const profile = loadProfile();
53
- const projectRoot = getProjectRoot();
54
- let stepCount = 0;
55
-
56
- // 1. Environment activation
57
- const envSetup = profile.env_setup || {};
58
- if (envSetup.python_env && envSetup.python_env !== 'system' && envSetup.python_env !== 'none') {
59
- stepCount++;
60
- if (envSetup.python_env.startsWith('conda:')) {
61
- const envName = envSetup.python_env.slice(6);
62
- log('info', `[${stepCount}] Python 环境: conda activate ${envName}`);
63
- runCmd(`conda activate ${envName}`);
64
- } else if (envSetup.python_env === 'venv') {
65
- log('info', `[${stepCount}] Python 环境: venv`);
66
- runCmd('source .venv/bin/activate || .venv\\Scripts\\activate');
67
- }
68
- }
69
- if (envSetup.node_version && envSetup.node_version !== 'none') {
70
- stepCount++;
71
- log('info', `[${stepCount}] Node.js: v${envSetup.node_version}`);
72
- runCmd(`nvm use ${envSetup.node_version}`);
73
- }
74
-
75
- // 2. Dependencies
76
- const pkgManagers = (profile.tech_stack && profile.tech_stack.package_managers) || [];
77
- for (const pm of pkgManagers) {
78
- stepCount++;
79
- if (pm === 'npm' || pm === 'yarn' || pm === 'pnpm') {
80
- if (fs.existsSync(`${projectRoot}/node_modules`)) {
81
- log('ok', `[${stepCount}] ${pm} 依赖已安装,跳过`);
82
- } else {
83
- log('info', `[${stepCount}] 安装依赖: ${pm} install`);
84
- runCmd(`${pm} install`, projectRoot);
85
- }
86
- } else if (pm === 'pip') {
87
- const reqFile = fs.existsSync(`${projectRoot}/requirements.txt`);
88
- if (reqFile) {
89
- log('info', `[${stepCount}] 安装依赖: pip install -r requirements.txt`);
90
- runCmd('pip install -r requirements.txt', projectRoot);
91
- }
92
- }
93
- }
94
-
95
- // 3. Custom init commands
96
- const customInit = profile.custom_init || [];
97
- for (const cmd of customInit) {
98
- stepCount++;
99
- log('info', `[${stepCount}] 自定义: ${cmd}`);
100
- runCmd(cmd, projectRoot);
101
- }
102
-
103
- // 4. Services
104
- const services = profile.services || [];
105
- for (const svc of services) {
106
- stepCount++;
107
- const free = await isPortFree(svc.port);
108
- if (!free) {
109
- log('ok', `[${stepCount}] ${svc.name} 已在端口 ${svc.port} 运行,跳过`);
110
- continue;
111
- }
112
-
113
- log('info', `[${stepCount}] 启动 ${svc.name} (端口 ${svc.port})...`);
114
- const cwd = svc.cwd ? `${projectRoot}/${svc.cwd}` : projectRoot;
115
- const child = spawn(svc.command, { cwd, shell: true, detached: true, stdio: 'ignore' });
116
- child.unref();
117
-
118
- if (svc.health_check) {
119
- const healthy = await waitForHealth(svc.health_check);
120
- if (healthy) {
121
- log('ok', `${svc.name} 就绪: ${svc.health_check}`);
122
- } else {
123
- log('warn', `${svc.name} 健康检查超时 (${svc.health_check}),继续执行`);
124
- }
125
- }
126
- }
127
-
128
- // Summary
129
- if (stepCount === 0) {
130
- log('info', '无需初始化操作');
131
- } else {
132
- log('ok', `初始化完成 (${stepCount} 步)`);
133
- }
134
-
135
- if (services.length > 0) {
136
- console.log('');
137
- for (const svc of services) {
138
- console.log(` ${svc.name}: http://localhost:${svc.port}`);
139
- }
140
- console.log('');
141
- }
142
- }
143
-
144
- module.exports = { init };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const net = require('net');
5
+ const http = require('http');
6
+ const { spawn, execSync } = require('child_process');
7
+ const { log } = require('../common/config');
8
+ const { assets } = require('../common/assets');
9
+ const { scan } = require('./scan');
10
+
11
+ function loadProfile() {
12
+ const data = assets.readJson('profile', null);
13
+ if (!data) {
14
+ log('error', 'project_profile.json 读取失败或已损坏');
15
+ process.exit(1);
16
+ }
17
+ return data;
18
+ }
19
+
20
+ function isPortFree(port) {
21
+ return new Promise(resolve => {
22
+ const server = net.createServer();
23
+ server.once('error', () => resolve(false));
24
+ server.once('listening', () => { server.close(); resolve(true); });
25
+ server.listen(port, '127.0.0.1');
26
+ });
27
+ }
28
+
29
+ function waitForHealth(url, timeoutMs = 15000) {
30
+ const start = Date.now();
31
+ return new Promise(resolve => {
32
+ const check = () => {
33
+ if (Date.now() - start > timeoutMs) { resolve(false); return; }
34
+ const req = http.get(url, res => {
35
+ resolve(res.statusCode < 500);
36
+ });
37
+ req.on('error', () => setTimeout(check, 1000));
38
+ req.setTimeout(3000, () => { req.destroy(); setTimeout(check, 1000); });
39
+ };
40
+ check();
41
+ });
42
+ }
43
+
44
+ function runCmd(cmd, cwd) {
45
+ try {
46
+ execSync(cmd, { cwd: cwd || assets.projectRoot, stdio: 'inherit', shell: true });
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function deployAssets() {
54
+ const deployed = assets.deployAll();
55
+ if (deployed.length > 0) {
56
+ for (const file of deployed) {
57
+ log('ok', `已部署 .claude-coder/assets/${file}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ async function init() {
63
+ assets.ensureDirs();
64
+ const projectRoot = assets.projectRoot;
65
+
66
+ if (!assets.exists('profile')) {
67
+ log('info', 'profile 不存在,正在执行项目扫描...');
68
+ const scanResult = await scan('', { projectRoot });
69
+ if (!scanResult.success) {
70
+ log('error', '项目扫描失败');
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ const profile = loadProfile();
76
+ let stepCount = 0;
77
+
78
+ deployAssets();
79
+
80
+ const envSetup = profile.env_setup || {};
81
+ if (envSetup.python_env && envSetup.python_env !== 'system' && envSetup.python_env !== 'none') {
82
+ stepCount++;
83
+ if (envSetup.python_env.startsWith('conda:')) {
84
+ const envName = envSetup.python_env.slice(6);
85
+ log('info', `[${stepCount}] Python 环境: conda activate ${envName}`);
86
+ runCmd(`conda activate ${envName}`);
87
+ } else if (envSetup.python_env === 'venv') {
88
+ log('info', `[${stepCount}] Python 环境: venv`);
89
+ runCmd('source .venv/bin/activate || .venv\\Scripts\\activate');
90
+ }
91
+ }
92
+ if (envSetup.node_version && envSetup.node_version !== 'none') {
93
+ stepCount++;
94
+ log('info', `[${stepCount}] Node.js: v${envSetup.node_version}`);
95
+ runCmd(`nvm use ${envSetup.node_version}`);
96
+ }
97
+
98
+ const pkgManagers = (profile.tech_stack && profile.tech_stack.package_managers) || [];
99
+ for (const pm of pkgManagers) {
100
+ stepCount++;
101
+ if (pm === 'npm' || pm === 'yarn' || pm === 'pnpm') {
102
+ if (fs.existsSync(`${projectRoot}/node_modules`)) {
103
+ log('ok', `[${stepCount}] ${pm} 依赖已安装,跳过`);
104
+ } else {
105
+ log('info', `[${stepCount}] 安装依赖: ${pm} install`);
106
+ runCmd(`${pm} install`, projectRoot);
107
+ }
108
+ } else if (pm === 'pip') {
109
+ const reqFile = fs.existsSync(`${projectRoot}/requirements.txt`);
110
+ if (reqFile) {
111
+ log('info', `[${stepCount}] 安装依赖: pip install -r requirements.txt`);
112
+ runCmd('pip install -r requirements.txt', projectRoot);
113
+ }
114
+ }
115
+ }
116
+
117
+ const customInit = profile.custom_init || [];
118
+ for (const cmd of customInit) {
119
+ stepCount++;
120
+ log('info', `[${stepCount}] 自定义: ${cmd}`);
121
+ runCmd(cmd, projectRoot);
122
+ }
123
+
124
+ const services = profile.services || [];
125
+ for (const svc of services) {
126
+ stepCount++;
127
+ const free = await isPortFree(svc.port);
128
+ if (!free) {
129
+ log('ok', `[${stepCount}] ${svc.name} 已在端口 ${svc.port} 运行,跳过`);
130
+ continue;
131
+ }
132
+
133
+ log('info', `[${stepCount}] 启动 ${svc.name} (端口 ${svc.port})...`);
134
+ const cwd = svc.cwd ? `${projectRoot}/${svc.cwd}` : projectRoot;
135
+ const child = spawn(svc.command, { cwd, shell: true, detached: true, stdio: 'ignore' });
136
+ child.unref();
137
+
138
+ if (svc.health_check) {
139
+ const healthy = await waitForHealth(svc.health_check);
140
+ if (healthy) {
141
+ log('ok', `${svc.name} 就绪: ${svc.health_check}`);
142
+ } else {
143
+ log('warn', `${svc.name} 健康检查超时 (${svc.health_check}),继续执行`);
144
+ }
145
+ }
146
+ }
147
+
148
+ if (stepCount === 0) {
149
+ log('info', '无需初始化操作');
150
+ } else {
151
+ log('ok', `初始化完成 (${stepCount} 步)`);
152
+ }
153
+
154
+ if (services.length > 0) {
155
+ console.log('');
156
+ for (const svc of services) {
157
+ console.log(` ${svc.name}: http://localhost:${svc.port}`);
158
+ }
159
+ console.log('');
160
+ }
161
+ }
162
+
163
+ module.exports = { init };
@@ -0,0 +1,318 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const { runSession } = require('./base');
8
+ const { buildQueryOptions } = require('./query');
9
+ const { buildPlanSystemPrompt, buildPlanPrompt } = require('./prompts');
10
+ const { log, loadConfig } = require('../common/config');
11
+ const { assets } = require('../common/assets');
12
+ const { extractResultText } = require('../common/logging');
13
+ const { printStats } = require('../common/tasks');
14
+
15
+ const EXIT_TIMEOUT_MS = 300000;
16
+ const PLANS_DIR = path.join(os.homedir(), '.claude', 'plans');
17
+
18
+ function buildPlanOnlyPrompt(userInput, interactive = false) {
19
+ return `${userInput}
20
+
21
+ 【工作流程】
22
+ 1. 探索项目代码库,理解结构和技术栈
23
+ 2. ${interactive ? '【约束】如果有不确定的关键决策点,请使用 AskUserQuestion 工具向用户提问。' : '【约束】不要提问,默认使用最佳推荐方案。'}
24
+ 3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
25
+ 4. 写入计划文件后,输出以下标记(独占一行):
26
+ PLAN_FILE_PATH: <计划文件绝对路径>
27
+ 5. 简要总结计划要点
28
+ `;
29
+ }
30
+
31
+ /**
32
+ * 从文本中提取计划文件路径
33
+ * 优先级:PLAN_FILE_PATH 标记 > .claude/plans/*.md > 反引号包裹 .md > 任意绝对 .md
34
+ */
35
+ function extractPlanPath(text) {
36
+ if (!text) return null;
37
+
38
+ const tagMatch = text.match(/PLAN_FILE_PATH:\s*(\S+\.md)/);
39
+ if (tagMatch) return tagMatch[1];
40
+
41
+ const plansMatch = text.match(/([^\s`'"(]*\.claude\/plans\/[^\s`'"()]+\.md)/);
42
+ if (plansMatch) return plansMatch[1];
43
+
44
+ const backtickMatch = text.match(/`([^`]+\.md)`/);
45
+ if (backtickMatch) return backtickMatch[1];
46
+
47
+ const absMatch = text.match(/(\/[^\s`'"]+\.md)/);
48
+ if (absMatch) return absMatch[1];
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * 多源提取计划路径(按可靠性从高到低)
55
+ * 1. Write 工具调用参数(最可靠)
56
+ * 2. assistant 消息流文本
57
+ * 3. result.result 文本
58
+ * 4. plans 目录最新文件(兜底)
59
+ */
60
+ function extractPlanPathFromCollected(collected, startTime) {
61
+ // 第一层:从 Write 工具调用参数中直接获取
62
+ for (const msg of collected) {
63
+ if (msg.type !== 'assistant' || !msg.message?.content) continue;
64
+ for (const block of msg.message.content) {
65
+ if (block.type === 'tool_use' && block.name === 'Write') {
66
+ const target = block.input?.file_path || block.input?.path || '';
67
+ if (target.includes('.claude/plans/') && target.endsWith('.md')) {
68
+ if (fs.existsSync(target)) return target;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // 第二层:从所有 assistant 文本中提取
75
+ let fullText = '';
76
+ for (const msg of collected) {
77
+ if (msg.type === 'assistant' && msg.message?.content) {
78
+ for (const block of msg.message.content) {
79
+ if (block.type === 'text' && block.text) fullText += block.text;
80
+ }
81
+ }
82
+ }
83
+ if (fullText) {
84
+ const p = extractPlanPath(fullText);
85
+ if (p && fs.existsSync(p)) return p;
86
+ }
87
+
88
+ // 第三层:从 result.result 中提取
89
+ const resultText = extractResultText(collected);
90
+ if (resultText) {
91
+ const p = extractPlanPath(resultText);
92
+ if (p && fs.existsSync(p)) return p;
93
+ }
94
+
95
+ // 第四层:扫描 plans 目录,找 session 期间新建的文件
96
+ if (fs.existsSync(PLANS_DIR)) {
97
+ try {
98
+ const files = fs.readdirSync(PLANS_DIR)
99
+ .filter(f => f.endsWith('.md'))
100
+ .map(f => {
101
+ const fp = path.join(PLANS_DIR, f);
102
+ return { path: fp, mtime: fs.statSync(fp).mtimeMs };
103
+ })
104
+ .filter(f => f.mtime >= startTime)
105
+ .sort((a, b) => b.mtime - a.mtime);
106
+ if (files.length > 0) {
107
+ log('info', `从 plans 目录发现新文件: ${path.basename(files[0].path)}`);
108
+ return files[0].path;
109
+ }
110
+ } catch { /* ignore */ }
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ function copyPlanToProject(generatedPath) {
117
+ const filename = path.basename(generatedPath);
118
+ const targetDir = path.join(assets.projectRoot, '.claude-coder', 'plan');
119
+ const targetPath = path.join(targetDir, filename);
120
+
121
+ try {
122
+ if (!fs.existsSync(targetDir)) {
123
+ fs.mkdirSync(targetDir, { recursive: true });
124
+ }
125
+ fs.copyFileSync(generatedPath, targetPath);
126
+ return targetPath;
127
+ } catch {
128
+ return generatedPath;
129
+ }
130
+ }
131
+
132
+ async function _executePlanGen(sdk, ctx, userInput, opts = {}) {
133
+ const interactive = opts.interactive || false;
134
+ const prompt = buildPlanOnlyPrompt(userInput, interactive);
135
+ const queryOpts = {
136
+ permissionMode: 'plan',
137
+ cwd: opts.projectRoot || assets.projectRoot,
138
+ hooks: ctx.hooks,
139
+ };
140
+ if (!interactive) {
141
+ queryOpts.disallowedTools = ['askUserQuestion'];
142
+ }
143
+ if (opts.model) queryOpts.model = opts.model;
144
+
145
+ const startTime = Date.now();
146
+ let exitPlanModeDetected = false;
147
+ let exitPlanModeTime = null;
148
+
149
+ const collected = [];
150
+ const session = sdk.query({ prompt, options: queryOpts });
151
+
152
+ for await (const msg of session) {
153
+ if (ctx._isStalled && ctx._isStalled()) {
154
+ log('warn', '停顿超时,中断 plan 生成');
155
+ break;
156
+ }
157
+
158
+ if (exitPlanModeDetected && exitPlanModeTime) {
159
+ const elapsed = Date.now() - exitPlanModeTime;
160
+ if (elapsed > EXIT_TIMEOUT_MS && msg.type !== 'result') {
161
+ log('warn', '检测到 ExitPlanMode,等待审批超时,尝试从已收集消息中提取路径');
162
+ break;
163
+ }
164
+ }
165
+
166
+ collected.push(msg);
167
+ ctx._logMessage(msg);
168
+
169
+ if (msg.type === 'assistant' && msg.message?.content) {
170
+ for (const block of msg.message.content) {
171
+ if (block.type === 'tool_use' && block.name === 'ExitPlanMode') {
172
+ exitPlanModeDetected = true;
173
+ exitPlanModeTime = Date.now();
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ const planPath = extractPlanPathFromCollected(collected, startTime);
180
+
181
+ if (planPath) {
182
+ const targetPath = copyPlanToProject(planPath);
183
+ return { success: true, targetPath, generatedPath: planPath };
184
+ }
185
+
186
+ log('warn', '无法从输出中提取计划路径');
187
+ log('info', `请手动查看: ${PLANS_DIR}`);
188
+ return { success: false, reason: 'no_path', targetPath: null };
189
+ }
190
+
191
+ async function runPlanSession(instruction, opts = {}) {
192
+ const planOnly = opts.planOnly || false;
193
+ const interactive = opts.interactive || false;
194
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
195
+ const label = planOnly ? 'plan only' : 'plan tasks';
196
+ const hookType = interactive ? 'plan_interactive' : 'plan';
197
+
198
+ return runSession(hookType, {
199
+ opts,
200
+ sessionNum: 0,
201
+ logFileName: `plan_${ts}.log`,
202
+ label,
203
+
204
+ async execute(sdk, ctx) {
205
+ log('info', '正在生成计划方案...');
206
+
207
+ const planResult = await _executePlanGen(sdk, ctx, instruction, opts);
208
+
209
+ if (!planResult.success) {
210
+ log('error', `\n计划生成失败: ${planResult.reason || planResult.error}`);
211
+ return { success: false, reason: planResult.reason };
212
+ }
213
+
214
+ log('ok', `\n计划已生成: ${planResult.targetPath}`);
215
+
216
+ if (planOnly) {
217
+ return { success: true, planPath: planResult.targetPath };
218
+ }
219
+
220
+ log('info', '正在生成任务列表...');
221
+
222
+ const tasksPrompt = buildPlanPrompt(planResult.targetPath);
223
+ const queryOpts = buildQueryOptions(ctx.config, opts);
224
+ queryOpts.systemPrompt = buildPlanSystemPrompt();
225
+ queryOpts.hooks = ctx.hooks;
226
+ queryOpts.abortController = ctx.abortController;
227
+
228
+ await ctx.runQuery(sdk, tasksPrompt, queryOpts);
229
+
230
+ log('ok', '任务追加完成');
231
+ return { success: true, planPath: planResult.targetPath };
232
+ },
233
+ });
234
+ }
235
+
236
+ async function promptAutoRun() {
237
+ if (!process.stdin.isTTY) return false;
238
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
239
+ return new Promise(resolve => {
240
+ rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
241
+ rl.close();
242
+ resolve(/^[Yy]/.test(answer.trim()));
243
+ });
244
+ });
245
+ }
246
+
247
+ async function run(input, opts = {}) {
248
+ let instruction = input || '';
249
+
250
+ let reqPath = null;
251
+ if (opts.readFile) {
252
+ reqPath = path.resolve(assets.projectRoot || process.cwd(), opts.readFile);
253
+ if (!fs.existsSync(reqPath)) {
254
+ log('error', `文件不存在: ${reqPath}`);
255
+ process.exit(1);
256
+ }
257
+ instruction = fs.readFileSync(reqPath, 'utf8');
258
+ log('ok', `已读取需求文件: ${reqPath}`);
259
+ }
260
+
261
+ if (!instruction) {
262
+ log('error', '用法: claude-coder plan "需求内容" 或 claude-coder plan -r [requirements.md]');
263
+ process.exit(1);
264
+ }
265
+
266
+ assets.ensureDirs();
267
+ const projectRoot = assets.projectRoot;
268
+
269
+ const config = loadConfig();
270
+ if (!opts.model) {
271
+ if (config.defaultOpus) {
272
+ opts.model = config.defaultOpus;
273
+ } else if (config.model) {
274
+ opts.model = config.model;
275
+ }
276
+ }
277
+
278
+ const displayModel = opts.model || config.model || '(default)';
279
+ log('ok', `模型配置已加载: ${config.provider || 'claude'} (plan 使用: ${displayModel})`);
280
+ if (opts.interactive) {
281
+ log('info', '交互模式已启用,模型可能会向您提问');
282
+ }
283
+
284
+ if (!assets.exists('profile')) {
285
+ log('error', 'profile 不存在,请先运行 claude-coder init 初始化项目');
286
+ process.exit(1);
287
+ }
288
+
289
+ const result = await runPlanSession(instruction, { projectRoot, ...opts });
290
+
291
+ if (result.success) {
292
+ if (reqPath && result.planPath) {
293
+ try {
294
+ const planName = path.basename(result.planPath, '.md');
295
+ const backupDir = path.dirname(result.planPath);
296
+ const backupPath = path.join(backupDir, `${planName}-requirements.md`);
297
+ fs.copyFileSync(reqPath, backupPath);
298
+ log('ok', `需求文档已备份: ${path.basename(backupPath)}`);
299
+ } catch { /* ignore */ }
300
+ }
301
+
302
+ printStats();
303
+
304
+ let shouldAutoRun = false;
305
+ if (!opts.planOnly) {
306
+ shouldAutoRun = await promptAutoRun();
307
+ }
308
+
309
+ if (shouldAutoRun) {
310
+ console.log('');
311
+ log('info', '开始自动执行任务...');
312
+ const { run: runCoding } = require('./runner');
313
+ await runCoding(opts);
314
+ }
315
+ }
316
+ }
317
+
318
+ module.exports = { runPlanSession, run };