diff --git a/.changeset/friendly-commandcode-support.md b/.changeset/friendly-commandcode-support.md new file mode 100644 index 000000000..eeabaf000 --- /dev/null +++ b/.changeset/friendly-commandcode-support.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": minor +--- + +### New Features + +- **Command Code support** — `openspec init` now supports Command Code as an adapterless skills-only tool. It installs the OpenSpec skills under `.commandcode/skills/` and invokes them as `/openspec-*` commands, matching Command Code's native skill surface. diff --git a/docs/cli.md b/docs/cli.md index c76ffb9ad..c294aa32b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -109,7 +109,7 @@ openspec init [path] [options] The welcome animation is also skipped when the `OPENSPEC_NO_ANIMATION` environment variable is set (any value, including empty), when `NO_COLOR` is set to a non-empty value, or when the OS reduced-motion preference is enabled (macOS Reduce Motion, GNOME animations disabled). -**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents` +**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents` > This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths. diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 756a80e87..5d8fe04eb 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -33,7 +33,7 @@ way it loads the file OpenSpec wrote. Find your tool's command path in the | `.../opsx-.*` — the filename is the command | `/opsx-` | Every other tool with generated command files, except Amazon Q and Devin | | `.devin/workflows/opsx-.md` — read by only one of Devin's two agents | `/opsx-` on Devin Desktop, `/openspec-` on Devin Local | Devin Desktop\*\*\*\* | | `.amazonq/prompts/opsx-.md` — a prompt, not a command | `@opsx-` | Amazon Q Developer | -| none — skills only | `/openspec-` | CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, shared `.agents` | +| none — skills only | `/openspec-` | Command Code, CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, shared `.agents` | | none — Kimi Code | `/skill:openspec-` | Kimi Code | | none — Codex CLI | `$openspec-` | Codex ([`/openspec-` is not recognized](https://github.com/openai/codex/issues/11817)) | @@ -70,6 +70,7 @@ to read the hint. | IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-.md` | | Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/.md` | | Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-.md` | +| Command Code (`command-code`) | `.commandcode/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | CodeArts (`codeartsagent`) | `.codeartsdoer/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/.md` | | Codex (`codex`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `$openspec-*`) | @@ -198,7 +199,7 @@ openspec init --tools none openspec init --profile core ``` -**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents` +**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents` ## Workflow-Dependent Installation diff --git a/src/core/config.ts b/src/core/config.ts index 4e027d28f..e8f7d2016 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -43,6 +43,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' }, { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' }, { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' }, + { name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code', skillsDir: '.commandcode' }, { name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' }, { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] }, { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'] }, diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index ce1440033..3678954a3 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -394,6 +394,19 @@ describe('available-tools', () => { expect(toolValues).toContain('claude'); }); + it('should detect Command Code when .commandcode directory exists', async () => { + await fs.mkdir(path.join(testDir, '.commandcode'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const toolValues = tools.map((t) => t.value); + expect(toolValues).toContain('command-code'); + + const commandCodeTool = tools.find((t) => t.value === 'command-code'); + expect(commandCodeTool).toBeDefined(); + expect(commandCodeTool?.name).toBe('Command Code'); + expect(commandCodeTool?.skillsDir).toBe('.commandcode'); + }); + it('should detect Mistral Vibe when .vibe directory exists', async () => { // Mistral Vibe uses skillsDir: '.vibe' without detectionPaths // This test ensures path semantics do not drift for Vibe skill detection diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 55e611f76..591122fea 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -503,6 +503,35 @@ describe('InitCommand', () => { ).toBe(true); }); + it('should support Command Code as an adapterless skills-only tool', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + const initCommand = new InitCommand({ tools: 'command-code', force: true }); + await initCommand.execute(testDir); + + const skillFile = path.join(testDir, '.commandcode', 'skills', 'openspec-explore', 'SKILL.md'); + expect(await fileExists(skillFile)).toBe(true); + + // Command Code documents /openspec-* skill invocations (no /skill: prefix) + const skill = await fs.readFile(skillFile, 'utf-8'); + expect(skill).toContain('/openspec-'); + expect(skill).not.toContain('/skill:'); + + const commandsDir = path.join(testDir, '.commandcode', 'commands'); + expect(await directoryExists(commandsDir)).toBe(false); + + const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String); + expect( + logCalls.some( + (entry) => entry.includes('Commands skipped for: command-code') && entry.includes('(no adapter)'), + ), + ).toBe(true); + }); + it('should support CodeArts as an adapterless skills-only tool', async () => { saveGlobalConfig({ featureFlags: {},