@fission-ai/openspec 1.0.2 → 1.1.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.
@@ -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 `---
@@ -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
@@ -310,7 +310,7 @@ export class InitCommand {
310
310
  if (adapter) {
311
311
  const generatedCommands = generateCommands(commandContents, adapter);
312
312
  for (const cmd of generatedCommands) {
313
- const commandFile = path.join(projectPath, cmd.path);
313
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
314
314
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
315
315
  }
316
316
  }
@@ -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)**
@@ -81,7 +81,7 @@ export class UpdateCommand {
81
81
  if (adapter) {
82
82
  const generatedCommands = generateCommands(commandContents, adapter);
83
83
  for (const cmd of generatedCommands) {
84
- const commandFile = path.join(resolvedProjectPath, cmd.path);
84
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
85
85
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
86
86
  }
87
87
  }
@@ -285,7 +285,7 @@ export class UpdateCommand {
285
285
  if (adapter) {
286
286
  const generatedCommands = generateCommands(commandContents, adapter);
287
287
  for (const cmd of generatedCommands) {
288
- const commandFile = path.join(projectPath, cmd.path);
288
+ const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
289
289
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
290
290
  }
291
291
  }
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.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",