closer-code 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.
Files changed (100) hide show
  1. package/.env.example +83 -0
  2. package/API_GUIDE.md +1411 -0
  3. package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
  4. package/CLAUDE.md +55 -0
  5. package/CTRL_C_EXPERIMENT.md +90 -0
  6. package/PROJECT_CLEANUP_SUMMARY.md +121 -0
  7. package/README.md +686 -0
  8. package/cloco.md +51 -0
  9. package/config.example.json +116 -0
  10. package/dist/bash-runner.js +128 -0
  11. package/dist/batch-cli.js +20736 -0
  12. package/dist/closer-cli.js +21190 -0
  13. package/dist/index.js +31228 -0
  14. package/docs/EXPORT_COMMAND.md +152 -0
  15. package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
  16. package/docs/GLOBAL_CONFIG.md +128 -0
  17. package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
  18. package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
  19. package/docs/QUICK_START_HISTORY.md +207 -0
  20. package/docs/TASK_PROGRESS_FEATURE.md +190 -0
  21. package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
  22. package/docs/THINKING_FEATURE.md +187 -0
  23. package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
  24. package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
  25. package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
  26. package/docs/WHY_THINKING_SHORT.md +201 -0
  27. package/package.json +49 -0
  28. package/scenarios/README.md +234 -0
  29. package/scenarios/run-all-scenarios.js +342 -0
  30. package/scenarios/scenario1-batch-converter.js +247 -0
  31. package/scenarios/scenario2-code-analyzer.js +375 -0
  32. package/scenarios/scenario3-doc-generator.js +371 -0
  33. package/scenarios/scenario4-log-analyzer.js +496 -0
  34. package/scenarios/scenario5-tdd-helper.js +681 -0
  35. package/src/ai-client-legacy.js +171 -0
  36. package/src/ai-client.js +221 -0
  37. package/src/bash-runner.js +148 -0
  38. package/src/batch-cli.js +327 -0
  39. package/src/cli.jsx +166 -0
  40. package/src/closer-cli.jsx +1103 -0
  41. package/src/closer-cli.jsx.backup +948 -0
  42. package/src/commands/batch.js +62 -0
  43. package/src/commands/chat.js +10 -0
  44. package/src/commands/config.js +154 -0
  45. package/src/commands/help.js +76 -0
  46. package/src/commands/history.js +192 -0
  47. package/src/commands/setup.js +17 -0
  48. package/src/commands/upgrade.js +101 -0
  49. package/src/commands/workflow-tests.js +125 -0
  50. package/src/config.js +343 -0
  51. package/src/conversation.js +962 -0
  52. package/src/git-helper.js +349 -0
  53. package/src/index.js +88 -0
  54. package/src/logger.js +347 -0
  55. package/src/plan.js +193 -0
  56. package/src/planner.js +397 -0
  57. package/src/search.js +195 -0
  58. package/src/setup.js +147 -0
  59. package/src/shortcuts.js +269 -0
  60. package/src/snippets.js +430 -0
  61. package/src/test-modules.js +118 -0
  62. package/src/tools.js +398 -0
  63. package/src/utils/cli.js +124 -0
  64. package/src/utils/validator.js +184 -0
  65. package/src/utils/version.js +33 -0
  66. package/src/utils/workflow-test.js +271 -0
  67. package/src/utils/workflow.js +268 -0
  68. package/test/demo-file-naming.js +92 -0
  69. package/test/demo-thinking.js +124 -0
  70. package/test/final-verification-report.md +303 -0
  71. package/test/research-thinking.js +130 -0
  72. package/test/test-auto-mkdir.js +123 -0
  73. package/test/test-e2e-empty-dir.md +108 -0
  74. package/test/test-export-logic.js +119 -0
  75. package/test/test-global-cloco.js +126 -0
  76. package/test/test-history-isolation.js +291 -0
  77. package/test/test-improved-thinking.js +43 -0
  78. package/test/test-long-message.js +65 -0
  79. package/test/test-plan-functionality.js +95 -0
  80. package/test/test-real-scenario.js +216 -0
  81. package/test/test-thinking-display.js +65 -0
  82. package/test/ui-verification-test.js +203 -0
  83. package/test/verify-history-isolation.sh +71 -0
  84. package/test/verify-thinking.js +339 -0
  85. package/test/workflows/empty-dir-creation.md +51 -0
  86. package/test/workflows/inventor/ascii-teacup.js +199 -0
  87. package/test/workflows/inventor/ascii-teacup.mjs +199 -0
  88. package/test/workflows/inventor/ascii_apple.hs +84 -0
  89. package/test/workflows/inventor/ascii_apple.py +91 -0
  90. package/test/workflows/inventor/cloco.md +3 -0
  91. package/test/workflows/longtalk/cloco.md +19 -0
  92. package/test/workflows/longtalk/emoji_500.txt +63 -0
  93. package/test/workflows/longtalk/emoji_list.txt +20 -0
  94. package/test/workflows/programmer/adder.md +33 -0
  95. package/test/workflows/programmer/expect.md +2 -0
  96. package/test/workflows/programmer/prompt.md +3 -0
  97. package/test/workflows/test-empty-dir-creation.js +113 -0
  98. package/test-ctrl-c.jsx +126 -0
  99. package/test-manual-file-creation.js +151 -0
  100. package/winfix.md +3 -0
package/src/tools.js ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * 工具执行引擎(使用 SDK)
3
+ *
4
+ * 使用 @anthropic-ai/sdk 的 betaZodTool 和 Zod 来定义工具
5
+ * 优势:
6
+ * - 类型安全的工具定义
7
+ * - 自动 schema 验证
8
+ * - SDK 自动处理工具调用循环(toolRunner)
9
+ * - 无需手工解析工具调用
10
+ */
11
+
12
+ import { z } from 'zod';
13
+ import { betaZodTool } from '@anthropic-ai/sdk/helpers/beta/zod';
14
+ import fs from 'fs/promises';
15
+ import path from 'path';
16
+ import { executeBashCommand } from './bash-runner.js';
17
+ import { glob } from 'glob';
18
+
19
+ /**
20
+ * 创建一个配置上下文,用于工具执行器
21
+ */
22
+ let toolExecutorContext = null;
23
+
24
+ export function setToolExecutorContext(config) {
25
+ toolExecutorContext = {
26
+ workingDir: config.behavior.workingDir,
27
+ enabledTools: new Set(config.tools.enabled)
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Bash 工具 - 执行 bash 命令
33
+ */
34
+ export const bashTool = betaZodTool({
35
+ name: 'bash',
36
+ description: 'Execute a bash shell command. Use this IMMEDIATELY when user asks to: list/show directory contents (ls, dir), run commands, execute tests, check file info, run git commands, or ANY terminal operation. DO NOT just say "I will check" - CALL THIS TOOL.',
37
+ inputSchema: z.object({
38
+ command: z.string().describe('The bash command to execute (e.g., "ls -la", "cat file.txt", "npm test", "git status")'),
39
+ timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000)')
40
+ }),
41
+ run: async (input) => {
42
+ if (!toolExecutorContext) {
43
+ throw new Error('Tool executor context not initialized');
44
+ }
45
+
46
+ const result = await executeBashCommand(input.command, {
47
+ cwd: toolExecutorContext.workingDir,
48
+ timeout: input.timeout || 30000
49
+ });
50
+
51
+ if (result.success) {
52
+ return JSON.stringify({
53
+ success: true,
54
+ stdout: result.stdout,
55
+ stderr: result.stderr,
56
+ exitCode: result.exitCode
57
+ });
58
+ } else {
59
+ return JSON.stringify({
60
+ success: false,
61
+ error: result.error,
62
+ stdout: result.stdout,
63
+ stderr: result.stderr
64
+ });
65
+ }
66
+ }
67
+ });
68
+
69
+ /**
70
+ * 读取文件工具
71
+ */
72
+ export const readFileTool = betaZodTool({
73
+ name: 'readFile',
74
+ description: 'Read the contents of a file',
75
+ inputSchema: z.object({
76
+ filePath: z.string().describe('Absolute or relative path to the file'),
77
+ encoding: z.string().optional().describe('File encoding (default: utf-8)')
78
+ }),
79
+ run: async (input) => {
80
+ if (!toolExecutorContext) {
81
+ throw new Error('Tool executor context not initialized');
82
+ }
83
+
84
+ const fullPath = path.resolve(toolExecutorContext.workingDir, input.filePath);
85
+ const content = await fs.readFile(fullPath, input.encoding || 'utf-8');
86
+
87
+ return JSON.stringify({
88
+ success: true,
89
+ content,
90
+ path: fullPath
91
+ });
92
+ }
93
+ });
94
+
95
+ /**
96
+ * 写入文件工具
97
+ */
98
+ export const writeFileTool = betaZodTool({
99
+ name: 'writeFile',
100
+ description: 'Write content to a file (creates or overwrites). Supports both plain text content and base64-encoded content. Returns detailed error messages if the operation fails, allowing you to analyze and fix the issue.',
101
+ inputSchema: z.object({
102
+ filePath: z.string().describe('Absolute or relative path to the file'),
103
+ content: z.string().optional().describe('Content to write to the file (plain text)'),
104
+ contentBase64: z.string().optional().describe('Content to write to the file (base64 encoded, use this for binary data or special characters)'),
105
+ encoding: z.string().optional().describe('File encoding (default: utf-8)')
106
+ }),
107
+ run: async (input) => {
108
+ if (!toolExecutorContext) {
109
+ throw new Error('Tool executor context not initialized');
110
+ }
111
+
112
+ const fullPath = path.resolve(toolExecutorContext.workingDir, input.filePath);
113
+
114
+ // 优先使用 contentBase64,否则使用 content
115
+ let dataToWrite;
116
+ if (input.contentBase64) {
117
+ dataToWrite = Buffer.from(input.contentBase64, 'base64');
118
+ } else if (input.content !== undefined) {
119
+ dataToWrite = input.content;
120
+ } else {
121
+ return JSON.stringify({
122
+ success: false,
123
+ error: 'Either content or contentBase64 must be provided'
124
+ });
125
+ }
126
+
127
+ try {
128
+ await fs.writeFile(fullPath, dataToWrite, input.encoding || 'utf-8');
129
+
130
+ return JSON.stringify({
131
+ success: true,
132
+ path: fullPath,
133
+ size: dataToWrite.length,
134
+ encoding: input.contentBase64 ? 'base64' : (input.encoding || 'utf-8')
135
+ });
136
+ } catch (error) {
137
+ // 提供详细的错误信息和修复建议
138
+ let errorDetail = {
139
+ success: false,
140
+ error: error.code,
141
+ message: error.message,
142
+ path: fullPath
143
+ };
144
+
145
+ // 针对常见错误提供修复建议
146
+ if (error.code === 'ENOENT') {
147
+ const parentDir = path.dirname(fullPath);
148
+ errorDetail.suggestion = `Parent directory does not exist. Create it first using: bash tool with "mkdir -p ${parentDir}"`;
149
+ errorDetail.hint = 'The parent directory needs to be created before writing the file.';
150
+ } else if (error.code === 'EACCES') {
151
+ errorDetail.suggestion = 'Permission denied. Check if you have write permissions for this location.';
152
+ errorDetail.hint = 'Try writing to a different location or check file permissions.';
153
+ } else if (error.code === 'ENOSPC') {
154
+ errorDetail.suggestion = 'No space left on device. Free up some disk space and retry.';
155
+ }
156
+
157
+ return JSON.stringify(errorDetail);
158
+ }
159
+ }
160
+ });
161
+
162
+ /**
163
+ * 编辑文件工具(替换文本)
164
+ */
165
+ export const editFileTool = betaZodTool({
166
+ name: 'editFile',
167
+ description: 'Edit a file by replacing exact string matches',
168
+ inputSchema: z.object({
169
+ filePath: z.string().describe('Path to the file to edit'),
170
+ oldText: z.string().describe('Exact text to replace (must be unique in the file)'),
171
+ newText: z.string().describe('New text to replace with'),
172
+ replaceAll: z.boolean().optional().describe('Replace all occurrences (default: false)')
173
+ }),
174
+ run: async (input) => {
175
+ if (!toolExecutorContext) {
176
+ throw new Error('Tool executor context not initialized');
177
+ }
178
+
179
+ const fullPath = path.resolve(toolExecutorContext.workingDir, input.filePath);
180
+ let content = await fs.readFile(fullPath, 'utf-8');
181
+
182
+ if (input.replaceAll) {
183
+ content = content.split(input.oldText).join(input.newText);
184
+ } else {
185
+ if (!content.includes(input.oldText)) {
186
+ return JSON.stringify({
187
+ success: false,
188
+ error: 'Old text not found in file'
189
+ });
190
+ }
191
+ content = content.replace(input.oldText, input.newText);
192
+ }
193
+
194
+ await fs.writeFile(fullPath, content, 'utf-8');
195
+
196
+ return JSON.stringify({
197
+ success: true,
198
+ path: fullPath,
199
+ replacements: 1
200
+ });
201
+ }
202
+ });
203
+
204
+ /**
205
+ * 搜索文件工具
206
+ */
207
+ export const searchFilesTool = betaZodTool({
208
+ name: 'searchFiles',
209
+ description: 'Search for files by name pattern using glob',
210
+ inputSchema: z.object({
211
+ pattern: z.string().describe('Glob pattern (e.g., "**/*.js", "src/**/*.ts")'),
212
+ cwd: z.string().optional().describe('Working directory (default: current directory)')
213
+ }),
214
+ run: async (input) => {
215
+ if (!toolExecutorContext) {
216
+ throw new Error('Tool executor context not initialized');
217
+ }
218
+
219
+ const searchDir = input.cwd
220
+ ? path.resolve(toolExecutorContext.workingDir, input.cwd)
221
+ : toolExecutorContext.workingDir;
222
+
223
+ const files = await glob(input.pattern, { cwd: searchDir });
224
+
225
+ return JSON.stringify({
226
+ success: true,
227
+ files,
228
+ count: files.length
229
+ });
230
+ }
231
+ });
232
+
233
+ /**
234
+ * 搜索代码内容工具
235
+ */
236
+ export const searchCodeTool = betaZodTool({
237
+ name: 'searchCode',
238
+ description: 'Search for text/patterns in file contents',
239
+ inputSchema: z.object({
240
+ pattern: z.string().describe('Regex pattern to search for'),
241
+ path: z.string().optional().describe('Directory to search in (default: current directory)'),
242
+ fileType: z.string().optional().describe('Filter by file type (e.g., "js", "py")')
243
+ }),
244
+ run: async (input) => {
245
+ if (!toolExecutorContext) {
246
+ throw new Error('Tool executor context not initialized');
247
+ }
248
+
249
+ const { searchCode } = await import('./search.js');
250
+ const results = await searchCode(input.pattern, {
251
+ path: input.path
252
+ ? path.resolve(toolExecutorContext.workingDir, input.path)
253
+ : toolExecutorContext.workingDir,
254
+ type: input.fileType
255
+ });
256
+
257
+ return JSON.stringify({
258
+ success: true,
259
+ ...results
260
+ });
261
+ }
262
+ });
263
+
264
+ /**
265
+ * 列出目录工具
266
+ */
267
+ export const listFilesTool = betaZodTool({
268
+ name: 'listFiles',
269
+ description: 'List files and directories in a path',
270
+ inputSchema: z.object({
271
+ dirPath: z.string().optional().describe('Directory path (default: current directory)'),
272
+ recursive: z.boolean().optional().describe('List recursively (default: false)'),
273
+ showHidden: z.boolean().optional().describe('Show hidden files (default: false)')
274
+ }),
275
+ run: async (input) => {
276
+ if (!toolExecutorContext) {
277
+ throw new Error('Tool executor context not initialized');
278
+ }
279
+
280
+ const fullPath = input.dirPath
281
+ ? path.resolve(toolExecutorContext.workingDir, input.dirPath)
282
+ : toolExecutorContext.workingDir;
283
+
284
+ const files = await fs.readdir(fullPath, { withFileTypes: true });
285
+
286
+ const result = [];
287
+ for (const file of files) {
288
+ if (!input.showHidden && file.name.startsWith('.')) continue;
289
+ result.push({
290
+ name: file.name,
291
+ type: file.isDirectory() ? 'directory' : 'file',
292
+ path: path.join(fullPath, file.name)
293
+ });
294
+ }
295
+
296
+ return JSON.stringify({
297
+ success: true,
298
+ files: result,
299
+ path: fullPath
300
+ });
301
+ }
302
+ });
303
+
304
+ /**
305
+ * 所有工具的导出映射
306
+ */
307
+ const TOOLS_MAP = {
308
+ bash: bashTool,
309
+ readFile: readFileTool,
310
+ writeFile: writeFileTool,
311
+ editFile: editFileTool,
312
+ searchFiles: searchFilesTool,
313
+ searchCode: searchCodeTool,
314
+ listFiles: listFilesTool
315
+ };
316
+
317
+ /**
318
+ * 获取启用工具的数组(用于 toolRunner)
319
+ * @param {Array<string>} enabledTools - 启用的工具名称数组
320
+ * @returns {Array} betaZodTool 对象数组
321
+ */
322
+ export function getToolDefinitions(enabledTools) {
323
+ return enabledTools
324
+ .map(toolName => TOOLS_MAP[toolName])
325
+ .filter(tool => tool !== undefined);
326
+ }
327
+
328
+ /**
329
+ * 获取工具的 JSON Schema 定义(用于兼容性)
330
+ * @param {Array<string>} enabledTools - 启用的工具名称数组
331
+ * @returns {Array} JSON Schema 格式的工具定义
332
+ */
333
+ export function getToolSchemaDefinitions(enabledTools) {
334
+ return enabledTools
335
+ .filter(toolName => TOOLS_MAP[toolName] !== undefined)
336
+ .map(toolName => {
337
+ const tool = TOOLS_MAP[toolName];
338
+ return {
339
+ name: tool.name,
340
+ description: tool.description,
341
+ inputSchema: tool.input_schema
342
+ };
343
+ });
344
+ }
345
+
346
+ /**
347
+ * 生成工具执行的简短摘要
348
+ * @param {string} toolName - 工具名称
349
+ * @param {Object} input - 工具输入参数
350
+ * @param {Object} result - 工具执行结果
351
+ * @returns {string} 简短摘要
352
+ */
353
+ export function generateToolSummary(toolName, input, result) {
354
+ const success = result?.success;
355
+
356
+ switch (toolName) {
357
+ case 'bash':
358
+ const cmd = input.command || '';
359
+ // 提取命令和第一个参数
360
+ const parts = cmd.trim().split(/\s+/);
361
+ const command = parts[0] || 'bash';
362
+ const arg1 = parts[1] ? parts[1].substring(0, 20) : '';
363
+ return success ? `✓ ${command} ${arg1}` : `✗ ${command}`;
364
+
365
+ case 'readFile':
366
+ const filePath = input.filePath || '';
367
+ const fileName = filePath.split('/').pop().substring(0, 20);
368
+ return success ? `📖 ${fileName}` : `✗ ${fileName}`;
369
+
370
+ case 'writeFile':
371
+ const writePath = input.filePath || '';
372
+ const writeFileName = writePath.split('/').pop().substring(0, 20);
373
+ return success ? `✍️ ${writeFileName}` : `✗ ${writeFileName}`;
374
+
375
+ case 'editFile':
376
+ const editPath = input.filePath || '';
377
+ const editFileName = editPath.split('/').pop().substring(0, 20);
378
+ return success ? `✏️ ${editFileName}` : `✗ ${editFileName}`;
379
+
380
+ case 'searchFiles':
381
+ const pattern = input.pattern || '';
382
+ const shortPattern = pattern.substring(0, 15);
383
+ return success ? `🔍 ${shortPattern}` : `✗ search`;
384
+
385
+ case 'searchCode':
386
+ const query = input.query || '';
387
+ const shortQuery = query.substring(0, 15);
388
+ return success ? `🔎 ${shortQuery}` : `✗ search`;
389
+
390
+ case 'listFiles':
391
+ const dirPath = input.path || '.';
392
+ const dirName = dirPath.split('/').pop() || '.';
393
+ return success ? `📁 ${dirName}` : `✗ ${dirName}`;
394
+
395
+ default:
396
+ return success ? `✓ ${toolName}` : `✗ ${toolName}`;
397
+ }
398
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CLI 工具函数
3
+ * 提供命令行参数解析、输出格式化等功能
4
+ */
5
+
6
+ // 特殊命令列表(不以 - 开头的非选项参数)
7
+ export const SPECIAL_COMMANDS = ['config', 'setup', 'upgrade', 'help', 'version', 'workflow-tests'];
8
+
9
+ /**
10
+ * 解析命令行参数
11
+ * @param {string[]} argv - 命令行参数数组(不包括 node 和脚本名)
12
+ * @returns {Object} 解析结果 { options, args, specialCommand }
13
+ */
14
+ export function parseOptions(argv) {
15
+ const options = {
16
+ help: false,
17
+ version: false,
18
+ batch: false,
19
+ b: false,
20
+ json: false,
21
+ file: null,
22
+ verbose: false,
23
+ debug: false,
24
+ all: false, // 运行所有 workflow 测试
25
+ workflow: null, // workflow 模式:workflow 案例路径
26
+ maxIterations: null, // 最大迭代次数
27
+ };
28
+ const args = [];
29
+ let specialCommand = null;
30
+
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const arg = argv[i];
33
+
34
+ // 处理选项
35
+ if (arg === '-h' || arg === '--help') {
36
+ options.help = true;
37
+ } else if (arg === '-v' || arg === '--version') {
38
+ options.version = true;
39
+ } else if (arg === '-b' || arg === '--batch') {
40
+ options.batch = true;
41
+ options.b = true;
42
+ } else if (arg === '-j' || arg === '--json') {
43
+ options.json = true;
44
+ } else if (arg === '-f' || arg === '--file') {
45
+ options.file = argv[++i] || null;
46
+ } else if (arg === '--verbose') {
47
+ options.verbose = true;
48
+ } else if (arg === '-d' || arg === '--debug') {
49
+ options.debug = true;
50
+ } else if (arg === '--all') {
51
+ // 运行所有 workflow 测试
52
+ options.all = true;
53
+ } else if (arg === '--workflow' || arg === '-w') {
54
+ // workflow 模式
55
+ options.workflow = argv[++i] || null;
56
+ // workflow 模式隐含 batch 模式
57
+ options.batch = true;
58
+ options.b = true;
59
+ } else if (arg === '--max-iterations') {
60
+ // 最大迭代次数
61
+ options.maxIterations = argv[++i] || null;
62
+ } else if (arg.startsWith('-')) {
63
+ // 其他选项(例如 --key value 或 -k value)
64
+ const key = arg.replace(/^-+/, '');
65
+ const nextArg = argv[i + 1];
66
+
67
+ // 如果下一个参数存在且不是选项,则作为值
68
+ if (nextArg !== undefined && !nextArg.startsWith('-')) {
69
+ options[key] = nextArg;
70
+ i++; // 跳过下一个参数
71
+ } else {
72
+ options[key] = true;
73
+ }
74
+ } else if (!specialCommand && SPECIAL_COMMANDS.includes(arg)) {
75
+ // 特殊命令(只在第一个非选项位置)
76
+ specialCommand = arg;
77
+ } else {
78
+ // 普通参数(提示词等)
79
+ args.push(arg);
80
+ }
81
+ }
82
+
83
+ return { options, args, specialCommand };
84
+ }
85
+
86
+ /**
87
+ * 显示错误信息
88
+ * @param {string} message - 错误信息
89
+ */
90
+ export function showError(message) {
91
+ console.error(`❌ ${message}`);
92
+ }
93
+
94
+ /**
95
+ * 显示提示信息
96
+ * @param {string} message - 提示信息
97
+ */
98
+ export function showTip(message) {
99
+ console.log(`💡 ${message}`);
100
+ }
101
+
102
+ /**
103
+ * 显示成功信息
104
+ * @param {string} message - 成功信息
105
+ */
106
+ export function showSuccess(message) {
107
+ console.log(`✅ ${message}`);
108
+ }
109
+
110
+ /**
111
+ * 显示信息
112
+ * @param {string} message - 信息内容
113
+ */
114
+ export function showInfo(message) {
115
+ console.log(`ℹ️ ${message}`);
116
+ }
117
+
118
+ /**
119
+ * 显示警告信息
120
+ * @param {string} message - 警告信息
121
+ */
122
+ export function showWarning(message) {
123
+ console.warn(`⚠️ ${message}`);
124
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * 验收标准验证器
3
+ * 解析 expect.md 并验证验收标准
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+
9
+ /**
10
+ * 解析验收标准
11
+ */
12
+ function parseExpect(expectContent) {
13
+ // 移除 markdown 标题符号和空格
14
+ const checks = expectContent
15
+ .split('\n')
16
+ .map(line => line.trim())
17
+ .map(line => line.replace(/^#+\s*/, '')) // 移除 # 标题符号
18
+ .filter(line => line.length > 0);
19
+
20
+ return checks;
21
+ }
22
+
23
+ /**
24
+ * 检查单个条件
25
+ */
26
+ async function checkSingleCondition(condition, workingDir) {
27
+ // 1. 文件存在性检查
28
+ // 匹配模式:"有一个 [文件名] 程序" 或 "存在 [文件名]"
29
+ if (condition.includes('有一个') && condition.includes('程序')) {
30
+ const match = condition.match(/(\w+)\s*程序/);
31
+ if (match) {
32
+ const programName = match[1];
33
+ const programPath = path.join(workingDir, programName);
34
+
35
+ try {
36
+ await fs.access(programPath, fs.constants.X_OK); // 检查可执行权限
37
+ return {
38
+ passed: true,
39
+ check: condition,
40
+ reason: `程序 ${programName} 存在且可执行`
41
+ };
42
+ } catch {
43
+ return {
44
+ passed: false,
45
+ check: condition,
46
+ reason: `程序 ${programName} 不存在或不可执行`
47
+ };
48
+ }
49
+ }
50
+ }
51
+
52
+ // 2. 文件存在性检查
53
+ // 匹配模式:"存在 [文件名]" 或 "有 [文件名]"
54
+ if (condition.includes('存在') || condition.includes('有')) {
55
+ // 尝试提取文件名
56
+ const match = condition.match(/(?:存在|有)\s+["']?([^"'\s]+)["']?/);
57
+ if (match) {
58
+ const fileName = match[1];
59
+ const filePath = path.join(workingDir, fileName);
60
+
61
+ try {
62
+ await fs.access(filePath, fs.constants.F_OK);
63
+ return {
64
+ passed: true,
65
+ check: condition,
66
+ reason: `文件 ${fileName} 存在`
67
+ };
68
+ } catch {
69
+ return {
70
+ passed: false,
71
+ check: condition,
72
+ reason: `文件 ${fileName} 不存在`
73
+ };
74
+ }
75
+ }
76
+ }
77
+
78
+ // 3. 可执行文件检查
79
+ // 匹配模式:"可执行的 [文件名]" 或 "[文件名] 可执行"
80
+ if (condition.includes('可执行')) {
81
+ const match = condition.match(/可执行的?\s*(\w+)/);
82
+ if (match) {
83
+ const fileName = match[1];
84
+ const filePath = path.join(workingDir, fileName);
85
+
86
+ try {
87
+ await fs.access(filePath, fs.constants.X_OK);
88
+ return {
89
+ passed: true,
90
+ check: condition,
91
+ reason: `文件 ${fileName} 可执行`
92
+ };
93
+ } catch {
94
+ return {
95
+ passed: false,
96
+ check: condition,
97
+ reason: `文件 ${fileName} 不存在或不可执行`
98
+ };
99
+ }
100
+ }
101
+ }
102
+
103
+ // 4. 目录存在性检查
104
+ // 匹配模式:"[目录名] 目录" 或 "目录 [目录名]"
105
+ if (condition.includes('目录')) {
106
+ const match = condition.match(/(\w+)\s*目录/);
107
+ if (match) {
108
+ const dirName = match[1];
109
+ const dirPath = path.join(workingDir, dirName);
110
+
111
+ try {
112
+ const stat = await fs.stat(dirPath);
113
+ if (stat.isDirectory()) {
114
+ return {
115
+ passed: true,
116
+ check: condition,
117
+ reason: `目录 ${dirName} 存在`
118
+ };
119
+ } else {
120
+ return {
121
+ passed: false,
122
+ check: condition,
123
+ reason: `${dirName} 不是目录`
124
+ };
125
+ }
126
+ } catch {
127
+ return {
128
+ passed: false,
129
+ check: condition,
130
+ reason: `目录 ${dirName} 不存在`
131
+ };
132
+ }
133
+ }
134
+ }
135
+
136
+ // 5. 默认:无法识别的条件,假设通过
137
+ // 这允许 future 扩展更多验证逻辑
138
+ return {
139
+ passed: true,
140
+ check: condition,
141
+ reason: '未实现验证逻辑,默认通过'
142
+ };
143
+ }
144
+
145
+ /**
146
+ * 验证验收标准
147
+ */
148
+ export async function validateExpect(expectContent, workingDir, verbose = false) {
149
+ const checks = parseExpect(expectContent);
150
+
151
+ if (verbose) {
152
+ console.log(`\n解析到 ${checks.length} 条验收标准:`);
153
+ checks.forEach((check, index) => {
154
+ console.log(` ${index + 1}. ${check}`);
155
+ });
156
+ }
157
+
158
+ const results = [];
159
+ let allPassed = true;
160
+
161
+ for (const check of checks) {
162
+ const result = await checkSingleCondition(check, workingDir);
163
+ results.push(result);
164
+
165
+ if (verbose) {
166
+ console.log(` ${result.passed ? '✓' : '✗'} ${check}`);
167
+ if (!result.passed) {
168
+ console.log(` 原因: ${result.reason}`);
169
+ }
170
+ }
171
+
172
+ if (!result.passed) {
173
+ allPassed = false;
174
+ // 遇到第一个失败就停止(AND 逻辑)
175
+ break;
176
+ }
177
+ }
178
+
179
+ return {
180
+ passed: allPassed,
181
+ reason: allPassed ? '所有验收标准已满足' : results.find(r => !r.passed)?.reason || '验证失败',
182
+ details: results
183
+ };
184
+ }