sillyspec 3.26.9 → 3.26.11

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 (98) 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 +111 -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 -81
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
  17. package/.gitattributes +2 -0
  18. package/.husky/pre-push +20 -20
  19. package/.npmrc-publish +2 -0
  20. package/README.md +201 -201
  21. package/SKILL.md +90 -90
  22. package/package.json +39 -39
  23. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  24. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/package.json +25 -25
  27. package/packages/dashboard/server/executor.js +86 -86
  28. package/packages/dashboard/server/index.js +601 -601
  29. package/packages/dashboard/server/parser.js +526 -526
  30. package/src/change-risk-profile.js +254 -254
  31. package/src/classify-change.js +109 -109
  32. package/src/config-schema.js +310 -310
  33. package/src/contract-matrix.js +379 -379
  34. package/src/db.js +350 -350
  35. package/src/dispatch/backends/local-agent.js +155 -155
  36. package/src/dispatch/backends/sillyhub-mcp.js +245 -245
  37. package/src/dispatch/probe.js +210 -210
  38. package/src/dispatch/strategy.js +121 -121
  39. package/src/docs-check.js +131 -4
  40. package/src/doctor-diagnostics.js +711 -711
  41. package/src/endpoint-extractor.js +401 -401
  42. package/src/fs-atomic.js +69 -69
  43. package/src/git-helper.js +79 -79
  44. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  45. package/src/hooks/worktree-guard.js +848 -848
  46. package/src/index.js +90 -8
  47. package/src/init.js +623 -623
  48. package/src/knowledge-match.js +130 -130
  49. package/src/local-detect.js +120 -120
  50. package/src/machine-interface.js +543 -543
  51. package/src/migrate.js +127 -127
  52. package/src/progress/change-registry.js +297 -260
  53. package/src/progress/consistency-doctor.js +355 -355
  54. package/src/progress/shared.js +36 -36
  55. package/src/progress/stage-machine.js +29 -1
  56. package/src/progress/step-store.js +261 -261
  57. package/src/progress.js +1131 -1127
  58. package/src/quicklog.js +693 -675
  59. package/src/review-tier.js +77 -77
  60. package/src/run/command.js +1385 -1364
  61. package/src/run/complete-handlers.js +198 -9
  62. package/src/run/complete.js +225 -24
  63. package/src/run/concurrent-detect.js +22 -0
  64. package/src/run/gates.js +755 -755
  65. package/src/run/prompt.js +761 -721
  66. package/src/run/quick-audit.js +155 -146
  67. package/src/run/shared.js +1268 -1197
  68. package/src/run/stage.js +487 -479
  69. package/src/scan-postcheck.js +371 -371
  70. package/src/setup.js +416 -416
  71. package/src/sillyhub-mcp/client.js +529 -529
  72. package/src/spec-sync.js +323 -0
  73. package/src/stage-contract.js +897 -897
  74. package/src/stage-review.js +549 -549
  75. package/src/stages/archive.js +155 -154
  76. package/src/stages/brainstorm-auto.js +236 -236
  77. package/src/stages/brainstorm.js +596 -597
  78. package/src/stages/cmd-existence.js +145 -145
  79. package/src/stages/doctor.js +371 -371
  80. package/src/stages/execute.js +1003 -991
  81. package/src/stages/explore.js +34 -34
  82. package/src/stages/index.js +29 -29
  83. package/src/stages/knowledge.js +498 -498
  84. package/src/stages/plan-postcheck.js +1302 -1281
  85. package/src/stages/plan.js +625 -604
  86. package/src/stages/quick.js +151 -143
  87. package/src/stages/scan.js +606 -605
  88. package/src/stages/status.js +64 -64
  89. package/src/stages/verify.js +264 -261
  90. package/src/sync.js +1223 -1125
  91. package/src/task-review.js +1019 -1019
  92. package/src/verify-postcheck.js +1186 -1186
  93. package/src/workflow.js +600 -600
  94. package/src/worktree-apply.js +1213 -1136
  95. package/src/worktree-deps.js +353 -353
  96. package/src/worktree.js +1481 -1437
  97. package/templates/workflows/archive-impact.yaml +79 -79
  98. 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,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
- 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
+ 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
+ }