openfeel 1.0.0

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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +140 -0
  3. package/README.md +17 -0
  4. package/README.zh-CN.md +140 -0
  5. package/bin/openfeel.js +13 -0
  6. package/dist/cli/index.d.ts +11 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +119 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/repl.d.ts +10 -0
  11. package/dist/cli/repl.d.ts.map +1 -0
  12. package/dist/cli/repl.js +74 -0
  13. package/dist/cli/repl.js.map +1 -0
  14. package/dist/commands/archive.d.ts +7 -0
  15. package/dist/commands/archive.d.ts.map +1 -0
  16. package/dist/commands/archive.js +38 -0
  17. package/dist/commands/archive.js.map +1 -0
  18. package/dist/commands/config.d.ts +15 -0
  19. package/dist/commands/config.d.ts.map +1 -0
  20. package/dist/commands/config.js +208 -0
  21. package/dist/commands/config.js.map +1 -0
  22. package/dist/commands/flow.d.ts +24 -0
  23. package/dist/commands/flow.d.ts.map +1 -0
  24. package/dist/commands/flow.js +1024 -0
  25. package/dist/commands/flow.js.map +1 -0
  26. package/dist/commands/init.d.ts +9 -0
  27. package/dist/commands/init.d.ts.map +1 -0
  28. package/dist/commands/init.js +62 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.d.ts +7 -0
  31. package/dist/commands/instructions.d.ts.map +1 -0
  32. package/dist/commands/instructions.js +36 -0
  33. package/dist/commands/instructions.js.map +1 -0
  34. package/dist/commands/knowledge.d.ts +7 -0
  35. package/dist/commands/knowledge.d.ts.map +1 -0
  36. package/dist/commands/knowledge.js +174 -0
  37. package/dist/commands/knowledge.js.map +1 -0
  38. package/dist/commands/lint.d.ts +12 -0
  39. package/dist/commands/lint.d.ts.map +1 -0
  40. package/dist/commands/lint.js +192 -0
  41. package/dist/commands/lint.js.map +1 -0
  42. package/dist/commands/plan.d.ts +7 -0
  43. package/dist/commands/plan.d.ts.map +1 -0
  44. package/dist/commands/plan.js +73 -0
  45. package/dist/commands/plan.js.map +1 -0
  46. package/dist/commands/project.d.ts +7 -0
  47. package/dist/commands/project.d.ts.map +1 -0
  48. package/dist/commands/project.js +189 -0
  49. package/dist/commands/project.js.map +1 -0
  50. package/dist/commands/roadmap.d.ts +7 -0
  51. package/dist/commands/roadmap.d.ts.map +1 -0
  52. package/dist/commands/roadmap.js +26 -0
  53. package/dist/commands/roadmap.js.map +1 -0
  54. package/dist/commands/stage.d.ts +17 -0
  55. package/dist/commands/stage.d.ts.map +1 -0
  56. package/dist/commands/stage.js +291 -0
  57. package/dist/commands/stage.js.map +1 -0
  58. package/dist/commands/update.d.ts +8 -0
  59. package/dist/commands/update.d.ts.map +1 -0
  60. package/dist/commands/update.js +87 -0
  61. package/dist/commands/update.js.map +1 -0
  62. package/dist/commands/view.d.ts +7 -0
  63. package/dist/commands/view.d.ts.map +1 -0
  64. package/dist/commands/view.js +77 -0
  65. package/dist/commands/view.js.map +1 -0
  66. package/dist/core/archive/merge.d.ts +20 -0
  67. package/dist/core/archive/merge.d.ts.map +1 -0
  68. package/dist/core/archive/merge.js +118 -0
  69. package/dist/core/archive/merge.js.map +1 -0
  70. package/dist/core/artifact-graph/graph.d.ts +62 -0
  71. package/dist/core/artifact-graph/graph.d.ts.map +1 -0
  72. package/dist/core/artifact-graph/graph.js +184 -0
  73. package/dist/core/artifact-graph/graph.js.map +1 -0
  74. package/dist/core/artifact-graph/index.d.ts +10 -0
  75. package/dist/core/artifact-graph/index.d.ts.map +1 -0
  76. package/dist/core/artifact-graph/index.js +8 -0
  77. package/dist/core/artifact-graph/index.js.map +1 -0
  78. package/dist/core/artifact-graph/instruction-loader.d.ts +35 -0
  79. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -0
  80. package/dist/core/artifact-graph/instruction-loader.js +358 -0
  81. package/dist/core/artifact-graph/instruction-loader.js.map +1 -0
  82. package/dist/core/artifact-graph/resolver.d.ts +10 -0
  83. package/dist/core/artifact-graph/resolver.d.ts.map +1 -0
  84. package/dist/core/artifact-graph/resolver.js +32 -0
  85. package/dist/core/artifact-graph/resolver.js.map +1 -0
  86. package/dist/core/artifact-graph/state.d.ts +9 -0
  87. package/dist/core/artifact-graph/state.d.ts.map +1 -0
  88. package/dist/core/artifact-graph/state.js +24 -0
  89. package/dist/core/artifact-graph/state.js.map +1 -0
  90. package/dist/core/artifact-graph/types.d.ts +22 -0
  91. package/dist/core/artifact-graph/types.d.ts.map +1 -0
  92. package/dist/core/artifact-graph/types.js +2 -0
  93. package/dist/core/artifact-graph/types.js.map +1 -0
  94. package/dist/core/config.d.ts +256 -0
  95. package/dist/core/config.d.ts.map +1 -0
  96. package/dist/core/config.js +393 -0
  97. package/dist/core/config.js.map +1 -0
  98. package/dist/core/flow-manager.d.ts +492 -0
  99. package/dist/core/flow-manager.d.ts.map +1 -0
  100. package/dist/core/flow-manager.js +2379 -0
  101. package/dist/core/flow-manager.js.map +1 -0
  102. package/dist/core/i18n-data/en.d.ts +26 -0
  103. package/dist/core/i18n-data/en.d.ts.map +1 -0
  104. package/dist/core/i18n-data/en.js +492 -0
  105. package/dist/core/i18n-data/en.js.map +1 -0
  106. package/dist/core/i18n-data/types.d.ts +18 -0
  107. package/dist/core/i18n-data/types.d.ts.map +1 -0
  108. package/dist/core/i18n-data/types.js +8 -0
  109. package/dist/core/i18n-data/types.js.map +1 -0
  110. package/dist/core/i18n-data/zh-CN.d.ts +28 -0
  111. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -0
  112. package/dist/core/i18n-data/zh-CN.js +524 -0
  113. package/dist/core/i18n-data/zh-CN.js.map +1 -0
  114. package/dist/core/i18n.d.ts +40 -0
  115. package/dist/core/i18n.d.ts.map +1 -0
  116. package/dist/core/i18n.js +135 -0
  117. package/dist/core/i18n.js.map +1 -0
  118. package/dist/core/init.d.ts +22 -0
  119. package/dist/core/init.d.ts.map +1 -0
  120. package/dist/core/init.js +292 -0
  121. package/dist/core/init.js.map +1 -0
  122. package/dist/core/metrics.d.ts +60 -0
  123. package/dist/core/metrics.d.ts.map +1 -0
  124. package/dist/core/metrics.js +137 -0
  125. package/dist/core/metrics.js.map +1 -0
  126. package/dist/core/pipeline-schema.d.ts +78 -0
  127. package/dist/core/pipeline-schema.d.ts.map +1 -0
  128. package/dist/core/pipeline-schema.js +57 -0
  129. package/dist/core/pipeline-schema.js.map +1 -0
  130. package/dist/core/plan/roadmap.d.ts +17 -0
  131. package/dist/core/plan/roadmap.d.ts.map +1 -0
  132. package/dist/core/plan/roadmap.js +71 -0
  133. package/dist/core/plan/roadmap.js.map +1 -0
  134. package/dist/core/plan/scheme.d.ts +34 -0
  135. package/dist/core/plan/scheme.d.ts.map +1 -0
  136. package/dist/core/plan/scheme.js +262 -0
  137. package/dist/core/plan/scheme.js.map +1 -0
  138. package/dist/core/plan/stage.d.ts +20 -0
  139. package/dist/core/plan/stage.d.ts.map +1 -0
  140. package/dist/core/plan/stage.js +120 -0
  141. package/dist/core/plan/stage.js.map +1 -0
  142. package/dist/core/public-logger.d.ts +84 -0
  143. package/dist/core/public-logger.d.ts.map +1 -0
  144. package/dist/core/public-logger.js +344 -0
  145. package/dist/core/public-logger.js.map +1 -0
  146. package/dist/core/schema.d.ts +70 -0
  147. package/dist/core/schema.d.ts.map +1 -0
  148. package/dist/core/schema.js +61 -0
  149. package/dist/core/schema.js.map +1 -0
  150. package/dist/core/template-loader.d.ts +22 -0
  151. package/dist/core/template-loader.d.ts.map +1 -0
  152. package/dist/core/template-loader.js +2848 -0
  153. package/dist/core/template-loader.js.map +1 -0
  154. package/dist/core/templates.d.ts +18 -0
  155. package/dist/core/templates.d.ts.map +1 -0
  156. package/dist/core/templates.js +50 -0
  157. package/dist/core/templates.js.map +1 -0
  158. package/dist/core/update.d.ts +40 -0
  159. package/dist/core/update.d.ts.map +1 -0
  160. package/dist/core/update.js +1569 -0
  161. package/dist/core/update.js.map +1 -0
  162. package/dist/core/view/entry.d.ts +37 -0
  163. package/dist/core/view/entry.d.ts.map +1 -0
  164. package/dist/core/view/entry.js +108 -0
  165. package/dist/core/view/entry.js.map +1 -0
  166. package/dist/core/workspace/identity.d.ts +60 -0
  167. package/dist/core/workspace/identity.d.ts.map +1 -0
  168. package/dist/core/workspace/identity.js +169 -0
  169. package/dist/core/workspace/identity.js.map +1 -0
  170. package/dist/core/workspace/knowledge.d.ts +55 -0
  171. package/dist/core/workspace/knowledge.d.ts.map +1 -0
  172. package/dist/core/workspace/knowledge.js +230 -0
  173. package/dist/core/workspace/knowledge.js.map +1 -0
  174. package/dist/core/workspace/structure.d.ts +9 -0
  175. package/dist/core/workspace/structure.d.ts.map +1 -0
  176. package/dist/core/workspace/structure.js +59 -0
  177. package/dist/core/workspace/structure.js.map +1 -0
  178. package/dist/index.d.ts +7 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +7 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/utils/kb-dedup.d.ts +46 -0
  183. package/dist/utils/kb-dedup.d.ts.map +1 -0
  184. package/dist/utils/kb-dedup.js +169 -0
  185. package/dist/utils/kb-dedup.js.map +1 -0
  186. package/dist/utils/path.d.ts +22 -0
  187. package/dist/utils/path.d.ts.map +1 -0
  188. package/dist/utils/path.js +37 -0
  189. package/dist/utils/path.js.map +1 -0
  190. package/package.json +61 -0
  191. package/schemas/spec-driven/schema.yaml +50 -0
  192. package/schemas/spec-driven/templates/design.md +43 -0
  193. package/schemas/spec-driven/templates/plan.md +46 -0
  194. package/schemas/spec-driven/templates/proposal.md +35 -0
  195. package/schemas/spec-driven/templates/tasks.md +39 -0
  196. package/scripts/patch-inquirer.js +70 -0
@@ -0,0 +1,1024 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, copyFileSync, readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { FlowManager } from '../core/flow-manager.js';
5
+ import { PipelinePhaseSchema, PIPELINE_PHASES } from '../core/pipeline-schema.js';
6
+ import { MetricsStore } from '../core/metrics.js';
7
+ import { t, getCliLang } from '../core/i18n.js';
8
+ export function registerFlowCommand(program) {
9
+ const flow = program
10
+ .command('flow')
11
+ .description('流水线状态管理');
12
+ // flow status — 输出流水线摘要
13
+ flow
14
+ .command('status')
15
+ .description('显示流水线状态摘要')
16
+ .option('--verbose', '增强输出:配置级联、最近状态变更、下游 Agent 就绪状态')
17
+ .option('-n, --lines <n>', '最近状态变更条数(默认 5)', '5')
18
+ .action((options) => {
19
+ const lang = getCliLang(process.cwd());
20
+ const mgr = createManager();
21
+ if (!options.verbose) {
22
+ console.log(mgr.summary(lang));
23
+ // 各阶段 phase 展示
24
+ const data = mgr.getData();
25
+ if (data?.pipeline?.current?.stage && data.stages[data.pipeline.current.stage]) {
26
+ const curStage = data.pipeline.current.stage;
27
+ const curPhase = data.stages[curStage].phase;
28
+ console.log('\n' + t('flow.status.currentStage', lang) + `: ${curStage} (${curPhase})`);
29
+ }
30
+ // 阶段耗时统计(含 phase 显示)
31
+ const allStats = mgr.getAllStageStats();
32
+ if (Object.keys(allStats).length > 0) {
33
+ console.log('\n' + t('flow.status.stageDuration', lang) + ':');
34
+ for (const [stageId, s] of Object.entries(allStats)) {
35
+ const duration = formatDuration(s.duration_ms);
36
+ const stagePhase = data?.stages[stageId]?.phase ?? '';
37
+ console.log(` ${stageId} [${stagePhase}]: ${duration}${s.end_time ? '' : ' ' + t('common.inProgress', lang)}`);
38
+ }
39
+ }
40
+ return;
41
+ }
42
+ const n = Math.max(1, parseInt(options.lines, 10) || 5);
43
+ const v = mgr.verboseSummary(n);
44
+ // ── 基本摘要 ──
45
+ const mgrData = mgr.getData();
46
+ const stagePhase = mgrData?.pipeline?.current?.stage && mgrData.stages[mgrData.pipeline.current.stage]
47
+ ? mgrData.stages[mgrData.pipeline.current.stage].phase
48
+ : '(无)';
49
+ console.log(t('flow.status.verboseTitle', lang) + '\n');
50
+ console.log(t('flow.status.globalStatus', lang) + `: ${v.basic.phase}`);
51
+ console.log(t('flow.status.currentStageLabel', lang) + `: ${mgrData?.pipeline?.current?.stage ?? t('common.none', lang)} — ` + t('flow.status.stagePhase', lang) + `: ${stagePhase}`);
52
+ console.log(t('flow.status.currentOp', lang) + `: ${v.basic.currentOp ?? t('common.none', lang)}`);
53
+ console.log(t('flow.status.retryCount', lang) + `: ${v.basic.retryCount}`);
54
+ console.log(t('flow.status.stagesCount', lang) + `: ${v.basic.stagesCount}`);
55
+ console.log(t('flow.status.opsCount', lang) + `: ${v.basic.opsCount}`);
56
+ console.log(t('flow.status.reviewPending', lang) + `: ${v.basic.reviewItemsOpen}`);
57
+ console.log(t('flow.status.logTotal', lang) + `: ${v.basic.recentLogs}`);
58
+ console.log('');
59
+ // ── 配置级联状态 ──
60
+ console.log(t('flow.status.cascadeTitle', lang));
61
+ const allKeys = new Set([
62
+ ...Object.keys(v.cascade.configDefaults),
63
+ ...Object.keys(v.cascade.statusOverrides),
64
+ ]);
65
+ if (allKeys.size === 0) {
66
+ console.log(t('common.noConfig', lang));
67
+ }
68
+ else {
69
+ console.log(t('flow.status.cascadeHeader', lang));
70
+ console.log('─────────────────────────────────────────────────');
71
+ for (const key of [...allKeys].sort()) {
72
+ const def = v.cascade.configDefaults[key] ?? '-';
73
+ const over = v.cascade.statusOverrides[key] ?? '-';
74
+ const eff = v.cascade.effective[key] ?? '-';
75
+ const overFlag = v.cascade.statusOverrides[key] ? '*' : ' ';
76
+ console.log(`${key.padEnd(18)} ${def.padEnd(12)} ${overFlag}${over.padEnd(11)} ${eff}`);
77
+ }
78
+ console.log(t('flow.status.cascadeNote', lang));
79
+ }
80
+ console.log('');
81
+ // ── 最近 N 条状态变更 ──
82
+ console.log(t('flow.status.recentTitleTmpl', lang, { n: String(n) }));
83
+ if (v.recentChanges.length === 0) {
84
+ console.log(t('common.noData', lang));
85
+ }
86
+ else {
87
+ console.log(t('flow.status.recentHeader', lang));
88
+ console.log('───────────────────────────────────────────────────────');
89
+ for (const change of v.recentChanges) {
90
+ console.log(`${change.time.padEnd(17)} ${change.agent.padEnd(14)} ${change.change.padEnd(20)} ${change.description}`);
91
+ }
92
+ }
93
+ console.log('');
94
+ // ── 下游 Agent 就绪状态 ──
95
+ console.log(t('flow.status.downstreamTitle', lang));
96
+ if (v.downstreamPhases.length === 0) {
97
+ console.log(t('flow.status.noDownstream', lang));
98
+ }
99
+ else {
100
+ console.log(t('flow.status.downstreamHeader', lang));
101
+ console.log('──────────────────────────────────');
102
+ for (const dp of v.downstreamPhases) {
103
+ console.log(`${dp.phase.padEnd(20)} ${dp.responsibleAgent}`);
104
+ }
105
+ }
106
+ console.log('');
107
+ // ── 跨会话恢复信息 ──
108
+ const recovery = mgr.recoverContext(lang);
109
+ console.log(t('flow.status.recoveryTitle', lang));
110
+ console.log(` ` + t('common.stage', lang) + `: ${recovery.phase ?? t('common.unknown', lang)}`);
111
+ console.log(` ` + t('common.op', lang) + `: ${recovery.currentOp ?? t('common.none', lang)}`);
112
+ console.log(` ` + t('common.status', lang) + `: ${recovery.stageStatus}`);
113
+ if (recovery.blockedBy) {
114
+ console.log(` ` + t('common.blockedBy', lang) + `: ${recovery.blockedBy}`);
115
+ }
116
+ if (recovery.pendingTasks.length > 0) {
117
+ console.log(' ' + t('flow.recover.pendingTasksTmpl', lang, { n: String(Math.min(recovery.pendingTasks.length, 10)) }) + ':');
118
+ for (let i = 0; i < Math.min(recovery.pendingTasks.length, 10); i++) {
119
+ console.log(` ${i + 1}. ${recovery.pendingTasks[i]}`);
120
+ }
121
+ if (recovery.pendingTasks.length > 10) {
122
+ console.log(t('flow.status.recoveryMoreTmpl', lang, { n: String(recovery.pendingTasks.length - 10) }));
123
+ }
124
+ }
125
+ });
126
+ // flow overview — 全状态可视化(/opfx:status 的后端实现)
127
+ flow
128
+ .command('overview')
129
+ .description('全状态可视化视图(/opfx:status 的后端实现)')
130
+ .action(() => {
131
+ const lang = getCliLang(process.cwd());
132
+ const mgr = createManager();
133
+ if (!mgr.isLoaded()) {
134
+ console.log(t('common.noInit', lang));
135
+ return;
136
+ }
137
+ const phase = mgr.getPhase();
138
+ const current = mgr.getCurrent();
139
+ const summary = mgr.getSummary();
140
+ const data = mgr.getData();
141
+ // ═══ 标题 ═══
142
+ console.log('');
143
+ console.log('╔══════════════════════════════════════════╗');
144
+ console.log('║' + t('flow.overview.title', lang).padStart(38) + ' ║');
145
+ console.log('╚══════════════════════════════════════════╝');
146
+ console.log('');
147
+ // ── 当前状态 ──
148
+ console.log(t('flow.overview.currentStatus', lang));
149
+ const curStagePhase = current?.stage && data?.stages[current.stage]
150
+ ? data.stages[current.stage].phase
151
+ : phase;
152
+ console.log(` ` + t('common.stage', lang) + `: ${curStagePhase}`);
153
+ console.log(` ` + t('common.op', lang) + `: ${current ? `${current.stage}.${current.op}` : t('common.none', lang)}`);
154
+ const retrySuffix = t('common.retry', lang).toLowerCase() === 'retry' ? 'times' : '次';
155
+ console.log(` ` + t('common.retry', lang) + `: ${summary.retryCount} ${retrySuffix}`);
156
+ console.log('');
157
+ // ── 阶段总览 ──
158
+ console.log(t('flow.overview.stagesOverview', lang));
159
+ if (!data || Object.keys(data.stages).length === 0) {
160
+ console.log(' ' + t('flow.overview.noStages', lang));
161
+ }
162
+ else {
163
+ console.log(` ` + t('flow.overview.totalStagesTmpl', lang, { n: String(Object.keys(data.stages).length) }) + `:`);
164
+ for (const [stageId, stageData] of Object.entries(data.stages)) {
165
+ const opsTotal = Object.keys(stageData.ops).length;
166
+ const opsDone = Object.values(stageData.ops).filter((o) => o.state === 'done').length;
167
+ const marker = current?.stage === stageId ? '← ' + t('flow.wizard.currentLabel', lang) : '';
168
+ const bar = opsTotal > 0
169
+ ? '█'.repeat(opsDone) + '░'.repeat(opsTotal - opsDone)
170
+ : t('flow.overview.noOps', lang);
171
+ console.log(` ${stageId}: phase=${stageData.phase} ${bar} ${opsDone}/${opsTotal} ${marker}`);
172
+ }
173
+ }
174
+ console.log('');
175
+ // ── 审查条目 ──
176
+ console.log(t('flow.overview.reviewSection', lang));
177
+ if (!data || data.reviews.length === 0) {
178
+ console.log(' ' + t('flow.overview.noReviews', lang));
179
+ }
180
+ else {
181
+ const openReviews = data.reviews.filter((r) => r.status === 'open');
182
+ const resolvedReviews = data.reviews.filter((r) => r.status === 'resolved');
183
+ const closedReviews = data.reviews.filter((r) => r.status === 'closed');
184
+ const blockingOpen = openReviews.filter((r) => r.blocking !== false);
185
+ const nonBlockingOpen = openReviews.filter((r) => r.blocking === false);
186
+ console.log(` ` + t('flow.overview.reviewOpen', lang) + `: ${openReviews.length}` + `(` + t('flow.review.labelBlocking', lang) + ` ${blockingOpen.length} / ` + t('flow.review.labelNonBlocking', lang) + ` ${nonBlockingOpen.length})`);
187
+ console.log(` ` + t('flow.overview.reviewResolved', lang) + `: ${resolvedReviews.length}`);
188
+ console.log(` ` + t('flow.overview.reviewClosed', lang) + `: ${closedReviews.length}`);
189
+ if (openReviews.length > 0) {
190
+ console.log('');
191
+ console.log(' ' + t('flow.overview.reviewPending', lang) + ':');
192
+ for (const rev of openReviews) {
193
+ const blockIcon = rev.blocking !== false ? '🔴' : '🟡';
194
+ const priIcon = rev.priority === 'high' ? '↑' : rev.priority === 'low' ? '↓' : '=';
195
+ console.log(` ${blockIcon} [${priIcon}] ${rev.id}: ${rev.title} (${rev.op})`);
196
+ }
197
+ }
198
+ }
199
+ console.log('');
200
+ // ── Bug 统计 ──
201
+ console.log(t('flow.overview.bugSection', lang));
202
+ const bugsIndexPath = resolve(process.cwd(), '.openfeel', 'bugs', 'index.md');
203
+ if (existsSync(bugsIndexPath)) {
204
+ try {
205
+ const bugsContent = readFileSync(bugsIndexPath, 'utf-8');
206
+ const openMatch = bugsContent.match(/open[::]\s*(\d+)/i);
207
+ const closedMatch = bugsContent.match(/closed[::]\s*(\d+)/i);
208
+ const openBugs = openMatch ? parseInt(openMatch[1]) : 0;
209
+ const closedBugs = closedMatch ? parseInt(closedMatch[1]) : 0;
210
+ console.log(` ` + t('flow.overview.bugOpen', lang) + `: ${openBugs} ` + t('flow.overview.bugClosed', lang) + `: ${closedBugs}`);
211
+ }
212
+ catch {
213
+ console.log(' ' + t('flow.overview.bugUnreadable', lang));
214
+ }
215
+ }
216
+ else {
217
+ console.log(' ' + t('flow.overview.bugUninitialized', lang));
218
+ }
219
+ console.log('');
220
+ // ── 最近日志 ──
221
+ console.log(t('flow.overview.recentLogs', lang));
222
+ if (!data || data.log.length === 0) {
223
+ console.log(' ' + t('flow.overview.noLogs', lang));
224
+ }
225
+ else {
226
+ const recentLogs = data.log.slice(-5).reverse();
227
+ for (const entry of recentLogs) {
228
+ const time = entry.time.substring(0, 19).replace('T', ' ');
229
+ console.log(` [${time}] ${entry.agent}: ${entry.action}`);
230
+ }
231
+ }
232
+ console.log('');
233
+ // ── 健康状态 ──
234
+ console.log(t('flow.overview.health', lang));
235
+ const health = mgr.healthCheck(true); // quick mode
236
+ const passCount = health.items.filter((i) => i.status === 'pass').length;
237
+ const warnCount = health.items.filter((i) => i.status === 'warn').length;
238
+ const failCount = health.items.filter((i) => i.status === 'fail').length;
239
+ console.log(' ' + t('flow.overview.healthStatsTmpl', lang, { n: String(passCount), m: String(warnCount), k: String(failCount) }));
240
+ if (!health.ok) {
241
+ console.log('');
242
+ for (const item of health.items.filter((i) => i.status === 'fail')) {
243
+ console.log(` ❌ ${item.section}: ${item.message}`);
244
+ }
245
+ }
246
+ console.log('');
247
+ console.log('════════════════════════════════════════════');
248
+ });
249
+ // flow current — 显示全局状态 + stage + stage phase + op + retry
250
+ flow
251
+ .command('current')
252
+ .description('显示当前阶段和操作')
253
+ .action(() => {
254
+ const lang = getCliLang(process.cwd());
255
+ const mgr = createManager();
256
+ if (!mgr.isLoaded()) {
257
+ console.log(t('common.noInit', lang));
258
+ return;
259
+ }
260
+ const phase = mgr.getPhase(); // MetaPhase: active/paused/done
261
+ const current = mgr.getCurrent();
262
+ const summary = mgr.getSummary();
263
+ const data = mgr.getData();
264
+ const stagePhase = current?.stage && data?.stages[current.stage]
265
+ ? data.stages[current.stage].phase
266
+ : t('common.none', lang);
267
+ console.log(t('flow.current.globalStatus', lang) + `: ${phase}`);
268
+ console.log(t('common.stage', lang) + `: ${current?.stage ?? t('common.none', lang)}`);
269
+ console.log(t('flow.current.stagePhase', lang) + `: ${stagePhase}`);
270
+ console.log(t('flow.current.currentOp', lang) + `: ${current ? `${current.stage}.${current.op}` : t('common.none', lang)}`);
271
+ console.log(t('flow.current.retryCount', lang) + `: ${summary.retryCount}`);
272
+ });
273
+ // flow metrics — 展示 Agent 性能指标
274
+ flow
275
+ .command('metrics')
276
+ .description('展示 Agent 性能指标')
277
+ .action(() => {
278
+ const lang = getCliLang(process.cwd());
279
+ const store = MetricsStore.getInstance();
280
+ store.load();
281
+ console.log(store.summary());
282
+ });
283
+ // flow stage — 阶段管理子命令组
284
+ const stageCmd = flow
285
+ .command('stage')
286
+ .description('阶段管理');
287
+ // flow stage add <stageId>
288
+ stageCmd
289
+ .command('add')
290
+ .description('新增流水线阶段')
291
+ .argument('<stageId>', '阶段 ID(如 v4.3)')
292
+ .action((stageId) => {
293
+ const lang = getCliLang(process.cwd());
294
+ const mgr = createManager();
295
+ if (!mgr.isLoaded()) {
296
+ console.error(t('common.errorNoInit', lang));
297
+ process.exit(1);
298
+ }
299
+ try {
300
+ mgr.addStage(stageId);
301
+ mgr.save();
302
+ console.log(t('flow.stage.addedTmpl', lang, { stage: stageId }));
303
+ }
304
+ catch (err) {
305
+ const msg = err instanceof Error ? err.message : String(err);
306
+ console.error(t('common.errorTmpl', lang, { msg }));
307
+ process.exit(1);
308
+ }
309
+ });
310
+ // flow advance --stage <id> --to <phase> [--op <id>] [--force]
311
+ flow
312
+ .command('advance')
313
+ .description('推进流水线阶段')
314
+ .option('--op <id>', '操作 ID(如 stage-01.op-001),仅用于日志/展示')
315
+ .requiredOption('--to <phase>', '目标阶段(如 exec_running)')
316
+ .option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
317
+ .option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
318
+ .action((options) => {
319
+ const lang = getCliLang(process.cwd());
320
+ // 自定义 --stage 必选校验(提供中文错误提示)
321
+ if (!options.stage) {
322
+ console.error(t('flow.advance.errorNoStage', lang));
323
+ process.exit(1);
324
+ }
325
+ const mgr = createManager();
326
+ if (!mgr.isLoaded()) {
327
+ console.error(t('common.errorNoInit', lang));
328
+ process.exit(1);
329
+ }
330
+ // 自动修复 phase/status 不一致(在 validate() 前执行)
331
+ if (options.stage) {
332
+ const repairResult = mgr.autoRepairInconsistency(options.stage);
333
+ if (repairResult.fixed) {
334
+ console.log(t('flow.advance.autoRepaired', lang) + `: ${repairResult.detail}`);
335
+ mgr.save();
336
+ }
337
+ }
338
+ const { valid, errors, warnings } = mgr.validate();
339
+ // 输出自动修正警告
340
+ if (warnings.length > 0) {
341
+ for (const w of warnings) {
342
+ console.warn(`[WARN] ${w}`);
343
+ }
344
+ console.log(t('flow.advance.warnAutoCorrect', lang));
345
+ }
346
+ if (!valid) {
347
+ console.error(t('flow.advance.errorInvalidFormat', lang));
348
+ for (const err of errors) {
349
+ console.error(` - ${err}`);
350
+ }
351
+ process.exit(1);
352
+ }
353
+ // 非法 phase 校验(P1):拒绝不在枚举中的目标 phase;--force 可跳过此项,但不可绕过 REV 阻塞检查
354
+ if (!options.force) {
355
+ const phaseResult = PipelinePhaseSchema.safeParse(options.to);
356
+ if (!phaseResult.success) {
357
+ console.error(t('flow.advance.errorInvalidPhaseTmpl', lang, { phase: options.to }));
358
+ console.error(t('flow.advance.labelValidPhases', lang) + `: [${PIPELINE_PHASES.join(', ')}]`);
359
+ console.error(t('flow.advance.hintUseForceFuzzy', lang));
360
+ process.exit(1);
361
+ }
362
+ }
363
+ // 阶段跳跃保护:基于 stage phase 检查当前 phase 到目标 phase 是否存在直接路径
364
+ if (!options.force) {
365
+ const phaseResult = PipelinePhaseSchema.safeParse(options.to);
366
+ if (phaseResult.success && !mgr.hasTransition(options.to, options.stage)) {
367
+ console.error(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: options.stage || '', to: options.to }));
368
+ console.error(t('flow.advance.hintUseForce', lang));
369
+ process.exit(1);
370
+ }
371
+ }
372
+ // 安全提示:跳过审查直接 done
373
+ const SKIP_WARN_PHASES = ['exec_running', 'review_pending'];
374
+ if (options.to === 'done' && options.stage) {
375
+ const stage = (mgr.getData()?.stages || {})[options.stage];
376
+ if (stage && SKIP_WARN_PHASES.includes(stage.phase)) {
377
+ console.warn(t('flow.advance.warnSkipReview', lang));
378
+ }
379
+ }
380
+ // REV 闭环(命令层兜底):推进到 done 时检查 blocking REV
381
+ if (options.to === 'done' && options.stage) {
382
+ const allReviews = mgr.getReviewItems();
383
+ const stageReviews = allReviews.filter((r) => r.op.startsWith(options.stage) || r.op === options.stage);
384
+ const blockingOpen = stageReviews.filter((r) => r.blocking !== false && r.status === 'open');
385
+ if (blockingOpen.length > 0) {
386
+ console.warn(`[!] 检测到 ${blockingOpen.length} 个未解决的阻塞 REV:`);
387
+ for (const rev of blockingOpen) {
388
+ console.warn(` ${rev.id}: ${rev.title} (priority=${rev.priority})`);
389
+ }
390
+ if (options.force) {
391
+ console.warn('[!] --force 已指定,但 REV 安全检查不可绕过。拒绝推进。');
392
+ }
393
+ console.error('错误:blocking REV 未解决前禁止推进到 done。');
394
+ console.error('请先解决上述 REV 或通过 flow review resolve 标记为非阻塞。');
395
+ process.exit(1);
396
+ }
397
+ }
398
+ let archived = false;
399
+ try {
400
+ archived = mgr.advanceStagePhase(options.stage, options.to, 'cli');
401
+ }
402
+ catch (err) {
403
+ const msg = err instanceof Error ? err.message : String(err);
404
+ console.error(t('common.errorTmpl', lang, { msg }));
405
+ process.exit(1);
406
+ }
407
+ mgr.save();
408
+ // 归档 commit 必须在 flow.json save 之后执行,确保 commit 包含本次 phase 变更
409
+ if (archived) {
410
+ mgr.autoCommitOnDone(options.stage);
411
+ }
412
+ console.log(t('flow.advance.okTmpl', lang, { stage: options.stage || '', to: options.to }));
413
+ // git 脏区检查(安全网):Executor 未提交时输出醒目警告
414
+ try {
415
+ const gitStatus = execSync('git status --porcelain', {
416
+ cwd: process.cwd(),
417
+ encoding: 'utf-8',
418
+ timeout: 5000,
419
+ }).trim();
420
+ if (gitStatus) {
421
+ console.warn('[!] ╔════════════════════════════════════════╗');
422
+ console.warn('[!] ║ ⚠ Git 脏区警告:存在未提交的变更 ║');
423
+ console.warn('[!] ║ 请确认 Executor 已完成 git commit ║');
424
+ console.warn('[!] ╚════════════════════════════════════════╝');
425
+ }
426
+ }
427
+ catch {
428
+ // git 不可用(无 .git 目录或 git 未安装)时静默跳过
429
+ }
430
+ if (options.op) {
431
+ console.log(t('flow.advance.opLabelTmpl', lang, { op: options.op }));
432
+ }
433
+ });
434
+ // flow attempt --op <id> --result <pass|fail>
435
+ flow
436
+ .command('attempt')
437
+ .description('记录操作执行结果')
438
+ .requiredOption('--op <id>', '操作 ID(如 stage-01.op-001)')
439
+ .requiredOption('--result <pass|fail>', '执行结果(pass 或 fail)')
440
+ .action((options) => {
441
+ const lang = getCliLang(process.cwd());
442
+ const mgr = createManager();
443
+ if (!mgr.isLoaded()) {
444
+ console.error(t('common.errorNoInit', lang));
445
+ process.exit(1);
446
+ }
447
+ if (options.result !== 'pass' && options.result !== 'fail') {
448
+ console.error(t('flow.attempt.errorInvalidResult', lang));
449
+ process.exit(1);
450
+ }
451
+ const outcome = mgr.recordAttempt(options.op, options.result);
452
+ mgr.save();
453
+ if (options.result === 'pass') {
454
+ console.log(t('flow.attempt.passTmpl', lang, { op: options.op }));
455
+ }
456
+ else if (outcome.shouldRetry) {
457
+ console.log(t('flow.attempt.failRetryTmpl', lang, { op: options.op }));
458
+ }
459
+ else if (outcome.shouldReplan) {
460
+ console.log(t('flow.attempt.failReplanTmpl', lang, { op: options.op }));
461
+ // BUG-03 修复:shouldReplan 时自动推进到 scheme_pending
462
+ mgr.advancePhase(options.op, 'scheme_pending');
463
+ mgr.save();
464
+ console.log(t('flow.attempt.autoReplan', lang));
465
+ }
466
+ });
467
+ // flow log [--last <n>]
468
+ flow
469
+ .command('log')
470
+ .description('显示最近操作日志')
471
+ .option('--last <n>', '显示最近 n 条(默认 10)', '10')
472
+ .action((options) => {
473
+ const lang = getCliLang(process.cwd());
474
+ const mgr = createManager();
475
+ if (!mgr.isLoaded()) {
476
+ console.log(t('flow.log.noInit', lang));
477
+ return;
478
+ }
479
+ const n = Math.max(1, parseInt(options.last, 10) || 10);
480
+ const data = mgr.getData();
481
+ if (!data || data.log.length === 0) {
482
+ console.log(t('flow.log.noLogs', lang));
483
+ return;
484
+ }
485
+ const recent = data.log.slice(-n);
486
+ console.log(t('flow.log.recentTitleTmpl', lang, { n: String(recent.length) }) + ':\n');
487
+ for (const entry of recent) {
488
+ const time = entry.time.substring(0, 19).replace('T', ' ');
489
+ console.log(`[${time}] ${entry.agent} — ${entry.action}`);
490
+ if (Object.keys(entry.detail).length > 0) {
491
+ console.log(` ` + t('flow.log.detail', lang) + `: ${JSON.stringify(entry.detail)}`);
492
+ }
493
+ console.log('');
494
+ }
495
+ });
496
+ // flow review — 审查子命令组
497
+ const reviewCmd = flow
498
+ .command('review')
499
+ .description('管理审查条目');
500
+ // flow review add --op <id> [--title] [--auto-fix <detail>]
501
+ reviewCmd
502
+ .command('add')
503
+ .description('添加审查条目')
504
+ .requiredOption('--op <id>', '操作 ID(如 stage-01.op-001)')
505
+ .option('--title <title>', '审查标题')
506
+ .option('--auto-fix <detail>', '自动修复说明,设置后跳过 scheme_pending 直接推进到 exec_running')
507
+ .option('--blocking [boolean]', '是否阻塞流水线(默认 true)', 'true')
508
+ .action((options) => {
509
+ const lang = getCliLang(process.cwd());
510
+ const mgr = createManager();
511
+ if (!mgr.isLoaded()) {
512
+ console.error(t('common.errorNoInit', lang));
513
+ process.exit(1);
514
+ }
515
+ // 自动生成 REV ID(基于已有审查条目数量递增)
516
+ const existingReviews = mgr.getReviewItems();
517
+ const revId = `REV-${String(existingReviews.length + 1).padStart(3, '0')}`;
518
+ const reviewItem = {
519
+ id: revId,
520
+ op: options.op,
521
+ status: 'open',
522
+ priority: 'medium',
523
+ title: options.title || t('flow.review.detail', lang) + `: ${options.op}`,
524
+ filed_by: 'cli',
525
+ filed_at: new Date().toISOString(),
526
+ canAutoFix: !!options.autoFix,
527
+ autoFixDetail: options.autoFix,
528
+ blocking: options.blocking !== undefined ? (options.blocking === 'false' || options.blocking === false ? false : true) : true,
529
+ };
530
+ if (options.autoFix) {
531
+ // opId 格式校验(来自 REV-011:命令层校验,避免无效 opId 穿透到 addAutoFixReview)
532
+ const dotIdx = options.op.lastIndexOf('.');
533
+ if (dotIdx === -1) {
534
+ console.error(t('common.invalidOpId', lang));
535
+ process.exit(1);
536
+ }
537
+ const stageId = options.op.substring(0, dotIdx);
538
+ const opLocalId = options.op.substring(dotIdx + 1);
539
+ // 校验 stage 是否存在于 flow.json
540
+ const data = mgr.getData();
541
+ if (!data || !data.stages[stageId]) {
542
+ console.error(t('flow.review.errorStageNotFoundTmpl', lang, { opId: options.op, stage: stageId }));
543
+ process.exit(1);
544
+ }
545
+ // 校验 op 是否存在于对应 stage 中
546
+ if (!data.stages[stageId].ops[opLocalId]) {
547
+ console.error(t('flow.review.errorOpNotFoundTmpl', lang, { opId: options.op, op: opLocalId, stage: stageId }));
548
+ process.exit(1);
549
+ }
550
+ // 自动修复路径:记录 REV 条目(状态直接 resolved),跳过 review_failed → scheme_pending
551
+ mgr.addAutoFixReview(reviewItem, options.op);
552
+ mgr.save();
553
+ const blockingLabel = reviewItem.blocking !== false ? t('flow.review.labelBlocking', lang) : t('flow.review.labelNonBlocking', lang);
554
+ console.log(t('flow.review.addedAutoFixTmpl', lang, { label: blockingLabel, revId }));
555
+ console.log(` ` + t('common.op', lang) + `: ${options.op}`);
556
+ console.log(` ` + t('flow.review.detail', lang) + `: ${options.autoFix}`);
557
+ console.log(t('flow.review.autoFixPhase', lang));
558
+ }
559
+ else {
560
+ mgr.addReview(reviewItem);
561
+ mgr.save();
562
+ const blockingLabel = reviewItem.blocking !== false ? t('flow.review.labelBlocking', lang) : t('flow.review.labelNonBlocking', lang);
563
+ console.log(t('flow.review.addedTmpl', lang, { label: blockingLabel, revId, op: options.op }) + (options.title ? ` — ${options.title}` : ''));
564
+ }
565
+ });
566
+ // flow review resolve <rev-id>
567
+ reviewCmd
568
+ .command('resolve')
569
+ .description('解决审查条目')
570
+ .argument('<rev-id>', '审查条目 ID(如 REV-001)')
571
+ .action((revId) => {
572
+ const lang = getCliLang(process.cwd());
573
+ const mgr = createManager();
574
+ if (!mgr.isLoaded()) {
575
+ console.error(t('common.errorNoInit', lang));
576
+ process.exit(1);
577
+ }
578
+ const ok = mgr.resolveReview(revId);
579
+ if (ok) {
580
+ mgr.save();
581
+ console.log(t('flow.review.resolvedTmpl', lang, { revId }));
582
+ }
583
+ else {
584
+ console.error(t('flow.review.notFoundTmpl', lang, { revId }));
585
+ process.exit(1);
586
+ }
587
+ });
588
+ // flow retry --op <id>
589
+ flow
590
+ .command('retry')
591
+ .description('查询操作的重试状态')
592
+ .requiredOption('--op <id>', '操作 ID(如 stage-01.op-001)')
593
+ .action((options) => {
594
+ const lang = getCliLang(process.cwd());
595
+ const mgr = createManager();
596
+ if (!mgr.isLoaded()) {
597
+ console.error(t('common.errorNoInit', lang));
598
+ process.exit(1);
599
+ }
600
+ // 通过 getData() 获取 op 的 max_attempts
601
+ const data = mgr.getData();
602
+ if (!data) {
603
+ console.error(t('flow.retry.errorNoData', lang));
604
+ process.exit(1);
605
+ }
606
+ // 解析 opId 查找 op
607
+ const dotIdx = options.op.lastIndexOf('.');
608
+ if (dotIdx === -1) {
609
+ console.error(t('common.invalidOpId', lang));
610
+ process.exit(1);
611
+ }
612
+ const stageId = options.op.substring(0, dotIdx);
613
+ const opLocalId = options.op.substring(dotIdx + 1);
614
+ const stage = data.stages[stageId];
615
+ if (!stage) {
616
+ console.error(t('flow.retry.errorStageNotFoundTmpl', lang, { stage: stageId }));
617
+ process.exit(1);
618
+ }
619
+ const op = stage.ops[opLocalId];
620
+ if (!op) {
621
+ console.error(t('flow.retry.errorOpNotFoundTmpl', lang, { op: options.op }));
622
+ process.exit(1);
623
+ }
624
+ const retryCount = mgr.getRetryCount(options.op);
625
+ console.log(t('common.op', lang) + `: ${options.op}`);
626
+ console.log(t('common.status', lang) + `: ${op.state}`);
627
+ console.log(t('flow.retry.attemptCount', lang) + `: ${retryCount} / ${op.max_attempts}`);
628
+ if (retryCount >= op.max_attempts) {
629
+ console.log(t('flow.retry.exhausted', lang));
630
+ }
631
+ });
632
+ // flow repair [--dry-run] [--backup]
633
+ flow
634
+ .command('repair')
635
+ .description('自动检测并修复 flow.json 中的常见问题')
636
+ .option('--dry-run', '仅检测不修复')
637
+ .option('--backup', '修复前备份为 .bak')
638
+ .action((options) => {
639
+ const lang = getCliLang(process.cwd());
640
+ const mgr = new FlowManager(process.cwd());
641
+ // --backup 选项:修复前手动备份
642
+ if (options.backup) {
643
+ const fp = resolve(process.cwd(), '.openfeel', 'flow.json');
644
+ if (existsSync(fp)) {
645
+ copyFileSync(fp, fp + '.bak');
646
+ console.log(t('flow.repair.backupOk', lang));
647
+ }
648
+ else {
649
+ console.log(t('flow.repair.noBackup', lang));
650
+ }
651
+ }
652
+ const result = mgr.repair(options.dryRun ?? false);
653
+ if (options.dryRun) {
654
+ console.log(t('flow.repair.dryRunTitle', lang));
655
+ }
656
+ if (result.recovered) {
657
+ console.log(t('flow.repair.recovered', lang));
658
+ }
659
+ for (const change of result.changes) {
660
+ console.log(` - ${change}`);
661
+ }
662
+ if (result.fixed) {
663
+ if (options.dryRun) {
664
+ console.log('\n' + t('flow.repair.dryRunHint', lang));
665
+ }
666
+ else {
667
+ console.log('\n' + t('flow.repair.fixDone', lang));
668
+ }
669
+ }
670
+ else {
671
+ if (options.dryRun && result.changes.length > 0) {
672
+ console.log('\n' + t('flow.repair.dryRunHint', lang));
673
+ }
674
+ else if (!result.recovered && result.changes.length === 0) {
675
+ console.log('\n' + t('flow.repair.noFix', lang));
676
+ }
677
+ else {
678
+ console.error('\n' + t('flow.repair.fixFailed', lang));
679
+ process.exit(1);
680
+ }
681
+ }
682
+ // 检测旧格式并建议迁移
683
+ if (mgr.isLoaded() && mgr.needsMigration()) {
684
+ console.log('\n' + t('flow.repair.migrationHint', lang));
685
+ console.log(' openfeel flow migrate');
686
+ console.log(' ' + t('flow.repair.migrationPreview', lang));
687
+ }
688
+ });
689
+ // flow migrate [--dry-run] [--no-backup]
690
+ flow
691
+ .command('migrate')
692
+ .description('将旧版 flow.json(v4.0 全局 phase)迁移到新版格式(v4.1 阶段级 phase)')
693
+ .option('--dry-run', '仅检测预览,不实际写入文件')
694
+ .option('--no-backup', '跳过 .bak 文件生成(默认生成 flow.json.v4.0.bak)')
695
+ .action((options) => {
696
+ const lang = getCliLang(process.cwd());
697
+ const mgr = createManager();
698
+ if (!mgr.isLoaded()) {
699
+ console.error(t('common.errorNoInit', lang));
700
+ process.exit(1);
701
+ }
702
+ // 检测是否已为新格式
703
+ if (!mgr.needsMigration()) {
704
+ console.log(t('flow.migrate.alreadyNew', lang));
705
+ return;
706
+ }
707
+ // Commander 的 --no-backup 生成 backup=false(而非 noBackup=true)
708
+ const noBackup = options.backup === false;
709
+ // --dry-run 输出迁移预览
710
+ if (options.dryRun) {
711
+ console.log(t('flow.migrate.dryRunTitle', lang) + '\n');
712
+ const result = mgr.migrate(true, noBackup);
713
+ for (const change of result.changes) {
714
+ console.log(` ${change}`);
715
+ }
716
+ if (result.migrated) {
717
+ console.log('\n' + t('flow.migrate.dryRunNote', lang));
718
+ }
719
+ return;
720
+ }
721
+ // 执行迁移
722
+ const result = mgr.migrate(false, noBackup);
723
+ if (!result.migrated) {
724
+ // 非迁移失败场景(如已是新版格式)已在上方 return
725
+ for (const change of result.changes) {
726
+ console.log(` ${change}`);
727
+ }
728
+ if (result.failed) {
729
+ console.error('\n' + t('flow.migrate.failed', lang));
730
+ process.exit(1);
731
+ }
732
+ return;
733
+ }
734
+ // 持久化
735
+ mgr.save();
736
+ console.log(t('flow.migrate.complete', lang) + ':\n');
737
+ for (const change of result.changes) {
738
+ console.log(` ${change}`);
739
+ }
740
+ console.log('\n' + t('flow.migrate.done', lang));
741
+ });
742
+ // flow checkpoint — Checkpoint 快照管理子命令组
743
+ const checkpointCmd = flow
744
+ .command('checkpoint')
745
+ .description('Checkpoint 快照管理(phase 推进时自动保存 flow.json 快照)');
746
+ // flow checkpoint list [stage]
747
+ checkpointCmd
748
+ .command('list')
749
+ .description('列出所有(或指定阶段的)Checkpoint 快照')
750
+ .argument('[stage]', '阶段 ID(可选),如 v5.3-stage-01')
751
+ .action((stage) => {
752
+ const lang = getCliLang(process.cwd());
753
+ const mgr = createManager();
754
+ const snapshots = mgr.listCheckpoints(stage);
755
+ if (snapshots.length === 0) {
756
+ console.log(stage
757
+ ? t('flow.checkpoint.noSnapshotsStageTmpl', lang, { stage })
758
+ : t('flow.checkpoint.noSnapshots', lang));
759
+ return;
760
+ }
761
+ console.log(t('flow.checkpoint.listTitle', lang) + (stage ? ` [${stage}]` : '') + ':');
762
+ for (const s of snapshots) {
763
+ console.log(` ${s}`);
764
+ }
765
+ console.log(t('flow.checkpoint.listCountTmpl', lang, { n: String(snapshots.length) }));
766
+ });
767
+ // flow checkpoint restore <checkpoint-file> [--force]
768
+ checkpointCmd
769
+ .command('restore')
770
+ .description('从 Checkpoint 快照恢复 flow.json(覆盖当前文件,需 --force 确认)')
771
+ .argument('<checkpoint-file>', '快照文件名(如 v5.3-stage-01-20260807T162300-exec_running.json)')
772
+ .option('--force', '确认恢复操作(覆盖当前 flow.json)')
773
+ .action((file, options) => {
774
+ const lang = getCliLang(process.cwd());
775
+ const mgr = createManager();
776
+ // 安全确认:恢复会覆盖当前 flow.json,必须显式 --force
777
+ if (!options.force) {
778
+ console.error(t('flow.checkpoint.restoreNeedForce', lang));
779
+ process.exit(1);
780
+ }
781
+ const ok = mgr.restoreCheckpoint(file);
782
+ if (ok) {
783
+ console.log(t('flow.checkpoint.restoreOkTmpl', lang, { file }));
784
+ }
785
+ else {
786
+ console.error(t('flow.checkpoint.restoreFailTmpl', lang, { file }));
787
+ process.exit(1);
788
+ }
789
+ });
790
+ // flow health [--quick]
791
+ flow
792
+ .command('health')
793
+ .description('全面健康检查 flow.json / 跨文件一致性 / 僵尸状态 / config.yaml 等')
794
+ .option('--quick', '仅检查关键项(phase/current 合法性,跳过其他检查)')
795
+ .action((options) => {
796
+ const lang = getCliLang(process.cwd());
797
+ const mgr = createManager();
798
+ console.log(t('flow.health.title', lang) + '\n');
799
+ const result = mgr.healthCheck(options.quick ?? false);
800
+ // 图标映射
801
+ const icon = (status) => {
802
+ if (status === 'pass')
803
+ return '✅';
804
+ if (status === 'warn')
805
+ return '⚠️ ';
806
+ return '❌';
807
+ };
808
+ for (const item of result.items) {
809
+ console.log(`${icon(item.status)} ${item.section}: ${item.message}`);
810
+ }
811
+ console.log('');
812
+ if (result.ok) {
813
+ console.log(t('flow.health.pass', lang));
814
+ }
815
+ else {
816
+ console.log(t('flow.health.hasFailures', lang));
817
+ }
818
+ if (options.quick) {
819
+ console.log(t('flow.health.quickMode', lang));
820
+ }
821
+ if (!result.ok) {
822
+ process.exit(1);
823
+ }
824
+ });
825
+ // flow recover — 跨会话上下文恢复
826
+ flow
827
+ .command('recover')
828
+ .description('跨会话上下文恢复:输出流水线状态、阻塞原因和待处理任务')
829
+ .action(() => {
830
+ const lang = getCliLang(process.cwd());
831
+ const mgr = createManager();
832
+ if (!mgr.isLoaded()) {
833
+ console.log(t('common.noInit', lang));
834
+ return;
835
+ }
836
+ const recovery = mgr.recoverContext(lang);
837
+ console.log('');
838
+ console.log(t('flow.recover.title', lang));
839
+ console.log('');
840
+ console.log(t('flow.recover.globalStatus', lang) + `: ${mgr.getPhase() ?? t('common.unknown', lang)}`);
841
+ console.log(t('flow.recover.phase', lang) + `: ${recovery.phase ?? t('common.unknown', lang)}`);
842
+ console.log(t('flow.recover.currentOp', lang) + `: ${recovery.currentOp ?? t('common.none', lang)}`);
843
+ console.log(t('flow.recover.stageStatus', lang) + `: ${recovery.stageStatus}`);
844
+ if (recovery.blockedBy) {
845
+ console.log(t('common.blockedBy', lang) + `: ${recovery.blockedBy}`);
846
+ }
847
+ if (recovery.pendingTasks.length > 0) {
848
+ console.log('');
849
+ console.log(t('flow.recover.pendingTasksTmpl', lang, { n: String(recovery.pendingTasks.length) }) + ':');
850
+ for (let i = 0; i < recovery.pendingTasks.length; i++) {
851
+ console.log(` ${i + 1}. ${recovery.pendingTasks[i]}`);
852
+ }
853
+ }
854
+ else {
855
+ console.log('');
856
+ console.log(t('flow.recover.noTasks', lang));
857
+ }
858
+ // 阶段耗时一览
859
+ const allStats = mgr.getAllStageStats();
860
+ if (Object.keys(allStats).length > 0) {
861
+ console.log('');
862
+ console.log(t('flow.recover.stageDuration', lang) + ':');
863
+ for (const [stageId, s] of Object.entries(allStats)) {
864
+ const duration = formatDuration(s.duration_ms);
865
+ const status = s.end_time ? t('common.completed', lang) : t('common.inProgress', lang);
866
+ console.log(` ${stageId}: ${duration} (${status})`);
867
+ }
868
+ }
869
+ console.log('');
870
+ console.log('═══════════════════════════');
871
+ });
872
+ // flow wizard — 交互式推进流水线(支持多 stage 选择,基于 stage phase)
873
+ flow
874
+ .command('wizard')
875
+ .description('交互式流水线向导,逐步推进阶段')
876
+ .action(async () => {
877
+ const lang = getCliLang(process.cwd());
878
+ const mgr = createManager();
879
+ if (!mgr.isLoaded()) {
880
+ console.log(t('common.noInit', lang));
881
+ return;
882
+ }
883
+ // 阶段标签映射(从 pipelineConfig.phases 动态生成)
884
+ const phaseLabels = mgr.getPhaseLabels();
885
+ try {
886
+ const { select } = await import('@inquirer/prompts');
887
+ // 交互主循环
888
+ for (;;) {
889
+ // 刷新当前状态
890
+ mgr.load();
891
+ const metaPhase = mgr.getPhase(); // MetaPhase: active/paused/done
892
+ const current = mgr.getCurrent();
893
+ const summary = mgr.getSummary();
894
+ const data = mgr.getData();
895
+ // 终态判断:pipeline.phase === 'done' 或所有 stage phase 均为 'done'
896
+ const allStagesDone = data && Object.keys(data.stages).length > 0
897
+ ? Object.values(data.stages).every(s => s.phase === 'done')
898
+ : false;
899
+ if (metaPhase === 'done' || allStagesDone) {
900
+ console.log(t('flow.wizard.done', lang));
901
+ return;
902
+ }
903
+ // 确定当前推进的 stage
904
+ const stages = data ? Object.keys(data.stages) : [];
905
+ let currentStage = current?.stage;
906
+ if (stages.length === 0) {
907
+ console.log(t('flow.wizard.noStages', lang));
908
+ return;
909
+ }
910
+ if (stages.length > 1) {
911
+ // 多个 stage 时让用户选择
912
+ currentStage = await select({
913
+ message: t('flow.wizard.selectStage', lang),
914
+ choices: stages.map(s => ({
915
+ name: s + (s === current?.stage ? ' ' + t('flow.wizard.currentLabel', lang) : ''),
916
+ value: s,
917
+ })),
918
+ });
919
+ }
920
+ else {
921
+ currentStage = stages[0];
922
+ }
923
+ if (!currentStage || !data?.stages[currentStage]) {
924
+ console.log(t('flow.wizard.unavailable', lang));
925
+ return;
926
+ }
927
+ const stagePhase = data.stages[currentStage].phase;
928
+ // 显示当前状态
929
+ console.log('\n' + t('flow.wizard.statusHeader', lang));
930
+ console.log(t('flow.status.globalStatus', lang) + `: ${metaPhase}`);
931
+ console.log(t('common.stage', lang) + `: ${currentStage}`);
932
+ console.log(t('flow.wizard.stagePhase', lang) + `: ${stagePhase} (${phaseLabels[stagePhase] ?? t('common.unknown', lang)})`);
933
+ if (current && current.stage === currentStage) {
934
+ console.log(t('flow.status.currentOp', lang) + `: ${current.stage}.${current.op}`);
935
+ }
936
+ console.log(t('flow.wizard.retryCount', lang) + `: ${summary.retryCount}`);
937
+ console.log(t('flow.wizard.pendingReviews', lang) + `: ${summary.reviewItemsOpen}`);
938
+ console.log('═══════════════════\n');
939
+ // 获取可达的下一步阶段(基于选定 stage 的 phase)
940
+ const availablePhases = mgr.getAvailablePhases(currentStage);
941
+ if (availablePhases.length === 0) {
942
+ console.log(t('flow.wizard.noNext', lang));
943
+ return;
944
+ }
945
+ const choices = availablePhases.map((p, idx) => ({
946
+ name: `${idx + 1}. ${p} → ${phaseLabels[p] ?? p}`,
947
+ value: p,
948
+ }));
949
+ // 添加退出选项
950
+ choices.push({
951
+ name: t('flow.wizard.exitOption', lang),
952
+ value: '__exit__',
953
+ });
954
+ const targetPhase = await select({
955
+ message: t('flow.wizard.selectAction', lang),
956
+ choices,
957
+ pageSize: 10,
958
+ });
959
+ // 用户选择退出
960
+ if (targetPhase === '__exit__') {
961
+ console.log(t('flow.wizard.exited', lang));
962
+ return;
963
+ }
964
+ // 预览变更
965
+ const prevLabel = phaseLabels[stagePhase] ?? stagePhase;
966
+ const nextLabel = phaseLabels[targetPhase] ?? targetPhase;
967
+ console.log(t('flow.wizard.previewTmpl', lang, { stage: currentStage, from: stagePhase, fromLabel: prevLabel, to: targetPhase, toLabel: nextLabel }));
968
+ const confirmed = await select({
969
+ message: t('flow.wizard.confirmTitle', lang),
970
+ choices: [
971
+ { name: t('flow.wizard.confirm', lang), value: 'yes' },
972
+ { name: t('common.cancel', lang), value: 'no' },
973
+ ],
974
+ });
975
+ if (confirmed !== 'yes') {
976
+ console.log(t('common.cancelled', lang));
977
+ continue;
978
+ }
979
+ // 执行推进(使用 advanceStagePhase,标记为 CLI 触发)
980
+ const archived = mgr.advanceStagePhase(currentStage, targetPhase, 'cli');
981
+ mgr.save();
982
+ // 归档 commit 必须在 flow.json save 之后执行,确保 commit 包含本次 phase 变更
983
+ if (archived) {
984
+ mgr.autoCommitOnDone(currentStage);
985
+ }
986
+ console.log(t('flow.wizard.advancedTmpl', lang, { stage: currentStage, from: stagePhase, to: targetPhase }));
987
+ // 到达终态时退出循环
988
+ if (targetPhase === 'done') {
989
+ console.log(t('flow.wizard.done', lang));
990
+ return;
991
+ }
992
+ }
993
+ }
994
+ catch (err) {
995
+ // 非 TTY 环境或用户中断等异常
996
+ if (err instanceof Error) {
997
+ console.error(t('common.errorTmpl', lang, { msg: err.message }));
998
+ }
999
+ }
1000
+ });
1001
+ }
1002
+ /** 创建 FlowManager 实例(使用当前工作目录) */
1003
+ function createManager() {
1004
+ return new FlowManager(process.cwd());
1005
+ }
1006
+ /** 格式化毫秒时长为人类可读形式 */
1007
+ function formatDuration(ms) {
1008
+ if (ms <= 0) {
1009
+ return '0ms';
1010
+ }
1011
+ if (ms < 1000) {
1012
+ return `${ms}ms`;
1013
+ }
1014
+ if (ms < 60000) {
1015
+ return `${(ms / 1000).toFixed(1)}s`;
1016
+ }
1017
+ const minutes = Math.floor(ms / 60000);
1018
+ const seconds = Math.round((ms % 60000) / 1000);
1019
+ if (seconds === 0) {
1020
+ return `${minutes}m`;
1021
+ }
1022
+ return `${minutes}m ${seconds}s`;
1023
+ }
1024
+ //# sourceMappingURL=flow.js.map