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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.2",
3
+ "version": "3.17.3",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
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
- const specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
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
- // 匹配 "1. project-name" 格式
1011
- const matches = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
1012
- if (matches) {
1013
- projectNames = matches.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
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(specRoot, '.runtime', 'platform-scan.json')
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: --done 不带 --spec-root 时恢复 ──
77
- console.log('\n=== Test 2: --done 恢复平台参数 ===')
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 3: specDir 有文档 → 校验通过 ===')
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 4: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
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 5: 非平台模式有文档 → 校验通过 ===')
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 6: 非平台模式缺文档 → 路径含 .sillyspec ===')
145
+ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
128
146
  {
129
147
  const cwd = setup('t6')
130
148
  const proj = basename(cwd)