@fission-ai/openspec 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,44 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
4
4
  import { Validator } from './validation/validator.js';
5
5
  import chalk from 'chalk';
6
6
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
7
+ /**
8
+ * Recursively copy a directory. Used when fs.rename fails (e.g. EPERM on Windows).
9
+ */
10
+ async function copyDirRecursive(src, dest) {
11
+ await fs.mkdir(dest, { recursive: true });
12
+ const entries = await fs.readdir(src, { withFileTypes: true });
13
+ for (const entry of entries) {
14
+ const srcPath = path.join(src, entry.name);
15
+ const destPath = path.join(dest, entry.name);
16
+ if (entry.isDirectory()) {
17
+ await copyDirRecursive(srcPath, destPath);
18
+ }
19
+ else {
20
+ await fs.copyFile(srcPath, destPath);
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * Move a directory from src to dest. On Windows, fs.rename() often fails with
26
+ * EPERM when the directory is non-empty or another process has it open (IDE,
27
+ * file watcher, antivirus). Fall back to copy-then-remove when rename fails
28
+ * with EPERM or EXDEV.
29
+ */
30
+ async function moveDirectory(src, dest) {
31
+ try {
32
+ await fs.rename(src, dest);
33
+ }
34
+ catch (err) {
35
+ const code = err?.code;
36
+ if (code === 'EPERM' || code === 'EXDEV') {
37
+ await copyDirRecursive(src, dest);
38
+ await fs.rm(src, { recursive: true, force: true });
39
+ }
40
+ else {
41
+ throw err;
42
+ }
43
+ }
44
+ }
7
45
  export class ArchiveCommand {
8
46
  async execute(changeName, options = {}) {
9
47
  const targetPath = '.';
@@ -225,8 +263,8 @@ export class ArchiveCommand {
225
263
  }
226
264
  // Create archive directory if needed
227
265
  await fs.mkdir(archiveDir, { recursive: true });
228
- // Move change to archive
229
- await fs.rename(changeDir, archivePath);
266
+ // Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
267
+ await moveDirectory(changeDir, archivePath);
230
268
  console.log(`Change '${changeName}' archived as '${archiveName}'.`);
231
269
  }
232
270
  async selectChange(changesDir) {
@@ -2,11 +2,14 @@
2
2
  * Codex Command Adapter
3
3
  *
4
4
  * Formats commands for Codex following its frontmatter specification.
5
+ * Codex custom prompts live in the global home directory (~/.codex/prompts/)
6
+ * and are not shared through the repository. The CODEX_HOME env var can
7
+ * override the default ~/.codex location.
5
8
  */
6
9
  import type { ToolCommandAdapter } from '../types.js';
7
10
  /**
8
11
  * Codex adapter for command generation.
9
- * File path: .codex/prompts/opsx-<id>.md
12
+ * File path: <CODEX_HOME>/prompts/opsx-<id>.md (absolute, global)
10
13
  * Frontmatter: description, argument-hint
11
14
  */
12
15
  export declare const codexAdapter: ToolCommandAdapter;
@@ -2,17 +2,29 @@
2
2
  * Codex Command Adapter
3
3
  *
4
4
  * Formats commands for Codex following its frontmatter specification.
5
+ * Codex custom prompts live in the global home directory (~/.codex/prompts/)
6
+ * and are not shared through the repository. The CODEX_HOME env var can
7
+ * override the default ~/.codex location.
5
8
  */
9
+ import os from 'os';
6
10
  import path from 'path';
11
+ /**
12
+ * Returns the Codex home directory.
13
+ * Respects the CODEX_HOME env var, defaulting to ~/.codex.
14
+ */
15
+ function getCodexHome() {
16
+ const envHome = process.env.CODEX_HOME?.trim();
17
+ return path.resolve(envHome ? envHome : path.join(os.homedir(), '.codex'));
18
+ }
7
19
  /**
8
20
  * Codex adapter for command generation.
9
- * File path: .codex/prompts/opsx-<id>.md
21
+ * File path: <CODEX_HOME>/prompts/opsx-<id>.md (absolute, global)
10
22
  * Frontmatter: description, argument-hint
11
23
  */
12
24
  export const codexAdapter = {
13
25
  toolId: 'codex',
14
26
  getFilePath(commandId) {
15
- return path.join('.codex', 'prompts', `opsx-${commandId}.md`);
27
+ return path.join(getCodexHome(), 'prompts', `opsx-${commandId}.md`);
16
28
  },
17
29
  formatFile(content) {
18
30
  return `---
@@ -4,6 +4,7 @@
4
4
  * Formats commands for OpenCode following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { transformToHyphenCommands } from '../../../utils/command-references.js';
7
8
  /**
8
9
  * OpenCode adapter for command generation.
9
10
  * File path: .opencode/command/opsx-<id>.md
@@ -15,11 +16,13 @@ export const opencodeAdapter = {
15
16
  return path.join('.opencode', 'command', `opsx-${commandId}.md`);
16
17
  },
17
18
  formatFile(content) {
19
+ // Transform command references from colon to hyphen format for OpenCode
20
+ const transformedBody = transformToHyphenCommands(content.body);
18
21
  return `---
19
22
  description: ${content.description}
20
23
  ---
21
24
 
22
- ${content.body}
25
+ ${transformedBody}
23
26
  `;
24
27
  },
25
28
  };
@@ -7,7 +7,7 @@
7
7
  import type { ToolCommandAdapter } from '../types.js';
8
8
  /**
9
9
  * Windsurf adapter for command generation.
10
- * File path: .windsurf/commands/opsx/<id>.md
10
+ * File path: .windsurf/workflows/opsx-<id>.md
11
11
  * Frontmatter: name, description, category, tags
12
12
  */
13
13
  export declare const windsurfAdapter: ToolCommandAdapter;
@@ -28,13 +28,13 @@ function formatTagsArray(tags) {
28
28
  }
29
29
  /**
30
30
  * Windsurf adapter for command generation.
31
- * File path: .windsurf/commands/opsx/<id>.md
31
+ * File path: .windsurf/workflows/opsx-<id>.md
32
32
  * Frontmatter: name, description, category, tags
33
33
  */
34
34
  export const windsurfAdapter = {
35
35
  toolId: 'windsurf',
36
36
  getFilePath(commandId) {
37
- return path.join('.windsurf', 'commands', 'opsx', `${commandId}.md`);
37
+ return path.join('.windsurf', 'workflows', `opsx-${commandId}.md`);
38
38
  },
39
39
  formatFile(content) {
40
40
  return `---
@@ -31,9 +31,10 @@ export interface ToolCommandAdapter {
31
31
  /** Tool identifier matching AIToolOption.value (e.g., 'claude', 'cursor') */
32
32
  toolId: string;
33
33
  /**
34
- * Returns the relative file path for a command.
34
+ * Returns the file path for a command.
35
35
  * @param commandId - The command identifier (e.g., 'explore')
36
- * @returns Relative path from project root (e.g., '.claude/commands/opsx/explore.md')
36
+ * @returns Path from project root (e.g., '.claude/commands/opsx/explore.md').
37
+ * May be absolute for tools with global-scoped prompts (e.g., Codex).
37
38
  */
38
39
  getFilePath(commandId: string): string;
39
40
  /**
@@ -47,7 +48,7 @@ export interface ToolCommandAdapter {
47
48
  * Result of generating a command file.
48
49
  */
49
50
  export interface GeneratedCommand {
50
- /** Relative file path from project root */
51
+ /** File path from project root, or absolute for global-scoped tools */
51
52
  path: string;
52
53
  /** Complete file content (frontmatter + body) */
53
54
  fileContent: string;
@@ -24,6 +24,7 @@ export const AI_TOOLS = [
24
24
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
25
25
  { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
26
26
  { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
27
+ { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
27
28
  { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' },
28
29
  { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
29
30
  ];
package/dist/core/init.js CHANGED
@@ -10,6 +10,7 @@ import ora from 'ora';
10
10
  import * as fs from 'fs';
11
11
  import { createRequire } from 'module';
12
12
  import { FileSystemUtils } from '../utils/file-system.js';
13
+ import { transformToHyphenCommands } from '../utils/command-references.js';
13
14
  import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
14
15
  import { PALETTE } from './styles/palette.js';
15
16
  import { isInteractive } from '../utils/interactive.js';
@@ -301,7 +302,9 @@ export class InitCommand {
301
302
  const skillDir = path.join(skillsDir, dirName);
302
303
  const skillFile = path.join(skillDir, 'SKILL.md');
303
304
  // Generate SKILL.md content with YAML frontmatter including generatedBy
304
- const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
305
+ // Use hyphen-based command references for OpenCode
306
+ const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
307
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
305
308
  // Write the skill file
306
309
  await FileSystemUtils.writeFile(skillFile, skillContent);
307
310
  }
@@ -310,7 +313,7 @@ export class InitCommand {
310
313
  if (adapter) {
311
314
  const generatedCommands = generateCommands(commandContents, adapter);
312
315
  for (const cmd of generatedCommands) {
313
- const commandFile = path.join(projectPath, cmd.path);
316
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
314
317
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
315
318
  }
316
319
  }
@@ -36,6 +36,7 @@ export declare function getCommandContents(): CommandContent[];
36
36
  *
37
37
  * @param template - The skill template
38
38
  * @param generatedByVersion - The OpenSpec version to embed in the file
39
+ * @param transformInstructions - Optional callback to transform the instructions content
39
40
  */
40
- export declare function generateSkillContent(template: SkillTemplate, generatedByVersion: string): string;
41
+ export declare function generateSkillContent(template: SkillTemplate, generatedByVersion: string, transformInstructions?: (instructions: string) => string): string;
41
42
  //# sourceMappingURL=skill-generation.d.ts.map
@@ -57,8 +57,12 @@ export function getCommandContents() {
57
57
  *
58
58
  * @param template - The skill template
59
59
  * @param generatedByVersion - The OpenSpec version to embed in the file
60
+ * @param transformInstructions - Optional callback to transform the instructions content
60
61
  */
61
- export function generateSkillContent(template, generatedByVersion) {
62
+ export function generateSkillContent(template, generatedByVersion, transformInstructions) {
63
+ const instructions = transformInstructions
64
+ ? transformInstructions(template.instructions)
65
+ : template.instructions;
62
66
  return `---
63
67
  name: ${template.name}
64
68
  description: ${template.description}
@@ -70,7 +74,7 @@ metadata:
70
74
  generatedBy: "${generatedByVersion}"
71
75
  ---
72
76
 
73
- ${template.instructions}
77
+ ${instructions}
74
78
  `;
75
79
  }
76
80
  //# sourceMappingURL=skill-generation.js.map
@@ -1730,7 +1730,7 @@ export function getOpsxContinueCommandTemplate() {
1730
1730
  **If all artifacts are complete (\`isComplete: true\`)**:
1731
1731
  - Congratulate the user
1732
1732
  - Show final status including the schema used
1733
- - Suggest: "All artifacts created! You can now implement this change or archive it."
1733
+ - Suggest: "All artifacts created! You can now implement this change with \`/opsx:apply\` or archive it with \`/opsx:archive\`."
1734
1734
  - STOP
1735
1735
 
1736
1736
  ---
@@ -1918,7 +1918,7 @@ Working on task 4/7: <task description>
1918
1918
  - [x] Task 2
1919
1919
  ...
1920
1920
 
1921
- All tasks complete! Ready to archive this change.
1921
+ All tasks complete! You can archive this change with \`/opsx:archive\`.
1922
1922
  \`\`\`
1923
1923
 
1924
1924
  **Output On Pause (Issue Encountered)**
@@ -9,6 +9,7 @@ import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import { createRequire } from 'module';
11
11
  import { FileSystemUtils } from '../utils/file-system.js';
12
+ import { transformToHyphenCommands } from '../utils/command-references.js';
12
13
  import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
13
14
  import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
14
15
  import { getConfiguredTools, getAllToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
@@ -73,7 +74,9 @@ export class UpdateCommand {
73
74
  for (const { template, dirName } of skillTemplates) {
74
75
  const skillDir = path.join(skillsDir, dirName);
75
76
  const skillFile = path.join(skillDir, 'SKILL.md');
76
- const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
77
+ // Use hyphen-based command references for OpenCode
78
+ const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
79
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
77
80
  await FileSystemUtils.writeFile(skillFile, skillContent);
78
81
  }
79
82
  // Update commands
@@ -81,7 +84,7 @@ export class UpdateCommand {
81
84
  if (adapter) {
82
85
  const generatedCommands = generateCommands(commandContents, adapter);
83
86
  for (const cmd of generatedCommands) {
84
- const commandFile = path.join(resolvedProjectPath, cmd.path);
87
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
85
88
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
86
89
  }
87
90
  }
@@ -277,7 +280,9 @@ export class UpdateCommand {
277
280
  for (const { template, dirName } of skillTemplates) {
278
281
  const skillDir = path.join(skillsDir, dirName);
279
282
  const skillFile = path.join(skillDir, 'SKILL.md');
280
- const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
283
+ // Use hyphen-based command references for OpenCode
284
+ const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
285
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
281
286
  await FileSystemUtils.writeFile(skillFile, skillContent);
282
287
  }
283
288
  // Create commands
@@ -285,7 +290,7 @@ export class UpdateCommand {
285
290
  if (adapter) {
286
291
  const generatedCommands = generateCommands(commandContents, adapter);
287
292
  for (const cmd of generatedCommands) {
288
- const commandFile = path.join(projectPath, cmd.path);
293
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
289
294
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
290
295
  }
291
296
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Command Reference Utilities
3
+ *
4
+ * Utilities for transforming command references to tool-specific formats.
5
+ */
6
+ /**
7
+ * Transforms colon-based command references to hyphen-based format.
8
+ * Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax.
9
+ *
10
+ * @param text - The text containing command references
11
+ * @returns Text with command references transformed to hyphen format
12
+ *
13
+ * @example
14
+ * transformToHyphenCommands('/opsx:new') // returns '/opsx-new'
15
+ * transformToHyphenCommands('Use /opsx:apply to implement') // returns 'Use /opsx-apply to implement'
16
+ */
17
+ export declare function transformToHyphenCommands(text: string): string;
18
+ //# sourceMappingURL=command-references.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Command Reference Utilities
3
+ *
4
+ * Utilities for transforming command references to tool-specific formats.
5
+ */
6
+ /**
7
+ * Transforms colon-based command references to hyphen-based format.
8
+ * Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax.
9
+ *
10
+ * @param text - The text containing command references
11
+ * @returns Text with command references transformed to hyphen format
12
+ *
13
+ * @example
14
+ * transformToHyphenCommands('/opsx:new') // returns '/opsx-new'
15
+ * transformToHyphenCommands('Use /opsx:apply to implement') // returns 'Use /opsx-apply to implement'
16
+ */
17
+ export function transformToHyphenCommands(text) {
18
+ return text.replace(/\/opsx:/g, '/opsx-');
19
+ }
20
+ //# sourceMappingURL=command-references.js.map
@@ -2,4 +2,5 @@ export { validateChangeName, createChange } from './change-utils.js';
2
2
  export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
3
3
  export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
4
4
  export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
5
+ export { transformToHyphenCommands } from './command-references.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,6 @@ export { validateChangeName, createChange } from './change-utils.js';
4
4
  export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
5
5
  // File system utilities
6
6
  export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
7
+ // Command reference utilities
8
+ export { transformToHyphenCommands } from './command-references.js';
7
9
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",