diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 2f9b61526..3e70f1133 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -29,7 +29,7 @@ For each tool you select, OpenSpec installs: | GitHub Copilot | `.github/skills/` | `.github/prompts/` | | iFlow | `.iflow/skills/` | `.iflow/commands/` | | Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` | -| OpenCode | `.opencode/skills/` | `.opencode/command/` | +| OpenCode | `.opencode/skills/` | `.opencode/commands/` | | Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` | | Qwen Code | `.qwen/skills/` | `.qwen/commands/` | | RooCode | `.roo/skills/` | `.roo/commands/` | diff --git a/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/.openspec.yaml b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/.openspec.yaml new file mode 100644 index 000000000..41094ca05 --- /dev/null +++ b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-06 diff --git a/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/design.md b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/design.md new file mode 100644 index 000000000..446023136 --- /dev/null +++ b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/design.md @@ -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? diff --git a/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/proposal.md b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/proposal.md new file mode 100644 index 000000000..b897a7731 --- /dev/null +++ b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/proposal.md @@ -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. diff --git a/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/specs/cli-update/spec.md b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/specs/cli-update/spec.md new file mode 100644 index 000000000..5a19de4cf --- /dev/null +++ b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/specs/cli-update/spec.md @@ -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 `` and `` 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 diff --git a/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/tasks.md b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/tasks.md new file mode 100644 index 000000000..814f86876 --- /dev/null +++ b/openspec/changes/archive/2026-02-06-fix-opencode-commands-dir/tasks.md @@ -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 diff --git a/openspec/specs/cli-update/spec.md b/openspec/specs/cli-update/spec.md index 5ba070c4d..c14c5086c 100644 --- a/openspec/specs/cli-update/spec.md +++ b/openspec/specs/cli-update/spec.md @@ -99,7 +99,7 @@ The update command SHALL refresh existing slash command files for configured too - **AND** skip creating missing files during update #### Scenario: Updating slash commands for OpenCode -- **WHEN** `.opencode/command/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` +- **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 diff --git a/src/core/command-generation/adapters/opencode.ts b/src/core/command-generation/adapters/opencode.ts index 2b078fc6c..908a27362 100644 --- a/src/core/command-generation/adapters/opencode.ts +++ b/src/core/command-generation/adapters/opencode.ts @@ -10,23 +10,26 @@ import { transformToHyphenCommands } from '../../../utils/command-references.js' /** * OpenCode adapter for command generation. - * File path: .opencode/command/opsx-.md + * File path: .opencode/commands/opsx-.md * Frontmatter: description */ export const opencodeAdapter: ToolCommandAdapter = { toolId: 'opencode', getFilePath(commandId: string): string { - return path.join('.opencode', 'command', `opsx-${commandId}.md`); + return path.join('.opencode', 'commands', `opsx-${commandId}.md`); }, formatFile(content: CommandContent): string { // Transform command references from colon to hyphen format for OpenCode const transformedBody = transformToHyphenCommands(content.body); + const archiveArguments = content.id === 'archive' + ? 'arguments: |\n \n $ARGUMENTS\n \n' + : ''; return `--- description: ${content.description} ---- +${archiveArguments}--- ${transformedBody} `; diff --git a/src/core/init.ts b/src/core/init.ts index ff314c120..097051622 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -39,6 +39,7 @@ import { getSkillTemplates, getCommandContents, generateSkillContent, + migrateOpenCodeCommands, type ToolSkillStatus, } from './shared/index.js'; @@ -89,11 +90,15 @@ export class InitCommand { // Validation happens silently in the background const extendMode = await this.validate(projectPath, openspecPath); + // Migrate OpenCode commands from legacy .opencode/command/ to .opencode/commands/ + // This must run BEFORE legacy cleanup so migrated files are properly detected + const canPrompt = this.canPromptInteractively(); + await migrateOpenCodeCommands(projectPath, canPrompt); + // Check for legacy artifacts and handle cleanup await this.handleLegacyCleanup(projectPath, extendMode); // Show animated welcome screen (interactive mode only) - const canPrompt = this.canPromptInteractively(); if (canPrompt) { const { showWelcomeScreen } = await import('../ui/welcome-screen.js'); await showWelcomeScreen(); diff --git a/src/core/legacy-cleanup.ts b/src/core/legacy-cleanup.ts index 498e49dd0..40b0d0e69 100644 --- a/src/core/legacy-cleanup.ts +++ b/src/core/legacy-cleanup.ts @@ -48,7 +48,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record { + 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; + } + + 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 }); + } + } +} diff --git a/src/core/update.ts b/src/core/update.ts index a9368a213..9a4072ba5 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -23,6 +23,7 @@ import { getCommandContents, generateSkillContent, getToolsWithSkillsDir, + migrateOpenCodeCommands, type ToolVersionStatus, } from './shared/index.js'; import { @@ -62,6 +63,9 @@ export class UpdateCommand { throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`); } + // 1b. Migrate legacy OpenCode commands if present + await migrateOpenCodeCommands(resolvedProjectPath, isInteractive()); + // 2. Detect and handle legacy artifacts + upgrade legacy tools to new skills const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath); @@ -131,6 +135,9 @@ export class UpdateCommand { for (const cmd of generatedCommands) { const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path); + if (!await FileSystemUtils.fileExists(commandFile)) { + continue; + } await FileSystemUtils.writeFile(commandFile, cmd.fileContent); } } diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index 5341f6a25..b10d58692 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -443,7 +443,7 @@ describe('command-generation/adapters', () => { it('should generate correct file path', () => { const filePath = opencodeAdapter.getFilePath('explore'); - expect(filePath).toBe(path.join('.opencode', 'command', 'opsx-explore.md')); + expect(filePath).toBe(path.join('.opencode', 'commands', 'opsx-explore.md')); }); it('should format file with description frontmatter', () => { @@ -452,6 +452,7 @@ describe('command-generation/adapters', () => { expect(output).toContain('description: Enter explore mode for thinking'); expect(output).toContain('---\n\n'); expect(output).toContain('This is the command body.'); + expect(output).not.toContain('arguments:'); }); it('should transform colon-based command references to hyphen-based', () => { @@ -480,6 +481,19 @@ describe('command-generation/adapters', () => { expect(output).toContain('/opsx-continue'); expect(output).toContain('/opsx-apply'); }); + + it('should include $ARGUMENTS placeholder in archive frontmatter', () => { + const archiveContent: CommandContent = { + ...sampleContent, + id: 'archive', + name: 'OpenSpec Archive', + description: 'Archive a completed change', + }; + const output = opencodeAdapter.formatFile(archiveContent); + expect(output).toContain('arguments: |'); + expect(output).toContain(''); + expect(output).toContain('$ARGUMENTS'); + }); }); describe('qoderAdapter', () => { diff --git a/test/core/shared/opencode-migration.test.ts b/test/core/shared/opencode-migration.test.ts new file mode 100644 index 000000000..2eb46c0ff --- /dev/null +++ b/test/core/shared/opencode-migration.test.ts @@ -0,0 +1,98 @@ +import path from 'path'; +import os from 'os'; +import { promises as fs } from 'fs'; +import { randomUUID } from 'crypto'; +import { migrateOpenCodeCommands } from '../../../src/core/shared/opencode-migration.js'; + +// Mock @inquirer/prompts +vi.mock('@inquirer/prompts', () => ({ + confirm: vi.fn() +})); + +describe('migrateOpenCodeCommands', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = path.join(os.tmpdir(), `openspec-opencode-migration-${randomUUID()}`); + await fs.mkdir(tempDir, { recursive: true }); + vi.restoreAllMocks(); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('moves legacy command files into .opencode/commands', async () => { + const legacyDir = path.join(tempDir, '.opencode', 'command'); + const nextDir = path.join(tempDir, '.opencode', 'commands'); + await fs.mkdir(legacyDir, { recursive: true }); + + const legacyFile = path.join(legacyDir, 'opsx-archive.md'); + await fs.writeFile(legacyFile, 'legacy content'); + + await migrateOpenCodeCommands(tempDir, false); + + const migratedFile = path.join(nextDir, 'opsx-archive.md'); + const migratedContent = await fs.readFile(migratedFile, 'utf-8'); + expect(migratedContent).toBe('legacy content'); + + const legacyEntries = await fs.readdir(legacyDir); + expect(legacyEntries).toHaveLength(0); + }); + + it('overwrites destination files on conflict', async () => { + const legacyDir = path.join(tempDir, '.opencode', 'command'); + const nextDir = path.join(tempDir, '.opencode', 'commands'); + await fs.mkdir(legacyDir, { recursive: true }); + await fs.mkdir(nextDir, { recursive: true }); + + const legacyFile = path.join(legacyDir, 'opsx-archive.md'); + const destFile = path.join(nextDir, 'opsx-archive.md'); + await fs.writeFile(destFile, 'current content'); + await fs.writeFile(legacyFile, 'legacy content'); + + await migrateOpenCodeCommands(tempDir, false); + + const migratedContent = await fs.readFile(destFile, 'utf-8'); + expect(migratedContent).toBe('current content'); + + const legacyEntries = await fs.readdir(legacyDir); + expect(legacyEntries).toHaveLength(0); + }); + + it('does nothing when legacy directory does not exist', async () => { + await migrateOpenCodeCommands(tempDir, false); + + const legacyDir = path.join(tempDir, '.opencode', 'command'); + await expect(fs.readdir(legacyDir)).rejects.toThrow(); + }); + + it('removes empty legacy directory when confirmed', async () => { + const legacyDir = path.join(tempDir, '.opencode', 'command'); + await fs.mkdir(legacyDir, { recursive: true }); + + // Ensure the migration actually changes something so cleanup prompt applies. + await fs.writeFile(path.join(legacyDir, 'opsx-archive.md'), 'legacy content'); + + const { confirm } = await import('@inquirer/prompts'); + (confirm as unknown as ReturnType).mockResolvedValue(true); + + await migrateOpenCodeCommands(tempDir, true); + + await expect(fs.readdir(legacyDir)).rejects.toThrow(); + }); + + it('keeps empty legacy directory when deletion is declined', async () => { + const legacyDir = path.join(tempDir, '.opencode', 'command'); + await fs.mkdir(legacyDir, { recursive: true }); + + const { confirm } = await import('@inquirer/prompts'); + (confirm as unknown as ReturnType).mockResolvedValue(false); + + await migrateOpenCodeCommands(tempDir, true); + + const entries = await fs.readdir(legacyDir); + expect(entries).toHaveLength(0); + }); +}); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index 21bb00f59..fd44e000f 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -158,10 +158,13 @@ Old instructions content 'old content' ); + const commandsDir = path.join(testDir, '.claude', 'commands', 'opsx'); + await fs.mkdir(commandsDir, { recursive: true }); + await fs.writeFile(path.join(commandsDir, 'explore.md'), 'old command'); + await updateCommand.execute(testDir); // Check opsx command files were created - const commandsDir = path.join(testDir, '.claude', 'commands', 'opsx'); const exploreCmd = path.join(commandsDir, 'explore.md'); const exists = await FileSystemUtils.fileExists(exploreCmd); expect(exists).toBe(true); @@ -185,8 +188,8 @@ Old instructions content 'old content' ); - await updateCommand.execute(testDir); - + const commandsDir = path.join(testDir, '.claude', 'commands', 'opsx'); + await fs.mkdir(commandsDir, { recursive: true }); const commandIds = [ 'explore', 'new', @@ -198,8 +201,12 @@ Old instructions content 'bulk-archive', 'verify', ]; + for (const cmdId of commandIds) { + await fs.writeFile(path.join(commandsDir, `${cmdId}.md`), 'old command'); + } + + await updateCommand.execute(testDir); - const commandsDir = path.join(testDir, '.claude', 'commands', 'opsx'); for (const cmdId of commandIds) { const cmdFile = path.join(commandsDir, `${cmdId}.md`); const exists = await FileSystemUtils.fileExists(cmdFile); @@ -267,6 +274,13 @@ Old instructions content 'old' ); + const qwenCommandsDir = path.join(testDir, '.qwen', 'commands'); + await fs.mkdir(qwenCommandsDir, { recursive: true }); + await fs.writeFile( + path.join(qwenCommandsDir, 'opsx-explore.toml'), + 'old command' + ); + await updateCommand.execute(testDir); // Check Qwen command format (TOML) - Qwen uses flat path structure: opsx-.toml @@ -295,6 +309,13 @@ Old instructions content 'old' ); + const windsurfCommandsDir = path.join(testDir, '.windsurf', 'workflows'); + await fs.mkdir(windsurfCommandsDir, { recursive: true }); + await fs.writeFile( + path.join(windsurfCommandsDir, 'opsx-explore.md'), + 'old command' + ); + await updateCommand.execute(testDir); // Check Windsurf command format