@fission-ai/openspec 0.5.0 → 0.6.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
@@ -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: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
@@ -0,0 +1,7 @@
1
+ export declare const PALETTE: {
2
+ white: import("chalk").ChalkInstance;
3
+ lightGray: import("chalk").ChalkInstance;
4
+ midGray: import("chalk").ChalkInstance;
5
+ darkGray: import("chalk").ChalkInstance;
6
+ };
7
+ //# sourceMappingURL=palette.d.ts.map
@@ -0,0 +1,8 @@
1
+ import chalk from 'chalk';
2
+ export const PALETTE = {
3
+ white: chalk.hex('#f4f4f4'),
4
+ lightGray: chalk.hex('#c8c8c8'),
5
+ midGray: chalk.hex('#8a8a8a'),
6
+ darkGray: chalk.hex('#4a4a4a')
7
+ };
8
+ //# sourceMappingURL=palette.js.map
@@ -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 { agentsTemplate as claudeTemplate } from './agents-template.js';
1
+ export { agentsRootStubTemplate as claudeTemplate } from './agents-root-stub.js';
2
2
  //# sourceMappingURL=claude-template.d.ts.map
@@ -1,2 +1,2 @@
1
- export { agentsTemplate as claudeTemplate } from './agents-template.js';
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 agentsTemplate;
23
+ return agentsRootStubTemplate;
23
24
  }
24
25
  static getSlashCommandBody(id) {
25
26
  return getSlashCommandBody(id);
@@ -1,10 +1,9 @@
1
1
  import path from 'path';
2
2
  import { FileSystemUtils } from '../utils/file-system.js';
3
- import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
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
- let updatedFiles = [];
28
- let failedFiles = [];
29
- let updatedSlashFiles = [];
30
- let failedSlashTools = [];
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
- // Only update if the file already exists
34
- if (await FileSystemUtils.fileExists(configFilePath)) {
35
- try {
36
- if (!await FileSystemUtils.canWriteFile(configFilePath)) {
37
- throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
38
- }
39
- await configurator.configure(resolvedProjectPath, openspecPath);
40
- updatedFiles.push(configurator.configFileName);
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
- catch (error) {
43
- failedFiles.push(configurator.configFileName);
44
- console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
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 = updatedSlashFiles.concat(updated);
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
- // 4. Success message (ASCII-safe)
62
- const instructionUpdates = ['openspec/AGENTS.md'];
63
- instructionUpdates.push(`AGENTS.md${rootAgentsExisted ? '' : ' (created)'}`);
64
- const messages = [`Updated OpenSpec instructions (${instructionUpdates.join(', ')})`];
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
- if (updatedSlashFiles.length > 0) {
69
- messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
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 (failedFiles.length > 0) {
72
- messages.push(`Failed to update: ${failedFiles.join(', ')}`);
72
+ if (updatedSlashFiles.length > 0) {
73
+ summaryParts.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
73
74
  }
74
- if (failedSlashTools.length > 0) {
75
- messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
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(messages.join('\n'));
82
+ console.log(summaryParts.join(' | '));
78
83
  }
79
84
  }
80
85
  //# sourceMappingURL=update.js.map
@@ -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.indexOf(startMarker);
60
- const endIndex = existingContent.indexOf(endMarker);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",