Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 82ba1f504e merge: resolve conflicts with main branch 2025-09-09 14:12:05 +10:00
Tabish Bidiwale d54fcc97f2 docs: mark completed tasks for diff command removal 2025-09-09 14:07:52 +10:00
Tabish Bidiwale ebff738860 feat: remove diff command in favor of show command
The diff command added unnecessary complexity and duplicated functionality
already available through the show command. Users can now use:
- `openspec show <change>` for structured change viewing
- `openspec show <change> --json --deltas-only` for delta-only views
- Standard git diff or other tools for file comparisons

This change:
- Removes ~227 lines of code and the jest-diff dependency
- Simplifies the CLI interface
- Reduces maintenance burden
- Aligns with verb-first command structure
2025-09-09 14:05:41 +10:00
Tabish Bidiwale 1bdaeef4da Update README.md 2025-09-07 09:16:53 +10:00
10 changed files with 123 additions and 457 deletions
+1 -5
View File
@@ -10,7 +10,7 @@
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.
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?
@@ -118,9 +118,6 @@ AI: "Following the tasks in openspec/changes/add-user-profile-api/tasks.md:
# View active changes (what's being worked on)
openspec list
# See the difference between proposed and current specs
openspec diff add-2fa
# Validate your changes are properly formatted
openspec validate add-2fa --strict
@@ -137,7 +134,6 @@ openspec list # See what changes you're working on
openspec archive <change> # Mark a change as complete after deployment
# Also useful:
openspec diff <change> # See what specs will change
openspec validate <change> # Check formatting before committing
openspec show <change> # View change details
```
-2
View File
@@ -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
-123
View File
@@ -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):
```
-1
View File
@@ -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"
}
-83
View File
@@ -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: {}
-15
View File
@@ -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.')
-227
View File
@@ -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;
}
}
-1
View File
@@ -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