@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.
- package/README.md +8 -13
- package/dist/commands/workflow/instructions.js +18 -72
- package/dist/commands/workflow/shared.d.ts +6 -1
- package/dist/commands/workflow/shared.js +21 -16
- package/dist/commands/workflow/status.js +21 -4
- package/dist/commands/workflow/templates.js +5 -4
- package/dist/core/artifact-graph/index.d.ts +1 -0
- package/dist/core/artifact-graph/index.js +1 -0
- package/dist/core/artifact-graph/instruction-loader.js +6 -4
- package/dist/core/artifact-graph/outputs.d.ts +14 -0
- package/dist/core/artifact-graph/outputs.js +39 -0
- package/dist/core/artifact-graph/state.js +2 -25
- 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/parsers/change-parser.js +7 -3
- package/dist/core/parsers/markdown-parser.d.ts +4 -0
- package/dist/core/parsers/markdown-parser.js +41 -1
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/specs-apply.js +8 -0
- package/dist/core/templates/workflows/apply-change.js +4 -4
- 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/templates/workflows/verify-change.js +6 -6
- package/dist/core/update.js +2 -2
- package/dist/core/validation/validator.js +9 -0
- package/dist/telemetry/config.d.ts +7 -1
- package/dist/telemetry/config.js +87 -19
- package/dist/telemetry/index.js +19 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +21 -1
- package/package.json +1 -1
- 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/
|
|
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
|
}
|
|
@@ -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[];
|