@localsummer/incspec 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -9
- package/commands/analyze.mjs +26 -10
- package/commands/apply.mjs +78 -33
- package/commands/archive.mjs +25 -3
- package/commands/collect-req.mjs +8 -0
- package/commands/help.mjs +9 -3
- package/commands/merge.mjs +64 -33
- package/commands/reset.mjs +166 -0
- package/commands/status.mjs +30 -7
- package/index.mjs +8 -1
- package/lib/agents.mjs +1 -1
- package/lib/config.mjs +7 -7
- package/lib/cursor.mjs +20 -5
- package/lib/workflow.mjs +115 -22
- package/package.json +1 -1
- package/templates/AGENTS.md +101 -502
- package/templates/INCSPEC_BLOCK.md +1 -1
- package/templates/WORKFLOW.md +1 -0
- package/templates/cursor-commands/analyze-codeflow.md +12 -1
- package/templates/cursor-commands/apply-increment-code.md +129 -1
- package/templates/cursor-commands/merge-to-baseline.md +87 -1
- package/templates/cursor-commands/structured-requirements-collection.md +6 -0
- package/templates/inc-spec-skill/SKILL.md +60 -5
- package/templates/inc-spec-skill/references/analyze-codeflow.md +12 -1
- package/templates/inc-spec-skill/references/apply-increment-code.md +129 -1
- package/templates/inc-spec-skill/references/merge-to-baseline.md +87 -1
- package/templates/inc-spec-skill/references/structured-requirements-collection.md +6 -0
- package/templates/inc-spec-skill/.security-scan-passed +0 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reset command - Reset current workflow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import {
|
|
8
|
+
ensureInitialized,
|
|
9
|
+
INCSPEC_DIR,
|
|
10
|
+
DIRS,
|
|
11
|
+
} from '../lib/config.mjs';
|
|
12
|
+
import {
|
|
13
|
+
readWorkflow,
|
|
14
|
+
archiveWorkflow,
|
|
15
|
+
STATUS,
|
|
16
|
+
} from '../lib/workflow.mjs';
|
|
17
|
+
import { archiveSpec } from '../lib/spec.mjs';
|
|
18
|
+
import {
|
|
19
|
+
colors,
|
|
20
|
+
colorize,
|
|
21
|
+
print,
|
|
22
|
+
printSuccess,
|
|
23
|
+
printInfo,
|
|
24
|
+
printWarning,
|
|
25
|
+
} from '../lib/terminal.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract module name from workflow name
|
|
29
|
+
* e.g., "analyze-incspec-cli" -> "incspec-cli"
|
|
30
|
+
* @param {string} workflowName
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function extractModuleName(workflowName) {
|
|
34
|
+
if (!workflowName) {
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Remove common prefixes like "analyze-"
|
|
39
|
+
const prefixes = ['analyze-', 'feature-', 'fix-', 'refactor-'];
|
|
40
|
+
let moduleName = workflowName;
|
|
41
|
+
|
|
42
|
+
for (const prefix of prefixes) {
|
|
43
|
+
if (moduleName.startsWith(prefix)) {
|
|
44
|
+
moduleName = moduleName.slice(prefix.length);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return moduleName || workflowName;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get output directory for a step index
|
|
54
|
+
* @param {number} stepIndex - 0-based step index
|
|
55
|
+
* @returns {string|null}
|
|
56
|
+
*/
|
|
57
|
+
function getOutputDirForStepIndex(stepIndex) {
|
|
58
|
+
// Step 1 (index 0) and Step 6 (index 5) -> baselines
|
|
59
|
+
if (stepIndex === 0 || stepIndex === 5) {
|
|
60
|
+
return DIRS.baselines;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Step 2-3 (index 1-2) -> requirements
|
|
64
|
+
if (stepIndex === 1 || stepIndex === 2) {
|
|
65
|
+
return DIRS.requirements;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 4 (index 3) -> increments
|
|
69
|
+
if (stepIndex === 3) {
|
|
70
|
+
return DIRS.increments;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collect output files from workflow steps
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
* @param {Object} workflow
|
|
80
|
+
* @returns {Array<{path: string, name: string}>}
|
|
81
|
+
*/
|
|
82
|
+
function collectWorkflowOutputs(projectRoot, workflow) {
|
|
83
|
+
const outputs = [];
|
|
84
|
+
|
|
85
|
+
if (!workflow?.steps) {
|
|
86
|
+
return outputs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
workflow.steps.forEach((step, index) => {
|
|
90
|
+
// Only collect completed steps with output
|
|
91
|
+
if (step.status !== STATUS.COMPLETED || !step.output || step.output === '-') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dir = getOutputDirForStepIndex(index);
|
|
96
|
+
if (!dir) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const outputPath = path.join(projectRoot, INCSPEC_DIR, dir, step.output);
|
|
101
|
+
if (fs.existsSync(outputPath)) {
|
|
102
|
+
outputs.push({
|
|
103
|
+
path: outputPath,
|
|
104
|
+
name: step.output,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return outputs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute reset command
|
|
114
|
+
* @param {Object} ctx - Command context
|
|
115
|
+
*/
|
|
116
|
+
export async function resetCommand(ctx) {
|
|
117
|
+
const { cwd } = ctx;
|
|
118
|
+
|
|
119
|
+
// Ensure initialized
|
|
120
|
+
const projectRoot = ensureInitialized(cwd);
|
|
121
|
+
|
|
122
|
+
// Read current workflow
|
|
123
|
+
const workflow = readWorkflow(projectRoot);
|
|
124
|
+
|
|
125
|
+
if (!workflow?.currentWorkflow) {
|
|
126
|
+
print('');
|
|
127
|
+
printInfo('当前无活跃工作流,无需重置');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const workflowName = workflow.currentWorkflow;
|
|
132
|
+
const moduleName = extractModuleName(workflowName);
|
|
133
|
+
|
|
134
|
+
print('');
|
|
135
|
+
print(colorize(' incspec 工作流重置', colors.bold, colors.cyan));
|
|
136
|
+
print(colorize(' ─────────────────', colors.dim));
|
|
137
|
+
print('');
|
|
138
|
+
print(colorize(`工作流: ${workflowName}`, colors.dim));
|
|
139
|
+
print('');
|
|
140
|
+
|
|
141
|
+
// Collect output files
|
|
142
|
+
const outputs = collectWorkflowOutputs(projectRoot, workflow);
|
|
143
|
+
|
|
144
|
+
// Archive output files
|
|
145
|
+
if (outputs.length > 0) {
|
|
146
|
+
print(colorize('归档产出文件:', colors.dim));
|
|
147
|
+
|
|
148
|
+
for (const output of outputs) {
|
|
149
|
+
try {
|
|
150
|
+
const archivePath = archiveSpec(projectRoot, output.path, true, moduleName);
|
|
151
|
+
print(colorize(` - ${output.name}`, colors.dim));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
printWarning(`归档失败: ${output.name} - ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
print('');
|
|
158
|
+
printSuccess(`已归档 ${outputs.length} 个产出文件到 archives/${new Date().toISOString().slice(0, 7)}/${moduleName}/`);
|
|
159
|
+
} else {
|
|
160
|
+
printInfo('无产出文件需要归档');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reset workflow state
|
|
164
|
+
archiveWorkflow(projectRoot);
|
|
165
|
+
printSuccess('工作流已重置');
|
|
166
|
+
}
|
package/commands/status.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
readWorkflow,
|
|
11
11
|
STEPS,
|
|
12
12
|
STATUS,
|
|
13
|
+
MODE,
|
|
14
|
+
QUICK_MODE_SKIPPED,
|
|
15
|
+
isQuickMode,
|
|
13
16
|
} from '../lib/workflow.mjs';
|
|
14
17
|
import {
|
|
15
18
|
colors,
|
|
@@ -49,7 +52,11 @@ export async function statusCommand(ctx) {
|
|
|
49
52
|
|
|
50
53
|
// Current workflow
|
|
51
54
|
if (workflow?.currentWorkflow) {
|
|
55
|
+
const quickMode = isQuickMode(workflow);
|
|
56
|
+
const modeLabel = quickMode ? '快速模式 (5步)' : '完整模式 (7步)';
|
|
57
|
+
|
|
52
58
|
print(colorize(`当前工作流: `, colors.bold) + colorize(workflow.currentWorkflow, colors.cyan));
|
|
59
|
+
print(colorize(`工作流模式: ${modeLabel}`, colors.dim));
|
|
53
60
|
print(colorize(`开始时间: ${workflow.startTime || '-'}`, colors.dim));
|
|
54
61
|
print(colorize(`最后更新: ${workflow.lastUpdate || '-'}`, colors.dim));
|
|
55
62
|
print('');
|
|
@@ -60,8 +67,14 @@ export async function statusCommand(ctx) {
|
|
|
60
67
|
|
|
61
68
|
STEPS.forEach((step, index) => {
|
|
62
69
|
const stepData = workflow.steps[index] || {};
|
|
63
|
-
|
|
70
|
+
let status = stepData.status || STATUS.PENDING;
|
|
64
71
|
const isCurrent = workflow.currentStep === step.id;
|
|
72
|
+
const isSkipped = quickMode && QUICK_MODE_SKIPPED.includes(step.id);
|
|
73
|
+
|
|
74
|
+
// Quick mode: mark skipped steps
|
|
75
|
+
if (isSkipped && status !== STATUS.SKIPPED) {
|
|
76
|
+
status = STATUS.SKIPPED;
|
|
77
|
+
}
|
|
65
78
|
|
|
66
79
|
// Determine display status
|
|
67
80
|
let displayStatus = status;
|
|
@@ -69,7 +82,12 @@ export async function statusCommand(ctx) {
|
|
|
69
82
|
displayStatus = STATUS.IN_PROGRESS;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
|
-
|
|
85
|
+
// Use dimmed style for skipped steps
|
|
86
|
+
if (status === STATUS.SKIPPED) {
|
|
87
|
+
print(colorize(` [-] ${step.id}. ${step.label}`, colors.dim) + colorize(' (已跳过)', colors.dim));
|
|
88
|
+
} else {
|
|
89
|
+
printStep(step.id, step.label, displayStatus);
|
|
90
|
+
}
|
|
73
91
|
|
|
74
92
|
// Show output file if completed
|
|
75
93
|
if (stepData.output && status === STATUS.COMPLETED) {
|
|
@@ -80,9 +98,14 @@ export async function statusCommand(ctx) {
|
|
|
80
98
|
print('');
|
|
81
99
|
|
|
82
100
|
// Next step hint
|
|
83
|
-
const nextStepIndex = workflow.steps.findIndex(
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
const nextStepIndex = workflow.steps.findIndex((step, index) => {
|
|
102
|
+
const stepNumber = index + 1;
|
|
103
|
+
// Skip excluded steps in quick mode
|
|
104
|
+
if (quickMode && QUICK_MODE_SKIPPED.includes(stepNumber)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return (step?.status || STATUS.PENDING) !== STATUS.COMPLETED;
|
|
108
|
+
});
|
|
86
109
|
if (nextStepIndex >= 0) {
|
|
87
110
|
const nextStep = STEPS[nextStepIndex];
|
|
88
111
|
print(colorize('下一步:', colors.bold));
|
|
@@ -107,8 +130,8 @@ export async function statusCommand(ctx) {
|
|
|
107
130
|
|
|
108
131
|
const recentHistory = workflow.history.slice(0, 5);
|
|
109
132
|
recentHistory.forEach(item => {
|
|
110
|
-
const statusIcon = item.status === 'completed'
|
|
111
|
-
? colorize('✓', colors.green)
|
|
133
|
+
const statusIcon = item.status === 'completed'
|
|
134
|
+
? colorize('✓', colors.green)
|
|
112
135
|
: colorize('○', colors.dim);
|
|
113
136
|
print(` ${statusIcon} ${item.name} (${item.startTime || '-'})`);
|
|
114
137
|
});
|
package/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { mergeCommand } from './commands/merge.mjs';
|
|
|
17
17
|
import { listCommand } from './commands/list.mjs';
|
|
18
18
|
import { validateCommand } from './commands/validate.mjs';
|
|
19
19
|
import { archiveCommand } from './commands/archive.mjs';
|
|
20
|
+
import { resetCommand } from './commands/reset.mjs';
|
|
20
21
|
import { syncCommand } from './commands/sync.mjs';
|
|
21
22
|
import { helpCommand } from './commands/help.mjs';
|
|
22
23
|
import { colors, colorize } from './lib/terminal.mjs';
|
|
@@ -156,7 +157,7 @@ async function main() {
|
|
|
156
157
|
await statusCommand(commandContext);
|
|
157
158
|
break;
|
|
158
159
|
|
|
159
|
-
// Workflow commands (Step 1-6)
|
|
160
|
+
// Workflow commands (Step 1-6, Step 7 archive is separate)
|
|
160
161
|
case 'analyze':
|
|
161
162
|
case 'a':
|
|
162
163
|
await analyzeCommand(commandContext);
|
|
@@ -203,6 +204,12 @@ async function main() {
|
|
|
203
204
|
await archiveCommand(commandContext);
|
|
204
205
|
break;
|
|
205
206
|
|
|
207
|
+
// Reset workflow
|
|
208
|
+
case 'reset':
|
|
209
|
+
case 'rs':
|
|
210
|
+
await resetCommand(commandContext);
|
|
211
|
+
break;
|
|
212
|
+
|
|
206
213
|
// Sync integrations
|
|
207
214
|
case 'sync':
|
|
208
215
|
case 's':
|
package/lib/agents.mjs
CHANGED
package/lib/config.mjs
CHANGED
|
@@ -200,23 +200,23 @@ export function writeProjectConfig(projectRoot, config) {
|
|
|
200
200
|
*/
|
|
201
201
|
function generateProjectContent(config) {
|
|
202
202
|
const templatePath = path.join(getTemplatesDir(), 'project.md');
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
if (fs.existsSync(templatePath)) {
|
|
205
205
|
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
// Replace variables
|
|
208
208
|
content = content.replace(/\{\{name\}\}/g, config.name || '');
|
|
209
209
|
content = content.replace(/\{\{version\}\}/g, config.version || '1.0.0');
|
|
210
210
|
content = content.replace(/\{\{source_dir\}\}/g, config.source_dir || 'src');
|
|
211
211
|
content = content.replace(/\{\{created_at\}\}/g, config.created_at || formatLocalDate(new Date()));
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
// Handle tech_stack array
|
|
214
214
|
const techStackLines = (config.tech_stack || []).map(item => ` - ${item}`).join('\n');
|
|
215
215
|
content = content.replace(/\{\{tech_stack\}\}/g, techStackLines);
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
return content;
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
// Fallback to hardcoded content if template not found
|
|
221
221
|
return generateFallbackProjectContent(config);
|
|
222
222
|
}
|
|
@@ -321,7 +321,7 @@ function writeAgentsFile(projectRoot) {
|
|
|
321
321
|
## 快速开始
|
|
322
322
|
|
|
323
323
|
1. 运行 \`incspec status\` 查看当前工作流状态
|
|
324
|
-
2. 按顺序执行
|
|
324
|
+
2. 按顺序执行7步工作流: analyze → collect-req → collect-dep → design → apply → merge → archive
|
|
325
325
|
3. 使用 \`incspec help\` 获取更多帮助
|
|
326
326
|
`;
|
|
327
327
|
fs.writeFileSync(agentsPath, fallbackContent, 'utf-8');
|
|
@@ -335,7 +335,7 @@ function writeAgentsFile(projectRoot) {
|
|
|
335
335
|
*/
|
|
336
336
|
export function ensureInitialized(cwd) {
|
|
337
337
|
const projectRoot = findProjectRoot(cwd);
|
|
338
|
-
|
|
338
|
+
|
|
339
339
|
if (!projectRoot || !isInitialized(projectRoot)) {
|
|
340
340
|
throw new Error(
|
|
341
341
|
`incspec 未初始化。请先运行 'incspec init' 初始化项目。`
|
package/lib/cursor.mjs
CHANGED
|
@@ -68,6 +68,13 @@ const COMMAND_MAP = [
|
|
|
68
68
|
label: '步骤6: 合并到基线',
|
|
69
69
|
description: '[incspec] 将增量融合为新的代码流基线快照',
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
source: 'inc-archive.md',
|
|
73
|
+
target: 'inc-archive.md',
|
|
74
|
+
step: 7,
|
|
75
|
+
label: '步骤7: 归档工作流产出',
|
|
76
|
+
description: '[incspec] 归档工作流产出文件到历史记录目录',
|
|
77
|
+
},
|
|
71
78
|
];
|
|
72
79
|
|
|
73
80
|
/**
|
|
@@ -174,16 +181,24 @@ description: [incspec] 显示帮助信息
|
|
|
174
181
|
|
|
175
182
|
## 工作流步骤
|
|
176
183
|
|
|
184
|
+
**完整模式 (7步):**
|
|
177
185
|
1. \`/incspec/inc-analyze\` - 分析代码流程,生成基线快照
|
|
178
186
|
2. \`/incspec/inc-collect-req\` - 收集结构化需求
|
|
179
187
|
3. \`/incspec/inc-collect-dep\` - 采集UI依赖
|
|
180
188
|
4. \`/incspec/inc-design\` - 生成增量设计蓝图
|
|
181
189
|
5. \`/incspec/inc-apply\` - 应用代码变更
|
|
182
190
|
6. \`/incspec/inc-merge\` - 合并到新基线
|
|
191
|
+
7. \`/incspec/inc-archive\` - 归档工作流产出
|
|
192
|
+
|
|
193
|
+
**快速模式 (5步):**
|
|
194
|
+
1. \`/incspec/inc-analyze --quick\` - 分析代码流程 (快速模式)
|
|
195
|
+
2. \`/incspec/inc-collect-req\` - 收集结构化需求
|
|
196
|
+
5. \`/incspec/inc-apply\` - 应用代码变更
|
|
197
|
+
6. \`/incspec/inc-merge\` - 合并到新基线
|
|
198
|
+
7. \`/incspec/inc-archive\` - 归档工作流产出
|
|
183
199
|
|
|
184
200
|
## 辅助命令
|
|
185
201
|
|
|
186
|
-
- \`/incspec/inc-archive\` - 归档规范文件到 archives 目录
|
|
187
202
|
- \`/incspec/inc-status\` - 查看当前工作流状态
|
|
188
203
|
- \`/incspec/inc-help\` - 显示帮助信息
|
|
189
204
|
|
|
@@ -194,7 +209,7 @@ incspec init # 初始化项目
|
|
|
194
209
|
incspec status # 查看工作流状态
|
|
195
210
|
incspec list # 列出规范文件
|
|
196
211
|
incspec validate # 验证规范完整性
|
|
197
|
-
incspec
|
|
212
|
+
incspec sync # 同步 IDE 命令
|
|
198
213
|
incspec help # 显示帮助
|
|
199
214
|
\`\`\`
|
|
200
215
|
|
|
@@ -255,14 +270,14 @@ incspec archive <file-path> --keep --yes
|
|
|
255
270
|
*/
|
|
256
271
|
export function syncToProject(projectRoot) {
|
|
257
272
|
const targetDir = path.join(projectRoot, CURSOR_COMMANDS_DIR);
|
|
258
|
-
|
|
273
|
+
|
|
259
274
|
// Create directory
|
|
260
275
|
if (!fs.existsSync(targetDir)) {
|
|
261
276
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
const commands = generateCursorCommands(projectRoot);
|
|
265
|
-
|
|
280
|
+
|
|
266
281
|
for (const cmd of commands) {
|
|
267
282
|
const filePath = path.join(targetDir, cmd.name);
|
|
268
283
|
fs.writeFileSync(filePath, cmd.content, 'utf-8');
|
|
@@ -282,7 +297,7 @@ export function syncToGlobal() {
|
|
|
282
297
|
}
|
|
283
298
|
|
|
284
299
|
const commands = generateCursorCommands();
|
|
285
|
-
|
|
300
|
+
|
|
286
301
|
for (const cmd of commands) {
|
|
287
302
|
const filePath = path.join(GLOBAL_CURSOR_DIR, cmd.name);
|
|
288
303
|
fs.writeFileSync(filePath, cmd.content, 'utf-8');
|
package/lib/workflow.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export const STEPS = [
|
|
|
18
18
|
{ id: 4, name: 'design-increment', label: '增量设计', command: 'design' },
|
|
19
19
|
{ id: 5, name: 'apply-code', label: '应用代码变更', command: 'apply' },
|
|
20
20
|
{ id: 6, name: 'merge-baseline', label: '合并到基线', command: 'merge' },
|
|
21
|
+
{ id: 7, name: 'archive-workflow', label: '归档工作流产出', command: 'archive' },
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
/** Step status */
|
|
@@ -25,8 +26,21 @@ export const STATUS = {
|
|
|
25
26
|
PENDING: 'pending',
|
|
26
27
|
IN_PROGRESS: 'in_progress',
|
|
27
28
|
COMPLETED: 'completed',
|
|
29
|
+
SKIPPED: 'skipped',
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
/** Workflow mode */
|
|
33
|
+
export const MODE = {
|
|
34
|
+
FULL: 'full',
|
|
35
|
+
QUICK: 'quick',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Quick mode valid steps (1-based) */
|
|
39
|
+
export const QUICK_MODE_STEPS = [1, 2, 5, 6, 7];
|
|
40
|
+
|
|
41
|
+
/** Quick mode skipped steps (1-based) */
|
|
42
|
+
export const QUICK_MODE_SKIPPED = [3, 4];
|
|
43
|
+
|
|
30
44
|
function normalizeOutputName(outputFile) {
|
|
31
45
|
if (!outputFile || typeof outputFile !== 'string') {
|
|
32
46
|
return outputFile;
|
|
@@ -117,6 +131,7 @@ export function parseWorkflow(content) {
|
|
|
117
131
|
const workflow = {
|
|
118
132
|
currentWorkflow: null,
|
|
119
133
|
currentStep: null,
|
|
134
|
+
mode: MODE.FULL,
|
|
120
135
|
startTime: null,
|
|
121
136
|
lastUpdate: null,
|
|
122
137
|
steps: [],
|
|
@@ -137,6 +152,13 @@ export function parseWorkflow(content) {
|
|
|
137
152
|
workflow.currentStep = parseInt(stepMatch[1], 10);
|
|
138
153
|
}
|
|
139
154
|
|
|
155
|
+
// Parse mode field
|
|
156
|
+
const modeMatch = content.match(/\*\*工作流模式\*\*:\s*(.+)/);
|
|
157
|
+
if (modeMatch) {
|
|
158
|
+
const modeValue = modeMatch[1].trim().toLowerCase();
|
|
159
|
+
workflow.mode = modeValue === 'quick' ? MODE.QUICK : MODE.FULL;
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
const startMatch = content.match(/\*\*开始时间\*\*:\s*(.+)/);
|
|
141
163
|
if (startMatch) {
|
|
142
164
|
workflow.startTime = startMatch[1].trim();
|
|
@@ -213,12 +235,14 @@ export function readWorkflow(projectRoot) {
|
|
|
213
235
|
*/
|
|
214
236
|
export function generateWorkflowContent(workflow) {
|
|
215
237
|
const now = formatLocalDateTime(new Date());
|
|
238
|
+
const mode = workflow.mode || MODE.FULL;
|
|
216
239
|
|
|
217
240
|
const lines = [
|
|
218
241
|
'# Workflow Status',
|
|
219
242
|
'',
|
|
220
243
|
`**当前工作流**: ${workflow.currentWorkflow || '-'}`,
|
|
221
244
|
`**当前步骤**: ${workflow.currentStep || '-'}`,
|
|
245
|
+
`**工作流模式**: ${mode}`,
|
|
222
246
|
`**开始时间**: ${workflow.startTime || '-'}`,
|
|
223
247
|
`**最后更新**: ${now}`,
|
|
224
248
|
'',
|
|
@@ -272,10 +296,11 @@ export function initWorkflow(projectRoot) {
|
|
|
272
296
|
const workflowPath = getWorkflowPath(projectRoot);
|
|
273
297
|
const content = generateInitialWorkflowContent();
|
|
274
298
|
fs.writeFileSync(workflowPath, content, 'utf-8');
|
|
275
|
-
|
|
299
|
+
|
|
276
300
|
return {
|
|
277
301
|
currentWorkflow: null,
|
|
278
302
|
currentStep: null,
|
|
303
|
+
mode: MODE.FULL,
|
|
279
304
|
startTime: null,
|
|
280
305
|
lastUpdate: null,
|
|
281
306
|
steps: STEPS.map(() => ({
|
|
@@ -294,13 +319,13 @@ export function initWorkflow(projectRoot) {
|
|
|
294
319
|
function generateInitialWorkflowContent() {
|
|
295
320
|
const templatePath = path.join(getTemplatesDir(), 'WORKFLOW.md');
|
|
296
321
|
const now = formatLocalDateTime(new Date());
|
|
297
|
-
|
|
322
|
+
|
|
298
323
|
if (fs.existsSync(templatePath)) {
|
|
299
324
|
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
300
325
|
content = content.replace(/\{\{last_update\}\}/g, now);
|
|
301
326
|
return content;
|
|
302
327
|
}
|
|
303
|
-
|
|
328
|
+
|
|
304
329
|
// Fallback to generated content
|
|
305
330
|
const workflow = {
|
|
306
331
|
currentWorkflow: null,
|
|
@@ -314,7 +339,7 @@ function generateInitialWorkflowContent() {
|
|
|
314
339
|
})),
|
|
315
340
|
history: [],
|
|
316
341
|
};
|
|
317
|
-
|
|
342
|
+
|
|
318
343
|
return generateWorkflowContent(workflow);
|
|
319
344
|
}
|
|
320
345
|
|
|
@@ -322,10 +347,12 @@ function generateInitialWorkflowContent() {
|
|
|
322
347
|
* Start a new workflow
|
|
323
348
|
* @param {string} projectRoot
|
|
324
349
|
* @param {string} workflowName
|
|
350
|
+
* @param {Object} options - { mode: 'full' | 'quick' }
|
|
325
351
|
* @returns {Object}
|
|
326
352
|
*/
|
|
327
|
-
export function startWorkflow(projectRoot, workflowName) {
|
|
353
|
+
export function startWorkflow(projectRoot, workflowName, options = {}) {
|
|
328
354
|
const now = formatLocalDateTime(new Date());
|
|
355
|
+
const mode = options.mode || MODE.FULL;
|
|
329
356
|
let workflow = readWorkflow(projectRoot);
|
|
330
357
|
|
|
331
358
|
if (!workflow) {
|
|
@@ -336,12 +363,12 @@ export function startWorkflow(projectRoot, workflowName) {
|
|
|
336
363
|
if (workflow.currentWorkflow) {
|
|
337
364
|
const progress = getWorkflowProgress(workflow);
|
|
338
365
|
const isComplete = progress.completed === progress.total;
|
|
339
|
-
|
|
366
|
+
|
|
340
367
|
// Format: "workflowName (completed/total)" for incomplete, just name for complete
|
|
341
|
-
const historyName = isComplete
|
|
342
|
-
? workflow.currentWorkflow
|
|
368
|
+
const historyName = isComplete
|
|
369
|
+
? workflow.currentWorkflow
|
|
343
370
|
: `${workflow.currentWorkflow} (${progress.completed}/${progress.total})`;
|
|
344
|
-
|
|
371
|
+
|
|
345
372
|
workflow.history.unshift({
|
|
346
373
|
name: historyName,
|
|
347
374
|
status: isComplete ? 'completed' : 'incomplete',
|
|
@@ -353,12 +380,24 @@ export function startWorkflow(projectRoot, workflowName) {
|
|
|
353
380
|
// Start new workflow
|
|
354
381
|
workflow.currentWorkflow = workflowName;
|
|
355
382
|
workflow.currentStep = 1;
|
|
383
|
+
workflow.mode = mode;
|
|
356
384
|
workflow.startTime = now;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
385
|
+
|
|
386
|
+
// Initialize steps - mark skipped steps in quick mode
|
|
387
|
+
workflow.steps = STEPS.map((step) => {
|
|
388
|
+
if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(step.id)) {
|
|
389
|
+
return {
|
|
390
|
+
status: STATUS.SKIPPED,
|
|
391
|
+
output: null,
|
|
392
|
+
completedAt: now,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
status: STATUS.PENDING,
|
|
397
|
+
output: null,
|
|
398
|
+
completedAt: null,
|
|
399
|
+
};
|
|
400
|
+
});
|
|
362
401
|
|
|
363
402
|
writeWorkflow(projectRoot, workflow);
|
|
364
403
|
return workflow;
|
|
@@ -384,6 +423,7 @@ export function updateStep(projectRoot, stepNumber, status, outputFile = null) {
|
|
|
384
423
|
|
|
385
424
|
const now = formatLocalDateTime(new Date());
|
|
386
425
|
const normalizedOutput = normalizeOutputName(outputFile);
|
|
426
|
+
const mode = workflow.mode || MODE.FULL;
|
|
387
427
|
|
|
388
428
|
workflow.steps[index] = {
|
|
389
429
|
status,
|
|
@@ -391,11 +431,15 @@ export function updateStep(projectRoot, stepNumber, status, outputFile = null) {
|
|
|
391
431
|
completedAt: status === STATUS.COMPLETED ? now : null,
|
|
392
432
|
};
|
|
393
433
|
|
|
394
|
-
// Update current step
|
|
434
|
+
// Update current step based on mode
|
|
395
435
|
if (status === STATUS.IN_PROGRESS) {
|
|
396
436
|
workflow.currentStep = stepNumber;
|
|
397
|
-
} else if (status === STATUS.COMPLETED
|
|
398
|
-
|
|
437
|
+
} else if (status === STATUS.COMPLETED) {
|
|
438
|
+
const nextStep = getNextStep(stepNumber, mode);
|
|
439
|
+
if (nextStep) {
|
|
440
|
+
workflow.currentStep = nextStep;
|
|
441
|
+
}
|
|
442
|
+
// nextStep is null means workflow is complete
|
|
399
443
|
}
|
|
400
444
|
|
|
401
445
|
writeWorkflow(projectRoot, workflow);
|
|
@@ -484,27 +528,39 @@ export function getStepByCommand(command) {
|
|
|
484
528
|
/**
|
|
485
529
|
* Calculate workflow completion progress
|
|
486
530
|
* @param {Object} workflow
|
|
487
|
-
* @returns {{completed: number, total: number, lastCompletedStep: number|null}}
|
|
531
|
+
* @returns {{completed: number, total: number, lastCompletedStep: number|null, mode: string}}
|
|
488
532
|
*/
|
|
489
533
|
export function getWorkflowProgress(workflow) {
|
|
490
534
|
if (!workflow || !workflow.steps) {
|
|
491
|
-
return { completed: 0, total: STEPS.length, lastCompletedStep: null };
|
|
535
|
+
return { completed: 0, total: STEPS.length, lastCompletedStep: null, mode: MODE.FULL };
|
|
492
536
|
}
|
|
493
537
|
|
|
538
|
+
const mode = workflow.mode || MODE.FULL;
|
|
494
539
|
let completed = 0;
|
|
495
540
|
let lastCompletedStep = null;
|
|
496
541
|
|
|
497
542
|
workflow.steps.forEach((step, index) => {
|
|
543
|
+
const stepNumber = index + 1;
|
|
544
|
+
|
|
545
|
+
// Skip counting skipped steps in quick mode
|
|
546
|
+
if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(stepNumber)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
498
550
|
if (step && step.status === STATUS.COMPLETED) {
|
|
499
551
|
completed++;
|
|
500
|
-
lastCompletedStep =
|
|
552
|
+
lastCompletedStep = stepNumber;
|
|
501
553
|
}
|
|
502
554
|
});
|
|
503
555
|
|
|
556
|
+
// Quick mode has 5 steps (1,2,5,6,7), full mode has 7 steps
|
|
557
|
+
const total = mode === MODE.QUICK ? QUICK_MODE_STEPS.length : STEPS.length;
|
|
558
|
+
|
|
504
559
|
return {
|
|
505
560
|
completed,
|
|
506
|
-
total
|
|
561
|
+
total,
|
|
507
562
|
lastCompletedStep,
|
|
563
|
+
mode,
|
|
508
564
|
};
|
|
509
565
|
}
|
|
510
566
|
|
|
@@ -534,7 +590,7 @@ export function isWorkflowIncomplete(workflow) {
|
|
|
534
590
|
*/
|
|
535
591
|
export function addToHistory(projectRoot, entry) {
|
|
536
592
|
let workflow = readWorkflow(projectRoot);
|
|
537
|
-
|
|
593
|
+
|
|
538
594
|
if (!workflow) {
|
|
539
595
|
workflow = {
|
|
540
596
|
currentWorkflow: null,
|
|
@@ -562,3 +618,40 @@ export function addToHistory(projectRoot, entry) {
|
|
|
562
618
|
writeWorkflow(projectRoot, workflow);
|
|
563
619
|
return workflow;
|
|
564
620
|
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Check if workflow is in quick mode
|
|
624
|
+
* @param {Object} workflow
|
|
625
|
+
* @returns {boolean}
|
|
626
|
+
*/
|
|
627
|
+
export function isQuickMode(workflow) {
|
|
628
|
+
return workflow?.mode === MODE.QUICK;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get next valid step based on mode
|
|
633
|
+
* @param {number} currentStep - Current step number (1-based)
|
|
634
|
+
* @param {string} mode - Workflow mode
|
|
635
|
+
* @returns {number|null} - Next step number or null if complete
|
|
636
|
+
*/
|
|
637
|
+
export function getNextStep(currentStep, mode) {
|
|
638
|
+
if (mode === MODE.QUICK) {
|
|
639
|
+
const currentIndex = QUICK_MODE_STEPS.indexOf(currentStep);
|
|
640
|
+
if (currentIndex >= 0 && currentIndex < QUICK_MODE_STEPS.length - 1) {
|
|
641
|
+
return QUICK_MODE_STEPS[currentIndex + 1];
|
|
642
|
+
}
|
|
643
|
+
return null; // Workflow complete
|
|
644
|
+
}
|
|
645
|
+
// Full mode
|
|
646
|
+
return currentStep < STEPS.length ? currentStep + 1 : null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check if a step should be skipped based on mode
|
|
651
|
+
* @param {number} stepNumber - Step number (1-based)
|
|
652
|
+
* @param {string} mode - Workflow mode
|
|
653
|
+
* @returns {boolean}
|
|
654
|
+
*/
|
|
655
|
+
export function shouldSkipStep(stepNumber, mode) {
|
|
656
|
+
return mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(stepNumber);
|
|
657
|
+
}
|