claude-coder 1.8.4 → 1.9.1

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 (78) hide show
  1. package/README.md +214 -167
  2. package/bin/cli.js +155 -172
  3. package/package.json +55 -53
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +362 -290
  32. package/src/commands/setup-modules/helpers.js +100 -99
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -95
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -61
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +245 -236
  40. package/src/common/config.js +125 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -222
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +213 -161
  48. package/src/core/coding.js +33 -55
  49. package/src/core/go.js +264 -310
  50. package/src/core/hooks.js +500 -533
  51. package/src/core/init.js +166 -171
  52. package/src/core/plan.js +188 -325
  53. package/src/core/prompts.js +247 -227
  54. package/src/core/repair.js +36 -46
  55. package/src/core/runner.js +458 -195
  56. package/src/core/scan.js +93 -89
  57. package/src/core/session.js +271 -57
  58. package/src/core/simplify.js +74 -53
  59. package/src/core/state.js +105 -0
  60. package/src/index.js +76 -0
  61. package/templates/bash-process.md +12 -12
  62. package/templates/codingSystem.md +65 -65
  63. package/templates/codingUser.md +17 -17
  64. package/templates/coreProtocol.md +29 -29
  65. package/templates/goSystem.md +130 -130
  66. package/templates/guidance.json +72 -53
  67. package/templates/planSystem.md +78 -78
  68. package/templates/planUser.md +8 -8
  69. package/templates/requirements.example.md +57 -57
  70. package/templates/scanSystem.md +120 -120
  71. package/templates/scanUser.md +10 -10
  72. package/templates/test_rule.md +194 -194
  73. package/templates/web-testing.md +17 -0
  74. package/types/index.d.ts +217 -0
  75. package/src/core/context.js +0 -117
  76. package/src/core/harness.js +0 -484
  77. package/src/core/query.js +0 -50
  78. package/templates/playwright.md +0 -17
package/src/core/scan.js CHANGED
@@ -1,89 +1,93 @@
1
- 'use strict';
2
-
3
- const { log } = require('../common/config');
4
- const { assets } = require('../common/assets');
5
- const { runSession } = require('./session');
6
- const { buildQueryOptions, hasCodeFiles } = require('./query');
7
- const { buildSystemPrompt, buildScanPrompt } = require('./prompts');
8
- const { extractResult } = require('../common/logging');
9
- const { RETRY } = require('../common/constants');
10
-
11
- function validateProfile() {
12
- if (!assets.exists('profile')) return { valid: false, issues: ['profile 不存在'] };
13
-
14
- const profile = assets.readJson('profile', null);
15
- if (!profile) return { valid: false, issues: ['profile 解析失败'] };
16
- const issues = [];
17
-
18
- if (!profile.tech_stack?.backend?.framework && !profile.tech_stack?.frontend?.framework) {
19
- issues.push('tech_stack 缺少 backend 或 frontend 框架');
20
- }
21
- if (profile.tech_stack?.backend?.framework && (!profile.services || profile.services.length === 0)) {
22
- issues.push('有后端框架但 services 为空(缺少启动命令和端口)');
23
- }
24
- if (!profile.existing_docs || profile.existing_docs.length === 0) {
25
- issues.push('existing_docs 为空(至少需要 README.md)');
26
- }
27
-
28
- return { valid: issues.length === 0, issues };
29
- }
30
-
31
- async function _runScanSession(opts = {}) {
32
- const projectType = hasCodeFiles(opts.projectRoot || assets.projectRoot) ? 'existing' : 'new';
33
- const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
34
-
35
- return runSession('scan', {
36
- opts,
37
- sessionNum: 0,
38
- logFileName: `scan_${dateStr}.log`,
39
- label: `scan (${projectType})`,
40
-
41
- async execute(sdk, ctx) {
42
- log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
43
-
44
- const prompt = buildScanPrompt(projectType);
45
- const queryOpts = buildQueryOptions(ctx.config, opts);
46
- queryOpts.systemPrompt = buildSystemPrompt('scan');
47
- queryOpts.hooks = ctx.hooks;
48
- queryOpts.abortController = ctx.abortController;
49
-
50
- const collected = await ctx.runQuery(sdk, prompt, queryOpts);
51
- const result = extractResult(collected);
52
-
53
- return { cost: result?.total_cost_usd ?? null };
54
- },
55
- });
56
- }
57
-
58
- async function scan(opts = {}) {
59
- assets.ensureDirs();
60
-
61
- const maxAttempts = RETRY.SCAN_ATTEMPTS;
62
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
63
- log('info', `初始化尝试 ${attempt} / ${maxAttempts} ...`);
64
-
65
- const result = await _runScanSession(opts);
66
-
67
- if (assets.exists('profile')) {
68
- const profileCheck = validateProfile();
69
- if (!profileCheck.valid) {
70
- log('warn', `profile 质量问题: ${profileCheck.issues.join('; ')}`);
71
- }
72
- log('ok', '项目扫描完成');
73
- return { success: true, cost: result.cost };
74
- }
75
-
76
- if (attempt < maxAttempts) {
77
- log('warn', '初始化未完成,将重试...');
78
- }
79
- }
80
-
81
- log('error', `初始化失败:已重试 ${maxAttempts} 次,关键文件仍未生成`);
82
- return { success: false, cost: null };
83
- }
84
-
85
- module.exports = {
86
- scan,
87
- validateProfile,
88
- _runScanSession,
89
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { log } = require('../common/config');
6
+ const { assets } = require('../common/assets');
7
+ const { buildSystemPrompt, buildScanPrompt } = require('./prompts');
8
+ const { Session } = require('./session');
9
+ const { RETRY } = require('../common/constants');
10
+
11
+ function hasCodeFiles(projectRoot) {
12
+ const markers = [
13
+ 'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py',
14
+ 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
15
+ 'Makefile', 'Dockerfile', 'docker-compose.yml',
16
+ 'README.md', 'main.py', 'app.py', 'index.js', 'index.ts',
17
+ ];
18
+ for (const m of markers) {
19
+ if (fs.existsSync(path.join(projectRoot, m))) return true;
20
+ }
21
+ for (const d of ['src', 'lib', 'app', 'backend', 'frontend', 'web', 'server', 'client']) {
22
+ if (fs.existsSync(path.join(projectRoot, d)) && fs.statSync(path.join(projectRoot, d)).isDirectory()) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ function validateProfile() {
28
+ if (!assets.exists('profile')) return { valid: false, issues: ['profile 不存在'] };
29
+
30
+ const profile = assets.readJson('profile', null);
31
+ if (!profile) return { valid: false, issues: ['profile 解析失败'] };
32
+ const issues = [];
33
+
34
+ if (!profile.tech_stack?.backend?.framework && !profile.tech_stack?.frontend?.framework) {
35
+ issues.push('tech_stack 缺少 backend 或 frontend 框架');
36
+ }
37
+ if (profile.tech_stack?.backend?.framework && (!profile.services || profile.services.length === 0)) {
38
+ issues.push('有后端框架但 services 为空(缺少启动命令和端口)');
39
+ }
40
+ if (!profile.existing_docs || profile.existing_docs.length === 0) {
41
+ issues.push('existing_docs 为空(至少需要 README.md)');
42
+ }
43
+
44
+ return { valid: issues.length === 0, issues };
45
+ }
46
+
47
+ async function executeScan(config, opts = {}) {
48
+ const maxAttempts = RETRY.SCAN_ATTEMPTS;
49
+
50
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
51
+ log('info', `初始化尝试 ${attempt} / ${maxAttempts} ...`);
52
+
53
+ const projectType = hasCodeFiles(opts.projectRoot || assets.projectRoot) ? 'existing' : 'new';
54
+ const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
55
+
56
+ const result = await Session.run('scan', config, {
57
+ logFileName: `scan_${dateStr}.log`,
58
+ label: `scan (${projectType})`,
59
+
60
+ async execute(session) {
61
+ log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
62
+
63
+ const prompt = buildScanPrompt(projectType);
64
+ const queryOpts = session.buildQueryOptions(opts);
65
+ queryOpts.systemPrompt = buildSystemPrompt('scan');
66
+
67
+ const { cost } = await session.runQuery(prompt, queryOpts);
68
+ return { cost };
69
+ },
70
+ });
71
+
72
+ if (assets.exists('profile')) {
73
+ const profileCheck = validateProfile();
74
+ if (!profileCheck.valid) {
75
+ log('warn', `profile 质量问题: ${profileCheck.issues.join('; ')}`);
76
+ }
77
+ log('ok', '项目扫描完成');
78
+ return { success: true, cost: result.cost };
79
+ }
80
+
81
+ if (attempt < maxAttempts) {
82
+ log('warn', '初始化未完成,将重试...');
83
+ }
84
+ }
85
+
86
+ log('error', `初始化失败:已重试 ${maxAttempts} 次,关键文件仍未生成`);
87
+ return { success: false, cost: null };
88
+ }
89
+
90
+ module.exports = {
91
+ executeScan,
92
+ validateProfile,
93
+ };
@@ -1,57 +1,271 @@
1
- 'use strict';
2
-
3
- const { loadSDK } = require('../common/sdk');
4
- const { writeSessionSeparator } = require('../common/logging');
5
- const { SessionContext } = require('./context');
6
-
7
- /**
8
- * 通用 Session 执行器
9
- * @param {string} type - session 类型
10
- * @param {object} config - 配置
11
- * @param {object} [config.externalCtx] - 外部传入的 SessionContext(共享日志和 indicator)
12
- */
13
- async function runSession(type, config) {
14
- const sdk = await loadSDK();
15
- const ctx = config.externalCtx || new SessionContext(type, config.opts);
16
-
17
- if (!config.externalCtx) {
18
- ctx.initLogging(config.logFileName, config.logStream);
19
- writeSessionSeparator(ctx.logStream, config.sessionNum || 0, config.label);
20
- }
21
-
22
- const stallTimeoutMin = ctx.initHooks(type);
23
- ctx.startIndicator(config.sessionNum || 0, stallTimeoutMin);
24
-
25
- try {
26
- const result = await config.execute(sdk, ctx);
27
- if (config.onSuccess) {
28
- await config.onSuccess(result, ctx);
29
- }
30
- // 只有非外部 ctx 才执行 finish
31
- if (!config.externalCtx) {
32
- ctx.finish();
33
- }
34
- return {
35
- exitCode: ctx.isStalled() ? 2 : 0,
36
- logFile: ctx.logFile,
37
- stalled: ctx.isStalled(),
38
- ...result,
39
- };
40
- } catch (err) {
41
- if (config.onError) {
42
- try { config.onError(err, ctx); } catch { /* ignore callback errors */ }
43
- }
44
- if (!config.externalCtx) {
45
- ctx.errorFinish(err);
46
- }
47
- return {
48
- exitCode: 1,
49
- error: err.message,
50
- logFile: ctx.logFile,
51
- };
52
- }
53
- }
54
-
55
- module.exports = {
56
- runSession,
57
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { buildEnvVars, log } = require('../common/config');
6
+ const { Indicator } = require('../common/indicator');
7
+ const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
8
+ const { createHooks } = require('./hooks');
9
+ const { assets } = require('../common/assets');
10
+
11
+ /**
12
+ * @typedef {Object} SessionRunOptions
13
+ * @property {string} logFileName - 日志文件名
14
+ * @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
15
+ * @property {number} [sessionNum=0] - 会话编号
16
+ * @property {string} [label=''] - 会话标签
17
+ * @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} QueryResult
22
+ * @property {Array<Object>} messages - 所有 SDK 消息
23
+ * @property {boolean} success - 是否成功完成
24
+ * @property {string|null} subtype - 结果子类型
25
+ * @property {number|null} cost - 美元费用
26
+ * @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
27
+ * @property {number|null} turns - 对话轮次
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} RunQueryOpts
32
+ * @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
33
+ */
34
+
35
+ /**
36
+ * SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
37
+ *
38
+ * 使用方式:
39
+ * ```js
40
+ * const result = await Session.run('coding', config, {
41
+ * logFileName: 'coding.log',
42
+ * async execute(session) {
43
+ * const queryOpts = session.buildQueryOptions();
44
+ * const { messages, success } = await session.runQuery(prompt, queryOpts);
45
+ * return { success };
46
+ * },
47
+ * });
48
+ * ```
49
+ */
50
+ class Session {
51
+ /** @type {Object|null} SDK 单例 */
52
+ static _sdk = null;
53
+
54
+ /**
55
+ * 确保 SDK 已加载(懒加载单例)
56
+ * @param {Object} config - 项目配置
57
+ * @returns {Promise<Object>} SDK 实例
58
+ */
59
+ static async ensureSDK(config) {
60
+ if (!Session._sdk) {
61
+ Object.assign(process.env, buildEnvVars(config));
62
+ const { loadSDK } = require('../common/sdk');
63
+ Session._sdk = await loadSDK();
64
+ }
65
+ return Session._sdk;
66
+ }
67
+
68
+ /**
69
+ * 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
70
+ * @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
71
+ * @param {Object} config - 项目配置
72
+ * @param {SessionRunOptions} options - 运行选项
73
+ * @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
74
+ */
75
+ static async run(type, config, { logFileName, logStream, sessionNum = 0, label = '', execute }) {
76
+ await Session.ensureSDK(config);
77
+ const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
78
+ try {
79
+ const result = await execute(session);
80
+ session.finish();
81
+ return {
82
+ exitCode: session.isStalled() ? 2 : 0,
83
+ logFile: session.logFile,
84
+ stalled: session.isStalled(),
85
+ ...result,
86
+ };
87
+ } catch (err) {
88
+ session.finish();
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {string} type - 会话类型
95
+ * @param {Object} config - 项目配置
96
+ * @param {Object} options
97
+ * @param {string} options.logFileName - 日志文件名
98
+ * @param {import('fs').WriteStream} [options.logStream] - 外部日志流
99
+ * @param {number} [options.sessionNum=0]
100
+ * @param {string} [options.label='']
101
+ */
102
+ constructor(type, config, { logFileName, logStream, sessionNum = 0, label = '' }) {
103
+ this.config = config;
104
+ this.type = type;
105
+ this.indicator = new Indicator();
106
+ /** @type {import('fs').WriteStream|null} */
107
+ this.logStream = null;
108
+ /** @type {string|null} */
109
+ this.logFile = null;
110
+ /** @type {Object|null} */
111
+ this.hooks = null;
112
+ /** @type {Function|null} */
113
+ this.cleanup = null;
114
+ this._isStalled = () => false;
115
+ this.abortController = new AbortController();
116
+
117
+ this._initLogging(logFileName, logStream);
118
+ writeSessionSeparator(this.logStream, sessionNum, label);
119
+ const stallTimeoutMin = this._initHooks(type);
120
+ this._startIndicator(sessionNum, stallTimeoutMin);
121
+ }
122
+
123
+ /**
124
+ * 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
125
+ * @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
126
+ * @returns {Object} SDK query options
127
+ */
128
+ buildQueryOptions(overrides = {}) {
129
+ const mode = overrides.permissionMode || 'bypassPermissions';
130
+ const base = {
131
+ permissionMode: mode,
132
+ cwd: overrides.projectRoot || assets.projectRoot,
133
+ env: buildEnvVars(this.config),
134
+ settingSources: ['project'],
135
+ hooks: this.hooks,
136
+ abortController: this.abortController,
137
+ };
138
+ if (mode === 'bypassPermissions') {
139
+ base.allowDangerouslySkipPermissions = true;
140
+ }
141
+ if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
142
+ if (overrides.model) base.model = overrides.model;
143
+ else if (this.config.model) base.model = this.config.model;
144
+ return base;
145
+ }
146
+
147
+ /**
148
+ * 执行一次 SDK 查询,遍历消息流并收集结果
149
+ * @param {string} prompt - 用户提示
150
+ * @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
151
+ * @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调)
152
+ * @returns {Promise<QueryResult>}
153
+ */
154
+ async runQuery(prompt, queryOpts, opts = {}) {
155
+ if (this.logStream?.writable) {
156
+ const sep = '-'.repeat(40);
157
+ if (queryOpts.systemPrompt) {
158
+ this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
159
+ }
160
+ this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
161
+ }
162
+
163
+ const sdk = Session._sdk;
164
+ const messages = [];
165
+ const querySession = sdk.query({ prompt, options: queryOpts });
166
+
167
+ for await (const message of querySession) {
168
+ if (this._isStalled()) {
169
+ log('warn', '停顿超时,中断消息循环');
170
+ break;
171
+ }
172
+ messages.push(message);
173
+ this._logMessage(message);
174
+
175
+ if (opts.onMessage) {
176
+ const action = opts.onMessage(message, messages);
177
+ if (action === 'break') break;
178
+ }
179
+ }
180
+
181
+ const sdkResult = extractResult(messages);
182
+ const cost = sdkResult?.total_cost_usd || null;
183
+ const usage = sdkResult?.usage || null;
184
+ const turns = sdkResult?.num_turns || null;
185
+
186
+ if (cost != null || turns != null) {
187
+ const parts = [];
188
+ if (turns != null) parts.push(`turns: ${turns}`);
189
+ if (cost != null) parts.push(`cost: $${cost}`);
190
+ if (usage) {
191
+ const inp = usage.input_tokens || 0;
192
+ const out = usage.output_tokens || 0;
193
+ parts.push(`tokens: ${inp}+${out}`);
194
+ }
195
+ const summary = parts.join(', ');
196
+ console.log('----- SESSION END -----');
197
+ log('info', `session 统计: ${summary}`);
198
+ if (this.logStream?.writable) {
199
+ this.logStream.write(`[SESSION_STATS] ${summary}\n`);
200
+ }
201
+ }
202
+
203
+ return {
204
+ messages,
205
+ success: sdkResult?.subtype === 'success',
206
+ subtype: sdkResult?.subtype || null,
207
+ cost, usage, turns,
208
+ };
209
+ }
210
+
211
+ /** 检查会话是否因停顿超时 */
212
+ isStalled() {
213
+ return this._isStalled();
214
+ }
215
+
216
+ /** 结束会话:清理 hooks、关闭日志流、停止 indicator */
217
+ finish() {
218
+ if (this.cleanup) this.cleanup();
219
+ if (this.logStream && !this._externalLogStream) this.logStream.end();
220
+ this.indicator.stop();
221
+ }
222
+
223
+ // ─── Private ────────────────────────────────────────────
224
+
225
+ _initLogging(logFileName, externalLogStream) {
226
+ if (externalLogStream) {
227
+ this.logStream = externalLogStream;
228
+ this._externalLogStream = true;
229
+ } else {
230
+ const logsDir = assets.dir('logs');
231
+ this.logFile = path.join(logsDir, logFileName);
232
+ this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
233
+ this._externalLogStream = false;
234
+ }
235
+ }
236
+
237
+ _initHooks(hookType) {
238
+ const stallTimeoutMs = this.config.stallTimeout * 1000;
239
+ const result = createHooks(hookType, this.indicator, this.logStream, {
240
+ stallTimeoutMs,
241
+ abortController: this.abortController,
242
+ editThreshold: this.config.editThreshold,
243
+ });
244
+ this.hooks = result.hooks;
245
+ this.cleanup = result.cleanup;
246
+ this._isStalled = result.isStalled;
247
+ return Math.floor(stallTimeoutMs / 60000);
248
+ }
249
+
250
+ _startIndicator(sessionNum, stallTimeoutMin) {
251
+ this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
252
+ }
253
+
254
+ _logMessage(message) {
255
+ const hasText = message.type === 'assistant'
256
+ && message.message?.content?.some(b => b.type === 'text' && b.text);
257
+
258
+ if (hasText && this.indicator) {
259
+ this.indicator.pauseRendering();
260
+ process.stderr.write('\r\x1b[K');
261
+ }
262
+
263
+ baseLogMessage(message, this.logStream, this.indicator);
264
+
265
+ if (hasText && this.indicator) {
266
+ this.indicator.resumeRendering();
267
+ }
268
+ }
269
+ }
270
+
271
+ module.exports = { Session };
@@ -1,53 +1,74 @@
1
- 'use strict';
2
-
3
- const { runSession } = require('./session');
4
- const { buildQueryOptions } = require('./query');
5
- const { log } = require('../common/config');
6
- const { assets } = require('../common/assets');
7
- const { execSync } = require('child_process');
8
-
9
- async function _runSimplifySession(n = 3, focus = null, opts = {}) {
10
- const projectRoot = assets.projectRoot;
11
- let diff = '';
12
- try {
13
- diff = execSync(`git diff HEAD~${n}..HEAD`, { cwd: projectRoot, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
14
- } catch (err) {
15
- log('warn', `无法获取最近 ${n} 个 commit 的 diff: ${err.message}`);
16
- }
17
-
18
- const focusLine = focus ? `\n审查聚焦方向:${focus}` : '';
19
- const prompt = `/simplify\n\n审查范围:最近 ${n} 个 commit${focusLine}\n\n${diff.slice(0, 50000)}`;
20
- const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
21
-
22
- return runSession('simplify', {
23
- opts,
24
- sessionNum: 0,
25
- logFileName: `simplify_${dateStr}.log`,
26
- label: 'simplify',
27
-
28
- async execute(sdk, ctx) {
29
- log('info', `正在审查最近 ${n} 个 commit 的代码变更...`);
30
-
31
- const queryOpts = buildQueryOptions(ctx.config, opts);
32
- queryOpts.hooks = ctx.hooks;
33
- queryOpts.abortController = ctx.abortController;
34
- queryOpts.disallowedTools = ['askUserQuestion'];
35
-
36
- await ctx.runQuery(sdk, prompt, queryOpts);
37
- log('ok', '代码审查完成');
38
-
39
- return {};
40
- },
41
- });
42
- }
43
-
44
- async function simplify(focus = null, opts = {}) {
45
- assets.ensureDirs();
46
- const n = opts.n || 3;
47
- return _runSimplifySession(n, focus, opts);
48
- }
49
-
50
- module.exports = {
51
- simplify,
52
- _runSimplifySession,
53
- };
1
+ 'use strict';
2
+
3
+ const { log } = require('../common/config');
4
+ const { assets } = require('../common/assets');
5
+ const { Session } = require('./session');
6
+ const { execSync } = require('child_process');
7
+
8
+ const AUTO_COMMIT_MSG = 'style: auto simplify';
9
+
10
+ function getSmartDiffRange(projectRoot, fallbackN) {
11
+ try {
12
+ const hash = execSync(
13
+ `git log --grep='${AUTO_COMMIT_MSG}' -1 --format='%H'`,
14
+ { cwd: projectRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
15
+ ).trim();
16
+ if (hash) return { range: `${hash}..HEAD`, label: `自上次 auto simplify 以来` };
17
+ } catch { /* ignore */ }
18
+ return { range: `HEAD~${fallbackN}..HEAD`, label: `最近 ${fallbackN} 个 commit` };
19
+ }
20
+
21
+ function commitIfDirty(projectRoot) {
22
+ try {
23
+ execSync('git diff --quiet HEAD', { cwd: projectRoot, stdio: 'pipe' });
24
+ } catch {
25
+ execSync(`git add -A && git commit -m "${AUTO_COMMIT_MSG}"`, { cwd: projectRoot, stdio: 'pipe' });
26
+ log('ok', `代码优化已提交: ${AUTO_COMMIT_MSG}`);
27
+ }
28
+ }
29
+
30
+ async function executeSimplify(config, focus = null, opts = {}) {
31
+ const n = opts.n || 3;
32
+ const projectRoot = assets.projectRoot;
33
+
34
+ const { range, label } = getSmartDiffRange(projectRoot, n);
35
+
36
+ let diff = '';
37
+ try {
38
+ diff = execSync(`git diff ${range}`, { cwd: projectRoot, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
39
+ } catch (err) {
40
+ log('warn', `无法获取 diff (${label}): ${err.message}`);
41
+ }
42
+
43
+ if (!diff.trim()) {
44
+ log('info', `无变更需要审查 (${label})`);
45
+ return { success: true };
46
+ }
47
+
48
+ const focusLine = focus ? `\n审查聚焦方向:${focus}` : '';
49
+ const prompt = `/simplify\n\n审查范围:${label}${focusLine}\n\n${diff.slice(0, 50000)}`;
50
+ const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
51
+
52
+ const result = await Session.run('simplify', config, {
53
+ logFileName: `simplify_${dateStr}.log`,
54
+ label: 'simplify',
55
+
56
+ async execute(session) {
57
+ log('info', `正在审查代码变更 (${label})...`);
58
+
59
+ const queryOpts = session.buildQueryOptions(opts);
60
+ queryOpts.disallowedTools = ['askUserQuestion'];
61
+
62
+ await session.runQuery(prompt, queryOpts);
63
+ log('ok', '代码审查完成');
64
+
65
+ return {};
66
+ },
67
+ });
68
+
69
+ commitIfDirty(projectRoot);
70
+
71
+ return result;
72
+ }
73
+
74
+ module.exports = { executeSimplify };