mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-10 09:22:32 +08:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82ba1f504e | ||
|
|
d54fcc97f2 | ||
|
|
ebff738860 | ||
|
|
1bdaeef4da | ||
|
|
2921676e93 | ||
|
|
792129bfe3 | ||
|
|
715ff513e3 | ||
|
|
f6913b7661 | ||
|
|
730bbc00af | ||
|
|
adfcc65b9f | ||
|
|
590541277d | ||
|
|
14e2cc628a | ||
|
|
299171c5cb | ||
|
|
bb6aae0205 | ||
|
|
23c0ab6358 | ||
|
|
02fe5b3547 | ||
|
|
5c6ae8e407 |
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 OpenSpec Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,119 +1,270 @@
|
||||
# OpenSpec
|
||||
|
||||
A specification-driven development system for maintaining living documentation alongside your code.
|
||||
[](https://github.com/Fission-AI/OpenSpec/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/@fission-ai/openspec)
|
||||
[](https://nodejs.org/)
|
||||
[](./LICENSE)
|
||||
[](https://conventionalcommits.org)
|
||||
|
||||
**Supported AI Tools:** ✅ Claude Code | 🔜 Cursor (coming soon) | 🔜 AGENTS.md support (coming soon)
|
||||
|
||||
Create **alignment** between humans and AI coding assistants through spec-driven development. **No API keys required.**
|
||||
|
||||
OpenSpec ensures you and your AI assistant agree on what to build before any code is written. By discussing and refining specifications first, you bring determinism to AI code generation, getting exactly what you want, not what the AI thinks you might want.
|
||||
|
||||
## Why OpenSpec?
|
||||
|
||||
**The Problem:** AI coding assistants are powerful but unpredictable. Without clear specifications, they generate code based on assumptions, often missing requirements or adding unwanted features. Teams waste time in review cycles because humans and AI aren't aligned on what to build.
|
||||
|
||||
**The Solution:** OpenSpec creates alignment BEFORE code is written:
|
||||
- **Human-AI Alignment** - You and your AI agree on specifications before implementation
|
||||
- **Deterministic Output** - Clear specs lead to predictable code generation
|
||||
- **Team Alignment** - Everyone reviews specs, not code surprises
|
||||
- **Focus on What, Not How** - Define requirements while AI handles implementation
|
||||
- **Living Documentation** - Specs evolve with your code as a natural byproduct
|
||||
|
||||
## What You Get
|
||||
|
||||
- **Alignment First** - Ensure humans and AI agree on what to build before writing code
|
||||
- **Predictable AI Output** - Turn non-deterministic AI into a reliable development partner
|
||||
- **Universal Tool Support** - Works with any AI assistant - Claude Code, Cursor, or future tools
|
||||
- **No API Keys Required** - Integrates through context rules, not external services
|
||||
- **Spec-Level Reviews** - Teams review intentions, not implementation details
|
||||
- **Clear Feature Scope** - Know exactly what you're building and what you're not
|
||||
- **Progress Tracking** - See what's proposed, in progress, or completed at a glance
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ SPECS │ │ CHANGES │ │ ARCHIVE │
|
||||
│ (Truth) │◀──────│ (Proposals) │──────▶│ (Completed) │
|
||||
└─────────────┘ └─────────────┘ └──────────────┘
|
||||
▲ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
└───────────────│ CODE │◀──────────────┘
|
||||
└─────────────┘
|
||||
|
||||
1. SPECS define current capabilities (what IS built)
|
||||
2. CHANGES propose modifications using deltas (what SHOULD change)
|
||||
3. CODE implements the changes following tasks
|
||||
4. ARCHIVE preserves completed changes after deployment
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 20.19.0
|
||||
|
||||
### Install OpenSpec
|
||||
|
||||
Install globally:
|
||||
|
||||
```bash
|
||||
npm install -g openspec
|
||||
npm install -g @fission-ai/openspec
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Getting Started
|
||||
|
||||
### 1. Initialize OpenSpec in Your Project
|
||||
|
||||
```bash
|
||||
# Initialize OpenSpec in your project
|
||||
# Navigate to your project
|
||||
cd my-project
|
||||
|
||||
# Initialize OpenSpec
|
||||
openspec init
|
||||
|
||||
# Update existing OpenSpec instructions (team-friendly)
|
||||
openspec update
|
||||
# Select your AI tool (more coming soon!):
|
||||
# "Which AI tool do you use?"
|
||||
# > Claude Code
|
||||
# Cursor (coming soon)
|
||||
|
||||
# List specs or changes
|
||||
openspec spec list # specs (IDs by default; use --long for details)
|
||||
openspec change list # changes (IDs by default; use --long for details)
|
||||
|
||||
# Show differences between specs and proposed changes
|
||||
openspec diff [change-name]
|
||||
|
||||
# Archive completed changes
|
||||
openspec archive [change-name]
|
||||
# This creates:
|
||||
# openspec/
|
||||
# ├── specs/ # Current specifications (truth)
|
||||
# ├── changes/ # Proposed changes
|
||||
# └── README.md # AI instructions for your tool
|
||||
```
|
||||
|
||||
## Commands
|
||||
### 2. Create Your First Change
|
||||
|
||||
### `openspec init`
|
||||
Jump straight into creating a change proposal with your AI assistant (works with Claude Code, Cursor, or any AI tool):
|
||||
|
||||
Initializes OpenSpec in your project by creating:
|
||||
- `openspec/` directory structure
|
||||
- `openspec/README.md` with OpenSpec instructions
|
||||
- AI tool configuration files (based on your selection)
|
||||
```markdown
|
||||
// Quick win - Add a simple new feature:
|
||||
You: "I want to add a user profile API endpoint.
|
||||
Please create an OpenSpec change proposal for this."
|
||||
|
||||
### `openspec update`
|
||||
AI: "I'll create an OpenSpec change proposal for the user profile API..."
|
||||
*Creates openspec/changes/add-user-profile-api/ with:*
|
||||
- proposal.md (why this feature is needed)
|
||||
- tasks.md (implementation checklist)
|
||||
- design.md (API design decisions)
|
||||
- specs/user-profile/spec.md (new requirements)
|
||||
|
||||
Updates OpenSpec instructions to the latest version. This command is **team-friendly** and only updates files that already exist:
|
||||
You: "The proposal looks good. Let's implement it."
|
||||
|
||||
- Always updates `openspec/README.md` with the latest OpenSpec instructions
|
||||
- **Only updates existing AI tool configuration files** (e.g., CLAUDE.md, CURSOR.md)
|
||||
- **Never creates new AI tool configuration files**
|
||||
- Preserves content outside of OpenSpec markers in AI tool files
|
||||
AI: "Following the tasks in openspec/changes/add-user-profile-api/tasks.md:
|
||||
Task 1.1: Create user profile model..."
|
||||
*Implements each task systematically*
|
||||
```
|
||||
|
||||
This allows team members to use different AI tools without conflicts. Each developer can maintain their preferred AI tool configuration file, and `openspec update` will respect their choice.
|
||||
### 3. Track Your Work
|
||||
|
||||
### `openspec spec`
|
||||
```bash
|
||||
# View active changes (what's being worked on)
|
||||
openspec list
|
||||
|
||||
Manage and view specifications.
|
||||
# Validate your changes are properly formatted
|
||||
openspec validate add-2fa --strict
|
||||
|
||||
Examples:
|
||||
- `openspec spec show <spec-id>`
|
||||
- Text mode: prints raw `spec.md` content
|
||||
- JSON mode (`--json`): returns minimal, stable shape
|
||||
- Filters are JSON-only: `--requirements`, `--no-scenarios`, `-r/--requirement <1-based>`
|
||||
- `openspec spec list`
|
||||
- Prints IDs only by default
|
||||
- Use `--long` to include `title` and `[requirements N]`
|
||||
- `openspec spec validate <spec-id>`
|
||||
- Text: human-readable summary to stdout/stderr
|
||||
- `--json` for structured report
|
||||
# After deployment, archive the completed change
|
||||
openspec archive add-2fa
|
||||
# This moves the change to archive/ and updates specs/
|
||||
```
|
||||
|
||||
### `openspec change`
|
||||
## Common Commands
|
||||
|
||||
Manage and view change proposals.
|
||||
```bash
|
||||
# Most used:
|
||||
openspec list # See what changes you're working on
|
||||
openspec archive <change> # Mark a change as complete after deployment
|
||||
|
||||
Examples:
|
||||
- `openspec change show <change-id>`
|
||||
- Text mode: prints raw `proposal.md` content
|
||||
- JSON mode (`--json`): `{ id, title, deltaCount, deltas }`
|
||||
- Filtering is JSON-only: `--deltas-only` (alias: `--requirements-only`, deprecated)
|
||||
- `openspec change list`
|
||||
- Prints IDs only by default
|
||||
- Use `--long` to include `title` and counts `[deltas N] [tasks x/y]`
|
||||
- `openspec change validate <change-id>`
|
||||
- Text: human-readable result
|
||||
- `--json` for structured report
|
||||
# Also useful:
|
||||
openspec validate <change> # Check formatting before committing
|
||||
openspec show <change> # View change details
|
||||
```
|
||||
|
||||
### `openspec diff [change-name]`
|
||||
## Example: How AI Creates OpenSpec Files
|
||||
|
||||
Shows the differences between current specs and proposed changes:
|
||||
- Displays a unified diff format
|
||||
- Helps review what will change before implementation
|
||||
- Useful for pull request reviews
|
||||
When you ask your AI assistant to "add two-factor authentication", it creates:
|
||||
|
||||
### `openspec archive [change-name]`
|
||||
```
|
||||
openspec/
|
||||
├── specs/
|
||||
│ └── auth/
|
||||
│ └── spec.md # Current auth spec (if exists)
|
||||
└── changes/
|
||||
└── add-2fa/ # AI creates this entire structure
|
||||
├── proposal.md # Why and what changes
|
||||
├── tasks.md # Implementation checklist
|
||||
├── design.md # Technical decisions (optional)
|
||||
└── specs/
|
||||
└── auth/
|
||||
└── spec.md # Delta showing additions
|
||||
```
|
||||
|
||||
Archives a completed change:
|
||||
- Moves change from `openspec/changes/` to `openspec/changes/archive/`
|
||||
- Adds a date prefix to the archived change
|
||||
- Updates specs to reflect the new state
|
||||
- Use `--skip-specs` to archive without updating specs (for abandoned changes)
|
||||
### AI-Generated Spec (created in `openspec/specs/auth/spec.md`):
|
||||
|
||||
## Team Collaboration
|
||||
```markdown
|
||||
# Auth Specification
|
||||
|
||||
OpenSpec is designed for team collaboration:
|
||||
## Purpose
|
||||
Authentication and session management.
|
||||
|
||||
## Requirements
|
||||
### Requirement: User Authentication
|
||||
The system SHALL issue a JWT on successful login.
|
||||
|
||||
#### Scenario: Valid credentials
|
||||
- WHEN a user submits valid credentials
|
||||
- THEN a JWT is returned
|
||||
```
|
||||
|
||||
### AI-Generated Change Delta (created in `openspec/changes/add-2fa/specs/auth/spec.md`):
|
||||
|
||||
```markdown
|
||||
# Delta for Auth
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
The system MUST require a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- WHEN a user submits valid credentials
|
||||
- THEN an OTP challenge is required
|
||||
```
|
||||
|
||||
### AI-Generated Tasks (created in `openspec/changes/add-2fa/tasks.md`):
|
||||
|
||||
```markdown
|
||||
## 1. Database Setup
|
||||
- [ ] 1.1 Add OTP secret column to users table
|
||||
- [ ] 1.2 Create OTP verification logs table
|
||||
|
||||
## 2. Backend Implementation
|
||||
- [ ] 2.1 Add OTP generation endpoint
|
||||
- [ ] 2.2 Modify login flow to require OTP
|
||||
- [ ] 2.3 Add OTP verification endpoint
|
||||
|
||||
## 3. Frontend Updates
|
||||
- [ ] 3.1 Create OTP input component
|
||||
- [ ] 3.2 Update login flow UI
|
||||
```
|
||||
|
||||
**Important:** You don't create these files manually. Your AI assistant generates them based on your requirements and the existing codebase.
|
||||
|
||||
## Understanding OpenSpec Files
|
||||
|
||||
### Delta Format
|
||||
|
||||
Deltas are "patches" that show how specs change:
|
||||
|
||||
- **`## ADDED Requirements`** - New capabilities
|
||||
- **`## MODIFIED Requirements`** - Changed behavior (include complete updated text)
|
||||
- **`## REMOVED Requirements`** - Deprecated features
|
||||
|
||||
**Format requirements:**
|
||||
- Use `### Requirement: <name>` for headers
|
||||
- Every requirement needs at least one `#### Scenario:` block
|
||||
- Use SHALL/MUST in requirement text
|
||||
|
||||
|
||||
## Why OpenSpec Works
|
||||
|
||||
OpenSpec creates **alignment** between you and your AI coding assistant:
|
||||
|
||||
1. **You describe** what you want to build
|
||||
2. **AI creates specs** before writing any code
|
||||
3. **You review and adjust** the specifications
|
||||
4. **AI implements** exactly what was specified
|
||||
5. **Everyone understands** what's being built through clear specs
|
||||
|
||||
**True Interoperability:** OpenSpec is designed to be universal. No API keys, no vendor lock-in. It works by adding context rules to ANY AI coding tool - whether you use Claude Code today, switch to Cursor tomorrow, or adopt the next breakthrough AI assistant. Your specs remain portable and your workflow stays consistent.
|
||||
|
||||
|
||||
## How OpenSpec Compares
|
||||
|
||||
### vs. Kiro.dev
|
||||
OpenSpec groups all changes for a feature in one place (`openspec/changes/feature-name/`), making it easy to track what needs to be done. Kiro spreads changes across multiple spec folders, making feature tracking harder.
|
||||
|
||||
### vs. No Specs
|
||||
Without specs, AI coding assistants generate code based on vague prompts, often missing requirements or adding unwanted features. OpenSpec ensures alignment before any code is written.
|
||||
|
||||
## Team Adoption
|
||||
|
||||
### Getting Started with Your Team
|
||||
|
||||
1. **Initialize OpenSpec** - Run `openspec init` in your project
|
||||
2. **Start with new features** - Use OpenSpec for your next change proposal
|
||||
3. **Build incrementally** - Each new feature adds to your spec library
|
||||
4. **Future capability** - We're working on tools to generate specs from existing code
|
||||
|
||||
**Tool Freedom:** Your team can use different AI assistants. One developer might use Claude Code while another uses Cursor - OpenSpec keeps everyone aligned through shared specifications. Run `openspec update` to configure for any supported tool without affecting others.
|
||||
|
||||
1. **AI Tool Flexibility**: Each team member can use their preferred AI assistant (Claude, Cursor, etc.)
|
||||
2. **Non-Invasive Updates**: The `update` command only modifies existing files, never forcing tools on team members
|
||||
3. **Specification Sharing**: The `openspec/` directory contains shared specifications that all team members work from
|
||||
4. **Change Tracking**: Proposed changes are visible to all team members for review before implementation
|
||||
|
||||
## Contributing
|
||||
|
||||
See `openspec/specs/` for the current system specifications and `openspec/changes/` for pending improvements.
|
||||
|
||||
## Notes
|
||||
|
||||
- The legacy `openspec list` command is deprecated. Use `openspec spec list` and `openspec change list`.
|
||||
- Text output is raw-first (no formatting or filtering). Prefer `--json` for tooling-friendly output.
|
||||
- Global `--no-color` disables ANSI colors and respects `NO_COLOR`.
|
||||
- Install dependencies: `npm install`
|
||||
- Build: `npm run build`
|
||||
- Test: `npm test`
|
||||
- Develop CLI locally: `npm run dev` or `npm run dev:cli`
|
||||
- Conventional commits (one-line): `type(scope): subject`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ After deployment, create separate PR to:
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec diff [change] # Show spec differences
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive [change] # Archive after deployment
|
||||
|
||||
@@ -290,7 +289,6 @@ Only add complexity with:
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec diff [change] # What's changing?
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive [change] # Mark complete
|
||||
```
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Remove Diff Command
|
||||
|
||||
## Problem
|
||||
|
||||
The `openspec diff` command adds unnecessary complexity to the OpenSpec CLI for several reasons:
|
||||
|
||||
1. **Redundant functionality**: The `openspec show` command already provides comprehensive visualization of changes through structured JSON output and markdown rendering
|
||||
2. **Maintenance burden**: The diff command requires a separate dependency (jest-diff) and additional code complexity (~227 lines)
|
||||
3. **Limited value**: Developers can achieve better diff visualization using existing tools:
|
||||
- Git diff for actual file changes
|
||||
- The `show` command for structured change viewing
|
||||
- Standard diff utilities for comparing spec files directly
|
||||
4. **Inconsistent with verb-noun pattern**: The command doesn't follow the preferred verb-first command structure that other commands are migrating to
|
||||
|
||||
## Solution
|
||||
|
||||
Remove the `openspec diff` command entirely and guide users to more appropriate alternatives:
|
||||
|
||||
1. **For viewing change content**: Use `openspec show <change-name>` which provides:
|
||||
- Structured JSON output with `--json` flag
|
||||
- Markdown rendering for human-readable format
|
||||
- Delta-only views with `--deltas-only` flag
|
||||
- Full spec content visualization
|
||||
|
||||
2. **For comparing files**: Use standard tools:
|
||||
- `git diff` for version control comparisons
|
||||
- System diff utilities for file-by-file comparisons
|
||||
- IDE diff viewers for visual comparisons
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Reduced complexity**: Removes ~227 lines of code and the jest-diff dependency
|
||||
- **Clearer user journey**: Directs users to the canonical `show` command for viewing changes
|
||||
- **Lower maintenance**: Fewer commands to maintain and test
|
||||
- **Better alignment**: Focuses on the core OpenSpec workflow without redundant features
|
||||
|
||||
## Implementation
|
||||
|
||||
### Files to Remove
|
||||
- `/src/core/diff.ts` - The entire diff command implementation
|
||||
- `/openspec/specs/cli-diff/spec.md` - The diff command specification
|
||||
|
||||
### Files to Update
|
||||
- `/src/cli/index.ts` - Remove diff command registration (lines 8, 84-96)
|
||||
- `/package.json` - Remove jest-diff dependency
|
||||
- `/README.md` - Remove diff command documentation
|
||||
- `/openspec/README.md` - Remove diff command references
|
||||
- Various documentation files mentioning `openspec diff`
|
||||
|
||||
### Migration Guide for Users
|
||||
|
||||
Users currently using `openspec diff` should transition to:
|
||||
|
||||
```bash
|
||||
# Before
|
||||
openspec diff add-feature
|
||||
|
||||
# After - view the change proposal
|
||||
openspec show add-feature
|
||||
|
||||
# After - view only the deltas
|
||||
openspec show add-feature --json --deltas-only
|
||||
|
||||
# After - use git for file comparisons
|
||||
git diff openspec/specs openspec/changes/add-feature/specs
|
||||
```
|
||||
|
||||
## Risks
|
||||
|
||||
- **User disruption**: Existing users may have workflows depending on the diff command
|
||||
- Mitigation: Provide clear migration guide and deprecation period
|
||||
|
||||
- **Loss of visual diff**: The colored, unified diff format will no longer be available
|
||||
- Mitigation: Users can use git diff or other tools for visual comparisons
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Successful removal with no broken dependencies
|
||||
- Documentation updated to reflect the change
|
||||
- Tests passing without the diff command
|
||||
- Reduced package size from removing jest-diff dependency
|
||||
@@ -0,0 +1,41 @@
|
||||
# Remove Diff Command - Tasks
|
||||
|
||||
## 1. Remove Core Implementation
|
||||
- [x] Delete `/src/core/diff.ts`
|
||||
- [x] Remove DiffCommand import from `/src/cli/index.ts`
|
||||
- [x] Remove diff command registration from CLI
|
||||
|
||||
## 2. Remove Specifications
|
||||
- [x] Delete `/openspec/specs/cli-diff/spec.md`
|
||||
- [x] Archive the spec for historical reference if needed
|
||||
|
||||
## 3. Update Dependencies
|
||||
- [x] Remove jest-diff from package.json dependencies
|
||||
- [x] Run pnpm install to update lock file
|
||||
|
||||
## 4. Update Documentation
|
||||
- [x] Update main README.md to remove diff command references
|
||||
- [x] Update openspec/README.md to remove diff command from command list
|
||||
- [x] Update CLAUDE.md template if it mentions diff command
|
||||
- [x] Update any example workflows that use diff command
|
||||
|
||||
## 5. Update Related Files
|
||||
- [x] Search and update any remaining references to "openspec diff" in:
|
||||
- Template files
|
||||
- Test files (if any exist for diff command)
|
||||
- Archive documentation
|
||||
- Change proposals
|
||||
|
||||
## 6. Add Deprecation Notice (Optional Phase)
|
||||
- [ ] Consider adding a deprecation warning before full removal
|
||||
- [ ] Provide helpful message directing users to `openspec show` command
|
||||
|
||||
## 7. Testing
|
||||
- [x] Ensure all tests pass after removal
|
||||
- [x] Verify CLI help text no longer shows diff command
|
||||
- [x] Test that show command provides adequate replacement functionality
|
||||
|
||||
## 8. Documentation of Alternative Workflows
|
||||
- [x] Document how to use `openspec show` for viewing changes
|
||||
- [x] Document how to use git diff for file comparisons
|
||||
- [x] Add migration guide to help text or documentation
|
||||
@@ -1,123 +0,0 @@
|
||||
# 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]
|
||||
```
|
||||
## Requirements
|
||||
### 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 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: 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"
|
||||
|
||||
### 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
|
||||
|
||||
### Requirement: Diff Command Enhancement
|
||||
|
||||
The diff command SHALL validate change structure before displaying differences.
|
||||
|
||||
#### Scenario: Validate before diff
|
||||
|
||||
- **WHEN** executing `openspec diff change-name`
|
||||
- **THEN** validate change structure
|
||||
- **AND** show validation warnings if present
|
||||
- **AND** continue with diff display
|
||||
|
||||
## 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):
|
||||
```
|
||||
@@ -63,7 +63,6 @@
|
||||
"@inquirer/prompts": "^7.8.0",
|
||||
"chalk": "^5.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"jest-diff": "^30.0.5",
|
||||
"ora": "^8.2.0",
|
||||
"zod": "^4.0.17"
|
||||
}
|
||||
|
||||
Generated
-83
@@ -17,9 +17,6 @@ importers:
|
||||
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
|
||||
@@ -390,18 +387,6 @@ 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==}
|
||||
|
||||
@@ -526,9 +511,6 @@ 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==}
|
||||
|
||||
@@ -598,10 +580,6 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
@@ -629,10 +607,6 @@ 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}
|
||||
@@ -793,10 +767,6 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
human-id@4.1.1:
|
||||
resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
|
||||
hasBin: true
|
||||
@@ -852,10 +822,6 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
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==}
|
||||
|
||||
@@ -996,19 +962,12 @@ packages:
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
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}
|
||||
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
read-yaml-file@1.1.0:
|
||||
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1107,10 +1066,6 @@ 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'}
|
||||
|
||||
term-size@2.2.1:
|
||||
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1608,14 +1563,6 @@ 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': {}
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
@@ -1708,8 +1655,6 @@ 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
|
||||
@@ -1791,8 +1736,6 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -1819,11 +1762,6 @@ 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: {}
|
||||
@@ -1985,8 +1923,6 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
human-id@4.1.1: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
@@ -2023,13 +1959,6 @@ snapshots:
|
||||
|
||||
isexe@2.0.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: {}
|
||||
|
||||
js-yaml@3.14.1:
|
||||
@@ -2143,18 +2072,10 @@ snapshots:
|
||||
|
||||
prettier@2.8.8: {}
|
||||
|
||||
pretty-format@30.0.5:
|
||||
dependencies:
|
||||
'@jest/schemas': 30.0.5
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
read-yaml-file@1.1.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2264,10 +2185,6 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
term-size@2.2.1: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
import { registerSpecCommand } from '../commands/spec.js';
|
||||
@@ -81,20 +80,6 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('diff [change-name]')
|
||||
.description('Show differences between proposed spec changes and current specs (includes validation warnings)')
|
||||
.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 items (changes by default). Use --specs to list specs.')
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { diffStringsUnified } from 'jest-diff';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import { Validator } from './validation/validator.js';
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Validate specs and show warnings (non-blocking)
|
||||
const validator = new Validator();
|
||||
let hasWarnings = false;
|
||||
|
||||
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');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
const report = await validator.validateSpec(specFile);
|
||||
|
||||
if (report.issues.length > 0) {
|
||||
const warnings = report.issues.filter(i => i.level === 'WARNING');
|
||||
const errors = report.issues.filter(i => i.level === 'ERROR');
|
||||
|
||||
if (errors.length > 0 || warnings.length > 0) {
|
||||
if (!hasWarnings) {
|
||||
console.log(chalk.yellow('\n⚠️ Validation warnings found:'));
|
||||
hasWarnings = true;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`\n ${entry.name}/spec.md:`));
|
||||
for (const issue of errors) {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
}
|
||||
for (const issue of warnings) {
|
||||
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Spec file doesn't exist, skip validation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarnings) {
|
||||
console.log(chalk.yellow('\nConsider fixing these issues before archiving.\n'));
|
||||
}
|
||||
} catch {
|
||||
// No specs directory, skip validation
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ After deployment, use \`openspec archive [change]\` (add \`--skip-specs\` for to
|
||||
openspec list # Active changes
|
||||
openspec list --specs # Existing specifications
|
||||
openspec show [item] # View details
|
||||
openspec diff [change] # Show spec differences
|
||||
openspec validate --strict # Validate thoroughly
|
||||
openspec archive [change] # Archive after deployment
|
||||
|
||||
|
||||
Reference in New Issue
Block a user