@localsummer/incspec 0.0.10 → 0.1.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/README.md CHANGED
@@ -444,6 +444,18 @@ incspec/
444
444
  3. **保留新基线** - 归档时新基线会被复制(而非移动)到归档目录,同时保留在 `baselines/` 作为下一轮起点。
445
445
  4. **定期清理** - 对于过期的归档,可手动删除或移至外部存储。
446
446
 
447
+ ## 回退与重置
448
+
449
+ ```bash
450
+ incspec reset # 全量重置,归档所有产出
451
+ incspec reset --to=3 # 回退到步骤 3,保留 1-3,重置 4-7
452
+ incspec reset -t 3 # 短选项形式
453
+ ```
454
+
455
+ **回退行为:** 保留目标步骤及之前状态,重置后续步骤为 pending,被重置步骤的产出自动归档。
456
+
457
+ **限制:** 目标步骤必须已完成(1-6),快速模式下不能回退到被跳过的步骤(3、4)。
458
+
447
459
  ## 目录结构
448
460
 
449
461
  ```
@@ -543,6 +555,10 @@ incspec archive --workflow # 同上,显式指定
543
555
  incspec archive <file> # 归档指定文件
544
556
  incspec archive <file> --keep # 复制而非移动
545
557
  incspec archive -y # 跳过确认提示
558
+
559
+ incspec reset # 重置当前工作流(别名:rs)
560
+ incspec reset --to=3 # 回退到步骤 3,保留步骤 1-3 的状态
561
+ incspec reset -t 3 # 同上,短选项形式
546
562
  ```
547
563
 
548
564
  </details>
@@ -564,6 +580,7 @@ incspec archive -y # 跳过确认提示
564
580
  | `validate` | `v` | 验证 |
565
581
  | `sync` | `s` | 同步 |
566
582
  | `update` | `up` | 更新 |
583
+ | `reset` | `rs` | 重置/回退 |
567
584
  | `help` | `h` | 帮助 |
568
585
 
569
586
  </details>
package/commands/help.mjs CHANGED
@@ -119,9 +119,12 @@ const COMMANDS = {
119
119
  ],
120
120
  },
121
121
  reset: {
122
- usage: 'incspec reset',
122
+ usage: 'incspec reset [--to=<step>]',
123
123
  aliases: ['rs'],
124
- description: '重置当前工作流,归档已有产出',
124
+ description: '重置当前工作流,归档已有产出(支持部分回退)',
125
+ options: [
126
+ ['-t, --to=<step>', '回退到指定步骤(1-6),保留该步骤及之前的状态'],
127
+ ],
125
128
  },
126
129
  sync: {
127
130
  usage: 'incspec sync [--cursor] [--claude] [--all] [--project|--global]',
package/commands/init.mjs CHANGED
@@ -121,7 +121,7 @@ export async function initCommand(ctx) {
121
121
  print('');
122
122
  print(colorize('下一步:', colors.bold));
123
123
  print(colorize(` 1. 运行 'incspec status' 查看工作流状态`, colors.dim));
124
- print(colorize(` 2. 运行 'incspec cursor-sync' 同步 Cursor 命令`, colors.dim));
124
+ print(colorize(` 2. 运行 'incspec sync' 同步 Cursor 命令`, colors.dim));
125
125
  print(colorize(` 3. 使用 /incspec/inc-analyze 开始第一个工作流`, colors.dim));
126
126
  print('');
127
127
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * reset command - Reset current workflow
2
+ * reset command - Reset current workflow (full or partial)
3
3
  */
4
4
 
5
5
  import * as fs from 'fs';
@@ -12,7 +12,9 @@ import {
12
12
  import {
13
13
  readWorkflow,
14
14
  archiveWorkflow,
15
+ resetToStep,
15
16
  STATUS,
17
+ STEPS,
16
18
  } from '../lib/workflow.mjs';
17
19
  import { archiveSpec } from '../lib/spec.mjs';
18
20
  import {
@@ -22,6 +24,7 @@ import {
22
24
  printSuccess,
23
25
  printInfo,
24
26
  printWarning,
27
+ printError,
25
28
  } from '../lib/terminal.mjs';
26
29
 
27
30
  /**
@@ -77,7 +80,7 @@ function getOutputDirForStepIndex(stepIndex) {
77
80
  * Collect output files from workflow steps
78
81
  * @param {string} projectRoot
79
82
  * @param {Object} workflow
80
- * @returns {Array<{path: string, name: string}>}
83
+ * @returns {Array<{path: string, name: string, stepIndex: number}>}
81
84
  */
82
85
  function collectWorkflowOutputs(projectRoot, workflow) {
83
86
  const outputs = [];
@@ -102,6 +105,7 @@ function collectWorkflowOutputs(projectRoot, workflow) {
102
105
  outputs.push({
103
106
  path: outputPath,
104
107
  name: step.output,
108
+ stepIndex: index,
105
109
  });
106
110
  }
107
111
  });
@@ -110,24 +114,104 @@ function collectWorkflowOutputs(projectRoot, workflow) {
110
114
  }
111
115
 
112
116
  /**
113
- * Execute reset command
114
- * @param {Object} ctx - Command context
117
+ * Collect output files for specific step indices
118
+ * @param {string} projectRoot
119
+ * @param {Array<{index: number, output: string}>} resetOutputs
120
+ * @returns {Array<{path: string, name: string, stepIndex: number}>}
115
121
  */
116
- export async function resetCommand(ctx) {
117
- const { cwd } = ctx;
122
+ function collectResetOutputs(projectRoot, resetOutputs) {
123
+ const outputs = [];
118
124
 
119
- // Ensure initialized
120
- const projectRoot = ensureInitialized(cwd);
125
+ for (const item of resetOutputs) {
126
+ const dir = getOutputDirForStepIndex(item.index);
127
+ if (!dir) {
128
+ continue;
129
+ }
121
130
 
122
- // Read current workflow
131
+ const outputPath = path.join(projectRoot, INCSPEC_DIR, dir, item.output);
132
+ if (fs.existsSync(outputPath)) {
133
+ outputs.push({
134
+ path: outputPath,
135
+ name: item.output,
136
+ stepIndex: item.index,
137
+ });
138
+ }
139
+ }
140
+
141
+ return outputs;
142
+ }
143
+
144
+ /**
145
+ * Archive output files
146
+ * @param {string} projectRoot
147
+ * @param {Array<{path: string, name: string}>} outputs
148
+ * @param {string} moduleName
149
+ * @returns {number} Number of archived files
150
+ */
151
+ function archiveOutputFiles(projectRoot, outputs, moduleName) {
152
+ let archivedCount = 0;
153
+
154
+ for (const output of outputs) {
155
+ try {
156
+ archiveSpec(projectRoot, output.path, true, moduleName);
157
+ print(colorize(` - ${output.name}`, colors.dim));
158
+ archivedCount++;
159
+ } catch (e) {
160
+ printWarning(`归档失败: ${output.name} - ${e.message}`);
161
+ }
162
+ }
163
+
164
+ return archivedCount;
165
+ }
166
+
167
+ /**
168
+ * Execute partial reset (reset to specific step)
169
+ * @param {string} projectRoot
170
+ * @param {number} targetStep
171
+ */
172
+ function executePartialReset(projectRoot, targetStep) {
123
173
  const workflow = readWorkflow(projectRoot);
174
+ const workflowName = workflow.currentWorkflow;
175
+ const moduleName = extractModuleName(workflowName);
176
+
177
+ print('');
178
+ print(colorize(' incspec 工作流部分回退', colors.bold, colors.cyan));
179
+ print(colorize(' ───────────────────────', colors.dim));
180
+ print('');
181
+ print(colorize(`工作流: ${workflowName}`, colors.dim));
182
+ print(colorize(`回退目标: 步骤 ${targetStep}`, colors.dim));
183
+ print('');
184
+
185
+ // Perform partial reset
186
+ const { workflow: updatedWorkflow, resetOutputs } = resetToStep(projectRoot, targetStep);
187
+
188
+ // Collect and archive output files from reset steps
189
+ const outputs = collectResetOutputs(projectRoot, resetOutputs);
190
+
191
+ if (outputs.length > 0) {
192
+ print(colorize('归档被重置步骤的产出文件:', colors.dim));
193
+
194
+ const archivedCount = archiveOutputFiles(projectRoot, outputs, moduleName);
124
195
 
125
- if (!workflow?.currentWorkflow) {
126
196
  print('');
127
- printInfo('当前无活跃工作流,无需重置');
128
- return;
197
+ printSuccess(`已归档 ${archivedCount} 个产出文件到 archives/${new Date().toISOString().slice(0, 7)}/${moduleName}/`);
198
+ } else {
199
+ printInfo('被重置步骤无产出文件需要归档');
129
200
  }
130
201
 
202
+ // Show reset result
203
+ print('');
204
+ const targetStepInfo = STEPS[targetStep - 1];
205
+ printSuccess(`已回退到步骤 ${targetStep} (${targetStepInfo.name})`);
206
+ print(colorize(`当前步骤已设为: ${updatedWorkflow.currentStep}`, colors.dim));
207
+ }
208
+
209
+ /**
210
+ * Execute full reset
211
+ * @param {string} projectRoot
212
+ */
213
+ function executeFullReset(projectRoot) {
214
+ const workflow = readWorkflow(projectRoot);
131
215
  const workflowName = workflow.currentWorkflow;
132
216
  const moduleName = extractModuleName(workflowName);
133
217
 
@@ -145,17 +229,10 @@ export async function resetCommand(ctx) {
145
229
  if (outputs.length > 0) {
146
230
  print(colorize('归档产出文件:', colors.dim));
147
231
 
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
- }
232
+ const archivedCount = archiveOutputFiles(projectRoot, outputs, moduleName);
156
233
 
157
234
  print('');
158
- printSuccess(`已归档 ${outputs.length} 个产出文件到 archives/${new Date().toISOString().slice(0, 7)}/${moduleName}/`);
235
+ printSuccess(`已归档 ${archivedCount} 个产出文件到 archives/${new Date().toISOString().slice(0, 7)}/${moduleName}/`);
159
236
  } else {
160
237
  printInfo('无产出文件需要归档');
161
238
  }
@@ -164,3 +241,49 @@ export async function resetCommand(ctx) {
164
241
  archiveWorkflow(projectRoot);
165
242
  printSuccess('工作流已重置');
166
243
  }
244
+
245
+ /**
246
+ * Execute reset command
247
+ * @param {Object} ctx - Command context
248
+ */
249
+ export async function resetCommand(ctx) {
250
+ const { cwd, options } = ctx;
251
+
252
+ // Ensure initialized
253
+ const projectRoot = ensureInitialized(cwd);
254
+
255
+ // Read current workflow
256
+ const workflow = readWorkflow(projectRoot);
257
+
258
+ if (!workflow?.currentWorkflow) {
259
+ print('');
260
+ printInfo('当前无活跃工作流,无需重置');
261
+ return;
262
+ }
263
+
264
+ // Check for --to option
265
+ const targetStepStr = options.to || options.t;
266
+
267
+ if (targetStepStr) {
268
+ // Partial reset mode
269
+ const targetStep = parseInt(targetStepStr, 10);
270
+
271
+ if (isNaN(targetStep)) {
272
+ print('');
273
+ printError(`无效的步骤编号: ${targetStepStr}`);
274
+ print(colorize('用法: incspec reset --to=<步骤编号>', colors.dim));
275
+ print(colorize('示例: incspec reset --to=3', colors.dim));
276
+ return;
277
+ }
278
+
279
+ try {
280
+ executePartialReset(projectRoot, targetStep);
281
+ } catch (error) {
282
+ print('');
283
+ printError(error.message);
284
+ }
285
+ } else {
286
+ // Full reset mode (original behavior)
287
+ executeFullReset(projectRoot);
288
+ }
289
+ }
package/commands/sync.mjs CHANGED
@@ -114,12 +114,12 @@ async function syncCursor(ctx) {
114
114
  {
115
115
  name: `当前目录 (${cwd}/.cursor/commands/incspec/)`,
116
116
  value: 'project',
117
- description: '仅对当前目录生效',
117
+ description: colorize('仅对当前目录生效', colors.blue),
118
118
  },
119
119
  {
120
120
  name: '全局目录 (~/.cursor/commands/incspec/)',
121
121
  value: 'global',
122
- description: '对所有项目生效',
122
+ description: colorize('对所有项目生效', colors.blue),
123
123
  },
124
124
  ];
125
125
 
@@ -182,12 +182,12 @@ async function syncClaude(ctx) {
182
182
  {
183
183
  name: `当前目录 (${cwd}/.claude/skills/inc-spec-skill/)`,
184
184
  value: 'project',
185
- description: '仅对当前目录生效',
185
+ description: colorize('仅对当前目录生效', colors.blue),
186
186
  },
187
187
  {
188
188
  name: '全局目录 (~/.claude/skills/inc-spec-skill/)',
189
189
  value: 'global',
190
- description: '对所有项目生效(推荐)',
190
+ description: colorize('对所有项目生效(推荐)', colors.blue),
191
191
  },
192
192
  ];
193
193
 
package/index.mjs CHANGED
@@ -34,13 +34,14 @@ function parseArgs(args) {
34
34
  options: {},
35
35
  };
36
36
 
37
- const valueOptions = new Set(['module', 'feature', 'source-dir', 'output', 'workflow']);
37
+ const valueOptions = new Set(['module', 'feature', 'source-dir', 'output', 'workflow', 'to']);
38
38
  const shortValueMap = new Map([
39
39
  ['m', 'module'],
40
40
  ['f', 'feature'],
41
41
  ['s', 'source-dir'],
42
42
  ['o', 'output'],
43
43
  ['w', 'workflow'],
44
+ ['t', 'to'],
44
45
  ]);
45
46
  let i = 0;
46
47
 
package/lib/cursor.mjs CHANGED
@@ -19,8 +19,8 @@ const GLOBAL_CURSOR_DIR = path.join(os.homedir(), '.cursor', 'commands', 'incspe
19
19
  /** Claude commands source directory (user local) */
20
20
  const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands', 'ai-increment');
21
21
 
22
- /** Built-in templates directory (fallback when Claude commands not installed) */
23
- const TEMPLATES_DIR = fileURLToPath(new URL('../templates/cursor-commands', import.meta.url));
22
+ /** Built-in templates directory */
23
+ const TEMPLATES_DIR = fileURLToPath(new URL('../templates/commands', import.meta.url));
24
24
 
25
25
  /**
26
26
  * Command mapping from Claude to Cursor
package/lib/workflow.mjs CHANGED
@@ -507,6 +507,81 @@ export function archiveWorkflow(projectRoot) {
507
507
  return workflow;
508
508
  }
509
509
 
510
+ /**
511
+ * Reset workflow to a specific step (partial reset)
512
+ * Keeps steps 1 to targetStep, resets steps after targetStep to pending
513
+ * @param {string} projectRoot
514
+ * @param {number} targetStep - 1-based step number to reset to (1-6)
515
+ * @returns {{workflow: Object, resetOutputs: Array<{index: number, stepNumber: number, output: string}>}}
516
+ */
517
+ export function resetToStep(projectRoot, targetStep) {
518
+ const workflow = readWorkflow(projectRoot);
519
+ if (!workflow || !workflow.currentWorkflow) {
520
+ throw new Error('工作流未初始化或无活跃工作流');
521
+ }
522
+
523
+ // Validate targetStep: must be 1-6 (step 7 is archive, cannot be a reset target)
524
+ if (targetStep < 1 || targetStep > 6) {
525
+ throw new Error(`无效的目标步骤: ${targetStep},有效范围为 1-6`);
526
+ }
527
+
528
+ // Check if target step is completed
529
+ const targetIndex = targetStep - 1;
530
+ const targetStepData = workflow.steps[targetIndex];
531
+ if (!targetStepData || targetStepData.status !== STATUS.COMPLETED) {
532
+ throw new Error(`目标步骤 ${targetStep} 尚未完成,无法回退到此步骤`);
533
+ }
534
+
535
+ // Handle quick mode: cannot reset to skipped steps
536
+ const mode = workflow.mode || MODE.FULL;
537
+ if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(targetStep)) {
538
+ throw new Error(`快速模式下步骤 ${targetStep} 被跳过,无法回退到此步骤`);
539
+ }
540
+
541
+ // Collect outputs from steps that will be reset (targetStep+1 to 7)
542
+ const resetOutputs = [];
543
+ for (let i = targetStep; i < STEPS.length; i++) {
544
+ const stepData = workflow.steps[i];
545
+ if (stepData && stepData.output && stepData.output !== '-') {
546
+ resetOutputs.push({
547
+ index: i,
548
+ stepNumber: i + 1,
549
+ output: stepData.output,
550
+ });
551
+ }
552
+ }
553
+
554
+ // Reset steps after targetStep
555
+ const now = formatLocalDateTime(new Date());
556
+ for (let i = targetStep; i < STEPS.length; i++) {
557
+ const stepNumber = i + 1;
558
+ // In quick mode, keep skipped steps as skipped
559
+ if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(stepNumber)) {
560
+ workflow.steps[i] = {
561
+ status: STATUS.SKIPPED,
562
+ output: null,
563
+ completedAt: workflow.steps[i]?.completedAt || now,
564
+ };
565
+ } else {
566
+ workflow.steps[i] = {
567
+ status: STATUS.PENDING,
568
+ output: null,
569
+ completedAt: null,
570
+ };
571
+ }
572
+ }
573
+
574
+ // Set currentStep to next step after targetStep
575
+ workflow.currentStep = getNextStep(targetStep, mode) || targetStep + 1;
576
+
577
+ writeWorkflow(projectRoot, workflow);
578
+
579
+ return {
580
+ workflow,
581
+ resetOutputs,
582
+ };
583
+ }
584
+
510
585
  /**
511
586
  * Get step info by number
512
587
  * @param {number} stepNumber
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localsummer/incspec",
3
- "version": "0.0.10",
3
+ "version": "0.1.1",
4
4
  "description": "面向 AI 编程助手的增量规范驱动开发工具",
5
5
  "bin": {
6
6
  "incspec": "index.mjs"
@@ -141,7 +141,8 @@ incspec archive [--yes] [--keep] # 步骤7
141
141
  # 验证与同步
142
142
  incspec validate / v [--strict] # 验证完整性
143
143
  incspec sync [--cursor|--claude|--all] [--global|--project] # IDE集成
144
- incspec reset [--archive|--clean] # 重置工作流状态
144
+ incspec reset # 完全重置工作流(归档所有产出)
145
+ incspec reset --to=<step> # 部分回退到指定步骤(保留1-N,重置N+1至7)
145
146
  ```
146
147
 
147
148
  ## 文件格式示例
@@ -210,6 +211,10 @@ baseline: home-baseline-v1
210
211
  | No baseline found | 先完成步骤 1 (analyze) |
211
212
  | Previous step not completed | 运行 `incspec status` 检查进度 |
212
213
  | Validation failed | 检查文件格式和编号序列 |
213
- | 工作流卡住 | 运行 `incspec reset` 重置状态 |
214
+ | 工作流卡住或状态异常 | `incspec reset` 完全重置,或 `incspec reset --to=N` 回退到步骤N |
215
+
216
+ **工作流重置**:
217
+ - 完全重置: `incspec reset` - 归档所有产出,回到初始状态
218
+ - 部分回退: `incspec reset --to=N` - 保留步骤1-N,重置N+1至7(示例:`--to=3` 保留1-3,重置4-7)
214
219
 
215
220
  记住: **基线是真相,增量是提案**。通过工作流周期保持它们同步。
@@ -101,6 +101,8 @@ description: 设计优先的前端功能增量编码工作流。适用于实现
101
101
  - `incspec status` - 查看工作流状态
102
102
  - `incspec list` - 列出规范文件
103
103
  - `incspec validate` - 验证项目健康状态
104
+ - `incspec reset` - 重置工作流到初始状态
105
+ - `incspec reset --to=N` - 回退到步骤N,保留1-N,重置N+1至7(示例:`--to=3` 保留1-3,重置4-7)
104
106
 
105
107
  ## 步骤详情
106
108
 
@@ -223,20 +225,36 @@ incspec archive <file> --keep # 复制而非移动
223
225
  - 步骤5后: 验证所有文件已正确创建/修改
224
226
  - 步骤7后: 确认工作区已清理
225
227
 
226
- **关键人工确认点:**
227
- - 完整模式: 步骤2后 + 步骤4后
228
- - 快速模式: 步骤2后 (需求确认后才能开始编码)
228
+ **工作流重置:**
229
+ - 完全重置: `incspec reset` - 归档所有产出,回到初始状态
230
+ - 部分回退: `incspec reset --to=N` - 保留步骤1-N,重置步骤N+1至7。示例:步骤5发现设计有误 → `--to=3` → 重做步骤4-7
231
+
232
+ **人工确认点:**
233
+
234
+ | 步骤 | 确认级别 | 快速模式 | 说明 |
235
+ |------|----------|----------|------|
236
+ | 1. 代码流程分析 | 自动 | 适用 | 技术性分析,错误会在后续暴露 |
237
+ | 2. 需求收集 | **必须确认** | 适用 | 需求理解错误会导致全部返工 |
238
+ | 3. 依赖收集 | 自动 | 跳过 | 遗漏会在编译阶段暴露 |
239
+ | 4. 增量设计 | **必须确认** | 跳过 | 架构方案需审批后才能编码 |
240
+ | 5. 代码应用 | 自动 | 适用 | 按蓝图执行,完成后报告结果 |
241
+ | 6. 基线合并 | 自动 | 适用 | 技术性操作,无决策 |
242
+ | 7. 归档 | 建议确认 | 适用 | 涉及文件移动,用户应知晓范围 |
243
+
244
+ - **完整模式**: 步骤2 + 步骤4必须确认
245
+ - **快速模式**: 仅步骤2必须确认(步骤3、4被跳过)
229
246
 
230
247
  ## 常见陷阱
231
248
 
232
- | 陷阱 | 后果 |
233
- |------|------|
234
- | 跳过代码流程分析 | 集成问题,返工 |
235
- | 接受模糊需求 | 编码时发现歧义 |
236
- | 遗漏依赖 | 导入错误,运行时失败 |
237
- | 编码先于设计 | 在不符合架构的实现上浪费精力 |
238
- | 跳过基线合并 | 对当前系统状态产生混淆 |
239
- | 忽略归档 | 工作区杂乱,历史丢失 |
249
+ | 陷阱 | 后果 | 解决方案 |
250
+ |------|------|----------|
251
+ | 跳过代码流程分析 | 集成问题,返工 | 始终从步骤1开始,建立准确基线 |
252
+ | 接受模糊需求 | 编码时发现歧义 | 步骤2必须确认,消除所有模糊术语 |
253
+ | 遗漏依赖 | 导入错误,运行时失败 | 完整模式严格执行步骤3 |
254
+ | 编码先于设计 | 在不符合架构的实现上浪费精力 | 完整模式严格执行步骤4,步骤4设计获批后才执行步骤5 |
255
+ | 跳过基线合并 | 对当前系统状态产生混淆 | 每次增量后必须执行步骤6 |
256
+ | 忽略归档 | 工作区杂乱,历史丢失 | 工作流完成后立即执行步骤7 |
257
+ | 工作流状态不一致 | CLI命令执行失败 | 使用 `incspec status` 检查状态,必要时使用 `incspec reset` 修复 |
240
258
 
241
259
  ## 参考资源
242
260