@fission-ai/openspec 0.5.0 → 0.7.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 +5 -2
- package/dist/core/config.js +5 -4
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/registry.js +3 -0
- package/dist/core/init.d.ts +8 -0
- package/dist/core/init.js +152 -56
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/agents-root-stub.d.ts +2 -0
- package/dist/core/templates/agents-root-stub.js +17 -0
- package/dist/core/templates/claude-template.d.ts +1 -1
- package/dist/core/templates/claude-template.js +1 -1
- package/dist/core/templates/index.js +2 -1
- package/dist/core/templates/slash-command-templates.js +2 -2
- package/dist/core/update.js +41 -36
- package/dist/core/validation/constants.d.ts +1 -1
- package/dist/core/validation/constants.js +1 -1
- package/dist/utils/file-system.js +36 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,9 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
|
|
84
84
|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
|
85
85
|
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
|
86
86
|
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
|
87
|
+
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
|
88
|
+
|
|
89
|
+
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
|
|
87
90
|
|
|
88
91
|
#### AGENTS.md Compatible
|
|
89
92
|
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/).
|
|
@@ -121,8 +124,8 @@ openspec init
|
|
|
121
124
|
```
|
|
122
125
|
|
|
123
126
|
**What happens during initialization:**
|
|
124
|
-
- You'll be prompted to
|
|
125
|
-
- OpenSpec automatically configures slash commands
|
|
127
|
+
- You'll be prompted to pick any natively supported AI tools (Claude Code, Cursor, OpenCode, etc.); other assistants always rely on the shared `AGENTS.md` stub
|
|
128
|
+
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
|
|
126
129
|
- A new `openspec/` directory structure is created in your project
|
|
127
130
|
|
|
128
131
|
**After setup:**
|
package/dist/core/config.js
CHANGED
|
@@ -4,9 +4,10 @@ export const OPENSPEC_MARKERS = {
|
|
|
4
4
|
end: '<!-- OPENSPEC:END -->'
|
|
5
5
|
};
|
|
6
6
|
export const AI_TOOLS = [
|
|
7
|
-
{ name: 'Claude Code
|
|
8
|
-
{ name: 'Cursor
|
|
9
|
-
{ name: 'OpenCode
|
|
10
|
-
{ name: '
|
|
7
|
+
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
|
8
|
+
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
|
9
|
+
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
|
10
|
+
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
|
11
|
+
{ name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
|
11
12
|
];
|
|
12
13
|
//# 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 KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
4
|
+
readonly toolId = "kilocode";
|
|
5
|
+
readonly isAvailable = true;
|
|
6
|
+
protected getRelativePath(id: SlashCommandId): string;
|
|
7
|
+
protected getFrontmatter(_id: SlashCommandId): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=kilocode.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SlashCommandConfigurator } from "./base.js";
|
|
2
|
+
const FILE_PATHS = {
|
|
3
|
+
proposal: ".kilocode/workflows/openspec-proposal.md",
|
|
4
|
+
apply: ".kilocode/workflows/openspec-apply.md",
|
|
5
|
+
archive: ".kilocode/workflows/openspec-archive.md"
|
|
6
|
+
};
|
|
7
|
+
export class KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
8
|
+
toolId = "kilocode";
|
|
9
|
+
isAvailable = true;
|
|
10
|
+
getRelativePath(id) {
|
|
11
|
+
return FILE_PATHS[id];
|
|
12
|
+
}
|
|
13
|
+
getFrontmatter(_id) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=kilocode.js.map
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
|
2
2
|
import { CursorSlashCommandConfigurator } from './cursor.js';
|
|
3
|
+
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
|
3
4
|
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
|
4
5
|
export class SlashCommandRegistry {
|
|
5
6
|
static configurators = new Map();
|
|
6
7
|
static {
|
|
7
8
|
const claude = new ClaudeSlashCommandConfigurator();
|
|
8
9
|
const cursor = new CursorSlashCommandConfigurator();
|
|
10
|
+
const kilocode = new KiloCodeSlashCommandConfigurator();
|
|
9
11
|
const opencode = new OpenCodeSlashCommandConfigurator();
|
|
10
12
|
this.configurators.set(claude.toolId, claude);
|
|
11
13
|
this.configurators.set(cursor.toolId, cursor);
|
|
14
|
+
this.configurators.set(kilocode.toolId, kilocode);
|
|
12
15
|
this.configurators.set(opencode.toolId, opencode);
|
|
13
16
|
}
|
|
14
17
|
static register(configurator) {
|
package/dist/core/init.d.ts
CHANGED
|
@@ -3,9 +3,16 @@ type ToolLabel = {
|
|
|
3
3
|
annotation?: string;
|
|
4
4
|
};
|
|
5
5
|
type ToolWizardChoice = {
|
|
6
|
+
kind: 'heading' | 'info';
|
|
7
|
+
value: string;
|
|
8
|
+
label: ToolLabel;
|
|
9
|
+
selectable: false;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'option';
|
|
6
12
|
value: string;
|
|
7
13
|
label: ToolLabel;
|
|
8
14
|
configured: boolean;
|
|
15
|
+
selectable: true;
|
|
9
16
|
};
|
|
10
17
|
type ToolWizardConfig = {
|
|
11
18
|
extendMode: boolean;
|
|
@@ -29,6 +36,7 @@ export declare class InitCommand {
|
|
|
29
36
|
private createDirectoryStructure;
|
|
30
37
|
private generateFiles;
|
|
31
38
|
private configureAITools;
|
|
39
|
+
private configureRootAgentsStub;
|
|
32
40
|
private displaySuccessMessage;
|
|
33
41
|
private formatToolNames;
|
|
34
42
|
private renderBanner;
|
package/dist/core/init.js
CHANGED
|
@@ -7,16 +7,11 @@ import { TemplateManager } from './templates/index.js';
|
|
|
7
7
|
import { ToolRegistry } from './configurators/registry.js';
|
|
8
8
|
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
9
9
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
10
|
+
import { PALETTE } from './styles/palette.js';
|
|
10
11
|
const PROGRESS_SPINNER = {
|
|
11
12
|
interval: 80,
|
|
12
13
|
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
13
14
|
};
|
|
14
|
-
const PALETTE = {
|
|
15
|
-
white: chalk.hex('#f4f4f4'),
|
|
16
|
-
lightGray: chalk.hex('#c8c8c8'),
|
|
17
|
-
midGray: chalk.hex('#8a8a8a'),
|
|
18
|
-
darkGray: chalk.hex('#4a4a4a'),
|
|
19
|
-
};
|
|
20
15
|
const LETTER_MAP = {
|
|
21
16
|
O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
|
|
22
17
|
P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
|
|
@@ -38,16 +33,27 @@ const parseToolLabel = (raw) => {
|
|
|
38
33
|
annotation: match[2].trim(),
|
|
39
34
|
};
|
|
40
35
|
};
|
|
36
|
+
const isSelectableChoice = (choice) => choice.selectable;
|
|
37
|
+
const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
|
|
38
|
+
const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
|
|
39
|
+
const LIST_SPACER_VALUE = '__list-spacer__';
|
|
41
40
|
const toolSelectionWizard = createPrompt((config, done) => {
|
|
42
41
|
const totalSteps = 3;
|
|
43
42
|
const [step, setStep] = useState('intro');
|
|
44
|
-
const
|
|
45
|
-
const
|
|
43
|
+
const selectableChoices = config.choices.filter(isSelectableChoice);
|
|
44
|
+
const initialCursorIndex = config.choices.findIndex((choice) => choice.selectable);
|
|
45
|
+
const [cursor, setCursor] = useState(initialCursorIndex === -1 ? 0 : initialCursorIndex);
|
|
46
|
+
const [selected, setSelected] = useState(() => {
|
|
47
|
+
const initial = new Set((config.initialSelected ?? []).filter((value) => selectableChoices.some((choice) => choice.value === value)));
|
|
48
|
+
return selectableChoices
|
|
49
|
+
.map((choice) => choice.value)
|
|
50
|
+
.filter((value) => initial.has(value));
|
|
51
|
+
});
|
|
46
52
|
const [error, setError] = useState(null);
|
|
47
53
|
const selectedSet = new Set(selected);
|
|
48
|
-
const pageSize = Math.max(
|
|
54
|
+
const pageSize = Math.max(config.choices.length, 1);
|
|
49
55
|
const updateSelected = (next) => {
|
|
50
|
-
const ordered =
|
|
56
|
+
const ordered = selectableChoices
|
|
51
57
|
.map((choice) => choice.value)
|
|
52
58
|
.filter((value) => next.has(value));
|
|
53
59
|
setSelected(ordered);
|
|
@@ -56,8 +62,13 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
56
62
|
items: config.choices,
|
|
57
63
|
active: cursor,
|
|
58
64
|
pageSize,
|
|
59
|
-
loop:
|
|
65
|
+
loop: false,
|
|
60
66
|
renderItem: ({ item, isActive }) => {
|
|
67
|
+
if (!item.selectable) {
|
|
68
|
+
const prefix = item.kind === 'info' ? ' ' : '';
|
|
69
|
+
const textColor = item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
|
|
70
|
+
return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(`${prefix}${item.label.primary}`)}`;
|
|
71
|
+
}
|
|
61
72
|
const isSelected = selectedSet.has(item.value);
|
|
62
73
|
const cursorSymbol = isActive
|
|
63
74
|
? PALETTE.white('›')
|
|
@@ -66,10 +77,32 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
66
77
|
? PALETTE.white('◉')
|
|
67
78
|
: PALETTE.midGray('○');
|
|
68
79
|
const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
|
|
69
|
-
const
|
|
80
|
+
const annotation = item.label.annotation
|
|
81
|
+
? PALETTE.midGray(` (${item.label.annotation})`)
|
|
82
|
+
: '';
|
|
83
|
+
const configuredNote = item.configured
|
|
84
|
+
? PALETTE.midGray(' (already configured)')
|
|
85
|
+
: '';
|
|
86
|
+
const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
|
|
70
87
|
return `${cursorSymbol} ${indicator} ${label}`;
|
|
71
88
|
},
|
|
72
89
|
});
|
|
90
|
+
const moveCursor = (direction) => {
|
|
91
|
+
if (selectableChoices.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let nextIndex = cursor;
|
|
95
|
+
while (true) {
|
|
96
|
+
nextIndex = nextIndex + direction;
|
|
97
|
+
if (nextIndex < 0 || nextIndex >= config.choices.length) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (config.choices[nextIndex]?.selectable) {
|
|
101
|
+
setCursor(nextIndex);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
73
106
|
useKeypress((key) => {
|
|
74
107
|
if (step === 'intro') {
|
|
75
108
|
if (isEnterKey(key)) {
|
|
@@ -79,20 +112,18 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
79
112
|
}
|
|
80
113
|
if (step === 'select') {
|
|
81
114
|
if (isUpKey(key)) {
|
|
82
|
-
|
|
83
|
-
setCursor(previousIndex);
|
|
115
|
+
moveCursor(-1);
|
|
84
116
|
setError(null);
|
|
85
117
|
return;
|
|
86
118
|
}
|
|
87
119
|
if (isDownKey(key)) {
|
|
88
|
-
|
|
89
|
-
setCursor(nextIndex);
|
|
120
|
+
moveCursor(1);
|
|
90
121
|
setError(null);
|
|
91
122
|
return;
|
|
92
123
|
}
|
|
93
124
|
if (isSpaceKey(key)) {
|
|
94
125
|
const current = config.choices[cursor];
|
|
95
|
-
if (!current)
|
|
126
|
+
if (!current || !current.selectable)
|
|
96
127
|
return;
|
|
97
128
|
const next = new Set(selected);
|
|
98
129
|
if (next.has(current.value)) {
|
|
@@ -106,16 +137,13 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
106
137
|
return;
|
|
107
138
|
}
|
|
108
139
|
if (isEnterKey(key)) {
|
|
109
|
-
if (selected.length === 0) {
|
|
110
|
-
setError('Select at least one AI tool to continue.');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
140
|
setStep('review');
|
|
114
141
|
setError(null);
|
|
115
142
|
return;
|
|
116
143
|
}
|
|
117
144
|
if (key.name === 'escape') {
|
|
118
|
-
|
|
145
|
+
const next = new Set();
|
|
146
|
+
updateSelected(next);
|
|
119
147
|
setError(null);
|
|
120
148
|
}
|
|
121
149
|
return;
|
|
@@ -124,7 +152,7 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
124
152
|
if (isEnterKey(key)) {
|
|
125
153
|
const finalSelection = config.choices
|
|
126
154
|
.map((choice) => choice.value)
|
|
127
|
-
.filter((value) => selectedSet.has(value));
|
|
155
|
+
.filter((value) => selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE);
|
|
128
156
|
done(finalSelection);
|
|
129
157
|
return;
|
|
130
158
|
}
|
|
@@ -134,9 +162,21 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
});
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
.
|
|
165
|
+
const rootStubChoice = selectableChoices.find((choice) => choice.value === ROOT_STUB_CHOICE_VALUE);
|
|
166
|
+
const rootStubSelected = rootStubChoice
|
|
167
|
+
? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
|
|
168
|
+
: false;
|
|
169
|
+
const nativeChoices = selectableChoices.filter((choice) => choice.value !== ROOT_STUB_CHOICE_VALUE);
|
|
170
|
+
const selectedNativeChoices = nativeChoices.filter((choice) => selectedSet.has(choice.value));
|
|
171
|
+
const formatSummaryLabel = (choice) => {
|
|
172
|
+
const annotation = choice.label.annotation
|
|
173
|
+
? PALETTE.midGray(` (${choice.label.annotation})`)
|
|
174
|
+
: '';
|
|
175
|
+
const configuredNote = choice.configured
|
|
176
|
+
? PALETTE.midGray(' (already configured)')
|
|
177
|
+
: '';
|
|
178
|
+
return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
|
|
179
|
+
};
|
|
140
180
|
const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
|
|
141
181
|
const lines = [];
|
|
142
182
|
lines.push(PALETTE.midGray(`Step ${stepIndex}/${totalSteps}`));
|
|
@@ -159,13 +199,16 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
159
199
|
lines.push('');
|
|
160
200
|
lines.push(page);
|
|
161
201
|
lines.push('');
|
|
162
|
-
|
|
163
|
-
|
|
202
|
+
lines.push(PALETTE.midGray('Selected configuration:'));
|
|
203
|
+
if (rootStubSelected && rootStubChoice) {
|
|
204
|
+
lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`);
|
|
205
|
+
}
|
|
206
|
+
if (selectedNativeChoices.length === 0) {
|
|
207
|
+
lines.push(` ${PALETTE.midGray('- No natively supported providers selected')}`);
|
|
164
208
|
}
|
|
165
209
|
else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
lines.push(` ${PALETTE.white('-')} ${PALETTE.white(name)}`);
|
|
210
|
+
selectedNativeChoices.forEach((choice) => {
|
|
211
|
+
lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`);
|
|
169
212
|
});
|
|
170
213
|
}
|
|
171
214
|
}
|
|
@@ -173,12 +216,15 @@ const toolSelectionWizard = createPrompt((config, done) => {
|
|
|
173
216
|
lines.push(PALETTE.white('Review selections'));
|
|
174
217
|
lines.push(PALETTE.midGray('Press Enter to confirm or Backspace to adjust.'));
|
|
175
218
|
lines.push('');
|
|
176
|
-
if (
|
|
177
|
-
lines.push(PALETTE.
|
|
219
|
+
if (rootStubSelected && rootStubChoice) {
|
|
220
|
+
lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(rootStubChoice)}`);
|
|
221
|
+
}
|
|
222
|
+
if (selectedNativeChoices.length === 0) {
|
|
223
|
+
lines.push(PALETTE.midGray('No natively supported providers selected. Universal instructions will still be applied.'));
|
|
178
224
|
}
|
|
179
225
|
else {
|
|
180
|
-
|
|
181
|
-
lines.push(`${PALETTE.white('▌')} ${
|
|
226
|
+
selectedNativeChoices.forEach((choice) => {
|
|
227
|
+
lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`);
|
|
182
228
|
});
|
|
183
229
|
}
|
|
184
230
|
}
|
|
@@ -202,13 +248,6 @@ export class InitCommand {
|
|
|
202
248
|
this.renderBanner(extendMode);
|
|
203
249
|
// Get configuration (after validation to avoid prompts if validation fails)
|
|
204
250
|
const config = await this.getConfiguration(existingToolStates, extendMode);
|
|
205
|
-
if (config.aiTools.length === 0) {
|
|
206
|
-
if (extendMode) {
|
|
207
|
-
throw new Error(`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
|
|
208
|
-
`Use 'openspec update' to update the structure.`);
|
|
209
|
-
}
|
|
210
|
-
throw new Error('You must select at least one AI tool to configure.');
|
|
211
|
-
}
|
|
212
251
|
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
213
252
|
const selectedIds = new Set(config.aiTools);
|
|
214
253
|
const selectedTools = availableTools.filter((tool) => selectedIds.has(tool.value));
|
|
@@ -231,13 +270,13 @@ export class InitCommand {
|
|
|
231
270
|
}
|
|
232
271
|
// Step 2: Configure AI tools
|
|
233
272
|
const toolSpinner = this.startSpinner('Configuring AI tools...');
|
|
234
|
-
await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
|
273
|
+
const rootStubStatus = await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
|
235
274
|
toolSpinner.stopAndPersist({
|
|
236
275
|
symbol: PALETTE.white('▌'),
|
|
237
276
|
text: PALETTE.white('AI tools configured'),
|
|
238
277
|
});
|
|
239
278
|
// Success message
|
|
240
|
-
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode);
|
|
279
|
+
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
|
|
241
280
|
}
|
|
242
281
|
async validate(projectPath, _openspecPath) {
|
|
243
282
|
const extendMode = await FileSystemUtils.directoryExists(_openspecPath);
|
|
@@ -253,25 +292,64 @@ export class InitCommand {
|
|
|
253
292
|
}
|
|
254
293
|
async promptForAITools(existingTools, extendMode) {
|
|
255
294
|
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
|
256
|
-
if (availableTools.length === 0) {
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
295
|
const baseMessage = extendMode
|
|
260
|
-
? 'Which AI tools would you like to add or refresh?'
|
|
261
|
-
: 'Which AI tools do you use?';
|
|
262
|
-
const
|
|
296
|
+
? 'Which natively supported AI tools would you like to add or refresh?'
|
|
297
|
+
: 'Which natively supported AI tools do you use?';
|
|
298
|
+
const initialNativeSelection = extendMode
|
|
263
299
|
? availableTools
|
|
264
300
|
.filter((tool) => existingTools[tool.value])
|
|
265
301
|
.map((tool) => tool.value)
|
|
266
302
|
: [];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
303
|
+
const initialSelected = Array.from(new Set(initialNativeSelection));
|
|
304
|
+
const choices = [
|
|
305
|
+
{
|
|
306
|
+
kind: 'heading',
|
|
307
|
+
value: '__heading-native__',
|
|
308
|
+
label: {
|
|
309
|
+
primary: 'Natively supported providers (✔ OpenSpec custom slash commands available)',
|
|
310
|
+
},
|
|
311
|
+
selectable: false,
|
|
312
|
+
},
|
|
313
|
+
...availableTools.map((tool) => ({
|
|
314
|
+
kind: 'option',
|
|
271
315
|
value: tool.value,
|
|
272
316
|
label: parseToolLabel(tool.name),
|
|
273
317
|
configured: Boolean(existingTools[tool.value]),
|
|
318
|
+
selectable: true,
|
|
274
319
|
})),
|
|
320
|
+
...(availableTools.length
|
|
321
|
+
? [
|
|
322
|
+
{
|
|
323
|
+
kind: 'info',
|
|
324
|
+
value: LIST_SPACER_VALUE,
|
|
325
|
+
label: { primary: '' },
|
|
326
|
+
selectable: false,
|
|
327
|
+
},
|
|
328
|
+
]
|
|
329
|
+
: []),
|
|
330
|
+
{
|
|
331
|
+
kind: 'heading',
|
|
332
|
+
value: OTHER_TOOLS_HEADING_VALUE,
|
|
333
|
+
label: {
|
|
334
|
+
primary: 'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)',
|
|
335
|
+
},
|
|
336
|
+
selectable: false,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
kind: 'option',
|
|
340
|
+
value: ROOT_STUB_CHOICE_VALUE,
|
|
341
|
+
label: {
|
|
342
|
+
primary: 'Universal AGENTS.md',
|
|
343
|
+
annotation: 'always available',
|
|
344
|
+
},
|
|
345
|
+
configured: extendMode,
|
|
346
|
+
selectable: true,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
return this.prompt({
|
|
350
|
+
extendMode,
|
|
351
|
+
baseMessage,
|
|
352
|
+
choices,
|
|
275
353
|
initialSelected,
|
|
276
354
|
});
|
|
277
355
|
}
|
|
@@ -321,6 +399,7 @@ export class InitCommand {
|
|
|
321
399
|
}
|
|
322
400
|
}
|
|
323
401
|
async configureAITools(projectPath, openspecDir, toolIds) {
|
|
402
|
+
const rootStubStatus = await this.configureRootAgentsStub(projectPath, openspecDir);
|
|
324
403
|
for (const toolId of toolIds) {
|
|
325
404
|
const configurator = ToolRegistry.get(toolId);
|
|
326
405
|
if (configurator && configurator.isAvailable) {
|
|
@@ -331,8 +410,19 @@ export class InitCommand {
|
|
|
331
410
|
await slashConfigurator.generateAll(projectPath, openspecDir);
|
|
332
411
|
}
|
|
333
412
|
}
|
|
413
|
+
return rootStubStatus;
|
|
334
414
|
}
|
|
335
|
-
|
|
415
|
+
async configureRootAgentsStub(projectPath, openspecDir) {
|
|
416
|
+
const configurator = ToolRegistry.get('agents');
|
|
417
|
+
if (!configurator || !configurator.isAvailable) {
|
|
418
|
+
return 'skipped';
|
|
419
|
+
}
|
|
420
|
+
const stubPath = path.join(projectPath, configurator.configFileName);
|
|
421
|
+
const existed = await FileSystemUtils.fileExists(stubPath);
|
|
422
|
+
await configurator.configure(projectPath, openspecDir);
|
|
423
|
+
return existed ? 'updated' : 'created';
|
|
424
|
+
}
|
|
425
|
+
displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus) {
|
|
336
426
|
console.log(); // Empty line for spacing
|
|
337
427
|
const successHeadline = extendMode
|
|
338
428
|
? 'OpenSpec tool configuration updated!'
|
|
@@ -341,6 +431,12 @@ export class InitCommand {
|
|
|
341
431
|
console.log();
|
|
342
432
|
console.log(PALETTE.lightGray('Tool summary:'));
|
|
343
433
|
const summaryLines = [
|
|
434
|
+
rootStubStatus === 'created'
|
|
435
|
+
? `${PALETTE.white('▌')} ${PALETTE.white('Root AGENTS.md stub created for other assistants')}`
|
|
436
|
+
: null,
|
|
437
|
+
rootStubStatus === 'updated'
|
|
438
|
+
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('Root AGENTS.md stub refreshed for other assistants')}`
|
|
439
|
+
: null,
|
|
344
440
|
created.length
|
|
345
441
|
? `${PALETTE.white('▌')} ${PALETTE.white('Created:')} ${this.formatToolNames(created)}`
|
|
346
442
|
: null,
|
|
@@ -380,7 +476,7 @@ export class InitCommand {
|
|
|
380
476
|
.map((tool) => tool.successLabel ?? tool.name)
|
|
381
477
|
.filter((name) => Boolean(name));
|
|
382
478
|
if (names.length === 0)
|
|
383
|
-
return PALETTE.lightGray('your
|
|
479
|
+
return PALETTE.lightGray('your AGENTS.md-compatible assistant');
|
|
384
480
|
if (names.length === 1)
|
|
385
481
|
return PALETTE.white(names[0]);
|
|
386
482
|
const base = names.slice(0, -1).map((name) => PALETTE.white(name));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const agentsRootStubTemplate = "# OpenSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\nAlways open `@/openspec/AGENTS.md` when the request:\n- Mentions planning or proposals (words like proposal, spec, change, plan)\n- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n- Sounds ambiguous and you need the authoritative spec before coding\n\nUse `@/openspec/AGENTS.md` to learn:\n- How to create and apply change proposals\n- Spec format and conventions\n- Project structure and guidelines\n\nKeep this managed block so 'openspec update' can refresh the instructions.\n";
|
|
2
|
+
//# sourceMappingURL=agents-root-stub.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const agentsRootStubTemplate = `# OpenSpec Instructions
|
|
2
|
+
|
|
3
|
+
These instructions are for AI assistants working in this project.
|
|
4
|
+
|
|
5
|
+
Always open \`@/openspec/AGENTS.md\` when the request:
|
|
6
|
+
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
|
7
|
+
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
|
8
|
+
- Sounds ambiguous and you need the authoritative spec before coding
|
|
9
|
+
|
|
10
|
+
Use \`@/openspec/AGENTS.md\` to learn:
|
|
11
|
+
- How to create and apply change proposals
|
|
12
|
+
- Spec format and conventions
|
|
13
|
+
- Project structure and guidelines
|
|
14
|
+
|
|
15
|
+
Keep this managed block so 'openspec update' can refresh the instructions.
|
|
16
|
+
`;
|
|
17
|
+
//# sourceMappingURL=agents-root-stub.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { agentsRootStubTemplate as claudeTemplate } from './agents-root-stub.js';
|
|
2
2
|
//# sourceMappingURL=claude-template.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { agentsRootStubTemplate as claudeTemplate } from './agents-root-stub.js';
|
|
2
2
|
//# sourceMappingURL=claude-template.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { agentsTemplate } from './agents-template.js';
|
|
2
2
|
import { projectTemplate } from './project-template.js';
|
|
3
3
|
import { claudeTemplate } from './claude-template.js';
|
|
4
|
+
import { agentsRootStubTemplate } from './agents-root-stub.js';
|
|
4
5
|
import { getSlashCommandBody } from './slash-command-templates.js';
|
|
5
6
|
export class TemplateManager {
|
|
6
7
|
static getTemplates(context = {}) {
|
|
@@ -19,7 +20,7 @@ export class TemplateManager {
|
|
|
19
20
|
return claudeTemplate;
|
|
20
21
|
}
|
|
21
22
|
static getAgentsStandardTemplate() {
|
|
22
|
-
return
|
|
23
|
+
return agentsRootStubTemplate;
|
|
23
24
|
}
|
|
24
25
|
static getSlashCommandBody(id) {
|
|
25
26
|
return getSlashCommandBody(id);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const baseGuardrails = `**Guardrails**
|
|
2
2
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
|
3
3
|
- Keep changes tightly scoped to the requested outcome.
|
|
4
|
-
- Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
|
|
4
|
+
- Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
|
|
5
5
|
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
|
|
6
6
|
const proposalSteps = `**Steps**
|
|
7
7
|
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
|
8
8
|
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
|
|
9
9
|
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
|
10
10
|
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
|
11
|
-
5. Draft spec deltas in \`changes/<id>/specs
|
|
11
|
+
5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
|
12
12
|
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
|
13
13
|
7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
|
|
14
14
|
const proposalReferences = `**Reference**
|
package/dist/core/update.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
3
|
-
import { OPENSPEC_DIR_NAME
|
|
4
|
-
import { agentsTemplate } from './templates/agents-template.js';
|
|
5
|
-
import { TemplateManager } from './templates/index.js';
|
|
3
|
+
import { OPENSPEC_DIR_NAME } from './config.js';
|
|
6
4
|
import { ToolRegistry } from './configurators/registry.js';
|
|
7
5
|
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
6
|
+
import { agentsTemplate } from './templates/agents-template.js';
|
|
8
7
|
export class UpdateCommand {
|
|
9
8
|
async execute(projectPath) {
|
|
10
9
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
@@ -16,34 +15,36 @@ export class UpdateCommand {
|
|
|
16
15
|
}
|
|
17
16
|
// 2. Update AGENTS.md (full replacement)
|
|
18
17
|
const agentsPath = path.join(openspecPath, 'AGENTS.md');
|
|
19
|
-
const rootAgentsPath = path.join(resolvedProjectPath, 'AGENTS.md');
|
|
20
|
-
const rootAgentsExisted = await FileSystemUtils.fileExists(rootAgentsPath);
|
|
21
18
|
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
|
|
22
|
-
const agentsStandardContent = TemplateManager.getAgentsStandardTemplate();
|
|
23
|
-
await FileSystemUtils.updateFileWithMarkers(rootAgentsPath, agentsStandardContent, OPENSPEC_MARKERS.start, OPENSPEC_MARKERS.end);
|
|
24
19
|
// 3. Update existing AI tool configuration files only
|
|
25
20
|
const configurators = ToolRegistry.getAll();
|
|
26
21
|
const slashConfigurators = SlashCommandRegistry.getAll();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
const updatedFiles = [];
|
|
23
|
+
const createdFiles = [];
|
|
24
|
+
const failedFiles = [];
|
|
25
|
+
const updatedSlashFiles = [];
|
|
26
|
+
const failedSlashTools = [];
|
|
31
27
|
for (const configurator of configurators) {
|
|
32
28
|
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
const fileExists = await FileSystemUtils.fileExists(configFilePath);
|
|
30
|
+
const shouldConfigure = fileExists || configurator.configFileName === 'AGENTS.md';
|
|
31
|
+
if (!shouldConfigure) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (fileExists && !await FileSystemUtils.canWriteFile(configFilePath)) {
|
|
36
|
+
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
|
|
41
37
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
await configurator.configure(resolvedProjectPath, openspecPath);
|
|
39
|
+
updatedFiles.push(configurator.configFileName);
|
|
40
|
+
if (!fileExists) {
|
|
41
|
+
createdFiles.push(configurator.configFileName);
|
|
45
42
|
}
|
|
46
43
|
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
failedFiles.push(configurator.configFileName);
|
|
46
|
+
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
47
48
|
}
|
|
48
49
|
for (const slashConfigurator of slashConfigurators) {
|
|
49
50
|
if (!slashConfigurator.isAvailable) {
|
|
@@ -51,30 +52,34 @@ export class UpdateCommand {
|
|
|
51
52
|
}
|
|
52
53
|
try {
|
|
53
54
|
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
|
|
54
|
-
updatedSlashFiles
|
|
55
|
+
updatedSlashFiles.push(...updated);
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
58
|
failedSlashTools.push(slashConfigurator.toolId);
|
|
58
59
|
console.error(`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (updatedFiles.length > 0) {
|
|
66
|
-
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
|
62
|
+
const summaryParts = [];
|
|
63
|
+
const instructionFiles = ['openspec/AGENTS.md'];
|
|
64
|
+
if (updatedFiles.includes('AGENTS.md')) {
|
|
65
|
+
instructionFiles.push(createdFiles.includes('AGENTS.md') ? 'AGENTS.md (created)' : 'AGENTS.md');
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
summaryParts.push(`Updated OpenSpec instructions (${instructionFiles.join(', ')})`);
|
|
68
|
+
const aiToolFiles = updatedFiles.filter((file) => file !== 'AGENTS.md');
|
|
69
|
+
if (aiToolFiles.length > 0) {
|
|
70
|
+
summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`);
|
|
70
71
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
72
|
+
if (updatedSlashFiles.length > 0) {
|
|
73
|
+
summaryParts.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const failedItems = [
|
|
76
|
+
...failedFiles,
|
|
77
|
+
...failedSlashTools.map((toolId) => `slash command refresh (${toolId})`),
|
|
78
|
+
];
|
|
79
|
+
if (failedItems.length > 0) {
|
|
80
|
+
summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
|
|
76
81
|
}
|
|
77
|
-
console.log(
|
|
82
|
+
console.log(summaryParts.join(' | '));
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
//# sourceMappingURL=update.js.map
|
|
@@ -26,7 +26,7 @@ export declare const VALIDATION_MESSAGES: {
|
|
|
26
26
|
readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
|
|
27
27
|
readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
|
|
28
28
|
readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
|
|
29
|
-
readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with .md files
|
|
29
|
+
readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
|
|
30
30
|
readonly GUIDE_MISSING_SPEC_SECTIONS: "Missing required sections. Expected headers: \"## Purpose\" and \"## Requirements\". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...";
|
|
31
31
|
readonly GUIDE_MISSING_CHANGE_SECTIONS: "Missing required sections. Expected headers: \"## Why\" and \"## What Changes\". Ensure deltas are documented in specs/ using delta headers.";
|
|
32
32
|
readonly GUIDE_SCENARIO_FORMAT: "Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...";
|
|
@@ -32,7 +32,7 @@ export const VALIDATION_MESSAGES = {
|
|
|
32
32
|
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
|
33
33
|
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
|
34
34
|
// Guidance snippets (appended to primary messages for remediation)
|
|
35
|
-
GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with .md files
|
|
35
|
+
GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
|
36
36
|
GUIDE_MISSING_SPEC_SECTIONS: 'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
|
|
37
37
|
GUIDE_MISSING_CHANGE_SECTIONS: 'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
|
|
38
38
|
GUIDE_SCENARIO_FORMAT: 'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
function isMarkerOnOwnLine(content, markerIndex, markerLength) {
|
|
4
|
+
let leftIndex = markerIndex - 1;
|
|
5
|
+
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
|
|
6
|
+
const char = content[leftIndex];
|
|
7
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
leftIndex--;
|
|
11
|
+
}
|
|
12
|
+
let rightIndex = markerIndex + markerLength;
|
|
13
|
+
while (rightIndex < content.length && content[rightIndex] !== '\n') {
|
|
14
|
+
const char = content[rightIndex];
|
|
15
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
rightIndex++;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function findMarkerIndex(content, marker, fromIndex = 0) {
|
|
23
|
+
let currentIndex = content.indexOf(marker, fromIndex);
|
|
24
|
+
while (currentIndex !== -1) {
|
|
25
|
+
if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
|
|
26
|
+
return currentIndex;
|
|
27
|
+
}
|
|
28
|
+
currentIndex = content.indexOf(marker, currentIndex + marker.length);
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
3
32
|
export class FileSystemUtils {
|
|
4
33
|
static async createDirectory(dirPath) {
|
|
5
34
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -56,9 +85,14 @@ export class FileSystemUtils {
|
|
|
56
85
|
let existingContent = '';
|
|
57
86
|
if (await this.fileExists(filePath)) {
|
|
58
87
|
existingContent = await this.readFile(filePath);
|
|
59
|
-
const startIndex = existingContent
|
|
60
|
-
const endIndex =
|
|
88
|
+
const startIndex = findMarkerIndex(existingContent, startMarker);
|
|
89
|
+
const endIndex = startIndex !== -1
|
|
90
|
+
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
|
|
91
|
+
: findMarkerIndex(existingContent, endMarker);
|
|
61
92
|
if (startIndex !== -1 && endIndex !== -1) {
|
|
93
|
+
if (endIndex < startIndex) {
|
|
94
|
+
throw new Error(`Invalid marker state in ${filePath}. End marker appears before start marker.`);
|
|
95
|
+
}
|
|
62
96
|
const before = existingContent.substring(0, startIndex);
|
|
63
97
|
const after = existingContent.substring(endIndex + endMarker.length);
|
|
64
98
|
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
|