diff --git a/AGENTS.md b/AGENTS.md index 13e4d2ba..7f111dd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,18 @@ # OpenSpec Instructions -This project uses OpenSpec to manage AI assistant workflows. +These instructions are for AI assistants working in this project. + +Always open `@/openspec/AGENTS.md` when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan) +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work +- Sounds ambiguous and you need the authoritative spec before coding + +Use `@/openspec/AGENTS.md` to learn: +- How to create and apply change proposals +- Spec format and conventions +- Project structure and guidelines + +Keep this managed block so 'openspec update' can refresh the instructions. -- Full guidance lives in '@/openspec/AGENTS.md'. -- Keep this managed block so 'openspec update' can refresh the instructions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f348dfc..e1eec63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Minor Changes - c29b06d: Add Windsurf support. +- Add Codex slash command support. OpenSpec now writes prompts directly to Codex's global directory (`~/.codex/prompts` or `$CODEX_HOME/prompts`) and refreshes them on `openspec update`. ## 0.7.0 diff --git a/README.md b/README.md index 0bd131db..caf603e1 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe | **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` | | **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) | | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) | +| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) | Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`. @@ -103,7 +104,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`. | Tools | |-------| -| Codex • Amp • Jules • Gemini CLI • GitHub Copilot • Others | +| Amp • Jules • Gemini CLI • GitHub Copilot • Others | ### Install & Initialize @@ -207,7 +208,7 @@ Or run the command yourself in terminal: $ openspec archive add-profile-filters --yes # Archive the completed change without prompts ``` -**Note:** Tools with native slash commands (Claude Code, Cursor) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change". +**Note:** Tools with native slash commands (Claude Code, Cursor, Codex) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change". ## Command Reference diff --git a/openspec/changes/add-codex-slash-command-support/proposal.md b/openspec/changes/add-codex-slash-command-support/proposal.md new file mode 100644 index 00000000..7c439d96 --- /dev/null +++ b/openspec/changes/add-codex-slash-command-support/proposal.md @@ -0,0 +1,25 @@ +## Why +- Codex (the VS Code extension formerly known as Codeium Chat) exposes "slash commands" by reading Markdown prompt files from `~/.codex/prompts/`. Each file name becomes the `/command` users can run, with numbered placeholders (`$1`, `$2`, …) bound to the arguments they supply. The workflow screenshot shared by Kevin Kern ("Codex problem analyzer") shows the format OpenSpec should target so teams can invoke curated workflows straight from the chat palette. +- Teams already rely on OpenSpec to manage the slash-command surface area for Claude, Cursor, OpenCode, Kilo Code, and Windsurf. Leaving Codex out forces them to manually copy/paste OpenSpec guardrails into `~/.codex/prompts/*.md`, which drifts quickly and undermines the "single source of truth" promise of the CLI. +- Codex commands live outside the repository (under the user's home directory), so shipping an automated configurator that both scaffolds the prompts and keeps them refreshed via `openspec update` eliminates error-prone manual steps and keeps OpenSpec instructions synchronized across assistants. + +## What Changes +- Add Codex to the `openspec init` tool picker with the same "already configured" detection we use for other editors, wiring an implementation that writes managed Markdown prompts directly to Codex's global directory (`~/.codex/prompts` or `$CODEX_HOME/prompts`) with OpenSpec marker blocks. +- Produce three Codex prompt files—`openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`—whose content mirrors the shared slash-command templates while adapting to Codex's numbered argument placeholders (e.g., `$1` for the change identifier or follow-up question text). +- Document Codex's global-only discovery and that OpenSpec writes prompts directly to `~/.codex/prompts` (or `$CODEX_HOME/prompts`). +- Teach `openspec update` to refresh existing Codex prompts in-place (and only when they already exist) in the global directory. +- Document Codex support alongside other slash-command integrations and add regression coverage that exercises init/update behaviour against a temporary global prompts directory via `CODEX_HOME`. + +## Impact +- Specs: `cli-init`, `cli-update` +- Code: `src/core/config.ts`, `src/core/configurators/slash/*`, `src/core/templates/slash-command-templates.ts`, CLI tool summaries, docs +- Tests: integration coverage for Codex prompt scaffolding and refresh logic +- Docs: README and CHANGELOG entries announcing Codex slash-command support + +## Current Spec Reference +- `specs/cli-init/spec.md` + - Requirements cover init UX, directory scaffolding, AI tool configuration, and the existing slash-command support for Claude Code, Cursor, and OpenCode. + - Our `## MODIFIED` delta in `changes/.../specs/cli-init/spec.md` copies the full "Slash Command Configuration" requirement (header, description, and all scenarios) before appending the new Codex scenario so archiving will retain every prior scenario. +- `specs/cli-update/spec.md` + - Requirements define update preconditions, template refresh behavior, and slash-command refresh logic for Claude Code, Cursor, and OpenCode. + - The corresponding delta preserves the entire "Slash Command Updates" requirement while adding the Codex refresh scenario, ensuring the archive workflow replaces the block without losing the existing scenarios or the "Missing slash command file" guardrail. diff --git a/openspec/changes/add-codex-slash-command-support/specs/cli-init/spec.md b/openspec/changes/add-codex-slash-command-support/specs/cli-init/spec.md new file mode 100644 index 00000000..58b6c11a --- /dev/null +++ b/openspec/changes/add-codex-slash-command-support/specs/cli-init/spec.md @@ -0,0 +1,56 @@ +## MODIFIED Requirements +### Requirement: AI Tool Configuration +The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system. +#### Scenario: Prompting for AI tool selection +- **WHEN** run interactively +- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu +- **AND** list every available tool with a checkbox: + - Claude Code (creates or refreshes CLAUDE.md and slash commands) + - Cursor (creates or refreshes `.cursor/commands/*` slash commands) + - OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands) + - Windsurf (creates or refreshes `.windsurf/workflows/openspec-*.md` workflows) + - Kilo Code (creates or refreshes `.kilocode/workflows/openspec-*.md` workflows) + - Codex (creates or refreshes global prompts at `~/.codex/prompts/openspec-*.md`) + - AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers) +- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content +- **AND** treat disabled tools as "coming soon" and keep them unselectable +- **AND** allow confirming with Enter after selecting one or more tools + +### Requirement: Slash Command Configuration +The init command SHALL generate slash command files for supported editors using shared templates. + +#### Scenario: Generating slash commands for Claude Code +- **WHEN** the user selects Claude Code during initialization +- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md` +- **AND** populate each file from shared templates so command text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating slash commands for Cursor +- **WHEN** the user selects Cursor during initialization +- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md` +- **AND** populate each file from shared templates so command text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating slash commands for OpenCode +- **WHEN** the user selects OpenCode during initialization +- **THEN** create `.opencode/command/openspec-proposal.md`, `.opencode/command/openspec-apply.md`, and `.opencode/command/openspec-archive.md` +- **AND** populate each file from shared templates so command text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating slash commands for Windsurf +- **WHEN** the user selects Windsurf during initialization +- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md` +- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating slash commands for Kilo Code +- **WHEN** the user selects Kilo Code during initialization +- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md` +- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating slash commands for Codex +- **WHEN** the user selects Codex during initialization +- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set) +- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text) +- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes diff --git a/openspec/changes/add-codex-slash-command-support/specs/cli-update/spec.md b/openspec/changes/add-codex-slash-command-support/specs/cli-update/spec.md new file mode 100644 index 00000000..70ae8dea --- /dev/null +++ b/openspec/changes/add-codex-slash-command-support/specs/cli-update/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements +### Requirement: Slash Command Updates +The update command SHALL refresh existing slash command files for configured tools without creating new ones. + +#### 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 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 OpenCode +- **WHEN** `.opencode/command/` 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 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: 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/add-codex-slash-command-support/tasks.md b/openspec/changes/add-codex-slash-command-support/tasks.md new file mode 100644 index 00000000..d59c1450 --- /dev/null +++ b/openspec/changes/add-codex-slash-command-support/tasks.md @@ -0,0 +1,19 @@ +## 1. CLI integration +- [x] 1.1 Add Codex to the init tool picker with display text that clarifies prompts live in the global `.codex/prompts/` directory and implement "already configured" detection by checking for managed Codex prompt files. +- [x] 1.2 Implement a `CodexSlashCommandConfigurator` that writes `.codex/prompts/openspec-{proposal,apply,archive}.md`, ensuring the prompt directory exists and wrapping content in OpenSpec markers. +// (No helper command required) +- [x] 1.3 Register the configurator with the slash-command registry and include Codex in init/update wiring so both commands invoke the new configurator when appropriate. + +## 2. Prompt templates +- [x] 2.1 Extend the shared slash-command templates (or add a Codex-specific wrapper) to inject numbered placeholders (`$1`, `$2`, …) where Codex expects user-supplied arguments. +- [x] 2.2 Verify generated Markdown stays within Codex's formatting expectations (no front matter, heading-first layout) and matches the problem-analyzer style shown in the reference screenshot. + +## 3. Update support & tests +- [x] 3.1 Update the `openspec update` flow to refresh existing Codex prompts without creating new ones when files are missing. +- [x] 3.2 Add integration coverage that exercises init/update against a temporary global Codex prompts directory by setting `CODEX_HOME`, asserting marker preservation and idempotent updates. +- [x] 3.3 Document Codex's global-only discovery and automatic installation in README and CHANGELOG. +- [x] 3.3 Confirm error handling surfaces clear paths when the CLI cannot write to the Codex prompt directory (permissions, missing home directory, etc.). + +## 4. Documentation +- [x] 4.1 Document Codex slash-command support in the README and changelog alongside other assistant integrations. +- [x] 4.2 Add a release note snippet that points Codex users to the generated `/openspec-proposal`, `/openspec-apply`, and `/openspec-archive` commands. diff --git a/src/core/config.ts b/src/core/config.ts index b7c76020..4547deb0 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -22,5 +22,6 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' }, { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' }, { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' }, - { name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } + { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' }, + { name: 'AGENTS.md (works with Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/configurators/slash/base.ts b/src/core/configurators/slash/base.ts index 8ece4861..edc1a268 100644 --- a/src/core/configurators/slash/base.ts +++ b/src/core/configurators/slash/base.ts @@ -67,7 +67,14 @@ export abstract class SlashCommandConfigurator { protected abstract getRelativePath(id: SlashCommandId): string; protected abstract getFrontmatter(id: SlashCommandId): string | undefined; - private async updateBody(filePath: string, body: string): Promise { + // Resolve absolute path for a given slash command target. Subclasses may override + // to redirect to tool-specific locations (e.g., global directories). + resolveAbsolutePath(projectPath: string, id: SlashCommandId): string { + const rel = this.getRelativePath(id); + return path.join(projectPath, rel); + } + + protected async updateBody(filePath: string, body: string): Promise { const content = await FileSystemUtils.readFile(filePath); const startIndex = content.indexOf(OPENSPEC_MARKERS.start); const endIndex = content.indexOf(OPENSPEC_MARKERS.end); diff --git a/src/core/configurators/slash/codex.ts b/src/core/configurators/slash/codex.ts new file mode 100644 index 00000000..b1c82f32 --- /dev/null +++ b/src/core/configurators/slash/codex.ts @@ -0,0 +1,86 @@ +import path from "path"; +import os from "os"; +import { SlashCommandConfigurator } from "./base.js"; +import { SlashCommandId, TemplateManager } from "../../templates/index.js"; +import { FileSystemUtils } from "../../../utils/file-system.js"; +import { OPENSPEC_MARKERS } from "../../config.js"; + +const FILE_PATHS: Record = { + proposal: ".codex/prompts/openspec-proposal.md", + apply: ".codex/prompts/openspec-apply.md", + archive: ".codex/prompts/openspec-archive.md", +}; + +export class CodexSlashCommandConfigurator extends SlashCommandConfigurator { + readonly toolId = "codex"; + readonly isAvailable = true; + + protected getRelativePath(id: SlashCommandId): string { + return FILE_PATHS[id]; + } + + protected getFrontmatter(id: SlashCommandId): string | undefined { + // Codex does not use YAML front matter. Provide a heading-style + // preface that captures the first numbered placeholder `$1`. + const headers: Record = { + proposal: "Request: $1", + apply: "Change ID: $1", + archive: "Change ID: $1", + }; + return headers[id]; + } + + private getGlobalPromptsDir(): string { + const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim()) + ? process.env.CODEX_HOME.trim() + : path.join(os.homedir(), ".codex"); + return path.join(home, "prompts"); + } + + // Codex discovers prompts globally. Generate directly in the global directory + // and wrap shared body with markers. + async generateAll(projectPath: string, _openspecDir: string): Promise { + const createdOrUpdated: string[] = []; + for (const target of this.getTargets()) { + const body = TemplateManager.getSlashCommandBody(target.id).trim(); + const promptsDir = this.getGlobalPromptsDir(); + const filePath = path.join(promptsDir, path.basename(target.path)); + + await FileSystemUtils.createDirectory(path.dirname(filePath)); + + if (await FileSystemUtils.fileExists(filePath)) { + await this.updateBody(filePath, body); + } else { + const header = this.getFrontmatter(target.id); + const sections: string[] = []; + if (header) sections.push(header.trim()); + sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`); + await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n"); + } + + createdOrUpdated.push(target.path); + } + return createdOrUpdated; + } + + async updateExisting(projectPath: string, _openspecDir: string): Promise { + const updated: string[] = []; + for (const target of this.getTargets()) { + const promptsDir = this.getGlobalPromptsDir(); + const filePath = path.join(promptsDir, path.basename(target.path)); + if (await FileSystemUtils.fileExists(filePath)) { + const body = TemplateManager.getSlashCommandBody(target.id).trim(); + await this.updateBody(filePath, body); + updated.push(target.path); + } + } + return updated; + } + + // Resolve to the global prompts location for configuration detection + resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string { + const promptsDir = this.getGlobalPromptsDir(); + const fileName = path.basename(FILE_PATHS[id]); + return path.join(promptsDir, fileName); + } +} diff --git a/src/core/configurators/slash/registry.ts b/src/core/configurators/slash/registry.ts index 7b22c2a8..c9072e71 100644 --- a/src/core/configurators/slash/registry.ts +++ b/src/core/configurators/slash/registry.ts @@ -4,6 +4,7 @@ import { CursorSlashCommandConfigurator } from './cursor.js'; import { WindsurfSlashCommandConfigurator } from './windsurf.js'; import { KiloCodeSlashCommandConfigurator } from './kilocode.js'; import { OpenCodeSlashCommandConfigurator } from './opencode.js'; +import { CodexSlashCommandConfigurator } from './codex.js'; export class SlashCommandRegistry { private static configurators: Map = new Map(); @@ -14,12 +15,14 @@ export class SlashCommandRegistry { const windsurf = new WindsurfSlashCommandConfigurator(); const kilocode = new KiloCodeSlashCommandConfigurator(); const opencode = new OpenCodeSlashCommandConfigurator(); + const codex = new CodexSlashCommandConfigurator(); this.configurators.set(claude.toolId, claude); this.configurators.set(cursor.toolId, cursor); this.configurators.set(windsurf.toolId, windsurf); this.configurators.set(kilocode.toolId, kilocode); this.configurators.set(opencode.toolId, opencode); + this.configurators.set(codex.toolId, codex); } static register(configurator: SlashCommandConfigurator): void { diff --git a/src/core/init.ts b/src/core/init.ts index b3fc07ce..33c96b2a 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -516,7 +516,7 @@ export class InitCommand { value: OTHER_TOOLS_HEADING_VALUE, label: { primary: - 'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)', + 'Other tools (use Universal AGENTS.md for Amp, VS Code, GitHub Copilot, …)', }, selectable: false, }, @@ -564,8 +564,11 @@ export class InitCommand { const slashConfigurator = SlashCommandRegistry.get(toolId); if (!slashConfigurator) return false; for (const target of slashConfigurator.getTargets()) { - if (await FileSystemUtils.fileExists(path.join(projectPath, target.path))) - return true; + const absolute = slashConfigurator.resolveAbsolutePath( + projectPath, + target.id + ); + if (await FileSystemUtils.fileExists(absolute)) return true; } return false; } @@ -748,6 +751,16 @@ export class InitCommand { '────────────────────────────────────────────────────────────\n' ) ); + + // Codex heads-up: prompts installed globally + const selectedToolIds = new Set(selectedTools.map((t) => t.value)); + if (selectedToolIds.has('codex')) { + console.log(PALETTE.white('Codex setup note')); + console.log( + PALETTE.midGray('Prompts installed to ~/.codex/prompts (or $CODEX_HOME/prompts).') + ); + console.log(); + } } private formatToolNames(tools: AIToolOption[]): string { diff --git a/src/core/update.ts b/src/core/update.ts index d18131af..1759f6c8 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -123,5 +123,7 @@ export class UpdateCommand { } console.log(summaryParts.join(' | ')); + + // No additional notes } } diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 11b8a4a2..ace4eba2 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -36,6 +36,7 @@ function queueSelections(...values: string[]) { describe('InitCommand', () => { let testDir: string; let initCommand: InitCommand; + let prevCodexHome: string | undefined; beforeEach(async () => { testDir = path.join(os.tmpdir(), `openspec-init-test-${Date.now()}`); @@ -44,6 +45,10 @@ describe('InitCommand', () => { mockPrompt.mockReset(); initCommand = new InitCommand({ prompt: mockPrompt }); + // Route Codex global directory into the test sandbox + prevCodexHome = process.env.CODEX_HOME; + process.env.CODEX_HOME = path.join(testDir, '.codex'); + // Mock console.log to suppress output during tests vi.spyOn(console, 'log').mockImplementation(() => {}); }); @@ -51,6 +56,8 @@ describe('InitCommand', () => { afterEach(async () => { await fs.rm(testDir, { recursive: true, force: true }); vi.restoreAllMocks(); + if (prevCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = prevCodexHome; }); describe('execute', () => { @@ -309,6 +316,42 @@ describe('InitCommand', () => { expect(archiveContent).toContain('openspec list --specs'); }); + it('should create Codex prompts with templates and placeholders', async () => { + queueSelections('codex', DONE); + + await initCommand.execute(testDir); + + const proposalPath = path.join( + testDir, + '.codex/prompts/openspec-proposal.md' + ); + const applyPath = path.join( + testDir, + '.codex/prompts/openspec-apply.md' + ); + const archivePath = path.join( + testDir, + '.codex/prompts/openspec-archive.md' + ); + + expect(await fileExists(proposalPath)).toBe(true); + expect(await fileExists(applyPath)).toBe(true); + expect(await fileExists(archivePath)).toBe(true); + + const proposalContent = await fs.readFile(proposalPath, 'utf-8'); + expect(proposalContent).toContain('Request: $1'); + expect(proposalContent).toContain(''); + expect(proposalContent).toContain('**Guardrails**'); + + const applyContent = await fs.readFile(applyPath, 'utf-8'); + expect(applyContent).toContain('Change ID: $1'); + expect(applyContent).toContain('Work through tasks sequentially'); + + const archiveContent = await fs.readFile(archivePath, 'utf-8'); + expect(archiveContent).toContain('Change ID: $1'); + expect(archiveContent).toContain('openspec archive --yes'); + }); + it('should create Kilo Code workflows with templates', async () => { queueSelections('kilocode', DONE); @@ -455,6 +498,18 @@ describe('InitCommand', () => { ); expect(wsChoice.configured).toBe(true); }); + + it('should mark Codex as already configured during extend mode', async () => { + queueSelections('codex', DONE, 'codex', DONE); + await initCommand.execute(testDir); + await initCommand.execute(testDir); + + const secondRunArgs = mockPrompt.mock.calls[1][0]; + const codexChoice = secondRunArgs.choices.find( + (choice: any) => choice.value === 'codex' + ); + expect(codexChoice.configured).toBe(true); + }); }); describe('error handling', () => { diff --git a/test/core/update.test.ts b/test/core/update.test.ts index 93c18d48..5bcf72ec 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -10,6 +10,7 @@ import { randomUUID } from 'crypto'; describe('UpdateCommand', () => { let testDir: string; let updateCommand: UpdateCommand; + let prevCodexHome: string | undefined; beforeEach(async () => { // Create a temporary test directory @@ -21,11 +22,17 @@ describe('UpdateCommand', () => { await fs.mkdir(openspecDir, { recursive: true }); updateCommand = new UpdateCommand(); + + // Route Codex global directory into the test sandbox + prevCodexHome = process.env.CODEX_HOME; + process.env.CODEX_HOME = path.join(testDir, '.codex'); }); afterEach(async () => { // Clean up test directory await fs.rm(testDir, { recursive: true, force: true }); + if (prevCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = prevCodexHome; }); it('should update only existing CLAUDE.md file', async () => { @@ -249,6 +256,61 @@ Old body consoleSpy.mockRestore(); }); + it('should refresh existing Codex prompts', async () => { + const codexPath = path.join( + testDir, + '.codex/prompts/openspec-apply.md' + ); + await fs.mkdir(path.dirname(codexPath), { recursive: true }); + const initialContent = `Change ID: $1\n\nOld body\n`; + await fs.writeFile(codexPath, initialContent); + + const consoleSpy = vi.spyOn(console, 'log'); + + await updateCommand.execute(testDir); + + const updated = await fs.readFile(codexPath, 'utf-8'); + expect(updated).toContain('Change ID: $1'); + expect(updated).toContain('Work through tasks sequentially'); + expect(updated).not.toContain('Old body'); + + const [logMessage] = consoleSpy.mock.calls[0]; + expect(logMessage).toContain( + 'Updated slash commands: .codex/prompts/openspec-apply.md' + ); + + consoleSpy.mockRestore(); + }); + + it('should not create missing Codex prompts on update', async () => { + const codexApply = path.join( + testDir, + '.codex/prompts/openspec-apply.md' + ); + + // Only create apply; leave proposal and archive missing + await fs.mkdir(path.dirname(codexApply), { recursive: true }); + await fs.writeFile( + codexApply, + 'Change ID: $1\n\nOld\n' + ); + + await updateCommand.execute(testDir); + + const codexProposal = path.join( + testDir, + '.codex/prompts/openspec-proposal.md' + ); + const codexArchive = path.join( + testDir, + '.codex/prompts/openspec-archive.md' + ); + + // Confirm they weren't created by update + await expect(FileSystemUtils.fileExists(codexProposal)).resolves.toBe(false); + await expect(FileSystemUtils.fileExists(codexArchive)).resolves.toBe(false); + }); + it('should preserve Windsurf content outside markers during update', async () => { const wsPath = path.join( testDir,