@angli/unit-test-tool 0.1.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 (151) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/README.md +232 -0
  3. package/dist/src/cli/commands/analyze.js +20 -0
  4. package/dist/src/cli/commands/guard.js +26 -0
  5. package/dist/src/cli/commands/init.js +39 -0
  6. package/dist/src/cli/commands/run.js +72 -0
  7. package/dist/src/cli/commands/schedule.js +29 -0
  8. package/dist/src/cli/commands/start.js +102 -0
  9. package/dist/src/cli/commands/status.js +27 -0
  10. package/dist/src/cli/commands/verify.js +15 -0
  11. package/dist/src/cli/context/create-context.js +101 -0
  12. package/dist/src/cli/index.js +23 -0
  13. package/dist/src/cli/utils/scan-dir.js +6 -0
  14. package/dist/src/core/analyzers/coverage-analyzer.js +8 -0
  15. package/dist/src/core/analyzers/dependency-complexity-analyzer.js +19 -0
  16. package/dist/src/core/analyzers/existing-test-analyzer.js +66 -0
  17. package/dist/src/core/analyzers/failure-history-analyzer.js +9 -0
  18. package/dist/src/core/analyzers/file-classifier-analyzer.js +24 -0
  19. package/dist/src/core/analyzers/index.js +37 -0
  20. package/dist/src/core/analyzers/llm-semantic-analyzer.js +3 -0
  21. package/dist/src/core/analyzers/path-priority-analyzer.js +34 -0
  22. package/dist/src/core/coverage/read-coverage-summary.js +183 -0
  23. package/dist/src/core/executor/claude-cli-executor.js +91 -0
  24. package/dist/src/core/middleware/loop-detection.js +6 -0
  25. package/dist/src/core/middleware/pre-completion-checklist.js +27 -0
  26. package/dist/src/core/middleware/silent-success-post-check.js +13 -0
  27. package/dist/src/core/planner/rank-candidates.js +3 -0
  28. package/dist/src/core/planner/rule-planner.js +41 -0
  29. package/dist/src/core/planner/score-candidate.js +49 -0
  30. package/dist/src/core/prompts/case-library.js +35 -0
  31. package/dist/src/core/prompts/edit-boundary-prompt.js +24 -0
  32. package/dist/src/core/prompts/retry-prompt.js +18 -0
  33. package/dist/src/core/prompts/system-prompt.js +27 -0
  34. package/dist/src/core/prompts/task-prompt.js +22 -0
  35. package/dist/src/core/reporter/index.js +48 -0
  36. package/dist/src/core/state-machine/index.js +12 -0
  37. package/dist/src/core/storage/defaults.js +16 -0
  38. package/dist/src/core/storage/event-store.js +16 -0
  39. package/dist/src/core/storage/lifecycle-store.js +18 -0
  40. package/dist/src/core/storage/report-store.js +16 -0
  41. package/dist/src/core/storage/state-store.js +11 -0
  42. package/dist/src/core/strategies/classify-failure.js +14 -0
  43. package/dist/src/core/strategies/switch-mock-strategy.js +9 -0
  44. package/dist/src/core/tools/analyze-baseline.js +54 -0
  45. package/dist/src/core/tools/guard.js +68 -0
  46. package/dist/src/core/tools/run-loop.js +108 -0
  47. package/dist/src/core/tools/run-with-claude-cli.js +645 -0
  48. package/dist/src/core/tools/verify-all.js +75 -0
  49. package/dist/src/core/worktrees/is-git-repo.js +10 -0
  50. package/dist/src/types/index.js +1 -0
  51. package/dist/src/types/logger.js +1 -0
  52. package/dist/src/utils/clock.js +10 -0
  53. package/dist/src/utils/command-runner.js +18 -0
  54. package/dist/src/utils/commands.js +28 -0
  55. package/dist/src/utils/duration.js +22 -0
  56. package/dist/src/utils/fs.js +53 -0
  57. package/dist/src/utils/logger.js +10 -0
  58. package/dist/src/utils/paths.js +21 -0
  59. package/dist/src/utils/process-lifecycle.js +74 -0
  60. package/dist/src/utils/prompts.js +20 -0
  61. package/dist/tests/core/create-context.test.js +41 -0
  62. package/dist/tests/core/default-state.test.js +10 -0
  63. package/dist/tests/core/failure-classification.test.js +7 -0
  64. package/dist/tests/core/loop-detection.test.js +7 -0
  65. package/dist/tests/core/paths.test.js +11 -0
  66. package/dist/tests/core/prompt-builders.test.js +33 -0
  67. package/dist/tests/core/score-candidate.test.js +28 -0
  68. package/dist/tests/core/state-machine.test.js +12 -0
  69. package/dist/tests/integration/status-report.test.js +21 -0
  70. package/docs/architecture.md +20 -0
  71. package/docs/demo.sh +266 -0
  72. package/docs/skill-integration.md +15 -0
  73. package/docs/state-machine.md +15 -0
  74. package/package.json +31 -0
  75. package/src/cli/commands/analyze.ts +22 -0
  76. package/src/cli/commands/guard.ts +28 -0
  77. package/src/cli/commands/init.ts +41 -0
  78. package/src/cli/commands/run.ts +79 -0
  79. package/src/cli/commands/schedule.ts +32 -0
  80. package/src/cli/commands/start.ts +111 -0
  81. package/src/cli/commands/status.ts +30 -0
  82. package/src/cli/commands/verify.ts +17 -0
  83. package/src/cli/context/create-context.ts +142 -0
  84. package/src/cli/index.ts +27 -0
  85. package/src/cli/utils/scan-dir.ts +5 -0
  86. package/src/core/analyzers/coverage-analyzer.ts +10 -0
  87. package/src/core/analyzers/dependency-complexity-analyzer.ts +25 -0
  88. package/src/core/analyzers/existing-test-analyzer.ts +76 -0
  89. package/src/core/analyzers/failure-history-analyzer.ts +12 -0
  90. package/src/core/analyzers/file-classifier-analyzer.ts +25 -0
  91. package/src/core/analyzers/index.ts +51 -0
  92. package/src/core/analyzers/llm-semantic-analyzer.ts +6 -0
  93. package/src/core/analyzers/path-priority-analyzer.ts +41 -0
  94. package/src/core/coverage/read-coverage-summary.ts +224 -0
  95. package/src/core/executor/claude-cli-executor.ts +94 -0
  96. package/src/core/middleware/loop-detection.ts +8 -0
  97. package/src/core/middleware/pre-completion-checklist.ts +32 -0
  98. package/src/core/middleware/silent-success-post-check.ts +16 -0
  99. package/src/core/planner/rank-candidates.ts +5 -0
  100. package/src/core/planner/rule-planner.ts +65 -0
  101. package/src/core/planner/score-candidate.ts +60 -0
  102. package/src/core/prompts/case-library.ts +36 -0
  103. package/src/core/prompts/edit-boundary-prompt.ts +26 -0
  104. package/src/core/prompts/retry-prompt.ts +22 -0
  105. package/src/core/prompts/system-prompt.ts +32 -0
  106. package/src/core/prompts/task-prompt.ts +26 -0
  107. package/src/core/reporter/index.ts +56 -0
  108. package/src/core/state-machine/index.ts +14 -0
  109. package/src/core/storage/defaults.ts +18 -0
  110. package/src/core/storage/event-store.ts +18 -0
  111. package/src/core/storage/lifecycle-store.ts +20 -0
  112. package/src/core/storage/report-store.ts +19 -0
  113. package/src/core/storage/state-store.ts +18 -0
  114. package/src/core/strategies/classify-failure.ts +9 -0
  115. package/src/core/strategies/switch-mock-strategy.ts +12 -0
  116. package/src/core/tools/analyze-baseline.ts +61 -0
  117. package/src/core/tools/guard.ts +89 -0
  118. package/src/core/tools/run-loop.ts +142 -0
  119. package/src/core/tools/run-with-claude-cli.ts +926 -0
  120. package/src/core/tools/verify-all.ts +83 -0
  121. package/src/core/worktrees/is-git-repo.ts +10 -0
  122. package/src/types/index.ts +291 -0
  123. package/src/types/logger.ts +6 -0
  124. package/src/utils/clock.ts +10 -0
  125. package/src/utils/command-runner.ts +24 -0
  126. package/src/utils/commands.ts +42 -0
  127. package/src/utils/duration.ts +20 -0
  128. package/src/utils/fs.ts +50 -0
  129. package/src/utils/logger.ts +12 -0
  130. package/src/utils/paths.ts +24 -0
  131. package/src/utils/process-lifecycle.ts +92 -0
  132. package/src/utils/prompts.ts +22 -0
  133. package/tests/core/create-context.test.ts +45 -0
  134. package/tests/core/default-state.test.ts +11 -0
  135. package/tests/core/failure-classification.test.ts +8 -0
  136. package/tests/core/loop-detection.test.ts +8 -0
  137. package/tests/core/paths.test.ts +13 -0
  138. package/tests/core/prompt-builders.test.ts +38 -0
  139. package/tests/core/score-candidate.test.ts +30 -0
  140. package/tests/core/state-machine.test.ts +14 -0
  141. package/tests/fixtures/simple-project/.openclaw-testbot/logs/events.jsonl +10 -0
  142. package/tests/fixtures/simple-project/.openclaw-testbot/plan.json +75 -0
  143. package/tests/fixtures/simple-project/.openclaw-testbot/reports/coverage-summary.json +9 -0
  144. package/tests/fixtures/simple-project/.openclaw-testbot/reports/final-report.json +14 -0
  145. package/tests/fixtures/simple-project/.openclaw-testbot/state.json +18 -0
  146. package/tests/fixtures/simple-project/coverage-summary.json +1 -0
  147. package/tests/fixtures/simple-project/package.json +8 -0
  148. package/tests/fixtures/simple-project/src/add.js +3 -0
  149. package/tests/fixtures/simple-project/test-runner.js +18 -0
  150. package/tests/integration/status-report.test.ts +24 -0
  151. package/tsconfig.json +18 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(*)",
5
+ "Edit(*)",
6
+ "Write(*)",
7
+ "Read(*)",
8
+ "Skill(claude-api)",
9
+ "Skill(claude-api:*)"
10
+ ],
11
+ "deny": [],
12
+ "ask": []
13
+ }
14
+ }
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # TestBot(unit-test-tool)
2
+
3
+ 面向项目的单元测试补全工具,基于 CLI 工作流完成基线分析、推荐测试目标,并调用 Claude CLI 进行补测。
4
+
5
+ ## 适用场景
6
+
7
+ - 需要快速提升项目单测覆盖率
8
+ - 希望以可控参数(覆盖率目标、扫描目录、指定文件)进行补测
9
+ - 想将“分析 → 补测 → 验证 → 汇总”串成自动化流程
10
+
11
+ ## 依赖与准备
12
+
13
+ - 需要本地可用的 Claude CLI(用于 `run` / `start` 补测流程)
14
+ - 在目标项目路径下可运行测试/校验命令(可通过参数覆盖)
15
+
16
+ ## 安装
17
+
18
+ ### 全局安装
19
+
20
+ ```bash
21
+ npm install -g @angli/unit-test-tool
22
+ ```
23
+
24
+ 安装后可直接使用:
25
+
26
+ ```bash
27
+ testbot --help
28
+ ```
29
+
30
+ ## 命令概览(保留关键命令)
31
+
32
+ - `testbot init`:初始化配置
33
+ - `testbot analyze`:基线分析
34
+ - `testbot run`:单次调用 Claude CLI 补测
35
+ - `testbot start`:初始化 + 基线分析 + 循环补测 + 状态汇总
36
+ - `testbot verify`:完整验证
37
+ - `testbot status`:输出状态
38
+ - `testbot report`:生成最终报告
39
+ - `testbot guard`:检测异常并尝试恢复
40
+ - `testbot schedule`:定时执行 guard
41
+
42
+ ## 参数说明(表格)
43
+
44
+ > 说明:以下为各命令支持的参数集合,不同命令支持的参数略有差异。
45
+
46
+ | 参数 | 说明 | 默认值 | 适用命令 |
47
+ |---|---|---|---|
48
+ | `--project <path>` | 项目路径 | `process.cwd()` 或必填 | `init`(必填)、`analyze`、`run`、`start`、`verify`、`status`、`report` |
49
+ | `--target <number>` | 覆盖率目标 | `80` | `init`、`analyze`、`run`、`start` |
50
+ | `--scan-dir <dir>` | 扫描目录 | `src` | `init`、`analyze`、`run`、`start` |
51
+ | `--file <path>` | 指定目标源文件 | 无 | `run`、`start` |
52
+ | `--topN <number>` | 推荐池大小 | `5` | `run`、`start` |
53
+ | `--permission-mode <mode>` | Claude CLI 权限模式 | 无 | `run`、`start` |
54
+ | `--allowed-tools <tools>` | Claude CLI 允许工具列表(逗号分隔) | 无 | `run`、`start` |
55
+ | `--timeout <ms>` | Claude CLI 超时(毫秒) | 无 | `run`、`start` |
56
+ | `--verify-cmd <cmd>` | 覆盖完整验证命令 | 无 | `init`、`run`、`start` |
57
+ | `--lint-cmd <cmd>` | 覆盖 lint 命令 | 无 | `init`、`run`、`start` |
58
+ | `--typecheck-cmd <cmd>` | 覆盖类型检查命令 | 无 | `init`、`run`、`start` |
59
+ | `--system-append <text>` | 追加项目级 system prompt 规则 | 无 | `init` |
60
+ | `--task-append <text>` | 追加项目级 task prompt 规则 | 无 | `init` |
61
+ | `--retry-append <text>` | 追加项目级 retry prompt 规则 | 无 | `init` |
62
+ | `--schedule` | 在 `start` 期间启用内置守护循环 | `false` | `start` |
63
+ | `--guard-interval <duration>` | 守护检查间隔 | `5m` | `start` |
64
+ | `--interval <duration>` | 定时 guard 间隔 | `5m` | `schedule` |
65
+ | `--stale-timeout <duration>` | heartbeat 超时阈值 | `2m` | `guard`、`schedule`、`start` |
66
+ | `--max-restart <count>` | 最大自动恢复次数 | `3` | `guard`、`schedule`、`start` |
67
+ | `--cooldown <duration>` | 两次恢复的冷却时间 | `60s` | `guard`、`schedule`、`start` |
68
+
69
+ ## 日志与回放
70
+
71
+ 运行时产物位于目标项目的 `.unit_test_tool_workspace/` 目录。
72
+
73
+ ### 目录结构
74
+
75
+ - `logs/`:stdout/stderr 日志、`events.jsonl`
76
+ - `prompts/`:每个任务独立的 prompt 文件
77
+ - `prompt-runs/`:按一次 attempt 追加的整轮 prompt 索引/引用日志
78
+ - `reports/`:coverage summary / final report
79
+
80
+ ### events.jsonl 怎么看
81
+
82
+ `events.jsonl` 是 JSONL 格式,一行一个事件。常见事件类型:
83
+
84
+ - `cli_run_started`:选中目标文件
85
+ - `cli_run_attempt`:开始调用 Claude CLI(含 prompt 路径)
86
+ - `cli_run_completed`:本轮成功
87
+ - `cli_run_failed`:本轮失败或 no-op
88
+ - `baseline_analyzed`:基线分析完成
89
+
90
+ 建议优先关注 `data` 里的这些字段:
91
+
92
+ - `runId`:本次运行批次(例如 `r20260403T022232Z`)
93
+ - `taskId`:任务 ID(例如 `r20260403T022232Z-001`)
94
+ - `attemptLabel`:尝试标记(例如 `a1`)
95
+ - `targetBaseName`:目标文件 basename
96
+ - `promptPath / stdoutPath / stderrPath`:任务级产物路径
97
+ - `runPromptPath`:整轮 prompt 汇总文件路径
98
+
99
+ ### 文件名组成规则
100
+
101
+ 任务级产物文件名统一遵循:
102
+
103
+ ```
104
+ {taskId}-a{attempt}-{baseName}-{pathHash}-{timestamp}.{kind}.{ext}
105
+ ```
106
+
107
+ 示例:
108
+
109
+ ```
110
+ r20260403T022232Z-001-a1-corpus-vue-4e2b1c9a-20260403T022232Z.prompt.txt
111
+ r20260403T022232Z-001-a1-corpus-vue-4e2b1c9a-20260403T022232Z.stdout.log
112
+ r20260403T022232Z-001-a1-corpus-vue-4e2b1c9a-20260403T022232Z.stderr.log
113
+ ```
114
+
115
+ 字段含义:
116
+
117
+ - `taskId`:`runId + 序号`
118
+ - `attempt`:第几次尝试
119
+ - `baseName`:目标文件 basename
120
+ - `pathHash`:目标文件路径 hash(短 hash)
121
+ - `timestamp`:本次 attempt 的时间戳
122
+ - `kind`:`prompt` / `stdout` / `stderr`
123
+
124
+ ### run 级 prompt 汇总文件
125
+
126
+ 整轮流程的 prompt 汇总写到单个文件(不再内嵌 prompt 正文,改为记录 prompt 文件路径):
127
+
128
+ ```
129
+ run-{runId}.prompt.log
130
+ ```
131
+
132
+ 查看完整 prompt 内容,请到 `prompts/*.prompt.txt`。
133
+
134
+ 推荐排查顺序:
135
+
136
+ 1. `state.json` / `final-report.json`
137
+ 2. `logs/events.jsonl`
138
+ 3. `prompts/` 和 `logs/` 里的单任务产物
139
+ 4. `prompt-runs/` 里的整轮回放
140
+
141
+
142
+ ### 1) 初始化配置
143
+
144
+ ```bash
145
+ testbot init --project /path/to/project --target 85 --scan-dir src
146
+ ```
147
+
148
+ 如需给某个项目追加测试生成规则,可在初始化时一并写入:
149
+
150
+ ```bash
151
+ testbot init \
152
+ --project /path/to/project \
153
+ --target 85 \
154
+ --system-append "项目里大量使用 provide/inject,优先复用现有 context 装配方式" \
155
+ --task-append "涉及表格组件时,优先断言列配置和事件,不做整表 DOM 快照" \
156
+ --retry-append "如果 wrapper 测试连续失败,退回到 render 参数和事件透传断言"
157
+ ```
158
+
159
+ ### 2) 仅做基线分析
160
+
161
+ ```bash
162
+ testbot analyze --project /path/to/project --target 80
163
+ ```
164
+
165
+ ### 3) 单次补测(可指定文件)
166
+
167
+ ```bash
168
+ testbot run --project /path/to/project --file src/foo.ts --topN 3 --permission-mode ask
169
+ ```
170
+
171
+ ### 4) 一站式流程(初始化 + 分析 + 循环补测 + 状态)
172
+
173
+ ```bash
174
+ testbot start --project /path/to/project --target 80 --topN 5
175
+ ```
176
+
177
+ ### 5) 完整验证与状态
178
+
179
+ ```bash
180
+ testbot verify --project /path/to/project
181
+
182
+ testbot status --project /path/to/project
183
+ ```
184
+
185
+ ### 7) 守护与定时恢复
186
+
187
+ ```bash
188
+ testbot guard --project /path/to/project
189
+
190
+ testbot schedule --project /path/to/project --interval 5m
191
+
192
+ # 在 start 期间启用守护
193
+
194
+ testbot start --project /path/to/project --schedule --guard-interval 5m
195
+ ```
196
+
197
+ ### 8) Claude CLI 项目级配置
198
+
199
+ 补测执行时会优先读取“待补测项目目录”下的 `.claude/settings.local.json`,并使用其中 `env` 的 `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` 覆盖全局环境变量。
200
+
201
+ 示例:
202
+
203
+ ```json
204
+ {
205
+ "env": {
206
+ "ANTHROPIC_AUTH_TOKEN": "...",
207
+ "ANTHROPIC_BASE_URL": "..."
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### 9) config.json 中的 promptOverrides
213
+
214
+ `testbot init` 写入的 `.unit_test_tool_workspace/config.json` 支持项目级 prompt 覆盖。工具会先使用内置“通用单测策略 + 案例片段库”,再追加这里的覆盖内容。
215
+
216
+ 示例:
217
+
218
+ ```json
219
+ {
220
+ "projectPath": "/path/to/project",
221
+ "coverageTarget": 85,
222
+ "include": ["src/**/*"],
223
+ "exclude": ["node_modules/**", "dist/**", "coverage/**"],
224
+ "mode": "serial",
225
+ "concurrency": 1,
226
+ "promptOverrides": {
227
+ "systemAppend": "项目里大量使用 provide/inject,生成测试前先识别上下文注入入口。",
228
+ "taskAppend": "表格/列表组件优先测列配置、事件和状态切换,不做大段 DOM 快照。",
229
+ "retryAppend": "render/HOC 测试连续失败时,回退到参数透传和派生 props 断言。"
230
+ }
231
+ }
232
+ ```
@@ -0,0 +1,20 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { analyzeBaseline } from '../../core/tools/analyze-baseline.js';
3
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js';
4
+ export function registerAnalyzeCommand(program) {
5
+ program
6
+ .command('analyze')
7
+ .option('--project <path>', 'project path')
8
+ .option('--target <number>', 'coverage target', '80')
9
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
10
+ .action(async (options) => {
11
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src');
12
+ const ctx = await createContext({
13
+ projectPath: options.project ?? process.cwd(),
14
+ coverageTarget: Number(options.target),
15
+ configOverrides: include ? { include } : undefined
16
+ });
17
+ const result = await analyzeBaseline(ctx);
18
+ console.log(result.summary);
19
+ });
20
+ }
@@ -0,0 +1,26 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { runGuard } from '../../core/tools/guard.js';
3
+ import { parseDuration } from '../../utils/duration.js';
4
+ export function registerGuardCommand(program) {
5
+ program
6
+ .command('guard')
7
+ .option('--project <path>', 'project path')
8
+ .option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
9
+ .option('--max-restart <count>', 'max restart attempts (default: 3)')
10
+ .option('--cooldown <duration>', 'restart cooldown (default: 60s)')
11
+ .action(async (options) => {
12
+ const ctx = await createContext({
13
+ projectPath: options.project ?? process.cwd(),
14
+ coverageTarget: 80
15
+ });
16
+ const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined;
17
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined;
18
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined;
19
+ const result = await runGuard(ctx, {
20
+ staleTimeoutMs: staleTimeoutMs,
21
+ maxRestart,
22
+ cooldownMs
23
+ });
24
+ console.log(result.summary);
25
+ });
26
+ }
@@ -0,0 +1,39 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js';
3
+ export function registerInitCommand(program) {
4
+ program
5
+ .command('init')
6
+ .requiredOption('--project <path>', 'project path')
7
+ .option('--target <number>', 'coverage target', '80')
8
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
9
+ .option('--verify-cmd <cmd>', 'override full verify command')
10
+ .option('--lint-cmd <cmd>', 'override lint command')
11
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
12
+ .option('--system-append <text>', 'append project-level system prompt guidance')
13
+ .option('--task-append <text>', 'append project-level task prompt guidance')
14
+ .option('--retry-append <text>', 'append project-level retry prompt guidance')
15
+ .action(async (options) => {
16
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src');
17
+ const commandOverrides = {
18
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
19
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
20
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
21
+ };
22
+ const promptOverrides = {
23
+ ...(options.systemAppend ? { systemAppend: options.systemAppend } : {}),
24
+ ...(options.taskAppend ? { taskAppend: options.taskAppend } : {}),
25
+ ...(options.retryAppend ? { retryAppend: options.retryAppend } : {})
26
+ };
27
+ const ctx = await createContext({
28
+ projectPath: options.project,
29
+ coverageTarget: Number(options.target),
30
+ configOverrides: {
31
+ ...(include ? { include } : {}),
32
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {}),
33
+ ...(Object.keys(promptOverrides).length > 0 ? { promptOverrides } : {})
34
+ }
35
+ });
36
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config);
37
+ console.log('config saved');
38
+ });
39
+ }
@@ -0,0 +1,72 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js';
3
+ import { runWithClaudeCli } from '../../core/tools/run-with-claude-cli.js';
4
+ import { startLifecycle } from '../../utils/process-lifecycle.js';
5
+ export function registerRunCommand(program) {
6
+ program
7
+ .command('run')
8
+ .option('--project <path>', 'project path')
9
+ .option('--target <number>', 'coverage target', '80')
10
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
11
+ .option('--file <path>', 'explicit target source file')
12
+ .option('--topN <number>', 'recommendation pool size', '5')
13
+ .option('--permission-mode <mode>', 'Claude CLI permission mode')
14
+ .option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
15
+ .option('--timeout <ms>', 'Claude CLI timeout in ms')
16
+ .option('--verify-cmd <cmd>', 'override full verify command')
17
+ .option('--lint-cmd <cmd>', 'override lint command')
18
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
19
+ .action(async (options) => {
20
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src');
21
+ const commandOverrides = {
22
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
23
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
24
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
25
+ };
26
+ const ctx = await createContext({
27
+ projectPath: options.project ?? process.cwd(),
28
+ coverageTarget: Number(options.target),
29
+ configOverrides: {
30
+ ...(include ? { include } : {}),
31
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
32
+ }
33
+ });
34
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config);
35
+ const lifecycle = await startLifecycle(ctx, {
36
+ command: 'run',
37
+ argv: process.argv.slice(2)
38
+ });
39
+ try {
40
+ const result = await runWithClaudeCli(ctx, {
41
+ targetFile: options.file,
42
+ topN: Number(options.topN),
43
+ permissionMode: options.permissionMode,
44
+ allowedTools: parseAllowedTools(options.allowedTools),
45
+ timeoutMs: options.timeout ? Number(options.timeout) : undefined
46
+ });
47
+ console.log(result.summary);
48
+ if (result.artifacts?.finalReportPath) {
49
+ console.log(`report: ${result.artifacts.finalReportPath}`);
50
+ }
51
+ await lifecycle.stop();
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ await ctx.lifecycleStore.patch({
56
+ status: 'crashed',
57
+ lastError: message,
58
+ updatedAt: ctx.clock.nowIso()
59
+ });
60
+ throw error;
61
+ }
62
+ });
63
+ }
64
+ function parseAllowedTools(value) {
65
+ if (!value)
66
+ return undefined;
67
+ const tools = value
68
+ .split(',')
69
+ .map((item) => item.trim())
70
+ .filter(Boolean);
71
+ return tools.length > 0 ? tools : undefined;
72
+ }
@@ -0,0 +1,29 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { parseDuration } from '../../utils/duration.js';
3
+ import { runSchedule } from '../../core/tools/guard.js';
4
+ const DEFAULT_INTERVAL_MS = 5 * 60_000;
5
+ export function registerScheduleCommand(program) {
6
+ program
7
+ .command('schedule')
8
+ .option('--project <path>', 'project path')
9
+ .option('--interval <duration>', 'guard interval (default: 5m)')
10
+ .option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
11
+ .option('--max-restart <count>', 'max restart attempts (default: 3)')
12
+ .option('--cooldown <duration>', 'restart cooldown (default: 60s)')
13
+ .action(async (options) => {
14
+ const ctx = await createContext({
15
+ projectPath: options.project ?? process.cwd(),
16
+ coverageTarget: 80
17
+ });
18
+ const intervalMs = options.interval ? parseDuration(options.interval) ?? DEFAULT_INTERVAL_MS : DEFAULT_INTERVAL_MS;
19
+ const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined;
20
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined;
21
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined;
22
+ console.log(`[schedule] interval=${intervalMs}ms`);
23
+ await runSchedule(ctx, intervalMs, {
24
+ staleTimeoutMs,
25
+ maxRestart,
26
+ cooldownMs
27
+ });
28
+ });
29
+ }
@@ -0,0 +1,102 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { analyzeBaseline } from '../../core/tools/analyze-baseline.js';
3
+ import { runLoop } from '../../core/tools/run-loop.js';
4
+ import { reportStatus } from '../../core/reporter/index.js';
5
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js';
6
+ import { startLifecycle } from '../../utils/process-lifecycle.js';
7
+ import { runSchedule } from '../../core/tools/guard.js';
8
+ import { parseDuration } from '../../utils/duration.js';
9
+ const DEFAULT_GUARD_INTERVAL_MS = 5 * 60_000;
10
+ export function registerStartCommand(program) {
11
+ program
12
+ .command('start')
13
+ .option('--project <path>', 'project path')
14
+ .option('--target <number>', 'coverage target', '80')
15
+ .option('--topN <number>', 'recommendation pool size', '5')
16
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
17
+ .option('--file <path>', 'explicit target source file')
18
+ .option('--permission-mode <mode>', 'Claude CLI permission mode')
19
+ .option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
20
+ .option('--timeout <ms>', 'Claude CLI timeout in ms')
21
+ .option('--verify-cmd <cmd>', 'override full verify command')
22
+ .option('--lint-cmd <cmd>', 'override lint command')
23
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
24
+ .option('--schedule', 'run built-in guard schedule')
25
+ .option('--guard-interval <duration>', 'guard interval (default: 5m)')
26
+ .option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
27
+ .option('--max-restart <count>', 'max restart attempts (default: 3)')
28
+ .option('--cooldown <duration>', 'restart cooldown (default: 60s)')
29
+ .action(async (options) => {
30
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src');
31
+ const commandOverrides = {
32
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
33
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
34
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
35
+ };
36
+ console.log('[1/4] 初始化配置...');
37
+ const ctx = await createContext({
38
+ projectPath: options.project ?? process.cwd(),
39
+ coverageTarget: Number(options.target),
40
+ configOverrides: {
41
+ ...(include ? { include } : {}),
42
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
43
+ }
44
+ });
45
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config);
46
+ console.log(`[1/4] 初始化完成:${ctx.paths.configPath}`);
47
+ const lifecycle = await startLifecycle(ctx, {
48
+ command: 'start',
49
+ argv: process.argv.slice(2)
50
+ });
51
+ try {
52
+ if (options.schedule) {
53
+ const intervalMs = options.guardInterval ? parseDuration(options.guardInterval) ?? DEFAULT_GUARD_INTERVAL_MS : DEFAULT_GUARD_INTERVAL_MS;
54
+ const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined;
55
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined;
56
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined;
57
+ await runSchedule(ctx, intervalMs, {
58
+ staleTimeoutMs,
59
+ maxRestart,
60
+ cooldownMs
61
+ });
62
+ }
63
+ console.log('[2/4] 开始基线分析...');
64
+ const baseline = await analyzeBaseline(ctx);
65
+ console.log(`[2/4] ${baseline.summary}`);
66
+ console.log('[3/4] 调用 Claude CLI 补测...');
67
+ const runResult = await runLoop(ctx, {
68
+ targetFile: options.file,
69
+ topN: Number(options.topN),
70
+ permissionMode: options.permissionMode,
71
+ allowedTools: parseAllowedTools(options.allowedTools),
72
+ timeoutMs: options.timeout ? Number(options.timeout) : undefined
73
+ });
74
+ console.log(`[3/4] ${runResult.summary}`);
75
+ console.log('[4/4] 输出状态...');
76
+ const status = await reportStatus(ctx);
77
+ console.log(`[4/4] ${status.summary}`);
78
+ if (runResult.artifacts?.finalReportPath) {
79
+ console.log(`[4/4] report generated: ${runResult.artifacts.finalReportPath}`);
80
+ }
81
+ await lifecycle.stop();
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ await ctx.lifecycleStore.patch({
86
+ status: 'crashed',
87
+ lastError: message,
88
+ updatedAt: ctx.clock.nowIso()
89
+ });
90
+ throw error;
91
+ }
92
+ });
93
+ }
94
+ function parseAllowedTools(value) {
95
+ if (!value)
96
+ return undefined;
97
+ const tools = value
98
+ .split(',')
99
+ .map((item) => item.trim())
100
+ .filter(Boolean);
101
+ return tools.length > 0 ? tools : undefined;
102
+ }
@@ -0,0 +1,27 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { reportStatus, buildFinalReport } from '../../core/reporter/index.js';
3
+ export function registerStatusCommand(program) {
4
+ program
5
+ .command('status')
6
+ .option('--project <path>', 'project path')
7
+ .action(async (options) => {
8
+ const ctx = await createContext({
9
+ projectPath: options.project ?? process.cwd(),
10
+ coverageTarget: 80
11
+ });
12
+ const result = await reportStatus(ctx);
13
+ console.log(result.summary);
14
+ });
15
+ program
16
+ .command('report')
17
+ .option('--project <path>', 'project path')
18
+ .action(async (options) => {
19
+ const ctx = await createContext({
20
+ projectPath: options.project ?? process.cwd(),
21
+ coverageTarget: 80
22
+ });
23
+ const report = await buildFinalReport(ctx);
24
+ await ctx.reportStore.saveFinalReport(report);
25
+ console.log('report generated');
26
+ });
27
+ }
@@ -0,0 +1,15 @@
1
+ import { createContext } from '../context/create-context.js';
2
+ import { verifyAll } from '../../core/tools/verify-all.js';
3
+ export function registerVerifyCommand(program) {
4
+ program
5
+ .command('verify')
6
+ .option('--project <path>', 'project path')
7
+ .action(async (options) => {
8
+ const ctx = await createContext({
9
+ projectPath: options.project ?? process.cwd(),
10
+ coverageTarget: 80
11
+ });
12
+ const result = await verifyAll(ctx);
13
+ console.log(result.summary);
14
+ });
15
+ }