@fission-ai/openspec 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +8 -13
  2. package/dist/commands/workflow/instructions.js +18 -72
  3. package/dist/commands/workflow/shared.d.ts +6 -1
  4. package/dist/commands/workflow/shared.js +21 -16
  5. package/dist/commands/workflow/status.js +21 -4
  6. package/dist/commands/workflow/templates.js +5 -4
  7. package/dist/core/artifact-graph/index.d.ts +1 -0
  8. package/dist/core/artifact-graph/index.js +1 -0
  9. package/dist/core/artifact-graph/instruction-loader.js +6 -4
  10. package/dist/core/artifact-graph/outputs.d.ts +14 -0
  11. package/dist/core/artifact-graph/outputs.js +39 -0
  12. package/dist/core/artifact-graph/state.js +2 -25
  13. package/dist/core/available-tools.d.ts +3 -2
  14. package/dist/core/available-tools.js +15 -2
  15. package/dist/core/command-generation/adapters/bob.d.ts +14 -0
  16. package/dist/core/command-generation/adapters/bob.js +45 -0
  17. package/dist/core/command-generation/adapters/index.d.ts +3 -0
  18. package/dist/core/command-generation/adapters/index.js +3 -0
  19. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  20. package/dist/core/command-generation/adapters/junie.js +26 -0
  21. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  22. package/dist/core/command-generation/adapters/lingma.js +30 -0
  23. package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
  24. package/dist/core/command-generation/adapters/opencode.js +2 -2
  25. package/dist/core/command-generation/adapters/pi.d.ts +4 -0
  26. package/dist/core/command-generation/adapters/pi.js +15 -1
  27. package/dist/core/command-generation/registry.js +6 -0
  28. package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
  29. package/dist/core/completions/installers/powershell-installer.js +69 -9
  30. package/dist/core/config.d.ts +1 -0
  31. package/dist/core/config.js +5 -1
  32. package/dist/core/init.js +6 -10
  33. package/dist/core/legacy-cleanup.d.ts +1 -1
  34. package/dist/core/legacy-cleanup.js +22 -10
  35. package/dist/core/parsers/change-parser.js +7 -3
  36. package/dist/core/parsers/markdown-parser.d.ts +4 -0
  37. package/dist/core/parsers/markdown-parser.js +41 -1
  38. package/dist/core/parsers/spec-structure.d.ts +9 -0
  39. package/dist/core/parsers/spec-structure.js +88 -0
  40. package/dist/core/specs-apply.js +8 -0
  41. package/dist/core/templates/workflows/apply-change.js +4 -4
  42. package/dist/core/templates/workflows/bulk-archive-change.js +2 -2
  43. package/dist/core/templates/workflows/explore.js +25 -25
  44. package/dist/core/templates/workflows/onboard.js +24 -24
  45. package/dist/core/templates/workflows/verify-change.js +6 -6
  46. package/dist/core/update.js +2 -2
  47. package/dist/core/validation/validator.js +9 -0
  48. package/dist/telemetry/config.d.ts +7 -1
  49. package/dist/telemetry/config.js +87 -19
  50. package/dist/telemetry/index.js +19 -0
  51. package/dist/utils/file-system.d.ts +5 -0
  52. package/dist/utils/file-system.js +21 -1
  53. package/package.json +1 -1
  54. package/scripts/postinstall.js +8 -72
@@ -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
  }
@@ -148,15 +148,19 @@ export class ChangeParser extends MarkdownParser {
148
148
  parseSectionsFromContent(content) {
149
149
  const normalizedContent = ChangeParser.normalizeContent(content);
150
150
  const lines = normalizedContent.split('\n');
151
+ const codeFenceLineMask = ChangeParser.buildCodeFenceMask(lines);
151
152
  const sections = [];
152
153
  const stack = [];
153
154
  for (let i = 0; i < lines.length; i++) {
154
155
  const line = lines[i];
156
+ if (codeFenceLineMask[i]) {
157
+ continue;
158
+ }
155
159
  const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
156
160
  if (headerMatch) {
157
161
  const level = headerMatch[1].length;
158
162
  const title = headerMatch[2].trim();
159
- const contentLines = this.getContentUntilNextHeaderFromLines(lines, i + 1, level);
163
+ const contentLines = this.getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, i + 1, level);
160
164
  const section = {
161
165
  level,
162
166
  title,
@@ -177,11 +181,11 @@ export class ChangeParser extends MarkdownParser {
177
181
  }
178
182
  return sections;
179
183
  }
180
- getContentUntilNextHeaderFromLines(lines, startLine, currentLevel) {
184
+ getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, startLine, currentLevel) {
181
185
  const contentLines = [];
182
186
  for (let i = startLine; i < lines.length; i++) {
183
187
  const line = lines[i];
184
- const headerMatch = line.match(/^(#{1,6})\s+/);
188
+ const headerMatch = codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
185
189
  if (headerMatch && headerMatch[1].length <= currentLevel) {
186
190
  break;
187
191
  }
@@ -7,9 +7,13 @@ export interface Section {
7
7
  }
8
8
  export declare class MarkdownParser {
9
9
  private lines;
10
+ private codeFenceLineMask;
10
11
  private currentLine;
11
12
  constructor(content: string);
12
13
  protected static normalizeContent(content: string): string;
14
+ protected static buildCodeFenceMask(lines: string[]): boolean[];
15
+ private static getFenceMarker;
16
+ private static isClosingFence;
13
17
  parseSpec(name: string): Spec;
14
18
  parseChange(name: string): Change;
15
19
  protected parseSections(): Section[];