@fission-ai/openspec 1.2.0 → 1.3.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/README.md +8 -9
- package/dist/commands/workflow/shared.d.ts +5 -0
- package/dist/commands/workflow/shared.js +21 -16
- package/dist/commands/workflow/status.js +18 -1
- package/dist/core/available-tools.d.ts +3 -2
- package/dist/core/available-tools.js +15 -2
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +45 -0
- package/dist/core/command-generation/adapters/index.d.ts +3 -0
- package/dist/core/command-generation/adapters/index.js +3 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
- package/dist/core/command-generation/adapters/opencode.js +2 -2
- package/dist/core/command-generation/adapters/pi.d.ts +4 -0
- package/dist/core/command-generation/adapters/pi.js +15 -1
- package/dist/core/command-generation/registry.js +6 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
- package/dist/core/completions/installers/powershell-installer.js +69 -9
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +5 -1
- package/dist/core/init.js +6 -10
- package/dist/core/legacy-cleanup.d.ts +1 -1
- package/dist/core/legacy-cleanup.js +22 -10
- package/dist/core/templates/workflows/bulk-archive-change.js +2 -2
- package/dist/core/templates/workflows/explore.js +25 -25
- package/dist/core/templates/workflows/onboard.js +24 -24
- package/dist/core/update.js +2 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -72
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Our philosophy:
|
|
|
36
36
|
> [!TIP]
|
|
37
37
|
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
|
|
38
38
|
>
|
|
39
|
-
> Run `/opsx:
|
|
39
|
+
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
|
|
40
40
|
|
|
41
41
|
<p align="center">
|
|
42
42
|
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
|
@@ -46,17 +46,14 @@ Our philosophy:
|
|
|
46
46
|
|
|
47
47
|
Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
|
|
48
48
|
|
|
49
|
-
<!-- TODO: Add GIF demo of /opsx:
|
|
49
|
+
<!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
|
|
50
50
|
|
|
51
51
|
## See it in action
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
|
-
You: /opsx:
|
|
54
|
+
You: /opsx:propose add-dark-mode
|
|
55
55
|
AI: Created openspec/changes/add-dark-mode/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
You: /opsx:ff # "fast-forward" - generate all planning docs
|
|
59
|
-
AI: ✓ proposal.md — why we're doing this, what's changing
|
|
56
|
+
✓ proposal.md — why we're doing this, what's changing
|
|
60
57
|
✓ specs/ — requirements and scenarios
|
|
61
58
|
✓ design.md — technical approach
|
|
62
59
|
✓ tasks.md — implementation checklist
|
|
@@ -101,10 +98,12 @@ cd your-project
|
|
|
101
98
|
openspec init
|
|
102
99
|
```
|
|
103
100
|
|
|
104
|
-
Now tell your AI: `/opsx:
|
|
101
|
+
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
|
|
102
|
+
|
|
103
|
+
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
|
105
104
|
|
|
106
105
|
> [!NOTE]
|
|
107
|
-
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support
|
|
106
|
+
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
|
|
108
107
|
>
|
|
109
108
|
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
|
|
110
109
|
|
|
@@ -37,6 +37,11 @@ export declare function getStatusColor(status: 'done' | 'ready' | 'blocked'): (t
|
|
|
37
37
|
* Gets the status indicator for an artifact.
|
|
38
38
|
*/
|
|
39
39
|
export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string;
|
|
40
|
+
/**
|
|
41
|
+
* Returns the list of available change directory names under openspec/changes/.
|
|
42
|
+
* Excludes the archive directory and hidden directories.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getAvailableChanges(projectRoot: string): Promise<string[]>;
|
|
40
45
|
/**
|
|
41
46
|
* Validates that a change exists and returns available changes if not.
|
|
42
47
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
@@ -52,26 +52,31 @@ export function getStatusIndicator(status) {
|
|
|
52
52
|
return color('[-]');
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the list of available change directory names under openspec/changes/.
|
|
57
|
+
* Excludes the archive directory and hidden directories.
|
|
58
|
+
*/
|
|
59
|
+
export async function getAvailableChanges(projectRoot) {
|
|
60
|
+
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
|
61
|
+
try {
|
|
62
|
+
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
|
63
|
+
return entries
|
|
64
|
+
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
|
65
|
+
.map((e) => e.name);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT')
|
|
69
|
+
return [];
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
55
73
|
/**
|
|
56
74
|
* Validates that a change exists and returns available changes if not.
|
|
57
75
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
58
76
|
*/
|
|
59
77
|
export async function validateChangeExists(changeName, projectRoot) {
|
|
60
|
-
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
|
61
|
-
// Get all change directories (not just those with proposal.md)
|
|
62
|
-
const getAvailableChanges = async () => {
|
|
63
|
-
try {
|
|
64
|
-
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
|
65
|
-
return entries
|
|
66
|
-
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
|
67
|
-
.map((e) => e.name);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
78
|
if (!changeName) {
|
|
74
|
-
const available = await getAvailableChanges();
|
|
79
|
+
const available = await getAvailableChanges(projectRoot);
|
|
75
80
|
if (available.length === 0) {
|
|
76
81
|
throw new Error('No changes found. Create one with: openspec new change <name>');
|
|
77
82
|
}
|
|
@@ -83,10 +88,10 @@ export async function validateChangeExists(changeName, projectRoot) {
|
|
|
83
88
|
throw new Error(`Invalid change name '${changeName}': ${nameValidation.error}`);
|
|
84
89
|
}
|
|
85
90
|
// Check directory existence directly
|
|
86
|
-
const changePath = path.join(
|
|
91
|
+
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
|
|
87
92
|
const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
|
|
88
93
|
if (!exists) {
|
|
89
|
-
const available = await getAvailableChanges();
|
|
94
|
+
const available = await getAvailableChanges(projectRoot);
|
|
90
95
|
if (available.length === 0) {
|
|
91
96
|
throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`);
|
|
92
97
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
|
|
9
|
-
import { validateChangeExists, validateSchemaExists, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
9
|
+
import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
10
10
|
// -----------------------------------------------------------------------------
|
|
11
11
|
// Command Implementation
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
@@ -14,6 +14,23 @@ export async function statusCommand(options) {
|
|
|
14
14
|
const spinner = ora('Loading change status...').start();
|
|
15
15
|
try {
|
|
16
16
|
const projectRoot = process.cwd();
|
|
17
|
+
// Handle no-changes case gracefully — status is informational,
|
|
18
|
+
// so "no changes" is a valid state, not an error.
|
|
19
|
+
if (!options.change) {
|
|
20
|
+
const available = await getAvailableChanges(projectRoot);
|
|
21
|
+
if (available.length === 0) {
|
|
22
|
+
spinner.stop();
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log('No active changes. Create one with: openspec new change <name>');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Changes exist but --change not provided
|
|
31
|
+
spinner.stop();
|
|
32
|
+
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
33
|
+
}
|
|
17
34
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
18
35
|
// Validate schema if explicitly provided
|
|
19
36
|
if (options.schema) {
|
|
@@ -9,8 +9,9 @@ import { type AIToolOption } from './config.js';
|
|
|
9
9
|
* Scans the project path for AI tool configuration directories and returns
|
|
10
10
|
* the tools that are present.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* For tools with `detectionPaths`, checks those specific paths (files or
|
|
13
|
+
* directories). Otherwise checks for the tool's `skillsDir` directory at
|
|
14
|
+
* the project root. Only tools with a `skillsDir` property are considered.
|
|
14
15
|
*/
|
|
15
16
|
export declare function getAvailableTools(projectPath: string): AIToolOption[];
|
|
16
17
|
//# sourceMappingURL=available-tools.d.ts.map
|
|
@@ -11,13 +11,26 @@ import { AI_TOOLS } from './config.js';
|
|
|
11
11
|
* Scans the project path for AI tool configuration directories and returns
|
|
12
12
|
* the tools that are present.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* For tools with `detectionPaths`, checks those specific paths (files or
|
|
15
|
+
* directories). Otherwise checks for the tool's `skillsDir` directory at
|
|
16
|
+
* the project root. Only tools with a `skillsDir` property are considered.
|
|
16
17
|
*/
|
|
17
18
|
export function getAvailableTools(projectPath) {
|
|
18
19
|
return AI_TOOLS.filter((tool) => {
|
|
19
20
|
if (!tool.skillsDir)
|
|
20
21
|
return false;
|
|
22
|
+
if (tool.detectionPaths && tool.detectionPaths.length > 0) {
|
|
23
|
+
// statSync without .isDirectory() — detection paths can be files or directories
|
|
24
|
+
return tool.detectionPaths.some((p) => {
|
|
25
|
+
try {
|
|
26
|
+
fs.statSync(path.join(projectPath, p));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
21
34
|
const dirPath = path.join(projectPath, tool.skillsDir);
|
|
22
35
|
try {
|
|
23
36
|
return fs.statSync(dirPath).isDirectory();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bob Shell Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Bob Shell following its markdown specification.
|
|
5
|
+
* Commands are stored in .bob/commands/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Bob Shell adapter for command generation.
|
|
10
|
+
* File path: .bob/commands/opsx-<id>.md
|
|
11
|
+
* Frontmatter: description, argument-hint
|
|
12
|
+
*/
|
|
13
|
+
export declare const bobAdapter: ToolCommandAdapter;
|
|
14
|
+
//# sourceMappingURL=bob.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bob Shell Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Bob Shell following its markdown specification.
|
|
5
|
+
* Commands are stored in .bob/commands/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
|
9
|
+
/**
|
|
10
|
+
* Escapes a string value for safe YAML output.
|
|
11
|
+
* Quotes the string if it contains special YAML characters.
|
|
12
|
+
*/
|
|
13
|
+
function escapeYamlValue(value) {
|
|
14
|
+
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
|
15
|
+
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
|
16
|
+
if (needsQuoting) {
|
|
17
|
+
// Use double quotes and escape internal double quotes and backslashes
|
|
18
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
19
|
+
return `"${escaped}"`;
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Bob Shell adapter for command generation.
|
|
25
|
+
* File path: .bob/commands/opsx-<id>.md
|
|
26
|
+
* Frontmatter: description, argument-hint
|
|
27
|
+
*/
|
|
28
|
+
export const bobAdapter = {
|
|
29
|
+
toolId: 'bob',
|
|
30
|
+
getFilePath(commandId) {
|
|
31
|
+
return path.join('.bob', 'commands', `opsx-${commandId}.md`);
|
|
32
|
+
},
|
|
33
|
+
formatFile(content) {
|
|
34
|
+
// Transform command references from colon to hyphen format for Bob
|
|
35
|
+
const transformedBody = transformToHyphenCommands(content.body);
|
|
36
|
+
return `---
|
|
37
|
+
description: ${escapeYamlValue(content.description)}
|
|
38
|
+
argument-hint: command arguments
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
${transformedBody}
|
|
42
|
+
`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=bob.js.map
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export { amazonQAdapter } from './amazon-q.js';
|
|
7
7
|
export { antigravityAdapter } from './antigravity.js';
|
|
8
8
|
export { auggieAdapter } from './auggie.js';
|
|
9
|
+
export { bobAdapter } from './bob.js';
|
|
9
10
|
export { claudeAdapter } from './claude.js';
|
|
10
11
|
export { clineAdapter } from './cline.js';
|
|
11
12
|
export { codexAdapter } from './codex.js';
|
|
@@ -18,11 +19,13 @@ export { factoryAdapter } from './factory.js';
|
|
|
18
19
|
export { geminiAdapter } from './gemini.js';
|
|
19
20
|
export { githubCopilotAdapter } from './github-copilot.js';
|
|
20
21
|
export { iflowAdapter } from './iflow.js';
|
|
22
|
+
export { junieAdapter } from './junie.js';
|
|
21
23
|
export { kilocodeAdapter } from './kilocode.js';
|
|
22
24
|
export { kiroAdapter } from './kiro.js';
|
|
23
25
|
export { opencodeAdapter } from './opencode.js';
|
|
24
26
|
export { piAdapter } from './pi.js';
|
|
25
27
|
export { qoderAdapter } from './qoder.js';
|
|
28
|
+
export { lingmaAdapter } from './lingma.js';
|
|
26
29
|
export { qwenAdapter } from './qwen.js';
|
|
27
30
|
export { roocodeAdapter } from './roocode.js';
|
|
28
31
|
export { windsurfAdapter } from './windsurf.js';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export { amazonQAdapter } from './amazon-q.js';
|
|
7
7
|
export { antigravityAdapter } from './antigravity.js';
|
|
8
8
|
export { auggieAdapter } from './auggie.js';
|
|
9
|
+
export { bobAdapter } from './bob.js';
|
|
9
10
|
export { claudeAdapter } from './claude.js';
|
|
10
11
|
export { clineAdapter } from './cline.js';
|
|
11
12
|
export { codexAdapter } from './codex.js';
|
|
@@ -18,11 +19,13 @@ export { factoryAdapter } from './factory.js';
|
|
|
18
19
|
export { geminiAdapter } from './gemini.js';
|
|
19
20
|
export { githubCopilotAdapter } from './github-copilot.js';
|
|
20
21
|
export { iflowAdapter } from './iflow.js';
|
|
22
|
+
export { junieAdapter } from './junie.js';
|
|
21
23
|
export { kilocodeAdapter } from './kilocode.js';
|
|
22
24
|
export { kiroAdapter } from './kiro.js';
|
|
23
25
|
export { opencodeAdapter } from './opencode.js';
|
|
24
26
|
export { piAdapter } from './pi.js';
|
|
25
27
|
export { qoderAdapter } from './qoder.js';
|
|
28
|
+
export { lingmaAdapter } from './lingma.js';
|
|
26
29
|
export { qwenAdapter } from './qwen.js';
|
|
27
30
|
export { roocodeAdapter } from './roocode.js';
|
|
28
31
|
export { windsurfAdapter } from './windsurf.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Junie Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Junie following its frontmatter specification.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Junie adapter for command generation.
|
|
9
|
+
* File path: .junie/commands/opsx-<id>.md
|
|
10
|
+
* Frontmatter: description
|
|
11
|
+
*/
|
|
12
|
+
export declare const junieAdapter: ToolCommandAdapter;
|
|
13
|
+
//# sourceMappingURL=junie.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Junie Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Junie following its frontmatter specification.
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Junie adapter for command generation.
|
|
9
|
+
* File path: .junie/commands/opsx-<id>.md
|
|
10
|
+
* Frontmatter: description
|
|
11
|
+
*/
|
|
12
|
+
export const junieAdapter = {
|
|
13
|
+
toolId: 'junie',
|
|
14
|
+
getFilePath(commandId) {
|
|
15
|
+
return path.join('.junie', 'commands', `opsx-${commandId}.md`);
|
|
16
|
+
},
|
|
17
|
+
formatFile(content) {
|
|
18
|
+
return `---
|
|
19
|
+
description: ${content.description}
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
${content.body}
|
|
23
|
+
`;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=junie.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lingma Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Lingma following its frontmatter specification.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Lingma adapter for command generation.
|
|
9
|
+
* File path: .lingma/commands/opsx/<id>.md
|
|
10
|
+
* Frontmatter: name, description, category, tags
|
|
11
|
+
*/
|
|
12
|
+
export declare const lingmaAdapter: ToolCommandAdapter;
|
|
13
|
+
//# sourceMappingURL=lingma.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lingma Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Lingma following its frontmatter specification.
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Lingma adapter for command generation.
|
|
9
|
+
* File path: .lingma/commands/opsx/<id>.md
|
|
10
|
+
* Frontmatter: name, description, category, tags
|
|
11
|
+
*/
|
|
12
|
+
export const lingmaAdapter = {
|
|
13
|
+
toolId: 'lingma',
|
|
14
|
+
getFilePath(commandId) {
|
|
15
|
+
return path.join('.lingma', 'commands', 'opsx', `${commandId}.md`);
|
|
16
|
+
},
|
|
17
|
+
formatFile(content) {
|
|
18
|
+
const tagsStr = content.tags.join(', ');
|
|
19
|
+
return `---
|
|
20
|
+
name: ${content.name}
|
|
21
|
+
description: ${content.description}
|
|
22
|
+
category: ${content.category}
|
|
23
|
+
tags: [${tagsStr}]
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
${content.body}
|
|
27
|
+
`;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=lingma.js.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { ToolCommandAdapter } from '../types.js';
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode adapter for command generation.
|
|
9
|
-
* File path: .opencode/
|
|
9
|
+
* File path: .opencode/commands/opsx-<id>.md
|
|
10
10
|
* Frontmatter: description
|
|
11
11
|
*/
|
|
12
12
|
export declare const opencodeAdapter: ToolCommandAdapter;
|
|
@@ -7,13 +7,13 @@ import path from 'path';
|
|
|
7
7
|
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
|
8
8
|
/**
|
|
9
9
|
* OpenCode adapter for command generation.
|
|
10
|
-
* File path: .opencode/
|
|
10
|
+
* File path: .opencode/commands/opsx-<id>.md
|
|
11
11
|
* Frontmatter: description
|
|
12
12
|
*/
|
|
13
13
|
export const opencodeAdapter = {
|
|
14
14
|
toolId: 'opencode',
|
|
15
15
|
getFilePath(commandId) {
|
|
16
|
-
return path.join('.opencode', '
|
|
16
|
+
return path.join('.opencode', 'commands', `opsx-${commandId}.md`);
|
|
17
17
|
},
|
|
18
18
|
formatFile(content) {
|
|
19
19
|
// Transform command references from colon to hyphen format for OpenCode
|
|
@@ -9,6 +9,10 @@ import type { ToolCommandAdapter } from '../types.js';
|
|
|
9
9
|
* Pi adapter for prompt template generation.
|
|
10
10
|
* File path: .pi/prompts/opsx-<id>.md
|
|
11
11
|
* Frontmatter: description
|
|
12
|
+
*
|
|
13
|
+
* Pi uses the filename (minus .md) as the slash command name, so
|
|
14
|
+
* opsx-propose.md → /opsx-propose. Command references in the body
|
|
15
|
+
* are transformed from /opsx: to /opsx- for consistency.
|
|
12
16
|
*/
|
|
13
17
|
export declare const piAdapter: ToolCommandAdapter;
|
|
14
18
|
//# sourceMappingURL=pi.d.ts.map
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* Pi prompt templates live in .pi/prompts/*.md with description frontmatter.
|
|
6
6
|
*/
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
|
9
|
+
const PI_INPUT_HEADING = /^\*\*Input\*\*:[^\n]*$/m;
|
|
10
|
+
function injectPiArgs(body) {
|
|
11
|
+
if (body.includes('$@') || body.includes('$ARGUMENTS')) {
|
|
12
|
+
return body;
|
|
13
|
+
}
|
|
14
|
+
return body.replace(PI_INPUT_HEADING, (heading) => `${heading}\n**Provided arguments**: $@`);
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
9
17
|
* Escapes a string value for safe YAML output.
|
|
10
18
|
* Quotes the string if it contains special YAML characters.
|
|
@@ -23,6 +31,10 @@ function escapeYamlValue(value) {
|
|
|
23
31
|
* Pi adapter for prompt template generation.
|
|
24
32
|
* File path: .pi/prompts/opsx-<id>.md
|
|
25
33
|
* Frontmatter: description
|
|
34
|
+
*
|
|
35
|
+
* Pi uses the filename (minus .md) as the slash command name, so
|
|
36
|
+
* opsx-propose.md → /opsx-propose. Command references in the body
|
|
37
|
+
* are transformed from /opsx: to /opsx- for consistency.
|
|
26
38
|
*/
|
|
27
39
|
export const piAdapter = {
|
|
28
40
|
toolId: 'pi',
|
|
@@ -30,11 +42,13 @@ export const piAdapter = {
|
|
|
30
42
|
return path.join('.pi', 'prompts', `opsx-${commandId}.md`);
|
|
31
43
|
},
|
|
32
44
|
formatFile(content) {
|
|
45
|
+
// Transform /opsx: references to /opsx- and inject $@ for template args
|
|
46
|
+
const transformedBody = transformToHyphenCommands(content.body);
|
|
33
47
|
return `---
|
|
34
48
|
description: ${escapeYamlValue(content.description)}
|
|
35
49
|
---
|
|
36
50
|
|
|
37
|
-
${
|
|
51
|
+
${injectPiArgs(transformedBody)}
|
|
38
52
|
`;
|
|
39
53
|
},
|
|
40
54
|
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { amazonQAdapter } from './adapters/amazon-q.js';
|
|
8
8
|
import { antigravityAdapter } from './adapters/antigravity.js';
|
|
9
9
|
import { auggieAdapter } from './adapters/auggie.js';
|
|
10
|
+
import { bobAdapter } from './adapters/bob.js';
|
|
10
11
|
import { claudeAdapter } from './adapters/claude.js';
|
|
11
12
|
import { clineAdapter } from './adapters/cline.js';
|
|
12
13
|
import { codexAdapter } from './adapters/codex.js';
|
|
@@ -19,11 +20,13 @@ import { factoryAdapter } from './adapters/factory.js';
|
|
|
19
20
|
import { geminiAdapter } from './adapters/gemini.js';
|
|
20
21
|
import { githubCopilotAdapter } from './adapters/github-copilot.js';
|
|
21
22
|
import { iflowAdapter } from './adapters/iflow.js';
|
|
23
|
+
import { junieAdapter } from './adapters/junie.js';
|
|
22
24
|
import { kilocodeAdapter } from './adapters/kilocode.js';
|
|
23
25
|
import { kiroAdapter } from './adapters/kiro.js';
|
|
24
26
|
import { opencodeAdapter } from './adapters/opencode.js';
|
|
25
27
|
import { piAdapter } from './adapters/pi.js';
|
|
26
28
|
import { qoderAdapter } from './adapters/qoder.js';
|
|
29
|
+
import { lingmaAdapter } from './adapters/lingma.js';
|
|
27
30
|
import { qwenAdapter } from './adapters/qwen.js';
|
|
28
31
|
import { roocodeAdapter } from './adapters/roocode.js';
|
|
29
32
|
import { windsurfAdapter } from './adapters/windsurf.js';
|
|
@@ -37,6 +40,7 @@ export class CommandAdapterRegistry {
|
|
|
37
40
|
CommandAdapterRegistry.register(amazonQAdapter);
|
|
38
41
|
CommandAdapterRegistry.register(antigravityAdapter);
|
|
39
42
|
CommandAdapterRegistry.register(auggieAdapter);
|
|
43
|
+
CommandAdapterRegistry.register(bobAdapter);
|
|
40
44
|
CommandAdapterRegistry.register(claudeAdapter);
|
|
41
45
|
CommandAdapterRegistry.register(clineAdapter);
|
|
42
46
|
CommandAdapterRegistry.register(codexAdapter);
|
|
@@ -49,11 +53,13 @@ export class CommandAdapterRegistry {
|
|
|
49
53
|
CommandAdapterRegistry.register(geminiAdapter);
|
|
50
54
|
CommandAdapterRegistry.register(githubCopilotAdapter);
|
|
51
55
|
CommandAdapterRegistry.register(iflowAdapter);
|
|
56
|
+
CommandAdapterRegistry.register(junieAdapter);
|
|
52
57
|
CommandAdapterRegistry.register(kilocodeAdapter);
|
|
53
58
|
CommandAdapterRegistry.register(kiroAdapter);
|
|
54
59
|
CommandAdapterRegistry.register(opencodeAdapter);
|
|
55
60
|
CommandAdapterRegistry.register(piAdapter);
|
|
56
61
|
CommandAdapterRegistry.register(qoderAdapter);
|
|
62
|
+
CommandAdapterRegistry.register(lingmaAdapter);
|
|
57
63
|
CommandAdapterRegistry.register(qwenAdapter);
|
|
58
64
|
CommandAdapterRegistry.register(roocodeAdapter);
|
|
59
65
|
CommandAdapterRegistry.register(windsurfAdapter);
|
|
@@ -10,6 +10,20 @@ export declare class PowerShellInstaller {
|
|
|
10
10
|
*/
|
|
11
11
|
private readonly PROFILE_MARKERS;
|
|
12
12
|
constructor(homeDir?: string);
|
|
13
|
+
/**
|
|
14
|
+
* Detect the encoding of a file by inspecting its BOM (Byte Order Mark).
|
|
15
|
+
* Returns the Node.js BufferEncoding and the raw BOM bytes to preserve on write.
|
|
16
|
+
*/
|
|
17
|
+
private detectEncoding;
|
|
18
|
+
/**
|
|
19
|
+
* Read a profile file, preserving its encoding metadata for round-trip writes.
|
|
20
|
+
* Throws if the file uses UTF-16 BE (unsupported by Node).
|
|
21
|
+
*/
|
|
22
|
+
private readProfileFile;
|
|
23
|
+
/**
|
|
24
|
+
* Write a profile file, preserving the original BOM and encoding.
|
|
25
|
+
*/
|
|
26
|
+
private writeProfileFile;
|
|
13
27
|
/**
|
|
14
28
|
* Get PowerShell profile path
|
|
15
29
|
* Prefers $PROFILE environment variable, falls back to platform defaults
|
|
@@ -17,6 +17,44 @@ export class PowerShellInstaller {
|
|
|
17
17
|
constructor(homeDir = os.homedir()) {
|
|
18
18
|
this.homeDir = homeDir;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Detect the encoding of a file by inspecting its BOM (Byte Order Mark).
|
|
22
|
+
* Returns the Node.js BufferEncoding and the raw BOM bytes to preserve on write.
|
|
23
|
+
*/
|
|
24
|
+
detectEncoding(buffer) {
|
|
25
|
+
// UTF-16 LE BOM: FF FE
|
|
26
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
27
|
+
return { encoding: 'utf16le', bom: Buffer.from([0xff, 0xfe]) };
|
|
28
|
+
}
|
|
29
|
+
// UTF-16 BE BOM: FE FF — not natively supported by Node
|
|
30
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
31
|
+
throw new Error('File is encoded as UTF-16 BE which is not supported. ' +
|
|
32
|
+
'Please re-save as UTF-8 or UTF-16 LE, then retry.');
|
|
33
|
+
}
|
|
34
|
+
// UTF-8 BOM: EF BB BF
|
|
35
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
36
|
+
return { encoding: 'utf-8', bom: Buffer.from([0xef, 0xbb, 0xbf]) };
|
|
37
|
+
}
|
|
38
|
+
// No BOM → default UTF-8
|
|
39
|
+
return { encoding: 'utf-8', bom: Buffer.alloc(0) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read a profile file, preserving its encoding metadata for round-trip writes.
|
|
43
|
+
* Throws if the file uses UTF-16 BE (unsupported by Node).
|
|
44
|
+
*/
|
|
45
|
+
async readProfileFile(filePath) {
|
|
46
|
+
const raw = await fs.readFile(filePath);
|
|
47
|
+
const { encoding, bom } = this.detectEncoding(raw);
|
|
48
|
+
const content = raw.subarray(bom.length).toString(encoding);
|
|
49
|
+
return { content, encoding, bom };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Write a profile file, preserving the original BOM and encoding.
|
|
53
|
+
*/
|
|
54
|
+
async writeProfileFile(filePath, content, encoding, bom) {
|
|
55
|
+
const body = Buffer.from(content, encoding);
|
|
56
|
+
await fs.writeFile(filePath, Buffer.concat([bom, body]));
|
|
57
|
+
}
|
|
20
58
|
/**
|
|
21
59
|
* Get PowerShell profile path
|
|
22
60
|
* Prefers $PROFILE environment variable, falls back to platform defaults
|
|
@@ -120,11 +158,24 @@ export class PowerShellInstaller {
|
|
|
120
158
|
const profileDir = path.dirname(profilePath);
|
|
121
159
|
await fs.mkdir(profileDir, { recursive: true });
|
|
122
160
|
let profileContent = '';
|
|
161
|
+
let fileEncoding = 'utf-8';
|
|
162
|
+
let fileBom = Buffer.alloc(0);
|
|
123
163
|
try {
|
|
124
|
-
|
|
164
|
+
const file = await this.readProfileFile(profilePath);
|
|
165
|
+
profileContent = file.content;
|
|
166
|
+
fileEncoding = file.encoding;
|
|
167
|
+
fileBom = file.bom;
|
|
125
168
|
}
|
|
126
|
-
catch {
|
|
127
|
-
//
|
|
169
|
+
catch (err) {
|
|
170
|
+
// If the file doesn't exist that's fine — we'll create it as UTF-8.
|
|
171
|
+
// Any other read error (permissions, unsupported encoding, etc.) → skip this profile.
|
|
172
|
+
if (err?.code === 'ENOENT') {
|
|
173
|
+
// keep defaults
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.warn(`Warning: Skipping ${profilePath}: ${err?.message ?? String(err)}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
128
179
|
}
|
|
129
180
|
// Check if already configured
|
|
130
181
|
const scriptLine = `. "${scriptPath}"`;
|
|
@@ -140,7 +191,7 @@ export class PowerShellInstaller {
|
|
|
140
191
|
'',
|
|
141
192
|
].join('\n');
|
|
142
193
|
const newContent = profileContent + openspecBlock;
|
|
143
|
-
await
|
|
194
|
+
await this.writeProfileFile(profilePath, newContent, fileEncoding, fileBom);
|
|
144
195
|
anyConfigured = true;
|
|
145
196
|
}
|
|
146
197
|
catch (error) {
|
|
@@ -161,13 +212,22 @@ export class PowerShellInstaller {
|
|
|
161
212
|
let anyRemoved = false;
|
|
162
213
|
for (const profilePath of profilePaths) {
|
|
163
214
|
try {
|
|
164
|
-
// Read profile content
|
|
215
|
+
// Read profile content with encoding detection
|
|
165
216
|
let profileContent;
|
|
217
|
+
let fileEncoding = 'utf-8';
|
|
218
|
+
let fileBom = Buffer.alloc(0);
|
|
166
219
|
try {
|
|
167
|
-
|
|
220
|
+
const file = await this.readProfileFile(profilePath);
|
|
221
|
+
profileContent = file.content;
|
|
222
|
+
fileEncoding = file.encoding;
|
|
223
|
+
fileBom = file.bom;
|
|
168
224
|
}
|
|
169
|
-
catch {
|
|
170
|
-
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (err?.code === 'ENOENT') {
|
|
227
|
+
continue; // Profile doesn't exist, nothing to remove
|
|
228
|
+
}
|
|
229
|
+
console.warn(`Warning: Could not read ${profilePath}: ${err?.message ?? String(err)}`);
|
|
230
|
+
continue;
|
|
171
231
|
}
|
|
172
232
|
// Remove OPENSPEC:START -> OPENSPEC:END block
|
|
173
233
|
const startMarker = '# OPENSPEC:START';
|
|
@@ -186,7 +246,7 @@ export class PowerShellInstaller {
|
|
|
186
246
|
const afterBlock = profileContent.substring(endIndex + endMarker.length);
|
|
187
247
|
// Clean up extra newlines
|
|
188
248
|
const newContent = (beforeBlock.trimEnd() + '\n' + afterBlock.trimStart()).trim() + '\n';
|
|
189
|
-
await
|
|
249
|
+
await this.writeProfileFile(profilePath, newContent, fileEncoding, fileBom);
|
|
190
250
|
anyRemoved = true;
|
|
191
251
|
}
|
|
192
252
|
catch (error) {
|
package/dist/core/config.d.ts
CHANGED
package/dist/core/config.js
CHANGED
|
@@ -7,9 +7,11 @@ export const AI_TOOLS = [
|
|
|
7
7
|
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
|
|
8
8
|
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
|
|
9
9
|
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
|
|
10
|
+
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
|
|
10
11
|
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
|
|
11
12
|
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
|
|
12
13
|
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
|
|
14
|
+
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
|
|
13
15
|
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
|
|
14
16
|
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' },
|
|
15
17
|
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' },
|
|
@@ -17,13 +19,15 @@ export const AI_TOOLS = [
|
|
|
17
19
|
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' },
|
|
18
20
|
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
|
|
19
21
|
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
|
|
20
|
-
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github' },
|
|
22
|
+
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },
|
|
21
23
|
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
|
|
24
|
+
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
|
|
22
25
|
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
|
|
23
26
|
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
|
|
24
27
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
|
25
28
|
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
|
26
29
|
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
|
|
30
|
+
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
|
|
27
31
|
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
|
28
32
|
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
|
|
29
33
|
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
|
package/dist/core/init.js
CHANGED
|
@@ -138,17 +138,13 @@ export class InitCommand {
|
|
|
138
138
|
console.log(formatDetectionSummary(detection));
|
|
139
139
|
console.log();
|
|
140
140
|
const canPrompt = this.canPromptInteractively();
|
|
141
|
-
if (this.force) {
|
|
142
|
-
// --force flag: proceed with cleanup automatically
|
|
141
|
+
if (this.force || !canPrompt) {
|
|
142
|
+
// --force flag or non-interactive mode: proceed with cleanup automatically.
|
|
143
|
+
// Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
|
|
144
|
+
// only removes markers (never deletes files), so auto-cleanup is safe.
|
|
143
145
|
await this.performLegacyCleanup(projectPath, detection);
|
|
144
146
|
return;
|
|
145
147
|
}
|
|
146
|
-
if (!canPrompt) {
|
|
147
|
-
// Non-interactive mode without --force: abort
|
|
148
|
-
console.log(chalk.red('Legacy files detected in non-interactive mode.'));
|
|
149
|
-
console.log(chalk.dim('Run interactively to upgrade, or use --force to auto-cleanup.'));
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
148
|
// Interactive mode: prompt for confirmation
|
|
153
149
|
const { confirm } = await import('@inquirer/prompts');
|
|
154
150
|
const shouldCleanup = await confirm({
|
|
@@ -383,8 +379,8 @@ export class InitCommand {
|
|
|
383
379
|
const skillDir = path.join(skillsDir, dirName);
|
|
384
380
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
385
381
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
386
|
-
// Use hyphen-based command references for
|
|
387
|
-
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
382
|
+
// Use hyphen-based command references for tools where filename = command name
|
|
383
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
388
384
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
389
385
|
// Write the skill file
|
|
390
386
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
@@ -19,7 +19,7 @@ export declare const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashComma
|
|
|
19
19
|
export interface LegacySlashCommandPattern {
|
|
20
20
|
type: 'directory' | 'files';
|
|
21
21
|
path?: string;
|
|
22
|
-
pattern?: string;
|
|
22
|
+
pattern?: string | string[];
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Result of legacy artifact detection
|
|
@@ -30,6 +30,7 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
30
30
|
'claude': { type: 'directory', path: '.claude/commands/openspec' },
|
|
31
31
|
'codebuddy': { type: 'directory', path: '.codebuddy/commands/openspec' },
|
|
32
32
|
'qoder': { type: 'directory', path: '.qoder/commands/openspec' },
|
|
33
|
+
'lingma': { type: 'directory', path: '.lingma/commands/openspec' },
|
|
33
34
|
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
|
34
35
|
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
|
35
36
|
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
@@ -44,10 +45,11 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
44
45
|
'roocode': { type: 'files', pattern: '.roo/commands/openspec-*.md' },
|
|
45
46
|
'auggie': { type: 'files', pattern: '.augment/commands/openspec-*.md' },
|
|
46
47
|
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
|
|
47
|
-
'opencode': { type: 'files', pattern: '.opencode/command/openspec-*.md' },
|
|
48
|
+
'opencode': { type: 'files', pattern: ['.opencode/command/opsx-*.md', '.opencode/command/openspec-*.md'] },
|
|
48
49
|
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
49
50
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
50
51
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
52
|
+
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
|
51
53
|
'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
|
|
52
54
|
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
53
55
|
};
|
|
@@ -132,8 +134,11 @@ export async function detectLegacySlashCommands(projectPath) {
|
|
|
132
134
|
}
|
|
133
135
|
else if (pattern.type === 'files' && pattern.pattern) {
|
|
134
136
|
// For file-based patterns, check for individual files
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
138
|
+
for (const p of patterns) {
|
|
139
|
+
const foundFiles = await findLegacySlashCommandFiles(projectPath, p);
|
|
140
|
+
files.push(...foundFiles);
|
|
141
|
+
}
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
return { directories, files };
|
|
@@ -466,14 +471,21 @@ export function getToolsFromLegacyArtifacts(detection) {
|
|
|
466
471
|
if (pattern.type === 'files' && pattern.pattern) {
|
|
467
472
|
// Convert glob pattern to regex for matching
|
|
468
473
|
// e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
474
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
475
|
+
let matched = false;
|
|
476
|
+
for (const p of patterns) {
|
|
477
|
+
const regexPattern = p
|
|
478
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
479
|
+
.replace(/\*/g, '.*'); // Replace * with .*
|
|
480
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
481
|
+
if (regex.test(normalizedFile)) {
|
|
482
|
+
tools.add(toolId);
|
|
483
|
+
matched = true;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
476
486
|
}
|
|
487
|
+
if (matched)
|
|
488
|
+
break;
|
|
477
489
|
}
|
|
478
490
|
}
|
|
479
491
|
}
|
|
@@ -77,7 +77,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|
|
77
77
|
Display a table summarizing all changes:
|
|
78
78
|
|
|
79
79
|
\`\`\`
|
|
80
|
-
| Change
|
|
80
|
+
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
|
81
81
|
|---------------------|-----------|-------|---------|-----------|--------|
|
|
82
82
|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
|
83
83
|
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
|
@@ -323,7 +323,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|
|
323
323
|
Display a table summarizing all changes:
|
|
324
324
|
|
|
325
325
|
\`\`\`
|
|
326
|
-
| Change
|
|
326
|
+
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
|
327
327
|
|---------------------|-----------|-------|---------|-----------|--------|
|
|
328
328
|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
|
329
329
|
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
|
@@ -49,10 +49,10 @@ Depending on what the user brings, you might:
|
|
|
49
49
|
│ Use ASCII diagrams liberally │
|
|
50
50
|
├─────────────────────────────────────────┤
|
|
51
51
|
│ │
|
|
52
|
-
│
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
│
|
|
52
|
+
│ ┌────────┐ ┌────────┐ │
|
|
53
|
+
│ │ State │────────▶│ State │ │
|
|
54
|
+
│ │ A │ │ B │ │
|
|
55
|
+
│ └────────┘ └────────┘ │
|
|
56
56
|
│ │
|
|
57
57
|
│ System diagrams, state machines, │
|
|
58
58
|
│ data flows, architecture sketches, │
|
|
@@ -107,14 +107,14 @@ If the user mentions a change or you detect one is relevant:
|
|
|
107
107
|
|
|
108
108
|
3. **Offer to capture when decisions are made**
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
| Insight Type | Where to Capture |
|
|
111
|
+
|----------------------------|--------------------------------|
|
|
112
|
+
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
|
113
|
+
| Requirement changed | \`specs/<capability>/spec.md\` |
|
|
114
|
+
| Design decision made | \`design.md\` |
|
|
115
|
+
| Scope changed | \`proposal.md\` |
|
|
116
|
+
| New work identified | \`tasks.md\` |
|
|
117
|
+
| Assumption invalidated | Relevant artifact |
|
|
118
118
|
|
|
119
119
|
Example offers:
|
|
120
120
|
- "That's a design decision. Capture it in design.md?"
|
|
@@ -220,7 +220,7 @@ User: A CLI tool that tracks local dev environments
|
|
|
220
220
|
You: That changes everything.
|
|
221
221
|
|
|
222
222
|
┌─────────────────────────────────────────────────┐
|
|
223
|
-
│
|
|
223
|
+
│ CLI TOOL DATA STORAGE │
|
|
224
224
|
└─────────────────────────────────────────────────┘
|
|
225
225
|
|
|
226
226
|
Key constraints:
|
|
@@ -344,10 +344,10 @@ Depending on what the user brings, you might:
|
|
|
344
344
|
│ Use ASCII diagrams liberally │
|
|
345
345
|
├─────────────────────────────────────────┤
|
|
346
346
|
│ │
|
|
347
|
-
│
|
|
348
|
-
│
|
|
349
|
-
│
|
|
350
|
-
│
|
|
347
|
+
│ ┌────────┐ ┌────────┐ │
|
|
348
|
+
│ │ State │────────▶│ State │ │
|
|
349
|
+
│ │ A │ │ B │ │
|
|
350
|
+
│ └────────┘ └────────┘ │
|
|
351
351
|
│ │
|
|
352
352
|
│ System diagrams, state machines, │
|
|
353
353
|
│ data flows, architecture sketches, │
|
|
@@ -404,14 +404,14 @@ If the user mentions a change or you detect one is relevant:
|
|
|
404
404
|
|
|
405
405
|
3. **Offer to capture when decisions are made**
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
407
|
+
| Insight Type | Where to Capture |
|
|
408
|
+
|----------------------------|--------------------------------|
|
|
409
|
+
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
|
410
|
+
| Requirement changed | \`specs/<capability>/spec.md\` |
|
|
411
|
+
| Design decision made | \`design.md\` |
|
|
412
|
+
| Scope changed | \`proposal.md\` |
|
|
413
|
+
| New work identified | \`tasks.md\` |
|
|
414
|
+
| Assumption invalidated | Relevant artifact |
|
|
415
415
|
|
|
416
416
|
Example offers:
|
|
417
417
|
- "That's a design decision. Capture it in design.md?"
|
|
@@ -468,21 +468,21 @@ This same rhythm works for any size change—a small fix or a major feature.
|
|
|
468
468
|
|
|
469
469
|
**Core workflow:**
|
|
470
470
|
|
|
471
|
-
| Command
|
|
472
|
-
|
|
473
|
-
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
|
474
|
-
| \`/opsx:explore\` | Think through problems before/during work
|
|
475
|
-
| \`/opsx:apply\`
|
|
476
|
-
| \`/opsx:archive\` | Archive a completed change
|
|
471
|
+
| Command | What it does |
|
|
472
|
+
|-------------------|--------------------------------------------|
|
|
473
|
+
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
|
474
|
+
| \`/opsx:explore\` | Think through problems before/during work |
|
|
475
|
+
| \`/opsx:apply\` | Implement tasks from a change |
|
|
476
|
+
| \`/opsx:archive\` | Archive a completed change |
|
|
477
477
|
|
|
478
478
|
**Additional commands:**
|
|
479
479
|
|
|
480
|
-
| Command
|
|
481
|
-
|
|
482
|
-
| \`/opsx:new\`
|
|
483
|
-
| \`/opsx:continue\` | Continue working on an existing change
|
|
484
|
-
| \`/opsx:ff\`
|
|
485
|
-
| \`/opsx:verify\`
|
|
480
|
+
| Command | What it does |
|
|
481
|
+
|--------------------|----------------------------------------------------------|
|
|
482
|
+
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
|
|
483
|
+
| \`/opsx:continue\` | Continue working on an existing change |
|
|
484
|
+
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
|
|
485
|
+
| \`/opsx:verify\` | Verify implementation matches artifacts |
|
|
486
486
|
|
|
487
487
|
---
|
|
488
488
|
|
|
@@ -520,21 +520,21 @@ If the user says they just want to see the commands or skip the tutorial:
|
|
|
520
520
|
|
|
521
521
|
**Core workflow:**
|
|
522
522
|
|
|
523
|
-
| Command
|
|
524
|
-
|
|
525
|
-
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
|
526
|
-
| \`/opsx:explore\`
|
|
527
|
-
| \`/opsx:apply <name>\`
|
|
528
|
-
| \`/opsx:archive <name>\` | Archive when done
|
|
523
|
+
| Command | What it does |
|
|
524
|
+
|--------------------------|--------------------------------------------|
|
|
525
|
+
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
|
526
|
+
| \`/opsx:explore\` | Think through problems (no code changes) |
|
|
527
|
+
| \`/opsx:apply <name>\` | Implement tasks |
|
|
528
|
+
| \`/opsx:archive <name>\` | Archive when done |
|
|
529
529
|
|
|
530
530
|
**Additional commands:**
|
|
531
531
|
|
|
532
|
-
| Command
|
|
533
|
-
|
|
534
|
-
| \`/opsx:new <name>\`
|
|
535
|
-
| \`/opsx:continue <name>\` | Continue an existing change
|
|
536
|
-
| \`/opsx:ff <name>\`
|
|
537
|
-
| \`/opsx:verify <name>\`
|
|
532
|
+
| Command | What it does |
|
|
533
|
+
|---------------------------|-------------------------------------|
|
|
534
|
+
| \`/opsx:new <name>\` | Start a new change, step by step |
|
|
535
|
+
| \`/opsx:continue <name>\` | Continue an existing change |
|
|
536
|
+
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
|
|
537
|
+
| \`/opsx:verify <name>\` | Verify implementation |
|
|
538
538
|
|
|
539
539
|
Try \`/opsx:propose\` to start your first change.
|
|
540
540
|
\`\`\`
|
package/dist/core/update.js
CHANGED
|
@@ -129,7 +129,7 @@ export class UpdateCommand {
|
|
|
129
129
|
const skillDir = path.join(skillsDir, dirName);
|
|
130
130
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
131
131
|
// Use hyphen-based command references for OpenCode
|
|
132
|
-
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
132
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
133
133
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
134
134
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
135
135
|
}
|
|
@@ -504,7 +504,7 @@ export class UpdateCommand {
|
|
|
504
504
|
const skillDir = path.join(skillsDir, dirName);
|
|
505
505
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
506
506
|
// Use hyphen-based command references for OpenCode
|
|
507
|
-
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
507
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
508
508
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
509
509
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
510
510
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Postinstall script
|
|
4
|
+
* Postinstall script that hints about shell completions
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Completion installation is opt-in: the user must run
|
|
7
|
+
* `openspec completion install` explicitly. This script only
|
|
8
|
+
* prints a one-line tip after npm install.
|
|
9
|
+
*
|
|
10
|
+
* The tip is suppressed when:
|
|
7
11
|
* - CI=true environment variable is set
|
|
8
12
|
* - OPENSPEC_NO_COMPLETIONS=1 environment variable is set
|
|
9
13
|
* - dist/ directory doesn't exist (dev setup scenario)
|
|
@@ -48,65 +52,6 @@ async function distExists() {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
/**
|
|
52
|
-
* Detect the user's shell
|
|
53
|
-
*/
|
|
54
|
-
async function detectShell() {
|
|
55
|
-
try {
|
|
56
|
-
const { detectShell } = await import('../dist/utils/shell-detection.js');
|
|
57
|
-
const result = detectShell();
|
|
58
|
-
return result.shell;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
// Fail silently if detection module doesn't exist
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Install completions for the detected shell
|
|
67
|
-
*/
|
|
68
|
-
async function installCompletions(shell) {
|
|
69
|
-
try {
|
|
70
|
-
const { CompletionFactory } = await import('../dist/core/completions/factory.js');
|
|
71
|
-
const { COMMAND_REGISTRY } = await import('../dist/core/completions/command-registry.js');
|
|
72
|
-
|
|
73
|
-
// Check if shell is supported
|
|
74
|
-
if (!CompletionFactory.isSupported(shell)) {
|
|
75
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Generate completion script
|
|
80
|
-
const generator = CompletionFactory.createGenerator(shell);
|
|
81
|
-
const script = generator.generate(COMMAND_REGISTRY);
|
|
82
|
-
|
|
83
|
-
// Install completion script
|
|
84
|
-
const installer = CompletionFactory.createInstaller(shell);
|
|
85
|
-
const result = await installer.install(script);
|
|
86
|
-
|
|
87
|
-
if (result.success) {
|
|
88
|
-
// Show success message based on installation type
|
|
89
|
-
if (result.isOhMyZsh) {
|
|
90
|
-
console.log(`✓ Shell completions installed`);
|
|
91
|
-
console.log(` Restart shell: exec zsh`);
|
|
92
|
-
} else if (result.zshrcConfigured) {
|
|
93
|
-
console.log(`✓ Shell completions installed and configured`);
|
|
94
|
-
console.log(` Restart shell: exec zsh`);
|
|
95
|
-
} else {
|
|
96
|
-
console.log(`✓ Shell completions installed to ~/.zsh/completions/`);
|
|
97
|
-
console.log(` Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)`);
|
|
98
|
-
console.log(` Then: exec zsh`);
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
// Installation failed, show tip for manual install
|
|
102
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
// Fail gracefully - show tip for manual install
|
|
106
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
55
|
/**
|
|
111
56
|
* Main function
|
|
112
57
|
*/
|
|
@@ -124,19 +69,10 @@ async function main() {
|
|
|
124
69
|
return;
|
|
125
70
|
}
|
|
126
71
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
if (!shell) {
|
|
130
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Install completions
|
|
135
|
-
await installCompletions(shell);
|
|
72
|
+
// Completions are opt-in — just print a hint
|
|
73
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
136
74
|
} catch (error) {
|
|
137
75
|
// Fail gracefully - never break npm install
|
|
138
|
-
// Show tip for manual install
|
|
139
|
-
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
140
76
|
}
|
|
141
77
|
}
|
|
142
78
|
|