@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.
Files changed (32) hide show
  1. package/README.md +8 -9
  2. package/dist/commands/workflow/shared.d.ts +5 -0
  3. package/dist/commands/workflow/shared.js +21 -16
  4. package/dist/commands/workflow/status.js +18 -1
  5. package/dist/core/available-tools.d.ts +3 -2
  6. package/dist/core/available-tools.js +15 -2
  7. package/dist/core/command-generation/adapters/bob.d.ts +14 -0
  8. package/dist/core/command-generation/adapters/bob.js +45 -0
  9. package/dist/core/command-generation/adapters/index.d.ts +3 -0
  10. package/dist/core/command-generation/adapters/index.js +3 -0
  11. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  12. package/dist/core/command-generation/adapters/junie.js +26 -0
  13. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  14. package/dist/core/command-generation/adapters/lingma.js +30 -0
  15. package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
  16. package/dist/core/command-generation/adapters/opencode.js +2 -2
  17. package/dist/core/command-generation/adapters/pi.d.ts +4 -0
  18. package/dist/core/command-generation/adapters/pi.js +15 -1
  19. package/dist/core/command-generation/registry.js +6 -0
  20. package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
  21. package/dist/core/completions/installers/powershell-installer.js +69 -9
  22. package/dist/core/config.d.ts +1 -0
  23. package/dist/core/config.js +5 -1
  24. package/dist/core/init.js +6 -10
  25. package/dist/core/legacy-cleanup.d.ts +1 -1
  26. package/dist/core/legacy-cleanup.js +22 -10
  27. package/dist/core/templates/workflows/bulk-archive-change.js +2 -2
  28. package/dist/core/templates/workflows/explore.js +25 -25
  29. package/dist/core/templates/workflows/onboard.js +24 -24
  30. package/dist/core/update.js +2 -2
  31. package/package.json +1 -1
  32. 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:onboard` to get started. → [Learn more here](docs/opsx.md)
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:new → /opsx:archive workflow -->
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:new add-dark-mode
54
+ You: /opsx:propose add-dark-mode
55
55
  AI: Created openspec/changes/add-dark-mode/
56
- Ready to create: proposal
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:new <what-you-want-to-build>`
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 20+ tools and growing.
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(changesPath, changeName);
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
- * Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
13
- * project root. Only tools with a `skillsDir` property are considered.
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
- * Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
15
- * project root. Only tools with a `skillsDir` property are considered.
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/command/opsx-<id>.md
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/command/opsx-<id>.md
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', 'command', `opsx-${commandId}.md`);
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
- ${content.body}
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
- profileContent = await fs.readFile(profilePath, 'utf-8');
164
+ const file = await this.readProfileFile(profilePath);
165
+ profileContent = file.content;
166
+ fileEncoding = file.encoding;
167
+ fileBom = file.bom;
125
168
  }
126
- catch {
127
- // Profile doesn't exist yet, that's fine
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 fs.writeFile(profilePath, newContent, 'utf-8');
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
- profileContent = await fs.readFile(profilePath, 'utf-8');
220
+ const file = await this.readProfileFile(profilePath);
221
+ profileContent = file.content;
222
+ fileEncoding = file.encoding;
223
+ fileBom = file.bom;
168
224
  }
169
- catch {
170
- continue; // Profile doesn't exist, nothing to remove
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 fs.writeFile(profilePath, newContent, 'utf-8');
249
+ await this.writeProfileFile(profilePath, newContent, fileEncoding, fileBom);
190
250
  anyRemoved = true;
191
251
  }
192
252
  catch (error) {
@@ -12,6 +12,7 @@ export interface AIToolOption {
12
12
  available: boolean;
13
13
  successLabel?: string;
14
14
  skillsDir?: string;
15
+ detectionPaths?: string[];
15
16
  }
16
17
  export declare const AI_TOOLS: AIToolOption[];
17
18
  //# sourceMappingURL=config.d.ts.map
@@ -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 OpenCode
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 foundFiles = await findLegacySlashCommandFiles(projectPath, pattern.pattern);
136
- files.push(...foundFiles);
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 regexPattern = pattern.pattern
470
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
471
- .replace(/\*/g, '.*'); // Replace * with .*
472
- const regex = new RegExp(`^${regexPattern}$`);
473
- if (regex.test(normalizedFile)) {
474
- tools.add(toolId);
475
- break;
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 | Artifacts | Tasks | Specs | Conflicts | Status |
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 | Artifacts | Tasks | Specs | Conflicts | Status |
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
- │ State │────────▶│ State │
54
- │ A │ │ B │
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
- | 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 |
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
- CLI TOOL DATA STORAGE │
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
- │ State │────────▶│ State │
349
- │ A │ │ B │
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
- | 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 |
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 | 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 |
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 | 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 |
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 | 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 |
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 | 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 |
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
  \`\`\`
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Postinstall script for auto-installing shell completions
4
+ * Postinstall script that hints about shell completions
5
5
  *
6
- * This script runs automatically after npm install unless:
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
- // Detect shell
128
- const shell = await detectShell();
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