@fission-ai/openspec 0.2.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/dist/core/init.js CHANGED
@@ -1,65 +1,307 @@
1
1
  import path from 'path';
2
- import { select } from '@inquirer/prompts';
2
+ import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
3
+ import chalk from 'chalk';
3
4
  import ora from 'ora';
4
5
  import { FileSystemUtils } from '../utils/file-system.js';
5
6
  import { TemplateManager } from './templates/index.js';
6
7
  import { ToolRegistry } from './configurators/registry.js';
7
8
  import { SlashCommandRegistry } from './configurators/slash/registry.js';
8
- import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
9
+ import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
10
+ const PROGRESS_SPINNER = {
11
+ interval: 80,
12
+ frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
13
+ };
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
+ const LETTER_MAP = {
21
+ O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
22
+ P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
23
+ E: ['██████', '██ ', '█████ ', '██ ', '██████'],
24
+ N: ['██ ██', '███ ██', '██ ███', '██ ██', '██ ██'],
25
+ S: [' █████', '██ ', ' ████ ', ' ██', '█████ '],
26
+ C: [' █████', '██ ', '██ ', '██ ', ' █████'],
27
+ ' ': [' ', ' ', ' ', ' ', ' '],
28
+ };
29
+ const sanitizeToolLabel = (raw) => raw.replace(/✅/gu, '✔').trim();
30
+ const parseToolLabel = (raw) => {
31
+ const sanitized = sanitizeToolLabel(raw);
32
+ const match = sanitized.match(/^(.*?)\s*\((.+)\)$/u);
33
+ if (!match) {
34
+ return { primary: sanitized };
35
+ }
36
+ return {
37
+ primary: match[1].trim(),
38
+ annotation: match[2].trim(),
39
+ };
40
+ };
41
+ const toolSelectionWizard = createPrompt((config, done) => {
42
+ const totalSteps = 3;
43
+ const [step, setStep] = useState('intro');
44
+ const [cursor, setCursor] = useState(0);
45
+ const [selected, setSelected] = useState(() => config.initialSelected ?? []);
46
+ const [error, setError] = useState(null);
47
+ const selectedSet = new Set(selected);
48
+ const pageSize = Math.max(Math.min(config.choices.length, 7), 1);
49
+ const updateSelected = (next) => {
50
+ const ordered = config.choices
51
+ .map((choice) => choice.value)
52
+ .filter((value) => next.has(value));
53
+ setSelected(ordered);
54
+ };
55
+ const page = usePagination({
56
+ items: config.choices,
57
+ active: cursor,
58
+ pageSize,
59
+ loop: config.choices.length > 1,
60
+ renderItem: ({ item, isActive }) => {
61
+ const isSelected = selectedSet.has(item.value);
62
+ const cursorSymbol = isActive
63
+ ? PALETTE.white('›')
64
+ : PALETTE.midGray(' ');
65
+ const indicator = isSelected
66
+ ? PALETTE.white('◉')
67
+ : PALETTE.midGray('○');
68
+ const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
69
+ const label = `${nameColor(item.label.primary)}${item.configured ? PALETTE.midGray(' (already configured)') : ''}`;
70
+ return `${cursorSymbol} ${indicator} ${label}`;
71
+ },
72
+ });
73
+ useKeypress((key) => {
74
+ if (step === 'intro') {
75
+ if (isEnterKey(key)) {
76
+ setStep('select');
77
+ }
78
+ return;
79
+ }
80
+ if (step === 'select') {
81
+ if (isUpKey(key)) {
82
+ const previousIndex = cursor <= 0 ? config.choices.length - 1 : cursor - 1;
83
+ setCursor(previousIndex);
84
+ setError(null);
85
+ return;
86
+ }
87
+ if (isDownKey(key)) {
88
+ const nextIndex = cursor >= config.choices.length - 1 ? 0 : cursor + 1;
89
+ setCursor(nextIndex);
90
+ setError(null);
91
+ return;
92
+ }
93
+ if (isSpaceKey(key)) {
94
+ const current = config.choices[cursor];
95
+ if (!current)
96
+ return;
97
+ const next = new Set(selected);
98
+ if (next.has(current.value)) {
99
+ next.delete(current.value);
100
+ }
101
+ else {
102
+ next.add(current.value);
103
+ }
104
+ updateSelected(next);
105
+ setError(null);
106
+ return;
107
+ }
108
+ if (isEnterKey(key)) {
109
+ if (selected.length === 0) {
110
+ setError('Select at least one AI tool to continue.');
111
+ return;
112
+ }
113
+ setStep('review');
114
+ setError(null);
115
+ return;
116
+ }
117
+ if (key.name === 'escape') {
118
+ setSelected([]);
119
+ setError(null);
120
+ }
121
+ return;
122
+ }
123
+ if (step === 'review') {
124
+ if (isEnterKey(key)) {
125
+ const finalSelection = config.choices
126
+ .map((choice) => choice.value)
127
+ .filter((value) => selectedSet.has(value));
128
+ done(finalSelection);
129
+ return;
130
+ }
131
+ if (isBackspaceKey(key) || key.name === 'escape') {
132
+ setStep('select');
133
+ setError(null);
134
+ }
135
+ }
136
+ });
137
+ const selectedNames = config.choices
138
+ .filter((choice) => selectedSet.has(choice.value))
139
+ .map((choice) => choice.label.primary);
140
+ const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
141
+ const lines = [];
142
+ lines.push(PALETTE.midGray(`Step ${stepIndex}/${totalSteps}`));
143
+ lines.push('');
144
+ if (step === 'intro') {
145
+ const introHeadline = config.extendMode
146
+ ? 'Extend your OpenSpec tooling'
147
+ : 'Configure your OpenSpec tooling';
148
+ const introBody = config.extendMode
149
+ ? 'We detected an existing setup. We will help you refresh or add integrations.'
150
+ : "Let's get your AI assistants connected so they understand OpenSpec.";
151
+ lines.push(PALETTE.white(introHeadline));
152
+ lines.push(PALETTE.midGray(introBody));
153
+ lines.push('');
154
+ lines.push(PALETTE.midGray('Press Enter to continue.'));
155
+ }
156
+ else if (step === 'select') {
157
+ lines.push(PALETTE.white(config.baseMessage));
158
+ lines.push(PALETTE.midGray('Use ↑/↓ to move · Space to toggle · Enter to review selections.'));
159
+ lines.push('');
160
+ lines.push(page);
161
+ lines.push('');
162
+ if (selectedNames.length === 0) {
163
+ lines.push(`${PALETTE.midGray('Selected')}: ${PALETTE.midGray('None selected yet')}`);
164
+ }
165
+ else {
166
+ lines.push(PALETTE.midGray('Selected:'));
167
+ selectedNames.forEach((name) => {
168
+ lines.push(` ${PALETTE.white('-')} ${PALETTE.white(name)}`);
169
+ });
170
+ }
171
+ }
172
+ else {
173
+ lines.push(PALETTE.white('Review selections'));
174
+ lines.push(PALETTE.midGray('Press Enter to confirm or Backspace to adjust.'));
175
+ lines.push('');
176
+ if (selectedNames.length === 0) {
177
+ lines.push(PALETTE.midGray('No tools selected. Press Backspace to return.'));
178
+ }
179
+ else {
180
+ selectedNames.forEach((name) => {
181
+ lines.push(`${PALETTE.white('▌')} ${PALETTE.white(name)}`);
182
+ });
183
+ }
184
+ }
185
+ if (error) {
186
+ return [lines.join('\n'), chalk.red(error)];
187
+ }
188
+ return lines.join('\n');
189
+ });
9
190
  export class InitCommand {
191
+ prompt;
192
+ constructor(options = {}) {
193
+ this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
194
+ }
10
195
  async execute(targetPath) {
11
196
  const projectPath = path.resolve(targetPath);
12
197
  const openspecDir = OPENSPEC_DIR_NAME;
13
198
  const openspecPath = path.join(projectPath, openspecDir);
14
199
  // Validation happens silently in the background
15
- await this.validate(projectPath, openspecPath);
200
+ const extendMode = await this.validate(projectPath, openspecPath);
201
+ const existingToolStates = await this.getExistingToolStates(projectPath);
202
+ this.renderBanner(extendMode);
16
203
  // Get configuration (after validation to avoid prompts if validation fails)
17
- const config = await this.getConfiguration();
204
+ 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
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
213
+ const selectedIds = new Set(config.aiTools);
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]);
18
219
  // Step 1: Create directory structure
19
- const structureSpinner = ora({ text: 'Creating OpenSpec structure...', stream: process.stdout }).start();
20
- await this.createDirectoryStructure(openspecPath);
21
- await this.generateFiles(openspecPath, config);
22
- structureSpinner.succeed('OpenSpec structure created');
220
+ if (!extendMode) {
221
+ const structureSpinner = this.startSpinner('Creating OpenSpec structure...');
222
+ await this.createDirectoryStructure(openspecPath);
223
+ await this.generateFiles(openspecPath, config);
224
+ structureSpinner.stopAndPersist({
225
+ symbol: PALETTE.white('▌'),
226
+ text: PALETTE.white('OpenSpec structure created'),
227
+ });
228
+ }
229
+ else {
230
+ ora({ stream: process.stdout }).info(PALETTE.midGray('ℹ OpenSpec already initialized. Skipping base scaffolding.'));
231
+ }
23
232
  // Step 2: Configure AI tools
24
- const toolSpinner = ora({ text: 'Configuring AI tools...', stream: process.stdout }).start();
233
+ const toolSpinner = this.startSpinner('Configuring AI tools...');
25
234
  await this.configureAITools(projectPath, openspecDir, config.aiTools);
26
- toolSpinner.succeed('AI tools configured');
235
+ toolSpinner.stopAndPersist({
236
+ symbol: PALETTE.white('▌'),
237
+ text: PALETTE.white('AI tools configured'),
238
+ });
27
239
  // Success message
28
- this.displaySuccessMessage(openspecDir, config);
240
+ this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode);
29
241
  }
30
- async validate(projectPath, openspecPath) {
31
- // Check if OpenSpec already exists
32
- if (await FileSystemUtils.directoryExists(openspecPath)) {
33
- throw new Error(`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
34
- `Use 'openspec update' to update the structure.`);
35
- }
242
+ async validate(projectPath, _openspecPath) {
243
+ const extendMode = await FileSystemUtils.directoryExists(_openspecPath);
36
244
  // Check write permissions
37
- if (!await FileSystemUtils.ensureWritePermissions(projectPath)) {
245
+ if (!(await FileSystemUtils.ensureWritePermissions(projectPath))) {
38
246
  throw new Error(`Insufficient permissions to write to ${projectPath}`);
39
247
  }
248
+ return extendMode;
40
249
  }
41
- async getConfiguration() {
42
- const config = {
43
- aiTools: []
44
- };
45
- // Single-select for better UX
46
- const selectedTool = await select({
47
- message: 'Which AI tool do you use?',
48
- choices: AI_TOOLS.map(tool => ({
49
- name: tool.available ? tool.name : `${tool.name} (coming soon)`,
250
+ async getConfiguration(existingTools, extendMode) {
251
+ const selectedTools = await this.promptForAITools(existingTools, extendMode);
252
+ return { aiTools: selectedTools };
253
+ }
254
+ async promptForAITools(existingTools, extendMode) {
255
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
256
+ if (availableTools.length === 0) {
257
+ return [];
258
+ }
259
+ const baseMessage = extendMode
260
+ ? 'Which AI tools would you like to add or refresh?'
261
+ : 'Which AI tools do you use?';
262
+ const initialSelected = extendMode
263
+ ? availableTools
264
+ .filter((tool) => existingTools[tool.value])
265
+ .map((tool) => tool.value)
266
+ : [];
267
+ return this.prompt({
268
+ extendMode,
269
+ baseMessage,
270
+ choices: availableTools.map((tool) => ({
50
271
  value: tool.value,
51
- disabled: !tool.available
52
- }))
272
+ label: parseToolLabel(tool.name),
273
+ configured: Boolean(existingTools[tool.value]),
274
+ })),
275
+ initialSelected,
53
276
  });
54
- config.aiTools = [selectedTool];
55
- return config;
277
+ }
278
+ async getExistingToolStates(projectPath) {
279
+ const states = {};
280
+ for (const tool of AI_TOOLS) {
281
+ states[tool.value] = await this.isToolConfigured(projectPath, tool.value);
282
+ }
283
+ return states;
284
+ }
285
+ async isToolConfigured(projectPath, toolId) {
286
+ const configFile = ToolRegistry.get(toolId)?.configFileName;
287
+ if (configFile &&
288
+ (await FileSystemUtils.fileExists(path.join(projectPath, configFile))))
289
+ return true;
290
+ const slashConfigurator = SlashCommandRegistry.get(toolId);
291
+ if (!slashConfigurator)
292
+ return false;
293
+ for (const target of slashConfigurator.getTargets()) {
294
+ if (await FileSystemUtils.fileExists(path.join(projectPath, target.path)))
295
+ return true;
296
+ }
297
+ return false;
56
298
  }
57
299
  async createDirectoryStructure(openspecPath) {
58
300
  const directories = [
59
301
  openspecPath,
60
302
  path.join(openspecPath, 'specs'),
61
303
  path.join(openspecPath, 'changes'),
62
- path.join(openspecPath, 'changes', 'archive')
304
+ path.join(openspecPath, 'changes', 'archive'),
63
305
  ];
64
306
  for (const dir of directories) {
65
307
  await FileSystemUtils.createDirectory(dir);
@@ -90,25 +332,91 @@ export class InitCommand {
90
332
  }
91
333
  }
92
334
  }
93
- displaySuccessMessage(openspecDir, config) {
335
+ displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode) {
94
336
  console.log(); // Empty line for spacing
95
- ora().succeed('OpenSpec initialized successfully!');
96
- // Get the selected tool name for display
97
- const selectedToolId = config.aiTools[0];
98
- const selectedTool = AI_TOOLS.find(t => t.value === selectedToolId);
99
- const toolName = selectedTool ? selectedTool.name : 'your AI assistant';
100
- console.log(`\nNext steps - Copy these prompts to ${toolName}:\n`);
101
- console.log('────────────────────────────────────────────────────────────');
102
- console.log('1. Populate your project context:');
103
- console.log(' "Please read openspec/project.md and help me fill it out');
104
- console.log(' with details about my project, tech stack, and conventions"\n');
105
- console.log('2. Create your first change proposal:');
106
- console.log(' "I want to add [YOUR FEATURE HERE]. Please create an');
107
- console.log(' OpenSpec change proposal for this feature"\n');
108
- console.log('3. Learn the OpenSpec workflow:');
109
- console.log(' "Please explain the OpenSpec workflow from openspec/AGENTS.md');
110
- console.log(' and how I should work with you on this project"');
111
- console.log('────────────────────────────────────────────────────────────\n');
337
+ const successHeadline = extendMode
338
+ ? 'OpenSpec tool configuration updated!'
339
+ : 'OpenSpec initialized successfully!';
340
+ ora().succeed(PALETTE.white(successHeadline));
341
+ console.log();
342
+ console.log(PALETTE.lightGray('Tool summary:'));
343
+ const summaryLines = [
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,
356
+ ].filter((line) => Boolean(line));
357
+ for (const line of summaryLines) {
358
+ console.log(line);
359
+ }
360
+ console.log();
361
+ console.log(PALETTE.midGray('Use `openspec update` to refresh shared OpenSpec instructions in the future.'));
362
+ // Get the selected tool name(s) for display
363
+ const toolName = this.formatToolNames(selectedTools);
364
+ console.log();
365
+ console.log(`Next steps - Copy these prompts to ${toolName}:`);
366
+ console.log(chalk.gray('────────────────────────────────────────────────────────────'));
367
+ console.log(PALETTE.white('1. Populate your project context:'));
368
+ console.log(PALETTE.lightGray(' "Please read openspec/project.md and help me fill it out'));
369
+ console.log(PALETTE.lightGray(' with details about my project, tech stack, and conventions"\n'));
370
+ console.log(PALETTE.white('2. Create your first change proposal:'));
371
+ console.log(PALETTE.lightGray(' "I want to add [YOUR FEATURE HERE]. Please create an'));
372
+ console.log(PALETTE.lightGray(' OpenSpec change proposal for this feature"\n'));
373
+ console.log(PALETTE.white('3. Learn the OpenSpec workflow:'));
374
+ console.log(PALETTE.lightGray(' "Please explain the OpenSpec workflow from openspec/AGENTS.md'));
375
+ console.log(PALETTE.lightGray(' and how I should work with you on this project"'));
376
+ console.log(PALETTE.darkGray('────────────────────────────────────────────────────────────\n'));
377
+ }
378
+ formatToolNames(tools) {
379
+ const names = tools
380
+ .map((tool) => tool.successLabel ?? tool.name)
381
+ .filter((name) => Boolean(name));
382
+ if (names.length === 0)
383
+ return PALETTE.lightGray('your AI assistant');
384
+ if (names.length === 1)
385
+ return PALETTE.white(names[0]);
386
+ const base = names.slice(0, -1).map((name) => PALETTE.white(name));
387
+ const last = PALETTE.white(names[names.length - 1]);
388
+ return `${base.join(PALETTE.midGray(', '))}${base.length ? PALETTE.midGray(', and ') : ''}${last}`;
389
+ }
390
+ renderBanner(_extendMode) {
391
+ const rows = ['', '', '', '', ''];
392
+ for (const char of 'OPENSPEC') {
393
+ const glyph = LETTER_MAP[char] ?? LETTER_MAP[' '];
394
+ for (let i = 0; i < rows.length; i += 1) {
395
+ rows[i] += `${glyph[i]} `;
396
+ }
397
+ }
398
+ const rowStyles = [
399
+ PALETTE.white,
400
+ PALETTE.lightGray,
401
+ PALETTE.midGray,
402
+ PALETTE.lightGray,
403
+ PALETTE.white,
404
+ ];
405
+ console.log();
406
+ rows.forEach((row, index) => {
407
+ console.log(rowStyles[index](row.replace(/\s+$/u, '')));
408
+ });
409
+ console.log();
410
+ console.log(PALETTE.white('Welcome to OpenSpec!'));
411
+ console.log();
412
+ }
413
+ startSpinner(text) {
414
+ return ora({
415
+ text,
416
+ stream: process.stdout,
417
+ color: 'gray',
418
+ spinner: PROGRESS_SPINNER,
419
+ }).start();
112
420
  }
113
421
  }
114
422
  //# sourceMappingURL=init.js.map
@@ -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 lines = content.split('\n');
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
- this.lines = content.split('\n');
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 lines = content.split('\n');
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 sections = splitTopLevelSections(content);
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*$/);
@@ -1,2 +1,2 @@
1
- export declare const claudeTemplate = "# OpenSpec Project\n\nThis project uses OpenSpec for spec-driven development. Specifications are the source of truth.\n\nSee @openspec/AGENTS.md for detailed conventions and guidelines.\n\n## Three-Stage Workflow\n\n### Stage 1: Creating Changes\nCreate proposal for: features, breaking changes, architecture changes\nSkip proposal for: bug fixes, typos, non-breaking updates\n\n### Stage 2: Implementing Changes\n1. Read proposal.md to understand the change\n2. Read design.md if it exists for technical context\n3. Read tasks.md for implementation checklist\n4. Complete tasks one by one\n5. Mark each task complete immediately: `- [x]`\n6. Validate strictly: `openspec validate [change] --strict`\n7. Approval gate: Do not start implementation until the proposal is approved\n\n### Stage 3: Archiving\nAfter deployment, use `openspec archive [change]` (add `--skip-specs` for tooling-only changes)\n\n## Before Any Task\n\n**Always:**\n- Check existing specs: `openspec list --specs`\n- Check active changes: `openspec list`\n- Read relevant specs before creating new ones\n- Prefer modifying existing specs over creating duplicates\n\n## CLI Quick Reference\n\n```bash\n# Essential\nopenspec list # Active changes\nopenspec list --specs # Existing specifications\nopenspec show [item] # View details\nopenspec validate --strict # Validate thoroughly\nopenspec archive [change] # Archive after deployment\n\n# Interactive\nopenspec show # Prompts for selection\nopenspec validate # Bulk validation\n\n# Debugging\nopenspec show [change] --json --deltas-only\n```\n\n## Creating Changes\n\n1. **Directory:** `changes/[change-id]/`\n - Change ID naming: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`), unique (append `-2`, `-3` if needed)\n2. **Files:**\n - `proposal.md` - Why, what, impact\n - `tasks.md` - Implementation checklist\n - `design.md` - Only if needed (cross-cutting, new deps/data model, security/perf/migration complexity, or high ambiguity)\n - `specs/[capability]/spec.md` - Delta changes (ADDED/MODIFIED/REMOVED). For multiple capabilities, include multiple files.\n3. **If ambiguous:** ask 1\u20132 clarifying questions before scaffolding\n\n## Search Guidance\n- Enumerate specs: `openspec spec list --long` (or `--json`)\n- Enumerate changes: `openspec list`\n- Show details: `openspec show <spec-id> --type spec`, `openspec show <change-id> --json --deltas-only`\n- Full-text search (use ripgrep): `rg -n \"Requirement:|Scenario:\" openspec/specs`\n\n## Critical: Scenario Format\n\n**CORRECT:**\n```markdown\n#### Scenario: User login\n- **WHEN** valid credentials\n- **THEN** return token\n```\n\n**WRONG:** Using bullets (- **Scenario**), bold (**Scenario:**), or ### headers\n\nEvery requirement MUST have scenarios using `#### Scenario:` format.\n\n## Complexity Management\n\n**Default to minimal:**\n- <100 lines of new code\n- Single-file implementations\n- No frameworks without justification\n- Boring, proven patterns\n\n**Only add complexity with:**\n- Performance data showing need\n- Concrete scale requirements (>1000 users)\n- Multiple proven use cases\n\n## Troubleshooting\n\n**\"Change must have at least one delta\"**\n- Check `changes/[name]/specs/` exists\n- Verify operation prefixes (## ADDED Requirements)\n\n**\"Requirement must have at least one scenario\"**\n- Use `#### Scenario:` format (4 hashtags)\n- Don't use bullets or bold\n\n**Debug:** `openspec show [change] --json --deltas-only`\n";
1
+ export { agentsTemplate as claudeTemplate } from './agents-template.js';
2
2
  //# sourceMappingURL=claude-template.d.ts.map
@@ -1,106 +1,2 @@
1
- export const claudeTemplate = `# OpenSpec Project
2
-
3
- This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
4
-
5
- See @openspec/AGENTS.md for detailed conventions and guidelines.
6
-
7
- ## Three-Stage Workflow
8
-
9
- ### Stage 1: Creating Changes
10
- Create proposal for: features, breaking changes, architecture changes
11
- Skip proposal for: bug fixes, typos, non-breaking updates
12
-
13
- ### Stage 2: Implementing Changes
14
- 1. Read proposal.md to understand the change
15
- 2. Read design.md if it exists for technical context
16
- 3. Read tasks.md for implementation checklist
17
- 4. Complete tasks one by one
18
- 5. Mark each task complete immediately: \`- [x]\`
19
- 6. Validate strictly: \`openspec validate [change] --strict\`
20
- 7. Approval gate: Do not start implementation until the proposal is approved
21
-
22
- ### Stage 3: Archiving
23
- After deployment, use \`openspec archive [change]\` (add \`--skip-specs\` for tooling-only changes)
24
-
25
- ## Before Any Task
26
-
27
- **Always:**
28
- - Check existing specs: \`openspec list --specs\`
29
- - Check active changes: \`openspec list\`
30
- - Read relevant specs before creating new ones
31
- - Prefer modifying existing specs over creating duplicates
32
-
33
- ## CLI Quick Reference
34
-
35
- \`\`\`bash
36
- # Essential
37
- openspec list # Active changes
38
- openspec list --specs # Existing specifications
39
- openspec show [item] # View details
40
- openspec validate --strict # Validate thoroughly
41
- openspec archive [change] # Archive after deployment
42
-
43
- # Interactive
44
- openspec show # Prompts for selection
45
- openspec validate # Bulk validation
46
-
47
- # Debugging
48
- openspec show [change] --json --deltas-only
49
- \`\`\`
50
-
51
- ## Creating Changes
52
-
53
- 1. **Directory:** \`changes/[change-id]/\`
54
- - Change ID naming: kebab-case, verb-led (\`add-\`, \`update-\`, \`remove-\`, \`refactor-\`), unique (append \`-2\`, \`-3\` if needed)
55
- 2. **Files:**
56
- - \`proposal.md\` - Why, what, impact
57
- - \`tasks.md\` - Implementation checklist
58
- - \`design.md\` - Only if needed (cross-cutting, new deps/data model, security/perf/migration complexity, or high ambiguity)
59
- - \`specs/[capability]/spec.md\` - Delta changes (ADDED/MODIFIED/REMOVED). For multiple capabilities, include multiple files.
60
- 3. **If ambiguous:** ask 1–2 clarifying questions before scaffolding
61
-
62
- ## Search Guidance
63
- - Enumerate specs: \`openspec spec list --long\` (or \`--json\`)
64
- - Enumerate changes: \`openspec list\`
65
- - Show details: \`openspec show <spec-id> --type spec\`, \`openspec show <change-id> --json --deltas-only\`
66
- - Full-text search (use ripgrep): \`rg -n "Requirement:|Scenario:" openspec/specs\`
67
-
68
- ## Critical: Scenario Format
69
-
70
- **CORRECT:**
71
- \`\`\`markdown
72
- #### Scenario: User login
73
- - **WHEN** valid credentials
74
- - **THEN** return token
75
- \`\`\`
76
-
77
- **WRONG:** Using bullets (- **Scenario**), bold (**Scenario:**), or ### headers
78
-
79
- Every requirement MUST have scenarios using \`#### Scenario:\` format.
80
-
81
- ## Complexity Management
82
-
83
- **Default to minimal:**
84
- - <100 lines of new code
85
- - Single-file implementations
86
- - No frameworks without justification
87
- - Boring, proven patterns
88
-
89
- **Only add complexity with:**
90
- - Performance data showing need
91
- - Concrete scale requirements (>1000 users)
92
- - Multiple proven use cases
93
-
94
- ## Troubleshooting
95
-
96
- **"Change must have at least one delta"**
97
- - Check \`changes/[name]/specs/\` exists
98
- - Verify operation prefixes (## ADDED Requirements)
99
-
100
- **"Requirement must have at least one scenario"**
101
- - Use \`#### Scenario:\` format (4 hashtags)
102
- - Don't use bullets or bold
103
-
104
- **Debug:** \`openspec show [change] --json --deltas-only\`
105
- `;
1
+ export { agentsTemplate as claudeTemplate } from './agents-template.js';
106
2
  //# sourceMappingURL=claude-template.js.map