mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6cad1631c | ||
|
|
0b1be19302 | ||
|
|
8f45a6f6ee | ||
|
|
6da77f01ce | ||
|
|
d90eccf959 | ||
|
|
f192a97aeb | ||
|
|
7781bbadd3 | ||
|
|
aeaa1d50cc | ||
|
|
fa5df9a329 | ||
|
|
f94f396c99 | ||
|
|
1f670f71d4 | ||
|
|
32b2901d13 | ||
|
|
2ad0b1d306 | ||
|
|
279d327899 | ||
|
|
5d848cf005 | ||
|
|
5607fd3ccb | ||
|
|
6a0d862258 | ||
|
|
1fe5f84fbc | ||
|
|
80e78ecd1e | ||
|
|
564135a530 | ||
|
|
b9e80641a0 | ||
|
|
0755994eaa | ||
|
|
dcabd6de31 | ||
|
|
aef6ce01ff | ||
|
|
441f9f444b | ||
|
|
b322829091 | ||
|
|
5167e65a5c | ||
|
|
5c6b4113a7 | ||
|
|
a8b76c3e69 | ||
|
|
d3237cac7b |
@@ -0,0 +1,87 @@
|
||||
# OpenSpec
|
||||
|
||||
A specification-driven development system for maintaining living documentation alongside your code.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g openspec
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize OpenSpec in your project
|
||||
openspec init
|
||||
|
||||
# Update existing OpenSpec instructions (team-friendly)
|
||||
openspec update
|
||||
|
||||
# List all specifications
|
||||
openspec list
|
||||
|
||||
# Show differences between specs and proposed changes
|
||||
openspec diff [change-name]
|
||||
|
||||
# Archive completed changes
|
||||
openspec archive [change-name]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `openspec init`
|
||||
|
||||
Initializes OpenSpec in your project by creating:
|
||||
- `openspec/` directory structure
|
||||
- `openspec/README.md` with OpenSpec instructions
|
||||
- AI tool configuration files (based on your selection)
|
||||
|
||||
### `openspec update`
|
||||
|
||||
Updates OpenSpec instructions to the latest version. This command is **team-friendly** and only updates files that already exist:
|
||||
|
||||
- Always updates `openspec/README.md` with the latest OpenSpec instructions
|
||||
- **Only updates existing AI tool configuration files** (e.g., CLAUDE.md, CURSOR.md)
|
||||
- **Never creates new AI tool configuration files**
|
||||
- Preserves content outside of OpenSpec markers in AI tool files
|
||||
|
||||
This allows team members to use different AI tools without conflicts. Each developer can maintain their preferred AI tool configuration file, and `openspec update` will respect their choice.
|
||||
|
||||
### `openspec list`
|
||||
|
||||
Lists all specifications and pending changes in your project:
|
||||
- Shows current specifications in `openspec/specs/`
|
||||
- Shows pending changes in `openspec/changes/`
|
||||
- Shows archived changes in `openspec/changes/archive/`
|
||||
|
||||
### `openspec diff [change-name]`
|
||||
|
||||
Shows the differences between current specs and proposed changes:
|
||||
- Displays a unified diff format
|
||||
- Helps review what will change before implementation
|
||||
- Useful for pull request reviews
|
||||
|
||||
### `openspec archive [change-name]`
|
||||
|
||||
Archives a completed change:
|
||||
- Moves change from `openspec/changes/` to `openspec/changes/archive/`
|
||||
- Adds a date prefix to the archived change
|
||||
- Updates specs to reflect the new state
|
||||
- Use `--skip-specs` to archive without updating specs (for abandoned changes)
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
OpenSpec is designed for team collaboration:
|
||||
|
||||
1. **AI Tool Flexibility**: Each team member can use their preferred AI assistant (Claude, Cursor, etc.)
|
||||
2. **Non-Invasive Updates**: The `update` command only modifies existing files, never forcing tools on team members
|
||||
3. **Specification Sharing**: The `openspec/` directory contains shared specifications that all team members work from
|
||||
4. **Change Tracking**: Proposed changes are visible to all team members for review before implementation
|
||||
|
||||
## Contributing
|
||||
|
||||
See `openspec/specs/` for the current system specifications and `openspec/changes/` for pending improvements.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,26 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [ ] 1.1 Create `src/core/list.ts` with list logic
|
||||
- [ ] 1.1.1 Implement directory scanning (exclude archive/)
|
||||
- [ ] 1.1.2 Implement task counting from tasks.md files
|
||||
- [ ] 1.1.3 Format output as simple table
|
||||
- [ ] 1.2 Add list command to CLI in `src/cli/index.ts`
|
||||
- [ ] 1.2.1 Register `openspec list` command
|
||||
- [ ] 1.2.2 Connect to list.ts implementation
|
||||
|
||||
## 2. Error Handling
|
||||
- [ ] 2.1 Handle missing openspec/changes/ directory
|
||||
- [ ] 2.2 Handle changes without tasks.md files
|
||||
- [ ] 2.3 Handle empty changes directory
|
||||
|
||||
## 3. Testing
|
||||
- [ ] 3.1 Add tests for list functionality
|
||||
- [ ] 3.1.1 Test with multiple changes
|
||||
- [ ] 3.1.2 Test with completed changes
|
||||
- [ ] 3.1.3 Test with no changes
|
||||
- [ ] 3.1.4 Test error conditions
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update CLI help text with list command
|
||||
- [ ] 4.2 Add list command to README if applicable
|
||||
@@ -1,15 +0,0 @@
|
||||
# Add @requirement Markers for Requirement Identification
|
||||
|
||||
## Why
|
||||
Specs contain WHEN/THEN patterns that define system requirements, but extracting these programmatically requires brittle regex parsing that may miss edge cases or break with formatting changes.
|
||||
|
||||
## What Changes
|
||||
- Define @requirement marker convention for identifying key requirements in specs
|
||||
- Each marker includes a brief identifier (e.g., @requirement user-register)
|
||||
- Markers appear directly before their WHEN/THEN blocks
|
||||
- Document convention in openspec-conventions spec
|
||||
|
||||
## Impact
|
||||
- Affected specs: openspec-conventions (new)
|
||||
- Affected code: None initially - enables future tooling
|
||||
- Breaking changes: None - additive convention only
|
||||
@@ -1,24 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Define Convention
|
||||
- [ ] 1.1 Document @requirement marker syntax
|
||||
- [ ] 1.2 Define identifier naming guidelines
|
||||
- [ ] 1.3 Specify marker placement rules
|
||||
- [ ] 1.4 Add examples of proper usage
|
||||
|
||||
## 2. Create Specification
|
||||
- [ ] 2.1 Write openspec-conventions spec
|
||||
- [ ] 2.2 Include requirement marker section
|
||||
- [ ] 2.3 Add good and bad examples
|
||||
- [ ] 2.4 Document edge cases
|
||||
|
||||
## 3. Update Existing Specs
|
||||
- [ ] 3.1 Add @requirement markers to cli-init spec
|
||||
- [ ] 3.2 Add @requirement markers to cli-update spec
|
||||
- [ ] 3.3 Add @requirement markers to cli-view spec
|
||||
- [ ] 3.4 Review and update any other existing specs
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update README with marker convention
|
||||
- [ ] 4.2 Add marker usage to CLAUDE.md
|
||||
- [ ] 4.3 Create examples for AI assistants
|
||||
@@ -0,0 +1,13 @@
|
||||
## Why
|
||||
The archive command currently forces users to either accept spec updates or cancel the entire archive operation. Users need flexibility to archive changes without updating specs, either through explicit flags or by declining the confirmation prompt. This is especially important for changes that don't modify specs (like tooling, documentation, or infrastructure updates).
|
||||
|
||||
## What Changes
|
||||
- Add new `--skip-specs` flag to the archive command that bypasses all spec update operations
|
||||
- Fix confirmation behavior: when users decline spec updates interactively, proceed with archiving instead of cancelling the entire operation
|
||||
- When `--skip-specs` flag is used, skip both the spec discovery and update confirmation steps entirely
|
||||
- Display clear message when specs are skipped (either via flag or user choice)
|
||||
- Flag can be combined with existing `--yes` flag for fully automated archiving without spec updates
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-archive
|
||||
- Affected code: src/core/archive.ts, src/cli/index.ts
|
||||
@@ -0,0 +1,167 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y] [--skip-specs]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
- `--skip-specs`: Skip spec update operations entirely (for changes without spec modifications)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Requirement: Change Selection
|
||||
|
||||
The command SHALL support both interactive and direct change selection methods.
|
||||
|
||||
#### Scenario: Interactive selection
|
||||
|
||||
- **WHEN** no change-name is provided
|
||||
- **THEN** display interactive list of available changes (excluding archive/)
|
||||
- **AND** allow user to select one
|
||||
|
||||
#### Scenario: Direct selection
|
||||
|
||||
- **WHEN** change-name is provided
|
||||
- **THEN** use that change directly
|
||||
- **AND** validate it exists
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The command SHALL verify task completion status before archiving to prevent premature archival.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display all incomplete tasks to the user
|
||||
- **AND** prompt for confirmation to continue
|
||||
- **AND** default to "No" for safety
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** all tasks are complete OR no tasks.md exists
|
||||
- **THEN** proceed with archiving without prompting
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The archive operation SHALL follow a structured process to safely move changes to the archive.
|
||||
|
||||
#### Scenario: Performing archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** execute these steps:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs unless `--skip-specs` is provided (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** do not overwrite existing archive
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** move succeeds
|
||||
- **THEN** display success message with archived name and list of updated specs (if any)
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality unless the `--skip-specs` flag is provided.
|
||||
|
||||
#### Scenario: Skipping spec updates
|
||||
|
||||
- **WHEN** the `--skip-specs` flag is provided
|
||||
- **THEN** skip all spec discovery and update operations
|
||||
- **AND** proceed directly to moving the change to archive
|
||||
- **AND** display message indicating specs were skipped
|
||||
|
||||
#### Scenario: Updating specs from change
|
||||
|
||||
- **WHEN** the change contains specs in `changes/[name]/specs/` AND `--skip-specs` is NOT provided
|
||||
- **THEN** execute these steps:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs exist in the change AND `--skip-specs` is NOT provided
|
||||
- **THEN** skip the spec update step
|
||||
- **AND** proceed with archiving
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
|
||||
|
||||
#### Scenario: Displaying confirmation
|
||||
|
||||
- **WHEN** prompting for confirmation AND `--skip-specs` is NOT provided
|
||||
- **THEN** display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- **AND** format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
#### Scenario: Handling confirmation response
|
||||
|
||||
- **WHEN** waiting for user confirmation
|
||||
- **THEN** default to "No" for safety (require explicit "y" or "yes")
|
||||
- **AND** skip confirmation when `--yes` or `-y` flag is provided
|
||||
- **AND** skip entire spec confirmation when `--skip-specs` flag is provided
|
||||
|
||||
#### Scenario: User declines spec update confirmation
|
||||
|
||||
- **WHEN** user declines the spec update confirmation
|
||||
- **THEN** skip the spec update operations
|
||||
- **AND** display message: "Skipping spec updates. Proceeding with archive."
|
||||
- **AND** continue with the archive operation
|
||||
- **AND** display success message indicating specs were not updated
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Requirement: Error Conditions
|
||||
|
||||
The command SHALL handle various error conditions gracefully.
|
||||
|
||||
#### Scenario: Handling errors
|
||||
|
||||
- **WHEN** errors occur
|
||||
- **THEN** handle the following conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**Non-blocking confirmation**: Declining spec updates doesn't cancel archiving - users can review specs and choose to update them separately if needed
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
**--skip-specs flag**: Enables archiving of changes that don't modify specs (like infrastructure, tooling, or documentation changes) without unnecessary spec update prompts or operations
|
||||
@@ -0,0 +1,21 @@
|
||||
## 1. Update Archive Command Implementation
|
||||
- [ ] 1.1 Add `skipSpecs` option to the archive command options interface
|
||||
- [ ] 1.2 Modify the execute method to skip spec operations when flag is set
|
||||
- [ ] 1.3 Fix confirmation behavior: when user declines spec updates, proceed with archiving instead of cancelling
|
||||
- [ ] 1.4 Update console output to indicate when specs are being skipped (via flag or user choice)
|
||||
- [ ] 1.5 Ensure archive continues after declining spec updates
|
||||
|
||||
## 2. Update CLI Interface
|
||||
- [ ] 2.1 Add `--skip-specs` flag to the archive command definition
|
||||
- [ ] 2.2 Pass the flag value to the archive command execute method
|
||||
|
||||
## 3. Update Tests
|
||||
- [ ] 3.1 Add test case for archiving with --skip-specs flag
|
||||
- [ ] 3.2 Add test case for declining spec updates but continuing with archive
|
||||
- [ ] 3.3 Verify that spec updates are skipped when flag is used
|
||||
- [ ] 3.4 Verify that archive proceeds when user declines spec updates
|
||||
- [ ] 3.5 Ensure existing behavior remains unchanged when flag is not used
|
||||
|
||||
## 4. Update Documentation
|
||||
- [ ] 4.1 Update the cli-archive spec to document the new --skip-specs flag
|
||||
- [ ] 4.2 Document the new behavior when declining spec updates interactively
|
||||
@@ -0,0 +1,26 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [x] 1.1 Create `src/core/list.ts` with list logic
|
||||
- [x] 1.1.1 Implement directory scanning (exclude archive/)
|
||||
- [x] 1.1.2 Implement task counting from tasks.md files
|
||||
- [x] 1.1.3 Format output as simple table
|
||||
- [x] 1.2 Add list command to CLI in `src/cli/index.ts`
|
||||
- [x] 1.2.1 Register `openspec list` command
|
||||
- [x] 1.2.2 Connect to list.ts implementation
|
||||
|
||||
## 2. Error Handling
|
||||
- [x] 2.1 Handle missing openspec/changes/ directory
|
||||
- [x] 2.2 Handle changes without tasks.md files
|
||||
- [x] 2.3 Handle empty changes directory
|
||||
|
||||
## 3. Testing
|
||||
- [x] 3.1 Add tests for list functionality
|
||||
- [x] 3.1.1 Test with multiple changes
|
||||
- [x] 3.1.2 Test with completed changes
|
||||
- [x] 3.1.3 Test with no changes
|
||||
- [x] 3.1.4 Test error conditions
|
||||
|
||||
## 4. Documentation
|
||||
- [x] 4.1 Update CLI help text with list command
|
||||
- [x] 4.2 Add list command to README if applicable
|
||||
@@ -0,0 +1,15 @@
|
||||
## Why
|
||||
Need a command to archive completed changes to the archive folder with proper date prefixing, following OpenSpec conventions. Currently changes must be manually moved and renamed.
|
||||
|
||||
## What Changes
|
||||
- Add new `archive` command to CLI that moves changes to `changes/archive/YYYY-MM-DD-[change-name]/`
|
||||
- Check for incomplete tasks before archiving and warn user
|
||||
- Allow interactive selection of change to archive
|
||||
- Prevent archiving if target directory already exists
|
||||
- Update main specs from the change's future state specs (copy from `changes/[name]/specs/` to `openspec/specs/`)
|
||||
- Show confirmation prompt before updating specs, displaying which specs will be created/updated
|
||||
- Support `--yes` flag to skip confirmations for automation
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-archive (new)
|
||||
- Affected code: src/cli/index.ts, src/core/archive.ts (new)
|
||||
@@ -0,0 +1,111 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Change Selection
|
||||
WHEN no change-name is provided
|
||||
THEN display interactive list of available changes (excluding archive/)
|
||||
AND allow user to select one
|
||||
|
||||
WHEN change-name is provided
|
||||
THEN use that change directly
|
||||
AND validate it exists
|
||||
|
||||
### Task Completion Check
|
||||
The command SHALL scan the change's tasks.md file for incomplete tasks (marked with `- [ ]`)
|
||||
|
||||
WHEN incomplete tasks are found
|
||||
THEN display all incomplete tasks to the user
|
||||
AND prompt for confirmation to continue
|
||||
AND default to "No" for safety
|
||||
|
||||
WHEN all tasks are complete OR no tasks.md exists
|
||||
THEN proceed with archiving without prompting
|
||||
|
||||
### Archive Process
|
||||
The archive operation SHALL:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
WHEN target archive already exists
|
||||
THEN fail with error message
|
||||
AND do not overwrite existing archive
|
||||
|
||||
WHEN move succeeds
|
||||
THEN display success message with archived name and list of updated specs
|
||||
|
||||
### Spec Update Process
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality:
|
||||
|
||||
WHEN the change contains specs in `changes/[name]/specs/`
|
||||
THEN:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
WHEN no specs exist in the change
|
||||
THEN skip the spec update step
|
||||
AND proceed with archiving
|
||||
|
||||
### Confirmation Behavior
|
||||
The spec update confirmation SHALL:
|
||||
- Display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- Format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
- Default to "No" for safety (require explicit "y" or "yes")
|
||||
- Skip confirmation when `--yes` or `-y` flag is provided
|
||||
|
||||
WHEN user declines the confirmation
|
||||
THEN abort the entire archive operation
|
||||
AND display message: "Archive cancelled. No changes were made."
|
||||
AND exit with non-zero status code
|
||||
|
||||
## Error Handling
|
||||
|
||||
SHALL handle the following error conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
@@ -0,0 +1,44 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [ ] 1.1 Create `src/core/archive.ts` with ArchiveCommand class
|
||||
- [ ] 1.1.1 Implement change selection (interactive if not provided)
|
||||
- [ ] 1.1.2 Implement incomplete task checking from tasks.md
|
||||
- [ ] 1.1.3 Implement confirmation prompt for incomplete tasks
|
||||
- [ ] 1.1.4 Implement spec update functionality
|
||||
- [ ] 1.1.4.1 Detect specs in change directory
|
||||
- [ ] 1.1.4.2 Compare with existing main specs
|
||||
- [ ] 1.1.4.3 Display summary of new vs updated specs
|
||||
- [ ] 1.1.4.4 Show confirmation prompt for spec updates
|
||||
- [ ] 1.1.4.5 Copy specs to main spec directory
|
||||
- [ ] 1.1.5 Implement archive move with date prefixing
|
||||
- [ ] 1.1.6 Support --yes flag to skip confirmations
|
||||
|
||||
## 2. CLI Integration
|
||||
- [ ] 2.1 Add archive command to `src/cli/index.ts`
|
||||
- [ ] 2.1.1 Import ArchiveCommand
|
||||
- [ ] 2.1.2 Register command with commander
|
||||
- [ ] 2.1.3 Add --yes/-y flag option
|
||||
- [ ] 2.1.4 Add proper error handling
|
||||
|
||||
## 3. Error Handling
|
||||
- [ ] 3.1 Handle missing openspec/changes/ directory
|
||||
- [ ] 3.2 Handle change not found
|
||||
- [ ] 3.3 Handle archive target already exists
|
||||
- [ ] 3.4 Handle user cancellation
|
||||
|
||||
## 4. Testing
|
||||
- [ ] 4.1 Test with fully completed change
|
||||
- [ ] 4.2 Test with incomplete tasks (warning shown)
|
||||
- [ ] 4.3 Test interactive selection mode
|
||||
- [ ] 4.4 Test duplicate archive prevention
|
||||
- [ ] 4.5 Test spec update functionality
|
||||
- [ ] 4.5.1 Test creating new specs
|
||||
- [ ] 4.5.2 Test updating existing specs
|
||||
- [ ] 4.5.3 Test confirmation prompt display
|
||||
- [ ] 4.5.4 Test declining confirmation (no changes made)
|
||||
- [ ] 4.5.5 Test --yes flag skips confirmation
|
||||
|
||||
## 5. Build and Validation
|
||||
- [ ] 5.1 Ensure TypeScript compilation succeeds
|
||||
- [ ] 5.2 Test command execution
|
||||
@@ -0,0 +1,28 @@
|
||||
# Fix Update Command Tool Selection
|
||||
|
||||
## Problem
|
||||
|
||||
The `openspec update` command currently forces the creation/update of CLAUDE.md regardless of which AI tool was selected during initialization. This violates the tool-agnostic design principle and creates confusion for users who selected different AI assistants.
|
||||
|
||||
Additionally, different team members may use different AI tools, so we cannot rely on a shared configuration file.
|
||||
|
||||
## Solution
|
||||
|
||||
Modify the update command to:
|
||||
1. Only update AI tool configuration files that already exist
|
||||
2. Never create new AI tool configuration files
|
||||
3. Always update the core OpenSpec files (README.md, etc.)
|
||||
|
||||
## Implementation
|
||||
|
||||
- Remove hardcoded CLAUDE.md update from update command
|
||||
- Implement file existence check before updating any AI tool config
|
||||
- Update each existing AI tool config file with its appropriate markers
|
||||
- No configuration file needed (avoids team conflicts)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Update command only modifies existing AI tool configuration files
|
||||
- No new AI tool files created during update
|
||||
- Team members can use different AI tools without conflicts
|
||||
- Existing projects continue to work (backward compatibility)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Update Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### Requirement: Update Behavior
|
||||
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates.
|
||||
|
||||
#### Scenario: Running update command
|
||||
|
||||
- **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)
|
||||
- For each supported AI tool configuration file:
|
||||
- Check if the file exists (e.g., CLAUDE.md, COPILOT.md)
|
||||
- If it exists, update it using appropriate markers
|
||||
- If it doesn't exist, skip it (do NOT create)
|
||||
- Preserve user content outside markers
|
||||
- Display ASCII-safe success message: "Updated OpenSpec instructions"
|
||||
|
||||
### Requirement: Prerequisites
|
||||
|
||||
The command SHALL require an existing OpenSpec structure before allowing updates.
|
||||
|
||||
#### Scenario: Checking prerequisites
|
||||
|
||||
- **GIVEN** the command requires an existing `openspec` directory (created by `openspec init`)
|
||||
- **WHEN** the `openspec` directory does not exist
|
||||
- **THEN** display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: File Handling
|
||||
|
||||
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
|
||||
- **AND** update only the AI tool configuration files that already exist
|
||||
- **AND** use the default directory name `openspec`
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
|
||||
The update command SHALL work for any team member regardless of their AI tool choice.
|
||||
|
||||
#### Scenario: Team member using Claude
|
||||
|
||||
- **GIVEN** a team member has CLAUDE.md in their project
|
||||
- **WHEN** running `openspec update`
|
||||
- **THEN** update the CLAUDE.md file with the latest template
|
||||
- **AND** preserve user content outside OpenSpec markers
|
||||
- **AND** NOT create files for other tools
|
||||
|
||||
#### Scenario: Team member using different tool
|
||||
|
||||
- **GIVEN** a team member has COPILOT.md but no CLAUDE.md
|
||||
- **WHEN** running `openspec update`
|
||||
- **THEN** update the COPILOT.md file if implementation exists
|
||||
- **AND** NOT create CLAUDE.md
|
||||
- **AND** preserve user content outside OpenSpec markers
|
||||
|
||||
#### Scenario: Mixed team environment
|
||||
|
||||
- **GIVEN** a repository with both CLAUDE.md and COPILOT.md (different team members)
|
||||
- **WHEN** any team member runs `openspec update`
|
||||
- **THEN** update all existing AI tool configuration files
|
||||
- **AND** NOT create new AI tool configuration files
|
||||
- **AND** each team member's preferred tool remains configured
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL handle edge cases gracefully.
|
||||
|
||||
#### Scenario: File permission errors
|
||||
|
||||
- **WHEN** file write fails
|
||||
- **THEN** let the error bubble up naturally with file path
|
||||
|
||||
#### Scenario: No AI tool files exist
|
||||
|
||||
- **GIVEN** no AI tool configuration files exist
|
||||
- **WHEN** running update
|
||||
- **THEN** only update openspec/README.md
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Custom directory names
|
||||
|
||||
- **WHEN** considering custom directory names
|
||||
- **THEN** not supported in this change
|
||||
- **AND** the default directory name `openspec` SHALL be used
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Users SHALL be able to:
|
||||
- Update OpenSpec instructions with a single command
|
||||
- Get the latest AI agent instructions for their existing tools
|
||||
- Work in teams where members use different AI tools
|
||||
- NOT have unwanted AI tool configuration files created
|
||||
|
||||
The update process SHALL be:
|
||||
- Simple and fast (no version checking)
|
||||
- Predictable (same result every time)
|
||||
- Self-contained (no network required)
|
||||
- Team-friendly (respects individual tool choices)
|
||||
@@ -0,0 +1,21 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Update Command
|
||||
- [x] Remove hardcoded CLAUDE.md update from `src/core/update.ts`
|
||||
- [x] Add logic to check for existing AI tool configuration files
|
||||
- [x] Update only existing files using their appropriate configurators
|
||||
- [x] Iterate through all registered configurators to check for existing files
|
||||
|
||||
## 2. Update Configurator Registry
|
||||
- [x] Add method to get all configurators for update command
|
||||
- [x] Ensure each configurator can check if its file exists
|
||||
|
||||
## 3. Add Tests
|
||||
- [x] Test update command with only CLAUDE.md present
|
||||
- [x] Test update command with no AI tool files present
|
||||
- [x] Test update command with multiple AI tool files present
|
||||
- [x] Test that update never creates new AI tool files
|
||||
|
||||
## 4. Update Documentation
|
||||
- [x] Update README to clarify team-friendly behavior
|
||||
- [x] Document that update only modifies existing files
|
||||
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
OpenSpec specifications lack a consistent structure that makes sections visually identifiable and programmatically parseable across different specs. This makes it harder to maintain consistency and build tooling.
|
||||
|
||||
## What Changes
|
||||
|
||||
**Specification Format Section**
|
||||
- From: No formal structure requirements for specifications
|
||||
- To: Structured format with `### Requirement:` and `#### Scenario:` headers
|
||||
- Reason: Visual consistency and parseability across all specs
|
||||
- Impact: Non-breaking - existing specs can migrate gradually
|
||||
|
||||
**Keyword Formatting**
|
||||
- From: Inconsistent use of WHEN/THEN/AND keywords
|
||||
- To: Bold keywords (**WHEN**, **THEN**, **AND**) in scenario bullets
|
||||
- Reason: Improved readability and consistent visual hierarchy
|
||||
- Impact: Non-breaking - formatting enhancement only
|
||||
|
||||
**Format Flexibility**
|
||||
- From: Implicit understanding that different content needs different formats
|
||||
- To: Explicit allowance for alternative formats (OpenAPI, JSON Schema, etc.)
|
||||
- Reason: Address concern that not all specs fit requirement/scenario pattern
|
||||
- Impact: Non-breaking - clarifies existing practice
|
||||
|
||||
**Migration Guidelines**
|
||||
- From: No migration guidance
|
||||
- To: Documented gradual migration approach
|
||||
- Reason: Allows incremental adoption without disrupting existing specs
|
||||
- Impact: Non-breaking - opt-in migration as specs are modified
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: openspec-conventions (enhancement to existing capability)
|
||||
- Affected code: None initially - this is a documentation standard enhancement
|
||||
- Migration: Gradual - existing specs migrate as they're modified
|
||||
- Tooling: Enables future parsing tools but doesn't require them
|
||||
+61
-100
@@ -36,6 +36,60 @@ openspec/
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Specification Format
|
||||
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
#### Scenario: Writing requirement sections
|
||||
|
||||
- **WHEN** documenting a requirement in a behavioral specification
|
||||
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
|
||||
- **AND** immediately follow with a SHALL statement describing core behavior
|
||||
- **AND** keep requirement names descriptive and under 50 characters
|
||||
|
||||
#### Scenario: Documenting scenarios
|
||||
|
||||
- **WHEN** documenting specific behaviors or use cases
|
||||
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
|
||||
- **AND** use bullet points with bold keywords for steps:
|
||||
- **GIVEN** for initial state (optional)
|
||||
- **WHEN** for conditions or triggers
|
||||
- **THEN** for expected outcomes
|
||||
- **AND** for additional outcomes or conditions
|
||||
|
||||
#### Scenario: Adding implementation details
|
||||
|
||||
- **WHEN** a step requires additional detail
|
||||
- **THEN** use sub-bullets under the main step
|
||||
- **AND** maintain consistent indentation
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
### Requirement: Format Flexibility
|
||||
|
||||
The structured format SHALL be the default for behavioral specifications, but alternative formats MAY be used when more appropriate for the content type.
|
||||
|
||||
#### Scenario: Documenting API specifications
|
||||
|
||||
- **WHEN** documenting REST API endpoints or GraphQL schemas
|
||||
- **THEN** OpenAPI, GraphQL SDL, or similar formats MAY be used
|
||||
- **AND** the spec SHALL clearly indicate the format being used
|
||||
- **AND** behavioral aspects SHALL still follow the structured format
|
||||
|
||||
#### Scenario: Documenting data schemas
|
||||
|
||||
- **WHEN** documenting data structures, database schemas, or configurations
|
||||
- **THEN** JSON Schema, SQL DDL, or similar formats MAY be used
|
||||
- **AND** include the structured format for behavioral rules and constraints
|
||||
|
||||
#### Scenario: Using simplified format
|
||||
|
||||
- **WHEN** documenting simple capabilities without complex scenarios
|
||||
- **THEN** a simplified WHEN/THEN format without full structure MAY be used
|
||||
- **AND** this should be consistent within the capability
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Future State Storage
|
||||
@@ -110,105 +164,6 @@ A proposal is NOT required for:
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Requirement Markers
|
||||
|
||||
### Marker Syntax
|
||||
|
||||
@requirement marker-syntax
|
||||
WHEN writing a requirement in a spec
|
||||
THEN prefix it with @requirement followed by a brief kebab-case identifier
|
||||
AND place the marker on the line immediately before the WHEN statement
|
||||
|
||||
@requirement marker-identifier
|
||||
WHEN choosing an identifier for @requirement
|
||||
THEN use kebab-case (lowercase with hyphens)
|
||||
AND keep it brief but descriptive (2-4 words)
|
||||
AND ensure it's unique within the spec
|
||||
|
||||
@requirement marker-placement
|
||||
WHEN adding @requirement markers to a spec
|
||||
THEN place them in the ## Behavior or ## Behaviors section
|
||||
AND ensure each WHEN/THEN block has exactly one marker
|
||||
AND maintain a blank line after each THEN block for readability
|
||||
|
||||
### Examples
|
||||
|
||||
@requirement valid-marker-example
|
||||
WHEN a spec includes properly formatted markers
|
||||
THEN tools can extract and identify requirements programmatically
|
||||
AND the spec remains human-readable
|
||||
|
||||
Example of correct usage:
|
||||
```markdown
|
||||
## Behavior
|
||||
|
||||
@requirement user-register
|
||||
WHEN user registers with valid email
|
||||
THEN create account and send confirmation
|
||||
|
||||
@requirement user-login
|
||||
WHEN user logs in with correct credentials
|
||||
THEN return JWT token with user data
|
||||
|
||||
@requirement invalid-credentials
|
||||
WHEN user provides invalid credentials
|
||||
THEN return 401 unauthorized error
|
||||
```
|
||||
|
||||
@requirement invalid-marker-detection
|
||||
WHEN a requirement lacks an @requirement marker
|
||||
THEN tools should gracefully skip it
|
||||
AND optionally warn about unmarked requirements
|
||||
|
||||
### Edge Cases
|
||||
|
||||
@requirement multiline-when-then
|
||||
WHEN a WHEN or THEN clause spans multiple lines
|
||||
THEN the @requirement marker still goes on the line before WHEN
|
||||
AND the entire block is considered part of that requirement
|
||||
|
||||
@requirement multiple-then-clauses
|
||||
WHEN a requirement has multiple THEN clauses using AND
|
||||
THEN treat them as part of the same requirement
|
||||
AND use a single @requirement marker for the entire block
|
||||
|
||||
@requirement nested-conditions
|
||||
WHEN requirements have nested conditions or complex logic
|
||||
THEN keep the @requirement marker simple
|
||||
AND let the WHEN/THEN content contain the complexity
|
||||
|
||||
## Spec Structure
|
||||
|
||||
@requirement spec-file-location
|
||||
WHEN creating a spec file
|
||||
THEN place it in openspec/specs/[capability-name]/spec.md
|
||||
AND use kebab-case for the capability name
|
||||
|
||||
@requirement spec-sections
|
||||
WHEN structuring a spec
|
||||
THEN include these sections in order:
|
||||
- # [Capability Name] Specification
|
||||
- ## Purpose (brief description)
|
||||
- ## Behavior or ## Behaviors (with @requirement markers)
|
||||
- ## Examples (optional, for complex requirements)
|
||||
|
||||
## Benefits of Requirement Markers
|
||||
|
||||
@requirement tooling-extraction
|
||||
WHEN tools need to extract requirements from specs
|
||||
THEN they can parse @requirement markers reliably
|
||||
AND avoid complex regex patterns for WHEN/THEN extraction
|
||||
|
||||
@requirement requirement-counting
|
||||
WHEN displaying change summaries
|
||||
THEN tools can count requirements by counting @requirement markers
|
||||
AND show accurate requirement counts per spec
|
||||
|
||||
@requirement requirement-referencing
|
||||
WHEN documenting or discussing specific requirements
|
||||
THEN use the @requirement identifier for clear reference
|
||||
AND maintain consistency across documentation
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
@@ -216,4 +171,10 @@ Clean future state storage provides:
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
|
||||
The structured format adds:
|
||||
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
|
||||
- **Parseability**: Consistent structure enables tooling and automation
|
||||
- **Flexibility**: Alternative formats supported where appropriate
|
||||
- **Gradual Adoption**: Existing specs can migrate incrementally
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1. Update OpenSpec Conventions Spec
|
||||
|
||||
- [x] 1.1 Add "Specification Format" section to openspec-conventions
|
||||
- [x] 1.2 Document structured format with Requirement/Scenario headers
|
||||
- [x] 1.3 Define bold keyword usage (WHEN/THEN/AND) for scenarios
|
||||
- [x] 1.4 Include examples demonstrating the format within the spec itself
|
||||
|
||||
## 2. Update Documentation
|
||||
|
||||
- [x] 2.1 Update the "Why This Approach" section with structured format benefits
|
||||
- [x] 2.2 Ensure spec follows its own format as a demonstration
|
||||
|
||||
## 3. Update Existing Specs
|
||||
|
||||
- [x] 3.1 Update cli-init spec to use structured format in Behavior section
|
||||
- [x] 3.2 Update cli-list spec to use structured format in Behavior section
|
||||
- [x] 3.3 Update cli-update spec to use structured format in Behavior section
|
||||
- [x] 3.4 Update cli-diff spec to use structured format in Behavior section
|
||||
- [x] 3.5 Update cli-archive spec to use structured format in Behavior section
|
||||
@@ -0,0 +1,155 @@
|
||||
# CLI Archive Command Specification
|
||||
|
||||
## Purpose
|
||||
The archive command moves completed changes from the active changes directory to the archive folder with date-based naming, following OpenSpec conventions.
|
||||
|
||||
## Command Syntax
|
||||
```bash
|
||||
openspec archive [change-name] [--yes|-y]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Requirement: Change Selection
|
||||
|
||||
The command SHALL support both interactive and direct change selection methods.
|
||||
|
||||
#### Scenario: Interactive selection
|
||||
|
||||
- **WHEN** no change-name is provided
|
||||
- **THEN** display interactive list of available changes (excluding archive/)
|
||||
- **AND** allow user to select one
|
||||
|
||||
#### Scenario: Direct selection
|
||||
|
||||
- **WHEN** change-name is provided
|
||||
- **THEN** use that change directly
|
||||
- **AND** validate it exists
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The command SHALL verify task completion status before archiving to prevent premature archival.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display all incomplete tasks to the user
|
||||
- **AND** prompt for confirmation to continue
|
||||
- **AND** default to "No" for safety
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** all tasks are complete OR no tasks.md exists
|
||||
- **THEN** proceed with archiving without prompting
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The archive operation SHALL follow a structured process to safely move changes to the archive.
|
||||
|
||||
#### Scenario: Performing archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** execute these steps:
|
||||
1. Create archive/ directory if it doesn't exist
|
||||
2. Generate target name as `YYYY-MM-DD-[change-name]` using current date
|
||||
3. Check if target directory already exists
|
||||
4. Update main specs from the change's future state specs (see Spec Update Process below)
|
||||
5. Move the entire change directory to the archive location
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** do not overwrite existing archive
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** move succeeds
|
||||
- **THEN** display success message with archived name and list of updated specs
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality.
|
||||
|
||||
#### Scenario: Updating specs from change
|
||||
|
||||
- **WHEN** the change contains specs in `changes/[name]/specs/`
|
||||
- **THEN** execute these steps:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs exist in the change
|
||||
- **THEN** skip the spec update step
|
||||
- **AND** proceed with archiving
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
|
||||
|
||||
#### Scenario: Displaying confirmation
|
||||
|
||||
- **WHEN** prompting for confirmation
|
||||
- **THEN** display a clear summary showing:
|
||||
- Which specs will be created (new capabilities)
|
||||
- Which specs will be updated (existing capabilities)
|
||||
- The source path for each spec
|
||||
- **AND** format the confirmation prompt as:
|
||||
```
|
||||
The following specs will be updated:
|
||||
|
||||
NEW specs to be created:
|
||||
- cli-archive (from changes/add-archive-command/specs/cli-archive/spec.md)
|
||||
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
```
|
||||
#### Scenario: Handling confirmation response
|
||||
|
||||
- **WHEN** waiting for user confirmation
|
||||
- **THEN** default to "No" for safety (require explicit "y" or "yes")
|
||||
- **AND** skip confirmation when `--yes` or `-y` flag is provided
|
||||
|
||||
#### Scenario: User declines confirmation
|
||||
|
||||
- **WHEN** user declines the confirmation
|
||||
- **THEN** abort the entire archive operation
|
||||
- **AND** display message: "Archive cancelled. No changes were made."
|
||||
- **AND** exit with non-zero status code
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Requirement: Error Conditions
|
||||
|
||||
The command SHALL handle various error conditions gracefully.
|
||||
|
||||
#### Scenario: Handling errors
|
||||
|
||||
- **WHEN** errors occur
|
||||
- **THEN** handle the following conditions:
|
||||
- Missing openspec/changes/ directory
|
||||
- Change not found
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
**Task checking**: Prevents accidental archiving of incomplete work
|
||||
**Date prefixing**: Maintains chronological order and prevents naming conflicts
|
||||
**No overwrite**: Preserves historical archives and prevents data loss
|
||||
**Spec updates before archiving**: Specs in the main directory represent current reality; when a change is deployed and archived, its future state specs become the new reality and must replace the main specs
|
||||
**Confirmation for spec updates**: Provides visibility into what will change, prevents accidental overwrites, and ensures users understand the impact before specs are modified
|
||||
**--yes flag for automation**: Allows CI/CD pipelines to archive without interactive prompts while maintaining safety by default for manual use
|
||||
@@ -0,0 +1,120 @@
|
||||
# CLI Diff Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec diff` command provides developers with a visual comparison between proposed spec changes and the current deployed specs.
|
||||
|
||||
## Command Syntax
|
||||
|
||||
```bash
|
||||
openspec diff [change-name]
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### Requirement: Without Arguments
|
||||
|
||||
The command SHALL provide an interactive selection when no change is specified.
|
||||
|
||||
#### Scenario: Running without arguments
|
||||
|
||||
- **WHEN** running `openspec diff` without arguments
|
||||
- **THEN** list all available changes in the `changes/` directory (excluding archive)
|
||||
- **AND** prompt user to select a change
|
||||
|
||||
### Requirement: With Change Name
|
||||
|
||||
The command SHALL compare specs when a specific change is provided.
|
||||
|
||||
#### Scenario: Running with change name
|
||||
|
||||
- **WHEN** running `openspec diff <change-name>`
|
||||
- **THEN** compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
|
||||
|
||||
### Requirement: Diff Output
|
||||
|
||||
The command SHALL generate appropriate diff output for all spec changes.
|
||||
|
||||
#### Scenario: Comparing existing files
|
||||
|
||||
- **WHEN** file exists in both locations
|
||||
- **THEN** show unified diff
|
||||
|
||||
#### Scenario: New files
|
||||
|
||||
- **WHEN** file only exists in change
|
||||
- **THEN** show as new file (all lines with +)
|
||||
|
||||
#### Scenario: Deleted files
|
||||
|
||||
- **WHEN** file only exists in current specs
|
||||
- **THEN** show as deleted (all lines with -)
|
||||
|
||||
### Requirement: Display Format
|
||||
|
||||
The command SHALL use standard unified diff format for consistency with existing tools.
|
||||
|
||||
#### Scenario: Formatting diff output
|
||||
|
||||
- **WHEN** displaying diff output
|
||||
- **THEN** use standard unified diff format:
|
||||
- Lines prefixed with `-` for removed content
|
||||
- Lines prefixed with `+` for added content
|
||||
- Lines without prefix for unchanged context
|
||||
- File headers showing the paths being compared
|
||||
|
||||
### Requirement: Color Support
|
||||
|
||||
The command SHALL enhance readability with colors when supported.
|
||||
|
||||
#### Scenario: Terminal with color support
|
||||
|
||||
- **WHEN** terminal supports colors
|
||||
- **THEN** display:
|
||||
- Removed lines in red
|
||||
- Added lines in green
|
||||
- File headers in bold
|
||||
- Context lines in default color
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL provide clear error messages for various failure conditions.
|
||||
|
||||
#### Scenario: Change not found
|
||||
|
||||
- **WHEN** specified change doesn't exist
|
||||
- **THEN** display error "Change '<name>' not found"
|
||||
|
||||
#### Scenario: No specs in change
|
||||
|
||||
- **WHEN** no specs directory in change
|
||||
- **THEN** display "No spec changes found for '<name>'"
|
||||
|
||||
#### Scenario: Missing changes directory
|
||||
|
||||
- **WHEN** changes directory doesn't exist
|
||||
- **THEN** display "No OpenSpec changes directory found"
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# View diff for specific change
|
||||
$ openspec diff add-auth-feature
|
||||
|
||||
--- specs/user-auth/spec.md
|
||||
+++ changes/add-auth-feature/specs/user-auth/spec.md
|
||||
@@ -10,6 +10,8 @@
|
||||
Users SHALL authenticate with email and password.
|
||||
|
||||
+Users MAY authenticate with OAuth providers.
|
||||
+
|
||||
WHEN credentials are valid THEN issue JWT token.
|
||||
|
||||
# List all changes and select
|
||||
$ openspec diff
|
||||
Available changes:
|
||||
1. add-auth-feature
|
||||
2. update-payment-flow
|
||||
3. add-status-command
|
||||
Select a change (1-3):
|
||||
```
|
||||
+110
-62
@@ -6,20 +6,28 @@ The `openspec init` command SHALL create a complete OpenSpec directory structure
|
||||
|
||||
## Behavior
|
||||
|
||||
### Progress Indicators
|
||||
### Requirement: Progress Indicators
|
||||
|
||||
WHEN executing initialization steps
|
||||
THEN validate environment silently in background (no output unless error)
|
||||
AND display progress with ora spinners:
|
||||
- Show spinner: "⠋ Creating OpenSpec structure..."
|
||||
- Then success: "✔ OpenSpec structure created"
|
||||
- Show spinner: "⠋ Configuring AI tools..."
|
||||
- Then success: "✔ AI tools configured"
|
||||
The command SHALL display progress indicators during initialization to provide clear feedback about each step.
|
||||
|
||||
### Directory Creation
|
||||
#### Scenario: Displaying initialization progress
|
||||
|
||||
WHEN `openspec init` is executed
|
||||
THEN create the following directory structure:
|
||||
- **WHEN** executing initialization steps
|
||||
- **THEN** validate environment silently in background (no output unless error)
|
||||
- **AND** display progress with ora spinners:
|
||||
- Show spinner: "⠋ Creating OpenSpec structure..."
|
||||
- Then success: "✔ OpenSpec structure created"
|
||||
- Show spinner: "⠋ Configuring AI tools..."
|
||||
- Then success: "✔ AI tools configured"
|
||||
|
||||
### Requirement: Directory Creation
|
||||
|
||||
The command SHALL create the complete OpenSpec directory structure with all required directories and files.
|
||||
|
||||
#### Scenario: Creating OpenSpec structure
|
||||
|
||||
- **WHEN** `openspec init` is executed
|
||||
- **THEN** create the following directory structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md
|
||||
@@ -29,27 +37,41 @@ openspec/
|
||||
└── archive/
|
||||
```
|
||||
|
||||
### File Generation
|
||||
### Requirement: File Generation
|
||||
|
||||
The command SHALL generate:
|
||||
- `README.md` containing complete OpenSpec instructions for AI assistants
|
||||
- `project.md` with project context template
|
||||
The command SHALL generate required template files with appropriate content for immediate use.
|
||||
|
||||
### AI Tool Configuration
|
||||
#### Scenario: Generating template files
|
||||
|
||||
WHEN run interactively
|
||||
THEN prompt user to select AI tools to configure:
|
||||
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
|
||||
- Cursor (future)
|
||||
- Aider (future)
|
||||
- **WHEN** initializing OpenSpec
|
||||
- **THEN** generate `README.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **AND** generate `project.md` with project context template
|
||||
|
||||
### AI Tool Configuration Details
|
||||
### Requirement: AI Tool Configuration
|
||||
|
||||
WHEN Claude Code is selected
|
||||
THEN create or update `CLAUDE.md` in the project root directory (not inside openspec/)
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions based on user selection.
|
||||
|
||||
WHEN CLAUDE.md does not exist
|
||||
THEN create new file with OpenSpec content wrapped in markers:
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt user to select AI tools to configure:
|
||||
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
|
||||
- Cursor (future)
|
||||
- Aider (future)
|
||||
|
||||
### Requirement: AI Tool Configuration Details
|
||||
|
||||
The command SHALL properly configure selected AI tools with OpenSpec-specific instructions using a marker system.
|
||||
|
||||
#### Scenario: Configuring Claude Code
|
||||
|
||||
- **WHEN** Claude Code is selected
|
||||
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
|
||||
|
||||
#### Scenario: Creating new CLAUDE.md
|
||||
|
||||
- **WHEN** CLAUDE.md does not exist
|
||||
- **THEN** create new file with OpenSpec content wrapped in markers:
|
||||
```markdown
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Project
|
||||
@@ -62,51 +84,71 @@ See @openspec/README.md for detailed conventions and guidelines.
|
||||
<!-- OPENSPEC:END -->
|
||||
```
|
||||
|
||||
WHEN CLAUDE.md already exists
|
||||
THEN preserve all existing content
|
||||
AND insert OpenSpec content at the beginning of the file using markers
|
||||
AND ensure markers don't duplicate if they already exist
|
||||
#### Scenario: Updating existing CLAUDE.md
|
||||
|
||||
The marker system SHALL:
|
||||
- Use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
|
||||
- Use `<!-- OPENSPEC:END -->` to mark the end of managed content
|
||||
- Allow OpenSpec to update its content without affecting user customizations
|
||||
- Preserve all content outside the markers intact
|
||||
- **WHEN** CLAUDE.md already exists
|
||||
- **THEN** preserve all existing content
|
||||
- **AND** insert OpenSpec content at the beginning of the file using markers
|
||||
- **AND** ensure markers don't duplicate if they already exist
|
||||
|
||||
#### Scenario: Managing content with markers
|
||||
|
||||
- **WHEN** using the marker system
|
||||
- **THEN** use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
|
||||
- **AND** use `<!-- OPENSPEC:END -->` to mark the end of managed content
|
||||
- **AND** allow OpenSpec to update its content without affecting user customizations
|
||||
- **AND** preserve all content outside the markers intact
|
||||
|
||||
WHY use markers:
|
||||
- Users may have existing CLAUDE.md instructions they want to keep
|
||||
- OpenSpec can update its instructions in future versions
|
||||
- Clear boundary between OpenSpec-managed and user-managed content
|
||||
|
||||
### Interactive Mode
|
||||
### Requirement: Interactive Mode
|
||||
|
||||
WHEN run
|
||||
THEN prompt user with: "Which AI tool do you use?"
|
||||
AND show single-select menu with available tools:
|
||||
- Claude Code
|
||||
AND show disabled options as "coming soon" (not selectable):
|
||||
- Cursor (coming soon)
|
||||
- Aider (coming soon)
|
||||
- Continue (coming soon)
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
|
||||
User navigation:
|
||||
- Use arrow keys to move between options
|
||||
- Press Enter to select the highlighted option
|
||||
#### Scenario: Displaying interactive menu
|
||||
|
||||
### Safety Checks
|
||||
- **WHEN** run
|
||||
- **THEN** prompt user with: "Which AI tool do you use?"
|
||||
- **AND** show single-select menu with available tools:
|
||||
- Claude Code
|
||||
- **AND** show disabled options as "coming soon" (not selectable):
|
||||
- Cursor (coming soon)
|
||||
- Aider (coming soon)
|
||||
- Continue (coming soon)
|
||||
|
||||
WHEN `openspec/` directory already exists
|
||||
THEN display error with ora fail indicator:
|
||||
"✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
|
||||
#### Scenario: Navigating the menu
|
||||
|
||||
WHEN checking initialization feasibility
|
||||
THEN verify write permissions in the target directory silently
|
||||
AND only display error if permissions are insufficient
|
||||
- **WHEN** user is in the menu
|
||||
- **THEN** allow arrow keys to move between options
|
||||
- **AND** allow Enter key to select the highlighted option
|
||||
|
||||
### Success Output
|
||||
### Requirement: Safety Checks
|
||||
|
||||
WHEN initialization completes successfully
|
||||
THEN display actionable prompts for AI-driven workflow:
|
||||
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
|
||||
|
||||
#### Scenario: Detecting existing initialization
|
||||
|
||||
- **WHEN** `openspec/` directory already exists
|
||||
- **THEN** display error with ora fail indicator:
|
||||
- "✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
|
||||
|
||||
#### Scenario: Checking write permissions
|
||||
|
||||
- **WHEN** checking initialization feasibility
|
||||
- **THEN** verify write permissions in the target directory silently
|
||||
- **AND** only display error if permissions are insufficient
|
||||
|
||||
### Requirement: Success Output
|
||||
|
||||
The command SHALL provide clear, actionable next steps upon successful initialization.
|
||||
|
||||
#### Scenario: Displaying success message
|
||||
|
||||
- **WHEN** initialization completes successfully
|
||||
- **THEN** display actionable prompts for AI-driven workflow:
|
||||
```
|
||||
✔ OpenSpec initialized successfully!
|
||||
|
||||
@@ -132,12 +174,18 @@ The prompts SHALL:
|
||||
- Guide users through the AI-driven workflow
|
||||
- Replace placeholder text ([YOUR FEATURE HERE]) with actual features
|
||||
|
||||
### Exit Codes
|
||||
### Requirement: Exit Codes
|
||||
|
||||
- 0: Success
|
||||
- 1: General error (including when OpenSpec directory already exists)
|
||||
- 2: Insufficient permissions (reserved for future use)
|
||||
- 3: User cancelled operation (reserved for future use)
|
||||
The command SHALL use consistent exit codes to indicate different failure modes.
|
||||
|
||||
#### Scenario: Returning exit codes
|
||||
|
||||
- **WHEN** the command completes
|
||||
- **THEN** return appropriate exit code:
|
||||
- 0: Success
|
||||
- 1: General error (including when OpenSpec directory already exists)
|
||||
- 2: Insufficient permissions (reserved for future use)
|
||||
- 3: User cancelled operation (reserved for future use)
|
||||
|
||||
## Why
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# List Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Requirement: Command Execution
|
||||
|
||||
The command SHALL scan and analyze all active changes to provide a comprehensive overview.
|
||||
|
||||
#### Scenario: Scanning for changes
|
||||
|
||||
- **WHEN** `openspec list` is executed
|
||||
- **THEN** scan the `openspec/changes/` directory for change directories
|
||||
- **AND** exclude the `archive/` subdirectory from results
|
||||
- **AND** parse each change's `tasks.md` file to count task completion
|
||||
|
||||
### Requirement: Task Counting
|
||||
|
||||
The command SHALL accurately count task completion status using standard markdown checkbox patterns.
|
||||
|
||||
#### Scenario: Counting tasks in tasks.md
|
||||
|
||||
- **WHEN** parsing a `tasks.md` file
|
||||
- **THEN** count tasks matching these patterns:
|
||||
- Completed: Lines containing `- [x]`
|
||||
- Incomplete: Lines containing `- [ ]`
|
||||
- **AND** calculate total tasks as the sum of completed and incomplete
|
||||
|
||||
### Requirement: Output Format
|
||||
|
||||
The command SHALL display changes in a clear, readable table format with progress indicators.
|
||||
|
||||
#### Scenario: Displaying change list
|
||||
|
||||
- **WHEN** displaying the list
|
||||
- **THEN** show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- **AND** use status indicators:
|
||||
- `✓` for fully completed changes (all tasks done)
|
||||
- Progress fraction for partial completion
|
||||
|
||||
Example output:
|
||||
```
|
||||
Changes:
|
||||
add-auth-feature 3/5 tasks
|
||||
update-api-docs ✓ Complete
|
||||
fix-validation 0/2 tasks
|
||||
add-list-command 1/4 tasks
|
||||
```
|
||||
|
||||
### Requirement: Empty State
|
||||
|
||||
The command SHALL provide clear feedback when no active changes are present.
|
||||
|
||||
#### Scenario: Handling empty state
|
||||
|
||||
- **WHEN** no active changes exist (only archive/ or empty changes/)
|
||||
- **THEN** display: "No active changes found."
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL gracefully handle missing files and directories with appropriate messages.
|
||||
|
||||
#### Scenario: Missing tasks.md file
|
||||
|
||||
- **WHEN** a change directory has no `tasks.md` file
|
||||
- **THEN** display the change with "No tasks" status
|
||||
|
||||
#### Scenario: Missing changes directory
|
||||
|
||||
- **WHEN** `openspec/changes/` directory doesn't exist
|
||||
- **THEN** display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: Sorting
|
||||
|
||||
The command SHALL maintain consistent ordering of changes for predictable output.
|
||||
|
||||
#### Scenario: Ordering changes
|
||||
|
||||
- **WHEN** displaying multiple changes
|
||||
- **THEN** sort them in alphabetical order by change name
|
||||
|
||||
## Why
|
||||
|
||||
Developers need a quick way to:
|
||||
- See what changes are in progress
|
||||
- Identify which changes are ready to archive
|
||||
- Understand the overall project evolution status
|
||||
- Get a bird's-eye view without opening multiple files
|
||||
|
||||
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
|
||||
@@ -6,45 +6,70 @@ As a developer using OpenSpec, I want to update the OpenSpec instructions in my
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### Update Behavior
|
||||
### Requirement: Update Behavior
|
||||
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates.
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
|
||||
|
||||
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)
|
||||
- Update the OpenSpec-managed block in `CLAUDE.md` using markers
|
||||
- Preserve user content outside markers
|
||||
- Create `CLAUDE.md` if missing
|
||||
- Display ASCII-safe success message: "Updated OpenSpec instructions"
|
||||
#### Scenario: Running update command
|
||||
|
||||
### Prerequisites
|
||||
- **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)
|
||||
- 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
|
||||
- Update only files that already exist using their markers
|
||||
- Preserve user content outside markers
|
||||
- **Never create new AI tool configuration files**
|
||||
- Display success message listing updated files
|
||||
|
||||
The command SHALL require:
|
||||
- An existing `openspec` directory (created by `openspec init`)
|
||||
### Requirement: Prerequisites
|
||||
|
||||
IF the `openspec` directory does not exist THEN:
|
||||
- Display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- Exit with code 1
|
||||
The command SHALL require an existing OpenSpec structure before allowing updates.
|
||||
|
||||
### File Handling
|
||||
#### Scenario: Checking prerequisites
|
||||
|
||||
The update command SHALL:
|
||||
- Completely replace `openspec/README.md` with the latest template
|
||||
- Update only the OpenSpec-managed block in `CLAUDE.md` using markers
|
||||
- Use the default directory name `openspec`
|
||||
- Be idempotent (repeated runs have no additional effect)
|
||||
- **GIVEN** the command requires an existing `openspec` directory (created by `openspec init`)
|
||||
- **WHEN** the `openspec` directory does not exist
|
||||
- **THEN** display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: File Handling
|
||||
|
||||
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
|
||||
- **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)
|
||||
- **AND** respect team members' AI tool choices by not creating unwanted files
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### File Permissions
|
||||
IF file write fails THEN let the error bubble up naturally with file path.
|
||||
### Requirement: Error Handling
|
||||
|
||||
### Missing CLAUDE.md
|
||||
IF CLAUDE.md doesn't exist THEN create it with the template content.
|
||||
The command SHALL handle edge cases gracefully.
|
||||
|
||||
### Custom Directory Name
|
||||
Not supported in this change. The default directory name `openspec` SHALL be used.
|
||||
#### Scenario: File permission errors
|
||||
|
||||
- **WHEN** file write fails
|
||||
- **THEN** let the error bubble up naturally with file path
|
||||
|
||||
#### Scenario: Missing AI tool files
|
||||
|
||||
- **WHEN** an AI tool configuration file doesn't exist
|
||||
- **THEN** skip updating that file
|
||||
- **AND** do not create it
|
||||
|
||||
#### Scenario: Custom directory names
|
||||
|
||||
- **WHEN** considering custom directory names
|
||||
- **THEN** not supported in this change
|
||||
- **AND** the default directory name `openspec` SHALL be used
|
||||
|
||||
## Success Criteria
|
||||
|
||||
|
||||
@@ -14,8 +14,14 @@ The system SHALL follow these principles:
|
||||
|
||||
## Directory Structure
|
||||
|
||||
WHEN an OpenSpec project is initialized
|
||||
THEN it SHALL have this structure:
|
||||
### Requirement: Project Structure
|
||||
|
||||
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
|
||||
|
||||
#### Scenario: Initializing project structure
|
||||
|
||||
- **WHEN** an OpenSpec project is initialized
|
||||
- **THEN** it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
@@ -36,23 +42,63 @@ openspec/
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Specification Format
|
||||
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
#### Scenario: Writing requirement sections
|
||||
|
||||
- **WHEN** documenting a requirement in a behavioral specification
|
||||
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
|
||||
- **AND** immediately follow with a SHALL statement describing core behavior
|
||||
- **AND** keep requirement names descriptive and under 50 characters
|
||||
|
||||
#### Scenario: Documenting scenarios
|
||||
|
||||
- **WHEN** documenting specific behaviors or use cases
|
||||
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
|
||||
- **AND** use bullet points with bold keywords for steps:
|
||||
- **GIVEN** for initial state (optional)
|
||||
- **WHEN** for conditions or triggers
|
||||
- **THEN** for expected outcomes
|
||||
- **AND** for additional outcomes or conditions
|
||||
|
||||
#### Scenario: Adding implementation details
|
||||
|
||||
- **WHEN** a step requires additional detail
|
||||
- **THEN** use sub-bullets under the main step
|
||||
- **AND** maintain consistent indentation
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Future State Storage
|
||||
### Requirement: Future State Storage
|
||||
|
||||
WHEN creating a change proposal
|
||||
THEN store the complete future state of affected specs
|
||||
AND use clean markdown without diff syntax
|
||||
Change proposals SHALL store complete future state specifications without diff syntax.
|
||||
|
||||
#### Scenario: Creating change proposals
|
||||
|
||||
- **WHEN** creating a change proposal
|
||||
- **THEN** store the complete future state of affected specs
|
||||
- **AND** use clean markdown without diff syntax
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Complete spec files as they will exist after the change
|
||||
- Clean markdown without `+` or `-` prefixes
|
||||
- All formatting and structure of the final intended state
|
||||
|
||||
### Proposal Format
|
||||
### Requirement: Proposal Format
|
||||
|
||||
WHEN documenting what changes
|
||||
THEN the proposal SHALL explicitly describe each change:
|
||||
Proposals SHALL explicitly document all changes with clear from/to comparisons.
|
||||
|
||||
#### Scenario: Documenting changes
|
||||
|
||||
- **WHEN** documenting what changes
|
||||
- **THEN** the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
@@ -78,8 +124,14 @@ The change process SHALL follow these states:
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
WHEN reviewing proposed changes
|
||||
THEN reviewers can compare using:
|
||||
### Requirement: Change Review
|
||||
|
||||
The system SHALL support multiple methods for reviewing proposed changes.
|
||||
|
||||
#### Scenario: Reviewing changes
|
||||
|
||||
- **WHEN** reviewing proposed changes
|
||||
- **THEN** reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
@@ -117,4 +169,9 @@ Clean future state storage provides:
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
|
||||
The structured format adds:
|
||||
- **Visual Consistency**: Requirement and Scenario prefixes make sections instantly recognizable
|
||||
- **Parseability**: Consistent structure enables tooling and automation
|
||||
- **Gradual Adoption**: Existing specs can migrate incrementally
|
||||
@@ -6,6 +6,7 @@ import { InitCommand } from '../core/init.js';
|
||||
import { UpdateCommand } from '../core/update.js';
|
||||
import { DiffCommand } from '../core/diff.js';
|
||||
import { ListCommand } from '../core/list.js';
|
||||
import { ArchiveCommand } from '../core/archive.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -90,4 +91,19 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('archive [change-name]')
|
||||
.description('Archive a completed change and update main specs')
|
||||
.option('-y, --yes', 'Skip confirmation prompts')
|
||||
.action(async (changeName?: string, options?: { yes?: boolean }) => {
|
||||
try {
|
||||
const archiveCommand = new ArchiveCommand();
|
||||
await archiveCommand.execute(changeName, options);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
@@ -0,0 +1,224 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { select, confirm } from '@inquirer/prompts';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
|
||||
interface SpecUpdate {
|
||||
source: string;
|
||||
target: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export class ArchiveCommand {
|
||||
async execute(changeName?: string, options: { yes?: boolean } = {}): Promise<void> {
|
||||
const targetPath = '.';
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
const archiveDir = path.join(changesDir, 'archive');
|
||||
const mainSpecsDir = path.join(targetPath, 'openspec', 'specs');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// Get change name interactively if not provided
|
||||
if (!changeName) {
|
||||
const selectedChange = await this.selectChange(changesDir);
|
||||
if (!selectedChange) {
|
||||
console.log('No change selected. Aborting.');
|
||||
return;
|
||||
}
|
||||
changeName = selectedChange;
|
||||
}
|
||||
|
||||
const changeDir = path.join(changesDir, changeName);
|
||||
|
||||
// Verify change exists
|
||||
try {
|
||||
const stat = await fs.stat(changeDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
|
||||
// Check for incomplete tasks
|
||||
const tasksPath = path.join(changeDir, 'tasks.md');
|
||||
const incompleteTasks = await this.checkIncompleteTasks(tasksPath);
|
||||
|
||||
if (incompleteTasks > 0) {
|
||||
if (!options.yes) {
|
||||
const proceed = await confirm({
|
||||
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
||||
default: false
|
||||
});
|
||||
if (!proceed) {
|
||||
console.log('Archive cancelled.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(`Warning: ${incompleteTasks} incomplete task(s) found. Continuing due to --yes flag.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Find specs to update
|
||||
const specUpdates = await this.findSpecUpdates(changeDir, mainSpecsDir);
|
||||
|
||||
if (specUpdates.length > 0) {
|
||||
console.log('\nSpecs to update:');
|
||||
for (const update of specUpdates) {
|
||||
const status = update.exists ? 'update' : 'create';
|
||||
const capability = path.basename(path.dirname(update.target));
|
||||
console.log(` ${capability}: ${status}`);
|
||||
}
|
||||
|
||||
if (!options.yes) {
|
||||
const proceed = await confirm({
|
||||
message: 'Proceed with spec updates?',
|
||||
default: true
|
||||
});
|
||||
if (!proceed) {
|
||||
console.log('Archive cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update specs
|
||||
for (const update of specUpdates) {
|
||||
await this.updateSpec(update);
|
||||
}
|
||||
console.log('Specs updated successfully.');
|
||||
}
|
||||
|
||||
// Create archive directory with date prefix
|
||||
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
||||
const archivePath = path.join(archiveDir, archiveName);
|
||||
|
||||
// Check if archive already exists
|
||||
try {
|
||||
await fs.access(archivePath);
|
||||
throw new Error(`Archive '${archiveName}' already exists.`);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create archive directory if needed
|
||||
await fs.mkdir(archiveDir, { recursive: true });
|
||||
|
||||
// Move change to archive
|
||||
await fs.rename(changeDir, archivePath);
|
||||
|
||||
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
||||
}
|
||||
|
||||
private async selectChange(changesDir: string): Promise<string | null> {
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Available changes:');
|
||||
const choices = changeDirs.map(name => ({
|
||||
name: name,
|
||||
value: name
|
||||
}));
|
||||
|
||||
try {
|
||||
const answer = await select({
|
||||
message: 'Select a change to archive',
|
||||
choices
|
||||
});
|
||||
return answer;
|
||||
} catch (error) {
|
||||
// User cancelled (Ctrl+C)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIncompleteTasks(tasksPath: string): Promise<number> {
|
||||
try {
|
||||
const content = await fs.readFile(tasksPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
let incompleteTasks = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('- [ ]')) {
|
||||
incompleteTasks++;
|
||||
}
|
||||
}
|
||||
|
||||
return incompleteTasks;
|
||||
} catch {
|
||||
// No tasks.md file or error reading it
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]> {
|
||||
const updates: SpecUpdate[] = [];
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
|
||||
// Check if target exists
|
||||
let exists = false;
|
||||
try {
|
||||
await fs.access(targetFile);
|
||||
exists = true;
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
updates.push({
|
||||
source: specFile,
|
||||
target: targetFile,
|
||||
exists
|
||||
});
|
||||
} catch {
|
||||
// Source spec doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory in change
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private async updateSpec(update: SpecUpdate): Promise<void> {
|
||||
// Create target directory if needed
|
||||
const targetDir = path.dirname(update.target);
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Copy spec file
|
||||
const content = await fs.readFile(update.source, 'utf-8');
|
||||
await fs.writeFile(update.target, content);
|
||||
}
|
||||
|
||||
private getArchiveDate(): string {
|
||||
// Returns date in YYYY-MM-DD format
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -82,7 +82,7 @@ export class DiffCommand {
|
||||
choices
|
||||
});
|
||||
|
||||
return answer;
|
||||
return answer as string;
|
||||
}
|
||||
|
||||
private async showDiffs(changeSpecsDir: string): Promise<void> {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class InitCommand {
|
||||
}))
|
||||
});
|
||||
|
||||
config.aiTools = [selectedTool];
|
||||
config.aiTools = [selectedTool as string];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
+32
-12
@@ -1,8 +1,8 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { TemplateManager } from './templates/index.js';
|
||||
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
|
||||
import { OPENSPEC_DIR_NAME } from './config.js';
|
||||
import { readmeTemplate } from './templates/readme-template.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
@@ -19,17 +19,37 @@ export class UpdateCommand {
|
||||
const readmePath = path.join(openspecPath, 'README.md');
|
||||
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
|
||||
|
||||
// 3. Update CLAUDE.md (marker-based)
|
||||
const claudePath = path.join(resolvedProjectPath, 'CLAUDE.md');
|
||||
const claudeContent = TemplateManager.getClaudeTemplate();
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
claudePath,
|
||||
claudeContent,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
// 3. Update existing AI tool configuration files only
|
||||
const configurators = ToolRegistry.getAll();
|
||||
let updatedFiles: string[] = [];
|
||||
let failedFiles: string[] = [];
|
||||
|
||||
for (const configurator of configurators) {
|
||||
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
||||
|
||||
// Only update if the file already exists
|
||||
if (await FileSystemUtils.fileExists(configFilePath)) {
|
||||
try {
|
||||
await configurator.configure(resolvedProjectPath, openspecPath);
|
||||
updatedFiles.push(configurator.configFileName);
|
||||
} catch (error) {
|
||||
failedFiles.push(configurator.configFileName);
|
||||
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Success message (ASCII-safe)
|
||||
console.log('Updated OpenSpec instructions');
|
||||
const messages: string[] = ['Updated OpenSpec instructions (README.md)'];
|
||||
|
||||
if (updatedFiles.length > 0) {
|
||||
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(messages.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { ArchiveCommand } from '../../src/core/archive.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Mock @inquirer/prompts
|
||||
vi.mock('@inquirer/prompts', () => ({
|
||||
select: vi.fn(),
|
||||
confirm: vi.fn()
|
||||
}));
|
||||
|
||||
describe('ArchiveCommand', () => {
|
||||
let tempDir: string;
|
||||
let archiveCommand: ArchiveCommand;
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temp directory
|
||||
tempDir = path.join(os.tmpdir(), `openspec-archive-test-${Date.now()}`);
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Change to temp directory
|
||||
process.chdir(tempDir);
|
||||
|
||||
// Create OpenSpec structure
|
||||
const openspecDir = path.join(tempDir, 'openspec');
|
||||
await fs.mkdir(path.join(openspecDir, 'changes'), { recursive: true });
|
||||
await fs.mkdir(path.join(openspecDir, 'specs'), { recursive: true });
|
||||
await fs.mkdir(path.join(openspecDir, 'changes', 'archive'), { recursive: true });
|
||||
|
||||
// Suppress console.log during tests
|
||||
console.log = vi.fn();
|
||||
|
||||
archiveCommand = new ArchiveCommand();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore console.log
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
// Clear mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Clean up temp directory
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should archive a change successfully', async () => {
|
||||
// Create a test change
|
||||
const changeName = 'test-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Create tasks.md with completed tasks
|
||||
const tasksContent = '- [x] Task 1\n- [x] Task 2';
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
|
||||
|
||||
// Execute archive with --yes flag
|
||||
await archiveCommand.execute(changeName, { yes: true });
|
||||
|
||||
// Check that change was moved to archive
|
||||
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
|
||||
const archives = await fs.readdir(archiveDir);
|
||||
|
||||
expect(archives.length).toBe(1);
|
||||
expect(archives[0]).toMatch(new RegExp(`\\d{4}-\\d{2}-\\d{2}-${changeName}`));
|
||||
|
||||
// Verify original change directory no longer exists
|
||||
await expect(fs.access(changeDir)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should warn about incomplete tasks', async () => {
|
||||
const changeName = 'incomplete-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Create tasks.md with incomplete tasks
|
||||
const tasksContent = '- [x] Task 1\n- [ ] Task 2\n- [ ] Task 3';
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
|
||||
|
||||
// Execute archive with --yes flag
|
||||
await archiveCommand.execute(changeName, { yes: true });
|
||||
|
||||
// Verify warning was logged
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Warning: 2 incomplete task(s) found')
|
||||
);
|
||||
});
|
||||
|
||||
it('should update specs when archiving', async () => {
|
||||
const changeName = 'spec-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
const changeSpecDir = path.join(changeDir, 'specs', 'test-capability');
|
||||
await fs.mkdir(changeSpecDir, { recursive: true });
|
||||
|
||||
// Create spec in change
|
||||
const specContent = '# Test Capability Spec\n\nTest content';
|
||||
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
|
||||
|
||||
// Execute archive with --yes flag
|
||||
await archiveCommand.execute(changeName, { yes: true });
|
||||
|
||||
// Verify spec was copied to main specs
|
||||
const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'test-capability', 'spec.md');
|
||||
const copiedContent = await fs.readFile(mainSpecPath, 'utf-8');
|
||||
expect(copiedContent).toBe(specContent);
|
||||
});
|
||||
|
||||
it('should throw error if change does not exist', async () => {
|
||||
await expect(
|
||||
archiveCommand.execute('non-existent-change', { yes: true })
|
||||
).rejects.toThrow("Change 'non-existent-change' not found.");
|
||||
});
|
||||
|
||||
it('should throw error if archive already exists', async () => {
|
||||
const changeName = 'duplicate-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Create existing archive with same date
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const archivePath = path.join(tempDir, 'openspec', 'changes', 'archive', `${date}-${changeName}`);
|
||||
await fs.mkdir(archivePath, { recursive: true });
|
||||
|
||||
// Try to archive
|
||||
await expect(
|
||||
archiveCommand.execute(changeName, { yes: true })
|
||||
).rejects.toThrow(`Archive '${date}-${changeName}' already exists.`);
|
||||
});
|
||||
|
||||
it('should handle changes without tasks.md', async () => {
|
||||
const changeName = 'no-tasks-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Execute archive without tasks.md
|
||||
await archiveCommand.execute(changeName, { yes: true });
|
||||
|
||||
// Should complete without warnings
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('incomplete task(s)')
|
||||
);
|
||||
|
||||
// Verify change was archived
|
||||
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
|
||||
const archives = await fs.readdir(archiveDir);
|
||||
expect(archives.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle changes without specs', async () => {
|
||||
const changeName = 'no-specs-feature';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Execute archive without specs
|
||||
await archiveCommand.execute(changeName, { yes: true });
|
||||
|
||||
// Should complete without spec updates
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Specs to update')
|
||||
);
|
||||
|
||||
// Verify change was archived
|
||||
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
|
||||
const archives = await fs.readdir(archiveDir);
|
||||
expect(archives.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error when openspec directory does not exist', async () => {
|
||||
// Remove openspec directory
|
||||
await fs.rm(path.join(tempDir, 'openspec'), { recursive: true });
|
||||
|
||||
await expect(
|
||||
archiveCommand.execute('any-change', { yes: true })
|
||||
).rejects.toThrow("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactive mode', () => {
|
||||
it('should use select prompt for change selection', async () => {
|
||||
const { select } = await import('@inquirer/prompts');
|
||||
const mockSelect = select as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
// Create test changes
|
||||
const change1 = 'feature-a';
|
||||
const change2 = 'feature-b';
|
||||
await fs.mkdir(path.join(tempDir, 'openspec', 'changes', change1), { recursive: true });
|
||||
await fs.mkdir(path.join(tempDir, 'openspec', 'changes', change2), { recursive: true });
|
||||
|
||||
// Mock select to return first change
|
||||
mockSelect.mockResolvedValueOnce(change1);
|
||||
|
||||
// Execute without change name
|
||||
await archiveCommand.execute(undefined, { yes: true });
|
||||
|
||||
// Verify select was called with correct options
|
||||
expect(mockSelect).toHaveBeenCalledWith({
|
||||
message: 'Select a change to archive',
|
||||
choices: [
|
||||
{ name: change1, value: change1 },
|
||||
{ name: change2, value: change2 }
|
||||
]
|
||||
});
|
||||
|
||||
// Verify the selected change was archived
|
||||
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
|
||||
const archives = await fs.readdir(archiveDir);
|
||||
expect(archives[0]).toContain(change1);
|
||||
});
|
||||
|
||||
it('should use confirm prompt for task warnings', async () => {
|
||||
const { confirm } = await import('@inquirer/prompts');
|
||||
const mockConfirm = confirm as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
const changeName = 'incomplete-interactive';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Create tasks.md with incomplete tasks
|
||||
const tasksContent = '- [ ] Task 1';
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
|
||||
|
||||
// Mock confirm to return true (proceed)
|
||||
mockConfirm.mockResolvedValueOnce(true);
|
||||
|
||||
// Execute without --yes flag
|
||||
await archiveCommand.execute(changeName);
|
||||
|
||||
// Verify confirm was called
|
||||
expect(mockConfirm).toHaveBeenCalledWith({
|
||||
message: 'Warning: 1 incomplete task(s) found. Continue?',
|
||||
default: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel when user declines task warning', async () => {
|
||||
const { confirm } = await import('@inquirer/prompts');
|
||||
const mockConfirm = confirm as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
const changeName = 'cancel-test';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
// Create tasks.md with incomplete tasks
|
||||
const tasksContent = '- [ ] Task 1';
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), tasksContent);
|
||||
|
||||
// Mock confirm to return false (cancel)
|
||||
mockConfirm.mockResolvedValueOnce(false);
|
||||
|
||||
// Execute without --yes flag
|
||||
await archiveCommand.execute(changeName);
|
||||
|
||||
// Verify archive was cancelled
|
||||
expect(console.log).toHaveBeenCalledWith('Archive cancelled.');
|
||||
|
||||
// Verify change was not archived
|
||||
await expect(fs.access(changeDir)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { UpdateCommand } from '../../src/core/update.js';
|
||||
import { FileSystemUtils } from '../../src/utils/file-system.js';
|
||||
import { ToolRegistry } from '../../src/core/configurators/registry.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
|
||||
describe('UpdateCommand', () => {
|
||||
let testDir: string;
|
||||
let updateCommand: UpdateCommand;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temporary test directory
|
||||
testDir = path.join(os.tmpdir(), `openspec-test-${Date.now()}`);
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
|
||||
// Create openspec directory
|
||||
const openspecDir = path.join(testDir, 'openspec');
|
||||
await fs.mkdir(openspecDir, { recursive: true });
|
||||
|
||||
updateCommand = new UpdateCommand();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test directory
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should update only existing CLAUDE.md file', async () => {
|
||||
// Create CLAUDE.md file with initial content
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
const initialContent = `# Project Instructions
|
||||
|
||||
Some existing content here.
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old OpenSpec content
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
More content after.`;
|
||||
await fs.writeFile(claudePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLAUDE.md was updated
|
||||
const updatedContent = await fs.readFile(claudePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain('This project uses OpenSpec');
|
||||
expect(updatedContent).toContain('Some existing content here');
|
||||
expect(updatedContent).toContain('More content after');
|
||||
|
||||
// Check console output
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.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');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLAUDE.md was not created
|
||||
const fileExists = await FileSystemUtils.fileExists(claudePath);
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
|
||||
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)');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should update multiple AI tool files if present', async () => {
|
||||
// TODO: When additional configurators are added (Cursor, Aider, etc.),
|
||||
// enhance this test to create multiple AI tool files and verify
|
||||
// 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.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Should report updating with new format
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nUpdated AI tool files: CLAUDE.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should never create new AI tool files', async () => {
|
||||
// Get all configurators
|
||||
const configurators = ToolRegistry.getAll();
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that no new AI tool files were created
|
||||
for (const configurator of configurators) {
|
||||
const configPath = path.join(testDir, configurator.configFileName);
|
||||
const fileExists = await FileSystemUtils.fileExists(configPath);
|
||||
expect(fileExists).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should update README.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);
|
||||
expect(fileExists).toBe(true);
|
||||
|
||||
const content = await fs.readFile(readmePath, 'utf-8');
|
||||
expect(content).toContain('# OpenSpec Instructions');
|
||||
});
|
||||
|
||||
it('should throw error if openspec directory does not exist', async () => {
|
||||
// Remove openspec directory
|
||||
await fs.rm(path.join(testDir, 'openspec'), { recursive: true, force: true });
|
||||
|
||||
// Execute update command and expect error
|
||||
await expect(updateCommand.execute(testDir)).rejects.toThrow(
|
||||
"No OpenSpec directory found. Run 'openspec init' first."
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle configurator errors gracefully', async () => {
|
||||
// Create CLAUDE.md file but make it read-only to cause an error
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
await fs.writeFile(claudePath, '<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->');
|
||||
await fs.chmod(claudePath, 0o444); // Read-only
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
const errorSpy = vi.spyOn(console, 'error');
|
||||
|
||||
// Execute update command - should not throw
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Should report the failure
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Updated OpenSpec instructions (README.md)\nFailed to update: CLAUDE.md'
|
||||
);
|
||||
|
||||
// Restore permissions for cleanup
|
||||
await fs.chmod(claudePath, 0o644);
|
||||
consoleSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user