Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 793ef1df37 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.
2025-10-09 01:49:17 +11:00
Tabish Bidiwale eca0804d66 chore: mark all Codex slash command tasks as completed
All tasks for the Codex slash command support feature have been implemented and tested.
2025-10-09 01:47:18 +11:00
Tabish Bidiwale b2c61a10a7 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.
2025-10-09 01:45:45 +11:00
Tabish Bidiwale ae6b48241e Merge branch 'main' into codex/add-codex-slash-command-support 2025-10-09 00:56:59 +11:00
Tabish Bidiwale edbf936273 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
2025-10-09 00:56:21 +11:00
Tabish Bidiwale 8b3a39e45f Document current cli specs and archive safety 2025-10-05 18:18:22 +11:00
19 changed files with 35 additions and 456 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@fission-ai/openspec": patch
---
Fix cross-platform path handling issues. This release includes fixes for joinPath behavior and slash command path resolution to ensure OpenSpec works correctly across all platforms.
-18
View File
@@ -1,23 +1,5 @@
# @fission-ai/openspec
## 0.9.1
### Patch Changes
- 8210970: Fix OpenSpec not working on Windows when Codex integration is selected. This release includes fixes for cross-platform path handling and normalization to ensure OpenSpec works correctly on Windows systems.
## 0.9.0
### Minor Changes
- efbbf3b: Add support for Codex and GitHub Copilot slash commands with YAML frontmatter and $ARGUMENTS
## Unreleased
### Minor Changes
- Add GitHub Copilot slash command support. OpenSpec now writes prompts to `.github/prompts/openspec-{proposal,apply,archive}.prompt.md` with YAML frontmatter and `$ARGUMENTS` placeholder, and refreshes them on `openspec update`.
## 0.8.1
### Patch Changes
+1 -2
View File
@@ -96,7 +96,6 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **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) |
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
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`.
@@ -105,7 +104,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
| Tools |
|-------|
| Amp • Jules • Gemini CLI • Others |
| Amp • Jules • Gemini CLI • GitHub Copilot • Others |
### Install & Initialize
@@ -1,13 +1,13 @@
## 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 YAML frontmatter for metadata (`description`, `argument-hint`) and `$ARGUMENTS` to capture user input. 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.
- 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 using YAML frontmatter (`description` and `argument-hint` fields) and `$ARGUMENTS` to capture all arguments as a single string (matching the GitHub Copilot pattern and official Codex specification).
- 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, updating both frontmatter and body.
- 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
@@ -1,25 +0,0 @@
## Why
- GitHub Copilot supports custom slash commands through markdown files in `.github/prompts/<name>.prompt.md`. Each file includes YAML frontmatter with a `description` label and uses `$ARGUMENTS` to capture user input. This format allows teams to expose curated workflows directly in Copilot's chat interface.
- Teams already rely on OpenSpec to manage slash-command configurations for Claude Code, Cursor, OpenCode, Codex, Kilo Code, and Windsurf. Excluding GitHub Copilot forces developers to manually maintain OpenSpec prompts in `.github/prompts/`, which leads to drift and undermines OpenSpec's "single source of truth" promise.
- GitHub Copilot discovers prompts from the repository's `.github/prompts/` directory, making it straightforward to version control and share across the team. Adding automated generation and refresh through `openspec init` and `openspec update` eliminates manual synchronization and keeps OpenSpec instructions consistent across all AI assistants.
## What Changes
- Add GitHub Copilot to the `openspec init` tool picker with "already configured" detection similar to other editors, wiring an implementation that writes managed Markdown prompt files to `.github/prompts/` with OpenSpec marker blocks.
- Generate three GitHub Copilot prompt files—`openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`—whose content mirrors shared slash-command templates while conforming to Copilot's frontmatter and `$ARGUMENTS` placeholder convention.
- Document GitHub Copilot's repository-based discovery and that OpenSpec writes prompts to `.github/prompts/` with managed blocks.
- Teach `openspec update` to refresh existing GitHub Copilot prompts in-place (only when they already exist) in the repository's `.github/prompts/` directory.
- Document GitHub Copilot support alongside other slash-command integrations and add test coverage that exercises init/update behavior for `.github/prompts/` files.
## Impact
- Specs: `cli-init`, `cli-update`
- Code: `src/core/configurators/slash/github-copilot.ts` (new), `src/core/configurators/slash/registry.ts`, `src/core/templates/slash-command-templates.ts`, CLI tool summaries, docs
- Tests: integration coverage for GitHub Copilot prompt scaffolding and refresh logic
- Docs: README and CHANGELOG entries announcing GitHub Copilot slash-command support
## Current Spec Reference
- `specs/cli-init/spec.md`
- Requirements cover init UX, directory scaffolding, AI tool configuration, and existing slash-command support for Claude Code, Cursor, OpenCode, Codex, Kilo Code, and Windsurf.
- Our `## MODIFIED` delta in `changes/.../specs/cli-init/spec.md` will copy the full "Slash Command Configuration" requirement (header, description, and all scenarios) before appending the new GitHub Copilot scenario so archiving retains every prior scenario.
- `specs/cli-update/spec.md`
- Requirements define update preconditions, template refresh behavior, and slash-command refresh logic for existing tools.
- The corresponding delta preserves the entire "Slash Command Updates" requirement while adding the GitHub Copilot refresh scenario, ensuring the archive workflow replaces the block without losing existing scenarios or the "Missing slash command file" guardrail.
@@ -1,30 +0,0 @@
## MODIFIED Requirements
### 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/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/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 GitHub Copilot
- **WHEN** the user selects GitHub Copilot during initialization
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
- **AND** include `$ARGUMENTS` placeholder to capture user input
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
@@ -1,29 +0,0 @@
## 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 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: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
@@ -1,30 +0,0 @@
## Implementation Tasks
- [x] Create `src/core/configurators/slash/github-copilot.ts` implementing `SlashCommandConfigurator` base class
- Implement `getRelativePath()` to return `.github/prompts/openspec-{proposal,apply,archive}.prompt.md`
- Implement `getFrontmatter()` to generate YAML frontmatter with `description` field and include `$ARGUMENTS` placeholder
- Implement `generateAll()` to create `.github/prompts/` directory and write three prompt files with frontmatter, markers, and shared template bodies
- Implement `updateExisting()` to refresh only the managed block between markers while preserving frontmatter
- Set `toolId = "github-copilot"` and `isAvailable = true`
- [x] Register GitHub Copilot configurator in `src/core/configurators/slash/registry.ts`
- Import `GitHubCopilotSlashCommandConfigurator`
- Add to `SLASH_COMMAND_CONFIGURATORS` array
- Update tool picker display name to "GitHub Copilot"
- [x] Update `src/core/init.ts` to include GitHub Copilot in the AI tool selection prompt
- Add GitHub Copilot to the available tools list with detection for existing `.github/prompts/openspec-*.prompt.md` files
- Display "(already configured)" when prompt files exist
- [x] Update `src/core/update.ts` to refresh GitHub Copilot prompts when they exist
- Call `updateExisting()` for GitHub Copilot configurator when `.github/prompts/` contains OpenSpec prompt files
- [x] Add integration tests for GitHub Copilot slash command generation
- Test `generateAll()` creates three prompt files with correct structure (frontmatter + markers + body)
- Test `updateExisting()` preserves frontmatter and only updates managed blocks
- Test that missing prompt files are not created during update
- [x] Update documentation
- Add GitHub Copilot to README slash-command support table
- Document `.github/prompts/` as the discovery location
- Add CHANGELOG entry for GitHub Copilot support
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.9.1",
"version": "0.8.1",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+1 -2
View File
@@ -23,6 +23,5 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
{ name: 'AGENTS.md (works with Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
+4 -3
View File
@@ -1,3 +1,4 @@
import path from 'path';
import { FileSystemUtils } from '../../../utils/file-system.js';
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
@@ -27,7 +28,7 @@ export abstract class SlashCommandConfigurator {
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
@@ -52,7 +53,7 @@ export abstract class SlashCommandConfigurator {
const updated: string[] = [];
for (const target of this.getTargets()) {
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateBody(filePath, body);
@@ -70,7 +71,7 @@ export abstract class SlashCommandConfigurator {
// to redirect to tool-specific locations (e.g., global directories).
resolveAbsolutePath(projectPath: string, id: SlashCommandId): string {
const rel = this.getRelativePath(id);
return FileSystemUtils.joinPath(projectPath, rel);
return path.join(projectPath, rel);
}
protected async updateBody(filePath: string, body: string): Promise<void> {
+16 -56
View File
@@ -5,7 +5,6 @@ import { SlashCommandId, TemplateManager } from "../../templates/index.js";
import { FileSystemUtils } from "../../../utils/file-system.js";
import { OPENSPEC_MARKERS } from "../../config.js";
// Use POSIX-style paths for consistent logging across platforms.
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: ".codex/prompts/openspec-proposal.md",
apply: ".codex/prompts/openspec-apply.md",
@@ -21,36 +20,21 @@ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
}
protected getFrontmatter(id: SlashCommandId): string | undefined {
// Codex supports YAML frontmatter with description and argument-hint fields,
// plus $ARGUMENTS to capture all arguments as a single string.
const frontmatter: Record<SlashCommandId, string> = {
proposal: `---
description: Scaffold a new OpenSpec change and validate strictly.
argument-hint: request or feature description
---
$ARGUMENTS`,
apply: `---
description: Implement an approved OpenSpec change and keep tasks in sync.
argument-hint: change-id
---
$ARGUMENTS`,
archive: `---
description: Archive a deployed OpenSpec change and update specs.
argument-hint: change-id
---
$ARGUMENTS`,
// 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 frontmatter[id];
return headers[id];
}
private getGlobalPromptsDir(): string {
const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim())
? process.env.CODEX_HOME.trim()
: FileSystemUtils.joinPath(os.homedir(), ".codex");
return FileSystemUtils.joinPath(home, "prompts");
: path.join(os.homedir(), ".codex");
return path.join(home, "prompts");
}
// Codex discovers prompts globally. Generate directly in the global directory
@@ -60,19 +44,16 @@ $ARGUMENTS`,
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const promptsDir = this.getGlobalPromptsDir();
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
const filePath = path.join(promptsDir, path.basename(target.path));
await FileSystemUtils.createDirectory(path.dirname(filePath));
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateFullFile(filePath, target.id, body);
await this.updateBody(filePath, body);
} else {
const frontmatter = this.getFrontmatter(target.id);
const header = this.getFrontmatter(target.id);
const sections: string[] = [];
if (frontmatter) sections.push(frontmatter.trim());
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");
}
@@ -86,41 +67,20 @@ $ARGUMENTS`,
const updated: string[] = [];
for (const target of this.getTargets()) {
const promptsDir = this.getGlobalPromptsDir();
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
const filePath = path.join(promptsDir, path.basename(target.path));
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateFullFile(filePath, target.id, body);
await this.updateBody(filePath, body);
updated.push(target.path);
}
}
return updated;
}
// Update both frontmatter and body in an existing file
private async updateFullFile(filePath: string, id: SlashCommandId, body: string): Promise<void> {
const content = await FileSystemUtils.readFile(filePath);
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
if (startIndex === -1) {
throw new Error(`Missing OpenSpec start marker in ${filePath}`);
}
// Replace everything before the start marker with the new frontmatter
const frontmatter = this.getFrontmatter(id);
const sections: string[] = [];
if (frontmatter) sections.push(frontmatter.trim());
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
}
// 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 FileSystemUtils.joinPath(promptsDir, fileName);
return path.join(promptsDir, fileName);
}
}
@@ -1,39 +0,0 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.github/prompts/openspec-proposal.prompt.md',
apply: '.github/prompts/openspec-apply.prompt.md',
archive: '.github/prompts/openspec-archive.prompt.md'
};
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
description: Scaffold a new OpenSpec change and validate strictly.
---
$ARGUMENTS`,
apply: `---
description: Implement an approved OpenSpec change and keep tasks in sync.
---
$ARGUMENTS`,
archive: `---
description: Archive a deployed OpenSpec change and update specs.
---
$ARGUMENTS`
};
export class GitHubCopilotSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'github-copilot';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
-3
View File
@@ -5,7 +5,6 @@ import { WindsurfSlashCommandConfigurator } from './windsurf.js';
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
import { CodexSlashCommandConfigurator } from './codex.js';
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -17,7 +16,6 @@ export class SlashCommandRegistry {
const kilocode = new KiloCodeSlashCommandConfigurator();
const opencode = new OpenCodeSlashCommandConfigurator();
const codex = new CodexSlashCommandConfigurator();
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(cursor.toolId, cursor);
@@ -25,7 +23,6 @@ export class SlashCommandRegistry {
this.configurators.set(kilocode.toolId, kilocode);
this.configurators.set(opencode.toolId, opencode);
this.configurators.set(codex.toolId, codex);
this.configurators.set(githubCopilot.toolId, githubCopilot);
}
static register(configurator: SlashCommandConfigurator): void {
+3 -3
View File
@@ -106,9 +106,9 @@ export class UpdateCommand {
}
if (updatedSlashFiles.length > 0) {
// Normalize to forward slashes for cross-platform log consistency
const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/'));
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
summaryParts.push(
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
);
}
const failedItems = [
-27
View File
@@ -42,33 +42,6 @@ function findMarkerIndex(
}
export class FileSystemUtils {
private static isWindowsBasePath(basePath: string): boolean {
return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
}
private static normalizeSegments(segments: string[]): string[] {
return segments
.flatMap((segment) => segment.split(/[\\/]+/u))
.filter((part) => part.length > 0);
}
static joinPath(basePath: string, ...segments: string[]): string {
const normalizedSegments = this.normalizeSegments(segments);
if (this.isWindowsBasePath(basePath)) {
const normalizedBasePath = path.win32.normalize(basePath);
return normalizedSegments.length
? path.win32.join(normalizedBasePath, ...normalizedSegments)
: normalizedBasePath;
}
const posixBasePath = basePath.replace(/\\/g, '/');
return normalizedSegments.length
? path.posix.join(posixBasePath, ...normalizedSegments)
: path.posix.normalize(posixBasePath);
}
static async createDirectory(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true });
}
+3 -63
View File
@@ -339,22 +339,16 @@ describe('InitCommand', () => {
expect(await fileExists(archivePath)).toBe(true);
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('argument-hint: request or feature description');
expect(proposalContent).toContain('$ARGUMENTS');
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('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('argument-hint: change-id');
expect(applyContent).toContain('$ARGUMENTS');
expect(applyContent).toContain('Change ID: $1');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(archivePath, 'utf-8');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('argument-hint: change-id');
expect(archiveContent).toContain('$ARGUMENTS');
expect(archiveContent).toContain('Change ID: $1');
expect(archiveContent).toContain('openspec archive <id> --yes');
});
@@ -394,48 +388,6 @@ describe('InitCommand', () => {
expect(archiveContent).not.toContain('---\n');
});
it('should create GitHub Copilot prompt files with templates', async () => {
queueSelections('github-copilot', DONE);
await initCommand.execute(testDir);
const proposalPath = path.join(
testDir,
'.github/prompts/openspec-proposal.prompt.md'
);
const applyPath = path.join(
testDir,
'.github/prompts/openspec-apply.prompt.md'
);
const archivePath = path.join(
testDir,
'.github/prompts/openspec-archive.prompt.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('---');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('$ARGUMENTS');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
const applyContent = await fs.readFile(applyPath, 'utf-8');
expect(applyContent).toContain('---');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('$ARGUMENTS');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(archivePath, 'utf-8');
expect(archiveContent).toContain('---');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('$ARGUMENTS');
expect(archiveContent).toContain('openspec archive <id> --yes');
});
it('should add new tool when OpenSpec already exists', async () => {
queueSelections('claude', DONE, 'cursor', DONE);
await initCommand.execute(testDir);
@@ -558,18 +510,6 @@ describe('InitCommand', () => {
);
expect(codexChoice.configured).toBe(true);
});
it('should mark GitHub Copilot as already configured during extend mode', async () => {
queueSelections('github-copilot', DONE, 'github-copilot', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const githubCopilotChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'github-copilot'
);
expect(githubCopilotChoice.configured).toBe(true);
});
});
describe('error handling', () => {
+3 -69
View File
@@ -262,7 +262,7 @@ Old body
'.codex/prompts/openspec-apply.md'
);
await fs.mkdir(path.dirname(codexPath), { recursive: true });
const initialContent = `---\ndescription: Old description\nargument-hint: old-hint\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
const initialContent = `Change ID: $1\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
await fs.writeFile(codexPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
@@ -270,12 +270,9 @@ Old body
await updateCommand.execute(testDir);
const updated = await fs.readFile(codexPath, 'utf-8');
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(updated).toContain('argument-hint: change-id');
expect(updated).toContain('$ARGUMENTS');
expect(updated).toContain('Change ID: $1');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
expect(updated).not.toContain('Old description');
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
@@ -295,7 +292,7 @@ Old body
await fs.mkdir(path.dirname(codexApply), { recursive: true });
await fs.writeFile(
codexApply,
'---\ndescription: Old\nargument-hint: old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
'Change ID: $1\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
);
await updateCommand.execute(testDir);
@@ -314,69 +311,6 @@ Old body
await expect(FileSystemUtils.fileExists(codexArchive)).resolves.toBe(false);
});
it('should refresh existing GitHub Copilot prompts', async () => {
const ghPath = path.join(
testDir,
'.github/prompts/openspec-apply.prompt.md'
);
await fs.mkdir(path.dirname(ghPath), { recursive: true });
const initialContent = `---
description: Implement an approved OpenSpec change and keep tasks in sync.
---
$ARGUMENTS
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(ghPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(ghPath, 'utf-8');
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(updated).toContain('$ARGUMENTS');
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: .github/prompts/openspec-apply.prompt.md'
);
consoleSpy.mockRestore();
});
it('should not create missing GitHub Copilot prompts on update', async () => {
const ghApply = path.join(
testDir,
'.github/prompts/openspec-apply.prompt.md'
);
// Only create apply; leave proposal and archive missing
await fs.mkdir(path.dirname(ghApply), { recursive: true });
await fs.writeFile(
ghApply,
'---\ndescription: Old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
);
await updateCommand.execute(testDir);
const ghProposal = path.join(
testDir,
'.github/prompts/openspec-proposal.prompt.md'
);
const ghArchive = path.join(
testDir,
'.github/prompts/openspec-archive.prompt.md'
);
// Confirm they weren't created by update
await expect(FileSystemUtils.fileExists(ghProposal)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
});
it('should preserve Windsurf content outside markers during update', async () => {
const wsPath = path.join(
testDir,
-48
View File
@@ -160,52 +160,4 @@ describe('FileSystemUtils', () => {
expect(hasPermission).toBe(true);
});
});
describe('joinPath', () => {
it('should join POSIX-style paths', () => {
const result = FileSystemUtils.joinPath(
'/tmp/project',
'.claude/commands/openspec/proposal.md'
);
expect(result).toBe('/tmp/project/.claude/commands/openspec/proposal.md');
});
it('should join Linux home directory paths', () => {
const result = FileSystemUtils.joinPath(
'/home/dev/workspace/openspec',
'.cursor/commands/install.md'
);
expect(result).toBe('/home/dev/workspace/openspec/.cursor/commands/install.md');
});
it('should join Windows drive-letter paths with backslashes', () => {
const result = FileSystemUtils.joinPath(
'C:\\Users\\dev\\project',
'.claude/commands/openspec/proposal.md'
);
expect(result).toBe(
'C:\\Users\\dev\\project\\.claude\\commands\\openspec\\proposal.md'
);
});
it('should join Windows paths that use forward slashes', () => {
const result = FileSystemUtils.joinPath(
'D:/workspace/app',
'.cursor/commands/openspec-apply.md'
);
expect(result).toBe(
'D:\\workspace\\app\\.cursor\\commands\\openspec-apply.md'
);
});
it('should join UNC-style Windows paths', () => {
const result = FileSystemUtils.joinPath(
'\\server\\share\\repo',
'.windsurf/workflows/openspec-archive.md'
);
expect(result).toBe(
'\\server\\share\\repo\\.windsurf\\workflows\\openspec-archive.md'
);
});
});
});