@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.
- package/dist/core/archive.js +40 -2
- package/dist/core/command-generation/adapters/codex.d.ts +4 -1
- package/dist/core/command-generation/adapters/codex.js +14 -2
- package/dist/core/command-generation/adapters/opencode.js +4 -1
- package/dist/core/command-generation/adapters/windsurf.d.ts +1 -1
- package/dist/core/command-generation/adapters/windsurf.js +2 -2
- package/dist/core/command-generation/types.d.ts +4 -3
- package/dist/core/config.js +1 -0
- package/dist/core/init.js +5 -2
- package/dist/core/shared/skill-generation.d.ts +2 -1
- package/dist/core/shared/skill-generation.js +6 -2
- package/dist/core/templates/skill-templates.js +2 -2
- package/dist/core/update.js +9 -4
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
package/dist/core/archive.js
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
${
|
|
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/
|
|
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/
|
|
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', '
|
|
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
|
|
34
|
+
* Returns the file path for a command.
|
|
35
35
|
* @param commandId - The command identifier (e.g., 'explore')
|
|
36
|
-
* @returns
|
|
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
|
-
/**
|
|
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;
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
${
|
|
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!
|
|
1921
|
+
All tasks complete! You can archive this change with \`/opsx:archive\`.
|
|
1922
1922
|
\`\`\`
|
|
1923
1923
|
|
|
1924
1924
|
**Output On Pause (Issue Encountered)**
|
package/dist/core/update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|