Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 6cbb803e48 refactor: switch to jest-diff for better output and simpler code
- Replace custom diff implementation with jest-diff
- Reduce code from 229 to 178 lines (22% reduction)
- Get professional GitHub-style diff output
- Automatic word-level highlighting built-in
- Smaller bundle size (jest-diff: 85KB vs diff: 492KB)
2025-08-13 15:45:14 +10:00
Tabish Bidiwale 183b82f266 feat: enhance diff command with word-level highlighting and better formatting
- Add word-level diff highlighting for changed lines
- Improve file headers with status indicators and statistics
- Add summary view showing total files changed and lines modified
- Enhanced visual formatting with separators and colors
- Extract magic strings as constants for maintainability
2025-08-13 15:29:43 +10:00
Tabish Bidiwale fce227a36e fix: address code review feedback for diff command
- Fix empty line filtering to preserve formatting in diffs
- Replace prompts with @inquirer/prompts for consistency
- Extract magic strings as constants
- Mark tests as incomplete in tasks.md
2025-08-12 16:12:15 +10:00
Tabish Bidiwale 467346f9fe feat: add diff command to view spec changes 2025-08-12 00:59:43 +10:00
Tabish Bidiwale 581a681a47 chore: archive add-complexity-guidelines change after deployment 2025-08-11 23:40:25 +10:00
Tabish Bidiwale 3093ca6ae6 Merge pull request #10 from Fission-AI/feat/complexity-guidelines
docs(openspec): add complexity guidelines to README and templates
2025-08-11 23:32:43 +10:00
Tabish Bidiwale 9d425865c1 docs(openspec): add complexity management guidelines and update templates 2025-08-11 23:17:42 +10:00
Tabish Bidiwale a490dbbc78 chore: archive add-update-command change and update specs 2025-08-11 22:29:34 +10:00
Tabish Bidiwale fc0e2319b1 Merge pull request #9 from Fission-AI/add-update-command-impl
feat(cli): add update command
2025-08-11 22:15:28 +10:00
Tabish Bidiwale edf6873afa feat(cli): add update command to refresh OpenSpec instructions and CLAUDE.md via markers 2025-08-11 21:37:46 +10:00
Tabish Bidiwale c0ce4adc0a Merge pull request #8 from Fission-AI/add-update-command
Add update command for OpenSpec instructions
2025-08-11 20:48:04 +10:00
21 changed files with 612 additions and 32 deletions
+26 -1
View File
@@ -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.
@@ -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
@@ -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,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
+59
View File
@@ -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)
+2
View File
@@ -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"
}
}
+86
View File
@@ -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: {}
+31
View File
@@ -3,6 +3,8 @@ 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';
const program = new Command();
@@ -44,4 +46,33 @@ 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.parse();
+179
View File
@@ -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;
}
}
+20 -1
View File
@@ -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
`;
+26 -1
View File
@@ -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.
`;
+35
View File
@@ -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');
}
}