sillyspec 3.9.0 → 3.9.1

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.
Files changed (212) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +17 -0
  2. package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
  4. package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
  7. package/.claude/skills/sillyspec-execute/SKILL.md +17 -0
  8. package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
  9. package/.claude/skills/sillyspec-export/SKILL.md +53 -0
  10. package/.claude/skills/sillyspec-init/SKILL.md +170 -0
  11. package/.claude/skills/sillyspec-plan/SKILL.md +52 -0
  12. package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
  13. package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
  14. package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
  15. package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
  16. package/.claude/skills/sillyspec-state/SKILL.md +54 -0
  17. package/.claude/skills/sillyspec-status/SKILL.md +17 -0
  18. package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
  19. package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
  20. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
  21. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
  22. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
  23. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
  24. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
  25. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
  26. package/.sillyspec/changes/auto-mode/design.md +50 -0
  27. package/.sillyspec/changes/auto-mode/proposal.md +19 -0
  28. package/.sillyspec/changes/auto-mode/requirements.md +21 -0
  29. package/.sillyspec/changes/auto-mode/tasks.md +7 -0
  30. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
  31. package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
  32. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  33. package/.sillyspec/changes/unified-docs-design/design.md +199 -0
  34. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  35. package/.sillyspec/knowledge/INDEX.md +8 -0
  36. package/.sillyspec/knowledge/uncategorized.md +3 -0
  37. package/.sillyspec/projects/sillyspec.yaml +3 -0
  38. package/README.md +12 -5
  39. package/package.json +7 -9
  40. package/packages/dashboard/dist/assets/index-CntACGUN.css +1 -0
  41. package/packages/dashboard/dist/assets/index-RsLVPAy7.js +7446 -0
  42. package/packages/dashboard/dist/index.html +3 -2
  43. package/packages/dashboard/package-lock.json +226 -6
  44. package/packages/dashboard/package.json +8 -5
  45. package/packages/dashboard/public/logo.jpg +0 -0
  46. package/packages/dashboard/server/executor.js +1 -1
  47. package/packages/dashboard/server/index.js +336 -113
  48. package/packages/dashboard/server/parser.js +333 -29
  49. package/packages/dashboard/server/watcher.js +203 -131
  50. package/packages/dashboard/src/App.vue +187 -11
  51. package/packages/dashboard/src/components/ActionBar.vue +26 -42
  52. package/packages/dashboard/src/components/CommandPalette.vue +40 -65
  53. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  54. package/packages/dashboard/src/components/DocPreview.vue +160 -0
  55. package/packages/dashboard/src/components/DocTree.vue +58 -0
  56. package/packages/dashboard/src/components/LogStream.vue +13 -33
  57. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  58. package/packages/dashboard/src/components/PipelineView.vue +80 -45
  59. package/packages/dashboard/src/components/ProjectList.vue +103 -45
  60. package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
  61. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  62. package/packages/dashboard/src/components/StepCard.vue +15 -15
  63. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  64. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  65. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  66. package/packages/dashboard/src/composables/useDashboard.js +20 -6
  67. package/packages/dashboard/src/composables/useKeyboard.js +6 -4
  68. package/packages/dashboard/src/main.js +4 -1
  69. package/packages/dashboard/src/style.css +17 -17
  70. package/src/index.js +134 -22
  71. package/src/init.js +83 -228
  72. package/src/migrate.js +117 -0
  73. package/src/progress.js +459 -0
  74. package/src/run.js +624 -0
  75. package/src/setup.js +2 -72
  76. package/src/stages/archive.js +54 -0
  77. package/src/stages/brainstorm.js +239 -0
  78. package/src/stages/doctor.js +303 -0
  79. package/src/stages/execute.js +262 -0
  80. package/src/stages/index.js +26 -0
  81. package/src/stages/plan.js +282 -0
  82. package/src/stages/propose.js +115 -0
  83. package/src/stages/quick.js +64 -0
  84. package/src/stages/scan.js +141 -0
  85. package/src/stages/status.js +65 -0
  86. package/src/stages/verify.js +135 -0
  87. package/dist/steps/brainstorm/01-load-context.md +0 -30
  88. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  89. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  90. package/dist/steps/brainstorm/04-module-split.md +0 -23
  91. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  92. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  93. package/dist/steps/brainstorm/07-present-design.md +0 -3
  94. package/dist/steps/brainstorm/08-write-design.md +0 -21
  95. package/dist/steps/brainstorm/09-self-review.md +0 -15
  96. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  97. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  98. package/dist/steps/brainstorm/manifest.yaml +0 -26
  99. package/dist/steps/execute/01-load-context.md +0 -41
  100. package/dist/steps/execute/02-scan-conventions.md +0 -47
  101. package/dist/steps/execute/03-skill-mcp.md +0 -19
  102. package/dist/steps/execute/04-assign-task.md +0 -22
  103. package/dist/steps/execute/04b-prompt-template.md +0 -54
  104. package/dist/steps/execute/05-write-test.md +0 -7
  105. package/dist/steps/execute/06-write-code.md +0 -8
  106. package/dist/steps/execute/07-run-test.md +0 -26
  107. package/dist/steps/execute/08-fix-issues.md +0 -28
  108. package/dist/steps/execute/09-next-task.md +0 -33
  109. package/dist/steps/execute/manifest.yaml +0 -28
  110. package/dist/steps/plan/01-load-context.md +0 -22
  111. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  112. package/dist/steps/plan/03-expand-tasks.md +0 -33
  113. package/dist/steps/plan/04-mark-order.md +0 -15
  114. package/dist/steps/plan/05-e2e-planning.md +0 -17
  115. package/dist/steps/plan/06-self-check.md +0 -16
  116. package/dist/steps/plan/07-save.md +0 -1
  117. package/dist/steps/plan/manifest.yaml +0 -18
  118. package/dist/steps/scan/01-env-detect.md +0 -51
  119. package/dist/steps/scan/02-tech-stack.md +0 -16
  120. package/dist/steps/scan/03-conventions.md +0 -16
  121. package/dist/steps/scan/04-structure.md +0 -19
  122. package/dist/steps/scan/05-quality.md +0 -18
  123. package/dist/steps/scan/06-complete.md +0 -49
  124. package/dist/steps/scan/manifest.yaml +0 -16
  125. package/dist/steps/verify/01-load-specs.md +0 -28
  126. package/dist/steps/verify/02-check-tasks.md +0 -1
  127. package/dist/steps/verify/03-check-design.md +0 -6
  128. package/dist/steps/verify/04-run-tests.md +0 -7
  129. package/dist/steps/verify/05-e2e-tests.md +0 -27
  130. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  131. package/dist/steps/verify/06-code-quality.md +0 -25
  132. package/dist/steps/verify/07-lint-check.md +0 -27
  133. package/dist/steps/verify/08-output-report.md +0 -14
  134. package/dist/steps/verify/manifest.yaml +0 -22
  135. package/docs/.vitepress/config.mts +0 -45
  136. package/docs/.vitepress/dist/404.html +0 -25
  137. package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
  138. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
  139. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
  140. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
  141. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
  142. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  144. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  145. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  146. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  147. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  148. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  149. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  150. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  151. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  152. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  153. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  154. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  155. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  156. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
  157. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
  158. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
  159. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
  160. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
  161. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
  162. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
  163. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
  164. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
  165. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
  166. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
  167. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
  168. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
  169. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
  170. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
  171. package/docs/.vitepress/dist/hashmap.json +0 -1
  172. package/docs/.vitepress/dist/index.html +0 -28
  173. package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
  174. package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
  175. package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
  176. package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
  177. package/docs/.vitepress/dist/sillyspec/install.html +0 -32
  178. package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
  179. package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
  180. package/docs/.vitepress/dist/vp-icons.css +0 -1
  181. package/docs/index.md +0 -34
  182. package/docs/sillyspec/commands.md +0 -218
  183. package/docs/sillyspec/dashboard.md +0 -51
  184. package/docs/sillyspec/file-io.md +0 -34
  185. package/docs/sillyspec/getting-started.md +0 -61
  186. package/docs/sillyspec/install.md +0 -51
  187. package/docs/sillyspec/lifecycle.md +0 -146
  188. package/docs/sillyspec/structure.md +0 -62
  189. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
  190. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
  191. package/src/step.js +0 -543
  192. package/templates/archive.md +0 -120
  193. package/templates/brainstorm.md +0 -170
  194. package/templates/continue.md +0 -32
  195. package/templates/execute.md +0 -304
  196. package/templates/explore.md +0 -59
  197. package/templates/export.md +0 -21
  198. package/templates/init.md +0 -61
  199. package/templates/plan.md +0 -146
  200. package/templates/quick.md +0 -135
  201. package/templates/scan-quick.md +0 -49
  202. package/templates/scan.md +0 -156
  203. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  204. package/templates/status.md +0 -75
  205. package/templates/verify.md +0 -236
  206. package/templates/workspace-sync.md +0 -99
  207. package/templates/workspace.md +0 -70
  208. /package/.sillyspec/{specs → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
  209. /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
  210. /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
  211. /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
  212. /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
package/src/migrate.js ADDED
@@ -0,0 +1,117 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from 'fs';
2
+ import { basename, join, resolve } from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * Migrate old .sillyspec/ structure to unified docs/<project>/ structure
7
+ * @param {string} projectDir - Path to the project directory
8
+ */
9
+ export function migrateDocs(projectDir) {
10
+ const sillyspecDir = join(projectDir, '.sillyspec');
11
+ if (!existsSync(sillyspecDir)) {
12
+ console.error('❌ .sillyspec/ 目录不存在');
13
+ process.exit(1);
14
+ }
15
+
16
+ // Determine project name from projects/*.yaml or directory name
17
+ let projectName = basename(resolve(projectDir));
18
+ const projectsDir = join(sillyspecDir, 'projects');
19
+ if (existsSync(projectsDir)) {
20
+ const yamlFiles = readdirSync(projectsDir).filter(f => f.endsWith('.yaml'));
21
+ if (yamlFiles.length > 0) {
22
+ projectName = yamlFiles[0].replace('.yaml', '');
23
+ }
24
+ }
25
+
26
+ console.log(chalk.cyan(`📦 迁移项目: ${projectName}`));
27
+ console.log('');
28
+
29
+ const docsBase = join(sillyspecDir, 'docs', projectName);
30
+ let migrated = 0;
31
+
32
+ // 1. codebase/ → docs/<project>/scan/
33
+ const codebaseDir = join(sillyspecDir, 'codebase');
34
+ if (existsSync(codebaseDir)) {
35
+ const targetDir = join(docsBase, 'scan');
36
+ mkdirSync(targetDir, { recursive: true });
37
+ const files = readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
38
+ for (const file of files) {
39
+ const src = join(codebaseDir, file);
40
+ const dest = join(targetDir, file);
41
+ if (!existsSync(dest)) {
42
+ copyFileSync(src, dest);
43
+ console.log(chalk.green(' ✅') + ` scan/${file}`);
44
+ migrated++;
45
+ } else {
46
+ console.log(chalk.yellow(' ⏭️') + ` scan/${file} (已存在)`);
47
+ }
48
+ }
49
+ }
50
+
51
+ // 2. specs/ is deprecated — designs live in changes/<变更名>/design.md
52
+
53
+ // 3. changes/archive/ → docs/<project>/archive/
54
+ const archiveDir = join(sillyspecDir, 'changes', 'archive');
55
+ if (existsSync(archiveDir)) {
56
+ const targetDir = join(docsBase, 'archive');
57
+ mkdirSync(targetDir, { recursive: true });
58
+ const entries = readdirSync(archiveDir);
59
+ for (const entry of entries) {
60
+ const src = join(archiveDir, entry);
61
+ const dest = join(targetDir, entry);
62
+ if (!existsSync(dest)) {
63
+ copyFileSync(src, dest);
64
+ console.log(chalk.green(' ✅') + ` archive/${entry}`);
65
+ migrated++;
66
+ } else {
67
+ console.log(chalk.yellow(' ⏭️') + ` archive/${entry} (已存在)`);
68
+ }
69
+ }
70
+ }
71
+
72
+ // 4. knowledge/ → docs/<project>/archive/ (append knowledge files)
73
+ const knowledgeDir = join(sillyspecDir, 'knowledge');
74
+ if (existsSync(knowledgeDir)) {
75
+ const targetDir = join(docsBase, 'archive');
76
+ mkdirSync(targetDir, { recursive: true });
77
+ const files = readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));
78
+ for (const file of files) {
79
+ const src = join(knowledgeDir, file);
80
+ const dest = join(targetDir, `knowledge-${file}`);
81
+ if (!existsSync(dest)) {
82
+ copyFileSync(src, dest);
83
+ console.log(chalk.green(' ✅') + ` archive/knowledge-${file}`);
84
+ migrated++;
85
+ } else {
86
+ console.log(chalk.yellow(' ⏭️') + ` archive/knowledge-${file} (已存在)`);
87
+ }
88
+ }
89
+ }
90
+
91
+ // 5. quicklog/ → docs/<project>/quicklog/
92
+ const quicklogDir = join(sillyspecDir, 'quicklog');
93
+ if (existsSync(quicklogDir)) {
94
+ const targetDir = join(docsBase, 'quicklog');
95
+ mkdirSync(targetDir, { recursive: true });
96
+ const files = readdirSync(quicklogDir).filter(f => f.endsWith('.md'));
97
+ for (const file of files) {
98
+ const src = join(quicklogDir, file);
99
+ const dest = join(targetDir, file);
100
+ if (!existsSync(dest)) {
101
+ copyFileSync(src, dest);
102
+ console.log(chalk.green(' ✅') + ` quicklog/${file}`);
103
+ migrated++;
104
+ } else {
105
+ console.log(chalk.yellow(' ⏭️') + ` quicklog/${file} (已存在)`);
106
+ }
107
+ }
108
+ }
109
+
110
+ console.log('');
111
+ if (migrated > 0) {
112
+ console.log(chalk.green(` ✅ 迁移完成,共迁移 ${migrated} 个文件`));
113
+ console.log(chalk.dim(' 旧文件保留在原位,确认无误后可手动删除'));
114
+ } else {
115
+ console.log(chalk.yellow(' 没有需要迁移的文件'));
116
+ }
117
+ }
@@ -0,0 +1,459 @@
1
+ /**
2
+ * SillySpec ProgressManager — 进度恢复管理
3
+ *
4
+ * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
5
+ *
6
+ * Schema v2: { project, currentStage, stages: { [name]: { status, steps, startedAt, completedAt } }, lastActive }
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync } from 'fs';
10
+ import { join, basename } from 'path';
11
+
12
+ const RUNTIME_DIR = '.sillyspec/.runtime';
13
+ const PROGRESS_FILE = 'progress.json';
14
+ const BACKUP_FILE = 'progress.json.bak';
15
+
16
+ const CURRENT_VERSION = 2;
17
+ const VALID_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'scan', 'quick', 'archive'];
18
+ const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
19
+
20
+ const STAGE_LABELS = {
21
+ brainstorm: '🧠 需求探索',
22
+ plan: '📐 实现计划',
23
+ execute: '⚡ 波次执行',
24
+ verify: '🔍 验证确认',
25
+ scan: '🔍 代码扫描',
26
+ quick: '⚡ 快速任务',
27
+ archive: '📦 归档变更',
28
+ };
29
+
30
+ function emptyStage() {
31
+ return { status: 'pending', steps: [], startedAt: null, completedAt: null };
32
+ }
33
+
34
+ function makeInitialProgress(project) {
35
+ const stages = {};
36
+ for (const s of VALID_STAGES) stages[s] = emptyStage();
37
+ return { _version: CURRENT_VERSION, project: project || '', currentStage: '', currentChange: null, stages, lastActive: null };
38
+ }
39
+
40
+ // ── ProgressManager ──
41
+
42
+ export class ProgressManager {
43
+ // ── 核心读写 ──
44
+
45
+ _path(cwd, ...parts) {
46
+ return join(cwd, RUNTIME_DIR, ...parts);
47
+ }
48
+
49
+ read(cwd) {
50
+ const progressPath = this._path(cwd, PROGRESS_FILE);
51
+ const backupPath = this._path(cwd, BACKUP_FILE);
52
+
53
+ for (const p of [progressPath, backupPath]) {
54
+ if (!existsSync(p)) continue;
55
+ const parsed = this._parseWithRecovery(readFileSync(p, 'utf8'));
56
+ if (parsed) {
57
+ if (p === backupPath) {
58
+ console.log('⚠️ progress.json 损坏,已从备份恢复');
59
+ writeFileSync(progressPath, JSON.stringify(parsed, null, 2) + '\n');
60
+ }
61
+ return parsed;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ _write(cwd, data) {
68
+ const progressPath = this._path(cwd, PROGRESS_FILE);
69
+ const tmpPath = progressPath + '.tmp';
70
+ this._ensureDir(cwd);
71
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
72
+ renameSync(tmpPath, progressPath);
73
+ }
74
+
75
+ _ensureDir(cwd) {
76
+ const runtimeDir = this._path(cwd);
77
+ if (!existsSync(runtimeDir)) {
78
+ mkdirSync(runtimeDir, { recursive: true });
79
+ for (const d of ['artifacts', 'history', 'logs', 'templates']) {
80
+ mkdirSync(join(runtimeDir, d), { recursive: true });
81
+ }
82
+ }
83
+ }
84
+
85
+ _backup(cwd) {
86
+ const p = this._path(cwd, PROGRESS_FILE);
87
+ if (existsSync(p)) copyFileSync(p, this._path(cwd, BACKUP_FILE));
88
+ }
89
+
90
+ // ── CLI 命令 ──
91
+
92
+ init(cwd) {
93
+ this._ensureDir(cwd);
94
+ const progressPath = this._path(cwd, PROGRESS_FILE);
95
+
96
+ if (existsSync(progressPath)) {
97
+ console.log(`ℹ️ progress.json 已存在,跳过`);
98
+ return this.read(cwd);
99
+ }
100
+
101
+ const project = basename(cwd);
102
+ const data = makeInitialProgress(project);
103
+ this._write(cwd, data);
104
+ console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
105
+
106
+ // 创建 user-inputs.md
107
+ const inputsPath = this._path(cwd, 'user-inputs.md');
108
+ if (!existsSync(inputsPath)) {
109
+ writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
110
+ }
111
+
112
+ this._ensureGitignore(cwd);
113
+ return data;
114
+ }
115
+
116
+ setStage(cwd, stage) {
117
+ if (!VALID_STAGES.includes(stage)) {
118
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
119
+ return;
120
+ }
121
+
122
+ const data = this._readOrInit(cwd);
123
+ if (!data) return;
124
+
125
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
126
+ const stageData = data.stages[stage];
127
+
128
+ data.currentStage = stage;
129
+ if (stageData.status === 'pending') {
130
+ stageData.status = 'in-progress';
131
+ stageData.startedAt = new Date().toLocaleString('zh-CN',{hour12:false});
132
+ }
133
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
134
+
135
+ this._backup(cwd);
136
+ this._write(cwd, data);
137
+ console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage} (${stageData.status})`);
138
+ }
139
+
140
+ addStep(cwd, stage, stepName) {
141
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
142
+ const data = this._requireStage(cwd, stage);
143
+ if (!data) return;
144
+
145
+ const stageData = data.stages[stage];
146
+ if (stageData.steps.some(s => s.name === stepName)) {
147
+ console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
148
+ return;
149
+ }
150
+
151
+ stageData.steps.push({ name: stepName, status: 'pending' });
152
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
153
+
154
+ this._backup(cwd);
155
+ this._write(cwd, data);
156
+ console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
157
+ }
158
+
159
+ updateStep(cwd, stage, stepName, options = {}) {
160
+ const { status, output } = options;
161
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
162
+ const data = this._requireStage(cwd, stage);
163
+ if (!data) return;
164
+
165
+ const stageData = data.stages[stage];
166
+ const step = stageData.steps.find(s => s.name === stepName);
167
+ if (!step) { console.log(`❌ 步骤不存在: ${stage}/${stepName}`); return; }
168
+
169
+ if (status) {
170
+ if (!VALID_STATUSES.includes(status)) {
171
+ console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
172
+ return;
173
+ }
174
+ step.status = status;
175
+ }
176
+ if (output !== undefined) step.output = output;
177
+
178
+ // 检查是否所有步骤都 completed
179
+ if (stageData.steps.length > 0 && stageData.steps.every(s => s.status === 'completed')) {
180
+ stageData.status = 'completed';
181
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false});
182
+ console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
183
+ }
184
+
185
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
186
+ this._backup(cwd);
187
+ this._write(cwd, data);
188
+ console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || step.status}`);
189
+ }
190
+
191
+ completeStage(cwd, stage) {
192
+ if (!VALID_STAGES.includes(stage)) {
193
+ console.log(`❌ 未知阶段: ${stage}`);
194
+ return;
195
+ }
196
+
197
+ const data = this._readOrInit(cwd);
198
+ if (!data) return;
199
+
200
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
201
+ const stageData = data.stages[stage];
202
+ stageData.status = 'completed';
203
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false});
204
+
205
+ // 标记所有未完成步骤为 completed
206
+ for (const step of stageData.steps) {
207
+ if (step.status === 'pending') step.status = 'completed';
208
+ }
209
+
210
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
211
+
212
+ // 归档到 history/(ISO 时间戳,去掉所有非法路径字符)
213
+ const historyDir = this._path(cwd, 'history');
214
+ mkdirSync(historyDir, { recursive: true });
215
+ const ts = new Date().toISOString().replace(/[:.TZ-]/g, '');
216
+ writeFileSync(join(historyDir, `${stage}-${ts}.json`), JSON.stringify({ stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
217
+
218
+ this._backup(cwd);
219
+ this._write(cwd, data);
220
+
221
+ console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
222
+ }
223
+
224
+ show(cwd) {
225
+ const data = this.read(cwd);
226
+ if (!data) {
227
+ console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
228
+ return;
229
+ }
230
+
231
+ console.log('');
232
+ console.log(' ═══════════════════════════════════════');
233
+ console.log(` 项目: ${data.project || '(未命名)'}`);
234
+ console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
235
+ console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
236
+ console.log(' ═══════════════════════════════════════');
237
+ console.log('');
238
+
239
+ const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫' };
240
+
241
+ for (const stage of VALID_STAGES) {
242
+ const stageData = data.stages[stage] || emptyStage();
243
+ const label = STAGE_LABELS[stage] || stage;
244
+ const icon = statusIcons[stageData.status] || '⬜';
245
+ const isCurrent = data.currentStage === stage ? ' ◀' : '';
246
+
247
+ console.log(` ${icon} ${label}${isCurrent}`);
248
+
249
+ if (stageData.steps && stageData.steps.length > 0) {
250
+ for (const step of stageData.steps) {
251
+ const si = statusIcons[step.status] || '○';
252
+ const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
253
+ console.log(` ${si} ${step.name}${out}`);
254
+ }
255
+ }
256
+
257
+ if (stageData.startedAt) {
258
+ console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
259
+ }
260
+ if (stageData.completedAt) {
261
+ console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
262
+ }
263
+ }
264
+
265
+ // 批量进度
266
+ if (data.batchProgress) {
267
+ const batchLine = this._renderBatchProgress(data.batchProgress);
268
+ if (batchLine) {
269
+ console.log('');
270
+ console.log(` ${batchLine}`);
271
+ }
272
+ }
273
+
274
+ console.log('');
275
+ }
276
+
277
+ status(cwd) {
278
+ this.show(cwd);
279
+ }
280
+
281
+ async validate(cwd) {
282
+ const data = this.read(cwd);
283
+ if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
284
+
285
+ const errors = [];
286
+ if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
287
+ errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
288
+ }
289
+ if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
290
+ if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
291
+
292
+ if (errors.length === 0) { console.log('✅ progress.json 格式正确'); return true; }
293
+
294
+ console.log(`⚠️ 发现问题,尝试修复...`);
295
+ let fixed = { ...data, stages: { ...data.stages } };
296
+ let changed = false;
297
+ if (!fixed.project) {
298
+ fixed.project = basename(cwd);
299
+ changed = true;
300
+ }
301
+ if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
302
+ fixed._version = CURRENT_VERSION;
303
+ changed = true;
304
+ }
305
+ for (const s of VALID_STAGES) {
306
+ if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
307
+ }
308
+ if (changed) {
309
+ this._backup(cwd);
310
+ this._write(cwd, fixed);
311
+ console.log('✅ 已修复并备份');
312
+ }
313
+
314
+ return true;
315
+ }
316
+
317
+ reset(cwd, stage) {
318
+ this._ensureDir(cwd);
319
+
320
+ if (stage) {
321
+ this._backup(cwd);
322
+ const data = this.read(cwd);
323
+ if (!data) { console.log('❌ 无法读取 progress.json'); return; }
324
+ if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
325
+ data.stages[stage] = emptyStage();
326
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
327
+ this._write(cwd, data);
328
+ console.log(`✅ 已重置阶段: ${stage}`);
329
+ } else {
330
+ const p = this._path(cwd, PROGRESS_FILE);
331
+ const backup = this._path(cwd, BACKUP_FILE);
332
+ let didReset = false;
333
+ if (existsSync(p)) { unlinkSync(p); didReset = true; }
334
+ if (existsSync(backup)) { unlinkSync(backup); didReset = true; }
335
+ if (didReset) {
336
+ console.log('✅ 已重置所有进度');
337
+ } else {
338
+ console.log('ℹ️ 无进度文件可重置');
339
+ }
340
+ }
341
+ }
342
+
343
+ // ── 内部辅助 ──
344
+
345
+ _readOrInit(cwd) {
346
+ let data = this.read(cwd);
347
+ if (!data) {
348
+ this._ensureDir(cwd);
349
+ const progressPath = this._path(cwd, PROGRESS_FILE);
350
+ if (!existsSync(progressPath)) {
351
+ data = makeInitialProgress(basename(cwd));
352
+ this._write(cwd, data);
353
+ } else {
354
+ console.log('❌ progress.json 损坏,请运行 sillyspec progress validate');
355
+ return null;
356
+ }
357
+ }
358
+ return data;
359
+ }
360
+
361
+ _requireStage(cwd, stage) {
362
+ if (!VALID_STAGES.includes(stage)) {
363
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
364
+ return null;
365
+ }
366
+ const data = this._readOrInit(cwd);
367
+ if (!data) return null;
368
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
369
+ return data;
370
+ }
371
+
372
+ _parseWithRecovery(jsonString) {
373
+ try { return JSON.parse(jsonString); } catch {}
374
+
375
+ let fixed = jsonString.replace(/,\s*([}\]])/g, '$1');
376
+ fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
377
+ fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
378
+ try { return JSON.parse(fixed); } catch {}
379
+
380
+ const lastBrace = fixed.lastIndexOf('}');
381
+ if (lastBrace > 0) {
382
+ let open = 0;
383
+ for (const ch of fixed.substring(0, lastBrace + 1)) {
384
+ if (ch === '{') open++;
385
+ if (ch === '}') open--;
386
+ }
387
+ try { return JSON.parse(fixed.substring(0, lastBrace + 1) + '}'.repeat(Math.max(0, open))); } catch {}
388
+ }
389
+ return null;
390
+ }
391
+
392
+ _timeAgo(dateStr) {
393
+ if (!dateStr) return '未知';
394
+ let ts = Date.parse(dateStr);
395
+ // toLocaleString('zh-CN') 格式(如 2026/5/12 21:09:00)可能解析失败,尝试手动解析
396
+ if (isNaN(ts)) {
397
+ const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
398
+ if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
399
+ }
400
+ if (isNaN(ts)) return dateStr;
401
+ const diff = Date.now() - ts;
402
+ const minutes = Math.floor(diff / 60000);
403
+ if (minutes < 1) return '刚刚';
404
+ if (minutes < 60) return `${minutes} 分钟前`;
405
+ const hours = Math.floor(minutes / 60);
406
+ if (hours < 24) return `${hours} 小时前`;
407
+ return `${Math.floor(hours / 24)} 天前`;
408
+ }
409
+
410
+ // ── 批量进度 ──
411
+
412
+ updateBatchProgress(cwd, batchData) {
413
+ const data = this._readOrInit(cwd);
414
+ if (!data) return;
415
+
416
+ if (!data.batchProgress) {
417
+ data.batchProgress = { total: 0, completed: 0, failed: 0, skipped: 0 };
418
+ }
419
+ if (batchData.total !== undefined) data.batchProgress.total = batchData.total;
420
+ if (batchData.completed !== undefined) data.batchProgress.completed = batchData.completed;
421
+ if (batchData.failed !== undefined) data.batchProgress.failed = batchData.failed;
422
+ if (batchData.skipped !== undefined) data.batchProgress.skipped = batchData.skipped;
423
+
424
+ data.lastActive = new Date().toLocaleString('zh-CN', { hour12: false });
425
+ this._backup(cwd);
426
+ this._write(cwd, data);
427
+ }
428
+
429
+ readBatchProgress(cwd) {
430
+ const data = this.read(cwd);
431
+ return data?.batchProgress || null;
432
+ }
433
+
434
+ _renderBatchProgress(batchProgress) {
435
+ if (!batchProgress || !batchProgress.total) return null;
436
+ const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
437
+ const done = Math.min(completed + failed + skipped, total);
438
+ const barLen = 20;
439
+ const filled = Math.round((completed / total) * barLen);
440
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
441
+ const parts = [];
442
+ if (failed > 0) parts.push(`${failed} 失败`);
443
+ if (skipped > 0) parts.push(`${skipped} 跳过`);
444
+ const suffix = parts.length ? ` (${parts.join(', ')})` : '';
445
+ return `📊 批量进度: ${bar} ${completed}/${total}${suffix}`;
446
+ }
447
+
448
+ _ensureGitignore(cwd) {
449
+ const gitignorePath = join(cwd, '.gitignore');
450
+ const rule = '.sillyspec/.runtime/';
451
+ if (existsSync(gitignorePath)) {
452
+ const content = readFileSync(gitignorePath, 'utf8');
453
+ if (content.includes(rule)) return;
454
+ writeFileSync(gitignorePath, content.trimEnd() + '\n' + rule + '\n');
455
+ } else {
456
+ writeFileSync(gitignorePath, rule + '\n');
457
+ }
458
+ }
459
+ }