sillyspec 3.8.7 → 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 (163) 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 +9 -11
  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 +136 -14
  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/docs/.vitepress/config.mts +0 -45
  88. package/docs/.vitepress/dist/404.html +0 -25
  89. package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
  90. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
  91. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
  92. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
  93. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
  94. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  95. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  96. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  97. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  98. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  99. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  100. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  101. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  102. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  103. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  104. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  105. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  106. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  107. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  108. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
  109. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
  110. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
  111. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
  112. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
  113. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
  114. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
  115. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
  116. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
  117. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
  118. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
  119. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
  120. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
  121. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
  122. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
  123. package/docs/.vitepress/dist/hashmap.json +0 -1
  124. package/docs/.vitepress/dist/index.html +0 -28
  125. package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
  126. package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
  127. package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
  128. package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
  129. package/docs/.vitepress/dist/sillyspec/install.html +0 -32
  130. package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
  131. package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
  132. package/docs/.vitepress/dist/vp-icons.css +0 -1
  133. package/docs/index.md +0 -34
  134. package/docs/sillyspec/commands.md +0 -218
  135. package/docs/sillyspec/dashboard.md +0 -51
  136. package/docs/sillyspec/file-io.md +0 -34
  137. package/docs/sillyspec/getting-started.md +0 -61
  138. package/docs/sillyspec/install.md +0 -51
  139. package/docs/sillyspec/lifecycle.md +0 -146
  140. package/docs/sillyspec/structure.md +0 -62
  141. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
  142. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
  143. package/templates/archive.md +0 -120
  144. package/templates/brainstorm.md +0 -170
  145. package/templates/continue.md +0 -32
  146. package/templates/execute.md +0 -304
  147. package/templates/explore.md +0 -59
  148. package/templates/export.md +0 -21
  149. package/templates/init.md +0 -61
  150. package/templates/plan.md +0 -146
  151. package/templates/quick.md +0 -135
  152. package/templates/scan-quick.md +0 -49
  153. package/templates/scan.md +0 -156
  154. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  155. package/templates/status.md +0 -75
  156. package/templates/verify.md +0 -236
  157. package/templates/workspace-sync.md +0 -99
  158. package/templates/workspace.md +0 -70
  159. /package/.sillyspec/{specs → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
  160. /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
  161. /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
  162. /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
  163. /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
+ }