-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Fix OpenCode command directory handling #675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
895d359
27e5c9f
fdb6516
c97eda3
6aeb012
460e965
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-02-06 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| ## Context | ||
|
|
||
| OpenSpec integration scripts currently generate OpenCode AI command markdown files under `.opencode/command/`, but the official documentation specifies `.opencode/commands/`. This change focuses on the OpenSpec initialization script that writes those markdown files. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
| - Align generated command file locations with `.opencode/commands/`. | ||
| - Limit changes to OpenSpec init/update flows that generate or migrate command markdown files. | ||
|
|
||
| **Non-Goals:** | ||
| - Redesign the command format or command execution model. | ||
| - Change the existing command content or add new command types. | ||
| - Update CLI behavior unrelated to command generation or migration. | ||
|
|
||
| ## Decisions | ||
|
|
||
| - Use `.opencode/commands/` as the single source of truth for generated command files, matching documentation. | ||
| - Alternative: keep `.opencode/command/` as-is and update docs. Rejected because documentation is authoritative and users already rely on it. | ||
| - Update OpenSpec init/update to write command markdown files directly into `.opencode/commands/`. | ||
| - Alternative: keep `.opencode/command/` and update docs. Rejected because the documentation is the source of truth. | ||
| - If `.opencode/command/` exists, migrate its files into `.opencode/commands/` and prompt to delete the old directory when it is empty. | ||
| - Alternative: leave the old directory indefinitely. Rejected to avoid confusion and duplicated locations. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| - [Risk] Existing users may have custom tooling pointing at `.opencode/command/` → Mitigation: migrate files and communicate the new location in OpenSpec output. | ||
| - [Trade-off] Migration logic adds a small amount of complexity to initialization → Mitigation: keep migration to a straightforward move/copy step plus optional cleanup. | ||
|
|
||
| ## Migration Plan | ||
|
|
||
| - Update OpenSpec init/update to create `.opencode/commands/` and generate command files there. | ||
| - If `.opencode/command/` exists, move or copy known command files into `.opencode/commands/`. | ||
| - If `.opencode/command/` is empty after migration, prompt the user to delete it. | ||
| - Update tests and documentation to reference `.opencode/commands/`. | ||
|
|
||
| ## Open Questions | ||
|
|
||
| - Should the cleanup prompt be opt-in or on by default when `.opencode/command/` is empty? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| ## Why | ||
|
|
||
| The OpenSpec integration scripts currently generate command files under `.opencode/command/`, while the official documentation requires `.opencode/commands/`. This mismatch breaks expected tooling behavior and creates confusion for users following the docs. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Align OpenCode command generation and update behavior to use the documented `.opencode/commands/` directory. | ||
| - Normalize any references that assume the singular directory so the CLI and docs agree. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
| - (none) | ||
|
|
||
| ### Modified Capabilities | ||
| - `cli-update`: Update the requirements to point to `.opencode/commands/` instead of `.opencode/command/`. | ||
|
|
||
| ## Impact | ||
|
|
||
| - OpenCode CLI command generation and update logic. | ||
| - Documentation references for command locations. | ||
| - Tests that assert command file paths. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| ## MODIFIED Requirements | ||
|
|
||
| ### Requirement: Slash Command Updates | ||
|
|
||
| The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments. | ||
|
|
||
| #### Scenario: Updating slash commands for Antigravity | ||
| - **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter | ||
| - **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs | ||
|
|
||
| #### Scenario: Updating slash commands for Claude Code | ||
| - **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for CodeBuddy Code | ||
| - **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md` | ||
| - **THEN** refresh each file using the shared CodeBuddy templates that include YAML frontmatter for the `description` and `argument-hint` fields | ||
| - **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`) | ||
| - **AND** preserve any user customizations outside the OpenSpec managed markers | ||
|
|
||
| #### Scenario: Updating slash commands for Cline | ||
| - **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** include Cline-specific Markdown heading frontmatter | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for Continue | ||
| - **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for Crush | ||
| - **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** include Crush-specific frontmatter with OpenSpec category and tags | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for Cursor | ||
| - **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for Factory Droid | ||
| - **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields | ||
| - **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid | ||
| - **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched | ||
| - **AND** skip creating missing files during update | ||
|
|
||
| #### Scenario: Updating slash commands for OpenCode | ||
| - **WHEN** `.opencode/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
| - **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments | ||
|
|
||
| #### Scenario: Updating slash commands for Windsurf | ||
| - **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates wrapped in OpenSpec markers | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
| - **AND** skip creating missing files (the update command only refreshes what already exists) | ||
|
|
||
| #### Scenario: Updating slash commands for Kilo Code | ||
| - **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates wrapped in OpenSpec markers | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
| - **AND** skip creating missing files (the update command only refreshes what already exists) | ||
|
|
||
| #### Scenario: Updating slash commands for Codex | ||
| - **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **WHEN** a user runs `openspec update` | ||
| - **THEN** refresh each file using the shared slash-command templates (including placeholder guidance) | ||
| - **AND** preserve any unmanaged content outside the OpenSpec marker block | ||
| - **AND** skip creation when a Codex prompt file is missing | ||
|
|
||
| #### Scenario: Updating slash commands for GitHub Copilot | ||
| - **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md` | ||
| - **THEN** refresh each file using shared templates while preserving the YAML frontmatter | ||
| - **AND** update only the OpenSpec-managed block between markers | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Updating slash commands for Gemini CLI | ||
| - **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml` | ||
| - **THEN** refresh the body of each file using the shared proposal/apply/archive templates | ||
| - **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact | ||
| - **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed | ||
|
|
||
| #### Scenario: Updating slash commands for iFlow CLI | ||
| - **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` | ||
| - **THEN** refresh each file using shared templates | ||
| - **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields | ||
| - **AND** update only the OpenSpec-managed block between markers | ||
| - **AND** ensure templates include instructions for the relevant workflow stage | ||
|
|
||
| #### Scenario: Missing slash command file | ||
| - **WHEN** a tool lacks a slash command file | ||
| - **THEN** do not create a new file during update |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ## 1. Scope And Specs | ||
|
|
||
| - [x] 1.1 Confirm no new capabilities and only `cli-update` spec is modified | ||
| - [x] 1.2 Review `openspec/specs/cli-update/spec.md` to locate the existing OpenCode slash command scenario | ||
| - [x] 1.3 Update the delta spec to replace `.opencode/command/` with `.opencode/commands/` and remove the Windows path variant if not required | ||
|
|
||
| ## 2. OpenSpec Initialization Script Update | ||
|
|
||
| - [x] 2.1 Find the OpenSpec initialization script that generates OpenCode command markdown files | ||
| - [x] 2.2 Change the OpenCode command output directory to `.opencode/commands/` | ||
| - [x] 2.3 If `.opencode/command/` exists, migrate known OpenCode command files into `.opencode/commands/` | ||
| - [x] 2.4 If `.opencode/command/` is empty after migration, prompt the user to delete it and remove it on confirmation | ||
| - [x] 2.5 Ensure the cleanup prompt is visible (pause spinner before asking) | ||
|
|
||
| ## 3. Tests And Verification | ||
|
|
||
| - [x] 3.1 Update any tests or fixtures that assert the OpenCode command directory path | ||
| - [x] 3.2 Run existing tests related to OpenSpec init/update and ensure they pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { FileSystemUtils } from '../../utils/file-system.js'; | ||
|
|
||
| export async function migrateOpenCodeCommands( | ||
| projectPath: string, | ||
| canPrompt: boolean | ||
| ): Promise<void> { | ||
| const legacyDir = path.join(projectPath, '.opencode', 'command'); | ||
| if (!await FileSystemUtils.directoryExists(legacyDir)) { | ||
| return; | ||
| } | ||
|
|
||
| const nextDir = path.join(projectPath, '.opencode', 'commands'); | ||
| await FileSystemUtils.createDirectory(nextDir); | ||
|
|
||
| const entries = await fs.promises.readdir(legacyDir, { withFileTypes: true }); | ||
| let changedAny = false; | ||
| for (const entry of entries) { | ||
| if (!entry.isFile()) continue; | ||
| const sourcePath = path.join(legacyDir, entry.name); | ||
| const destinationPath = path.join(nextDir, entry.name); | ||
| // Prefer the new location on conflicts: keep destination, discard legacy. | ||
| if (await FileSystemUtils.fileExists(destinationPath)) { | ||
| await fs.promises.unlink(sourcePath); | ||
| changedAny = true; | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| await fs.promises.rename(sourcePath, destinationPath); | ||
| } catch (error: any) { | ||
| if (error?.code !== 'EXDEV') { | ||
| throw error; | ||
| } | ||
| // Fallback for cross-device moves. | ||
| await fs.promises.copyFile(sourcePath, destinationPath); | ||
| await fs.promises.unlink(sourcePath); | ||
| } | ||
| changedAny = true; | ||
| } | ||
JcMinarro marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
21
to
41
|
||
|
|
||
| const remaining = await fs.promises.readdir(legacyDir); | ||
| if (changedAny && remaining.length === 0 && canPrompt) { | ||
| const { confirm } = await import('@inquirer/prompts'); | ||
| const shouldRemove = await confirm({ | ||
| message: 'OpenCode commands have moved to .opencode/commands. The old .opencode/command directory is now empty and can be removed. Delete it?', | ||
| default: true, | ||
| }); | ||
| if (shouldRemove) { | ||
| await fs.promises.rm(legacyDir, { recursive: true, force: true }); | ||
| } | ||
| } | ||
| } | ||
JcMinarro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.