@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
package/docs/demo.sh ADDED
@@ -0,0 +1,266 @@
1
+ #!/bin/bash
2
+
3
+ # 自动单元测试执行脚本
4
+ # 用途:逐个读取 serviceToTest.txt 中的类名,自动编写单元测试
5
+ # 支持 Claude Code 和 Codex 两种模式
6
+
7
+ set -uo pipefail
8
+
9
+ # ============================================
10
+ # 配置变量
11
+ # ============================================
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ WORKSPACE_DIR="${SCRIPT_DIR}"
14
+ SERVICE_LIST_FILE="${WORKSPACE_DIR}/serviceToTest.txt"
15
+ TEST_INFO_FILE="${WORKSPACE_DIR}/testInfo.txt"
16
+ TEMP_DIR="${WORKSPACE_DIR}/tmp_logs"
17
+
18
+ # 选择运行模式: "claude" 或 "codex"(如果未设置则由用户选择)
19
+ RUN_MODE=""
20
+
21
+ # Claude Code 配置
22
+ CLAUDE_CMD=(claude)
23
+
24
+ # Codex 配置
25
+ CODEX_CMD=(
26
+ codex exec
27
+ --dangerously-bypass-approvals-and-sandbox
28
+ --cd "${WORKSPACE_DIR}"
29
+ )
30
+
31
+ # ============================================
32
+ # 颜色输出
33
+ # ============================================
34
+ RED='\033[0;31m'
35
+ GREEN='\033[0;32m'
36
+ YELLOW='\033[1;33m'
37
+ BLUE='\033[0;34m'
38
+ NC='\033[0m'
39
+
40
+ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
41
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
42
+ log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
43
+ log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
44
+
45
+ # ============================================
46
+ # 解析命令行参数
47
+ # ============================================
48
+ parse_args() {
49
+ if [ $# -gt 0 ]; then
50
+ case "$1" in
51
+ claude|codex)
52
+ RUN_MODE="$1"
53
+ log_info "使用命令行指定的运行模式: $RUN_MODE"
54
+ ;;
55
+ *)
56
+ echo "用法: $0 [claude|codex]"
57
+ echo " 不带参数时,会交互式选择运行模式"
58
+ echo " claude - 使用 Claude Code 执行"
59
+ echo " codex - 使用 Codex 执行"
60
+ exit 1
61
+ ;;
62
+ esac
63
+ fi
64
+ }
65
+
66
+ # ============================================
67
+ # 交互式选择运行模式
68
+ # ============================================
69
+ select_run_mode() {
70
+ # 如果 RUN_MODE 已是有效值,直接使用
71
+ if [ "$RUN_MODE" = "claude" ] || [ "$RUN_MODE" = "codex" ]; then
72
+ log_info "使用已设置的运行模式: $RUN_MODE"
73
+ return
74
+ fi
75
+
76
+ echo ""
77
+ echo "请选择运行模式:"
78
+ echo " 1) claude - 使用 Claude Code 执行"
79
+ echo " 2) codex - 使用 Codex 执行"
80
+ echo ""
81
+ read -p "请输入选项 [1]: " choice
82
+
83
+ case "$choice" in
84
+ 1|"")
85
+ RUN_MODE="claude"
86
+ ;;
87
+ 2)
88
+ RUN_MODE="codex"
89
+ ;;
90
+ *)
91
+ log_error "无效的选项: $choice"
92
+ exit 1
93
+ ;;
94
+ esac
95
+ }
96
+
97
+ # ============================================
98
+ # 检查前置条件
99
+ # ============================================
100
+ check_prerequisites() {
101
+ # 确保 RUN_MODE 已设置
102
+ if [ -z "$RUN_MODE" ]; then
103
+ log_error "运行模式未设置"
104
+ exit 1
105
+ fi
106
+
107
+ log_step "检查前置条件..."
108
+
109
+ if [ ! -f "$SERVICE_LIST_FILE" ]; then
110
+ log_error "待测试文件清单不存在: $SERVICE_LIST_FILE"
111
+ exit 1
112
+ fi
113
+
114
+ if [ "$RUN_MODE" = "codex" ]; then
115
+ if ! command -v codex >/dev/null 2>&1; then
116
+ log_error "codex 命令未找到"
117
+ exit 1
118
+ fi
119
+ elif [ "$RUN_MODE" = "claude" ]; then
120
+ if ! command -v claude >/dev/null 2>&1; then
121
+ log_error "claude 命令未找到"
122
+ exit 1
123
+ fi
124
+ else
125
+ log_error "未知的运行模式: $RUN_MODE,请设置 RUN_MODE 为 'claude' 或 'codex'"
126
+ exit 1
127
+ fi
128
+
129
+ mkdir -p "${TEMP_DIR}"
130
+ touch "${TEST_INFO_FILE}"
131
+
132
+ cd "$WORKSPACE_DIR" || exit 1
133
+
134
+ log_info "运行模式: $RUN_MODE"
135
+ log_info "前置检查通过"
136
+ }
137
+
138
+ # ============================================
139
+ # 获取下一个待处理的类
140
+ # ============================================
141
+ get_next_service() {
142
+ awk 'NF { print; exit }' "$SERVICE_LIST_FILE"
143
+ }
144
+
145
+ # ============================================
146
+ # 移除已完成的类
147
+ # ============================================
148
+ remove_completed_service() {
149
+ local tmp_file="${SERVICE_LIST_FILE}.tmp"
150
+
151
+ awk '
152
+ BEGIN { removed = 0 }
153
+ {
154
+ if (!removed && NF) {
155
+ removed = 1
156
+ next
157
+ }
158
+ print
159
+ }
160
+ ' "$SERVICE_LIST_FILE" > "$tmp_file" && mv "$tmp_file" "$SERVICE_LIST_FILE"
161
+ }
162
+
163
+ # ============================================
164
+ # 构建 prompt
165
+ # ============================================
166
+ build_prompt() {
167
+ local service_class=$1
168
+
169
+ if [ "$RUN_MODE" = "codex" ]; then
170
+ cat <<EOF
171
+ 参考单测Skill对给定的类${service_class}编写单元测试,将单测结论追加到testInfo.txt文件。
172
+ EOF
173
+ elif [ "$RUN_MODE" = "claude" ]; then
174
+ # Claude Code 使用相同的 prompt
175
+ echo "参考单测Skill对给定的类${service_class}编写单元测试,将单测结论追加到testInfo.txt文件。"
176
+ fi
177
+ }
178
+
179
+ # ============================================
180
+ # 执行单元测试编写
181
+ # ============================================
182
+ run_unit_test() {
183
+ local service_class=$1
184
+ local class_name
185
+ local prompt
186
+ local log_file
187
+ local exit_code
188
+
189
+ class_name=$(echo "$service_class" | rev | cut -d'.' -f1 | rev)
190
+ log_file="${TEMP_DIR}/${RUN_MODE}_output_${class_name}.log"
191
+ prompt=$(build_prompt "$service_class")
192
+
193
+ log_step "=========================================="
194
+ log_step "处理: $service_class"
195
+ log_step "=========================================="
196
+ log_info "日志文件: $log_file"
197
+ log_info "使用模式: $RUN_MODE"
198
+
199
+ if [ "$RUN_MODE" = "codex" ]; then
200
+ printf '%s\n' "$prompt" | "${CODEX_CMD[@]}" - 2>&1 | tee "$log_file"
201
+ exit_code=${PIPESTATUS[1]}
202
+ elif [ "$RUN_MODE" = "claude" ]; then
203
+ # Claude Code: 使用 claude -p 命令配合 prompt
204
+ "${CLAUDE_CMD[@]}" -p "$prompt" --verbose --output-format stream-json 2>&1 | tee "$log_file"
205
+ exit_code=${PIPESTATUS[0]}
206
+ fi
207
+
208
+ log_info "$RUN_MODE 执行完成,退出码: $exit_code"
209
+
210
+ if [ "$exit_code" -ne 0 ]; then
211
+ log_error "$RUN_MODE 执行失败,当前类未从 serviceToTest.txt 中移除"
212
+ return "$exit_code"
213
+ fi
214
+
215
+ remove_completed_service
216
+ log_info "已完成并从队列中移除: $service_class"
217
+
218
+ return 0
219
+ }
220
+
221
+ # ============================================
222
+ # 显示剩余待处理数量
223
+ # ============================================
224
+ show_remaining() {
225
+ local count
226
+ count=$(awk 'NF { count++ } END { print count + 0 }' "$SERVICE_LIST_FILE")
227
+ log_info "剩余待测试: $count"
228
+ }
229
+
230
+ # ============================================
231
+ # 主流程
232
+ # ============================================
233
+ main() {
234
+ local service
235
+
236
+ echo ""
237
+ log_info "=========================================="
238
+ log_info " 自动单元测试编写脚本"
239
+ log_info "=========================================="
240
+ echo ""
241
+
242
+ parse_args "$@"
243
+ select_run_mode
244
+ check_prerequisites
245
+ show_remaining
246
+
247
+ while true; do
248
+ service=$(get_next_service)
249
+
250
+ if [ -z "$service" ]; then
251
+ echo ""
252
+ log_info "=========================================="
253
+ log_info " 所有单元测试已完成"
254
+ log_info "=========================================="
255
+ break
256
+ fi
257
+
258
+ if ! run_unit_test "$service"; then
259
+ exit 1
260
+ fi
261
+
262
+ show_remaining
263
+ done
264
+ }
265
+
266
+ main "$@"
@@ -0,0 +1,15 @@
1
+ # Skill 集成说明
2
+
3
+ 建议将本工具作为 OpenClaw skill 的外部 CLI 入口使用。
4
+
5
+ 推荐映射:
6
+ - `init`:初始化配置与基线
7
+ - `analyze`:分析测试框架与覆盖率现状
8
+ - `run`:调用 Claude CLI 完成补测
9
+ - `verify`:执行全量校验
10
+ - `status`:查看运行状态
11
+ - `report`:输出最终报告
12
+
13
+ 建议外部调度:
14
+ - 直接调用 `run` 并在失败时重试或人工介入
15
+ - 需要周期性汇总时调用 `status/report`
@@ -0,0 +1,15 @@
1
+ # 状态说明
2
+
3
+ 阶段枚举:
4
+ - INIT
5
+ - ANALYZE_BASELINE
6
+ - WRITING_TESTS
7
+ - VERIFYING_FULL
8
+ - BLOCKED
9
+ - DONE
10
+
11
+ 典型流程:
12
+ 1. analyzeBaseline 进入 ANALYZE_BASELINE
13
+ 2. runWithClaudeCli 进入 WRITING_TESTS
14
+ 3. verifyAll 进入 VERIFYING_FULL
15
+ 4. 验证通过进入 DONE,否则进入 BLOCKED
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@angli/unit-test-tool",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "testbot": "./dist/cli/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "dev": "tsx src/cli/index.ts",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "lint": "tsc -p tsconfig.json --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^14.0.0",
17
+ "execa": "^9.6.0",
18
+ "fast-glob": "^3.3.3",
19
+ "pino": "^10.0.0",
20
+ "zod": "^4.1.11"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.6.0",
24
+ "tsx": "^4.20.6",
25
+ "typescript": "^5.9.3",
26
+ "vitest": "^3.2.4"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { analyzeBaseline } from '../../core/tools/analyze-baseline.js'
4
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
5
+
6
+ export function registerAnalyzeCommand(program: Command) {
7
+ program
8
+ .command('analyze')
9
+ .option('--project <path>', 'project path')
10
+ .option('--target <number>', 'coverage target', '80')
11
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
12
+ .action(async (options) => {
13
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
14
+ const ctx = await createContext({
15
+ projectPath: options.project ?? process.cwd(),
16
+ coverageTarget: Number(options.target),
17
+ configOverrides: include ? { include } : undefined
18
+ })
19
+ const result = await analyzeBaseline(ctx)
20
+ console.log(result.summary)
21
+ })
22
+ }
@@ -0,0 +1,28 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { runGuard } from '../../core/tools/guard.js'
4
+ import { parseDuration } from '../../utils/duration.js'
5
+
6
+ export function registerGuardCommand(program: Command) {
7
+ program
8
+ .command('guard')
9
+ .option('--project <path>', 'project path')
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 staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
19
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
20
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
21
+ const result = await runGuard(ctx, {
22
+ staleTimeoutMs: staleTimeoutMs,
23
+ maxRestart,
24
+ cooldownMs
25
+ })
26
+ console.log(result.summary)
27
+ })
28
+ }
@@ -0,0 +1,41 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
4
+
5
+ export function registerInitCommand(program: Command) {
6
+ program
7
+ .command('init')
8
+ .requiredOption('--project <path>', 'project path')
9
+ .option('--target <number>', 'coverage target', '80')
10
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
11
+ .option('--verify-cmd <cmd>', 'override full verify command')
12
+ .option('--lint-cmd <cmd>', 'override lint command')
13
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
14
+ .option('--system-append <text>', 'append project-level system prompt guidance')
15
+ .option('--task-append <text>', 'append project-level task prompt guidance')
16
+ .option('--retry-append <text>', 'append project-level retry prompt guidance')
17
+ .action(async (options) => {
18
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
19
+ const commandOverrides = {
20
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
21
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
22
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
23
+ }
24
+ const promptOverrides = {
25
+ ...(options.systemAppend ? { systemAppend: options.systemAppend } : {}),
26
+ ...(options.taskAppend ? { taskAppend: options.taskAppend } : {}),
27
+ ...(options.retryAppend ? { retryAppend: options.retryAppend } : {})
28
+ }
29
+ const ctx = await createContext({
30
+ projectPath: options.project,
31
+ coverageTarget: Number(options.target),
32
+ configOverrides: {
33
+ ...(include ? { include } : {}),
34
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {}),
35
+ ...(Object.keys(promptOverrides).length > 0 ? { promptOverrides } : {})
36
+ }
37
+ })
38
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
39
+ console.log('config saved')
40
+ })
41
+ }
@@ -0,0 +1,79 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
4
+ import { runWithClaudeCli } from '../../core/tools/run-with-claude-cli.js'
5
+ import { startLifecycle } from '../../utils/process-lifecycle.js'
6
+
7
+ export function registerRunCommand(program: Command) {
8
+ program
9
+ .command('run')
10
+ .option('--project <path>', 'project path')
11
+ .option('--target <number>', 'coverage target', '80')
12
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
13
+ .option('--file <path>', 'explicit target source file')
14
+ .option('--topN <number>', 'recommendation pool size', '5')
15
+ .option('--permission-mode <mode>', 'Claude CLI permission mode')
16
+ .option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
17
+ .option('--timeout <ms>', 'Claude CLI timeout in ms')
18
+ .option('--verify-cmd <cmd>', 'override full verify command')
19
+ .option('--lint-cmd <cmd>', 'override lint command')
20
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
21
+ .action(async (options) => {
22
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
23
+ const commandOverrides = {
24
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
25
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
26
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
27
+ }
28
+
29
+ const ctx = await createContext({
30
+ projectPath: options.project ?? process.cwd(),
31
+ coverageTarget: Number(options.target),
32
+ configOverrides: {
33
+ ...(include ? { include } : {}),
34
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
35
+ }
36
+ })
37
+
38
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
39
+
40
+ const lifecycle = await startLifecycle(ctx, {
41
+ command: 'run',
42
+ argv: process.argv.slice(2)
43
+ })
44
+
45
+ try {
46
+ const result = await runWithClaudeCli(ctx, {
47
+ targetFile: options.file,
48
+ topN: Number(options.topN),
49
+ permissionMode: options.permissionMode,
50
+ allowedTools: parseAllowedTools(options.allowedTools),
51
+ timeoutMs: options.timeout ? Number(options.timeout) : undefined
52
+ })
53
+
54
+ console.log(result.summary)
55
+ if (result.artifacts?.finalReportPath) {
56
+ console.log(`report: ${result.artifacts.finalReportPath}`)
57
+ }
58
+
59
+ await lifecycle.stop()
60
+ } catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error)
62
+ await ctx.lifecycleStore.patch({
63
+ status: 'crashed',
64
+ lastError: message,
65
+ updatedAt: ctx.clock.nowIso()
66
+ })
67
+ throw error
68
+ }
69
+ })
70
+ }
71
+
72
+ function parseAllowedTools(value?: string): string[] | undefined {
73
+ if (!value) return undefined
74
+ const tools = value
75
+ .split(',')
76
+ .map((item) => item.trim())
77
+ .filter(Boolean)
78
+ return tools.length > 0 ? tools : undefined
79
+ }
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { parseDuration } from '../../utils/duration.js'
4
+ import { runSchedule } from '../../core/tools/guard.js'
5
+
6
+ const DEFAULT_INTERVAL_MS = 5 * 60_000
7
+
8
+ export function registerScheduleCommand(program: Command) {
9
+ program
10
+ .command('schedule')
11
+ .option('--project <path>', 'project path')
12
+ .option('--interval <duration>', 'guard interval (default: 5m)')
13
+ .option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
14
+ .option('--max-restart <count>', 'max restart attempts (default: 3)')
15
+ .option('--cooldown <duration>', 'restart cooldown (default: 60s)')
16
+ .action(async (options) => {
17
+ const ctx = await createContext({
18
+ projectPath: options.project ?? process.cwd(),
19
+ coverageTarget: 80
20
+ })
21
+ const intervalMs = options.interval ? parseDuration(options.interval) ?? DEFAULT_INTERVAL_MS : DEFAULT_INTERVAL_MS
22
+ const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
23
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
24
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
25
+ console.log(`[schedule] interval=${intervalMs}ms`)
26
+ await runSchedule(ctx, intervalMs, {
27
+ staleTimeoutMs,
28
+ maxRestart,
29
+ cooldownMs
30
+ })
31
+ })
32
+ }
@@ -0,0 +1,111 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { analyzeBaseline } from '../../core/tools/analyze-baseline.js'
4
+ import { runLoop } from '../../core/tools/run-loop.js'
5
+ import { reportStatus } from '../../core/reporter/index.js'
6
+ import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
7
+ import { startLifecycle } from '../../utils/process-lifecycle.js'
8
+ import { runSchedule } from '../../core/tools/guard.js'
9
+ import { parseDuration } from '../../utils/duration.js'
10
+
11
+ const DEFAULT_GUARD_INTERVAL_MS = 5 * 60_000
12
+
13
+ export function registerStartCommand(program: Command) {
14
+ program
15
+ .command('start')
16
+ .option('--project <path>', 'project path')
17
+ .option('--target <number>', 'coverage target', '80')
18
+ .option('--topN <number>', 'recommendation pool size', '5')
19
+ .option('--scan-dir <dir>', 'scan dir (default: src)')
20
+ .option('--file <path>', 'explicit target source file')
21
+ .option('--permission-mode <mode>', 'Claude CLI permission mode')
22
+ .option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
23
+ .option('--timeout <ms>', 'Claude CLI timeout in ms')
24
+ .option('--verify-cmd <cmd>', 'override full verify command')
25
+ .option('--lint-cmd <cmd>', 'override lint command')
26
+ .option('--typecheck-cmd <cmd>', 'override typecheck command')
27
+ .option('--schedule', 'run built-in guard schedule')
28
+ .option('--guard-interval <duration>', 'guard interval (default: 5m)')
29
+ .option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
30
+ .option('--max-restart <count>', 'max restart attempts (default: 3)')
31
+ .option('--cooldown <duration>', 'restart cooldown (default: 60s)')
32
+ .action(async (options) => {
33
+ const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
34
+ const commandOverrides = {
35
+ ...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
36
+ ...(options.lintCmd ? { lint: options.lintCmd } : {}),
37
+ ...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
38
+ }
39
+
40
+ console.log('[1/4] 初始化配置...')
41
+ const ctx = await createContext({
42
+ projectPath: options.project ?? process.cwd(),
43
+ coverageTarget: Number(options.target),
44
+ configOverrides: {
45
+ ...(include ? { include } : {}),
46
+ ...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
47
+ }
48
+ })
49
+ await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
50
+ console.log(`[1/4] 初始化完成:${ctx.paths.configPath}`)
51
+
52
+ const lifecycle = await startLifecycle(ctx, {
53
+ command: 'start',
54
+ argv: process.argv.slice(2)
55
+ })
56
+
57
+ try {
58
+ if (options.schedule) {
59
+ const intervalMs = options.guardInterval ? parseDuration(options.guardInterval) ?? DEFAULT_GUARD_INTERVAL_MS : DEFAULT_GUARD_INTERVAL_MS
60
+ const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
61
+ const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
62
+ const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
63
+ await runSchedule(ctx, intervalMs, {
64
+ staleTimeoutMs,
65
+ maxRestart,
66
+ cooldownMs
67
+ })
68
+ }
69
+
70
+ console.log('[2/4] 开始基线分析...')
71
+ const baseline = await analyzeBaseline(ctx)
72
+ console.log(`[2/4] ${baseline.summary}`)
73
+
74
+ console.log('[3/4] 调用 Claude CLI 补测...')
75
+ const runResult = await runLoop(ctx, {
76
+ targetFile: options.file,
77
+ topN: Number(options.topN),
78
+ permissionMode: options.permissionMode,
79
+ allowedTools: parseAllowedTools(options.allowedTools),
80
+ timeoutMs: options.timeout ? Number(options.timeout) : undefined
81
+ })
82
+ console.log(`[3/4] ${runResult.summary}`)
83
+
84
+ console.log('[4/4] 输出状态...')
85
+ const status = await reportStatus(ctx)
86
+ console.log(`[4/4] ${status.summary}`)
87
+ if (runResult.artifacts?.finalReportPath) {
88
+ console.log(`[4/4] report generated: ${runResult.artifacts.finalReportPath}`)
89
+ }
90
+
91
+ await lifecycle.stop()
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error)
94
+ await ctx.lifecycleStore.patch({
95
+ status: 'crashed',
96
+ lastError: message,
97
+ updatedAt: ctx.clock.nowIso()
98
+ })
99
+ throw error
100
+ }
101
+ })
102
+ }
103
+
104
+ function parseAllowedTools(value?: string): string[] | undefined {
105
+ if (!value) return undefined
106
+ const tools = value
107
+ .split(',')
108
+ .map((item) => item.trim())
109
+ .filter(Boolean)
110
+ return tools.length > 0 ? tools : undefined
111
+ }
@@ -0,0 +1,30 @@
1
+ import { Command } from 'commander'
2
+ import { createContext } from '../context/create-context.js'
3
+ import { reportStatus, buildFinalReport } from '../../core/reporter/index.js'
4
+
5
+ export function registerStatusCommand(program: Command) {
6
+ program
7
+ .command('status')
8
+ .option('--project <path>', 'project path')
9
+ .action(async (options) => {
10
+ const ctx = await createContext({
11
+ projectPath: options.project ?? process.cwd(),
12
+ coverageTarget: 80
13
+ })
14
+ const result = await reportStatus(ctx)
15
+ console.log(result.summary)
16
+ })
17
+
18
+ program
19
+ .command('report')
20
+ .option('--project <path>', 'project path')
21
+ .action(async (options) => {
22
+ const ctx = await createContext({
23
+ projectPath: options.project ?? process.cwd(),
24
+ coverageTarget: 80
25
+ })
26
+ const report = await buildFinalReport(ctx)
27
+ await ctx.reportStore.saveFinalReport(report)
28
+ console.log('report generated')
29
+ })
30
+ }