sillyspec 3.22.8 → 3.23.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.
- package/package.json +1 -1
- package/src/hooks/worktree-guard.js +86 -31
- package/src/index.js +141 -5
- package/src/local-detect.js +110 -0
- package/src/machine-interface.js +471 -0
- package/src/progress.js +110 -5
- package/src/run.js +227 -46
- package/src/stage-contract.js +107 -1
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/quick.js +25 -14
- package/src/stages/scan.js +7 -16
- package/src/stages/verify.js +3 -1
- package/src/sync.js +73 -5
- package/src/task-review.js +104 -1
- package/src/verify-postcheck.js +433 -0
- package/src/worktree-apply.js +19 -4
- package/src/worktree.js +12 -0
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - 拦截提示针对每个阶段给出具体修复建议
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
13
13
|
import path from 'path'
|
|
14
14
|
|
|
15
15
|
// ── 常量 ──
|
|
@@ -555,6 +555,72 @@ function buildStageHint(stage) {
|
|
|
555
555
|
return hint.join('\n')
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* 读取并合并所有活跃 quick session 的 guard(D-002@v1)
|
|
560
|
+
*
|
|
561
|
+
* 为什么合并而不是读单个 session:
|
|
562
|
+
* - hook 是独立进程(agent 写文件时触发,非 quick CLI),无法可靠知道当前 agent 属于哪个 quick session。
|
|
563
|
+
* - `current-quick-run-id` 是单文件多会话覆盖,读到的可能是他者 session。
|
|
564
|
+
* - 对策:读所有活跃 quick-sessions 下各 session 的 guard.json,合并 baselineFiles / allowedFiles 并集。
|
|
565
|
+
* 多会话时保护范围过宽(保护所有 session 的 baseline),但不误拦任何 session 的合法写——安全侧倾斜。
|
|
566
|
+
*
|
|
567
|
+
* 兼容:旧版单文件 `.runtime/quick-guard.json`(task-03 前格式,无 session 目录)也并入合并。
|
|
568
|
+
*
|
|
569
|
+
* @param {string} projectRoot
|
|
570
|
+
* @returns {{ baselineFiles: string[], allowedFiles: string[], hasGuard: boolean }}
|
|
571
|
+
* - baselineFiles/allowedFiles 为所有活跃 session 的并集(去重,保持首次出现顺序)
|
|
572
|
+
* - hasGuard 表示是否存在任何 guard(用于区分"无 quick 任务"与"有 guard 但列表为空")
|
|
573
|
+
*/
|
|
574
|
+
function readAllQuickGuards(projectRoot) {
|
|
575
|
+
const baselineSet = new Set()
|
|
576
|
+
const allowedSet = new Set()
|
|
577
|
+
let hasGuard = false
|
|
578
|
+
|
|
579
|
+
const sessionsDir = path.join(projectRoot, '.sillyspec', '.runtime', 'quick-sessions')
|
|
580
|
+
let sessionDirs = []
|
|
581
|
+
try {
|
|
582
|
+
const entries = readdirSync(sessionsDir, { withFileTypes: true })
|
|
583
|
+
sessionDirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
584
|
+
} catch { /* 目录不存在或不可读 → 无活跃 session */ }
|
|
585
|
+
|
|
586
|
+
for (const sessionName of sessionDirs) {
|
|
587
|
+
const guardFile = path.join(sessionsDir, sessionName, 'guard.json')
|
|
588
|
+
if (!existsSync(guardFile)) continue
|
|
589
|
+
let guard
|
|
590
|
+
try {
|
|
591
|
+
guard = JSON.parse(readFileSync(guardFile, 'utf8'))
|
|
592
|
+
} catch { continue } // 损坏的 guard.json 跳过(不污染并集)
|
|
593
|
+
hasGuard = true
|
|
594
|
+
for (const f of Array.isArray(guard.baselineFiles) ? guard.baselineFiles : []) {
|
|
595
|
+
if (typeof f === 'string' && f) baselineSet.add(f)
|
|
596
|
+
}
|
|
597
|
+
for (const f of Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []) {
|
|
598
|
+
if (typeof f === 'string' && f) allowedSet.add(f)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 兼容:旧版单文件 quick-guard.json(task-03 前格式,迁移期残留)
|
|
603
|
+
const legacyGuardFile = path.join(projectRoot, '.sillyspec', '.runtime', 'quick-guard.json')
|
|
604
|
+
if (existsSync(legacyGuardFile)) {
|
|
605
|
+
try {
|
|
606
|
+
const guard = JSON.parse(readFileSync(legacyGuardFile, 'utf8'))
|
|
607
|
+
hasGuard = true
|
|
608
|
+
for (const f of Array.isArray(guard.baselineFiles) ? guard.baselineFiles : []) {
|
|
609
|
+
if (typeof f === 'string' && f) baselineSet.add(f)
|
|
610
|
+
}
|
|
611
|
+
for (const f of Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []) {
|
|
612
|
+
if (typeof f === 'string' && f) allowedSet.add(f)
|
|
613
|
+
}
|
|
614
|
+
} catch { /* 旧文件损坏,忽略 */ }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
baselineFiles: [...baselineSet],
|
|
619
|
+
allowedFiles: [...allowedSet],
|
|
620
|
+
hasGuard,
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
558
624
|
// ── 公共接口 ──
|
|
559
625
|
|
|
560
626
|
/**
|
|
@@ -595,26 +661,20 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
595
661
|
}
|
|
596
662
|
}
|
|
597
663
|
|
|
598
|
-
// quick 阶段:检查
|
|
664
|
+
// quick 阶段:检查 baselineFiles(合并所有活跃 session guard 并集,D-002@v1)
|
|
599
665
|
if (stage === 'quick') {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
`当前 quick 任务不能修改任务开始前已修改的文件。`,
|
|
612
|
-
`如确需修改,请在 quick 完成后单独处理此文件。`,
|
|
613
|
-
].join('\n')
|
|
614
|
-
}
|
|
666
|
+
const { baselineFiles } = readAllQuickGuards(projectRoot)
|
|
667
|
+
const relTarget = path.relative(projectRoot, absPath)
|
|
668
|
+
// 如果目标是 baseline protected file,阻止写入(并集 = 保护所有 session 的 baseline)
|
|
669
|
+
if (baselineFiles.some(f => relTarget === f || relTarget.startsWith(f + path.sep))) {
|
|
670
|
+
return {
|
|
671
|
+
blocked: true,
|
|
672
|
+
reason: [
|
|
673
|
+
`⚠️ quick 变更边界保护:${relTarget} 是 baseline 文件,不允许覆盖。`,
|
|
674
|
+
`当前 quick 任务不能修改任务开始前已修改的文件。`,
|
|
675
|
+
`如确需修改,请在 quick 完成后单独处理此文件。`,
|
|
676
|
+
].join('\n')
|
|
615
677
|
}
|
|
616
|
-
} catch {
|
|
617
|
-
// quick-guard.json 不存在(非 quick 任务或未记录),放行
|
|
618
678
|
}
|
|
619
679
|
return { blocked: false }
|
|
620
680
|
}
|
|
@@ -679,24 +739,19 @@ export function shouldBlockBash(command, cwd) {
|
|
|
679
739
|
}
|
|
680
740
|
}
|
|
681
741
|
|
|
682
|
-
// quick
|
|
742
|
+
// quick 阶段:检查合并后的 baselineFiles(所有活跃 session guard 并集,D-002@v1)
|
|
683
743
|
if (stage === 'quick') {
|
|
684
744
|
// 危险黑名单仍然拦截
|
|
685
745
|
if (matchDangerBlacklist(command)) {
|
|
686
746
|
return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
|
|
687
747
|
}
|
|
688
|
-
// 检查命令是否会覆盖 baseline files
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// 检查命令中是否引用了 baseline file
|
|
694
|
-
for (const f of baselineFiles) {
|
|
695
|
-
if (command.includes(f) && (command.includes('> ') || command.includes(' tee ') || command.includes('sed ') || command.includes('mv '))) {
|
|
696
|
-
return { blocked: true, reason: `quick 变更边界保护:命令可能覆盖 baseline 文件 ${f}` }
|
|
697
|
-
}
|
|
748
|
+
// 检查命令是否会覆盖 baseline files(并集 = 保护所有 session 的 baseline)
|
|
749
|
+
const { baselineFiles } = readAllQuickGuards(projectRoot)
|
|
750
|
+
for (const f of baselineFiles) {
|
|
751
|
+
if (command.includes(f) && (command.includes('> ') || command.includes(' tee ') || command.includes('sed ') || command.includes('mv '))) {
|
|
752
|
+
return { blocked: true, reason: `quick 变更边界保护:命令可能覆盖 baseline 文件 ${f}` }
|
|
698
753
|
}
|
|
699
|
-
}
|
|
754
|
+
}
|
|
700
755
|
return { blocked: false }
|
|
701
756
|
}
|
|
702
757
|
|
package/src/index.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
|
|
7
7
|
* 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { join, resolve } from 'path';
|
|
11
11
|
import { cmdInit, getVersion } from './init.js';
|
|
12
12
|
import { ProgressManager, resolvePlatformSpecDir } from './progress.js';
|
|
13
|
+
import { detectLocalYaml } from './local-detect.js';
|
|
13
14
|
|
|
14
15
|
// ── CLI 入口 ──
|
|
15
16
|
|
|
@@ -57,7 +58,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
57
58
|
|
|
58
59
|
sillyspec progress <cmd> 进度记录(轻量,不强制顺序)
|
|
59
60
|
init | show | set-stage <stage> | add-step <stage> <name> |
|
|
60
|
-
update-step <stage> <name> --status <st> [--output <t>] | complete-stage <stage> |
|
|
61
|
+
update-step <stage> <name> --status <st> [--output <t>] [--force] | complete-stage <stage> [--force] |
|
|
61
62
|
check | repair [--apply] | validate | reset [--stage X] |
|
|
62
63
|
batch --total N --completed M [--failed F] [--skipped K] | batch --status
|
|
63
64
|
|
|
@@ -69,8 +70,12 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
69
70
|
cleanup <change> [--force] 清理 worktree
|
|
70
71
|
doctor [--fix] [--stale-hours N] 健康检查 + 修复
|
|
71
72
|
|
|
73
|
+
sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
|
|
74
|
+
|
|
72
75
|
sillyspec workflow check <name> [--project <p>] [--change <c>] [--json]
|
|
73
76
|
sillyspec workflow list
|
|
77
|
+
sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
|
|
78
|
+
sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
|
|
74
79
|
sillyspec modules <rebuild | status | migrate>
|
|
75
80
|
sillyspec change-rename <旧变更名> <新变更名>
|
|
76
81
|
sillyspec knowledge <search --query "..." --limit N
|
|
@@ -106,6 +111,23 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
106
111
|
`);
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// --json 模式输出纪律(design §8 风险表):machine-interface 的 runGate/runDerive 执行期间,
|
|
115
|
+
// 被调模块(validators/task-review/verify-postcheck)的人类可读 console.log 会污染 stdout 破坏 JSON。
|
|
116
|
+
// 在整个调用期间把 console.log/info 重定向到 stderr,stdout 留给最终 JSON envelope(D-005@v1)。
|
|
117
|
+
async function withJsonOutput(json, fn) {
|
|
118
|
+
if (!json) return fn();
|
|
119
|
+
const origLog = console.log;
|
|
120
|
+
const origInfo = console.info;
|
|
121
|
+
console.log = (...a) => process.stderr.write(a.map(String).join(' ') + '\n');
|
|
122
|
+
console.info = (...a) => process.stderr.write(a.map(String).join(' ') + '\n');
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
} finally {
|
|
126
|
+
console.log = origLog;
|
|
127
|
+
console.info = origInfo;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
async function main() {
|
|
110
132
|
const args = process.argv.slice(2);
|
|
111
133
|
|
|
@@ -246,13 +268,14 @@ async function main() {
|
|
|
246
268
|
if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
|
|
247
269
|
if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
|
|
248
270
|
}
|
|
249
|
-
|
|
271
|
+
const updForce = args.includes('--force');
|
|
272
|
+
await pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
|
|
250
273
|
break;
|
|
251
274
|
}
|
|
252
275
|
case 'complete-stage': {
|
|
253
276
|
const compStageName = filteredArgs[2];
|
|
254
|
-
if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage>'); break; }
|
|
255
|
-
pm.completeStage(dir, compStageName, progChangeName);
|
|
277
|
+
if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); break; }
|
|
278
|
+
await pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
|
|
256
279
|
break;
|
|
257
280
|
}
|
|
258
281
|
case 'batch': {
|
|
@@ -286,6 +309,70 @@ async function main() {
|
|
|
286
309
|
}
|
|
287
310
|
break;
|
|
288
311
|
}
|
|
312
|
+
case 'gate': {
|
|
313
|
+
// 机器门控(machine-interface v1):聚合「变更的 <stage> 阶段此刻能否标记完成」的综合结论。
|
|
314
|
+
// 只读查询,不依赖 worktree、不写状态(D-002@v1)。
|
|
315
|
+
const gateStage = filteredArgs[1];
|
|
316
|
+
const gateChangeIdx = args.indexOf('--change');
|
|
317
|
+
const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] ? args[gateChangeIdx + 1] : null;
|
|
318
|
+
if (!gateStage || gateStage.startsWith('-') || !gateChange) {
|
|
319
|
+
console.error('用法: sillyspec gate <stage> --change <name> [--json]\n stage: brainstorm | plan | execute | verify | archive | ...');
|
|
320
|
+
process.exit(2);
|
|
321
|
+
}
|
|
322
|
+
const { runGate } = await import('./machine-interface.js');
|
|
323
|
+
// --spec-dir 透传 runGate.specBase(gate CLI 接线修复,P3 坑 3 sillyspec 侧)。
|
|
324
|
+
// runGate 已分离 cwd(跑测试) + specBase(读 local.yaml/spec),但 gate case 之前
|
|
325
|
+
// 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
|
|
326
|
+
// resolveSpecDir(cwd),向后兼容。
|
|
327
|
+
const gateOpts = { cwd: dir };
|
|
328
|
+
if (specDir) gateOpts.specBase = specDir;
|
|
329
|
+
const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
|
|
330
|
+
if (json) {
|
|
331
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
332
|
+
} else {
|
|
333
|
+
const icon = envelope.ok ? '✅' : '❌';
|
|
334
|
+
console.log(`${icon} gate ${gateStage} [${gateChange}]: ${envelope.ok ? '通过' : '未通过'} (exit ${exitCode})`);
|
|
335
|
+
for (const c of (envelope.checks || [])) {
|
|
336
|
+
const ci = c.ok ? '✅' : '❌';
|
|
337
|
+
const tag = c.informational ? ' (informational)' : '';
|
|
338
|
+
console.log(` ${ci} ${c.id}${tag}`);
|
|
339
|
+
for (const e of (c.errors || [])) console.log(` ✗ ${e}`);
|
|
340
|
+
for (const w of (c.warnings || [])) console.log(` ⚠ ${w}`);
|
|
341
|
+
}
|
|
342
|
+
for (const e of (envelope.errors || [])) console.log(` ✗ ${e}`);
|
|
343
|
+
for (const w of (envelope.warnings || [])) console.log(` ⚠ ${w}`);
|
|
344
|
+
}
|
|
345
|
+
process.exitCode = exitCode;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'derive': {
|
|
349
|
+
// 单项事实核验(machine-interface v1):查询变更某一 facet 的真实状态,返回结构化 data。
|
|
350
|
+
const facet = filteredArgs[1];
|
|
351
|
+
const deriveChangeIdx = args.indexOf('--change');
|
|
352
|
+
const deriveChange = deriveChangeIdx >= 0 && args[deriveChangeIdx + 1] ? args[deriveChangeIdx + 1] : null;
|
|
353
|
+
if (!facet || facet.startsWith('-') || !deriveChange) {
|
|
354
|
+
console.error('用法: sillyspec derive <facet> --change <name> [--json]\n facet: execute-evidence | verify-test | task-reviews | artifacts');
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
const { runDerive } = await import('./machine-interface.js');
|
|
358
|
+
// --spec-dir 透传 runDerive.specBase(与 gate case 对称,P3 坑 3 sillyspec 侧)。
|
|
359
|
+
// derive verify-test facet 内部同样调 runVerifyTestCheck 依赖 specBase,平台模式
|
|
360
|
+
// 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
|
|
361
|
+
const deriveOpts = { cwd: dir };
|
|
362
|
+
if (specDir) deriveOpts.specBase = specDir;
|
|
363
|
+
const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
|
|
364
|
+
if (json) {
|
|
365
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
366
|
+
} else {
|
|
367
|
+
const icon = envelope.ok ? '✅' : '❌';
|
|
368
|
+
console.log(`${icon} derive ${facet} [${deriveChange}]: ${envelope.ok ? '通过' : '未通过'} (exit ${exitCode})`);
|
|
369
|
+
if (envelope.data) console.log(` data: ${JSON.stringify(envelope.data, null, 2)}`);
|
|
370
|
+
for (const e of (envelope.errors || [])) console.log(` ✗ ${e}`);
|
|
371
|
+
for (const w of (envelope.warnings || [])) console.log(` ⚠ ${w}`);
|
|
372
|
+
}
|
|
373
|
+
process.exitCode = exitCode;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
289
376
|
case 'docs': {
|
|
290
377
|
const docsSubCmd = filteredArgs[1];
|
|
291
378
|
if (docsSubCmd === 'migrate') {
|
|
@@ -1041,6 +1128,55 @@ SillySpec modules — 模块文档管理
|
|
|
1041
1128
|
}
|
|
1042
1129
|
break;
|
|
1043
1130
|
}
|
|
1131
|
+
case 'local': {
|
|
1132
|
+
// 本地配置探测(task-04 / D-001@v1):纯 fs 嗅探项目类型 → 生成 local.yaml。
|
|
1133
|
+
// 轻量独立路由,不跑 scan、不消耗 token。探测逻辑归属 local-detect.js(task-02)。
|
|
1134
|
+
const localSubCmd = filteredArgs[1];
|
|
1135
|
+
if (localSubCmd !== 'detect') {
|
|
1136
|
+
console.error('用法: sillyspec local detect [--dir <path>]\n 纯 fs 嗅探项目类型并生成 local.yaml(不跑 scan、零 token)');
|
|
1137
|
+
process.exit(2);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const detected = detectLocalYaml(dir);
|
|
1141
|
+
const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
1142
|
+
const localYamlPath = join(specRoot, 'local.yaml');
|
|
1143
|
+
|
|
1144
|
+
if (existsSync(localYamlPath)) {
|
|
1145
|
+
console.log(`ℹ️ local.yaml 已存在,跳过: ${localYamlPath}`);
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// 序列化为 local.yaml 文本(与 scan.js 生成本地配置步骤的 yaml 模板格式一致,向后兼容)
|
|
1150
|
+
const c = detected.commands || {};
|
|
1151
|
+
const lines = [];
|
|
1152
|
+
lines.push('# SillySpec 本地配置(自动生成,可手动修改)');
|
|
1153
|
+
lines.push('project:');
|
|
1154
|
+
lines.push(` type: ${detected.project.type} # nodejs/maven/gradle/make/generic`);
|
|
1155
|
+
lines.push('');
|
|
1156
|
+
lines.push('commands:');
|
|
1157
|
+
if (c.build) lines.push(` build: "${c.build}"`);
|
|
1158
|
+
if (c.test) lines.push(` test: "${c.test}"`);
|
|
1159
|
+
if (c.lint) lines.push(` lint: "${c.lint}"`);
|
|
1160
|
+
lines.push('');
|
|
1161
|
+
lines.push('# 测试策略:full=全量测试, module=只测变更模块, skip=跳过测试');
|
|
1162
|
+
lines.push('test_strategy: module');
|
|
1163
|
+
lines.push('');
|
|
1164
|
+
lines.push('# 模块测试路径映射(可选)');
|
|
1165
|
+
lines.push('# module_paths:');
|
|
1166
|
+
lines.push('# user-service: "user/"');
|
|
1167
|
+
lines.push('# order-service: "order/"');
|
|
1168
|
+
const yamlText = lines.join('\n') + '\n';
|
|
1169
|
+
|
|
1170
|
+
// 原子写:mkdir -p specRoot → 写 tmp → rename
|
|
1171
|
+
mkdirSync(specRoot, { recursive: true });
|
|
1172
|
+
const tmpPath = localYamlPath + '.tmp';
|
|
1173
|
+
writeFileSync(tmpPath, yamlText, 'utf8');
|
|
1174
|
+
renameSync(tmpPath, localYamlPath);
|
|
1175
|
+
|
|
1176
|
+
console.log(`✅ 已生成 local.yaml (type: ${detected.project.type})`);
|
|
1177
|
+
console.log(` 路径: ${localYamlPath}`);
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1044
1180
|
default:
|
|
1045
1181
|
console.error(`❌ 未知命令: ${command}`);
|
|
1046
1182
|
printUsage();
|
|
@@ -0,0 +1,110 @@
|
|
|
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(npm run build / npm test / npm run lint)
|
|
46
|
+
* - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
|
|
47
|
+
* - build.gradle → gradle(./gradlew 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
|
+
return {
|
|
61
|
+
project: { type: 'nodejs' },
|
|
62
|
+
commands: {
|
|
63
|
+
build: 'npm run build',
|
|
64
|
+
test: 'npm test',
|
|
65
|
+
lint: 'npm run lint',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. maven
|
|
71
|
+
if (existsSync(join(workdir, 'pom.xml'))) {
|
|
72
|
+
return {
|
|
73
|
+
project: { type: 'maven' },
|
|
74
|
+
commands: {
|
|
75
|
+
build: 'mvn compile',
|
|
76
|
+
test: 'mvn test',
|
|
77
|
+
lint: 'mvn checkstyle:check',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. gradle
|
|
83
|
+
if (existsSync(join(workdir, 'build.gradle'))) {
|
|
84
|
+
return {
|
|
85
|
+
project: { type: 'gradle' },
|
|
86
|
+
commands: {
|
|
87
|
+
build: './gradlew build',
|
|
88
|
+
test: './gradlew test',
|
|
89
|
+
lint: './gradlew check',
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. make(test 命令从 Makefile 解析;build/lint 无则省略)
|
|
95
|
+
const makefilePath = join(workdir, 'Makefile')
|
|
96
|
+
if (existsSync(makefilePath)) {
|
|
97
|
+
const makefileText = readFileSync(makefilePath, 'utf8')
|
|
98
|
+
const test = parseMakefileTestCommand(makefileText)
|
|
99
|
+
return {
|
|
100
|
+
project: { type: 'make' },
|
|
101
|
+
commands: { test },
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 5. generic
|
|
106
|
+
return {
|
|
107
|
+
project: { type: 'generic' },
|
|
108
|
+
commands: {},
|
|
109
|
+
}
|
|
110
|
+
}
|