sillyspec 3.26.10 → 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.
- package/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/.npmrc-publish +2 -0
- package/README.md +201 -201
- package/SKILL.md +90 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +601 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +310 -310
- package/src/contract-matrix.js +379 -379
- package/src/db.js +350 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +210 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +131 -4
- package/src/doctor-diagnostics.js +711 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +79 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +848 -848
- package/src/index.js +90 -8
- package/src/init.js +623 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +120 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +29 -1
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1131 -1127
- package/src/quicklog.js +693 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1385 -1364
- package/src/run/complete-handlers.js +1443 -1399
- package/src/run/complete.js +225 -24
- package/src/run/gates.js +755 -755
- package/src/run/prompt.js +761 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1268 -1215
- package/src/run/stage.js +487 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +416 -416
- package/src/sillyhub-mcp/client.js +529 -529
- package/src/spec-sync.js +323 -319
- package/src/stage-contract.js +897 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +155 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +596 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +371 -371
- package/src/stages/execute.js +1003 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1302 -1302
- package/src/stages/plan.js +625 -623
- package/src/stages/quick.js +151 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1223 -1135
- package/src/task-review.js +1019 -1019
- package/src/verify-postcheck.js +1186 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1213 -1136
- package/src/worktree-deps.js +353 -353
- package/src/worktree.js +1481 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/knowledge-match.js
CHANGED
|
@@ -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
|
+
}
|
package/src/local-detect.js
CHANGED
|
@@ -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
|
+
}
|