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