@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate command - Validate 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
|
+
FILES,
|
|
12
|
+
parseFrontmatter,
|
|
13
|
+
} from '../lib/config.mjs';
|
|
14
|
+
import { listSpecs, readSpec } from '../lib/spec.mjs';
|
|
15
|
+
import { readWorkflow, STEPS } from '../lib/workflow.mjs';
|
|
16
|
+
import {
|
|
17
|
+
colors,
|
|
18
|
+
colorize,
|
|
19
|
+
print,
|
|
20
|
+
printSuccess,
|
|
21
|
+
printWarning,
|
|
22
|
+
printError,
|
|
23
|
+
} from '../lib/terminal.mjs';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute validate command
|
|
27
|
+
* @param {Object} ctx - Command context
|
|
28
|
+
*/
|
|
29
|
+
export async function validateCommand(ctx) {
|
|
30
|
+
const { cwd, options } = ctx;
|
|
31
|
+
|
|
32
|
+
// Ensure initialized
|
|
33
|
+
const projectRoot = ensureInitialized(cwd);
|
|
34
|
+
|
|
35
|
+
print('');
|
|
36
|
+
print(colorize(' incspec 规范验证', colors.bold, colors.cyan));
|
|
37
|
+
print(colorize(' ────────────────', colors.dim));
|
|
38
|
+
print('');
|
|
39
|
+
|
|
40
|
+
const errors = [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
|
|
43
|
+
// 1. Check core files
|
|
44
|
+
print(colorize('检查核心文件...', colors.bold));
|
|
45
|
+
|
|
46
|
+
const projectPath = path.join(projectRoot, INCSPEC_DIR, FILES.project);
|
|
47
|
+
if (!fs.existsSync(projectPath)) {
|
|
48
|
+
errors.push(`缺少 ${FILES.project} 文件`);
|
|
49
|
+
} else {
|
|
50
|
+
const content = fs.readFileSync(projectPath, 'utf-8');
|
|
51
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
52
|
+
|
|
53
|
+
if (!frontmatter.name) {
|
|
54
|
+
warnings.push(`${FILES.project}: 缺少 name 字段`);
|
|
55
|
+
}
|
|
56
|
+
if (!frontmatter.tech_stack || frontmatter.tech_stack.length === 0) {
|
|
57
|
+
warnings.push(`${FILES.project}: 缺少 tech_stack 字段`);
|
|
58
|
+
}
|
|
59
|
+
printSuccess(`${FILES.project} 存在`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const workflowPath = path.join(projectRoot, INCSPEC_DIR, FILES.workflow);
|
|
63
|
+
if (!fs.existsSync(workflowPath)) {
|
|
64
|
+
warnings.push(`缺少 ${FILES.workflow} 文件 (将在首次使用时创建)`);
|
|
65
|
+
} else {
|
|
66
|
+
printSuccess(`${FILES.workflow} 存在`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Check directories
|
|
70
|
+
print('');
|
|
71
|
+
print(colorize('检查目录结构...', colors.bold));
|
|
72
|
+
|
|
73
|
+
for (const [key, dir] of Object.entries(DIRS)) {
|
|
74
|
+
const dirPath = path.join(projectRoot, INCSPEC_DIR, dir);
|
|
75
|
+
if (!fs.existsSync(dirPath)) {
|
|
76
|
+
warnings.push(`缺少目录: ${dir}/`);
|
|
77
|
+
} else {
|
|
78
|
+
printSuccess(`${dir}/ 存在`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Check spec files format
|
|
83
|
+
print('');
|
|
84
|
+
print(colorize('检查规范文件格式...', colors.bold));
|
|
85
|
+
|
|
86
|
+
const specTypes = ['baselines', 'increments'];
|
|
87
|
+
let checkedCount = 0;
|
|
88
|
+
|
|
89
|
+
for (const type of specTypes) {
|
|
90
|
+
const specs = listSpecs(projectRoot, type);
|
|
91
|
+
for (const spec of specs) {
|
|
92
|
+
checkedCount++;
|
|
93
|
+
try {
|
|
94
|
+
const { frontmatter, body } = readSpec(spec.path);
|
|
95
|
+
|
|
96
|
+
// Check for required sections in baselines
|
|
97
|
+
if (type === 'baselines') {
|
|
98
|
+
if (!body.includes('## 1.') && !body.includes('# ')) {
|
|
99
|
+
warnings.push(`${spec.name}: 可能缺少标准章节结构`);
|
|
100
|
+
}
|
|
101
|
+
if (!body.includes('sequenceDiagram') && !body.includes('graph ')) {
|
|
102
|
+
warnings.push(`${spec.name}: 未检测到 Mermaid 图表`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for required sections in increments
|
|
107
|
+
if (type === 'increments') {
|
|
108
|
+
const requiredSections = ['模块1', '模块2', '模块3', '模块4', '模块5'];
|
|
109
|
+
const missingSections = requiredSections.filter(s => !body.includes(s));
|
|
110
|
+
if (missingSections.length > 0) {
|
|
111
|
+
warnings.push(`${spec.name}: 可能缺少模块 ${missingSections.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
errors.push(`${spec.name}: 读取失败 - ${e.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (checkedCount > 0) {
|
|
121
|
+
printSuccess(`已检查 ${checkedCount} 个规范文件`);
|
|
122
|
+
} else {
|
|
123
|
+
print(colorize(' (暂无规范文件)', colors.dim));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Check workflow consistency
|
|
127
|
+
print('');
|
|
128
|
+
print(colorize('检查工作流一致性...', colors.bold));
|
|
129
|
+
|
|
130
|
+
const workflow = readWorkflow(projectRoot);
|
|
131
|
+
if (workflow?.currentWorkflow) {
|
|
132
|
+
printSuccess(`当前工作流: ${workflow.currentWorkflow}`);
|
|
133
|
+
|
|
134
|
+
// Check step outputs exist
|
|
135
|
+
workflow.steps.forEach((step, index) => {
|
|
136
|
+
if (step.output && step.status === 'completed') {
|
|
137
|
+
// Determine expected path based on step
|
|
138
|
+
let expectedPath;
|
|
139
|
+
if (index === 0 || index === 5) {
|
|
140
|
+
expectedPath = path.join(projectRoot, INCSPEC_DIR, DIRS.baselines, step.output);
|
|
141
|
+
} else if (index === 1 || index === 2) {
|
|
142
|
+
expectedPath = path.join(projectRoot, INCSPEC_DIR, DIRS.requirements, step.output);
|
|
143
|
+
} else if (index === 3) {
|
|
144
|
+
expectedPath = path.join(projectRoot, INCSPEC_DIR, DIRS.increments, step.output);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (expectedPath && !fs.existsSync(expectedPath)) {
|
|
148
|
+
warnings.push(`步骤 ${index + 1} 输出文件不存在: ${step.output}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
print(colorize(' (无活跃工作流)', colors.dim));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Summary
|
|
157
|
+
print('');
|
|
158
|
+
print(colorize('验证结果:', colors.bold));
|
|
159
|
+
print('');
|
|
160
|
+
|
|
161
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
162
|
+
printSuccess('所有检查通过!');
|
|
163
|
+
} else {
|
|
164
|
+
if (errors.length > 0) {
|
|
165
|
+
print(colorize(`错误 (${errors.length}):`, colors.red, colors.bold));
|
|
166
|
+
errors.forEach(e => print(colorize(` ✗ ${e}`, colors.red)));
|
|
167
|
+
print('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (warnings.length > 0) {
|
|
171
|
+
print(colorize(`警告 (${warnings.length}):`, colors.yellow, colors.bold));
|
|
172
|
+
warnings.forEach(w => print(colorize(` ⚠ ${w}`, colors.yellow)));
|
|
173
|
+
print('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Exit with error code if strict mode
|
|
178
|
+
if (options.strict && errors.length > 0) {
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* incspec CLI
|
|
5
|
+
* Incremental spec-driven development workflow tool
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { initCommand } from './commands/init.mjs';
|
|
9
|
+
import { updateCommand } from './commands/update.mjs';
|
|
10
|
+
import { statusCommand } from './commands/status.mjs';
|
|
11
|
+
import { analyzeCommand } from './commands/analyze.mjs';
|
|
12
|
+
import { collectReqCommand } from './commands/collect-req.mjs';
|
|
13
|
+
import { collectDepCommand } from './commands/collect-dep.mjs';
|
|
14
|
+
import { designCommand } from './commands/design.mjs';
|
|
15
|
+
import { applyCommand } from './commands/apply.mjs';
|
|
16
|
+
import { mergeCommand } from './commands/merge.mjs';
|
|
17
|
+
import { listCommand } from './commands/list.mjs';
|
|
18
|
+
import { validateCommand } from './commands/validate.mjs';
|
|
19
|
+
import { archiveCommand } from './commands/archive.mjs';
|
|
20
|
+
import { cursorSyncCommand } from './commands/cursor-sync.mjs';
|
|
21
|
+
import { helpCommand } from './commands/help.mjs';
|
|
22
|
+
import { colors, colorize } from './lib/terminal.mjs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse command line arguments
|
|
26
|
+
* @param {string[]} args
|
|
27
|
+
* @returns {{command: string, args: string[], options: Object}}
|
|
28
|
+
*/
|
|
29
|
+
function parseArgs(args) {
|
|
30
|
+
const result = {
|
|
31
|
+
command: '',
|
|
32
|
+
args: [],
|
|
33
|
+
options: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const valueOptions = new Set(['module', 'feature', 'source-dir', 'output', 'workflow']);
|
|
37
|
+
const shortValueMap = new Map([
|
|
38
|
+
['m', 'module'],
|
|
39
|
+
['f', 'feature'],
|
|
40
|
+
['s', 'source-dir'],
|
|
41
|
+
['o', 'output'],
|
|
42
|
+
['w', 'workflow'],
|
|
43
|
+
]);
|
|
44
|
+
let i = 0;
|
|
45
|
+
|
|
46
|
+
while (i < args.length) {
|
|
47
|
+
const arg = args[i];
|
|
48
|
+
|
|
49
|
+
if (arg === '--') {
|
|
50
|
+
result.args.push(...args.slice(i + 1));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const shortValueMatch = arg.match(/^-([a-zA-Z0-9])=(.+)$/);
|
|
55
|
+
if (shortValueMatch) {
|
|
56
|
+
const key = shortValueMatch[1];
|
|
57
|
+
const value = shortValueMatch[2];
|
|
58
|
+
const longKey = shortValueMap.get(key);
|
|
59
|
+
if (longKey) {
|
|
60
|
+
result.options[longKey] = value;
|
|
61
|
+
} else {
|
|
62
|
+
result.options[key] = value;
|
|
63
|
+
}
|
|
64
|
+
} else if (arg.startsWith('--')) {
|
|
65
|
+
// Long option: --key=value or --key
|
|
66
|
+
const eqIndex = arg.indexOf('=');
|
|
67
|
+
if (eqIndex !== -1) {
|
|
68
|
+
const key = arg.slice(2, eqIndex);
|
|
69
|
+
const value = arg.slice(eqIndex + 1);
|
|
70
|
+
result.options[key] = value;
|
|
71
|
+
} else {
|
|
72
|
+
const key = arg.slice(2);
|
|
73
|
+
const nextArg = args[i + 1];
|
|
74
|
+
if (valueOptions.has(key) && nextArg && !nextArg.startsWith('-')) {
|
|
75
|
+
result.options[key] = nextArg;
|
|
76
|
+
i += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
result.options[key] = true;
|
|
80
|
+
}
|
|
81
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
82
|
+
// Short option: -k
|
|
83
|
+
const key = arg.slice(1);
|
|
84
|
+
const nextArg = args[i + 1];
|
|
85
|
+
const longKey = shortValueMap.get(key);
|
|
86
|
+
if (longKey && nextArg && !nextArg.startsWith('-')) {
|
|
87
|
+
result.options[longKey] = nextArg;
|
|
88
|
+
i += 2;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
result.options[key] = true;
|
|
92
|
+
} else if (!result.command) {
|
|
93
|
+
result.command = arg;
|
|
94
|
+
} else {
|
|
95
|
+
result.args.push(arg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Main CLI entry point
|
|
106
|
+
*/
|
|
107
|
+
async function main() {
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
const parsed = parseArgs(args);
|
|
110
|
+
|
|
111
|
+
// Handle --help or -h flag
|
|
112
|
+
if (parsed.options.help || parsed.options.h) {
|
|
113
|
+
await helpCommand({ command: parsed.command || undefined });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle --version or -v flag
|
|
118
|
+
if (parsed.options.version || parsed.options.v) {
|
|
119
|
+
const { createRequire } = await import('module');
|
|
120
|
+
const require = createRequire(import.meta.url);
|
|
121
|
+
try {
|
|
122
|
+
const pkg = require('./package.json');
|
|
123
|
+
console.log(pkg.version);
|
|
124
|
+
} catch {
|
|
125
|
+
console.log('unknown');
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Default to help if no command
|
|
131
|
+
if (!parsed.command) {
|
|
132
|
+
await helpCommand();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Route to appropriate command
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
const commandContext = { cwd, args: parsed.args, options: parsed.options };
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
switch (parsed.command) {
|
|
142
|
+
// Initialize
|
|
143
|
+
case 'init':
|
|
144
|
+
await initCommand(commandContext);
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
// Update templates
|
|
148
|
+
case 'update':
|
|
149
|
+
case 'up':
|
|
150
|
+
await updateCommand(commandContext);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
// Status
|
|
154
|
+
case 'status':
|
|
155
|
+
case 'st':
|
|
156
|
+
await statusCommand(commandContext);
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
// Workflow commands (Step 1-6)
|
|
160
|
+
case 'analyze':
|
|
161
|
+
case 'a':
|
|
162
|
+
await analyzeCommand(commandContext);
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'collect-req':
|
|
166
|
+
case 'cr':
|
|
167
|
+
await collectReqCommand(commandContext);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'collect-dep':
|
|
171
|
+
case 'cd':
|
|
172
|
+
await collectDepCommand(commandContext);
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'design':
|
|
176
|
+
case 'd':
|
|
177
|
+
await designCommand(commandContext);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case 'apply':
|
|
181
|
+
case 'ap':
|
|
182
|
+
await applyCommand(commandContext);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case 'merge':
|
|
186
|
+
case 'm':
|
|
187
|
+
await mergeCommand(commandContext);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
// Management commands
|
|
191
|
+
case 'list':
|
|
192
|
+
case 'ls':
|
|
193
|
+
await listCommand(commandContext);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'validate':
|
|
197
|
+
case 'v':
|
|
198
|
+
await validateCommand(commandContext);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'archive':
|
|
202
|
+
case 'ar':
|
|
203
|
+
await archiveCommand(commandContext);
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
// Cursor integration
|
|
207
|
+
case 'cursor-sync':
|
|
208
|
+
case 'cs':
|
|
209
|
+
await cursorSyncCommand(commandContext);
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
// Help
|
|
213
|
+
case 'help':
|
|
214
|
+
case 'h':
|
|
215
|
+
await helpCommand({ command: parsed.args[0] });
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
console.error(colorize(`Unknown command: ${parsed.command}`, colors.red));
|
|
220
|
+
console.error(colorize("Run 'incspec help' for usage information.", colors.dim));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(colorize(`Error: ${error.message}`, colors.red));
|
|
225
|
+
if (parsed.options.debug) {
|
|
226
|
+
console.error(error.stack);
|
|
227
|
+
}
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Run the CLI
|
|
233
|
+
main().catch((error) => {
|
|
234
|
+
console.error(colorize(`Fatal error: ${error.message}`, colors.red));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
package/lib/agents.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS.md file read/write utilities for incspec
|
|
3
|
+
* Handles managed block insertion/update in project AGENTS.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getTemplatesDir } from './config.mjs';
|
|
9
|
+
|
|
10
|
+
const INCSPEC_BLOCK_START = '<!-- INCSPEC:START -->';
|
|
11
|
+
const INCSPEC_BLOCK_END = '<!-- INCSPEC:END -->';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the project AGENTS.md file path
|
|
15
|
+
* @param {string} projectRoot - Project root directory
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function getProjectAgentsFilePath(projectRoot) {
|
|
19
|
+
return path.join(projectRoot, 'AGENTS.md');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the incspec block template content
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function getIncspecBlockTemplate() {
|
|
27
|
+
const templatePath = path.join(getTemplatesDir(), 'INCSPEC_BLOCK.md');
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(templatePath)) {
|
|
30
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback content if template not found
|
|
34
|
+
return `<!-- INCSPEC:START -->
|
|
35
|
+
# IncSpec 指令
|
|
36
|
+
|
|
37
|
+
本指令适用于在此项目中工作的 AI 助手。
|
|
38
|
+
|
|
39
|
+
当请求符合以下情况时,请始终打开 \`@/incspec/AGENTS.md\`:
|
|
40
|
+
- 涉及增量开发或编码工作流
|
|
41
|
+
- 引入需要分步实现的新功能
|
|
42
|
+
- 需要基线分析、需求收集或代码生成
|
|
43
|
+
- 请求含义模糊,需要先了解规范工作流再编码
|
|
44
|
+
|
|
45
|
+
通过 \`@/incspec/AGENTS.md\` 可以了解:
|
|
46
|
+
- 如何使用 6 步增量编码工作流
|
|
47
|
+
- 规范格式与约定
|
|
48
|
+
- 项目结构与指南
|
|
49
|
+
|
|
50
|
+
请保留此托管块,以便 'incspec init' 可以刷新指令内容。
|
|
51
|
+
|
|
52
|
+
<!-- INCSPEC:END -->
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Update or create AGENTS.md with incspec managed block
|
|
58
|
+
* - If file doesn't exist, create it with the block
|
|
59
|
+
* - If file exists but no markers, append the block
|
|
60
|
+
* - If file exists with markers, replace content between markers
|
|
61
|
+
* @param {string} projectRoot - Project root directory
|
|
62
|
+
* @returns {{created: boolean, updated: boolean}}
|
|
63
|
+
*/
|
|
64
|
+
export function updateProjectAgentsFile(projectRoot) {
|
|
65
|
+
const filePath = getProjectAgentsFilePath(projectRoot);
|
|
66
|
+
const blockContent = getIncspecBlockTemplate();
|
|
67
|
+
|
|
68
|
+
let existingContent = '';
|
|
69
|
+
let fileExists = false;
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(filePath)) {
|
|
72
|
+
existingContent = fs.readFileSync(filePath, 'utf-8');
|
|
73
|
+
fileExists = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let newContent;
|
|
77
|
+
let updated = false;
|
|
78
|
+
|
|
79
|
+
if (!fileExists) {
|
|
80
|
+
// File doesn't exist, create new with block content
|
|
81
|
+
newContent = blockContent;
|
|
82
|
+
} else {
|
|
83
|
+
// File exists, check for markers
|
|
84
|
+
const startIndex = existingContent.indexOf(INCSPEC_BLOCK_START);
|
|
85
|
+
const endIndex = existingContent.indexOf(INCSPEC_BLOCK_END);
|
|
86
|
+
|
|
87
|
+
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
|
|
88
|
+
// Markers found, replace content between markers (inclusive)
|
|
89
|
+
const endMarkerLength = INCSPEC_BLOCK_END.length;
|
|
90
|
+
newContent =
|
|
91
|
+
existingContent.substring(0, startIndex) +
|
|
92
|
+
blockContent.trim() +
|
|
93
|
+
existingContent.substring(endIndex + endMarkerLength);
|
|
94
|
+
updated = true;
|
|
95
|
+
} else {
|
|
96
|
+
// Markers not found, append block to end of file
|
|
97
|
+
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
98
|
+
newContent = existingContent + separator + blockContent;
|
|
99
|
+
updated = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
created: !fileExists,
|
|
107
|
+
updated: updated,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if AGENTS.md has incspec block
|
|
113
|
+
* @param {string} projectRoot - Project root directory
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function hasIncspecBlock(projectRoot) {
|
|
117
|
+
const filePath = getProjectAgentsFilePath(projectRoot);
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(filePath)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
124
|
+
return content.includes(INCSPEC_BLOCK_START) && content.includes(INCSPEC_BLOCK_END);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove incspec block from AGENTS.md
|
|
129
|
+
* @param {string} projectRoot - Project root directory
|
|
130
|
+
* @returns {boolean} True if block was removed
|
|
131
|
+
*/
|
|
132
|
+
export function removeIncspecBlock(projectRoot) {
|
|
133
|
+
const filePath = getProjectAgentsFilePath(projectRoot);
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(filePath)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
140
|
+
const startIndex = content.indexOf(INCSPEC_BLOCK_START);
|
|
141
|
+
const endIndex = content.indexOf(INCSPEC_BLOCK_END);
|
|
142
|
+
|
|
143
|
+
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const endMarkerLength = INCSPEC_BLOCK_END.length;
|
|
148
|
+
let newContent =
|
|
149
|
+
content.substring(0, startIndex) +
|
|
150
|
+
content.substring(endIndex + endMarkerLength);
|
|
151
|
+
|
|
152
|
+
// Clean up extra newlines
|
|
153
|
+
newContent = newContent.replace(/\n{3,}/g, '\n\n').trim();
|
|
154
|
+
|
|
155
|
+
if (newContent) {
|
|
156
|
+
fs.writeFileSync(filePath, newContent + '\n', 'utf-8');
|
|
157
|
+
} else {
|
|
158
|
+
// If file is empty after removal, delete it
|
|
159
|
+
fs.unlinkSync(filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
}
|