@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/lib/config.mjs ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Configuration management for incspec
3
+ * - Find project root
4
+ * - Read/write project.md
5
+ * - Default configurations
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ /** incspec directory name */
13
+ export const INCSPEC_DIR = 'incspec';
14
+
15
+ /** Default subdirectories */
16
+ export const DIRS = {
17
+ baselines: 'baselines',
18
+ requirements: 'requirements',
19
+ increments: 'increments',
20
+ archives: 'archives',
21
+ };
22
+
23
+ /** Core files */
24
+ export const FILES = {
25
+ project: 'project.md',
26
+ workflow: 'WORKFLOW.md',
27
+ agents: 'AGENTS.md',
28
+ };
29
+
30
+ /**
31
+ * Get the templates directory path
32
+ * @returns {string}
33
+ */
34
+ export function getTemplatesDir() {
35
+ // Get the directory of this module file
36
+ const currentFileUrl = import.meta.url;
37
+ const currentFilePath = fileURLToPath(currentFileUrl);
38
+ const libDir = path.dirname(currentFilePath);
39
+ const cliRoot = path.dirname(libDir);
40
+ return path.join(cliRoot, 'templates');
41
+ }
42
+
43
+ /**
44
+ * Find project root by looking for incspec/ directory
45
+ * @param {string} startDir - Starting directory
46
+ * @returns {string|null} Project root path or null if not found
47
+ */
48
+ export function findProjectRoot(startDir) {
49
+ let currentDir = path.resolve(startDir);
50
+ const root = path.parse(currentDir).root;
51
+
52
+ while (currentDir !== root) {
53
+ const incspecPath = path.join(currentDir, INCSPEC_DIR);
54
+ if (fs.existsSync(incspecPath) && fs.statSync(incspecPath).isDirectory()) {
55
+ return currentDir;
56
+ }
57
+ currentDir = path.dirname(currentDir);
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Get incspec directory path
65
+ * @param {string} projectRoot
66
+ * @returns {string}
67
+ */
68
+ export function getIncspecDir(projectRoot) {
69
+ return path.join(projectRoot, INCSPEC_DIR);
70
+ }
71
+
72
+ /**
73
+ * Check if incspec is initialized in directory
74
+ * @param {string} cwd
75
+ * @returns {boolean}
76
+ */
77
+ export function isInitialized(cwd) {
78
+ const incspecDir = path.join(cwd, INCSPEC_DIR);
79
+ const projectFile = path.join(incspecDir, FILES.project);
80
+ return fs.existsSync(incspecDir) && fs.existsSync(projectFile);
81
+ }
82
+
83
+ /**
84
+ * Parse frontmatter from markdown content
85
+ * @param {string} content
86
+ * @returns {{frontmatter: Object, body: string}}
87
+ */
88
+ export function parseFrontmatter(content) {
89
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
90
+ const match = content.match(frontmatterRegex);
91
+
92
+ if (!match) {
93
+ return { frontmatter: {}, body: content };
94
+ }
95
+
96
+ const frontmatterStr = match[1];
97
+ const body = match[2];
98
+
99
+ // Simple YAML-like parsing
100
+ const frontmatter = {};
101
+ const lines = frontmatterStr.split('\n');
102
+ let currentKey = null;
103
+ let currentArray = null;
104
+
105
+ for (const line of lines) {
106
+ const trimmed = line.trim();
107
+ if (!trimmed) continue;
108
+
109
+ // Array item
110
+ if (trimmed.startsWith('- ')) {
111
+ if (currentArray !== null) {
112
+ currentArray.push(trimmed.slice(2).trim());
113
+ }
114
+ continue;
115
+ }
116
+
117
+ // Key-value pair
118
+ const colonIndex = trimmed.indexOf(':');
119
+ if (colonIndex !== -1) {
120
+ const key = trimmed.slice(0, colonIndex).trim();
121
+ const value = trimmed.slice(colonIndex + 1).trim();
122
+
123
+ if (value === '') {
124
+ // Start of array or nested object
125
+ frontmatter[key] = [];
126
+ currentKey = key;
127
+ currentArray = frontmatter[key];
128
+ } else {
129
+ frontmatter[key] = value;
130
+ currentKey = key;
131
+ currentArray = null;
132
+ }
133
+ }
134
+ }
135
+
136
+ return { frontmatter, body };
137
+ }
138
+
139
+ /**
140
+ * Serialize frontmatter to YAML-like string
141
+ * @param {Object} frontmatter
142
+ * @returns {string}
143
+ */
144
+ export function serializeFrontmatter(frontmatter) {
145
+ const lines = ['---'];
146
+
147
+ for (const [key, value] of Object.entries(frontmatter)) {
148
+ if (Array.isArray(value)) {
149
+ lines.push(`${key}:`);
150
+ value.forEach(item => lines.push(` - ${item}`));
151
+ } else {
152
+ lines.push(`${key}: ${value}`);
153
+ }
154
+ }
155
+
156
+ lines.push('---');
157
+ return lines.join('\n');
158
+ }
159
+
160
+ /**
161
+ * Read project configuration
162
+ * @param {string} projectRoot
163
+ * @returns {Object|null}
164
+ */
165
+ export function readProjectConfig(projectRoot) {
166
+ const projectPath = path.join(projectRoot, INCSPEC_DIR, FILES.project);
167
+
168
+ if (!fs.existsSync(projectPath)) {
169
+ return null;
170
+ }
171
+
172
+ const content = fs.readFileSync(projectPath, 'utf-8');
173
+ const { frontmatter, body } = parseFrontmatter(content);
174
+
175
+ return {
176
+ ...frontmatter,
177
+ _body: body,
178
+ _path: projectPath,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Write project configuration
184
+ * @param {string} projectRoot
185
+ * @param {Object} config
186
+ */
187
+ export function writeProjectConfig(projectRoot, config) {
188
+ const projectPath = path.join(projectRoot, INCSPEC_DIR, FILES.project);
189
+ const content = generateProjectContent(config);
190
+ fs.writeFileSync(projectPath, content, 'utf-8');
191
+ }
192
+
193
+ /**
194
+ * Generate project.md content from template
195
+ * @param {Object} config
196
+ * @returns {string}
197
+ */
198
+ function generateProjectContent(config) {
199
+ const templatePath = path.join(getTemplatesDir(), 'project.md');
200
+
201
+ if (fs.existsSync(templatePath)) {
202
+ let content = fs.readFileSync(templatePath, 'utf-8');
203
+
204
+ // Replace variables
205
+ content = content.replace(/\{\{name\}\}/g, config.name || '');
206
+ content = content.replace(/\{\{version\}\}/g, config.version || '1.0.0');
207
+ 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
+
210
+ // Handle tech_stack array
211
+ const techStackLines = (config.tech_stack || []).map(item => ` - ${item}`).join('\n');
212
+ content = content.replace(/\{\{tech_stack\}\}/g, techStackLines);
213
+
214
+ return content;
215
+ }
216
+
217
+ // Fallback to hardcoded content if template not found
218
+ return generateFallbackProjectContent(config);
219
+ }
220
+
221
+ /**
222
+ * Fallback project content generator
223
+ * @param {Object} config
224
+ * @returns {string}
225
+ */
226
+ function generateFallbackProjectContent(config) {
227
+ const { _body, _path, ...frontmatter } = config;
228
+ return serializeFrontmatter(frontmatter) + '\n' + getDefaultProjectBody();
229
+ }
230
+
231
+ /**
232
+ * Get default project configuration
233
+ * @param {Object} options
234
+ * @returns {Object}
235
+ */
236
+ export function getDefaultConfig(options = {}) {
237
+ const now = new Date().toISOString().split('T')[0];
238
+
239
+ return {
240
+ name: options.name || path.basename(process.cwd()),
241
+ version: '1.0.0',
242
+ tech_stack: options.tech_stack || [],
243
+ source_dir: options.source_dir || 'src',
244
+ created_at: now,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Get default project.md body content
250
+ * @returns {string}
251
+ */
252
+ function getDefaultProjectBody() {
253
+ return `
254
+ # Project Overview
255
+
256
+ ## 模块列表
257
+
258
+ | 模块名 | 路径 | 描述 | 当前基线版本 |
259
+ |--------|------|------|-------------|
260
+ | - | - | - | - |
261
+
262
+ ## 技术约束
263
+
264
+ - [添加项目特定的技术约束]
265
+
266
+ ## 备注
267
+
268
+ [项目特定的说明和注意事项]
269
+ `;
270
+ }
271
+
272
+ /**
273
+ * Create incspec directory structure
274
+ * @param {string} projectRoot
275
+ * @param {Object} config
276
+ */
277
+ export function createIncspecStructure(projectRoot, config) {
278
+ const incspecDir = path.join(projectRoot, INCSPEC_DIR);
279
+
280
+ // Create main directory
281
+ if (!fs.existsSync(incspecDir)) {
282
+ fs.mkdirSync(incspecDir, { recursive: true });
283
+ }
284
+
285
+ // Create subdirectories
286
+ for (const dir of Object.values(DIRS)) {
287
+ const dirPath = path.join(incspecDir, dir);
288
+ if (!fs.existsSync(dirPath)) {
289
+ fs.mkdirSync(dirPath, { recursive: true });
290
+ }
291
+ }
292
+
293
+ // Write project.md
294
+ writeProjectConfig(projectRoot, config);
295
+
296
+ // Write AGENTS.md from template
297
+ writeAgentsFile(projectRoot);
298
+ }
299
+
300
+ /**
301
+ * Write AGENTS.md file from template
302
+ * @param {string} projectRoot
303
+ */
304
+ function writeAgentsFile(projectRoot) {
305
+ const agentsPath = path.join(projectRoot, INCSPEC_DIR, FILES.agents);
306
+ const templatePath = path.join(getTemplatesDir(), 'AGENTS.md');
307
+
308
+ // Read template content
309
+ if (fs.existsSync(templatePath)) {
310
+ const content = fs.readFileSync(templatePath, 'utf-8');
311
+ fs.writeFileSync(agentsPath, content, 'utf-8');
312
+ } else {
313
+ // Fallback: write a minimal AGENTS.md if template not found
314
+ const fallbackContent = `# IncSpec 使用指南
315
+
316
+ 请参阅 incspec-cli 文档获取完整的 AI 编码助手使用指南。
317
+
318
+ ## 快速开始
319
+
320
+ 1. 运行 \`incspec status\` 查看当前工作流状态
321
+ 2. 按顺序执行6步工作流: analyze → collect-req → collect-dep → design → apply → merge
322
+ 3. 使用 \`incspec help\` 获取更多帮助
323
+ `;
324
+ fs.writeFileSync(agentsPath, fallbackContent, 'utf-8');
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Ensure incspec is initialized, throw error if not
330
+ * @param {string} cwd
331
+ * @returns {string} Project root path
332
+ */
333
+ export function ensureInitialized(cwd) {
334
+ const projectRoot = findProjectRoot(cwd);
335
+
336
+ if (!projectRoot || !isInitialized(projectRoot)) {
337
+ throw new Error(
338
+ `incspec 未初始化。请先运行 'incspec init' 初始化项目。`
339
+ );
340
+ }
341
+
342
+ return projectRoot;
343
+ }
package/lib/cursor.mjs ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Cursor integration utilities
3
+ * - Generate Cursor slash commands
4
+ * - Sync to project or global
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { fileURLToPath } from 'url';
11
+ import { INCSPEC_DIR, DIRS } from './config.mjs';
12
+
13
+ /** Cursor commands directory */
14
+ const CURSOR_COMMANDS_DIR = '.cursor/commands/incspec';
15
+
16
+ /** Global Cursor commands directory */
17
+ const GLOBAL_CURSOR_DIR = path.join(os.homedir(), '.cursor', 'commands', 'incspec');
18
+
19
+ /** Claude commands source directory (user local) */
20
+ const CLAUDE_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands', 'ai-increment');
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));
24
+
25
+ /**
26
+ * Command mapping from Claude to Cursor
27
+ */
28
+ const COMMAND_MAP = [
29
+ {
30
+ source: 'analyze-codeflow.md',
31
+ target: 'inc-analyze.md',
32
+ step: 1,
33
+ label: '步骤1: 分析代码流程',
34
+ description: '[incspec] 分析代码工作流,生成API时序图和依赖关系基线快照',
35
+ },
36
+ {
37
+ source: 'structured-requirements-collection.md',
38
+ target: 'inc-collect-req.md',
39
+ step: 2,
40
+ label: '步骤2: 结构化需求收集',
41
+ description: '[incspec] 交互式收集需求,生成5列结构化表格',
42
+ },
43
+ {
44
+ source: 'ui-dependency-collection.md',
45
+ target: 'inc-collect-dep.md',
46
+ step: 3,
47
+ label: '步骤3: UI依赖采集',
48
+ description: '[incspec] 交互式采集6维度UI依赖信息',
49
+ },
50
+ {
51
+ source: 'analyze-increment-codeflow.md',
52
+ target: 'inc-design.md',
53
+ step: 4,
54
+ label: '步骤4: 增量设计',
55
+ description: '[incspec] 基于需求和依赖生成增量设计蓝图(7大模块)',
56
+ },
57
+ {
58
+ source: 'apply-increment-code.md',
59
+ target: 'inc-apply.md',
60
+ step: 5,
61
+ label: '步骤5: 应用代码变更',
62
+ description: '[incspec] 根据增量设计执行代码变更',
63
+ },
64
+ {
65
+ source: 'merge-to-baseline.md',
66
+ target: 'inc-merge.md',
67
+ step: 6,
68
+ label: '步骤6: 合并到基线',
69
+ description: '[incspec] 将增量融合为新的代码流基线快照',
70
+ },
71
+ ];
72
+
73
+ /**
74
+ * Generate Cursor command content
75
+ * Templates already contain complete Cursor command format with CLI sync instructions,
76
+ * so we just pass through the source content directly.
77
+ * @param {Object} cmd - Command definition
78
+ * @param {string} sourceContent - Original template content (already complete)
79
+ * @param {string} projectRoot - Project root path (optional, unused)
80
+ * @returns {string}
81
+ */
82
+ function generateCursorCommand(cmd, sourceContent, projectRoot = null) {
83
+ // Templates are already complete Cursor commands, pass through directly
84
+ return sourceContent;
85
+ }
86
+
87
+ /**
88
+ * Get source file path with fallback to user local commands
89
+ * @param {string} fileName - Source file name
90
+ * @returns {string|null} Path to source file or null if not found
91
+ */
92
+ function getSourcePath(fileName) {
93
+ // Try built-in templates first (preferred)
94
+ const templatePath = path.join(TEMPLATES_DIR, fileName);
95
+ if (fs.existsSync(templatePath)) {
96
+ return templatePath;
97
+ }
98
+
99
+ // Fallback to user local Claude commands
100
+ const claudePath = path.join(CLAUDE_COMMANDS_DIR, fileName);
101
+ if (fs.existsSync(claudePath)) {
102
+ return claudePath;
103
+ }
104
+
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Generate all Cursor commands
110
+ * @param {string} projectRoot - Optional project root for customization
111
+ * @returns {Array<{name: string, content: string}>}
112
+ */
113
+ export function generateCursorCommands(projectRoot = null) {
114
+ const commands = [];
115
+
116
+ for (const cmd of COMMAND_MAP) {
117
+ const sourcePath = getSourcePath(cmd.source);
118
+
119
+ let content;
120
+ if (sourcePath) {
121
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
122
+ content = generateCursorCommand(cmd, sourceContent, projectRoot);
123
+ } else {
124
+ // Generate placeholder if source doesn't exist
125
+ content = `---
126
+ description: ${cmd.description}
127
+ ---
128
+
129
+ # ${cmd.label}
130
+
131
+ > 源命令文件不存在: ${cmd.source}
132
+ > 请确保已正确安装 incspec-cli 或 ai-incremental-coding 技能。
133
+
134
+ 请参考 incspec 文档手动执行此步骤。
135
+ `;
136
+ }
137
+
138
+ commands.push({
139
+ name: cmd.target,
140
+ content,
141
+ });
142
+ }
143
+
144
+ // Add utility commands
145
+ commands.push({
146
+ name: 'inc-status.md',
147
+ content: `---
148
+ description: [incspec] 查看当前工作流状态
149
+ ---
150
+
151
+ # 查看工作流状态
152
+
153
+ 请运行以下命令查看当前工作流状态:
154
+
155
+ \`\`\`bash
156
+ incspec status
157
+ \`\`\`
158
+
159
+ 或直接读取状态文件:
160
+
161
+ \`\`\`bash
162
+ cat ${INCSPEC_DIR}/WORKFLOW.md
163
+ \`\`\`
164
+ `,
165
+ });
166
+
167
+ commands.push({
168
+ name: 'inc-help.md',
169
+ content: `---
170
+ description: [incspec] 显示帮助信息
171
+ ---
172
+
173
+ # incspec 帮助
174
+
175
+ ## 工作流步骤
176
+
177
+ 1. \`/incspec/inc-analyze\` - 分析代码流程,生成基线快照
178
+ 2. \`/incspec/inc-collect-req\` - 收集结构化需求
179
+ 3. \`/incspec/inc-collect-dep\` - 采集UI依赖
180
+ 4. \`/incspec/inc-design\` - 生成增量设计蓝图
181
+ 5. \`/incspec/inc-apply\` - 应用代码变更
182
+ 6. \`/incspec/inc-merge\` - 合并到新基线
183
+
184
+ ## 辅助命令
185
+
186
+ - \`/incspec/inc-archive\` - 归档规范文件到 archives 目录
187
+ - \`/incspec/inc-status\` - 查看当前工作流状态
188
+ - \`/incspec/inc-help\` - 显示帮助信息
189
+
190
+ ## CLI 命令
191
+
192
+ \`\`\`bash
193
+ incspec init # 初始化项目
194
+ incspec status # 查看工作流状态
195
+ incspec list # 列出规范文件
196
+ incspec validate # 验证规范完整性
197
+ incspec cursor-sync # 同步 Cursor 命令
198
+ incspec help # 显示帮助
199
+ \`\`\`
200
+
201
+ ## 目录结构
202
+
203
+ \`\`\`
204
+ ${INCSPEC_DIR}/
205
+ ├── project.md # 项目配置
206
+ ├── WORKFLOW.md # 工作流状态
207
+ ├── baselines/ # 基线快照
208
+ ├── requirements/ # 需求文档
209
+ ├── increments/ # 增量设计
210
+ └── archives/ # 历史归档 (YYYY-MM/{module}/)
211
+ \`\`\`
212
+ `,
213
+ });
214
+
215
+ // Add archive command from template
216
+ const archiveSourcePath = getSourcePath('inc-archive.md');
217
+ if (archiveSourcePath) {
218
+ const archiveContent = fs.readFileSync(archiveSourcePath, 'utf-8');
219
+ commands.push({
220
+ name: 'inc-archive.md',
221
+ content: archiveContent,
222
+ });
223
+ } else {
224
+ commands.push({
225
+ name: 'inc-archive.md',
226
+ content: `---
227
+ description: [incspec] 归档规范文件到 archives 目录
228
+ ---
229
+
230
+ # 归档规范文件
231
+
232
+ 请运行以下命令归档规范文件:
233
+
234
+ \`\`\`bash
235
+ # 归档当前工作流全部产出文件(默认模式)
236
+ incspec archive --yes
237
+
238
+ # 归档指定文件(默认移动模式,删除原文件)
239
+ incspec archive <file-path> --yes
240
+
241
+ # 归档并保留原文件(复制模式)
242
+ incspec archive <file-path> --keep --yes
243
+ \`\`\`
244
+ `,
245
+ });
246
+ }
247
+
248
+ return commands;
249
+ }
250
+
251
+ /**
252
+ * Sync commands to project directory
253
+ * @param {string} projectRoot
254
+ * @returns {number} Number of files written
255
+ */
256
+ export function syncToProject(projectRoot) {
257
+ const targetDir = path.join(projectRoot, CURSOR_COMMANDS_DIR);
258
+
259
+ // Create directory
260
+ if (!fs.existsSync(targetDir)) {
261
+ fs.mkdirSync(targetDir, { recursive: true });
262
+ }
263
+
264
+ const commands = generateCursorCommands(projectRoot);
265
+
266
+ for (const cmd of commands) {
267
+ const filePath = path.join(targetDir, cmd.name);
268
+ fs.writeFileSync(filePath, cmd.content, 'utf-8');
269
+ }
270
+
271
+ return commands.length;
272
+ }
273
+
274
+ /**
275
+ * Sync commands to global Cursor directory
276
+ * @returns {number} Number of files written
277
+ */
278
+ export function syncToGlobal() {
279
+ // Create directory
280
+ if (!fs.existsSync(GLOBAL_CURSOR_DIR)) {
281
+ fs.mkdirSync(GLOBAL_CURSOR_DIR, { recursive: true });
282
+ }
283
+
284
+ const commands = generateCursorCommands();
285
+
286
+ for (const cmd of commands) {
287
+ const filePath = path.join(GLOBAL_CURSOR_DIR, cmd.name);
288
+ fs.writeFileSync(filePath, cmd.content, 'utf-8');
289
+ }
290
+
291
+ return commands.length;
292
+ }
293
+
294
+ /**
295
+ * Check if Cursor commands exist
296
+ * @param {string} projectRoot
297
+ * @returns {{project: boolean, global: boolean}}
298
+ */
299
+ export function checkCursorCommands(projectRoot) {
300
+ const projectDir = path.join(projectRoot, CURSOR_COMMANDS_DIR);
301
+ const globalDir = GLOBAL_CURSOR_DIR;
302
+
303
+ return {
304
+ project: fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0,
305
+ global: fs.existsSync(globalDir) && fs.readdirSync(globalDir).length > 0,
306
+ };
307
+ }