mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b0f494754 | ||
|
|
21a0e74b74 | ||
|
|
485ef07ec7 | ||
|
|
ce5ceadbe7 | ||
|
|
9a173b917c | ||
|
|
79baabbed1 | ||
|
|
818a5922ce | ||
|
|
d8d2930182 | ||
|
|
6af6e0ccb6 | ||
|
|
50e6660018 | ||
|
|
4bbb52dda4 | ||
|
|
4a0ae49b6e | ||
|
|
646c516b0d | ||
|
|
8c1b580f03 |
@@ -1,5 +1,13 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ce5cead: - Add an `openspec view` dashboard that rolls up spec counts and change progress at a glance
|
||||
- Generate and update AI slash commands alongside the renamed `openspec/AGENTS.md` instructions file
|
||||
- Remove the deprecated `openspec diff` command and direct users to `openspec show`
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -17,9 +17,17 @@
|
||||
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/openspec_dashboard.png" alt="OpenSpec dashboard preview" width="90%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates.
|
||||
</p>
|
||||
|
||||
# OpenSpec
|
||||
|
||||
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | 🔜 AGENTS.md support (coming soon)
|
||||
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | ✅ AGENTS.md instructions
|
||||
|
||||
Create **alignment** between humans and AI coding assistants through spec-driven development. **No API keys required.**
|
||||
|
||||
@@ -92,7 +100,7 @@ openspec init
|
||||
# openspec/
|
||||
# ├── specs/ # Current specifications (truth)
|
||||
# ├── changes/ # Proposed changes
|
||||
# └── README.md # AI instructions for your tool
|
||||
# └── AGENTS.md # AI instructions for your tool
|
||||
```
|
||||
|
||||
### 2. Create Your First Change
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
@@ -40,20 +40,26 @@ Skip proposal for:
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Mark complete immediately** - Update `- [x]` after each task
|
||||
6. **Validate strictly** - Run `openspec validate [change] --strict` and address issues
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive [change] --skip-specs` for tooling-only changes
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Allow Additional AI Tool Initialization After Setup
|
||||
|
||||
## Summary
|
||||
- Let `openspec init` configure new AI coding tools for projects that already contain an OpenSpec structure.
|
||||
- Keep the initialization flow safe by skipping structure creation and only generating files for tools the user explicitly selects.
|
||||
- Provide clear feedback so users know which tool files were added versus already present.
|
||||
|
||||
## Motivation
|
||||
Today `openspec init` exits with an error once an `openspec/` directory exists. That protects the directory layout, but it blocks
|
||||
teams that start with one assistant (for example, Claude Code) and later want to add another such as Cursor. They have to create
|
||||
those files by hand or rerun `init` in a clean clone, which undermines the "easy onboarding" promise. Letting the command extend
|
||||
an existing installation keeps the workflow consistent and avoids manual file management.
|
||||
|
||||
## Proposal
|
||||
1. Detect an existing OpenSpec structure at the start of `openspec init` and branch into an "extend" mode instead of exiting.
|
||||
- Announce that the base structure already exists and that the command will only manage AI tool configuration files.
|
||||
- Keep the existing guard for directories or files we must not overwrite.
|
||||
2. Present the usual AI tool selection prompt even in extend mode, showing which tools are already configured.
|
||||
- Skip disabled options that remain "coming soon".
|
||||
- Mark already configured tools as such so users know whether selecting them will refresh or add files.
|
||||
3. When the user selects additional tools, generate the same initialization files that a fresh run would create (e.g., Cursor
|
||||
workspace files) while leaving untouched tools intact apart from marker-managed sections.
|
||||
- Do nothing when the user selects no new tools and keep the previous error messaging to avoid silently succeeding.
|
||||
4. Summarize the outcome (created, refreshed, skipped) before exiting with code 0 when work was performed.
|
||||
- Include friendly guidance that future updates to shared content still come from `openspec update`.
|
||||
|
||||
## Out of Scope
|
||||
- Changing how `openspec update` discovers or updates AI tool files.
|
||||
- Supporting brand-new AI tools beyond those already wired into the CLI.
|
||||
- Adding non-interactive flags for selecting multiple tools in one run (follow-up if needed).
|
||||
|
||||
## Risks & Mitigations
|
||||
- **User confusion about extend mode** → Explicitly log what will happen before prompting and summarise results afterward.
|
||||
- **Accidental overwrites** → Continue using marker-based updates and skip files unless the user chooses that tool.
|
||||
- **Inconsistent state if init fails mid-run** → Reuse existing rollback/transaction logic so partial writes clean up.
|
||||
@@ -0,0 +1,20 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Safety Checks
|
||||
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
|
||||
|
||||
#### Scenario: Detecting existing initialization
|
||||
- **WHEN** the `openspec/` directory already exists
|
||||
- **THEN** inform the user that OpenSpec is already initialized and skip recreating the base structure
|
||||
- **AND** continue to the AI tool selection step so additional tools can be configured
|
||||
- **AND** display the existing-initialization error message only when the user declines to add any AI tools
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Additional AI Tool Initialization
|
||||
`openspec init` SHALL allow users to add configuration files for new AI coding assistants after the initial setup.
|
||||
|
||||
#### Scenario: Configuring an extra tool after initial setup
|
||||
- **GIVEN** an `openspec/` directory already exists and at least one AI tool file is present
|
||||
- **WHEN** the user runs `openspec init` and selects a different supported AI tool
|
||||
- **THEN** generate that tool's configuration files with OpenSpec markers the same way as during first-time initialization
|
||||
- **AND** leave existing tool configuration files unchanged except for managed sections that need refreshing
|
||||
- **AND** exit with code 0 and display a success summary highlighting the newly added tool files
|
||||
@@ -0,0 +1,16 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Extend Init Guard
|
||||
- [ ] 1.1 Detect existing OpenSpec structures at the start of `openspec init` and enter an extend mode instead of failing.
|
||||
- [ ] 1.2 Log that core scaffolding will be skipped while still protecting against missing write permissions.
|
||||
|
||||
## 2. Update AI Tool Selection
|
||||
- [ ] 2.1 Present AI tool choices even in extend mode, indicating which tools are already configured.
|
||||
- [ ] 2.2 Ensure disabled "coming soon" tools remain non-selectable.
|
||||
|
||||
## 3. Generate Additional Tool Files
|
||||
- [ ] 3.1 Create configuration files for newly selected tools while leaving untouched tools unaffected apart from marker-managed sections.
|
||||
- [ ] 3.2 Summarize created, refreshed, and skipped tools before exiting with the appropriate code.
|
||||
|
||||
## 4. Verification
|
||||
- [ ] 4.1 Add tests covering rerunning `openspec init` to add another tool and the scenario where the user declines to add anything.
|
||||
@@ -1,16 +1,16 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Templates and Configurators
|
||||
- [ ] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
|
||||
- [ ] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
|
||||
- [x] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
|
||||
- [x] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
|
||||
|
||||
## 2. Claude Code Integration
|
||||
- [ ] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
|
||||
- [ ] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
|
||||
- [x] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
|
||||
- [x] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
|
||||
|
||||
## 3. Cursor Integration
|
||||
- [ ] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
|
||||
- [ ] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
|
||||
- [x] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
|
||||
- [x] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
|
||||
|
||||
## 4. Verification
|
||||
- [ ] 4.1 Add tests verifying slash command files are created and updated correctly.
|
||||
- [x] 4.1 Add tests verifying slash command files are created and updated correctly.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Update Agent Instruction File Name - Tasks
|
||||
|
||||
## 1. Rename Instruction File
|
||||
- [ ] Rename `openspec/README.md` to `openspec/AGENTS.md`
|
||||
- [ ] Update root references to new path
|
||||
- [x] Rename `openspec/README.md` to `openspec/AGENTS.md`
|
||||
- [x] Update root references to new path
|
||||
|
||||
## 2. Update Templates
|
||||
- [ ] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
|
||||
- [ ] Update exported constant from `readmeTemplate` to `agentsTemplate`
|
||||
- [x] Rename `src/core/templates/readme-template.ts` to `agents-template.ts`
|
||||
- [x] Update exported constant from `readmeTemplate` to `agentsTemplate`
|
||||
|
||||
## 3. Adjust CLI Commands
|
||||
- [ ] Modify `openspec init` to generate `AGENTS.md`
|
||||
- [ ] Update `openspec update` to refresh `AGENTS.md`
|
||||
- [ ] Ensure CLAUDE.md markers link to `@openspec/AGENTS.md`
|
||||
- [x] Modify `openspec init` to generate `AGENTS.md`
|
||||
- [x] Update `openspec update` to refresh `AGENTS.md`
|
||||
- [x] Ensure CLAUDE.md markers link to `@openspec/AGENTS.md`
|
||||
|
||||
## 4. Update Specifications
|
||||
- [ ] Modify `cli-init` spec to reference `AGENTS.md`
|
||||
- [ ] Modify `cli-update` spec to reference `AGENTS.md`
|
||||
- [ ] Modify `openspec-conventions` spec to include `AGENTS.md` in project structure
|
||||
- [x] Modify `cli-init` spec to reference `AGENTS.md`
|
||||
- [x] Modify `cli-update` spec to reference `AGENTS.md`
|
||||
- [x] Modify `openspec-conventions` spec to include `AGENTS.md` in project structure
|
||||
|
||||
## 5. Validation
|
||||
- [ ] `pnpm test`
|
||||
- [x] `pnpm test`
|
||||
|
||||
@@ -31,7 +31,7 @@ The command SHALL create the complete OpenSpec directory structure with all requ
|
||||
```
|
||||
openspec/
|
||||
├── project.md
|
||||
├── README.md
|
||||
├── AGENTS.md
|
||||
├── specs/
|
||||
└── changes/
|
||||
└── archive/
|
||||
@@ -44,7 +44,7 @@ The command SHALL generate required template files with appropriate content for
|
||||
#### Scenario: Generating template files
|
||||
|
||||
- **WHEN** initializing OpenSpec
|
||||
- **THEN** generate `README.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **AND** generate `project.md` with project context template
|
||||
|
||||
### Requirement: AI Tool Configuration
|
||||
@@ -80,7 +80,7 @@ This document provides instructions for AI coding assistants on how to use OpenS
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
See @openspec/AGENTS.md for detailed conventions and guidelines.
|
||||
<!-- OPENSPEC:END -->
|
||||
```
|
||||
|
||||
@@ -164,7 +164,7 @@ Next steps - Copy these prompts to Claude:
|
||||
OpenSpec change proposal for this feature"
|
||||
|
||||
3. Learn the OpenSpec workflow:
|
||||
"Please explain the OpenSpec workflow from openspec/README.md
|
||||
"Please explain the OpenSpec workflow from openspec/AGENTS.md
|
||||
and how I should work with you on this project"
|
||||
────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@ The update command SHALL update OpenSpec instruction files to the latest templat
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** the command SHALL:
|
||||
- Check if the `openspec` directory exists
|
||||
- Replace `openspec/README.md` with the latest template (complete replacement)
|
||||
- Replace `openspec/AGENTS.md` with the latest template (complete replacement)
|
||||
- Update **only existing** AI tool configuration files (e.g., CLAUDE.md)
|
||||
- Check each registered AI tool configurator
|
||||
- For each configurator, check if its file exists
|
||||
@@ -40,7 +40,7 @@ The update command SHALL handle file updates in a predictable and safe manner.
|
||||
#### Scenario: Updating files
|
||||
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/README.md` with the latest template
|
||||
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
|
||||
- **AND** use the default directory name `openspec`
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
@@ -64,7 +64,7 @@ The update command SHALL always update the core OpenSpec files and display an AS
|
||||
#### Scenario: Successful update
|
||||
|
||||
- **WHEN** the update completes successfully
|
||||
- **THEN** replace `openspec/README.md` with the latest template
|
||||
- **THEN** replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** update existing AI tool configuration files within markers
|
||||
- **AND** display the message: "Updated OpenSpec instructions"
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ An OpenSpec project SHALL maintain a consistent directory structure for specific
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── AGENTS.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
@@ -245,7 +245,7 @@ An OpenSpec project SHALL maintain a consistent directory structure for specific
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── AGENTS.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export const OPENSPEC_MARKERS = {
|
||||
|
||||
export const AI_TOOLS = [
|
||||
{ name: 'Claude Code', value: 'claude', available: true },
|
||||
{ name: 'Cursor', value: 'cursor', available: false },
|
||||
{ name: 'Cursor', value: 'cursor', available: true },
|
||||
{ name: 'Aider', value: 'aider', available: false },
|
||||
{ name: 'Continue', value: 'continue', available: false }
|
||||
];
|
||||
];
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../../../utils/file-system.js';
|
||||
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../../config.js';
|
||||
|
||||
export interface SlashCommandTarget {
|
||||
id: SlashCommandId;
|
||||
path: string;
|
||||
kind: 'slash';
|
||||
}
|
||||
|
||||
const ALL_COMMANDS: SlashCommandId[] = ['proposal', 'apply', 'archive'];
|
||||
|
||||
export abstract class SlashCommandConfigurator {
|
||||
abstract readonly toolId: string;
|
||||
abstract readonly isAvailable: boolean;
|
||||
|
||||
getTargets(): SlashCommandTarget[] {
|
||||
return ALL_COMMANDS.map((id) => ({
|
||||
id,
|
||||
path: this.getRelativePath(id),
|
||||
kind: 'slash'
|
||||
}));
|
||||
}
|
||||
|
||||
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 filePath = path.join(projectPath, target.path);
|
||||
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
await this.updateBody(filePath, body);
|
||||
} else {
|
||||
const frontmatter = this.getFrontmatter(target.id);
|
||||
const sections: string[] = [];
|
||||
if (frontmatter) {
|
||||
sections.push(frontmatter.trim());
|
||||
}
|
||||
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
|
||||
const content = sections.join('\n') + '\n';
|
||||
await FileSystemUtils.writeFile(filePath, content);
|
||||
}
|
||||
|
||||
createdOrUpdated.push(target.path);
|
||||
}
|
||||
|
||||
return createdOrUpdated;
|
||||
}
|
||||
|
||||
async updateExisting(projectPath: string, _openspecDir: string): Promise<string[]> {
|
||||
const updated: string[] = [];
|
||||
|
||||
for (const target of this.getTargets()) {
|
||||
const filePath = path.join(projectPath, 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;
|
||||
}
|
||||
|
||||
protected abstract getRelativePath(id: SlashCommandId): string;
|
||||
protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
|
||||
|
||||
private 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);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error(`Missing OpenSpec markers in ${filePath}`);
|
||||
}
|
||||
|
||||
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
|
||||
const after = content.slice(endIndex);
|
||||
const updatedContent = `${before}\n${body}\n${after}`;
|
||||
|
||||
await FileSystemUtils.writeFile(filePath, updatedContent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.claude/commands/openspec/proposal.md',
|
||||
apply: '.claude/commands/openspec/apply.md',
|
||||
archive: '.claude/commands/openspec/archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: OpenSpec: Proposal
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---`,
|
||||
apply: `---
|
||||
name: OpenSpec: Apply
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
category: OpenSpec
|
||||
tags: [openspec, apply]
|
||||
---`,
|
||||
archive: `---
|
||||
name: OpenSpec: Archive
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
category: OpenSpec
|
||||
tags: [openspec, archive]
|
||||
---`
|
||||
};
|
||||
|
||||
export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'claude';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.cursor/commands/openspec-proposal.md',
|
||||
apply: '.cursor/commands/openspec-apply.md',
|
||||
archive: '.cursor/commands/openspec-archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: /openspec-proposal
|
||||
id: openspec-proposal
|
||||
category: OpenSpec
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---`,
|
||||
apply: `---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---`,
|
||||
archive: `---
|
||||
name: /openspec-archive
|
||||
id: openspec-archive
|
||||
category: OpenSpec
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---`
|
||||
};
|
||||
|
||||
export class CursorSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'cursor';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
||||
import { CursorSlashCommandConfigurator } from './cursor.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
|
||||
static {
|
||||
const claude = new ClaudeSlashCommandConfigurator();
|
||||
const cursor = new CursorSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
this.configurators.set(configurator.toolId, configurator);
|
||||
}
|
||||
|
||||
static get(toolId: string): SlashCommandConfigurator | undefined {
|
||||
return this.configurators.get(toolId);
|
||||
}
|
||||
|
||||
static getAll(): SlashCommandConfigurator[] {
|
||||
return Array.from(this.configurators.values());
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -4,6 +4,7 @@ import ora from 'ora';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { TemplateManager, ProjectContext } from './templates/index.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
||||
import { OpenSpecConfig, AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
||||
|
||||
export class InitCommand {
|
||||
@@ -105,6 +106,11 @@ export class InitCommand {
|
||||
if (configurator && configurator.isAvailable) {
|
||||
await configurator.configure(projectPath, openspecDir);
|
||||
}
|
||||
|
||||
const slashConfigurator = SlashCommandRegistry.get(toolId);
|
||||
if (slashConfigurator && slashConfigurator.isAvailable) {
|
||||
await slashConfigurator.generateAll(projectPath, openspecDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +132,8 @@ export class InitCommand {
|
||||
console.log(' "I want to add [YOUR FEATURE HERE]. Please create an');
|
||||
console.log(' OpenSpec change proposal for this feature"\n');
|
||||
console.log('3. Learn the OpenSpec workflow:');
|
||||
console.log(' "Please explain the OpenSpec workflow from openspec/README.md');
|
||||
console.log(' "Please explain the OpenSpec workflow from openspec/AGENTS.md');
|
||||
console.log(' and how I should work with you on this project"');
|
||||
console.log('────────────────────────────────────────────────────────────\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const readmeTemplate = `# OpenSpec Instructions
|
||||
export const agentsTemplate = `# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
@@ -40,20 +40,26 @@ Skip proposal for:
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review \`openspec/project.md\`, \`openspec list\`, and \`openspec list --specs\` to understand current context.
|
||||
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and spec deltas under \`openspec/changes/<id>/\`.
|
||||
3. Draft spec deltas using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
|
||||
4. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Mark complete immediately** - Update \`- [x]\` after each task
|
||||
6. **Validate strictly** - Run \`openspec validate [change] --strict\` and address issues
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move \`changes/[name]/\` → \`changes/archive/YYYY-MM-DD-[name]/\`
|
||||
- Update \`specs/\` if capabilities changed
|
||||
- Use \`openspec archive [change] --skip-specs\` for tooling-only changes
|
||||
- Run \`openspec validate --strict\` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
@@ -2,7 +2,7 @@ export const claudeTemplate = `# OpenSpec Project
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
See @openspec/AGENTS.md for detailed conventions and guidelines.
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readmeTemplate } from './readme-template.js';
|
||||
import { agentsTemplate } from './agents-template.js';
|
||||
import { projectTemplate, ProjectContext } from './project-template.js';
|
||||
import { claudeTemplate } from './claude-template.js';
|
||||
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
|
||||
|
||||
export interface Template {
|
||||
path: string;
|
||||
@@ -11,8 +12,8 @@ export class TemplateManager {
|
||||
static getTemplates(context: ProjectContext = {}): Template[] {
|
||||
return [
|
||||
{
|
||||
path: 'README.md',
|
||||
content: readmeTemplate
|
||||
path: 'AGENTS.md',
|
||||
content: agentsTemplate
|
||||
},
|
||||
{
|
||||
path: 'project.md',
|
||||
@@ -24,6 +25,11 @@ export class TemplateManager {
|
||||
static getClaudeTemplate(): string {
|
||||
return claudeTemplate;
|
||||
}
|
||||
|
||||
static getSlashCommandBody(id: SlashCommandId): string {
|
||||
return getSlashCommandBody(id);
|
||||
}
|
||||
}
|
||||
|
||||
export { ProjectContext } from './project-template.js';
|
||||
export { ProjectContext } from './project-template.js';
|
||||
export type { SlashCommandId } from './slash-command-templates.js';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export type SlashCommandId = 'proposal' | 'apply' | 'archive';
|
||||
|
||||
const baseGuardrails = `**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
|
||||
|
||||
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
|
||||
|
||||
const proposalSteps = `**Steps**
|
||||
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
|
||||
|
||||
const proposalReferences = `**Reference**
|
||||
- Use \`openspec show <id> --json --deltas-only\` or \`openspec show <spec> --type spec\` to inspect details when validation fails.
|
||||
- Search existing requirements with \`rg -n "Requirement:|Scenario:" openspec/specs\` before writing new ones.
|
||||
- Explore the codebase with \`rg <keyword>\`, \`ls\`, or direct file reads so proposals align with current implementation realities.`;
|
||||
|
||||
const applySteps = `**Steps**
|
||||
1. Read \`changes/<id>/proposal.md\`, \`design.md\` (if present), and \`tasks.md\` to confirm scope and acceptance criteria.
|
||||
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||
3. Mark each task \`- [x]\` immediately after completing it to keep the checklist in sync.
|
||||
4. Reference \`openspec list\` or \`openspec show <item>\` when additional context is required.`;
|
||||
|
||||
const applyReferences = `**Reference**
|
||||
- Use \`openspec show <id> --json --deltas-only\` if you need additional context from the proposal while implementing.`;
|
||||
|
||||
const archiveSteps = `**Steps**
|
||||
1. Identify the requested change ID (via the prompt or \`openspec list\`).
|
||||
2. Run \`openspec archive <id>\` to let the CLI move the change and apply spec updates (use \`--skip-specs\` only for tooling-only work).
|
||||
3. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
|
||||
4. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;
|
||||
|
||||
const archiveReferences = `**Reference**
|
||||
- Inspect refreshed specs with \`openspec list --specs\` and address any validation issues before handing off.`;
|
||||
|
||||
export const slashCommandBodies: Record<SlashCommandId, string> = {
|
||||
proposal: [proposalGuardrails, proposalSteps, proposalReferences].join('\n\n'),
|
||||
apply: [baseGuardrails, applySteps, applyReferences].join('\n\n'),
|
||||
archive: [baseGuardrails, archiveSteps, archiveReferences].join('\n\n')
|
||||
};
|
||||
|
||||
export function getSlashCommandBody(id: SlashCommandId): string {
|
||||
return slashCommandBodies[id];
|
||||
}
|
||||
+36
-5
@@ -1,8 +1,9 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { OPENSPEC_DIR_NAME } from './config.js';
|
||||
import { readmeTemplate } from './templates/readme-template.js';
|
||||
import { agentsTemplate } from './templates/agents-template.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
||||
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
@@ -15,14 +16,17 @@ export class UpdateCommand {
|
||||
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
||||
}
|
||||
|
||||
// 2. Update README.md (full replacement)
|
||||
const readmePath = path.join(openspecPath, 'README.md');
|
||||
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
|
||||
// 2. Update AGENTS.md (full replacement)
|
||||
const agentsPath = path.join(openspecPath, 'AGENTS.md');
|
||||
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
|
||||
|
||||
// 3. Update existing AI tool configuration files only
|
||||
const configurators = ToolRegistry.getAll();
|
||||
const slashConfigurators = SlashCommandRegistry.getAll();
|
||||
let updatedFiles: string[] = [];
|
||||
let failedFiles: string[] = [];
|
||||
let updatedSlashFiles: string[] = [];
|
||||
let failedSlashTools: string[] = [];
|
||||
|
||||
for (const configurator of configurators) {
|
||||
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
||||
@@ -30,6 +34,9 @@ export class UpdateCommand {
|
||||
// Only update if the file already exists
|
||||
if (await FileSystemUtils.fileExists(configFilePath)) {
|
||||
try {
|
||||
if (!await FileSystemUtils.canWriteFile(configFilePath)) {
|
||||
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
|
||||
}
|
||||
await configurator.configure(resolvedProjectPath, openspecPath);
|
||||
updatedFiles.push(configurator.configFileName);
|
||||
} catch (error) {
|
||||
@@ -39,16 +46,40 @@ export class UpdateCommand {
|
||||
}
|
||||
}
|
||||
|
||||
for (const slashConfigurator of slashConfigurators) {
|
||||
if (!slashConfigurator.isAvailable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
|
||||
updatedSlashFiles = updatedSlashFiles.concat(updated);
|
||||
} catch (error) {
|
||||
failedSlashTools.push(slashConfigurator.toolId);
|
||||
console.error(
|
||||
`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Success message (ASCII-safe)
|
||||
const messages: string[] = ['Updated OpenSpec instructions (README.md)'];
|
||||
const messages: string[] = ['Updated OpenSpec instructions (AGENTS.md)'];
|
||||
|
||||
if (updatedFiles.length > 0) {
|
||||
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (updatedSlashFiles.length > 0) {
|
||||
messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedSlashTools.length > 0) {
|
||||
messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(messages.join('\n'));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,25 @@ export class FileSystemUtils {
|
||||
}
|
||||
}
|
||||
|
||||
static async canWriteFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
if (!stats.isFile()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (stats.mode & 0o222) !== 0;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async directoryExists(dirPath: string): Promise<boolean> {
|
||||
try {
|
||||
const stats = await fs.stat(dirPath);
|
||||
|
||||
+62
-8
@@ -40,17 +40,17 @@ describe('InitCommand', () => {
|
||||
expect(await directoryExists(path.join(openspecPath, 'changes', 'archive'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should create README.md and project.md', async () => {
|
||||
it('should create AGENTS.md and project.md', async () => {
|
||||
vi.mocked(prompts.select).mockResolvedValue('claude');
|
||||
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
|
||||
const openspecPath = path.join(testDir, 'openspec');
|
||||
expect(await fileExists(path.join(openspecPath, 'README.md'))).toBe(true);
|
||||
expect(await fileExists(path.join(openspecPath, 'AGENTS.md'))).toBe(true);
|
||||
expect(await fileExists(path.join(openspecPath, 'project.md'))).toBe(true);
|
||||
|
||||
const readmeContent = await fs.readFile(path.join(openspecPath, 'README.md'), 'utf-8');
|
||||
expect(readmeContent).toContain('OpenSpec Instructions');
|
||||
|
||||
const agentsContent = await fs.readFile(path.join(openspecPath, 'AGENTS.md'), 'utf-8');
|
||||
expect(agentsContent).toContain('OpenSpec Instructions');
|
||||
|
||||
const projectContent = await fs.readFile(path.join(openspecPath, 'project.md'), 'utf-8');
|
||||
expect(projectContent).toContain('Project Context');
|
||||
@@ -86,6 +86,60 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create Claude slash command files with templates', async () => {
|
||||
vi.mocked(prompts.select).mockResolvedValue('claude');
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const claudeProposal = path.join(testDir, '.claude/commands/openspec/proposal.md');
|
||||
const claudeApply = path.join(testDir, '.claude/commands/openspec/apply.md');
|
||||
const claudeArchive = path.join(testDir, '.claude/commands/openspec/archive.md');
|
||||
|
||||
expect(await fileExists(claudeProposal)).toBe(true);
|
||||
expect(await fileExists(claudeApply)).toBe(true);
|
||||
expect(await fileExists(claudeArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(claudeProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('name: OpenSpec: Proposal');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(claudeApply, 'utf-8');
|
||||
expect(applyContent).toContain('name: OpenSpec: Apply');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(claudeArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('name: OpenSpec: Archive');
|
||||
expect(archiveContent).toContain('openspec archive <id>');
|
||||
expect(archiveContent).toContain('`--skip-specs` only for tooling-only work');
|
||||
});
|
||||
|
||||
it('should create Cursor slash command files with templates', async () => {
|
||||
vi.mocked(prompts.select).mockResolvedValue('cursor');
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const cursorProposal = path.join(testDir, '.cursor/commands/openspec-proposal.md');
|
||||
const cursorApply = path.join(testDir, '.cursor/commands/openspec-apply.md');
|
||||
const cursorArchive = path.join(testDir, '.cursor/commands/openspec-archive.md');
|
||||
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
expect(await fileExists(cursorApply)).toBe(true);
|
||||
expect(await fileExists(cursorArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(cursorProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('name: /openspec-proposal');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
|
||||
|
||||
const applyContent = await fs.readFile(cursorApply, 'utf-8');
|
||||
expect(applyContent).toContain('id: openspec-apply');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(cursorArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('name: /openspec-archive');
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
});
|
||||
|
||||
it('should throw error if OpenSpec already exists', async () => {
|
||||
const openspecPath = path.join(testDir, 'openspec');
|
||||
await fs.mkdir(openspecPath, { recursive: true });
|
||||
@@ -180,4 +234,4 @@ async function directoryExists(dirPath: string): Promise<boolean> {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+94
-10
@@ -56,11 +56,42 @@ More content after.`;
|
||||
|
||||
// Check console output
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
|
||||
'Updated OpenSpec instructions (AGENTS.md)\nUpdated AI tool files: CLAUDE.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Claude slash command files', async () => {
|
||||
const proposalPath = path.join(testDir, '.claude/commands/openspec/proposal.md');
|
||||
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
name: OpenSpec: Proposal
|
||||
description: Old description
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old slash content
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(proposalPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(updated).toContain('name: OpenSpec: Proposal');
|
||||
expect(updated).toContain('**Guardrails**');
|
||||
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
|
||||
expect(updated).not.toContain('Old slash content');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (AGENTS.md)\nUpdated slash commands: .claude/commands/openspec/proposal.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create CLAUDE.md if it does not exist', async () => {
|
||||
// Ensure CLAUDE.md does not exist
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
@@ -73,13 +104,43 @@ More content after.`;
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Cursor slash command files', async () => {
|
||||
const cursorPath = path.join(testDir, '.cursor/commands/openspec-apply.md');
|
||||
await fs.mkdir(path.dirname(cursorPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Old description
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(cursorPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(cursorPath, 'utf-8');
|
||||
expect(updated).toContain('id: openspec-apply');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (AGENTS.md)\nUpdated slash commands: .cursor/commands/openspec-apply.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle no AI tool files present', async () => {
|
||||
// Execute update command with no AI tool files
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Should only update OpenSpec instructions
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Updated OpenSpec instructions (README.md)');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Updated OpenSpec instructions (AGENTS.md)');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -89,6 +150,7 @@ More content after.`;
|
||||
// that all existing files are updated in a single operation.
|
||||
// For now, we test with just CLAUDE.md.
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
await fs.mkdir(path.dirname(claudePath), { recursive: true });
|
||||
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
@@ -96,11 +158,33 @@ More content after.`;
|
||||
|
||||
// Should report updating with new format
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
|
||||
'Updated OpenSpec instructions (AGENTS.md)\nUpdated AI tool files: CLAUDE.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip creating missing slash commands during update', async () => {
|
||||
const proposalPath = path.join(testDir, '.claude/commands/openspec/proposal.md');
|
||||
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
||||
await fs.writeFile(proposalPath, `---
|
||||
name: OpenSpec: Proposal
|
||||
description: Existing file
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old content
|
||||
<!-- OPENSPEC:END -->`);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const applyExists = await FileSystemUtils.fileExists(path.join(testDir, '.claude/commands/openspec/apply.md'));
|
||||
const archiveExists = await FileSystemUtils.fileExists(path.join(testDir, '.claude/commands/openspec/archive.md'));
|
||||
|
||||
expect(applyExists).toBe(false);
|
||||
expect(archiveExists).toBe(false);
|
||||
});
|
||||
|
||||
it('should never create new AI tool files', async () => {
|
||||
// Get all configurators
|
||||
const configurators = ToolRegistry.getAll();
|
||||
@@ -116,16 +200,16 @@ More content after.`;
|
||||
}
|
||||
});
|
||||
|
||||
it('should update README.md in openspec directory', async () => {
|
||||
it('should update AGENTS.md in openspec directory', async () => {
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that README.md was created/updated
|
||||
const readmePath = path.join(testDir, 'openspec', 'README.md');
|
||||
const fileExists = await FileSystemUtils.fileExists(readmePath);
|
||||
// Check that AGENTS.md was created/updated
|
||||
const agentsPath = path.join(testDir, 'openspec', 'AGENTS.md');
|
||||
const fileExists = await FileSystemUtils.fileExists(agentsPath);
|
||||
expect(fileExists).toBe(true);
|
||||
|
||||
const content = await fs.readFile(readmePath, 'utf-8');
|
||||
const content = await fs.readFile(agentsPath, 'utf-8');
|
||||
expect(content).toContain('# OpenSpec Instructions');
|
||||
});
|
||||
|
||||
@@ -162,7 +246,7 @@ More content after.`;
|
||||
// Should report the failure
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nFailed to update: CLAUDE.md'
|
||||
'Updated OpenSpec instructions (AGENTS.md)\nFailed to update: CLAUDE.md'
|
||||
);
|
||||
|
||||
// Restore permissions for cleanup
|
||||
@@ -171,4 +255,4 @@ More content after.`;
|
||||
errorSpy.mockRestore();
|
||||
writeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user