sillyspec 3.17.1 → 3.17.3
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/init.js +9 -1
- package/src/run.js +88 -61
- package/test/platform-recovery.test.mjs +28 -8
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, resolve, dirname, basename } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
@@ -108,6 +108,14 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
|
108
108
|
// projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
|
|
109
109
|
const spec = specDir || join(projectDir, '.sillyspec');
|
|
110
110
|
|
|
111
|
+
// 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
|
|
112
|
+
const legacyDir = join(projectDir, '.sillyspec');
|
|
113
|
+
if (specDir && existsSync(legacyDir)) {
|
|
114
|
+
try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
|
|
115
|
+
if (!existsSync(legacyDir)) console.log('🧹 已清理旧版本残留的源码 .sillyspec/ 目录');
|
|
116
|
+
else console.error('⚠️ 清理残留 .sillyspec/ 失败');
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
// 创建基础目录
|
|
112
120
|
// spec/projects/ → 项目注册表
|
|
113
121
|
// spec/docs/<name>/ → 统一文档中心
|
package/src/run.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 支持多变更并行:每个变更状态存储在 sillyspec.db 中。
|
|
6
6
|
*/
|
|
7
7
|
import { basename, join, resolve } from 'path'
|
|
8
|
-
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
9
|
import { ProgressManager } from './progress.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -139,7 +139,9 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
139
139
|
return result
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
async function triggerSync(cwd, changeName) {
|
|
142
|
+
async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
143
|
+
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
144
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
143
145
|
try {
|
|
144
146
|
if (changeName && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) return
|
|
145
147
|
const syncMod = await import('./sync.js')
|
|
@@ -154,12 +156,13 @@ async function triggerSync(cwd, changeName) {
|
|
|
154
156
|
* 审批检查辅助函数:execute 阶段启动前检查
|
|
155
157
|
* @returns {{ status: string, reason?: string } | null}
|
|
156
158
|
*/
|
|
157
|
-
async function checkApproval(cwd, changeName) {
|
|
159
|
+
async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
160
|
+
// 平台模式不需要 CLI 内置审批检查
|
|
161
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
|
|
158
162
|
try {
|
|
159
163
|
const syncMod = await import('./sync.js')
|
|
160
164
|
return await syncMod.checkApproval(changeName, cwd)
|
|
161
165
|
} catch (e) {
|
|
162
|
-
// sync.js 不存在或检查失败,静默跳过
|
|
163
166
|
return null
|
|
164
167
|
}
|
|
165
168
|
}
|
|
@@ -474,20 +477,13 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
474
477
|
// 跨 --done 生命周期:从 metadata 文件恢复 platformOpts
|
|
475
478
|
// 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
|
|
476
479
|
// 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
if (resolvedSpecDir) {
|
|
486
|
-
candidatePaths.push(join(resolve(resolvedSpecDir), '.runtime', 'platform-scan.json'))
|
|
487
|
-
}
|
|
488
|
-
candidatePaths.push(join(specRoot, '.runtime', 'platform-scan.json')) // cwd/.sillyspec/.runtime/
|
|
489
|
-
|
|
490
|
-
let platformOptsFile = candidatePaths.find(p => existsSync(p)) || candidatePaths[0]
|
|
480
|
+
let specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
|
|
481
|
+
// 平台参数恢复策略:
|
|
482
|
+
// 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
|
|
483
|
+
// 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
|
|
484
|
+
const platformPointer = join(cwd, '.sillyspec-platform.json')
|
|
485
|
+
const platformScanFile = join(specRoot, '.runtime', 'platform-scan.json')
|
|
486
|
+
let platformOptsFile = existsSync(platformPointer) ? platformPointer : platformScanFile
|
|
491
487
|
let platformFileExists = existsSync(platformOptsFile)
|
|
492
488
|
// 如果命令行没传 spec-root,尝试从持久化文件恢复
|
|
493
489
|
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
@@ -506,6 +502,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
506
502
|
console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
|
|
507
503
|
process.exit(1)
|
|
508
504
|
}
|
|
505
|
+
// 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
|
|
506
|
+
specRoot = platformOpts.specRoot || specRoot
|
|
509
507
|
} catch (e) {
|
|
510
508
|
console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
|
|
511
509
|
console.error(` 错误: ${e.message}`)
|
|
@@ -528,10 +526,9 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
528
526
|
scanRunId: platformOpts.scanRunId,
|
|
529
527
|
savedAt: new Date().toISOString(),
|
|
530
528
|
}, null, 2) + '\n')
|
|
531
|
-
// 恢复指针:在 cwd
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
writeFileSync(join(cwdRuntimeDir, 'platform-scan.json'), JSON.stringify({
|
|
529
|
+
// 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
|
|
530
|
+
// 供后续 --done(不带 --spec-root)找到 specDir
|
|
531
|
+
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
|
|
535
532
|
specRoot: platformOpts.specRoot,
|
|
536
533
|
runtimeRoot: platformOpts.runtimeRoot,
|
|
537
534
|
workspaceId: platformOpts.workspaceId,
|
|
@@ -543,6 +540,18 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
543
540
|
}
|
|
544
541
|
}
|
|
545
542
|
|
|
543
|
+
// 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
|
|
544
|
+
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
545
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
546
|
+
|
|
547
|
+
// 平台模式:清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
|
|
548
|
+
if (platformOpts.specRoot) {
|
|
549
|
+
const legacyDir = join(cwd, '.sillyspec')
|
|
550
|
+
if (existsSync(legacyDir)) {
|
|
551
|
+
try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
546
555
|
// 解析 --output
|
|
547
556
|
let outputText = null
|
|
548
557
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -654,7 +663,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
654
663
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
655
664
|
if (changed && effectiveChange) {
|
|
656
665
|
await pm._write(cwd, progress, effectiveChange)
|
|
657
|
-
triggerSync(cwd, effectiveChange)
|
|
666
|
+
triggerSync(cwd, effectiveChange, platformOpts)
|
|
658
667
|
progress = await pm.read(cwd, effectiveChange) || progress
|
|
659
668
|
}
|
|
660
669
|
|
|
@@ -704,7 +713,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
704
713
|
|
|
705
714
|
// execute 阶段启动前检查审批
|
|
706
715
|
if (stageName === 'execute' && !skipApproval) {
|
|
707
|
-
const approval = await checkApproval(cwd, changeName)
|
|
716
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
708
717
|
if (approval) {
|
|
709
718
|
if (approval.status === 'rejected') {
|
|
710
719
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -721,7 +730,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
721
730
|
if (autoDetectChange(progress, cwd)) {
|
|
722
731
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
723
732
|
await pm._write(cwd, progress, changeName)
|
|
724
|
-
triggerSync(cwd, changeName)
|
|
733
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
725
734
|
}
|
|
726
735
|
|
|
727
736
|
const stageData = progress.stages[stageName]
|
|
@@ -735,7 +744,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
735
744
|
progress.currentStage = stageName
|
|
736
745
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
737
746
|
await pm._write(cwd, progress, changeName)
|
|
738
|
-
triggerSync(cwd, changeName)
|
|
747
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
739
748
|
}
|
|
740
749
|
|
|
741
750
|
const steps = stageData.steps
|
|
@@ -751,7 +760,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
751
760
|
stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
752
761
|
stageData.completedAt = null
|
|
753
762
|
await pm._write(cwd, progress, changeName)
|
|
754
|
-
triggerSync(cwd, changeName)
|
|
763
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
755
764
|
currentIdx = 0
|
|
756
765
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
757
766
|
}
|
|
@@ -777,7 +786,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
777
786
|
startedAt: new Date().toISOString(),
|
|
778
787
|
}
|
|
779
788
|
// 写入 quick-guard.json 供 worktree-guard hook 读取
|
|
780
|
-
const guardFile = join(
|
|
789
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
781
790
|
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
782
791
|
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
783
792
|
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
@@ -802,8 +811,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
802
811
|
}
|
|
803
812
|
}
|
|
804
813
|
|
|
805
|
-
function validateMetadata(cwd, stageName) {
|
|
806
|
-
const changesDir = join(
|
|
814
|
+
function validateMetadata(cwd, stageName, specBase) {
|
|
815
|
+
const changesDir = join(specBase, 'changes')
|
|
807
816
|
if (!existsSync(changesDir)) return
|
|
808
817
|
|
|
809
818
|
const cutoff = Date.now() - 10 * 60 * 1000
|
|
@@ -837,11 +846,11 @@ function validateMetadata(cwd, stageName) {
|
|
|
837
846
|
* 验证关键文件是否存在于正确的变更目录下
|
|
838
847
|
* 防止 AI 将文件写到错误的路径
|
|
839
848
|
*/
|
|
840
|
-
function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
849
|
+
function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
841
850
|
const effectiveChange = changeName || progress.currentChange
|
|
842
851
|
if (!effectiveChange) return
|
|
843
852
|
|
|
844
|
-
const changeDir = join(
|
|
853
|
+
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
845
854
|
if (!existsSync(changeDir)) return
|
|
846
855
|
|
|
847
856
|
// 每个阶段完成后预期存在的文件
|
|
@@ -867,7 +876,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
867
876
|
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
868
877
|
for (const f of missing) {
|
|
869
878
|
// 检查是否写到了错误的位置
|
|
870
|
-
const wrongPath = join(
|
|
879
|
+
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
871
880
|
if (existsSync(wrongPath)) {
|
|
872
881
|
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
873
882
|
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
@@ -887,7 +896,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
887
896
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
888
897
|
process.exit(1)
|
|
889
898
|
}
|
|
890
|
-
const changesDir = join(
|
|
899
|
+
const changesDir = join(specBase, 'changes')
|
|
891
900
|
const archiveDir = join(changesDir, 'archive')
|
|
892
901
|
const srcDir = join(changesDir, archiveChangeName)
|
|
893
902
|
const date = new Date().toISOString().slice(0, 10)
|
|
@@ -915,6 +924,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
915
924
|
|
|
916
925
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
917
926
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
927
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
918
928
|
const stageData = progress.stages[stageName]
|
|
919
929
|
if (!stageData || !stageData.steps) {
|
|
920
930
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
@@ -938,7 +948,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
938
948
|
// 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
|
|
939
949
|
const artifactBase = platformOpts?.runtimeRoot
|
|
940
950
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
|
|
941
|
-
: join(
|
|
951
|
+
: join(specBase, '.runtime', 'artifacts')
|
|
942
952
|
mkdirSync(artifactBase, { recursive: true })
|
|
943
953
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
944
954
|
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
@@ -1010,16 +1020,33 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1010
1020
|
// 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
|
|
1011
1021
|
let projectNames = []
|
|
1012
1022
|
if (outputText) {
|
|
1013
|
-
//
|
|
1014
|
-
const
|
|
1015
|
-
if (
|
|
1016
|
-
projectNames =
|
|
1023
|
+
// 匹配方式 1: "1. project-name" 编号列表
|
|
1024
|
+
const numbered = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
|
|
1025
|
+
if (numbered) {
|
|
1026
|
+
projectNames = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
|
|
1027
|
+
}
|
|
1028
|
+
// 匹配方式 2: 括号枚举 "子项目frontend/order-service/user-service" 或 "项目: a, b, c"
|
|
1029
|
+
if (projectNames.length === 0) {
|
|
1030
|
+
const parenMatch = outputText.match(/(?:子项目|项目)[\s::]*(\S+(?:[\/、,,]+\S+)*)/)
|
|
1031
|
+
if (parenMatch) {
|
|
1032
|
+
projectNames = parenMatch[1]
|
|
1033
|
+
.split(/[\/、,,]+/)
|
|
1034
|
+
.map(s => s.trim())
|
|
1035
|
+
.filter(Boolean)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// 匹配方式 3: 结构化 YAML block "scan_projects:\n - id: name"
|
|
1039
|
+
if (projectNames.length === 0) {
|
|
1040
|
+
const yamlMatch = outputText.match(/scan_projects:\s*\n((?:\s+-\s+id:\s+\S+\s*\n?)+)/)
|
|
1041
|
+
if (yamlMatch) {
|
|
1042
|
+
projectNames = [...yamlMatch[1].matchAll(/-\s+id:\s*(\S+)/g)].map(m => m[1])
|
|
1043
|
+
}
|
|
1017
1044
|
}
|
|
1018
1045
|
}
|
|
1019
1046
|
if (projectNames.length === 0) {
|
|
1020
1047
|
// 回退:读取所有已注册项目
|
|
1021
1048
|
console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
|
|
1022
|
-
const projectsDir = join(
|
|
1049
|
+
const projectsDir = join(specBase, 'projects')
|
|
1023
1050
|
if (existsSync(projectsDir)) {
|
|
1024
1051
|
projectNames = readdirSync(projectsDir)
|
|
1025
1052
|
.filter(f => f.endsWith('.yaml'))
|
|
@@ -1031,8 +1058,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1031
1058
|
}
|
|
1032
1059
|
|
|
1033
1060
|
// 保存到 runtime 供后续使用 + 防重复展开
|
|
1034
|
-
const scanStatePath = join(
|
|
1035
|
-
mkdirSync(join(
|
|
1061
|
+
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
1062
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
1036
1063
|
let scanState = { projects: projectNames, expanded: false }
|
|
1037
1064
|
if (existsSync(scanStatePath)) {
|
|
1038
1065
|
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
@@ -1051,14 +1078,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1051
1078
|
let insertPos = insertBase
|
|
1052
1079
|
for (const pName of projectNames) {
|
|
1053
1080
|
// 读取项目配置获取 projectRoot
|
|
1054
|
-
const projYaml = join(
|
|
1081
|
+
const projYaml = join(specBase, 'projects', `${pName}.yaml`)
|
|
1055
1082
|
let projectRoot = '.'
|
|
1056
1083
|
if (existsSync(projYaml)) {
|
|
1057
1084
|
const yamlContent = readFileSync(projYaml, 'utf8')
|
|
1058
1085
|
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
1059
1086
|
if (pathMatch) projectRoot = pathMatch[1].trim()
|
|
1060
1087
|
}
|
|
1061
|
-
const docOutputDir = `.sillyspec/docs/${pName}`
|
|
1088
|
+
const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
|
|
1062
1089
|
const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
|
|
1063
1090
|
|
|
1064
1091
|
for (const ppStep of perProjectSteps) {
|
|
@@ -1094,11 +1121,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1094
1121
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1095
1122
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1096
1123
|
await pm._write(cwd, progress, changeName)
|
|
1097
|
-
triggerSync(cwd, changeName)
|
|
1124
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1098
1125
|
|
|
1099
1126
|
// Append to user-inputs.md
|
|
1100
1127
|
if (outputText) {
|
|
1101
|
-
const inputsPath = join(
|
|
1128
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1102
1129
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1103
1130
|
appendFileSync(inputsPath, entry)
|
|
1104
1131
|
}
|
|
@@ -1109,7 +1136,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1109
1136
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
1110
1137
|
const { join } = await import('path')
|
|
1111
1138
|
const { execSync } = await import('child_process')
|
|
1112
|
-
const manifestDir =
|
|
1139
|
+
const manifestDir = platformOpts.specRoot
|
|
1113
1140
|
mkdirSync(manifestDir, { recursive: true })
|
|
1114
1141
|
let sourceCommit = null
|
|
1115
1142
|
try {
|
|
@@ -1130,7 +1157,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1130
1157
|
}
|
|
1131
1158
|
// 清理平台参数临时文件
|
|
1132
1159
|
const { unlinkSync } = await import('fs')
|
|
1133
|
-
const platformOptsFile = join(
|
|
1160
|
+
const platformOptsFile = join(manifestDir, '.runtime', 'platform-scan.json')
|
|
1134
1161
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1135
1162
|
|
|
1136
1163
|
// CLI 层 post-check(替代旧的简单检查)
|
|
@@ -1176,10 +1203,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1176
1203
|
printScanPostCheckResult(postResult)
|
|
1177
1204
|
}
|
|
1178
1205
|
|
|
1179
|
-
validateMetadata(cwd, stageName)
|
|
1206
|
+
validateMetadata(cwd, stageName, specBase)
|
|
1180
1207
|
|
|
1181
1208
|
// 验证关键文件是否在正确的变更目录下
|
|
1182
|
-
validateFileLocations(cwd, stageName, progress, changeName)
|
|
1209
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
1183
1210
|
|
|
1184
1211
|
// 辅助阶段完成后重置步骤
|
|
1185
1212
|
const stageDef = stageRegistry[stageName]
|
|
@@ -1251,7 +1278,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1251
1278
|
// 清理 quick-guard.json
|
|
1252
1279
|
try {
|
|
1253
1280
|
const { unlinkSync } = await import('fs')
|
|
1254
|
-
const guardFile = join(
|
|
1281
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
1255
1282
|
unlinkSync(guardFile)
|
|
1256
1283
|
} catch {}
|
|
1257
1284
|
if (review.status === 'blocked') {
|
|
@@ -1272,11 +1299,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1272
1299
|
|
|
1273
1300
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1274
1301
|
await pm._write(cwd, progress, changeName)
|
|
1275
|
-
triggerSync(cwd, changeName)
|
|
1302
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1276
1303
|
|
|
1277
1304
|
// Append to user-inputs.md
|
|
1278
1305
|
if (outputText) {
|
|
1279
|
-
const inputsPath = join(
|
|
1306
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1280
1307
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1281
1308
|
appendFileSync(inputsPath, entry)
|
|
1282
1309
|
}
|
|
@@ -1302,7 +1329,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1302
1329
|
projectsToCheck = [currentProjectName]
|
|
1303
1330
|
} else {
|
|
1304
1331
|
// 兼容旧模式(未展开):检查所有项目
|
|
1305
|
-
const projectsDir = join(
|
|
1332
|
+
const projectsDir = join(specBase, 'projects')
|
|
1306
1333
|
const projectFiles = existsSync(projectsDir)
|
|
1307
1334
|
? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
|
|
1308
1335
|
: []
|
|
@@ -1392,7 +1419,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
1392
1419
|
steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1393
1420
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1394
1421
|
await pm._write(cwd, progress, changeName)
|
|
1395
|
-
triggerSync(cwd, changeName)
|
|
1422
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1396
1423
|
|
|
1397
1424
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|
|
1398
1425
|
|
|
@@ -1455,7 +1482,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName) {
|
|
|
1455
1482
|
}
|
|
1456
1483
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1457
1484
|
await pm._write(cwd, progress, changeName)
|
|
1458
|
-
triggerSync(cwd, changeName)
|
|
1485
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1459
1486
|
console.log(`🔄 ${stageName} 阶段已重置`)
|
|
1460
1487
|
}
|
|
1461
1488
|
|
|
@@ -1481,7 +1508,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1481
1508
|
const changed = await ensureStageSteps(progress, stage, cwd)
|
|
1482
1509
|
if (stageChanged || changed) {
|
|
1483
1510
|
await pm._write(cwd, progress, changeName)
|
|
1484
|
-
triggerSync(cwd, changeName)
|
|
1511
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1485
1512
|
}
|
|
1486
1513
|
progress = await pm.read(cwd, changeName)
|
|
1487
1514
|
return progress
|
|
@@ -1532,7 +1559,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1532
1559
|
}
|
|
1533
1560
|
// execute 阶段启动前检查审批
|
|
1534
1561
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1535
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1562
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1536
1563
|
if (approval) {
|
|
1537
1564
|
if (approval.status === 'rejected') {
|
|
1538
1565
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1562,7 +1589,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1562
1589
|
const defSteps = await getStageSteps(currentStage, cwd, progress)
|
|
1563
1590
|
// execute 阶段启动前检查审批
|
|
1564
1591
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1565
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1592
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1566
1593
|
if (approval) {
|
|
1567
1594
|
if (approval.status === 'rejected') {
|
|
1568
1595
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1595,7 +1622,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1595
1622
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1596
1623
|
await ensureStageSteps(progress, next, cwd)
|
|
1597
1624
|
await pm._write(cwd, progress, changeName)
|
|
1598
|
-
triggerSync(cwd, changeName)
|
|
1625
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1599
1626
|
progress = await pm.read(cwd, changeName)
|
|
1600
1627
|
|
|
1601
1628
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
@@ -1604,7 +1631,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1604
1631
|
if (firstPending !== -1) {
|
|
1605
1632
|
// execute 阶段启动前检查审批
|
|
1606
1633
|
if (next === 'execute' && !skipApproval) {
|
|
1607
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1634
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1608
1635
|
if (approval) {
|
|
1609
1636
|
if (approval.status === 'rejected') {
|
|
1610
1637
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -60,19 +60,39 @@ console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
|
|
|
60
60
|
run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws1 --scan-run-id sr1`)
|
|
61
61
|
|
|
62
62
|
const inSpecDir = join(sd, '.runtime', 'platform-scan.json')
|
|
63
|
-
const
|
|
63
|
+
const pointerFile = join(cwd, '.sillyspec-platform.json')
|
|
64
64
|
assert(existsSync(inSpecDir), `platform-scan.json 在 specDir/.runtime/`)
|
|
65
|
-
assert(existsSync(
|
|
65
|
+
assert(existsSync(pointerFile), `恢复指针在 cwd/.sillyspec-platform.json(不在 .sillyspec 内)`)
|
|
66
66
|
|
|
67
67
|
const content = JSON.parse(readFileSync(inSpecDir, 'utf8'))
|
|
68
68
|
assert(content.specRoot === sd, `specRoot 指向 specDir`)
|
|
69
69
|
assert(content.workspaceId === 'ws1', `workspaceId 保存正确`)
|
|
70
70
|
assert(content.scanRunId === 'sr1', `scanRunId 保存正确`)
|
|
71
|
+
// 关键:cwd/.sillyspec/ 不应被创建
|
|
72
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `cwd/.sillyspec/ 未被创建(源码零污染)`)
|
|
71
73
|
clean(cwd, sd)
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// ── Test 2:
|
|
75
|
-
console.log('\n=== Test 2:
|
|
76
|
+
// ── Test 2: 残留清理:旧版本创建的 cwd/.sillyspec 会被自动删除 ──
|
|
77
|
+
console.log('\n=== Test 2: 旧版本残留清理 ===')
|
|
78
|
+
{
|
|
79
|
+
const cwd = setup('t2'), sd = spec('t2')
|
|
80
|
+
// 模拟旧版本创建的残留
|
|
81
|
+
mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
|
|
82
|
+
writeFileSync(join(cwd, '.sillyspec', '.runtime', 'old.db'), 'x')
|
|
83
|
+
mkdirSync(join(cwd, '.sillyspec', 'changes'), { recursive: true })
|
|
84
|
+
assert(existsSync(join(cwd, '.sillyspec')), `残留存在`)
|
|
85
|
+
// init 时应清理
|
|
86
|
+
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
87
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `init 清理了 cwd/.sillyspec/`)
|
|
88
|
+
// run 时也不应再创建
|
|
89
|
+
run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws --scan-run-id sr 2>&1`)
|
|
90
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `run 后 cwd/.sillyspec/ 仍不存在`)
|
|
91
|
+
clean(cwd, sd)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Test 3: --done 不带 --spec-root 时恢复 ──
|
|
95
|
+
console.log('\n=== Test 3: --done 恢复平台参数 ===')
|
|
76
96
|
{
|
|
77
97
|
const cwd = setup('t2'), sd = spec('t2')
|
|
78
98
|
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
@@ -87,7 +107,7 @@ console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
|
87
107
|
// ── Test 3-6: stage-contract 路径(通过 runValidators) ──
|
|
88
108
|
const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
|
|
89
109
|
|
|
90
|
-
console.log('\n=== Test
|
|
110
|
+
console.log('\n=== Test 5: specDir 有文档 → 校验通过 ===')
|
|
91
111
|
{
|
|
92
112
|
const cwd = setup('t3'), sd = spec('t3')
|
|
93
113
|
const proj = basename(cwd)
|
|
@@ -98,7 +118,7 @@ console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
|
|
|
98
118
|
clean(cwd, sd)
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
console.log('\n=== Test
|
|
121
|
+
console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
|
|
102
122
|
{
|
|
103
123
|
const cwd = setup('t4'), sd = spec('t4')
|
|
104
124
|
const proj = basename(cwd)
|
|
@@ -112,7 +132,7 @@ console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .si
|
|
|
112
132
|
clean(cwd, sd)
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
console.log('\n=== Test
|
|
135
|
+
console.log('\n=== Test 7: 非平台模式有文档 → 校验通过 ===')
|
|
116
136
|
{
|
|
117
137
|
const cwd = setup('t5')
|
|
118
138
|
const proj = basename(cwd)
|
|
@@ -122,7 +142,7 @@ console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
|
|
|
122
142
|
clean(cwd)
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
console.log('\n=== Test
|
|
145
|
+
console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
|
|
126
146
|
{
|
|
127
147
|
const cwd = setup('t6')
|
|
128
148
|
const proj = basename(cwd)
|