mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
feat: Add codex custom slash command support (#120)
* Document current cli specs and archive safety * feat: add Codex slash command support Add support for generating and updating Codex prompts in `.codex/prompts/` directory: - Implement CodexSlashCommandConfigurator for managing openspec-*.md prompts - Update AI_TOOLS configuration to include Codex as an available option - Add Codex registration in SlashCommandRegistry - Update documentation to reflect Codex support - Add comprehensive test coverage for Codex init and update workflows * feat: complete Codex slash command support implementation Add comprehensive Codex slash command support with Claude.md integration, config-driven command management, and full test coverage. Update documentation and changelog to reflect changes. * chore: mark all Codex slash command tasks as completed All tasks for the Codex slash command support feature have been implemented and tested. * docs: remove Codex-specific note from README Remove the Codex-specific installation note as it's no longer needed in the main setup instructions. This information is better suited for tool-specific documentation.
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# 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.
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
+2
-1
@@ -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' }
|
||||
];
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
const content = await FileSystemUtils.readFile(filePath);
|
||||
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
||||
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
|
||||
|
||||
@@ -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<SlashCommandId, string> = {
|
||||
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<SlashCommandId, string> = {
|
||||
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<string[]> {
|
||||
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<string[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string, SlashCommandConfigurator> = 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 {
|
||||
|
||||
+16
-3
@@ -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 {
|
||||
|
||||
@@ -123,5 +123,7 @@ export class UpdateCommand {
|
||||
}
|
||||
|
||||
console.log(summaryParts.join(' | '));
|
||||
|
||||
// No additional notes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('<!-- OPENSPEC:START -->');
|
||||
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 <id> --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', () => {
|
||||
|
||||
@@ -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<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
|
||||
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<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user