sillyspec 3.17.2 → 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 +54 -24
- package/test/platform-recovery.test.mjs +24 -6
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,7 +477,7 @@ 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
|
-
|
|
480
|
+
let specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
|
|
478
481
|
// 平台参数恢复策略:
|
|
479
482
|
// 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
|
|
480
483
|
// 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
|
|
@@ -499,6 +502,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
499
502
|
console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
|
|
500
503
|
process.exit(1)
|
|
501
504
|
}
|
|
505
|
+
// 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
|
|
506
|
+
specRoot = platformOpts.specRoot || specRoot
|
|
502
507
|
} catch (e) {
|
|
503
508
|
console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
|
|
504
509
|
console.error(` 错误: ${e.message}`)
|
|
@@ -539,6 +544,14 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
539
544
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
540
545
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
541
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
|
+
|
|
542
555
|
// 解析 --output
|
|
543
556
|
let outputText = null
|
|
544
557
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -650,7 +663,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
650
663
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
651
664
|
if (changed && effectiveChange) {
|
|
652
665
|
await pm._write(cwd, progress, effectiveChange)
|
|
653
|
-
triggerSync(cwd, effectiveChange)
|
|
666
|
+
triggerSync(cwd, effectiveChange, platformOpts)
|
|
654
667
|
progress = await pm.read(cwd, effectiveChange) || progress
|
|
655
668
|
}
|
|
656
669
|
|
|
@@ -700,7 +713,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
700
713
|
|
|
701
714
|
// execute 阶段启动前检查审批
|
|
702
715
|
if (stageName === 'execute' && !skipApproval) {
|
|
703
|
-
const approval = await checkApproval(cwd, changeName)
|
|
716
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
704
717
|
if (approval) {
|
|
705
718
|
if (approval.status === 'rejected') {
|
|
706
719
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -717,7 +730,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
717
730
|
if (autoDetectChange(progress, cwd)) {
|
|
718
731
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
719
732
|
await pm._write(cwd, progress, changeName)
|
|
720
|
-
triggerSync(cwd, changeName)
|
|
733
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
721
734
|
}
|
|
722
735
|
|
|
723
736
|
const stageData = progress.stages[stageName]
|
|
@@ -731,7 +744,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
731
744
|
progress.currentStage = stageName
|
|
732
745
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
733
746
|
await pm._write(cwd, progress, changeName)
|
|
734
|
-
triggerSync(cwd, changeName)
|
|
747
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
735
748
|
}
|
|
736
749
|
|
|
737
750
|
const steps = stageData.steps
|
|
@@ -747,7 +760,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
747
760
|
stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
748
761
|
stageData.completedAt = null
|
|
749
762
|
await pm._write(cwd, progress, changeName)
|
|
750
|
-
triggerSync(cwd, changeName)
|
|
763
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
751
764
|
currentIdx = 0
|
|
752
765
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
753
766
|
}
|
|
@@ -1007,10 +1020,27 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1007
1020
|
// 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
|
|
1008
1021
|
let projectNames = []
|
|
1009
1022
|
if (outputText) {
|
|
1010
|
-
//
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
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
|
+
}
|
|
1014
1044
|
}
|
|
1015
1045
|
}
|
|
1016
1046
|
if (projectNames.length === 0) {
|
|
@@ -1091,7 +1121,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1091
1121
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1092
1122
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1093
1123
|
await pm._write(cwd, progress, changeName)
|
|
1094
|
-
triggerSync(cwd, changeName)
|
|
1124
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1095
1125
|
|
|
1096
1126
|
// Append to user-inputs.md
|
|
1097
1127
|
if (outputText) {
|
|
@@ -1127,7 +1157,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1127
1157
|
}
|
|
1128
1158
|
// 清理平台参数临时文件
|
|
1129
1159
|
const { unlinkSync } = await import('fs')
|
|
1130
|
-
const platformOptsFile = join(
|
|
1160
|
+
const platformOptsFile = join(manifestDir, '.runtime', 'platform-scan.json')
|
|
1131
1161
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1132
1162
|
|
|
1133
1163
|
// CLI 层 post-check(替代旧的简单检查)
|
|
@@ -1269,7 +1299,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1269
1299
|
|
|
1270
1300
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1271
1301
|
await pm._write(cwd, progress, changeName)
|
|
1272
|
-
triggerSync(cwd, changeName)
|
|
1302
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1273
1303
|
|
|
1274
1304
|
// Append to user-inputs.md
|
|
1275
1305
|
if (outputText) {
|
|
@@ -1389,7 +1419,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
1389
1419
|
steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1390
1420
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1391
1421
|
await pm._write(cwd, progress, changeName)
|
|
1392
|
-
triggerSync(cwd, changeName)
|
|
1422
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1393
1423
|
|
|
1394
1424
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|
|
1395
1425
|
|
|
@@ -1452,7 +1482,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName) {
|
|
|
1452
1482
|
}
|
|
1453
1483
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1454
1484
|
await pm._write(cwd, progress, changeName)
|
|
1455
|
-
triggerSync(cwd, changeName)
|
|
1485
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1456
1486
|
console.log(`🔄 ${stageName} 阶段已重置`)
|
|
1457
1487
|
}
|
|
1458
1488
|
|
|
@@ -1478,7 +1508,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1478
1508
|
const changed = await ensureStageSteps(progress, stage, cwd)
|
|
1479
1509
|
if (stageChanged || changed) {
|
|
1480
1510
|
await pm._write(cwd, progress, changeName)
|
|
1481
|
-
triggerSync(cwd, changeName)
|
|
1511
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1482
1512
|
}
|
|
1483
1513
|
progress = await pm.read(cwd, changeName)
|
|
1484
1514
|
return progress
|
|
@@ -1529,7 +1559,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1529
1559
|
}
|
|
1530
1560
|
// execute 阶段启动前检查审批
|
|
1531
1561
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1532
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1562
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1533
1563
|
if (approval) {
|
|
1534
1564
|
if (approval.status === 'rejected') {
|
|
1535
1565
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1559,7 +1589,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1559
1589
|
const defSteps = await getStageSteps(currentStage, cwd, progress)
|
|
1560
1590
|
// execute 阶段启动前检查审批
|
|
1561
1591
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1562
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1592
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1563
1593
|
if (approval) {
|
|
1564
1594
|
if (approval.status === 'rejected') {
|
|
1565
1595
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1592,7 +1622,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1592
1622
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1593
1623
|
await ensureStageSteps(progress, next, cwd)
|
|
1594
1624
|
await pm._write(cwd, progress, changeName)
|
|
1595
|
-
triggerSync(cwd, changeName)
|
|
1625
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1596
1626
|
progress = await pm.read(cwd, changeName)
|
|
1597
1627
|
|
|
1598
1628
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
@@ -1601,7 +1631,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1601
1631
|
if (firstPending !== -1) {
|
|
1602
1632
|
// execute 阶段启动前检查审批
|
|
1603
1633
|
if (next === 'execute' && !skipApproval) {
|
|
1604
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1634
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1605
1635
|
if (approval) {
|
|
1606
1636
|
if (approval.status === 'rejected') {
|
|
1607
1637
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -73,8 +73,26 @@ console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
|
|
|
73
73
|
clean(cwd, sd)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// ── Test 2:
|
|
77
|
-
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 恢复平台参数 ===')
|
|
78
96
|
{
|
|
79
97
|
const cwd = setup('t2'), sd = spec('t2')
|
|
80
98
|
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
@@ -89,7 +107,7 @@ console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
|
89
107
|
// ── Test 3-6: stage-contract 路径(通过 runValidators) ──
|
|
90
108
|
const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
|
|
91
109
|
|
|
92
|
-
console.log('\n=== Test
|
|
110
|
+
console.log('\n=== Test 5: specDir 有文档 → 校验通过 ===')
|
|
93
111
|
{
|
|
94
112
|
const cwd = setup('t3'), sd = spec('t3')
|
|
95
113
|
const proj = basename(cwd)
|
|
@@ -100,7 +118,7 @@ console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
|
|
|
100
118
|
clean(cwd, sd)
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
console.log('\n=== Test
|
|
121
|
+
console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
|
|
104
122
|
{
|
|
105
123
|
const cwd = setup('t4'), sd = spec('t4')
|
|
106
124
|
const proj = basename(cwd)
|
|
@@ -114,7 +132,7 @@ console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .si
|
|
|
114
132
|
clean(cwd, sd)
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
console.log('\n=== Test
|
|
135
|
+
console.log('\n=== Test 7: 非平台模式有文档 → 校验通过 ===')
|
|
118
136
|
{
|
|
119
137
|
const cwd = setup('t5')
|
|
120
138
|
const proj = basename(cwd)
|
|
@@ -124,7 +142,7 @@ console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
|
|
|
124
142
|
clean(cwd)
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
console.log('\n=== Test
|
|
145
|
+
console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
|
|
128
146
|
{
|
|
129
147
|
const cwd = setup('t6')
|
|
130
148
|
const proj = basename(cwd)
|