mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cbb803e48 | ||
|
|
183b82f266 | ||
|
|
fce227a36e | ||
|
|
467346f9fe |
@@ -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
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,69 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [ ] 1.1 Create `src/core/list.ts` with list logic
|
||||
- [ ] 1.1.1 Implement directory scanning (exclude archive/)
|
||||
- [ ] 1.1.2 Implement task counting from tasks.md files
|
||||
- [ ] 1.1.3 Format output as simple table
|
||||
- [ ] 1.2 Add list command to CLI in `src/cli/index.ts`
|
||||
- [ ] 1.2.1 Register `openspec list` command
|
||||
- [ ] 1.2.2 Connect to list.ts implementation
|
||||
|
||||
## 2. Error Handling
|
||||
- [ ] 2.1 Handle missing openspec/changes/ directory
|
||||
- [ ] 2.2 Handle changes without tasks.md files
|
||||
- [ ] 2.3 Handle empty changes directory
|
||||
|
||||
## 3. Testing
|
||||
- [ ] 3.1 Add tests for list functionality
|
||||
- [ ] 3.1.1 Test with multiple changes
|
||||
- [ ] 3.1.2 Test with completed changes
|
||||
- [ ] 3.1.3 Test with no changes
|
||||
- [ ] 3.1.4 Test error conditions
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update CLI help text with list command
|
||||
- [ ] 4.2 Add list command to README if applicable
|
||||
@@ -53,7 +53,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.8.0",
|
||||
"chalk": "^5.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"jest-diff": "^30.0.5",
|
||||
"ora": "^8.2.0"
|
||||
}
|
||||
}
|
||||
Generated
+86
@@ -11,9 +11,15 @@ importers:
|
||||
'@inquirer/prompts':
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0(@types/node@24.2.0)
|
||||
chalk:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
jest-diff:
|
||||
specifier: ^30.0.5
|
||||
version: 30.0.5
|
||||
ora:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
@@ -310,6 +316,18 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@jest/diff-sequences@30.0.1':
|
||||
resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/get-type@30.0.1':
|
||||
resolution: {integrity: sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4':
|
||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
||||
|
||||
@@ -416,6 +434,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sinclair/typebox@0.34.38':
|
||||
resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
|
||||
|
||||
@@ -478,6 +499,10 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -490,6 +515,10 @@ packages:
|
||||
resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.5.0:
|
||||
resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
@@ -585,6 +614,10 @@ packages:
|
||||
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -605,6 +638,10 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
jest-diff@30.0.5:
|
||||
resolution: {integrity: sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
@@ -668,6 +705,13 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
pretty-format@30.0.5:
|
||||
resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -724,6 +768,10 @@ packages:
|
||||
strip-literal@3.0.0:
|
||||
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -1048,6 +1096,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
'@jest/diff-sequences@30.0.1': {}
|
||||
|
||||
'@jest/get-type@30.0.1': {}
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.34.38
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
@@ -1112,6 +1168,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
||||
optional: true
|
||||
|
||||
'@sinclair/typebox@0.34.38': {}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
@@ -1189,6 +1247,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
@@ -1201,6 +1261,11 @@ snapshots:
|
||||
loupe: 3.2.0
|
||||
pathval: 2.0.1
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.5.0: {}
|
||||
|
||||
chardet@0.7.0: {}
|
||||
@@ -1289,6 +1354,8 @@ snapshots:
|
||||
|
||||
get-east-asian-width@1.3.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -1301,6 +1368,13 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
jest-diff@30.0.5:
|
||||
dependencies:
|
||||
'@jest/diff-sequences': 30.0.1
|
||||
'@jest/get-type': 30.0.1
|
||||
chalk: 4.1.2
|
||||
pretty-format: 30.0.5
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
log-symbols@6.0.0:
|
||||
@@ -1356,6 +1430,14 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
pretty-format@30.0.5:
|
||||
dependencies:
|
||||
'@jest/schemas': 30.0.5
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
@@ -1431,6 +1513,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -60,4 +61,18 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('diff [change-name]')
|
||||
.description('Show differences between proposed spec changes and current specs')
|
||||
.action(async (changeName?: string) => {
|
||||
try {
|
||||
const diffCommand = new DiffCommand();
|
||||
await diffCommand.execute(changeName);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user