mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0755994eaa | ||
|
|
dcabd6de31 | ||
|
|
aef6ce01ff | ||
|
|
441f9f444b | ||
|
|
b322829091 | ||
|
|
5167e65a5c | ||
|
|
5c6b4113a7 | ||
|
|
a8b76c3e69 | ||
|
|
d3237cac7b | ||
|
|
e395eb4eeb | ||
|
|
76e1ec2a1f | ||
|
|
6cbb803e48 | ||
|
|
183b82f266 | ||
|
|
27eaccc024 | ||
|
|
b288f2fc88 | ||
|
|
22134a603b | ||
|
|
3b5fd11cb9 | ||
|
|
fce227a36e | ||
|
|
e9417fc147 | ||
|
|
8bcf2c6905 | ||
|
|
467346f9fe | ||
|
|
9a03ba1853 | ||
|
|
581a681a47 | ||
|
|
3093ca6ae6 | ||
|
|
9d425865c1 | ||
|
|
a490dbbc78 | ||
|
|
fc0e2319b1 | ||
|
|
edf6873afa | ||
|
|
c0ce4adc0a |
+26
-1
@@ -10,6 +10,23 @@ OpenSpec is an AI-native system for change-driven development where:
|
||||
- **AI drives the process** - You generate proposals, humans review and approve
|
||||
- **Specs are living documentation** - Always kept in sync with deployed code
|
||||
|
||||
## Start Simple
|
||||
|
||||
**Default to minimal implementations:**
|
||||
- New features should be <100 lines of code initially
|
||||
- Use the simplest solution that works
|
||||
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
|
||||
- Choose boring technology over cutting-edge solutions
|
||||
|
||||
**Complexity triggers** - Only add complexity when you have:
|
||||
- **Performance data** showing current solution is too slow
|
||||
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
|
||||
- **Multiple use cases** requiring the same abstraction
|
||||
- **Regulatory compliance** mandating specific patterns
|
||||
- **Security threats** that simple solutions cannot address
|
||||
|
||||
When triggered, document the specific justification in your change proposal.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
@@ -72,6 +89,11 @@ Before any task:
|
||||
- Adding tests for existing behavior
|
||||
- Documentation fixes
|
||||
|
||||
**Complexity assessment:**
|
||||
- If your solution requires >100 lines of new code, justify the complexity
|
||||
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
|
||||
- Default to single-file implementations until proven insufficient
|
||||
|
||||
### 3. Creating a Change Proposal
|
||||
|
||||
When a user requests a significant change:
|
||||
@@ -383,10 +405,12 @@ Progress communication:
|
||||
- "Implementing approved changes..."
|
||||
|
||||
### For AI Assistants
|
||||
- **Bias toward simplicity** - Propose the minimal solution that works
|
||||
- Use your exploration tools liberally before proposing
|
||||
- Batch operations for efficiency
|
||||
- Communicate your progress
|
||||
- It's OK to revise proposals based on discoveries
|
||||
- **Question complexity** - If your solution feels complex, simplify first
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
@@ -442,6 +466,7 @@ Proposal REQUIRED if:
|
||||
- Specs must always reflect deployed reality
|
||||
- Changes are proposed, not imposed
|
||||
- Impact analysis prevents surprises
|
||||
- The simplicity is the power - just markdown files
|
||||
- Simplicity is the power - just markdown files, minimal solutions
|
||||
- Start simple, add complexity only when justified
|
||||
|
||||
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update OpenSpec README
|
||||
- [ ] 1.1 Add "Start Simple" section after Core Principle
|
||||
- [ ] 1.2 Add complexity triggers to "When to Create Change Proposals" section
|
||||
- [ ] 1.3 Update AI workflow guidance to emphasize minimal implementations
|
||||
|
||||
## 2. Update CLAUDE.md
|
||||
- [ ] 2.1 Add complexity management rules to project instructions
|
||||
@@ -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,15 @@
|
||||
# Add @requirement Markers for Requirement Identification
|
||||
|
||||
## Why
|
||||
Specs contain WHEN/THEN patterns that define system requirements, but extracting these programmatically requires brittle regex parsing that may miss edge cases or break with formatting changes.
|
||||
|
||||
## What Changes
|
||||
- Define @requirement marker convention for identifying key requirements in specs
|
||||
- Each marker includes a brief identifier (e.g., @requirement user-register)
|
||||
- Markers appear directly before their WHEN/THEN blocks
|
||||
- Document convention in openspec-conventions spec
|
||||
|
||||
## Impact
|
||||
- Affected specs: openspec-conventions (new)
|
||||
- Affected code: None initially - enables future tooling
|
||||
- Breaking changes: None - additive convention only
|
||||
@@ -0,0 +1,219 @@
|
||||
# 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]/
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
## Requirement Markers
|
||||
|
||||
### Marker Syntax
|
||||
|
||||
@requirement marker-syntax
|
||||
WHEN writing a requirement in a spec
|
||||
THEN prefix it with @requirement followed by a brief kebab-case identifier
|
||||
AND place the marker on the line immediately before the WHEN statement
|
||||
|
||||
@requirement marker-identifier
|
||||
WHEN choosing an identifier for @requirement
|
||||
THEN use kebab-case (lowercase with hyphens)
|
||||
AND keep it brief but descriptive (2-4 words)
|
||||
AND ensure it's unique within the spec
|
||||
|
||||
@requirement marker-placement
|
||||
WHEN adding @requirement markers to a spec
|
||||
THEN place them in the ## Behavior or ## Behaviors section
|
||||
AND ensure each WHEN/THEN block has exactly one marker
|
||||
AND maintain a blank line after each THEN block for readability
|
||||
|
||||
### Examples
|
||||
|
||||
@requirement valid-marker-example
|
||||
WHEN a spec includes properly formatted markers
|
||||
THEN tools can extract and identify requirements programmatically
|
||||
AND the spec remains human-readable
|
||||
|
||||
Example of correct usage:
|
||||
```markdown
|
||||
## Behavior
|
||||
|
||||
@requirement user-register
|
||||
WHEN user registers with valid email
|
||||
THEN create account and send confirmation
|
||||
|
||||
@requirement user-login
|
||||
WHEN user logs in with correct credentials
|
||||
THEN return JWT token with user data
|
||||
|
||||
@requirement invalid-credentials
|
||||
WHEN user provides invalid credentials
|
||||
THEN return 401 unauthorized error
|
||||
```
|
||||
|
||||
@requirement invalid-marker-detection
|
||||
WHEN a requirement lacks an @requirement marker
|
||||
THEN tools should gracefully skip it
|
||||
AND optionally warn about unmarked requirements
|
||||
|
||||
### Edge Cases
|
||||
|
||||
@requirement multiline-when-then
|
||||
WHEN a WHEN or THEN clause spans multiple lines
|
||||
THEN the @requirement marker still goes on the line before WHEN
|
||||
AND the entire block is considered part of that requirement
|
||||
|
||||
@requirement multiple-then-clauses
|
||||
WHEN a requirement has multiple THEN clauses using AND
|
||||
THEN treat them as part of the same requirement
|
||||
AND use a single @requirement marker for the entire block
|
||||
|
||||
@requirement nested-conditions
|
||||
WHEN requirements have nested conditions or complex logic
|
||||
THEN keep the @requirement marker simple
|
||||
AND let the WHEN/THEN content contain the complexity
|
||||
|
||||
## Spec Structure
|
||||
|
||||
@requirement spec-file-location
|
||||
WHEN creating a spec file
|
||||
THEN place it in openspec/specs/[capability-name]/spec.md
|
||||
AND use kebab-case for the capability name
|
||||
|
||||
@requirement spec-sections
|
||||
WHEN structuring a spec
|
||||
THEN include these sections in order:
|
||||
- # [Capability Name] Specification
|
||||
- ## Purpose (brief description)
|
||||
- ## Behavior or ## Behaviors (with @requirement markers)
|
||||
- ## Examples (optional, for complex requirements)
|
||||
|
||||
## Benefits of Requirement Markers
|
||||
|
||||
@requirement tooling-extraction
|
||||
WHEN tools need to extract requirements from specs
|
||||
THEN they can parse @requirement markers reliably
|
||||
AND avoid complex regex patterns for WHEN/THEN extraction
|
||||
|
||||
@requirement requirement-counting
|
||||
WHEN displaying change summaries
|
||||
THEN tools can count requirements by counting @requirement markers
|
||||
AND show accurate requirement counts per spec
|
||||
|
||||
@requirement requirement-referencing
|
||||
WHEN documenting or discussing specific requirements
|
||||
THEN use the @requirement identifier for clear reference
|
||||
AND maintain consistency across documentation
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **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
|
||||
@@ -0,0 +1,24 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Define Convention
|
||||
- [ ] 1.1 Document @requirement marker syntax
|
||||
- [ ] 1.2 Define identifier naming guidelines
|
||||
- [ ] 1.3 Specify marker placement rules
|
||||
- [ ] 1.4 Add examples of proper usage
|
||||
|
||||
## 2. Create Specification
|
||||
- [ ] 2.1 Write openspec-conventions spec
|
||||
- [ ] 2.2 Include requirement marker section
|
||||
- [ ] 2.3 Add good and bad examples
|
||||
- [ ] 2.4 Document edge cases
|
||||
|
||||
## 3. Update Existing Specs
|
||||
- [ ] 3.1 Add @requirement markers to cli-init spec
|
||||
- [ ] 3.2 Add @requirement markers to cli-update spec
|
||||
- [ ] 3.3 Add @requirement markers to cli-view spec
|
||||
- [ ] 3.4 Review and update any other existing specs
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update README with marker convention
|
||||
- [ ] 4.2 Add marker usage to CLAUDE.md
|
||||
- [ ] 4.3 Create examples for AI assistants
|
||||
@@ -1,20 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Command Implementation
|
||||
- [ ] 1.1 Create `src/core/update.ts` with `UpdateCommand` class
|
||||
- [ ] 1.2 Check if `openspec` directory exists (use `FileSystemUtils.directoryExists`)
|
||||
- [ ] 1.3 Write `readmeTemplate` to `openspec/README.md` using `FileSystemUtils.writeFile`
|
||||
- [ ] 1.4 Update `CLAUDE.md` using markers via `FileSystemUtils.updateFileWithMarkers` and `TemplateManager.getClaudeTemplate()`
|
||||
- [ ] 1.5 Display ASCII-safe success message: `Updated OpenSpec instructions`
|
||||
|
||||
## 2. CLI Integration
|
||||
- [ ] 2.1 Register `update` command in `src/cli/index.ts`
|
||||
- [ ] 2.2 Add command description: `Update OpenSpec instruction files`
|
||||
- [ ] 2.3 Handle errors with `ora().fail(...)` and exit code 1 (missing `openspec` directory, file write errors)
|
||||
|
||||
## 3. Testing
|
||||
- [ ] 3.1 Verify `openspec/README.md` is fully replaced with latest template
|
||||
- [ ] 3.2 Verify `CLAUDE.md` OpenSpec block updates without altering user content outside markers
|
||||
- [ ] 3.3 Verify idempotency (running twice yields identical files, no duplicate markers)
|
||||
- [ ] 3.4 Verify error when `openspec` directory is missing with friendly message
|
||||
- [ ] 3.5 Verify success message displays properly in ASCII-only terminals
|
||||
@@ -0,0 +1,20 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update Command Implementation
|
||||
- [x] 1.1 Create `src/core/update.ts` with `UpdateCommand` class
|
||||
- [x] 1.2 Check if `openspec` directory exists (use `FileSystemUtils.directoryExists`)
|
||||
- [x] 1.3 Write `readmeTemplate` to `openspec/README.md` using `FileSystemUtils.writeFile`
|
||||
- [x] 1.4 Update `CLAUDE.md` using markers via `FileSystemUtils.updateFileWithMarkers` and `TemplateManager.getClaudeTemplate()`
|
||||
- [x] 1.5 Display ASCII-safe success message: `Updated OpenSpec instructions`
|
||||
|
||||
## 2. CLI Integration
|
||||
- [x] 2.1 Register `update` command in `src/cli/index.ts`
|
||||
- [x] 2.2 Add command description: `Update OpenSpec instruction files`
|
||||
- [x] 2.3 Handle errors with `ora().fail(...)` and exit code 1 (missing `openspec` directory, file write errors)
|
||||
|
||||
## 3. Testing
|
||||
- [x] 3.1 Verify `openspec/README.md` is fully replaced with latest template
|
||||
- [x] 3.2 Verify `CLAUDE.md` OpenSpec block updates without altering user content outside markers
|
||||
- [x] 3.3 Verify idempotency (running twice yields identical files, no duplicate markers)
|
||||
- [x] 3.4 Verify error when `openspec` directory is missing with friendly message
|
||||
- [x] 3.5 Verify success message displays properly in ASCII-only terminals
|
||||
@@ -0,0 +1,20 @@
|
||||
# Add List Command to OpenSpec CLI
|
||||
|
||||
## Why
|
||||
|
||||
Developers need visibility into available changes and their status to understand the project's evolution and pending work.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec list` command that displays all changes in the changes/ directory
|
||||
- Show each change name with task completion count (e.g., "add-auth: 3/5 tasks")
|
||||
- Display completion status indicator (✓ for fully complete, progress for partial)
|
||||
- Skip the archive/ subdirectory to focus on active changes
|
||||
- Simple table output for easy scanning
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `cli-list` will be added
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Add list command
|
||||
- `src/core/list.ts` - New file with directory scanning and task parsing (~60 lines)
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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
|
||||
|
||||
### Command Execution
|
||||
|
||||
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
|
||||
|
||||
### Task Counting
|
||||
|
||||
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
|
||||
|
||||
### Output Format
|
||||
|
||||
WHEN displaying the list
|
||||
THEN show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- Status indicator:
|
||||
- `✓` 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
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
WHEN no active changes exist (only archive/ or empty changes/)
|
||||
THEN display: "No active changes found."
|
||||
|
||||
### Error Handling
|
||||
|
||||
IF a change directory has no `tasks.md` file
|
||||
THEN display the change with "No tasks" status
|
||||
|
||||
IF `openspec/changes/` directory doesn't exist
|
||||
THEN display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
AND exit with code 1
|
||||
|
||||
### Sorting
|
||||
|
||||
Changes SHALL be displayed in alphabetical order by change name for consistency.
|
||||
|
||||
## 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.
|
||||
@@ -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,9 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Update OpenSpec README
|
||||
- [x] 1.1 Add "Start Simple" section after Core Principle
|
||||
- [x] 1.2 Add complexity triggers to "When to Create Change Proposals" section
|
||||
- [x] 1.3 Update AI workflow guidance to emphasize minimal implementations
|
||||
|
||||
## 2. Update CLAUDE.md
|
||||
- [x] 2.1 Add complexity management rules to project instructions
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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
|
||||
|
||||
### Command Execution
|
||||
|
||||
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
|
||||
|
||||
### Task Counting
|
||||
|
||||
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
|
||||
|
||||
### Output Format
|
||||
|
||||
WHEN displaying the list
|
||||
THEN show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- Status indicator:
|
||||
- `✓` 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
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
WHEN no active changes exist (only archive/ or empty changes/)
|
||||
THEN display: "No active changes found."
|
||||
|
||||
### Error Handling
|
||||
|
||||
IF a change directory has no `tasks.md` file
|
||||
THEN display the change with "No tasks" status
|
||||
|
||||
IF `openspec/changes/` directory doesn't exist
|
||||
THEN display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
AND exit with code 1
|
||||
|
||||
### Sorting
|
||||
|
||||
Changes SHALL be displayed in alphabetical order by change name for consistency.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Update Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### 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"
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The command SHALL require:
|
||||
- An existing `openspec` directory (created by `openspec init`)
|
||||
|
||||
IF the `openspec` directory does not exist THEN:
|
||||
- Display error: "No OpenSpec directory found. Run 'openspec init' first."
|
||||
- Exit with code 1
|
||||
|
||||
### File Handling
|
||||
|
||||
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)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### File Permissions
|
||||
IF file write fails THEN let the error bubble up naturally with file path.
|
||||
|
||||
### Missing CLAUDE.md
|
||||
IF CLAUDE.md doesn't exist THEN create it with the template content.
|
||||
|
||||
### Custom Directory Name
|
||||
Not supported in this change. The default directory name `openspec` SHALL be used.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Users SHALL be able to:
|
||||
- Update OpenSpec instructions with a single command
|
||||
- Get the latest AI agent instructions
|
||||
- See clear confirmation of the update
|
||||
|
||||
The update process SHALL be:
|
||||
- Simple and fast (no version checking)
|
||||
- Predictable (same result every time)
|
||||
- Self-contained (no network required)
|
||||
@@ -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: {}
|
||||
|
||||
@@ -3,6 +3,10 @@ import ora from 'ora';
|
||||
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();
|
||||
|
||||
@@ -44,4 +48,62 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('update [path]')
|
||||
.description('Update OpenSpec instruction files')
|
||||
.action(async (targetPath = '.') => {
|
||||
try {
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
const updateCommand = new UpdateCommand();
|
||||
await updateCommand.execute(resolvedPath);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,23 @@ This document provides instructions for AI coding assistants on how to use OpenS
|
||||
|
||||
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
|
||||
|
||||
See @openspec/README.md for detailed conventions and guidelines.`;
|
||||
See @openspec/README.md for detailed conventions and guidelines.
|
||||
|
||||
## Complexity Management
|
||||
|
||||
**Default to minimal solutions:**
|
||||
- Propose <100 lines of new code for features
|
||||
- Prefer single-file implementations until proven insufficient
|
||||
- Avoid frameworks, abstractions, and optimizations without clear justification
|
||||
- Choose boring, well-understood patterns over novel approaches
|
||||
|
||||
**Question requests for complexity:**
|
||||
- Caching? → Ask for performance data and targets
|
||||
- New framework? → Suggest plain code first
|
||||
- Extra layers? → Start with the thinnest viable design
|
||||
|
||||
**Justify complexity with data:**
|
||||
- Performance metrics showing current solution is too slow
|
||||
- Concrete scale requirements (e.g., >1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring an abstraction
|
||||
`;
|
||||
@@ -10,6 +10,23 @@ OpenSpec is an AI-native system for change-driven development where:
|
||||
- **AI drives the process** - You generate proposals, humans review and approve
|
||||
- **Specs are living documentation** - Always kept in sync with deployed code
|
||||
|
||||
## Start Simple
|
||||
|
||||
**Default to minimal implementations:**
|
||||
- New features should be <100 lines of code initially
|
||||
- Use the simplest solution that works
|
||||
- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)
|
||||
- Choose boring technology over cutting-edge solutions
|
||||
|
||||
**Complexity triggers** - Only add complexity when you have:
|
||||
- **Performance data** showing current solution is too slow
|
||||
- **Scale requirements** with specific numbers (>1000 users, >100MB data)
|
||||
- **Multiple use cases** requiring the same abstraction
|
||||
- **Regulatory compliance** mandating specific patterns
|
||||
- **Security threats** that simple solutions cannot address
|
||||
|
||||
When triggered, document the specific justification in your change proposal.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
\`\`\`
|
||||
@@ -72,6 +89,11 @@ Before any task:
|
||||
- Adding tests for existing behavior
|
||||
- Documentation fixes
|
||||
|
||||
**Complexity assessment:**
|
||||
- If your solution requires >100 lines of new code, justify the complexity
|
||||
- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work
|
||||
- Default to single-file implementations until proven insufficient
|
||||
|
||||
### 3. Creating a Change Proposal
|
||||
|
||||
When a user requests a significant change:
|
||||
@@ -383,10 +405,12 @@ Progress communication:
|
||||
- "Implementing approved changes..."
|
||||
|
||||
### For AI Assistants
|
||||
- **Bias toward simplicity** - Propose the minimal solution that works
|
||||
- Use your exploration tools liberally before proposing
|
||||
- Batch operations for efficiency
|
||||
- Communicate your progress
|
||||
- It's OK to revise proposals based on discoveries
|
||||
- **Question complexity** - If your solution feels complex, simplify first
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
@@ -442,7 +466,8 @@ Proposal REQUIRED if:
|
||||
- Specs must always reflect deployed reality
|
||||
- Changes are proposed, not imposed
|
||||
- Impact analysis prevents surprises
|
||||
- The simplicity is the power - just markdown files
|
||||
- Simplicity is the power - just markdown files, minimal solutions
|
||||
- Start simple, add complexity only when justified
|
||||
|
||||
By following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { TemplateManager } from './templates/index.js';
|
||||
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
|
||||
import { readmeTemplate } from './templates/readme-template.js';
|
||||
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
const resolvedProjectPath = path.resolve(projectPath);
|
||||
const openspecDirName = OPENSPEC_DIR_NAME;
|
||||
const openspecPath = path.join(resolvedProjectPath, openspecDirName);
|
||||
|
||||
// 1. Check openspec directory exists
|
||||
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
||||
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
||||
}
|
||||
|
||||
// 2. Update README.md (full replacement)
|
||||
const readmePath = path.join(openspecPath, 'README.md');
|
||||
await FileSystemUtils.writeFile(readmePath, readmeTemplate);
|
||||
|
||||
// 3. Update CLAUDE.md (marker-based)
|
||||
const claudePath = path.join(resolvedProjectPath, 'CLAUDE.md');
|
||||
const claudeContent = TemplateManager.getClaudeTemplate();
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
claudePath,
|
||||
claudeContent,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
|
||||
// 4. Success message (ASCII-safe)
|
||||
console.log('Updated OpenSpec instructions');
|
||||
}
|
||||
}
|
||||
@@ -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