sillyspec 3.26.10 → 3.26.13

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 (101) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +85 -85
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
  4. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  5. package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
  6. package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
  7. package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
  8. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  9. package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
  10. package/.claude/skills/sillyspec-quick/SKILL.md +115 -111
  11. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  12. package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
  13. package/.claude/skills/sillyspec-state/SKILL.md +64 -64
  14. package/.claude/skills/sillyspec-status/SKILL.md +54 -54
  15. package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
  17. package/.gitattributes +2 -0
  18. package/.husky/pre-push +20 -20
  19. package/README.md +203 -201
  20. package/SKILL.md +91 -90
  21. package/package.json +39 -39
  22. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  23. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  24. package/packages/dashboard/dist/index.html +16 -16
  25. package/packages/dashboard/package.json +25 -25
  26. package/packages/dashboard/server/executor.js +95 -86
  27. package/packages/dashboard/server/index.js +663 -601
  28. package/packages/dashboard/server/parser.js +526 -526
  29. package/src/change-risk-profile.js +254 -254
  30. package/src/classify-change.js +109 -109
  31. package/src/config-schema.js +313 -310
  32. package/src/contract-matrix.js +388 -379
  33. package/src/db.js +370 -350
  34. package/src/dispatch/backends/local-agent.js +155 -155
  35. package/src/dispatch/backends/sillyhub-mcp.js +245 -245
  36. package/src/dispatch/probe.js +246 -210
  37. package/src/dispatch/strategy.js +121 -121
  38. package/src/docs-check.js +188 -16
  39. package/src/docs-debt.js +6 -2
  40. package/src/docs-gate.js +1 -0
  41. package/src/doctor-diagnostics.js +898 -711
  42. package/src/endpoint-extractor.js +401 -401
  43. package/src/fs-atomic.js +71 -69
  44. package/src/git-helper.js +82 -79
  45. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  46. package/src/hooks/worktree-guard.js +854 -848
  47. package/src/index.js +251 -20
  48. package/src/init.js +624 -623
  49. package/src/knowledge-match.js +130 -130
  50. package/src/local-detect.js +128 -120
  51. package/src/machine-interface.js +543 -543
  52. package/src/migrate.js +127 -127
  53. package/src/modules.js +7 -5
  54. package/src/progress/change-registry.js +297 -260
  55. package/src/progress/consistency-doctor.js +355 -355
  56. package/src/progress/shared.js +36 -36
  57. package/src/progress/stage-machine.js +589 -550
  58. package/src/progress/step-store.js +261 -261
  59. package/src/progress.js +1285 -1127
  60. package/src/quicklog.js +724 -693
  61. package/src/review-tier.js +77 -77
  62. package/src/run/command.js +1408 -1364
  63. package/src/run/complete-handlers.js +1450 -1399
  64. package/src/run/complete.js +1181 -920
  65. package/src/run/gates.js +878 -755
  66. package/src/run/prompt.js +765 -761
  67. package/src/run/quick-audit.js +155 -146
  68. package/src/run/shared.js +1348 -1215
  69. package/src/run/stage.js +527 -479
  70. package/src/scan-postcheck.js +371 -371
  71. package/src/setup.js +414 -416
  72. package/src/sillyhub-mcp/client.js +597 -529
  73. package/src/spec-dir-typo.js +3 -24
  74. package/src/spec-sync.js +358 -319
  75. package/src/stage-contract.js +871 -897
  76. package/src/stage-review.js +549 -549
  77. package/src/stages/archive.js +156 -155
  78. package/src/stages/brainstorm-auto.js +236 -236
  79. package/src/stages/brainstorm.js +597 -597
  80. package/src/stages/cmd-existence.js +145 -145
  81. package/src/stages/doctor.js +386 -371
  82. package/src/stages/execute.js +1014 -1003
  83. package/src/stages/explore.js +34 -34
  84. package/src/stages/index.js +29 -29
  85. package/src/stages/knowledge.js +498 -498
  86. package/src/stages/plan-postcheck.js +1323 -1302
  87. package/src/stages/plan.js +642 -623
  88. package/src/stages/quick.js +153 -151
  89. package/src/stages/scan.js +607 -607
  90. package/src/stages/status.js +64 -64
  91. package/src/stages/verify.js +264 -264
  92. package/src/sync.js +1325 -1135
  93. package/src/task-review.js +1204 -1019
  94. package/src/taskcard.js +178 -0
  95. package/src/verify-postcheck.js +1199 -1186
  96. package/src/workflow.js +600 -600
  97. package/src/worktree-apply.js +1237 -1136
  98. package/src/worktree-deps.js +453 -353
  99. package/src/worktree.js +1527 -1481
  100. package/templates/workflows/archive-impact.yaml +79 -79
  101. package/templates/workflows/scan-docs.yaml +132 -132
@@ -1,130 +1,130 @@
1
- /**
2
- * knowledge-match.js — knowledge 关键词匹配引擎
3
- * 从 INDEX.md 解析知识条目,按任务上下文匹配并生成 hit report
4
- */
5
-
6
- import { existsSync, readFileSync } from 'fs'
7
- import { join } from 'path'
8
-
9
- /**
10
- * 从 INDEX.md 解析所有知识条目
11
- * @param {string} indexDir - knowledge 目录路径
12
- * @returns {{ category: string, keywords: string[], file: string, anchor: string, display: string, line: string }[]}
13
- */
14
- export function parseKnowledgeIndex(indexDir) {
15
- const indexPath = join(indexDir, 'INDEX.md')
16
- if (!existsSync(indexPath)) return []
17
-
18
- const content = readFileSync(indexPath, 'utf8')
19
- const entries = []
20
- let currentCategory = ''
21
-
22
- for (const line of content.split('\n')) {
23
- // 匹配 ## Category 行
24
- const catMatch = line.match(/^##\s+(.+)/)
25
- if (catMatch) {
26
- currentCategory = catMatch[1].trim()
27
- continue
28
- }
29
-
30
- // 匹配条目行:- 关键词1|关键词2 → [显示名](文件名#锚点)
31
- const entryMatch = line.match(/^-\s+(.+?)\s*→\s*\[(.+?)\]\(([^#)]+)(?:#([^)]+))?\)/)
32
- if (entryMatch) {
33
- const keywords = entryMatch[1].split('|').map(k => k.trim()).filter(Boolean)
34
- const display = entryMatch[2].trim()
35
- const file = entryMatch[3].trim()
36
- const anchor = entryMatch[4] ? entryMatch[4].trim() : ''
37
- entries.push({ category: currentCategory, keywords, file, anchor, display, line })
38
- }
39
- }
40
-
41
- return entries
42
- }
43
-
44
- function escapeRegex(s) {
45
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
46
- }
47
-
48
- /**
49
- * 单个关键词是否命中上下文。
50
- * - 过短关键词(<2 字符)视为噪音,不参与匹配
51
- * - 非 ASCII(中文等)用子串匹配
52
- * - ASCII 关键词用词边界匹配,避免子串误命中(如 "DB" 不命中 "dashboard")
53
- */
54
- function keywordMatchesContext(keyword, contextLower) {
55
- const kw = keyword.toLowerCase().trim()
56
- if (kw.length < 2) return false
57
- if (/[^\x00-\x7f]/.test(kw)) return contextLower.includes(kw)
58
- return new RegExp(`(^|[^a-z0-9])${escapeRegex(kw)}([^a-z0-9]|$)`).test(contextLower)
59
- }
60
-
61
- /**
62
- * 用任务上下文匹配知识条目
63
- * @param {string} indexDir - knowledge 目录路径
64
- * @param {string} taskContext - 任务上下文(task 名称 + 描述,用于关键词匹配)
65
- * @returns {{ matched: boolean, entries: Array, report: string, json: object }}
66
- */
67
- export function matchKnowledge(indexDir, taskContext) {
68
- const indexPath = join(indexDir, 'INDEX.md')
69
-
70
- // INDEX.md 不存在
71
- if (!existsSync(indexPath)) {
72
- return {
73
- matched: false,
74
- entries: [],
75
- report: 'Status: no matches (INDEX.md not found)',
76
- json: { matched: false, entry_count: 0, entries: [] }
77
- }
78
- }
79
-
80
- const allEntries = parseKnowledgeIndex(indexDir)
81
- if (allEntries.length === 0 || !taskContext) {
82
- return {
83
- matched: false,
84
- entries: [],
85
- report: 'Status: no matches',
86
- json: { matched: false, entry_count: 0, entries: [] }
87
- }
88
- }
89
-
90
- const contextLower = taskContext.toLowerCase()
91
- const matched = allEntries.filter(entry => {
92
- return entry.keywords.some(kw => keywordMatchesContext(kw, contextLower))
93
- })
94
-
95
- if (matched.length === 0) {
96
- return {
97
- matched: false,
98
- entries: [],
99
- report: 'Status: no matches',
100
- json: { matched: false, entry_count: 0, entries: [] }
101
- }
102
- }
103
-
104
- const sources = matched.map(e => {
105
- const base = e.anchor ? `${e.file}#${e.anchor}` : e.file
106
- return ` - ${base}`
107
- }).join('\n')
108
-
109
- const report = [
110
- 'Knowledge Context',
111
- '─────────────────',
112
- `Status: matched`,
113
- `Entries: ${matched.length}`,
114
- 'Sources:',
115
- sources
116
- ].join('\n')
117
-
118
- const json = {
119
- matched: true,
120
- entry_count: matched.length,
121
- entries: matched.map(e => ({
122
- file: e.file,
123
- anchor: e.anchor,
124
- keywords: e.keywords,
125
- category: e.category
126
- }))
127
- }
128
-
129
- return { matched: true, entries: matched, report, json }
130
- }
1
+ /**
2
+ * knowledge-match.js — knowledge 关键词匹配引擎
3
+ * 从 INDEX.md 解析知识条目,按任务上下文匹配并生成 hit report
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs'
7
+ import { join } from 'path'
8
+
9
+ /**
10
+ * 从 INDEX.md 解析所有知识条目
11
+ * @param {string} indexDir - knowledge 目录路径
12
+ * @returns {{ category: string, keywords: string[], file: string, anchor: string, display: string, line: string }[]}
13
+ */
14
+ export function parseKnowledgeIndex(indexDir) {
15
+ const indexPath = join(indexDir, 'INDEX.md')
16
+ if (!existsSync(indexPath)) return []
17
+
18
+ const content = readFileSync(indexPath, 'utf8')
19
+ const entries = []
20
+ let currentCategory = ''
21
+
22
+ for (const line of content.split('\n')) {
23
+ // 匹配 ## Category 行
24
+ const catMatch = line.match(/^##\s+(.+)/)
25
+ if (catMatch) {
26
+ currentCategory = catMatch[1].trim()
27
+ continue
28
+ }
29
+
30
+ // 匹配条目行:- 关键词1|关键词2 → [显示名](文件名#锚点)
31
+ const entryMatch = line.match(/^-\s+(.+?)\s*→\s*\[(.+?)\]\(([^#)]+)(?:#([^)]+))?\)/)
32
+ if (entryMatch) {
33
+ const keywords = entryMatch[1].split('|').map(k => k.trim()).filter(Boolean)
34
+ const display = entryMatch[2].trim()
35
+ const file = entryMatch[3].trim()
36
+ const anchor = entryMatch[4] ? entryMatch[4].trim() : ''
37
+ entries.push({ category: currentCategory, keywords, file, anchor, display, line })
38
+ }
39
+ }
40
+
41
+ return entries
42
+ }
43
+
44
+ function escapeRegex(s) {
45
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
46
+ }
47
+
48
+ /**
49
+ * 单个关键词是否命中上下文。
50
+ * - 过短关键词(<2 字符)视为噪音,不参与匹配
51
+ * - 非 ASCII(中文等)用子串匹配
52
+ * - ASCII 关键词用词边界匹配,避免子串误命中(如 "DB" 不命中 "dashboard")
53
+ */
54
+ function keywordMatchesContext(keyword, contextLower) {
55
+ const kw = keyword.toLowerCase().trim()
56
+ if (kw.length < 2) return false
57
+ if (/[^\x00-\x7f]/.test(kw)) return contextLower.includes(kw)
58
+ return new RegExp(`(^|[^a-z0-9])${escapeRegex(kw)}([^a-z0-9]|$)`).test(contextLower)
59
+ }
60
+
61
+ /**
62
+ * 用任务上下文匹配知识条目
63
+ * @param {string} indexDir - knowledge 目录路径
64
+ * @param {string} taskContext - 任务上下文(task 名称 + 描述,用于关键词匹配)
65
+ * @returns {{ matched: boolean, entries: Array, report: string, json: object }}
66
+ */
67
+ export function matchKnowledge(indexDir, taskContext) {
68
+ const indexPath = join(indexDir, 'INDEX.md')
69
+
70
+ // INDEX.md 不存在
71
+ if (!existsSync(indexPath)) {
72
+ return {
73
+ matched: false,
74
+ entries: [],
75
+ report: 'Status: no matches (INDEX.md not found)',
76
+ json: { matched: false, entry_count: 0, entries: [] }
77
+ }
78
+ }
79
+
80
+ const allEntries = parseKnowledgeIndex(indexDir)
81
+ if (allEntries.length === 0 || !taskContext) {
82
+ return {
83
+ matched: false,
84
+ entries: [],
85
+ report: 'Status: no matches',
86
+ json: { matched: false, entry_count: 0, entries: [] }
87
+ }
88
+ }
89
+
90
+ const contextLower = taskContext.toLowerCase()
91
+ const matched = allEntries.filter(entry => {
92
+ return entry.keywords.some(kw => keywordMatchesContext(kw, contextLower))
93
+ })
94
+
95
+ if (matched.length === 0) {
96
+ return {
97
+ matched: false,
98
+ entries: [],
99
+ report: 'Status: no matches',
100
+ json: { matched: false, entry_count: 0, entries: [] }
101
+ }
102
+ }
103
+
104
+ const sources = matched.map(e => {
105
+ const base = e.anchor ? `${e.file}#${e.anchor}` : e.file
106
+ return ` - ${base}`
107
+ }).join('\n')
108
+
109
+ const report = [
110
+ 'Knowledge Context',
111
+ '─────────────────',
112
+ `Status: matched`,
113
+ `Entries: ${matched.length}`,
114
+ 'Sources:',
115
+ sources
116
+ ].join('\n')
117
+
118
+ const json = {
119
+ matched: true,
120
+ entry_count: matched.length,
121
+ entries: matched.map(e => ({
122
+ file: e.file,
123
+ anchor: e.anchor,
124
+ keywords: e.keywords,
125
+ category: e.category
126
+ }))
127
+ }
128
+
129
+ return { matched: true, entries: matched, report, json }
130
+ }
@@ -1,120 +1,128 @@
1
- /**
2
- * Local Detect — 纯 fs 项目类型嗅探
3
- *
4
- * 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
5
- * 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
6
- * commands,不该被迫跑完整 scan。
7
- *
8
- * 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
9
- * 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
10
- * 不引 yaml 依赖。
11
- */
12
-
13
- import { existsSync, readFileSync } from 'fs'
14
- import { join } from 'path'
15
-
16
- /**
17
- * 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
18
- * 支持两种常见写法:
19
- * - 同行命令:`test: pytest` → 'pytest'
20
- * - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
21
- * 边界:
22
- * - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
23
- * - 命令行内 `#` 注释截断;取第一个非空命令行
24
- * @param {string} makefileText
25
- * @returns {string}
26
- */
27
- function parseMakefileTestCommand(makefileText) {
28
- if (!makefileText) return 'make test'
29
- // 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
30
- const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
31
- if (!head) return 'make test'
32
- const inline = (head[1] || '').trim()
33
- if (inline) return inline
34
- // 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
35
- const after = makefileText.slice(head.index + head[0].length)
36
- const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
37
- if (cont && cont[1]) return cont[1].trim()
38
- return 'make test'
39
- }
40
-
41
- /**
42
- * 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
43
- *
44
- * 嗅探规则(按顺序,命中即返回):
45
- * - package.json → nodejs(核验 scripts.{build,test,lint} 存在才写对应键;缺失不写)
46
- * - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
47
- * - build.gradle → gradle(gradlew 存在用 ./gradlew 否则 gradle;build / test / check)
48
- * - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
49
- * - 都没有 → generic(commands 为空对象)
50
- *
51
- * @param {string} workdir - 项目根目录
52
- * @returns {{
53
- * project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
54
- * commands: { build?: string, test?: string, lint?: string }
55
- * }}
56
- */
57
- export function detectLocalYaml(workdir) {
58
- // 1. nodejs
59
- if (existsSync(join(workdir, 'package.json'))) {
60
- const pkgPath = join(workdir, 'package.json')
61
- const raw = readFileSync(pkgPath, 'utf8')
62
- let pkg
63
- try {
64
- pkg = JSON.parse(raw)
65
- } catch (e) {
66
- throw new Error('package.json 解析失败:' + pkgPath)
67
- }
68
- const scripts = (pkg && pkg.scripts) || {}
69
- const commands = {}
70
- if (scripts.build) commands.build = 'npm run build'
71
- if (scripts.test) commands.test = 'npm test'
72
- if (scripts.lint) commands.lint = 'npm run lint'
73
- return {
74
- project: { type: 'nodejs' },
75
- commands,
76
- }
77
- }
78
-
79
- // 2. maven
80
- if (existsSync(join(workdir, 'pom.xml'))) {
81
- return {
82
- project: { type: 'maven' },
83
- commands: {
84
- build: 'mvn compile',
85
- test: 'mvn test',
86
- lint: 'mvn checkstyle:check',
87
- },
88
- }
89
- }
90
-
91
- // 3. gradle
92
- if (existsSync(join(workdir, 'build.gradle'))) {
93
- const gradlePrefix = existsSync(join(workdir, 'gradlew')) ? './gradlew' : 'gradle'
94
- return {
95
- project: { type: 'gradle' },
96
- commands: {
97
- build: `${gradlePrefix} build`,
98
- test: `${gradlePrefix} test`,
99
- lint: `${gradlePrefix} check`,
100
- },
101
- }
102
- }
103
-
104
- // 4. make(test 命令从 Makefile 解析;build/lint 无则省略)
105
- const makefilePath = join(workdir, 'Makefile')
106
- if (existsSync(makefilePath)) {
107
- const makefileText = readFileSync(makefilePath, 'utf8')
108
- const test = parseMakefileTestCommand(makefileText)
109
- return {
110
- project: { type: 'make' },
111
- commands: { test },
112
- }
113
- }
114
-
115
- // 5. generic
116
- return {
117
- project: { type: 'generic' },
118
- commands: {},
119
- }
120
- }
1
+ /**
2
+ * Local Detect — 纯 fs 项目类型嗅探
3
+ *
4
+ * 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
5
+ * 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
6
+ * commands,不该被迫跑完整 scan。
7
+ *
8
+ * 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
9
+ * 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
10
+ * 不引 yaml 依赖。
11
+ */
12
+
13
+ import { existsSync, readFileSync } from 'fs'
14
+ import { join } from 'path'
15
+
16
+ /**
17
+ * 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
18
+ * 支持两种常见写法:
19
+ * - 同行命令:`test: pytest` → 'pytest'
20
+ * - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
21
+ * 边界:
22
+ * - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
23
+ * - 命令行内 `#` 注释截断;取第一个非空命令行
24
+ * @param {string} makefileText
25
+ * @returns {string}
26
+ */
27
+ function parseMakefileTestCommand(makefileText) {
28
+ if (!makefileText) return 'make test'
29
+ // 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
30
+ const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
31
+ if (!head) return 'make test'
32
+ const inline = (head[1] || '').trim()
33
+ if (inline) return inline
34
+ // 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
35
+ const after = makefileText.slice(head.index + head[0].length)
36
+ const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
37
+ if (cont && cont[1]) return cont[1].trim()
38
+ return 'make test'
39
+ }
40
+
41
+ /**
42
+ * 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
43
+ *
44
+ * 嗅探规则(按顺序,命中即返回):
45
+ * - package.json → nodejs(核验 scripts.{build,test,lint} 存在才写对应键;缺失不写)
46
+ * - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
47
+ * - build.gradle → gradle(gradlew 存在用 ./gradlew 否则 gradle;build / test / check)
48
+ * - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
49
+ * - 都没有 → generic(commands 为空对象)
50
+ *
51
+ * @param {string} workdir - 项目根目录
52
+ * @returns {{
53
+ * project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
54
+ * commands: { build?: string, test?: string, lint?: string }
55
+ * }}
56
+ */
57
+ export function detectLocalYaml(workdir) {
58
+ // 1. nodejs
59
+ if (existsSync(join(workdir, 'package.json'))) {
60
+ const pkgPath = join(workdir, 'package.json')
61
+ const raw = readFileSync(pkgPath, 'utf8')
62
+ let pkg
63
+ try {
64
+ pkg = JSON.parse(raw)
65
+ } catch (e) {
66
+ throw new Error('package.json 解析失败:' + pkgPath)
67
+ }
68
+ const scripts = (pkg && pkg.scripts) || {}
69
+ const commands = {}
70
+ if (scripts.build) commands.build = 'npm run build'
71
+ if (scripts.test) commands.test = 'npm test'
72
+ if (scripts.lint) commands.lint = 'npm run lint'
73
+ return {
74
+ project: { type: 'nodejs' },
75
+ commands,
76
+ }
77
+ }
78
+
79
+ // 2. maven
80
+ if (existsSync(join(workdir, 'pom.xml'))) {
81
+ return {
82
+ project: { type: 'maven' },
83
+ commands: {
84
+ build: 'mvn compile',
85
+ test: 'mvn test',
86
+ lint: 'mvn checkstyle:check',
87
+ },
88
+ }
89
+ }
90
+
91
+ // 3. gradle
92
+ if (existsSync(join(workdir, 'build.gradle'))) {
93
+ // Windows 上 cmd.exe 执行不了 ./gradlew(shell 脚本)——优先 gradlew.bat;
94
+ // 仅有 POSIX gradlew 且在 win32 时退回全局 gradle(体检 BUG-08)
95
+ const isWin = process.platform === 'win32'
96
+ let gradlePrefix = 'gradle'
97
+ if (isWin) {
98
+ gradlePrefix = existsSync(join(workdir, 'gradlew.bat')) ? 'gradlew.bat' : 'gradle'
99
+ } else {
100
+ gradlePrefix = existsSync(join(workdir, 'gradlew')) ? './gradlew' : 'gradle'
101
+ }
102
+ return {
103
+ project: { type: 'gradle' },
104
+ commands: {
105
+ build: `${gradlePrefix} build`,
106
+ test: `${gradlePrefix} test`,
107
+ lint: `${gradlePrefix} check`,
108
+ },
109
+ }
110
+ }
111
+
112
+ // 4. make(test 命令从 Makefile 解析;build/lint 无则省略)
113
+ const makefilePath = join(workdir, 'Makefile')
114
+ if (existsSync(makefilePath)) {
115
+ const makefileText = readFileSync(makefilePath, 'utf8')
116
+ const test = parseMakefileTestCommand(makefileText)
117
+ return {
118
+ project: { type: 'make' },
119
+ commands: { test },
120
+ }
121
+ }
122
+
123
+ // 5. generic
124
+ return {
125
+ project: { type: 'generic' },
126
+ commands: {},
127
+ }
128
+ }