@localsummer/incspec 0.0.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.
- package/LICENSE +15 -0
- package/README.md +540 -0
- package/commands/analyze.mjs +133 -0
- package/commands/apply.mjs +111 -0
- package/commands/archive.mjs +340 -0
- package/commands/collect-dep.mjs +85 -0
- package/commands/collect-req.mjs +80 -0
- package/commands/cursor-sync.mjs +116 -0
- package/commands/design.mjs +131 -0
- package/commands/help.mjs +235 -0
- package/commands/init.mjs +127 -0
- package/commands/list.mjs +111 -0
- package/commands/merge.mjs +112 -0
- package/commands/status.mjs +117 -0
- package/commands/update.mjs +189 -0
- package/commands/validate.mjs +181 -0
- package/index.mjs +236 -0
- package/lib/agents.mjs +163 -0
- package/lib/config.mjs +343 -0
- package/lib/cursor.mjs +307 -0
- package/lib/spec.mjs +300 -0
- package/lib/terminal.mjs +292 -0
- package/lib/workflow.mjs +563 -0
- package/package.json +40 -0
- package/templates/AGENTS.md +610 -0
- package/templates/INCSPEC_BLOCK.md +19 -0
- package/templates/WORKFLOW.md +22 -0
- package/templates/cursor-commands/analyze-codeflow.md +341 -0
- package/templates/cursor-commands/analyze-increment-codeflow.md +246 -0
- package/templates/cursor-commands/apply-increment-code.md +392 -0
- package/templates/cursor-commands/inc-archive.md +278 -0
- package/templates/cursor-commands/merge-to-baseline.md +329 -0
- package/templates/cursor-commands/structured-requirements-collection.md +123 -0
- package/templates/cursor-commands/ui-dependency-collection.md +143 -0
- package/templates/project.md +24 -0
package/lib/workflow.mjs
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow state management for incspec
|
|
3
|
+
* - Read/write WORKFLOW.md
|
|
4
|
+
* - Track current step
|
|
5
|
+
* - Manage workflow history
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { INCSPEC_DIR, FILES, getTemplatesDir } from './config.mjs';
|
|
11
|
+
|
|
12
|
+
/** Workflow steps definition */
|
|
13
|
+
export const STEPS = [
|
|
14
|
+
{ id: 1, name: 'analyze-codeflow', label: '代码流程分析', command: 'analyze' },
|
|
15
|
+
{ id: 2, name: 'collect-requirements', label: '结构化需求收集', command: 'collect-req' },
|
|
16
|
+
{ id: 3, name: 'collect-dependencies', label: 'UI依赖采集', command: 'collect-dep' },
|
|
17
|
+
{ id: 4, name: 'design-increment', label: '增量设计', command: 'design' },
|
|
18
|
+
{ id: 5, name: 'apply-code', label: '应用代码变更', command: 'apply' },
|
|
19
|
+
{ id: 6, name: 'merge-baseline', label: '合并到基线', command: 'merge' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** Step status */
|
|
23
|
+
export const STATUS = {
|
|
24
|
+
PENDING: 'pending',
|
|
25
|
+
IN_PROGRESS: 'in_progress',
|
|
26
|
+
COMPLETED: 'completed',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function normalizeOutputName(outputFile) {
|
|
30
|
+
if (!outputFile || typeof outputFile !== 'string') {
|
|
31
|
+
return outputFile;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!/[\\/]/.test(outputFile)) {
|
|
35
|
+
return outputFile;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parts = outputFile.split(/[/\\]+/);
|
|
39
|
+
return parts[parts.length - 1] || outputFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureTableSeparator(content, sectionTitle, headerLine, separatorLine) {
|
|
43
|
+
const sectionIndex = content.indexOf(sectionTitle);
|
|
44
|
+
if (sectionIndex === -1) {
|
|
45
|
+
return { content, updated: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const headerIndex = content.indexOf(headerLine, sectionIndex);
|
|
49
|
+
if (headerIndex === -1) {
|
|
50
|
+
return { content, updated: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headerLineEnd = content.indexOf('\n', headerIndex);
|
|
54
|
+
if (headerLineEnd === -1) {
|
|
55
|
+
return { content, updated: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const nextLineStart = headerLineEnd + 1;
|
|
59
|
+
const nextLineEnd = content.indexOf('\n', nextLineStart);
|
|
60
|
+
const nextLine = content
|
|
61
|
+
.slice(nextLineStart, nextLineEnd === -1 ? content.length : nextLineEnd)
|
|
62
|
+
.replace(/\r$/, '');
|
|
63
|
+
|
|
64
|
+
if (nextLine.trim() === separatorLine.trim()) {
|
|
65
|
+
return { content, updated: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updatedContent =
|
|
69
|
+
content.slice(0, nextLineStart) +
|
|
70
|
+
`${separatorLine}\n` +
|
|
71
|
+
content.slice(nextLineStart);
|
|
72
|
+
|
|
73
|
+
return { content: updatedContent, updated: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeWorkflowContent(content) {
|
|
77
|
+
let updated = false;
|
|
78
|
+
let normalized = content;
|
|
79
|
+
|
|
80
|
+
const stepsResult = ensureTableSeparator(
|
|
81
|
+
normalized,
|
|
82
|
+
'## 步骤进度',
|
|
83
|
+
'| 步骤 | 状态 | 输出文件 | 完成时间 |',
|
|
84
|
+
'|------|------|---------|---------|'
|
|
85
|
+
);
|
|
86
|
+
normalized = stepsResult.content;
|
|
87
|
+
updated = updated || stepsResult.updated;
|
|
88
|
+
|
|
89
|
+
const historyResult = ensureTableSeparator(
|
|
90
|
+
normalized,
|
|
91
|
+
'## 工作流历史',
|
|
92
|
+
'| 工作流 | 状态 | 开始时间 | 完成时间 |',
|
|
93
|
+
'|--------|------|---------|---------|'
|
|
94
|
+
);
|
|
95
|
+
normalized = historyResult.content;
|
|
96
|
+
updated = updated || historyResult.updated;
|
|
97
|
+
|
|
98
|
+
return { content: normalized, updated };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get workflow file path
|
|
103
|
+
* @param {string} projectRoot
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getWorkflowPath(projectRoot) {
|
|
107
|
+
return path.join(projectRoot, INCSPEC_DIR, FILES.workflow);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse WORKFLOW.md content
|
|
112
|
+
* @param {string} content
|
|
113
|
+
* @returns {Object}
|
|
114
|
+
*/
|
|
115
|
+
export function parseWorkflow(content) {
|
|
116
|
+
const workflow = {
|
|
117
|
+
currentWorkflow: null,
|
|
118
|
+
currentStep: null,
|
|
119
|
+
startTime: null,
|
|
120
|
+
lastUpdate: null,
|
|
121
|
+
steps: [],
|
|
122
|
+
history: [],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Parse current workflow info
|
|
126
|
+
const workflowMatch = content.match(/\*\*当前工作流\*\*:\s*(.+)/);
|
|
127
|
+
if (workflowMatch) {
|
|
128
|
+
workflow.currentWorkflow = workflowMatch[1].trim();
|
|
129
|
+
if (workflow.currentWorkflow === '-' || workflow.currentWorkflow === 'none') {
|
|
130
|
+
workflow.currentWorkflow = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const stepMatch = content.match(/\*\*当前步骤\*\*:\s*(\d+)/);
|
|
135
|
+
if (stepMatch) {
|
|
136
|
+
workflow.currentStep = parseInt(stepMatch[1], 10);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const startMatch = content.match(/\*\*开始时间\*\*:\s*(.+)/);
|
|
140
|
+
if (startMatch) {
|
|
141
|
+
workflow.startTime = startMatch[1].trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const updateMatch = content.match(/\*\*最后更新\*\*:\s*(.+)/);
|
|
145
|
+
if (updateMatch) {
|
|
146
|
+
workflow.lastUpdate = updateMatch[1].trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Parse steps table
|
|
150
|
+
const stepsTableMatch = content.match(/## 步骤进度\n\n\|[^\n]+\n\|[^\n]+\n([\s\S]*?)(?=\n##|\n*$)/);
|
|
151
|
+
if (stepsTableMatch) {
|
|
152
|
+
const rows = stepsTableMatch[1].trim().split('\n').filter(r => r.trim());
|
|
153
|
+
workflow.steps = rows.map(row => {
|
|
154
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
155
|
+
if (cells.length >= 4) {
|
|
156
|
+
return {
|
|
157
|
+
step: cells[0],
|
|
158
|
+
status: cells[1],
|
|
159
|
+
output: cells[2] === '-' ? null : cells[2],
|
|
160
|
+
completedAt: cells[3] === '-' ? null : cells[3],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}).filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse history table
|
|
168
|
+
const historyMatch = content.match(/## 工作流历史\n\n\|[^\n]+\n\|[^\n]+\n([\s\S]*?)(?=\n##|\n*$)/);
|
|
169
|
+
if (historyMatch) {
|
|
170
|
+
const rows = historyMatch[1].trim().split('\n').filter(r => r.trim());
|
|
171
|
+
workflow.history = rows.map(row => {
|
|
172
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
173
|
+
if (cells.length >= 4) {
|
|
174
|
+
return {
|
|
175
|
+
name: cells[0],
|
|
176
|
+
status: cells[1],
|
|
177
|
+
startTime: cells[2] === '-' ? null : cells[2],
|
|
178
|
+
endTime: cells[3] === '-' ? null : cells[3],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}).filter(Boolean);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return workflow;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Read workflow state
|
|
190
|
+
* @param {string} projectRoot
|
|
191
|
+
* @returns {Object|null}
|
|
192
|
+
*/
|
|
193
|
+
export function readWorkflow(projectRoot) {
|
|
194
|
+
const workflowPath = getWorkflowPath(projectRoot);
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(workflowPath)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
201
|
+
const normalized = normalizeWorkflowContent(content);
|
|
202
|
+
if (normalized.updated) {
|
|
203
|
+
fs.writeFileSync(workflowPath, normalized.content, 'utf-8');
|
|
204
|
+
}
|
|
205
|
+
return parseWorkflow(normalized.content);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate WORKFLOW.md content
|
|
210
|
+
* @param {Object} workflow
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
export function generateWorkflowContent(workflow) {
|
|
214
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
215
|
+
|
|
216
|
+
const lines = [
|
|
217
|
+
'# Workflow Status',
|
|
218
|
+
'',
|
|
219
|
+
`**当前工作流**: ${workflow.currentWorkflow || '-'}`,
|
|
220
|
+
`**当前步骤**: ${workflow.currentStep || '-'}`,
|
|
221
|
+
`**开始时间**: ${workflow.startTime || '-'}`,
|
|
222
|
+
`**最后更新**: ${now}`,
|
|
223
|
+
'',
|
|
224
|
+
'## 步骤进度',
|
|
225
|
+
'',
|
|
226
|
+
'| 步骤 | 状态 | 输出文件 | 完成时间 |',
|
|
227
|
+
'|------|------|---------|---------|',
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// Generate steps
|
|
231
|
+
STEPS.forEach((step, index) => {
|
|
232
|
+
const stepData = workflow.steps[index] || {};
|
|
233
|
+
const status = stepData.status || STATUS.PENDING;
|
|
234
|
+
const output = stepData.output || '-';
|
|
235
|
+
const completedAt = stepData.completedAt || '-';
|
|
236
|
+
lines.push(`| ${step.id}. ${step.name} | ${status} | ${output} | ${completedAt} |`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push('## 工作流历史');
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push('| 工作流 | 状态 | 开始时间 | 完成时间 |');
|
|
243
|
+
lines.push('|--------|------|---------|---------|');
|
|
244
|
+
|
|
245
|
+
// Generate history
|
|
246
|
+
if (workflow.history && workflow.history.length > 0) {
|
|
247
|
+
workflow.history.forEach(item => {
|
|
248
|
+
lines.push(`| ${item.name} | ${item.status} | ${item.startTime || '-'} | ${item.endTime || '-'} |`);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Write workflow state
|
|
257
|
+
* @param {string} projectRoot
|
|
258
|
+
* @param {Object} workflow
|
|
259
|
+
*/
|
|
260
|
+
export function writeWorkflow(projectRoot, workflow) {
|
|
261
|
+
const workflowPath = getWorkflowPath(projectRoot);
|
|
262
|
+
const content = generateWorkflowContent(workflow);
|
|
263
|
+
fs.writeFileSync(workflowPath, content, 'utf-8');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Initialize empty workflow
|
|
268
|
+
* @param {string} projectRoot
|
|
269
|
+
*/
|
|
270
|
+
export function initWorkflow(projectRoot) {
|
|
271
|
+
const workflowPath = getWorkflowPath(projectRoot);
|
|
272
|
+
const content = generateInitialWorkflowContent();
|
|
273
|
+
fs.writeFileSync(workflowPath, content, 'utf-8');
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
currentWorkflow: null,
|
|
277
|
+
currentStep: null,
|
|
278
|
+
startTime: null,
|
|
279
|
+
lastUpdate: null,
|
|
280
|
+
steps: STEPS.map(() => ({
|
|
281
|
+
status: STATUS.PENDING,
|
|
282
|
+
output: null,
|
|
283
|
+
completedAt: null,
|
|
284
|
+
})),
|
|
285
|
+
history: [],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Generate initial WORKFLOW.md content from template
|
|
291
|
+
* @returns {string}
|
|
292
|
+
*/
|
|
293
|
+
function generateInitialWorkflowContent() {
|
|
294
|
+
const templatePath = path.join(getTemplatesDir(), 'WORKFLOW.md');
|
|
295
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
296
|
+
|
|
297
|
+
if (fs.existsSync(templatePath)) {
|
|
298
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
299
|
+
content = content.replace(/\{\{last_update\}\}/g, now);
|
|
300
|
+
return content;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fallback to generated content
|
|
304
|
+
const workflow = {
|
|
305
|
+
currentWorkflow: null,
|
|
306
|
+
currentStep: null,
|
|
307
|
+
startTime: null,
|
|
308
|
+
lastUpdate: null,
|
|
309
|
+
steps: STEPS.map(() => ({
|
|
310
|
+
status: STATUS.PENDING,
|
|
311
|
+
output: null,
|
|
312
|
+
completedAt: null,
|
|
313
|
+
})),
|
|
314
|
+
history: [],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return generateWorkflowContent(workflow);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Start a new workflow
|
|
322
|
+
* @param {string} projectRoot
|
|
323
|
+
* @param {string} workflowName
|
|
324
|
+
* @returns {Object}
|
|
325
|
+
*/
|
|
326
|
+
export function startWorkflow(projectRoot, workflowName) {
|
|
327
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
328
|
+
let workflow = readWorkflow(projectRoot);
|
|
329
|
+
|
|
330
|
+
if (!workflow) {
|
|
331
|
+
workflow = initWorkflow(projectRoot);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Archive current workflow if exists
|
|
335
|
+
if (workflow.currentWorkflow) {
|
|
336
|
+
const progress = getWorkflowProgress(workflow);
|
|
337
|
+
const isComplete = progress.completed === progress.total;
|
|
338
|
+
|
|
339
|
+
// Format: "workflowName (completed/total)" for incomplete, just name for complete
|
|
340
|
+
const historyName = isComplete
|
|
341
|
+
? workflow.currentWorkflow
|
|
342
|
+
: `${workflow.currentWorkflow} (${progress.completed}/${progress.total})`;
|
|
343
|
+
|
|
344
|
+
workflow.history.unshift({
|
|
345
|
+
name: historyName,
|
|
346
|
+
status: isComplete ? 'completed' : 'incomplete',
|
|
347
|
+
startTime: workflow.startTime,
|
|
348
|
+
endTime: now,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Start new workflow
|
|
353
|
+
workflow.currentWorkflow = workflowName;
|
|
354
|
+
workflow.currentStep = 1;
|
|
355
|
+
workflow.startTime = now;
|
|
356
|
+
workflow.steps = STEPS.map(() => ({
|
|
357
|
+
status: STATUS.PENDING,
|
|
358
|
+
output: null,
|
|
359
|
+
completedAt: null,
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
writeWorkflow(projectRoot, workflow);
|
|
363
|
+
return workflow;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Update step status
|
|
368
|
+
* @param {string} projectRoot
|
|
369
|
+
* @param {number} stepNumber - 1-based step number
|
|
370
|
+
* @param {string} status
|
|
371
|
+
* @param {string} outputFile
|
|
372
|
+
*/
|
|
373
|
+
export function updateStep(projectRoot, stepNumber, status, outputFile = null) {
|
|
374
|
+
const workflow = readWorkflow(projectRoot);
|
|
375
|
+
if (!workflow) {
|
|
376
|
+
throw new Error('工作流未初始化');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const index = stepNumber - 1;
|
|
380
|
+
if (index < 0 || index >= STEPS.length) {
|
|
381
|
+
throw new Error(`无效的步骤编号: ${stepNumber}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
385
|
+
const normalizedOutput = normalizeOutputName(outputFile);
|
|
386
|
+
|
|
387
|
+
workflow.steps[index] = {
|
|
388
|
+
status,
|
|
389
|
+
output: normalizedOutput,
|
|
390
|
+
completedAt: status === STATUS.COMPLETED ? now : null,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Update current step
|
|
394
|
+
if (status === STATUS.IN_PROGRESS) {
|
|
395
|
+
workflow.currentStep = stepNumber;
|
|
396
|
+
} else if (status === STATUS.COMPLETED && stepNumber < STEPS.length) {
|
|
397
|
+
workflow.currentStep = stepNumber + 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
writeWorkflow(projectRoot, workflow);
|
|
401
|
+
return workflow;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Complete current workflow
|
|
406
|
+
* @param {string} projectRoot
|
|
407
|
+
*/
|
|
408
|
+
export function completeWorkflow(projectRoot) {
|
|
409
|
+
const workflow = readWorkflow(projectRoot);
|
|
410
|
+
if (!workflow) {
|
|
411
|
+
throw new Error('工作流未初始化');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
415
|
+
|
|
416
|
+
// Add to history
|
|
417
|
+
workflow.history.unshift({
|
|
418
|
+
name: workflow.currentWorkflow,
|
|
419
|
+
status: 'completed',
|
|
420
|
+
startTime: workflow.startTime,
|
|
421
|
+
endTime: now,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Reset current workflow
|
|
425
|
+
workflow.currentWorkflow = null;
|
|
426
|
+
workflow.currentStep = null;
|
|
427
|
+
workflow.startTime = null;
|
|
428
|
+
|
|
429
|
+
writeWorkflow(projectRoot, workflow);
|
|
430
|
+
return workflow;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Archive current workflow
|
|
435
|
+
* @param {string} projectRoot
|
|
436
|
+
*/
|
|
437
|
+
export function archiveWorkflow(projectRoot) {
|
|
438
|
+
const workflow = readWorkflow(projectRoot);
|
|
439
|
+
if (!workflow || !workflow.currentWorkflow) {
|
|
440
|
+
throw new Error('工作流未初始化');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
444
|
+
|
|
445
|
+
workflow.history.unshift({
|
|
446
|
+
name: workflow.currentWorkflow,
|
|
447
|
+
status: 'archived',
|
|
448
|
+
startTime: workflow.startTime || '-',
|
|
449
|
+
endTime: now,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
workflow.currentWorkflow = null;
|
|
453
|
+
workflow.currentStep = null;
|
|
454
|
+
workflow.startTime = null;
|
|
455
|
+
workflow.steps = STEPS.map(() => ({
|
|
456
|
+
status: STATUS.PENDING,
|
|
457
|
+
output: null,
|
|
458
|
+
completedAt: null,
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
writeWorkflow(projectRoot, workflow);
|
|
462
|
+
return workflow;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get step info by number
|
|
467
|
+
* @param {number} stepNumber
|
|
468
|
+
* @returns {Object|null}
|
|
469
|
+
*/
|
|
470
|
+
export function getStepInfo(stepNumber) {
|
|
471
|
+
return STEPS.find(s => s.id === stepNumber) || null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get step info by command name
|
|
476
|
+
* @param {string} command
|
|
477
|
+
* @returns {Object|null}
|
|
478
|
+
*/
|
|
479
|
+
export function getStepByCommand(command) {
|
|
480
|
+
return STEPS.find(s => s.command === command) || null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Calculate workflow completion progress
|
|
485
|
+
* @param {Object} workflow
|
|
486
|
+
* @returns {{completed: number, total: number, lastCompletedStep: number|null}}
|
|
487
|
+
*/
|
|
488
|
+
export function getWorkflowProgress(workflow) {
|
|
489
|
+
if (!workflow || !workflow.steps) {
|
|
490
|
+
return { completed: 0, total: STEPS.length, lastCompletedStep: null };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let completed = 0;
|
|
494
|
+
let lastCompletedStep = null;
|
|
495
|
+
|
|
496
|
+
workflow.steps.forEach((step, index) => {
|
|
497
|
+
if (step && step.status === STATUS.COMPLETED) {
|
|
498
|
+
completed++;
|
|
499
|
+
lastCompletedStep = index + 1;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
completed,
|
|
505
|
+
total: STEPS.length,
|
|
506
|
+
lastCompletedStep,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Check if workflow is incomplete (has uncompleted steps)
|
|
512
|
+
* @param {Object} workflow
|
|
513
|
+
* @returns {boolean}
|
|
514
|
+
*/
|
|
515
|
+
export function isWorkflowIncomplete(workflow) {
|
|
516
|
+
if (!workflow || !workflow.currentWorkflow) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const progress = getWorkflowProgress(workflow);
|
|
521
|
+
return progress.completed < progress.total;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Add entry to workflow history
|
|
526
|
+
* @param {string} projectRoot
|
|
527
|
+
* @param {Object} entry - History entry
|
|
528
|
+
* @param {string} entry.name - Workflow/file name
|
|
529
|
+
* @param {string} entry.status - Status (e.g., 'archived', 'completed')
|
|
530
|
+
* @param {string} [entry.startTime] - Start time (optional)
|
|
531
|
+
* @param {string} [entry.endTime] - End time (optional, defaults to now)
|
|
532
|
+
* @returns {Object} Updated workflow
|
|
533
|
+
*/
|
|
534
|
+
export function addToHistory(projectRoot, entry) {
|
|
535
|
+
let workflow = readWorkflow(projectRoot);
|
|
536
|
+
|
|
537
|
+
if (!workflow) {
|
|
538
|
+
workflow = {
|
|
539
|
+
currentWorkflow: null,
|
|
540
|
+
currentStep: null,
|
|
541
|
+
startTime: null,
|
|
542
|
+
lastUpdate: null,
|
|
543
|
+
steps: STEPS.map(() => ({
|
|
544
|
+
status: STATUS.PENDING,
|
|
545
|
+
output: null,
|
|
546
|
+
completedAt: null,
|
|
547
|
+
})),
|
|
548
|
+
history: [],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
553
|
+
|
|
554
|
+
workflow.history.unshift({
|
|
555
|
+
name: entry.name,
|
|
556
|
+
status: entry.status,
|
|
557
|
+
startTime: entry.startTime || '-',
|
|
558
|
+
endTime: entry.endTime || now,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
writeWorkflow(projectRoot, workflow);
|
|
562
|
+
return workflow;
|
|
563
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@localsummer/incspec",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "面向 AI 编程助手的增量规范驱动开发工具",
|
|
5
|
+
"bin": {
|
|
6
|
+
"incspec": "index.mjs"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.mjs",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"incspec",
|
|
15
|
+
"incremental",
|
|
16
|
+
"spec-driven",
|
|
17
|
+
"cli",
|
|
18
|
+
"workflow",
|
|
19
|
+
"ai"
|
|
20
|
+
],
|
|
21
|
+
"author": "localSummer",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/localSummer/IncSpec.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/localSummer/IncSpec#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/localSummer/IncSpec/issues"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.mjs",
|
|
36
|
+
"commands/",
|
|
37
|
+
"lib/",
|
|
38
|
+
"templates/"
|
|
39
|
+
]
|
|
40
|
+
}
|