mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a559e0d00 | ||
|
|
a3924f17b2 | ||
|
|
b11e862b0f | ||
|
|
099585afcb | ||
|
|
f023fc317e | ||
|
|
38a1463af0 | ||
|
|
f2399d3280 | ||
|
|
f699e10778 | ||
|
|
c824d8927f | ||
|
|
5821b24ab3 | ||
|
|
e812eb9e78 | ||
|
|
abfe13c5a7 | ||
|
|
0d5a75d3a0 | ||
|
|
b30c0ad27e | ||
|
|
1cada18186 | ||
|
|
7781bbadd3 | ||
|
|
aeaa1d50cc | ||
|
|
fa5df9a329 | ||
|
|
f94f396c99 | ||
|
|
1f670f71d4 | ||
|
|
32b2901d13 | ||
|
|
2ad0b1d306 | ||
|
|
279d327899 | ||
|
|
5d848cf005 | ||
|
|
5607fd3ccb | ||
|
|
6a0d862258 | ||
|
|
1fe5f84fbc | ||
|
|
80e78ecd1e | ||
|
|
564135a530 | ||
|
|
b9e80641a0 | ||
|
|
0755994eaa | ||
|
|
dcabd6de31 | ||
|
|
aef6ce01ff | ||
|
|
441f9f444b | ||
|
|
b322829091 | ||
|
|
5167e65a5c | ||
|
|
5c6b4113a7 | ||
|
|
a8b76c3e69 | ||
|
|
d3237cac7b | ||
|
|
e395eb4eeb | ||
|
|
76e1ec2a1f | ||
|
|
6cbb803e48 | ||
|
|
183b82f266 | ||
|
|
27eaccc024 | ||
|
|
b288f2fc88 | ||
|
|
22134a603b | ||
|
|
3b5fd11cb9 | ||
|
|
fce227a36e | ||
|
|
e9417fc147 | ||
|
|
467346f9fe | ||
|
|
9a03ba1853 |
@@ -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
|
||||
@@ -0,0 +1,66 @@
|
||||
# Adopt Delta-Based Changes for Specifications
|
||||
|
||||
## Why
|
||||
|
||||
The current approach of storing complete future states in change proposals creates a poor review experience. When reviewing changes on GitHub, reviewers see entire spec files (often 100+ lines) as "added" in green, making it impossible to identify what actually changed. With the recent structured format adoption, we now have clear section boundaries that enable a better approach: storing only additions and modifications.
|
||||
|
||||
## What Changes
|
||||
|
||||
Store only the requirements that actually change, not complete future states:
|
||||
|
||||
- **ADDED Requirements**: New capabilities being introduced
|
||||
- **MODIFIED Requirements**: Existing requirements being changed (must match current header)
|
||||
- **REMOVED Requirements**: Deprecated capabilities
|
||||
- **RENAMED Requirements**: Explicit header changes (e.g., `FROM: Old Name` → `TO: New Name`)
|
||||
|
||||
The archive command will programmatically apply these deltas using normalized header matching (trim leading/trailing whitespace) instead of manually copying entire files.
|
||||
|
||||
## Impact
|
||||
|
||||
**Affected specs**: openspec-conventions, cli-archive, cli-diff
|
||||
|
||||
**Benefits**:
|
||||
- GitHub diffs show only actual changes (25 lines instead of 150+)
|
||||
- Reviewers immediately see what's being added, modified, or removed
|
||||
- Conflicts are more apparent when two changes modify the same requirement
|
||||
- Archive command can programmatically apply changes
|
||||
|
||||
**Format**: Delta format only - all changes must use ADDED/MODIFIED/REMOVED sections.
|
||||
|
||||
## Example
|
||||
|
||||
Instead of storing a 150-line complete future spec, store only:
|
||||
|
||||
```markdown
|
||||
# User Authentication - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: OAuth Support
|
||||
Users SHALL authenticate via OAuth providers including Google and GitHub.
|
||||
|
||||
#### Scenario: OAuth login flow
|
||||
- **WHEN** user selects OAuth provider
|
||||
- **THEN** redirect to provider authorization
|
||||
- **AND** exchange authorization code for tokens
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Session Management
|
||||
Sessions SHALL expire after 30 minutes of inactivity.
|
||||
|
||||
#### Scenario: Inactive session timeout
|
||||
- **WHEN** no activity for 30 minutes ← (was 60 minutes)
|
||||
- **THEN** invalidate session token
|
||||
- **AND** require re-authentication
|
||||
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Basic Authentication`
|
||||
- TO: `### Requirement: Email Authentication`
|
||||
```
|
||||
|
||||
This makes reviews focused and changes explicit.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Git naturally detects conflicts when two changes modify the same requirement header. This is actually better than full-state storage where Git might silently merge incompatible changes.
|
||||
@@ -0,0 +1,46 @@
|
||||
# CLI Archive Command - Changes
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.
|
||||
|
||||
#### Scenario: Applying delta changes
|
||||
|
||||
- **WHEN** archiving a change with delta-based specs
|
||||
- **THEN** parse and apply delta changes as defined in openspec-conventions
|
||||
- **AND** validate all operations before applying
|
||||
|
||||
#### Scenario: Validating delta changes
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** perform validations as specified in openspec-conventions
|
||||
- **AND** if validation fails, show specific errors and abort
|
||||
|
||||
#### Scenario: Conflict detection
|
||||
|
||||
- **WHEN** applying deltas would create duplicate requirement headers
|
||||
- **THEN** abort with error message showing the conflict
|
||||
- **AND** suggest manual resolution
|
||||
|
||||
### Requirement: Display Output
|
||||
|
||||
The command SHALL provide clear feedback about delta operations.
|
||||
|
||||
#### Scenario: Showing delta application
|
||||
|
||||
- **WHEN** applying delta changes
|
||||
- **THEN** display for each spec:
|
||||
- Number of requirements added
|
||||
- Number of requirements modified
|
||||
- Number of requirements removed
|
||||
- Number of requirements renamed
|
||||
- **AND** use standard output symbols (+ ~ - →) as defined in openspec-conventions:
|
||||
```
|
||||
Applying changes to specs/user-auth/spec.md:
|
||||
+ 2 added
|
||||
~ 3 modified
|
||||
- 1 removed
|
||||
→ 1 renamed
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# CLI Diff Command - Changes
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Display Format
|
||||
|
||||
**Reason for removal**: The standard unified diff format is replaced by requirement-level side-by-side comparison that better shows semantic changes rather than line-by-line text differences.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Diff Output
|
||||
|
||||
The command SHALL show a requirement-level comparison displaying only changed requirements.
|
||||
|
||||
#### Scenario: Side-by-side comparison of changes
|
||||
|
||||
- **WHEN** running `openspec diff <change>`
|
||||
- **THEN** display only requirements that have changed
|
||||
- **AND** show them in a side-by-side format that:
|
||||
- Clearly shows the current version on the left
|
||||
- Shows the future version on the right
|
||||
- Indicates new requirements (not in current)
|
||||
- Indicates removed requirements (not in future)
|
||||
- Aligns modified requirements for easy comparison
|
||||
|
||||
### Requirement: Validation
|
||||
|
||||
The command SHALL validate that changes can be applied successfully.
|
||||
|
||||
#### Scenario: Invalid delta references
|
||||
|
||||
- **WHEN** delta references non-existent requirement
|
||||
- **THEN** show error message with specific requirement
|
||||
- **AND** continue showing other valid changes
|
||||
- **AND** clearly mark failed changes in the output
|
||||
@@ -0,0 +1,109 @@
|
||||
# OpenSpec Conventions - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
|
||||
|
||||
#### Scenario: Matching requirements programmatically
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
|
||||
- **AND** match using normalized headers: `normalize(header) = trim(header)`
|
||||
- **AND** compare headers with case-sensitive equality after normalization
|
||||
|
||||
#### Scenario: Handling requirement renames
|
||||
|
||||
- **WHEN** renaming a requirement
|
||||
- **THEN** use a special `## RENAMED Requirements` section
|
||||
- **AND** specify both old and new names explicitly:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
- **AND** if content also changes, include under MODIFIED using the NEW header
|
||||
|
||||
#### Scenario: Validating header uniqueness
|
||||
|
||||
- **WHEN** creating or modifying requirements
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
|
||||
#### Scenario: Creating change proposals with additions
|
||||
|
||||
- **WHEN** creating a change proposal that adds new requirements
|
||||
- **THEN** include only the new requirements under `## ADDED Requirements`
|
||||
- **AND** each requirement SHALL include its complete content
|
||||
- **AND** use the standard structured format for requirements and scenarios
|
||||
|
||||
#### Scenario: Creating change proposals with modifications
|
||||
|
||||
- **WHEN** creating a change proposal that modifies existing requirements
|
||||
- **THEN** include the modified requirements under `## MODIFIED Requirements`
|
||||
- **AND** use the same header text as in the current spec (normalized)
|
||||
- **AND** include the complete modified requirement (not a diff)
|
||||
- **AND** optionally annotate what changed with inline comments like `← (was X)`
|
||||
|
||||
#### Scenario: Creating change proposals with removals
|
||||
|
||||
- **WHEN** creating a change proposal that removes requirements
|
||||
- **THEN** list them under `## REMOVED Requirements`
|
||||
- **AND** use the normalized header text for identification
|
||||
- **AND** include reason for removal
|
||||
- **AND** document any migration path if applicable
|
||||
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Delta files showing only what changes
|
||||
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
|
||||
- Normalized header matching for requirement identification
|
||||
- Complete requirements using the structured format
|
||||
- Clear indication of change type for each requirement
|
||||
|
||||
#### Scenario: Using standard output symbols
|
||||
|
||||
- **WHEN** displaying delta operations in CLI output
|
||||
- **THEN** use these standard symbols:
|
||||
- `+` for ADDED (green)
|
||||
- `~` for MODIFIED (yellow)
|
||||
- `-` for REMOVED (red)
|
||||
- `→` for RENAMED (cyan)
|
||||
|
||||
### Requirement: Archive Process Enhancement
|
||||
|
||||
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
|
||||
|
||||
#### Scenario: Archiving changes with deltas
|
||||
|
||||
- **WHEN** archiving a completed change
|
||||
- **THEN** the archive command SHALL:
|
||||
1. Parse RENAMED sections first and apply renames
|
||||
2. Parse REMOVED sections and remove by normalized header match
|
||||
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
|
||||
4. Parse ADDED sections and append new requirements
|
||||
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
|
||||
- **AND** validate that ADDED headers don't already exist
|
||||
- **AND** generate the updated spec in the main specs/ directory
|
||||
|
||||
#### Scenario: Handling conflicts during archive
|
||||
|
||||
- **WHEN** delta changes conflict with current spec state
|
||||
- **THEN** the archive command SHALL report specific conflicts
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Future State Storage
|
||||
|
||||
**Reason for removal**: Replaced by delta-based change storage which provides better review experience and clearer change tracking.
|
||||
|
||||
**Migration path**: All new changes must use delta format.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Conventions
|
||||
- [ ] 1.1 Update openspec-conventions spec with delta-based approach
|
||||
- [ ] 1.2 Add Header-Based Requirement Identification
|
||||
- [ ] 1.3 Define ADDED/MODIFIED/REMOVED/RENAMED sections
|
||||
- [ ] 1.4 Document standard output symbols (+ ~ - →)
|
||||
- [ ] 1.5 Update openspec/README.md with delta-based conventions
|
||||
- [ ] 1.6 Update examples to use delta format
|
||||
|
||||
## 2. Update Diff Command
|
||||
- [ ] 2.1 Update cli-diff spec with requirement-level comparison
|
||||
- [ ] 2.2 Parse specs into requirement-level structures
|
||||
- [ ] 2.3 Apply deltas to generate future state
|
||||
- [ ] 2.4 Implement side-by-side comparison view (changes only)
|
||||
- [ ] 2.5 Add tests for requirement-level comparison
|
||||
- [ ] 2.6 Add tests for side-by-side view formatting
|
||||
|
||||
## 3. Update Archive Command
|
||||
- [ ] 3.1 Update cli-archive spec with delta processing behavior
|
||||
- [ ] 3.2 Implement normalized header matching (trim whitespace)
|
||||
- [ ] 3.3 Parse delta sections (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
- [ ] 3.4 Apply changes in order: RENAMED → REMOVED → MODIFIED → ADDED
|
||||
- [ ] 3.5 Validate delta operations:
|
||||
- [ ] 3.5.1 MODIFIED/REMOVED requirements exist
|
||||
- [ ] 3.5.2 ADDED requirements don't already exist
|
||||
- [ ] 3.5.3 RENAMED FROM headers exist, TO headers don't
|
||||
- [ ] 3.5.4 No duplicate headers within specs
|
||||
- [ ] 3.5.5 Renamed requirements aren't also in ADDED
|
||||
- [ ] 3.6 Display operation counts (+ 2 added, ~ 3 modified, etc.)
|
||||
- [ ] 3.7 Add tests for header normalization
|
||||
- [ ] 3.8 Add tests for applying deltas in correct order
|
||||
- [ ] 3.9 Add tests for validation edge cases
|
||||
|
||||
## Notes
|
||||
- Archive command is critical path - must work reliably
|
||||
- All new changes must use delta format
|
||||
- Header normalization: normalize(header) = trim(header)
|
||||
- Diff command shows only changed requirements in side-by-side comparison
|
||||
@@ -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,19 @@
|
||||
# Add Diff Command to OpenSpec CLI
|
||||
|
||||
## Why
|
||||
|
||||
Developers need to easily view differences between proposed spec changes and current specs without manually comparing files.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec diff [change-name]` command that shows differences between change specs and current specs
|
||||
- Compare files in `changes/[change-name]/specs/` with corresponding files in `specs/`
|
||||
- Display unified diff output showing added/removed/modified lines
|
||||
- Support colored output for better readability
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `cli-diff` will be added
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Add diff command
|
||||
- `src/core/diff.ts` - New file with diff logic (~80 lines)
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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
|
||||
|
||||
### 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
|
||||
|
||||
### With Change Name
|
||||
|
||||
WHEN running `openspec diff <change-name>`
|
||||
THEN compare all spec files in `changes/<change-name>/specs/` with corresponding files in `specs/`
|
||||
|
||||
### Diff Output
|
||||
|
||||
FOR each spec file in the change:
|
||||
- IF file exists in both locations THEN show unified diff
|
||||
- IF file only exists in change THEN show as new file (all lines with +)
|
||||
- IF file only exists in current specs THEN show as deleted (all lines with -)
|
||||
|
||||
### Display Format
|
||||
|
||||
The diff SHALL 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
|
||||
|
||||
### Color Support
|
||||
|
||||
WHEN terminal supports colors:
|
||||
- Removed lines displayed in red
|
||||
- Added lines displayed in green
|
||||
- File headers displayed in bold
|
||||
- Context lines in default color
|
||||
|
||||
### Error Handling
|
||||
|
||||
WHEN specified change doesn't exist THEN display error "Change '<name>' not found"
|
||||
WHEN no specs directory in change THEN display "No spec changes found for '<name>'"
|
||||
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):
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [x] 1.1 Create `src/core/diff.ts` with diff logic
|
||||
- [x] 1.2 Implement change directory scanning
|
||||
- [x] 1.3 Implement file comparison using unified diff format
|
||||
- [x] 1.4 Add color support for terminal output
|
||||
|
||||
## 2. CLI Integration
|
||||
- [x] 2.1 Add diff command to `src/cli/index.ts`
|
||||
- [x] 2.2 Implement interactive change selection when no argument provided
|
||||
- [x] 2.3 Add error handling for missing changes
|
||||
|
||||
## 3. Enhancements
|
||||
- [x] 3.1 Replace with jest-diff for professional diff output
|
||||
- [x] 3.2 Improve file headers with status and statistics
|
||||
- [x] 3.3 Add summary view with file counts and line changes
|
||||
|
||||
## 4. Testing
|
||||
- [ ] 4.1 Test diff generation for modified files
|
||||
- [ ] 4.2 Test handling of new files
|
||||
- [ ] 4.3 Test handling of deleted files
|
||||
- [ ] 4.4 Test interactive mode
|
||||
@@ -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
|
||||
@@ -0,0 +1,180 @@
|
||||
# OpenSpec Conventions Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
- Specs reflect what IS currently built and deployed
|
||||
- Changes contain proposals for what SHOULD be changed
|
||||
- AI drives the documentation process
|
||||
- Specs are living documentation kept in sync with deployed code
|
||||
|
||||
## Directory Structure
|
||||
|
||||
WHEN an OpenSpec project is initialized
|
||||
THEN it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── 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
|
||||
|
||||
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
|
||||
|
||||
WHEN documenting what changes
|
||||
THEN the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
## Change Lifecycle
|
||||
|
||||
The change process SHALL follow these states:
|
||||
|
||||
1. **Propose**: AI creates change with future state specs and explicit proposal
|
||||
2. **Review**: Humans review proposal and future state
|
||||
3. **Approve**: Change is approved for implementation
|
||||
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
|
||||
5. **Deploy**: Changes are deployed to production
|
||||
6. **Update**: Specs in `specs/` are updated to match deployed reality
|
||||
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
## Viewing 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
|
||||
|
||||
The system relies on tools to generate diffs rather than storing them.
|
||||
|
||||
## Capability Naming
|
||||
|
||||
Capabilities SHALL use:
|
||||
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
|
||||
- Hyphenated lowercase names
|
||||
- Singular focus (one responsibility per capability)
|
||||
- No nesting (flat structure under `specs/`)
|
||||
|
||||
## When Changes Require Proposals
|
||||
|
||||
A proposal SHALL be created for:
|
||||
- New features or capabilities
|
||||
- Breaking changes to existing behavior
|
||||
- Architecture or pattern changes
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting access patterns
|
||||
|
||||
A proposal is NOT required for:
|
||||
- Bug fixes restoring intended behavior
|
||||
- Typos or formatting fixes
|
||||
- Non-breaking dependency updates
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **Readability**: No diff syntax pollution
|
||||
- **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
|
||||
|
||||
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,65 @@ 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.
|
||||
|
||||
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 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"
|
||||
|
||||
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 block in `CLAUDE.md` using markers
|
||||
- **AND** use the default directory name `openspec`
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
|
||||
## 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 CLAUDE.md
|
||||
|
||||
- **WHEN** CLAUDE.md doesn't exist
|
||||
- **THEN** create it with the template content
|
||||
|
||||
#### 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
|
||||
@@ -53,7 +53,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.8.0",
|
||||
"chalk": "^5.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"jest-diff": "^30.0.5",
|
||||
"ora": "^8.2.0"
|
||||
}
|
||||
}
|
||||
Generated
+86
@@ -11,9 +11,15 @@ importers:
|
||||
'@inquirer/prompts':
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0(@types/node@24.2.0)
|
||||
chalk:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
jest-diff:
|
||||
specifier: ^30.0.5
|
||||
version: 30.0.5
|
||||
ora:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
@@ -310,6 +316,18 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@jest/diff-sequences@30.0.1':
|
||||
resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/get-type@30.0.1':
|
||||
resolution: {integrity: sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4':
|
||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
||||
|
||||
@@ -416,6 +434,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sinclair/typebox@0.34.38':
|
||||
resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
|
||||
|
||||
@@ -478,6 +499,10 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -490,6 +515,10 @@ packages:
|
||||
resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.5.0:
|
||||
resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
@@ -585,6 +614,10 @@ packages:
|
||||
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -605,6 +638,10 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
jest-diff@30.0.5:
|
||||
resolution: {integrity: sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
@@ -668,6 +705,13 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
pretty-format@30.0.5:
|
||||
resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -724,6 +768,10 @@ packages:
|
||||
strip-literal@3.0.0:
|
||||
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -1048,6 +1096,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
'@jest/diff-sequences@30.0.1': {}
|
||||
|
||||
'@jest/get-type@30.0.1': {}
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.34.38
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
@@ -1112,6 +1168,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
||||
optional: true
|
||||
|
||||
'@sinclair/typebox@0.34.38': {}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
@@ -1189,6 +1247,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
@@ -1201,6 +1261,11 @@ snapshots:
|
||||
loupe: 3.2.0
|
||||
pathval: 2.0.1
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.5.0: {}
|
||||
|
||||
chardet@0.7.0: {}
|
||||
@@ -1289,6 +1354,8 @@ snapshots:
|
||||
|
||||
get-east-asian-width@1.3.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -1301,6 +1368,13 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
jest-diff@30.0.5:
|
||||
dependencies:
|
||||
'@jest/diff-sequences': 30.0.1
|
||||
'@jest/get-type': 30.0.1
|
||||
chalk: 4.1.2
|
||||
pretty-format: 30.0.5
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
log-symbols@6.0.0:
|
||||
@@ -1356,6 +1430,14 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
pretty-format@30.0.5:
|
||||
dependencies:
|
||||
'@jest/schemas': 30.0.5
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
@@ -1431,6 +1513,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
@@ -4,6 +4,9 @@ import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
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();
|
||||
|
||||
@@ -60,4 +63,47 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('diff [change-name]')
|
||||
.description('Show differences between proposed spec changes and current specs')
|
||||
.action(async (changeName?: string) => {
|
||||
try {
|
||||
const diffCommand = new DiffCommand();
|
||||
await diffCommand.execute(changeName);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List all active changes with their task status')
|
||||
.action(async () => {
|
||||
try {
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute();
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { diffStringsUnified } from 'jest-diff';
|
||||
import { select } from '@inquirer/prompts';
|
||||
|
||||
// Constants
|
||||
const ARCHIVE_DIR = 'archive';
|
||||
const MARKDOWN_EXT = '.md';
|
||||
const OPENSPEC_DIR = 'openspec';
|
||||
const CHANGES_DIR = 'changes';
|
||||
const SPECS_DIR = 'specs';
|
||||
|
||||
export class DiffCommand {
|
||||
private filesChanged: number = 0;
|
||||
private linesAdded: number = 0;
|
||||
private linesRemoved: number = 0;
|
||||
|
||||
async execute(changeName?: string): Promise<void> {
|
||||
const changesDir = path.join(process.cwd(), OPENSPEC_DIR, CHANGES_DIR);
|
||||
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error('No OpenSpec changes directory found');
|
||||
}
|
||||
|
||||
if (!changeName) {
|
||||
changeName = await this.selectChange(changesDir);
|
||||
if (!changeName) return;
|
||||
}
|
||||
|
||||
const changeDir = path.join(changesDir, changeName);
|
||||
|
||||
try {
|
||||
await fs.access(changeDir);
|
||||
} catch {
|
||||
throw new Error(`Change '${changeName}' not found`);
|
||||
}
|
||||
|
||||
const changeSpecsDir = path.join(changeDir, SPECS_DIR);
|
||||
|
||||
try {
|
||||
await fs.access(changeSpecsDir);
|
||||
} catch {
|
||||
console.log(`No spec changes found for '${changeName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset counters
|
||||
this.filesChanged = 0;
|
||||
this.linesAdded = 0;
|
||||
this.linesRemoved = 0;
|
||||
|
||||
await this.showDiffs(changeSpecsDir);
|
||||
|
||||
// Show summary
|
||||
if (this.filesChanged > 0) {
|
||||
console.log(chalk.bold(`\n📊 Summary: ${this.filesChanged} file(s) changed, ${chalk.green(`+${this.linesAdded}`)} ${chalk.red(`-${this.linesRemoved}`)}`));
|
||||
}
|
||||
}
|
||||
|
||||
private async selectChange(changesDir: string): Promise<string | undefined> {
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changes = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== ARCHIVE_DIR)
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changes.length === 0) {
|
||||
console.log('No changes found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log('Available changes:');
|
||||
const choices = changes.map((name) => ({
|
||||
name: name,
|
||||
value: name
|
||||
}));
|
||||
|
||||
const answer = await select({
|
||||
message: 'Select a change',
|
||||
choices
|
||||
});
|
||||
|
||||
return answer as string;
|
||||
}
|
||||
|
||||
private async showDiffs(changeSpecsDir: string): Promise<void> {
|
||||
const currentSpecsDir = path.join(process.cwd(), OPENSPEC_DIR, SPECS_DIR);
|
||||
await this.walkAndDiff(changeSpecsDir, currentSpecsDir, '');
|
||||
}
|
||||
|
||||
private async walkAndDiff(changeDir: string, currentDir: string, relativePath: string): Promise<void> {
|
||||
const entries = await fs.readdir(path.join(changeDir, relativePath), { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(relativePath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.walkAndDiff(changeDir, currentDir, entryPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) {
|
||||
await this.diffFile(
|
||||
path.join(changeDir, entryPath),
|
||||
path.join(currentDir, entryPath),
|
||||
entryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async diffFile(changePath: string, currentPath: string, displayPath: string): Promise<void> {
|
||||
let changeContent = '';
|
||||
let currentContent = '';
|
||||
let isNewFile = false;
|
||||
let isDeleted = false;
|
||||
|
||||
try {
|
||||
changeContent = await fs.readFile(changePath, 'utf-8');
|
||||
} catch {
|
||||
changeContent = '';
|
||||
}
|
||||
|
||||
try {
|
||||
currentContent = await fs.readFile(currentPath, 'utf-8');
|
||||
} catch {
|
||||
currentContent = '';
|
||||
isNewFile = true;
|
||||
}
|
||||
|
||||
if (changeContent === currentContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changeContent === '' && currentContent !== '') {
|
||||
isDeleted = true;
|
||||
}
|
||||
|
||||
// Enhanced header with file status
|
||||
console.log(chalk.bold.cyan(`\n${'═'.repeat(60)}`));
|
||||
console.log(chalk.bold.cyan(`📄 ${displayPath}`));
|
||||
|
||||
if (isNewFile) {
|
||||
console.log(chalk.green(` Status: NEW FILE`));
|
||||
} else if (isDeleted) {
|
||||
console.log(chalk.red(` Status: DELETED`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Status: MODIFIED`));
|
||||
}
|
||||
|
||||
// Use jest-diff for the actual diff with custom options
|
||||
const diffOptions = {
|
||||
aAnnotation: 'Current',
|
||||
bAnnotation: 'Proposed',
|
||||
aColor: chalk.red,
|
||||
bColor: chalk.green,
|
||||
commonColor: chalk.gray,
|
||||
contextLines: 3,
|
||||
expand: false,
|
||||
includeChangeCounts: true,
|
||||
};
|
||||
|
||||
const diff = diffStringsUnified(currentContent, changeContent, diffOptions);
|
||||
|
||||
// Count lines for statistics (approximate)
|
||||
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
||||
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
||||
|
||||
console.log(chalk.gray(` Lines: ${chalk.green(`+${addedLines}`)} ${chalk.red(`-${removedLines}`)}`));
|
||||
console.log(chalk.bold.cyan(`${'─'.repeat(60)}\n`));
|
||||
|
||||
// Display the diff
|
||||
console.log(diff);
|
||||
|
||||
// Update counters
|
||||
this.filesChanged++;
|
||||
this.linesAdded += addedLines;
|
||||
this.linesRemoved += removedLines;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class InitCommand {
|
||||
}))
|
||||
});
|
||||
|
||||
config.aiTools = [selectedTool];
|
||||
config.aiTools = [selectedTool as string];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
interface ChangeInfo {
|
||||
name: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
}
|
||||
|
||||
export class ListCommand {
|
||||
async execute(targetPath: string = '.'): Promise<void> {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const tasksPath = path.join(changesDir, changeDir, 'tasks.md');
|
||||
let completedTasks = 0;
|
||||
let incompleteTasks = 0;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(tasksPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('- [x]')) {
|
||||
completedTasks++;
|
||||
} else if (line.includes('- [ ]')) {
|
||||
incompleteTasks++;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No tasks.md file
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: 0,
|
||||
totalTasks: 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks,
|
||||
totalTasks: completedTasks + incompleteTasks
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
for (const change of changes) {
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
|
||||
let status: string;
|
||||
if (change.totalTasks === 0) {
|
||||
status = 'No tasks';
|
||||
} else if (change.completedTasks === change.totalTasks) {
|
||||
status = '✓ Complete';
|
||||
} else {
|
||||
status = `${change.completedTasks}/${change.totalTasks} tasks`;
|
||||
}
|
||||
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ListCommand } from '../../src/core/list.js';
|
||||
|
||||
describe('ListCommand', () => {
|
||||
let tempDir: string;
|
||||
let originalLog: typeof console.log;
|
||||
let logOutput: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temp directory
|
||||
tempDir = path.join(os.tmpdir(), `openspec-list-test-${Date.now()}`);
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Mock console.log to capture output
|
||||
originalLog = console.log;
|
||||
console.log = (...args: any[]) => {
|
||||
logOutput.push(args.join(' '));
|
||||
};
|
||||
logOutput = [];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore console.log
|
||||
console.log = originalLog;
|
||||
|
||||
// Clean up temp directory
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should handle missing openspec/changes directory', async () => {
|
||||
const listCommand = new ListCommand();
|
||||
|
||||
await expect(listCommand.execute(tempDir)).rejects.toThrow(
|
||||
"No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty changes directory', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toEqual(['No active changes found.']);
|
||||
});
|
||||
|
||||
it('should exclude archive directory', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'archive'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'my-change'), { recursive: true });
|
||||
|
||||
// Create tasks.md with some tasks
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'my-change', 'tasks.md'),
|
||||
'- [x] Task 1\n- [ ] Task 2\n'
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toContain('Changes:');
|
||||
expect(logOutput.some(line => line.includes('my-change'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('archive'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should count tasks correctly', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'test-change'), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'test-change', 'tasks.md'),
|
||||
`# Tasks
|
||||
- [x] Completed task 1
|
||||
- [x] Completed task 2
|
||||
- [ ] Incomplete task 1
|
||||
- [ ] Incomplete task 2
|
||||
- [ ] Incomplete task 3
|
||||
Regular text that should be ignored
|
||||
`
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('2/5 tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should show complete status for fully completed changes', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'completed-change'), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'completed-change', 'tasks.md'),
|
||||
'- [x] Task 1\n- [x] Task 2\n- [x] Task 3\n'
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('✓ Complete'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle changes without tasks.md', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should sort changes alphabetically', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
const changeLines = logOutput.filter(line =>
|
||||
line.includes('alpha') || line.includes('middle') || line.includes('zebra')
|
||||
);
|
||||
|
||||
expect(changeLines[0]).toContain('alpha');
|
||||
expect(changeLines[1]).toContain('middle');
|
||||
expect(changeLines[2]).toContain('zebra');
|
||||
});
|
||||
|
||||
it('should handle multiple changes with various states', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
|
||||
// Complete change
|
||||
await fs.mkdir(path.join(changesDir, 'completed'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'completed', 'tasks.md'),
|
||||
'- [x] Task 1\n- [x] Task 2\n'
|
||||
);
|
||||
|
||||
// Partial change
|
||||
await fs.mkdir(path.join(changesDir, 'partial'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'partial', 'tasks.md'),
|
||||
'- [x] Done\n- [ ] Not done\n- [ ] Also not done\n'
|
||||
);
|
||||
|
||||
// No tasks
|
||||
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toContain('Changes:');
|
||||
expect(logOutput.some(line => line.includes('completed') && line.includes('✓ Complete'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('partial') && line.includes('1/3 tasks'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user