@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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * apply command - Step 5: Apply increment code
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import {
7
+ ensureInitialized,
8
+ INCSPEC_DIR,
9
+ DIRS,
10
+ readProjectConfig,
11
+ } from '../lib/config.mjs';
12
+ import {
13
+ readWorkflow,
14
+ updateStep,
15
+ STATUS,
16
+ } from '../lib/workflow.mjs';
17
+ import { listSpecs } from '../lib/spec.mjs';
18
+ import {
19
+ colors,
20
+ colorize,
21
+ print,
22
+ printSuccess,
23
+ printWarning,
24
+ printInfo,
25
+ prompt,
26
+ } from '../lib/terminal.mjs';
27
+
28
+ const STEP_NUMBER = 5;
29
+
30
+ /**
31
+ * Execute apply command
32
+ * @param {Object} ctx - Command context
33
+ */
34
+ export async function applyCommand(ctx) {
35
+ const { cwd, args, options } = ctx;
36
+
37
+ // Ensure initialized
38
+ const projectRoot = ensureInitialized(cwd);
39
+
40
+ // Get workflow state
41
+ const workflow = readWorkflow(projectRoot);
42
+
43
+ if (!workflow?.currentWorkflow) {
44
+ printWarning('没有活跃的工作流。请先运行 incspec analyze 开始新工作流。');
45
+ return;
46
+ }
47
+
48
+ // Get increment file
49
+ let incrementPath = args[0];
50
+ if (!incrementPath) {
51
+ const increments = listSpecs(projectRoot, 'increments');
52
+ if (increments.length > 0) {
53
+ const featureName = workflow.currentWorkflow.replace(/^analyze-/, '');
54
+ const matched = increments.find(spec => spec.name.startsWith(`${featureName}-increment-`));
55
+ if (matched) {
56
+ incrementPath = matched.path;
57
+ } else {
58
+ incrementPath = increments[0].path;
59
+ printWarning(`未找到与当前工作流匹配的增量文件,已使用最近文件: ${increments[0].name}`);
60
+ }
61
+ } else {
62
+ printWarning('未找到增量设计文件。请先运行步骤 4 (design)。');
63
+ return;
64
+ }
65
+ }
66
+
67
+ // Get source directory
68
+ const config = readProjectConfig(projectRoot);
69
+ const sourceDir = typeof options['source-dir'] === 'string'
70
+ ? options['source-dir']
71
+ : (config?.source_dir || 'src');
72
+
73
+ print('');
74
+ print(colorize('步骤 5: 应用代码变更', colors.bold, colors.cyan));
75
+ print(colorize('────────────────────', colors.dim));
76
+ print('');
77
+ print(colorize(`当前工作流: ${workflow.currentWorkflow}`, colors.dim));
78
+ print(colorize(`增量设计文件: ${incrementPath}`, colors.dim));
79
+ print(colorize(`源代码目录: ${sourceDir}`, colors.dim));
80
+ print('');
81
+
82
+ // Update workflow status
83
+ updateStep(projectRoot, STEP_NUMBER, STATUS.IN_PROGRESS);
84
+
85
+ print(colorize('使用说明:', colors.bold));
86
+ print('');
87
+ print(colorize('请在 Cursor 中运行以下命令:', colors.cyan));
88
+ print('');
89
+ print(colorize(` /incspec/inc-apply ${incrementPath}`, colors.bold, colors.white));
90
+ print('');
91
+ print(colorize('或使用 Claude Code 命令:', colors.cyan));
92
+ print('');
93
+ print(colorize(` /ai-increment:apply-increment-code ${incrementPath} ${path.join(projectRoot, sourceDir)}`, colors.bold, colors.white));
94
+ print('');
95
+ print(colorize('该命令将:', colors.dim));
96
+ print(colorize(' 1. 解析增量设计文件中的变更计划', colors.dim));
97
+ print(colorize(' 2. 按依赖顺序排序变更', colors.dim));
98
+ print(colorize(' 3. 执行代码新建/修改/删除', colors.dim));
99
+ print(colorize(' 4. 输出变更摘要', colors.dim));
100
+ print('');
101
+ printWarning('请在执行前仔细审查增量设计文件!');
102
+ print('');
103
+ printInfo(`完成后运行 'incspec status' 查看进度`);
104
+ print('');
105
+
106
+ // Handle --complete flag
107
+ if (options.complete) {
108
+ updateStep(projectRoot, STEP_NUMBER, STATUS.COMPLETED, '代码已应用');
109
+ printSuccess(`步骤 5 已标记为完成`);
110
+ }
111
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * archive command - Archive spec files
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 { archiveSpec, getSpecInfo } from '../lib/spec.mjs';
13
+ import { archiveWorkflow, readWorkflow, STATUS } from '../lib/workflow.mjs';
14
+ import {
15
+ colors,
16
+ colorize,
17
+ print,
18
+ printSuccess,
19
+ printWarning,
20
+ printError,
21
+ confirm,
22
+ } from '../lib/terminal.mjs';
23
+
24
+ const ARCHIVABLE_STEP_INDEXES = [0, 1, 2, 3, 5];
25
+
26
+ function escapeRegExp(value) {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+
30
+ function normalizeOutputName(outputFile) {
31
+ if (!outputFile || typeof outputFile !== 'string') {
32
+ return outputFile;
33
+ }
34
+
35
+ if (!/[\\/]/.test(outputFile)) {
36
+ return outputFile;
37
+ }
38
+
39
+ const parts = outputFile.split(/[/\\]+/);
40
+ return parts[parts.length - 1] || outputFile;
41
+ }
42
+
43
+ function collectArchivedFiles(projectRoot) {
44
+ const archiveRoot = path.join(projectRoot, INCSPEC_DIR, DIRS.archives);
45
+ if (!fs.existsSync(archiveRoot)) {
46
+ return [];
47
+ }
48
+
49
+ const files = [];
50
+
51
+ // Helper to collect .md files from a directory
52
+ const collectMdFiles = (dir) => {
53
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
54
+ entries.forEach(entry => {
55
+ const entryPath = path.join(dir, entry.name);
56
+ if (entry.isFile() && entry.name.endsWith('.md')) {
57
+ files.push({ name: entry.name, mtime: fs.statSync(entryPath).mtime });
58
+ }
59
+ });
60
+ };
61
+
62
+ // Traverse: archives/ -> YYYY-MM/ -> [module/] -> files
63
+ const topEntries = fs.readdirSync(archiveRoot, { withFileTypes: true });
64
+ topEntries.forEach(entry => {
65
+ const entryPath = path.join(archiveRoot, entry.name);
66
+ if (entry.isFile() && entry.name.endsWith('.md')) {
67
+ // Legacy: files directly in archives/
68
+ files.push({ name: entry.name, mtime: fs.statSync(entryPath).mtime });
69
+ return;
70
+ }
71
+
72
+ if (!entry.isDirectory()) {
73
+ return;
74
+ }
75
+
76
+ // Level 1: YYYY-MM directory
77
+ const subEntries = fs.readdirSync(entryPath, { withFileTypes: true });
78
+ subEntries.forEach(subEntry => {
79
+ const subPath = path.join(entryPath, subEntry.name);
80
+ if (subEntry.isFile() && subEntry.name.endsWith('.md')) {
81
+ // Legacy: files directly in YYYY-MM/
82
+ files.push({ name: subEntry.name, mtime: fs.statSync(subPath).mtime });
83
+ return;
84
+ }
85
+
86
+ if (subEntry.isDirectory()) {
87
+ // Level 2: module directory - collect files inside
88
+ collectMdFiles(subPath);
89
+ }
90
+ });
91
+ });
92
+
93
+ return files;
94
+ }
95
+
96
+ function parseWorkflowTime(value) {
97
+ if (!value || value === '-') {
98
+ return null;
99
+ }
100
+
101
+ const parsed = new Date(value.replace(' ', 'T'));
102
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
103
+ }
104
+
105
+ function isOutputArchived(archivedFiles, outputFile, completedAt) {
106
+ const ext = path.extname(outputFile);
107
+ const base = path.basename(outputFile, ext);
108
+ const pattern = new RegExp(`^${escapeRegExp(base)}(-copy\\d+)?${escapeRegExp(ext)}$`);
109
+ return archivedFiles.some(file => pattern.test(file.name) && file.mtime >= completedAt);
110
+ }
111
+
112
+ function getArchivableOutputs(workflow) {
113
+ if (!workflow?.currentWorkflow || !workflow.steps?.length) {
114
+ return null;
115
+ }
116
+
117
+ const outputs = [];
118
+ for (const index of ARCHIVABLE_STEP_INDEXES) {
119
+ const step = workflow.steps[index];
120
+ if (!step || step.status !== STATUS.COMPLETED || !step.output || step.output === '-') {
121
+ return null;
122
+ }
123
+
124
+ const completedAt = parseWorkflowTime(step.completedAt);
125
+ if (!completedAt) {
126
+ return null;
127
+ }
128
+
129
+ outputs.push({ name: normalizeOutputName(step.output), completedAt, index });
130
+ }
131
+
132
+ return outputs;
133
+ }
134
+
135
+ function getOutputDirForStepIndex(stepIndex) {
136
+ if (stepIndex === 0 || stepIndex === 5) {
137
+ return DIRS.baselines;
138
+ }
139
+
140
+ if (stepIndex === 1 || stepIndex === 2) {
141
+ return DIRS.requirements;
142
+ }
143
+
144
+ if (stepIndex === 3) {
145
+ return DIRS.increments;
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ function getOutputPath(projectRoot, output) {
152
+ const dir = getOutputDirForStepIndex(output.index);
153
+ if (!dir) {
154
+ return null;
155
+ }
156
+
157
+ return path.join(projectRoot, INCSPEC_DIR, dir, output.name);
158
+ }
159
+
160
+ function shouldArchiveWorkflow(projectRoot, workflow) {
161
+ const outputs = getArchivableOutputs(workflow);
162
+ if (!outputs || outputs.length === 0) {
163
+ return false;
164
+ }
165
+
166
+ const archivedFiles = collectArchivedFiles(projectRoot);
167
+ if (archivedFiles.length === 0) {
168
+ return false;
169
+ }
170
+
171
+ return outputs.every(output => isOutputArchived(archivedFiles, output.name, output.completedAt));
172
+ }
173
+
174
+ /**
175
+ * Execute archive command
176
+ * @param {Object} ctx - Command context
177
+ */
178
+ export async function archiveCommand(ctx) {
179
+ const { cwd, args, options } = ctx;
180
+
181
+ // Ensure initialized
182
+ const projectRoot = ensureInitialized(cwd);
183
+
184
+ print('');
185
+ print(colorize(' incspec 归档管理', colors.bold, colors.cyan));
186
+ print(colorize(' ────────────────', colors.dim));
187
+ print('');
188
+
189
+ const keepOriginal = options.keep || options.k;
190
+ const skipConfirm = options.yes || options.y;
191
+ const archiveCurrentWorkflow = options.workflow || !args[0];
192
+
193
+ if (archiveCurrentWorkflow) {
194
+ if (options.workflow && args[0]) {
195
+ printWarning('已忽略文件参数,按当前工作流归档。');
196
+ }
197
+
198
+ const workflow = readWorkflow(projectRoot);
199
+ if (!workflow?.currentWorkflow) {
200
+ printError('没有活跃工作流,无法归档当前工作流产出。');
201
+ print(colorize('如需归档单文件,请提供 file-path。', colors.dim));
202
+ return;
203
+ }
204
+
205
+ const outputs = getArchivableOutputs(workflow);
206
+ if (!outputs || outputs.length === 0) {
207
+ printError('当前工作流尚未产出完整的可归档文件。');
208
+ print(colorize('请先完成工作流产出,或使用 file-path 归档单文件。', colors.dim));
209
+ return;
210
+ }
211
+
212
+ const archivedFiles = collectArchivedFiles(projectRoot);
213
+ const targets = [];
214
+ const skipped = [];
215
+ const missing = [];
216
+
217
+ outputs.forEach(output => {
218
+ const outputPath = getOutputPath(projectRoot, output);
219
+ if (outputPath && fs.existsSync(outputPath)) {
220
+ targets.push({ path: outputPath, name: output.name });
221
+ return;
222
+ }
223
+
224
+ if (isOutputArchived(archivedFiles, output.name, output.completedAt)) {
225
+ skipped.push(output.name);
226
+ return;
227
+ }
228
+
229
+ missing.push(output.name);
230
+ });
231
+
232
+ if (missing.length > 0) {
233
+ printError(`以下产出文件未找到: ${missing.join(', ')}`);
234
+ return;
235
+ }
236
+
237
+ if (targets.length > 0) {
238
+ print(colorize('将归档以下文件:', colors.dim));
239
+ targets.forEach(target => print(colorize(` - ${target.name}`, colors.dim)));
240
+ print('');
241
+ }
242
+
243
+ if (skipped.length > 0) {
244
+ printWarning(`以下文件已归档,跳过: ${skipped.join(', ')}`);
245
+ }
246
+
247
+ if (targets.length > 0 && !skipConfirm) {
248
+ const action = keepOriginal ? '复制' : '移动';
249
+ const shouldProceed = await confirm(`确认${action}当前工作流的 ${targets.length} 个文件到归档目录?`);
250
+ if (!shouldProceed) {
251
+ print(colorize('已取消。', colors.dim));
252
+ return;
253
+ }
254
+ }
255
+
256
+ try {
257
+ const workflowModule = workflow.currentWorkflow;
258
+ for (const target of targets) {
259
+ const archivePath = archiveSpec(projectRoot, target.path, !keepOriginal, workflowModule);
260
+ printSuccess(`文件已${keepOriginal ? '复制' : '移动'}到: ${archivePath}`);
261
+ }
262
+
263
+ const refreshed = readWorkflow(projectRoot);
264
+ if (refreshed?.currentWorkflow && shouldArchiveWorkflow(projectRoot, refreshed)) {
265
+ const workflowName = refreshed.currentWorkflow;
266
+ archiveWorkflow(projectRoot);
267
+ printSuccess(`工作流 "${workflowName}" 已记录为归档。`);
268
+ }
269
+ } catch (e) {
270
+ printError(`归档失败: ${e.message}`);
271
+ }
272
+
273
+ return;
274
+ }
275
+
276
+ // Get file to archive
277
+ let filePath = args[0];
278
+
279
+ if (!filePath) {
280
+ printError('请提供要归档的文件路径。');
281
+ return;
282
+ }
283
+
284
+ // Validate file exists
285
+ if (!fs.existsSync(filePath)) {
286
+ // Try to resolve relative path
287
+ const possiblePaths = [
288
+ path.join(projectRoot, filePath),
289
+ path.join(projectRoot, INCSPEC_DIR, filePath),
290
+ path.join(projectRoot, INCSPEC_DIR, DIRS.baselines, filePath),
291
+ path.join(projectRoot, INCSPEC_DIR, DIRS.increments, filePath),
292
+ path.join(projectRoot, INCSPEC_DIR, DIRS.requirements, filePath),
293
+ ];
294
+
295
+ const found = possiblePaths.find(p => fs.existsSync(p));
296
+ if (found) {
297
+ filePath = found;
298
+ } else {
299
+ printError(`文件不存在: ${filePath}`);
300
+ return;
301
+ }
302
+ }
303
+
304
+ const fileName = path.basename(filePath);
305
+ const info = getSpecInfo(filePath);
306
+
307
+ print(colorize(`文件: ${fileName}`, colors.bold));
308
+ print(colorize(`类型: ${info.type}`, colors.dim));
309
+ if (info.version) {
310
+ print(colorize(`版本: v${info.version}`, colors.dim));
311
+ }
312
+ print('');
313
+
314
+ // Confirm
315
+ const action = keepOriginal ? '复制' : '移动';
316
+
317
+ if (!skipConfirm) {
318
+ const shouldProceed = await confirm(`确认${action}文件到归档目录?`);
319
+ if (!shouldProceed) {
320
+ print(colorize('已取消。', colors.dim));
321
+ return;
322
+ }
323
+ }
324
+
325
+ // Archive (default: move, with --keep: copy)
326
+ try {
327
+ const workflow = readWorkflow(projectRoot);
328
+ const workflowModule = workflow?.currentWorkflow || null;
329
+ const archivePath = archiveSpec(projectRoot, filePath, !keepOriginal, workflowModule);
330
+ printSuccess(`文件已${action}到: ${archivePath}`);
331
+
332
+ if (workflow?.currentWorkflow && shouldArchiveWorkflow(projectRoot, workflow)) {
333
+ const workflowName = workflow.currentWorkflow;
334
+ archiveWorkflow(projectRoot);
335
+ printSuccess(`工作流 "${workflowName}" 已记录为归档。`);
336
+ }
337
+ } catch (e) {
338
+ printError(`归档失败: ${e.message}`);
339
+ }
340
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * collect-dep command - Step 3: Collect UI dependencies
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import {
7
+ ensureInitialized,
8
+ INCSPEC_DIR,
9
+ DIRS,
10
+ } from '../lib/config.mjs';
11
+ import {
12
+ readWorkflow,
13
+ updateStep,
14
+ STATUS,
15
+ } from '../lib/workflow.mjs';
16
+ import {
17
+ colors,
18
+ colorize,
19
+ print,
20
+ printSuccess,
21
+ printWarning,
22
+ printInfo,
23
+ } from '../lib/terminal.mjs';
24
+
25
+ const STEP_NUMBER = 3;
26
+ const OUTPUT_FILE = 'ui-dependencies.md';
27
+
28
+ /**
29
+ * Execute collect-dep command
30
+ * @param {Object} ctx - Command context
31
+ */
32
+ export async function collectDepCommand(ctx) {
33
+ const { cwd, options } = ctx;
34
+
35
+ // Ensure initialized
36
+ const projectRoot = ensureInitialized(cwd);
37
+
38
+ // Get workflow state
39
+ const workflow = readWorkflow(projectRoot);
40
+
41
+ if (!workflow?.currentWorkflow) {
42
+ printWarning('没有活跃的工作流。请先运行 incspec analyze 开始新工作流。');
43
+ return;
44
+ }
45
+
46
+ const outputPath = path.join(INCSPEC_DIR, DIRS.requirements, OUTPUT_FILE);
47
+
48
+ print('');
49
+ print(colorize('步骤 3: UI 依赖采集', colors.bold, colors.cyan));
50
+ print(colorize('───────────────────', colors.dim));
51
+ print('');
52
+ print(colorize(`当前工作流: ${workflow.currentWorkflow}`, colors.dim));
53
+ print(colorize(`输出文件: ${outputPath}`, colors.dim));
54
+ print('');
55
+
56
+ // Update workflow status
57
+ updateStep(projectRoot, STEP_NUMBER, STATUS.IN_PROGRESS);
58
+
59
+ print(colorize('使用说明:', colors.bold));
60
+ print('');
61
+ print(colorize('请在 Cursor 中运行以下命令:', colors.cyan));
62
+ print('');
63
+ print(colorize(` /incspec/inc-collect-dep`, colors.bold, colors.white));
64
+ print('');
65
+ print(colorize('或使用 Claude Code 命令:', colors.cyan));
66
+ print('');
67
+ print(colorize(` /ai-increment:ui-dependency-collection ${path.join(projectRoot, INCSPEC_DIR, DIRS.requirements)}`, colors.bold, colors.white));
68
+ print('');
69
+ print(colorize('该命令将交互式采集 6 维度 UI 依赖:', colors.dim));
70
+ print(colorize(' - UI组件库 (Arco/Antd)', colors.dim));
71
+ print(colorize(' - 状态管理 (Store)', colors.dim));
72
+ print(colorize(' - API 交互', colors.dim));
73
+ print(colorize(' - 类型定义 (Type)', colors.dim));
74
+ print(colorize(' - 工具函数 (Utils)', colors.dim));
75
+ print(colorize(' - 定位上下文 (Context)', colors.dim));
76
+ print('');
77
+ printInfo(`完成后运行 'incspec status' 查看进度`);
78
+ print('');
79
+
80
+ // Handle --complete flag
81
+ if (options.complete) {
82
+ updateStep(projectRoot, STEP_NUMBER, STATUS.COMPLETED, OUTPUT_FILE);
83
+ printSuccess(`步骤 3 已标记为完成: ${OUTPUT_FILE}`);
84
+ }
85
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * collect-req command - Step 2: Collect structured requirements
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import {
7
+ ensureInitialized,
8
+ INCSPEC_DIR,
9
+ DIRS,
10
+ } from '../lib/config.mjs';
11
+ import {
12
+ readWorkflow,
13
+ updateStep,
14
+ STATUS,
15
+ } from '../lib/workflow.mjs';
16
+ import {
17
+ colors,
18
+ colorize,
19
+ print,
20
+ printSuccess,
21
+ printWarning,
22
+ printInfo,
23
+ } from '../lib/terminal.mjs';
24
+
25
+ const STEP_NUMBER = 2;
26
+ const OUTPUT_FILE = 'structured-requirements.md';
27
+
28
+ /**
29
+ * Execute collect-req command
30
+ * @param {Object} ctx - Command context
31
+ */
32
+ export async function collectReqCommand(ctx) {
33
+ const { cwd, options } = ctx;
34
+
35
+ // Ensure initialized
36
+ const projectRoot = ensureInitialized(cwd);
37
+
38
+ // Get workflow state
39
+ const workflow = readWorkflow(projectRoot);
40
+
41
+ if (!workflow?.currentWorkflow) {
42
+ printWarning('没有活跃的工作流。请先运行 incspec analyze 开始新工作流。');
43
+ return;
44
+ }
45
+
46
+ const outputPath = path.join(INCSPEC_DIR, DIRS.requirements, OUTPUT_FILE);
47
+
48
+ print('');
49
+ print(colorize('步骤 2: 结构化需求收集', colors.bold, colors.cyan));
50
+ print(colorize('─────────────────────────', colors.dim));
51
+ print('');
52
+ print(colorize(`当前工作流: ${workflow.currentWorkflow}`, colors.dim));
53
+ print(colorize(`输出文件: ${outputPath}`, colors.dim));
54
+ print('');
55
+
56
+ // Update workflow status
57
+ updateStep(projectRoot, STEP_NUMBER, STATUS.IN_PROGRESS);
58
+
59
+ print(colorize('使用说明:', colors.bold));
60
+ print('');
61
+ print(colorize('请在 Cursor 中运行以下命令:', colors.cyan));
62
+ print('');
63
+ print(colorize(` /incspec/inc-collect-req`, colors.bold, colors.white));
64
+ print('');
65
+ print(colorize('或使用 Claude Code 命令:', colors.cyan));
66
+ print('');
67
+ print(colorize(` /ai-increment:structured-requirements-collection ${path.join(projectRoot, INCSPEC_DIR, DIRS.requirements)}`, colors.bold, colors.white));
68
+ print('');
69
+ print(colorize('该命令将交互式收集需求,生成 5 列结构化表格:', colors.dim));
70
+ print(colorize(' | 新增/修改功能 | 涉及UI组件 | 触发条件 | 影响的核心状态 | 预期数据流向 |', colors.dim));
71
+ print('');
72
+ printInfo(`完成后运行 'incspec status' 查看进度`);
73
+ print('');
74
+
75
+ // Handle --complete flag
76
+ if (options.complete) {
77
+ updateStep(projectRoot, STEP_NUMBER, STATUS.COMPLETED, OUTPUT_FILE);
78
+ printSuccess(`步骤 2 已标记为完成: ${OUTPUT_FILE}`);
79
+ }
80
+ }