@fission-ai/openspec 0.3.0 → 0.4.0
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/README.md +4 -2
- package/dist/core/config.js +1 -0
- package/dist/core/configurators/slash/opencode.d.ts +9 -0
- package/dist/core/configurators/slash/opencode.js +36 -0
- package/dist/core/configurators/slash/registry.js +3 -0
- package/dist/core/init.js +51 -78
- package/dist/core/parsers/change-parser.js +3 -2
- package/dist/core/parsers/markdown-parser.d.ts +1 -0
- package/dist/core/parsers/markdown-parser.js +5 -1
- package/dist/core/parsers/requirement-blocks.js +10 -5
- package/dist/core/templates/slash-command-templates.js +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
|
|
16
16
|
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
|
|
17
17
|
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
|
|
18
|
+
<a href="https://discord.gg/saTQQGQZ"><img alt="Discord" src="https://img.shields.io/discord/1411657095639601154?logo=discord&logoColor=white&style=flat-square" /></a>
|
|
18
19
|
</p>
|
|
19
20
|
|
|
20
21
|
<p align="center">
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
</p>
|
|
23
24
|
|
|
24
25
|
<p align="center">
|
|
25
|
-
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates.
|
|
26
|
+
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/saTQQGQZ">OpenSpec Discord</a> for help and questions.
|
|
26
27
|
</p>
|
|
27
28
|
|
|
28
29
|
# OpenSpec
|
|
@@ -82,13 +83,14 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
|
|
82
83
|
|------|----------|
|
|
83
84
|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
|
84
85
|
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
|
86
|
+
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
|
85
87
|
|
|
86
88
|
#### AGENTS.md Compatible
|
|
87
89
|
These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
|
|
88
90
|
|
|
89
91
|
| Tools |
|
|
90
92
|
|-------|
|
|
91
|
-
| Codex • Amp • Jules •
|
|
93
|
+
| Codex • Amp • Jules • Gemini CLI • GitHub Copilot • Others |
|
|
92
94
|
|
|
93
95
|
### Install & Initialize
|
|
94
96
|
|
package/dist/core/config.js
CHANGED
|
@@ -6,6 +6,7 @@ export const OPENSPEC_MARKERS = {
|
|
|
6
6
|
export const AI_TOOLS = [
|
|
7
7
|
{ name: 'Claude Code (✅ OpenSpec custom slash commands available)', value: 'claude', available: true, successLabel: 'Claude Code' },
|
|
8
8
|
{ name: 'Cursor (✅ OpenSpec custom slash commands available)', value: 'cursor', available: true, successLabel: 'Cursor' },
|
|
9
|
+
{ name: 'OpenCode (✅ OpenSpec custom slash commands available)', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
|
9
10
|
{ name: 'AGENTS.md (works with Codex, Amp, Copilot, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant' }
|
|
10
11
|
];
|
|
11
12
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SlashCommandConfigurator } from "./base.js";
|
|
2
|
+
import { SlashCommandId } from "../../templates/index.js";
|
|
3
|
+
export declare class OpenCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
4
|
+
readonly toolId = "opencode";
|
|
5
|
+
readonly isAvailable = true;
|
|
6
|
+
protected getRelativePath(id: SlashCommandId): string;
|
|
7
|
+
protected getFrontmatter(id: SlashCommandId): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=opencode.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SlashCommandConfigurator } from "./base.js";
|
|
2
|
+
const FILE_PATHS = {
|
|
3
|
+
proposal: ".opencode/command/openspec-proposal.md",
|
|
4
|
+
apply: ".opencode/command/openspec-apply.md",
|
|
5
|
+
archive: ".opencode/command/openspec-archive.md",
|
|
6
|
+
};
|
|
7
|
+
const FRONTMATTER = {
|
|
8
|
+
proposal: `---
|
|
9
|
+
agent: build
|
|
10
|
+
description: Scaffold a new OpenSpec change and validate strictly.
|
|
11
|
+
---
|
|
12
|
+
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
|
|
13
|
+
<UserRequest>
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
</UserRequest>
|
|
16
|
+
`,
|
|
17
|
+
apply: `---
|
|
18
|
+
agent: build
|
|
19
|
+
description: Implement an approved OpenSpec change and keep tasks in sync.
|
|
20
|
+
---`,
|
|
21
|
+
archive: `---
|
|
22
|
+
agent: build
|
|
23
|
+
description: Archive a deployed OpenSpec change and update specs.
|
|
24
|
+
---`,
|
|
25
|
+
};
|
|
26
|
+
export class OpenCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
27
|
+
toolId = "opencode";
|
|
28
|
+
isAvailable = true;
|
|
29
|
+
getRelativePath(id) {
|
|
30
|
+
return FILE_PATHS[id];
|
|
31
|
+
}
|
|
32
|
+
getFrontmatter(id) {
|
|
33
|
+
return FRONTMATTER[id];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=opencode.js.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
|
2
2
|
import { CursorSlashCommandConfigurator } from './cursor.js';
|
|
3
|
+
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
|
3
4
|
export class SlashCommandRegistry {
|
|
4
5
|
static configurators = new Map();
|
|
5
6
|
static {
|
|
6
7
|
const claude = new ClaudeSlashCommandConfigurator();
|
|
7
8
|
const cursor = new CursorSlashCommandConfigurator();
|
|
9
|
+
const opencode = new OpenCodeSlashCommandConfigurator();
|
|
8
10
|
this.configurators.set(claude.toolId, claude);
|
|
9
11
|
this.configurators.set(cursor.toolId, cursor);
|
|
12
|
+
this.configurators.set(opencode.toolId, opencode);
|
|
10
13
|
}
|
|
11
14
|
static register(configurator) {
|
|
12
15
|
this.configurators.set(configurator.toolId, configurator);
|
package/dist/core/init.js
CHANGED
|
@@ -1,72 +1,30 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState } from '@inquirer/core';
|
|
2
|
+
import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
6
6
|
import { TemplateManager } from './templates/index.js';
|
|
7
7
|
import { ToolRegistry } from './configurators/registry.js';
|
|
8
8
|
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
9
|
-
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
9
|
+
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
10
10
|
const PROGRESS_SPINNER = {
|
|
11
11
|
interval: 80,
|
|
12
|
-
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓']
|
|
12
|
+
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
13
13
|
};
|
|
14
14
|
const PALETTE = {
|
|
15
15
|
white: chalk.hex('#f4f4f4'),
|
|
16
16
|
lightGray: chalk.hex('#c8c8c8'),
|
|
17
17
|
midGray: chalk.hex('#8a8a8a'),
|
|
18
|
-
darkGray: chalk.hex('#4a4a4a')
|
|
18
|
+
darkGray: chalk.hex('#4a4a4a'),
|
|
19
19
|
};
|
|
20
20
|
const LETTER_MAP = {
|
|
21
|
-
O: [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
],
|
|
28
|
-
P: [
|
|
29
|
-
'█████ ',
|
|
30
|
-
'██ ██',
|
|
31
|
-
'█████ ',
|
|
32
|
-
'██ ',
|
|
33
|
-
'██ '
|
|
34
|
-
],
|
|
35
|
-
E: [
|
|
36
|
-
'██████',
|
|
37
|
-
'██ ',
|
|
38
|
-
'█████ ',
|
|
39
|
-
'██ ',
|
|
40
|
-
'██████'
|
|
41
|
-
],
|
|
42
|
-
N: [
|
|
43
|
-
'██ ██',
|
|
44
|
-
'███ ██',
|
|
45
|
-
'██ ███',
|
|
46
|
-
'██ ██',
|
|
47
|
-
'██ ██'
|
|
48
|
-
],
|
|
49
|
-
S: [
|
|
50
|
-
' █████',
|
|
51
|
-
'██ ',
|
|
52
|
-
' ████ ',
|
|
53
|
-
' ██',
|
|
54
|
-
'█████ '
|
|
55
|
-
],
|
|
56
|
-
C: [
|
|
57
|
-
' █████',
|
|
58
|
-
'██ ',
|
|
59
|
-
'██ ',
|
|
60
|
-
'██ ',
|
|
61
|
-
' █████'
|
|
62
|
-
],
|
|
63
|
-
' ': [
|
|
64
|
-
' ',
|
|
65
|
-
' ',
|
|
66
|
-
' ',
|
|
67
|
-
' ',
|
|
68
|
-
' '
|
|
69
|
-
]
|
|
21
|
+
O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
|
|
22
|
+
P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
|
|
23
|
+
E: ['██████', '██ ', '█████ ', '██ ', '██████'],
|
|
24
|
+
N: ['██ ██', '███ ██', '██ ███', '██ ██', '██ ██'],
|
|
25
|
+
S: [' █████', '██ ', ' ████ ', ' ██', '█████ '],
|
|
26
|
+
C: [' █████', '██ ', '██ ', '██ ', ' █████'],
|
|
27
|
+
' ': [' ', ' ', ' ', ' ', ' '],
|
|
70
28
|
};
|
|
71
29
|
const sanitizeToolLabel = (raw) => raw.replace(/✅/gu, '✔').trim();
|
|
72
30
|
const parseToolLabel = (raw) => {
|
|
@@ -77,7 +35,7 @@ const parseToolLabel = (raw) => {
|
|
|
77
35
|
}
|
|
78
36
|
return {
|
|
79
37
|
primary: match[1].trim(),
|
|
80
|
-
annotation: match[2].trim()
|
|
38
|
+
annotation: match[2].trim(),
|
|
81
39
|
};
|
|
82
40
|
};
|
|
83
41
|
const toolSelectionWizard = createPrompt((config, done) => {
|
|
@@ -101,12 +59,16 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
101
59
|
loop: config.choices.length > 1,
|
|
102
60
|
renderItem: ({ item, isActive }) => {
|
|
103
61
|
const isSelected = selectedSet.has(item.value);
|
|
104
|
-
const cursorSymbol = isActive
|
|
105
|
-
|
|
62
|
+
const cursorSymbol = isActive
|
|
63
|
+
? PALETTE.white('›')
|
|
64
|
+
: PALETTE.midGray(' ');
|
|
65
|
+
const indicator = isSelected
|
|
66
|
+
? PALETTE.white('◉')
|
|
67
|
+
: PALETTE.midGray('○');
|
|
106
68
|
const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
|
|
107
69
|
const label = `${nameColor(item.label.primary)}${item.configured ? PALETTE.midGray(' (already configured)') : ''}`;
|
|
108
70
|
return `${cursorSymbol} ${indicator} ${label}`;
|
|
109
|
-
}
|
|
71
|
+
},
|
|
110
72
|
});
|
|
111
73
|
useKeypress((key) => {
|
|
112
74
|
if (step === 'intro') {
|
|
@@ -247,13 +209,13 @@ export class InitCommand {
|
|
|
247
209
|
}
|
|
248
210
|
throw new Error('You must select at least one AI tool to configure.');
|
|
249
211
|
}
|
|
250
|
-
const availableTools = AI_TOOLS.filter(tool => tool.available);
|
|
212
|
+
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
251
213
|
const selectedIds = new Set(config.aiTools);
|
|
252
|
-
const selectedTools = availableTools.filter(tool => selectedIds.has(tool.value));
|
|
253
|
-
const created = selectedTools.filter(tool => !existingToolStates[tool.value]);
|
|
254
|
-
const refreshed = selectedTools.filter(tool => existingToolStates[tool.value]);
|
|
255
|
-
const skippedExisting = availableTools.filter(tool => !selectedIds.has(tool.value) && existingToolStates[tool.value]);
|
|
256
|
-
const skipped = availableTools.filter(tool => !selectedIds.has(tool.value) && !existingToolStates[tool.value]);
|
|
214
|
+
const selectedTools = availableTools.filter((tool) => selectedIds.has(tool.value));
|
|
215
|
+
const created = selectedTools.filter((tool) => !existingToolStates[tool.value]);
|
|
216
|
+
const refreshed = selectedTools.filter((tool) => existingToolStates[tool.value]);
|
|
217
|
+
const skippedExisting = availableTools.filter((tool) => !selectedIds.has(tool.value) && existingToolStates[tool.value]);
|
|
218
|
+
const skipped = availableTools.filter((tool) => !selectedIds.has(tool.value) && !existingToolStates[tool.value]);
|
|
257
219
|
// Step 1: Create directory structure
|
|
258
220
|
if (!extendMode) {
|
|
259
221
|
const structureSpinner = this.startSpinner('Creating OpenSpec structure...');
|
|
@@ -261,7 +223,7 @@ export class InitCommand {
|
|
|
261
223
|
await this.generateFiles(openspecPath, config);
|
|
262
224
|
structureSpinner.stopAndPersist({
|
|
263
225
|
symbol: PALETTE.white('▌'),
|
|
264
|
-
text: PALETTE.white('OpenSpec structure created')
|
|
226
|
+
text: PALETTE.white('OpenSpec structure created'),
|
|
265
227
|
});
|
|
266
228
|
}
|
|
267
229
|
else {
|
|
@@ -272,7 +234,7 @@ export class InitCommand {
|
|
|
272
234
|
await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
|
273
235
|
toolSpinner.stopAndPersist({
|
|
274
236
|
symbol: PALETTE.white('▌'),
|
|
275
|
-
text: PALETTE.white('AI tools configured')
|
|
237
|
+
text: PALETTE.white('AI tools configured'),
|
|
276
238
|
});
|
|
277
239
|
// Success message
|
|
278
240
|
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode);
|
|
@@ -280,7 +242,7 @@ export class InitCommand {
|
|
|
280
242
|
async validate(projectPath, _openspecPath) {
|
|
281
243
|
const extendMode = await FileSystemUtils.directoryExists(_openspecPath);
|
|
282
244
|
// Check write permissions
|
|
283
|
-
if (!await FileSystemUtils.ensureWritePermissions(projectPath)) {
|
|
245
|
+
if (!(await FileSystemUtils.ensureWritePermissions(projectPath))) {
|
|
284
246
|
throw new Error(`Insufficient permissions to write to ${projectPath}`);
|
|
285
247
|
}
|
|
286
248
|
return extendMode;
|
|
@@ -290,7 +252,7 @@ export class InitCommand {
|
|
|
290
252
|
return { aiTools: selectedTools };
|
|
291
253
|
}
|
|
292
254
|
async promptForAITools(existingTools, extendMode) {
|
|
293
|
-
const availableTools = AI_TOOLS.filter(tool => tool.available);
|
|
255
|
+
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
294
256
|
if (availableTools.length === 0) {
|
|
295
257
|
return [];
|
|
296
258
|
}
|
|
@@ -298,7 +260,9 @@ export class InitCommand {
|
|
|
298
260
|
? 'Which AI tools would you like to add or refresh?'
|
|
299
261
|
: 'Which AI tools do you use?';
|
|
300
262
|
const initialSelected = extendMode
|
|
301
|
-
? availableTools
|
|
263
|
+
? availableTools
|
|
264
|
+
.filter((tool) => existingTools[tool.value])
|
|
265
|
+
.map((tool) => tool.value)
|
|
302
266
|
: [];
|
|
303
267
|
return this.prompt({
|
|
304
268
|
extendMode,
|
|
@@ -306,9 +270,9 @@ export class InitCommand {
|
|
|
306
270
|
choices: availableTools.map((tool) => ({
|
|
307
271
|
value: tool.value,
|
|
308
272
|
label: parseToolLabel(tool.name),
|
|
309
|
-
configured: Boolean(existingTools[tool.value])
|
|
273
|
+
configured: Boolean(existingTools[tool.value]),
|
|
310
274
|
})),
|
|
311
|
-
initialSelected
|
|
275
|
+
initialSelected,
|
|
312
276
|
});
|
|
313
277
|
}
|
|
314
278
|
async getExistingToolStates(projectPath) {
|
|
@@ -320,7 +284,8 @@ export class InitCommand {
|
|
|
320
284
|
}
|
|
321
285
|
async isToolConfigured(projectPath, toolId) {
|
|
322
286
|
const configFile = ToolRegistry.get(toolId)?.configFileName;
|
|
323
|
-
if (configFile &&
|
|
287
|
+
if (configFile &&
|
|
288
|
+
(await FileSystemUtils.fileExists(path.join(projectPath, configFile))))
|
|
324
289
|
return true;
|
|
325
290
|
const slashConfigurator = SlashCommandRegistry.get(toolId);
|
|
326
291
|
if (!slashConfigurator)
|
|
@@ -336,7 +301,7 @@ export class InitCommand {
|
|
|
336
301
|
openspecPath,
|
|
337
302
|
path.join(openspecPath, 'specs'),
|
|
338
303
|
path.join(openspecPath, 'changes'),
|
|
339
|
-
path.join(openspecPath, 'changes', 'archive')
|
|
304
|
+
path.join(openspecPath, 'changes', 'archive'),
|
|
340
305
|
];
|
|
341
306
|
for (const dir of directories) {
|
|
342
307
|
await FileSystemUtils.createDirectory(dir);
|
|
@@ -376,10 +341,18 @@ export class InitCommand {
|
|
|
376
341
|
console.log();
|
|
377
342
|
console.log(PALETTE.lightGray('Tool summary:'));
|
|
378
343
|
const summaryLines = [
|
|
379
|
-
created.length
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
344
|
+
created.length
|
|
345
|
+
? `${PALETTE.white('▌')} ${PALETTE.white('Created:')} ${this.formatToolNames(created)}`
|
|
346
|
+
: null,
|
|
347
|
+
refreshed.length
|
|
348
|
+
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('Refreshed:')} ${this.formatToolNames(refreshed)}`
|
|
349
|
+
: null,
|
|
350
|
+
skippedExisting.length
|
|
351
|
+
? `${PALETTE.midGray('▌')} ${PALETTE.midGray('Skipped (already configured):')} ${this.formatToolNames(skippedExisting)}`
|
|
352
|
+
: null,
|
|
353
|
+
skipped.length
|
|
354
|
+
? `${PALETTE.darkGray('▌')} ${PALETTE.darkGray('Skipped:')} ${this.formatToolNames(skipped)}`
|
|
355
|
+
: null,
|
|
383
356
|
].filter((line) => Boolean(line));
|
|
384
357
|
for (const line of summaryLines) {
|
|
385
358
|
console.log(line);
|
|
@@ -427,7 +400,7 @@ export class InitCommand {
|
|
|
427
400
|
PALETTE.lightGray,
|
|
428
401
|
PALETTE.midGray,
|
|
429
402
|
PALETTE.lightGray,
|
|
430
|
-
PALETTE.white
|
|
403
|
+
PALETTE.white,
|
|
431
404
|
];
|
|
432
405
|
console.log();
|
|
433
406
|
rows.forEach((row, index) => {
|
|
@@ -442,7 +415,7 @@ export class InitCommand {
|
|
|
442
415
|
text,
|
|
443
416
|
stream: process.stdout,
|
|
444
417
|
color: 'gray',
|
|
445
|
-
spinner: PROGRESS_SPINNER
|
|
418
|
+
spinner: PROGRESS_SPINNER,
|
|
446
419
|
}).start();
|
|
447
420
|
}
|
|
448
421
|
}
|
|
@@ -124,7 +124,7 @@ export class ChangeParser extends MarkdownParser {
|
|
|
124
124
|
}
|
|
125
125
|
parseRenames(content) {
|
|
126
126
|
const renames = [];
|
|
127
|
-
const lines = content.split('\n');
|
|
127
|
+
const lines = ChangeParser.normalizeContent(content).split('\n');
|
|
128
128
|
let currentRename = {};
|
|
129
129
|
for (const line of lines) {
|
|
130
130
|
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
@@ -146,7 +146,8 @@ export class ChangeParser extends MarkdownParser {
|
|
|
146
146
|
return renames;
|
|
147
147
|
}
|
|
148
148
|
parseSectionsFromContent(content) {
|
|
149
|
-
const
|
|
149
|
+
const normalizedContent = ChangeParser.normalizeContent(content);
|
|
150
|
+
const lines = normalizedContent.split('\n');
|
|
150
151
|
const sections = [];
|
|
151
152
|
const stack = [];
|
|
152
153
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9,6 +9,7 @@ export declare class MarkdownParser {
|
|
|
9
9
|
private lines;
|
|
10
10
|
private currentLine;
|
|
11
11
|
constructor(content: string);
|
|
12
|
+
protected static normalizeContent(content: string): string;
|
|
12
13
|
parseSpec(name: string): Spec;
|
|
13
14
|
parseChange(name: string): Change;
|
|
14
15
|
protected parseSections(): Section[];
|
|
@@ -2,9 +2,13 @@ export class MarkdownParser {
|
|
|
2
2
|
lines;
|
|
3
3
|
currentLine;
|
|
4
4
|
constructor(content) {
|
|
5
|
-
|
|
5
|
+
const normalized = MarkdownParser.normalizeContent(content);
|
|
6
|
+
this.lines = normalized.split('\n');
|
|
6
7
|
this.currentLine = 0;
|
|
7
8
|
}
|
|
9
|
+
static normalizeContent(content) {
|
|
10
|
+
return content.replace(/\r\n?/g, '\n');
|
|
11
|
+
}
|
|
8
12
|
parseSpec(name) {
|
|
9
13
|
const sections = this.parseSections();
|
|
10
14
|
const purpose = this.findSection(sections, 'Purpose')?.content || '';
|
|
@@ -6,7 +6,8 @@ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/;
|
|
|
6
6
|
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
7
7
|
*/
|
|
8
8
|
export function extractRequirementsSection(content) {
|
|
9
|
-
const
|
|
9
|
+
const normalized = normalizeLineEndings(content);
|
|
10
|
+
const lines = normalized.split('\n');
|
|
10
11
|
const reqHeaderIndex = lines.findIndex(l => /^##\s+Requirements\s*$/i.test(l));
|
|
11
12
|
if (reqHeaderIndex === -1) {
|
|
12
13
|
// No requirements section; create an empty one at the end
|
|
@@ -70,11 +71,15 @@ export function extractRequirementsSection(content) {
|
|
|
70
71
|
after: after.startsWith('\n') ? after : '\n' + after,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
74
|
+
function normalizeLineEndings(content) {
|
|
75
|
+
return content.replace(/\r\n?/g, '\n');
|
|
76
|
+
}
|
|
73
77
|
/**
|
|
74
78
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
75
79
|
*/
|
|
76
80
|
export function parseDeltaSpec(content) {
|
|
77
|
-
const
|
|
81
|
+
const normalized = normalizeLineEndings(content);
|
|
82
|
+
const sections = splitTopLevelSections(normalized);
|
|
78
83
|
const added = parseRequirementBlocksFromSection(sections['ADDED Requirements'] || '');
|
|
79
84
|
const modified = parseRequirementBlocksFromSection(sections['MODIFIED Requirements'] || '');
|
|
80
85
|
const removedNames = parseRemovedNames(sections['REMOVED Requirements'] || '');
|
|
@@ -103,7 +108,7 @@ function splitTopLevelSections(content) {
|
|
|
103
108
|
function parseRequirementBlocksFromSection(sectionBody) {
|
|
104
109
|
if (!sectionBody)
|
|
105
110
|
return [];
|
|
106
|
-
const lines = sectionBody.split('\n');
|
|
111
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
107
112
|
const blocks = [];
|
|
108
113
|
let i = 0;
|
|
109
114
|
while (i < lines.length) {
|
|
@@ -133,7 +138,7 @@ function parseRemovedNames(sectionBody) {
|
|
|
133
138
|
if (!sectionBody)
|
|
134
139
|
return [];
|
|
135
140
|
const names = [];
|
|
136
|
-
const lines = sectionBody.split('\n');
|
|
141
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
137
142
|
for (const line of lines) {
|
|
138
143
|
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
|
139
144
|
if (m) {
|
|
@@ -152,7 +157,7 @@ function parseRenamedPairs(sectionBody) {
|
|
|
152
157
|
if (!sectionBody)
|
|
153
158
|
return [];
|
|
154
159
|
const pairs = [];
|
|
155
|
-
const lines = sectionBody.split('\n');
|
|
160
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
156
161
|
let current = {};
|
|
157
162
|
for (const line of lines) {
|
|
158
163
|
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
@@ -24,7 +24,7 @@ const applyReferences = `**Reference**
|
|
|
24
24
|
- Use \`openspec show <id> --json --deltas-only\` if you need additional context from the proposal while implementing.`;
|
|
25
25
|
const archiveSteps = `**Steps**
|
|
26
26
|
1. Identify the requested change ID (via the prompt or \`openspec list\`).
|
|
27
|
-
2. Run \`openspec archive <id
|
|
27
|
+
2. Run \`openspec archive <id> --yes\` to let the CLI move the change and apply spec updates without prompts (use \`--skip-specs\` only for tooling-only work).
|
|
28
28
|
3. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
|
|
29
29
|
4. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;
|
|
30
30
|
const archiveReferences = `**Reference**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"author": "OpenSpec Contributors",
|
|
19
19
|
"type": "module",
|
|
20
20
|
"publishConfig": {
|
|
21
|
-
"access": "public"
|
|
22
|
-
"tag": "next"
|
|
21
|
+
"access": "public"
|
|
23
22
|
},
|
|
24
23
|
"exports": {
|
|
25
24
|
".": {
|