@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/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
|
+
}
|