@localsummer/incspec 0.0.6 → 0.0.8

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 (38) hide show
  1. package/README.md +76 -15
  2. package/commands/analyze.mjs +28 -12
  3. package/commands/apply.mjs +78 -33
  4. package/commands/archive.mjs +25 -3
  5. package/commands/collect-dep.mjs +2 -2
  6. package/commands/collect-req.mjs +10 -2
  7. package/commands/design.mjs +2 -2
  8. package/commands/help.mjs +20 -11
  9. package/commands/list.mjs +2 -1
  10. package/commands/merge.mjs +64 -33
  11. package/commands/reset.mjs +166 -0
  12. package/commands/status.mjs +30 -7
  13. package/commands/sync.mjs +210 -0
  14. package/commands/update.mjs +2 -1
  15. package/index.mjs +13 -6
  16. package/lib/agents.mjs +1 -1
  17. package/lib/claude.mjs +144 -0
  18. package/lib/config.mjs +13 -10
  19. package/lib/cursor.mjs +20 -5
  20. package/lib/terminal.mjs +108 -0
  21. package/lib/workflow.mjs +123 -29
  22. package/package.json +1 -1
  23. package/templates/AGENTS.md +89 -36
  24. package/templates/INCSPEC_BLOCK.md +1 -1
  25. package/templates/WORKFLOW.md +1 -0
  26. package/templates/cursor-commands/analyze-codeflow.md +12 -1
  27. package/templates/cursor-commands/apply-increment-code.md +129 -1
  28. package/templates/cursor-commands/merge-to-baseline.md +87 -1
  29. package/templates/cursor-commands/structured-requirements-collection.md +6 -0
  30. package/templates/inc-spec-skill/SKILL.md +286 -0
  31. package/templates/inc-spec-skill/references/analyze-codeflow.md +368 -0
  32. package/templates/inc-spec-skill/references/analyze-increment-codeflow.md +246 -0
  33. package/templates/inc-spec-skill/references/apply-increment-code.md +520 -0
  34. package/templates/inc-spec-skill/references/inc-archive.md +278 -0
  35. package/templates/inc-spec-skill/references/merge-to-baseline.md +415 -0
  36. package/templates/inc-spec-skill/references/structured-requirements-collection.md +129 -0
  37. package/templates/inc-spec-skill/references/ui-dependency-collection.md +143 -0
  38. package/commands/cursor-sync.mjs +0 -116
package/lib/claude.mjs ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Claude Code integration utilities
3
+ * - Sync inc-spec-skill to global or project
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ /** Claude skills directory name */
12
+ const CLAUDE_SKILLS_DIR_NAME = 'skills';
13
+
14
+ /** Skill name */
15
+ const SKILL_NAME = 'inc-spec-skill';
16
+
17
+ /** Global Claude skills directory */
18
+ const GLOBAL_CLAUDE_SKILLS_DIR = path.join(os.homedir(), '.claude', CLAUDE_SKILLS_DIR_NAME);
19
+
20
+ /** Project Claude skills directory (relative to project root) */
21
+ const PROJECT_CLAUDE_SKILLS_DIR = path.join('.claude', CLAUDE_SKILLS_DIR_NAME);
22
+
23
+ /** Skill template source directory */
24
+ const SKILL_TEMPLATE_DIR = fileURLToPath(new URL('../templates/inc-spec-skill', import.meta.url));
25
+
26
+ /**
27
+ * Copy directory recursively
28
+ * @param {string} src - Source directory
29
+ * @param {string} dest - Destination directory
30
+ */
31
+ function copyDirRecursive(src, dest) {
32
+ if (!fs.existsSync(dest)) {
33
+ fs.mkdirSync(dest, { recursive: true });
34
+ }
35
+
36
+ const entries = fs.readdirSync(src, { withFileTypes: true });
37
+
38
+ for (const entry of entries) {
39
+ const srcPath = path.join(src, entry.name);
40
+ const destPath = path.join(dest, entry.name);
41
+
42
+ if (entry.isDirectory()) {
43
+ copyDirRecursive(srcPath, destPath);
44
+ } else {
45
+ fs.copyFileSync(srcPath, destPath);
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Count files in directory recursively
52
+ * @param {string} dir - Directory path
53
+ * @returns {number} File count
54
+ */
55
+ function countFiles(dir) {
56
+ let count = 0;
57
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
58
+
59
+ for (const entry of entries) {
60
+ if (entry.isDirectory()) {
61
+ count += countFiles(path.join(dir, entry.name));
62
+ } else {
63
+ count++;
64
+ }
65
+ }
66
+
67
+ return count;
68
+ }
69
+
70
+ /**
71
+ * Sync skill to global Claude skills directory
72
+ * @returns {{count: number, targetDir: string}}
73
+ */
74
+ export function syncToGlobalClaude() {
75
+ const targetDir = path.join(GLOBAL_CLAUDE_SKILLS_DIR, SKILL_NAME);
76
+
77
+ // Remove existing if present
78
+ if (fs.existsSync(targetDir)) {
79
+ fs.rmSync(targetDir, { recursive: true, force: true });
80
+ }
81
+
82
+ // Copy skill template
83
+ copyDirRecursive(SKILL_TEMPLATE_DIR, targetDir);
84
+
85
+ const count = countFiles(targetDir);
86
+
87
+ return { count, targetDir };
88
+ }
89
+
90
+ /**
91
+ * Sync skill to project Claude skills directory
92
+ * @param {string} projectRoot - Project root path
93
+ * @returns {{count: number, targetDir: string}}
94
+ */
95
+ export function syncToProjectClaude(projectRoot) {
96
+ const targetDir = path.join(projectRoot, PROJECT_CLAUDE_SKILLS_DIR, SKILL_NAME);
97
+
98
+ // Remove existing if present
99
+ if (fs.existsSync(targetDir)) {
100
+ fs.rmSync(targetDir, { recursive: true, force: true });
101
+ }
102
+
103
+ // Copy skill template
104
+ copyDirRecursive(SKILL_TEMPLATE_DIR, targetDir);
105
+
106
+ const count = countFiles(targetDir);
107
+
108
+ return { count, targetDir };
109
+ }
110
+
111
+ /**
112
+ * Check if Claude skill exists
113
+ * @param {string} projectRoot - Optional project root
114
+ * @returns {{project: boolean, global: boolean}}
115
+ */
116
+ export function checkClaudeSkill(projectRoot = null) {
117
+ const globalDir = path.join(GLOBAL_CLAUDE_SKILLS_DIR, SKILL_NAME);
118
+ const result = {
119
+ global: fs.existsSync(globalDir) && fs.readdirSync(globalDir).length > 0,
120
+ project: false,
121
+ };
122
+
123
+ if (projectRoot) {
124
+ const projectDir = path.join(projectRoot, PROJECT_CLAUDE_SKILLS_DIR, SKILL_NAME);
125
+ result.project = fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0;
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Get skill template info
133
+ * @returns {{fileCount: number, hasSkillMd: boolean, hasReferences: boolean}}
134
+ */
135
+ export function getSkillTemplateInfo() {
136
+ const skillMdPath = path.join(SKILL_TEMPLATE_DIR, 'SKILL.md');
137
+ const referencesDir = path.join(SKILL_TEMPLATE_DIR, 'references');
138
+
139
+ return {
140
+ fileCount: countFiles(SKILL_TEMPLATE_DIR),
141
+ hasSkillMd: fs.existsSync(skillMdPath),
142
+ hasReferences: fs.existsSync(referencesDir) && fs.readdirSync(referencesDir).length > 0,
143
+ };
144
+ }
package/lib/config.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import { formatLocalDate } from './terminal.mjs';
11
12
 
12
13
  /** incspec directory name */
13
14
  export const INCSPEC_DIR = 'incspec';
@@ -51,7 +52,9 @@ export function findProjectRoot(startDir) {
51
52
 
52
53
  while (currentDir !== root) {
53
54
  const incspecPath = path.join(currentDir, INCSPEC_DIR);
54
- if (fs.existsSync(incspecPath) && fs.statSync(incspecPath).isDirectory()) {
55
+ const projectFile = path.join(incspecPath, FILES.project);
56
+ // 检查 project.md 是否存在,避免 macOS 大小写不敏感导致的误判
57
+ if (fs.existsSync(projectFile)) {
55
58
  return currentDir;
56
59
  }
57
60
  currentDir = path.dirname(currentDir);
@@ -197,23 +200,23 @@ export function writeProjectConfig(projectRoot, config) {
197
200
  */
198
201
  function generateProjectContent(config) {
199
202
  const templatePath = path.join(getTemplatesDir(), 'project.md');
200
-
203
+
201
204
  if (fs.existsSync(templatePath)) {
202
205
  let content = fs.readFileSync(templatePath, 'utf-8');
203
-
206
+
204
207
  // Replace variables
205
208
  content = content.replace(/\{\{name\}\}/g, config.name || '');
206
209
  content = content.replace(/\{\{version\}\}/g, config.version || '1.0.0');
207
210
  content = content.replace(/\{\{source_dir\}\}/g, config.source_dir || 'src');
208
- content = content.replace(/\{\{created_at\}\}/g, config.created_at || new Date().toISOString().split('T')[0]);
209
-
211
+ content = content.replace(/\{\{created_at\}\}/g, config.created_at || formatLocalDate(new Date()));
212
+
210
213
  // Handle tech_stack array
211
214
  const techStackLines = (config.tech_stack || []).map(item => ` - ${item}`).join('\n');
212
215
  content = content.replace(/\{\{tech_stack\}\}/g, techStackLines);
213
-
216
+
214
217
  return content;
215
218
  }
216
-
219
+
217
220
  // Fallback to hardcoded content if template not found
218
221
  return generateFallbackProjectContent(config);
219
222
  }
@@ -234,7 +237,7 @@ function generateFallbackProjectContent(config) {
234
237
  * @returns {Object}
235
238
  */
236
239
  export function getDefaultConfig(options = {}) {
237
- const now = new Date().toISOString().split('T')[0];
240
+ const now = formatLocalDate(new Date());
238
241
 
239
242
  return {
240
243
  name: options.name || path.basename(process.cwd()),
@@ -318,7 +321,7 @@ function writeAgentsFile(projectRoot) {
318
321
  ## 快速开始
319
322
 
320
323
  1. 运行 \`incspec status\` 查看当前工作流状态
321
- 2. 按顺序执行6步工作流: analyze → collect-req → collect-dep → design → apply → merge
324
+ 2. 按顺序执行7步工作流: analyze → collect-req → collect-dep → design → apply → merge → archive
322
325
  3. 使用 \`incspec help\` 获取更多帮助
323
326
  `;
324
327
  fs.writeFileSync(agentsPath, fallbackContent, 'utf-8');
@@ -332,7 +335,7 @@ function writeAgentsFile(projectRoot) {
332
335
  */
333
336
  export function ensureInitialized(cwd) {
334
337
  const projectRoot = findProjectRoot(cwd);
335
-
338
+
336
339
  if (!projectRoot || !isInitialized(projectRoot)) {
337
340
  throw new Error(
338
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 cursor-sync # 同步 Cursor 命令
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/terminal.mjs CHANGED
@@ -241,6 +241,88 @@ export async function select({ message, choices }) {
241
241
  });
242
242
  }
243
243
 
244
+ /**
245
+ * Interactive checkbox (multi-select) prompt
246
+ * @param {Object} options
247
+ * @param {string} options.message
248
+ * @param {Array<{name: string, value: any, checked?: boolean}>} options.choices
249
+ * @returns {Promise<any[]>}
250
+ */
251
+ export async function checkbox({ message, choices }) {
252
+ return new Promise((resolve) => {
253
+ let cursor = 0;
254
+ const selected = choices.map(c => c.checked || false);
255
+
256
+ const rl = readline.createInterface({
257
+ input: process.stdin,
258
+ output: process.stdout,
259
+ });
260
+
261
+ if (process.stdin.isTTY) {
262
+ process.stdin.setRawMode(true);
263
+ }
264
+ readline.emitKeypressEvents(process.stdin, rl);
265
+
266
+ const render = () => {
267
+ process.stdout.write('\x1b[2J\x1b[H');
268
+
269
+ console.log(colorize(message, colors.bold, colors.cyan));
270
+ console.log(colorize('(space: toggle, a: toggle all, enter: confirm)', colors.dim));
271
+ console.log();
272
+
273
+ choices.forEach((choice, index) => {
274
+ const isCursor = cursor === index;
275
+ const isSelected = selected[index];
276
+ const pointer = isCursor ? colorize('>', colors.cyan) : ' ';
277
+ const check = isSelected ? colorize('[x]', colors.green) : '[ ]';
278
+ const name = isCursor ? colorize(choice.name, colors.bold) : choice.name;
279
+
280
+ console.log(`${pointer} ${check} ${name}`);
281
+ });
282
+ };
283
+
284
+ const cleanup = () => {
285
+ if (process.stdin.isTTY) {
286
+ process.stdin.setRawMode(false);
287
+ }
288
+ rl.close();
289
+ };
290
+
291
+ const handleKeypress = (str, key) => {
292
+ if (!key) return;
293
+
294
+ if (key.name === 'up' || key.name === 'k') {
295
+ cursor = cursor > 0 ? cursor - 1 : choices.length - 1;
296
+ render();
297
+ } else if (key.name === 'down' || key.name === 'j') {
298
+ cursor = cursor < choices.length - 1 ? cursor + 1 : 0;
299
+ render();
300
+ } else if (key.name === 'space') {
301
+ selected[cursor] = !selected[cursor];
302
+ render();
303
+ } else if (str === 'a') {
304
+ const allSelected = selected.every(s => s);
305
+ selected.fill(!allSelected);
306
+ render();
307
+ } else if (key.name === 'return') {
308
+ cleanup();
309
+ process.stdin.removeListener('keypress', handleKeypress);
310
+ process.stdout.write('\x1b[2J\x1b[H');
311
+ const result = choices.filter((_, i) => selected[i]).map(c => c.value);
312
+ resolve(result);
313
+ } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
314
+ cleanup();
315
+ process.stdin.removeListener('keypress', handleKeypress);
316
+ process.stdout.write('\x1b[2J\x1b[H');
317
+ process.exit(0);
318
+ }
319
+ };
320
+
321
+ process.stdin.on('keypress', handleKeypress);
322
+ render();
323
+ });
324
+ }
325
+
244
326
  /**
245
327
  * Format a table for terminal output
246
328
  * @param {string[]} headers
@@ -290,3 +372,29 @@ export function spinner(message) {
290
372
  }
291
373
  };
292
374
  }
375
+
376
+ /**
377
+ * Format date to local datetime string
378
+ * @param {Date} date - Date object
379
+ * @returns {string} Format: YYYY-MM-DD HH:mm
380
+ */
381
+ export function formatLocalDateTime(date) {
382
+ const year = date.getFullYear();
383
+ const month = String(date.getMonth() + 1).padStart(2, '0');
384
+ const day = String(date.getDate()).padStart(2, '0');
385
+ const hours = String(date.getHours()).padStart(2, '0');
386
+ const minutes = String(date.getMinutes()).padStart(2, '0');
387
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
388
+ }
389
+
390
+ /**
391
+ * Format date to local date string
392
+ * @param {Date} date - Date object
393
+ * @returns {string} Format: YYYY-MM-DD
394
+ */
395
+ export function formatLocalDate(date) {
396
+ const year = date.getFullYear();
397
+ const month = String(date.getMonth() + 1).padStart(2, '0');
398
+ const day = String(date.getDate()).padStart(2, '0');
399
+ return `${year}-${month}-${day}`;
400
+ }