mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5c3b33264 | ||
|
|
c7824626d3 |
@@ -1,11 +0,0 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
# Minimal configuration for getting started
|
||||
language: "en-US"
|
||||
reviews:
|
||||
profile: "chill"
|
||||
high_level_summary: true
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
base_branches:
|
||||
- ".*"
|
||||
@@ -1,92 +0,0 @@
|
||||
# Dev Container Setup
|
||||
|
||||
This directory contains the VS Code dev container configuration for OpenSpec development.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Node.js 20 LTS** (>=20.19.0) - TypeScript/JavaScript runtime
|
||||
- **pnpm** - Fast, disk space efficient package manager
|
||||
- **Git + GitHub CLI** - Version control tools
|
||||
- **VS Code Extensions**:
|
||||
- ESLint & Prettier for code quality
|
||||
- Vitest Explorer for running tests
|
||||
- GitLens for enhanced git integration
|
||||
- Error Lens for inline error highlighting
|
||||
- Code Spell Checker
|
||||
- Path IntelliSense
|
||||
|
||||
## How to Use
|
||||
|
||||
### First Time Setup
|
||||
|
||||
1. **Install Prerequisites** (on your local machine):
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
- [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
|
||||
2. **Open in Container**:
|
||||
- Open this project in VS Code
|
||||
- You'll see a notification: "Folder contains a Dev Container configuration file"
|
||||
- Click "Reopen in Container"
|
||||
|
||||
OR
|
||||
|
||||
- Open Command Palette (`Cmd/Ctrl+Shift+P`)
|
||||
- Type "Dev Containers: Reopen in Container"
|
||||
- Press Enter
|
||||
|
||||
3. **Wait for Setup**:
|
||||
- The container will build (first time takes a few minutes)
|
||||
- `pnpm install` runs automatically via `postCreateCommand`
|
||||
- All extensions install automatically
|
||||
|
||||
### Daily Development
|
||||
|
||||
Once set up, the container preserves your development environment:
|
||||
|
||||
```bash
|
||||
# Run development build
|
||||
pnpm run dev
|
||||
|
||||
# Run CLI in development
|
||||
pnpm run dev:cli
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm test:watch
|
||||
|
||||
# Build the project
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### SSH Keys
|
||||
|
||||
Your SSH keys are mounted read-only from `~/.ssh`, so git operations work seamlessly with GitHub/GitLab.
|
||||
|
||||
### Rebuilding the Container
|
||||
|
||||
If you modify `.devcontainer/devcontainer.json`:
|
||||
- Command Palette → "Dev Containers: Rebuild Container"
|
||||
|
||||
## Benefits
|
||||
|
||||
- No need to install Node.js or pnpm on your local machine
|
||||
- Consistent development environment across team members
|
||||
- Isolated from other Node.js projects on your machine
|
||||
- All dependencies and tools containerized
|
||||
- Easy onboarding for new developers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Container won't build:**
|
||||
- Ensure Docker Desktop is running
|
||||
- Check Docker has enough memory allocated (recommend 4GB+)
|
||||
|
||||
**Extensions not appearing:**
|
||||
- Rebuild the container: "Dev Containers: Rebuild Container"
|
||||
|
||||
**Permission issues:**
|
||||
- The container runs as the `node` user (non-root)
|
||||
- Files created in the container are owned by this user
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "OpenSpec Development",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm",
|
||||
|
||||
// Additional tools and features
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {
|
||||
"version": "latest",
|
||||
"ppa": true
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "latest"
|
||||
}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// Set default container specific settings
|
||||
"settings": {
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"terminal.integrated.defaultProfile.linux": "bash"
|
||||
},
|
||||
|
||||
// Add extensions you want installed when the container is created
|
||||
"extensions": [
|
||||
// TypeScript/JavaScript essentials
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// Testing
|
||||
"vitest.explorer",
|
||||
|
||||
// Git
|
||||
"eamodio.gitlens",
|
||||
|
||||
// Utilities
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"usernamehw.errorlens",
|
||||
"christian-kohler.path-intellisense"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created
|
||||
"postCreateCommand": "corepack enable && corepack prepare pnpm@latest --activate && pnpm install",
|
||||
|
||||
// Configure mounts to preserve SSH keys for git operations
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,readonly,type=bind,consistency=cached"
|
||||
],
|
||||
|
||||
// Set the default user to 'node' (non-root user)
|
||||
"remoteUser": "node",
|
||||
|
||||
// Ensure git is properly configured
|
||||
"initializeCommand": "echo 'Initializing dev container...'"
|
||||
}
|
||||
@@ -147,6 +147,3 @@ docs/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
.DS_Store
|
||||
|
||||
# Pnpm
|
||||
.pnpm-store/
|
||||
@@ -92,7 +92,6 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
|------|----------|
|
||||
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
|
||||
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
@@ -100,7 +99,6 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
|
||||
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
|
||||
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
|
||||
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
|
||||
|
||||
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
## Why
|
||||
Add support for Cline (VS Code extension) in OpenSpec to enable developers to use Cline's AI-powered coding capabilities for spec-driven development workflows.
|
||||
|
||||
## What Changes
|
||||
- Add Cline slash command configurator for proposal, apply, and archive operations
|
||||
- Add Cline root CLINE.md configurator for project-level instructions
|
||||
- Add Cline template exports
|
||||
- Update tool and slash command registries to include Cline
|
||||
- Add comprehensive test coverage
|
||||
- **BREAKING**: None - this is additive functionality
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-init (new tool option)
|
||||
- Affected code: src/core/configurators/slash/cline.ts, src/core/configurators/cline.ts, registry files
|
||||
- New files: .clinerules/openspec-*.md, CLINE.md
|
||||
@@ -1,27 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Cline Tool Support
|
||||
The system SHALL provide Cline (VS Code extension) as a supported tool option during OpenSpec initialization.
|
||||
|
||||
#### Scenario: Initialize project with Cline support
|
||||
- **WHEN** user runs `openspec init --tools cline`
|
||||
- **THEN** Cline-specific rule files are configured in `.clinerules/`
|
||||
- **AND** CLINE.md root file includes OpenSpec workflow instructions
|
||||
- **AND** Cline is registered as available configurator
|
||||
|
||||
#### Scenario: Cline proposal rule generation
|
||||
- **WHEN** Cline rules are configured
|
||||
- **THEN** `.clinerules/openspec-proposal.md` contains proposal workflow with guardrails
|
||||
- **AND** Includes Cline-specific Markdown heading frontmatter
|
||||
- **AND** Follows established slash command template pattern
|
||||
|
||||
#### Scenario: Cline apply and archive rules
|
||||
- **WHEN** Cline rules are configured
|
||||
- **THEN** `.clinerules/openspec-apply.md` contains implementation workflow
|
||||
- **AND** `.clinerules/openspec-archive.md` contains archiving workflow
|
||||
- **AND** Both commands include appropriate headers and references
|
||||
|
||||
#### Scenario: Cline root instructions
|
||||
- **WHEN** Cline is selected during initialization
|
||||
- **THEN** CLINE.md is created at project root
|
||||
- **AND** Contains OpenSpec markers for managed content
|
||||
- **AND** References `@/openspec/AGENTS.md` for workflow instructions
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1. Implementation
|
||||
- [x] 1.1 Create ClineSlashCommandConfigurator class in src/core/configurators/slash/cline.ts
|
||||
- [x] 1.2 Create ClineConfigurator class in src/core/configurators/cline.ts
|
||||
- [x] 1.3 Create cline-template.ts for template exports
|
||||
- [x] 1.4 Define file paths for Cline rules (.clinerules/)
|
||||
- [x] 1.5 Create Cline-specific frontmatter (Markdown heading format)
|
||||
- [x] 1.6 Register Cline in slash/registry.ts
|
||||
- [x] 1.7 Register Cline in configurators/registry.ts
|
||||
- [x] 1.8 Add Cline to AI_TOOLS in config.ts
|
||||
- [x] 1.9 Add getClineTemplate() to templates/index.ts
|
||||
- [x] 1.10 Update README with Cline documentation
|
||||
|
||||
## 2. Testing
|
||||
- [x] 2.1 Add init tests for CLINE.md creation and updates
|
||||
- [x] 2.2 Add init tests for .clinerules/ file creation
|
||||
- [x] 2.3 Add update tests for CLINE.md updates
|
||||
- [x] 2.4 Add update tests for .clinerules/ file refreshes
|
||||
- [x] 2.5 Test integration with openspec init --tools cline
|
||||
- [x] 2.6 Verify all 225 tests pass
|
||||
@@ -1,13 +0,0 @@
|
||||
## Why
|
||||
Add support for Crush AI assistant in OpenSpec to enable developers to use Crush's enhanced capabilities for spec-driven development workflows.
|
||||
|
||||
## What Changes
|
||||
- Add Crush slash command configurator for proposal, apply, and archive operations
|
||||
- Add Crush-specific AGENTS.md configuration template
|
||||
- Update tool registry to include Crush configurator
|
||||
- **BREAKING**: None - this is additive functionality
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-init (new tool option)
|
||||
- Affected code: src/core/configurators/slash/crush.ts, registry.ts
|
||||
- New files: .crush/commands/openspec/ (proposal.md, apply.md, archive.md)
|
||||
@@ -1,21 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Crush Tool Support
|
||||
The system SHALL provide Crush AI assistant as a supported tool option during OpenSpec initialization.
|
||||
|
||||
#### Scenario: Initialize project with Crush support
|
||||
- **WHEN** user runs `openspec init --tool crush`
|
||||
- **THEN** Crush-specific slash commands are configured in `.crush/commands/openspec/`
|
||||
- **AND** Crush AGENTS.md includes OpenSpec workflow instructions
|
||||
- **AND** Crush is registered as available configurator
|
||||
|
||||
#### Scenario: Crush proposal command generation
|
||||
- **WHEN** Crush slash commands are configured
|
||||
- **THEN** `.crush/commands/openspec/proposal.md` contains proposal workflow with guardrails
|
||||
- **AND** Includes Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** Follows established slash command template pattern
|
||||
|
||||
#### Scenario: Crush apply and archive commands
|
||||
- **WHEN** Crush slash commands are configured
|
||||
- **THEN** `.crush/commands/openspec/apply.md` contains implementation workflow
|
||||
- **AND** `.crush/commands/openspec/archive.md` contains archiving workflow
|
||||
- **AND** Both commands include appropriate frontmatter and references
|
||||
@@ -1,7 +0,0 @@
|
||||
## 1. Implementation
|
||||
- [x] 1.1 Create CrushSlashCommandConfigurator class in src/core/configurators/slash/crush.ts
|
||||
- [x] 1.2 Define file paths for Crush commands (.crush/commands/openspec/)
|
||||
- [x] 1.3 Create Crush-specific frontmatter for proposal, apply, archive commands
|
||||
- [x] 1.4 Register Crush configurator in slash/registry.ts
|
||||
- [x] 1.5 Add Crush to available tools in cli-init command
|
||||
- [x] 1.6 Test integration with openspec init --tool crush
|
||||
@@ -17,10 +17,7 @@ export interface AIToolOption {
|
||||
}
|
||||
|
||||
export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
|
||||
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import path from 'path';
|
||||
import { ToolConfigurator } from './base.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { TemplateManager } from '../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../config.js';
|
||||
|
||||
export class ClineConfigurator implements ToolConfigurator {
|
||||
name = 'Cline';
|
||||
configFileName = 'CLINE.md';
|
||||
isAvailable = true;
|
||||
|
||||
async configure(projectPath: string, openspecDir: string): Promise<void> {
|
||||
const filePath = path.join(projectPath, this.configFileName);
|
||||
const content = TemplateManager.getClineTemplate();
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
content,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ToolConfigurator } from './base.js';
|
||||
import { ClaudeConfigurator } from './claude.js';
|
||||
import { ClineConfigurator } from './cline.js';
|
||||
import { AgentsStandardConfigurator } from './agents.js';
|
||||
|
||||
export class ToolRegistry {
|
||||
@@ -8,11 +7,9 @@ export class ToolRegistry {
|
||||
|
||||
static {
|
||||
const claudeConfigurator = new ClaudeConfigurator();
|
||||
const clineConfigurator = new ClineConfigurator();
|
||||
const agentsConfigurator = new AgentsStandardConfigurator();
|
||||
// Register with the ID that matches the checkbox value
|
||||
this.tools.set('claude', claudeConfigurator);
|
||||
this.tools.set('cline', clineConfigurator);
|
||||
this.tools.set('agents', agentsConfigurator);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.augment/commands/openspec-proposal.md',
|
||||
apply: '.augment/commands/openspec-apply.md',
|
||||
archive: '.augment/commands/openspec-archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
argument-hint: feature description or request
|
||||
---`,
|
||||
apply: `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
argument-hint: change-id
|
||||
---`,
|
||||
archive: `---
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
argument-hint: change-id
|
||||
---`
|
||||
};
|
||||
|
||||
export class AuggieSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'auggie';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.clinerules/openspec-proposal.md',
|
||||
apply: '.clinerules/openspec-apply.md',
|
||||
archive: '.clinerules/openspec-archive.md'
|
||||
};
|
||||
|
||||
export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'cline';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
const descriptions: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
const description = descriptions[id];
|
||||
return `# OpenSpec: ${id.charAt(0).toUpperCase() + id.slice(1)}\n\n${description}`;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.crush/commands/openspec/proposal.md',
|
||||
apply: '.crush/commands/openspec/apply.md',
|
||||
archive: '.crush/commands/openspec/archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: OpenSpec: Proposal
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---`,
|
||||
apply: `---
|
||||
name: OpenSpec: Apply
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
category: OpenSpec
|
||||
tags: [openspec, apply]
|
||||
---`,
|
||||
archive: `---
|
||||
name: OpenSpec: Archive
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
category: OpenSpec
|
||||
tags: [openspec, archive]
|
||||
---`
|
||||
};
|
||||
|
||||
export class CrushSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'crush';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,6 @@ import { CodexSlashCommandConfigurator } from './codex.js';
|
||||
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
|
||||
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
|
||||
import { FactorySlashCommandConfigurator } from './factory.js';
|
||||
import { AuggieSlashCommandConfigurator } from './auggie.js';
|
||||
import { ClineSlashCommandConfigurator } from './cline.js';
|
||||
import { CrushSlashCommandConfigurator } from './crush.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -25,9 +22,6 @@ export class SlashCommandRegistry {
|
||||
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
|
||||
const amazonQ = new AmazonQSlashCommandConfigurator();
|
||||
const factory = new FactorySlashCommandConfigurator();
|
||||
const auggie = new AuggieSlashCommandConfigurator();
|
||||
const cline = new ClineSlashCommandConfigurator();
|
||||
const crush = new CrushSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
@@ -38,9 +32,6 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(githubCopilot.toolId, githubCopilot);
|
||||
this.configurators.set(amazonQ.toolId, amazonQ);
|
||||
this.configurators.set(factory.toolId, factory);
|
||||
this.configurators.set(auggie.toolId, auggie);
|
||||
this.configurators.set(cline.toolId, cline);
|
||||
this.configurators.set(crush.toolId, crush);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
@@ -101,12 +101,6 @@ export interface DeltaPlan {
|
||||
modified: RequirementBlock[];
|
||||
removed: string[]; // requirement names
|
||||
renamed: Array<{ from: string; to: string }>;
|
||||
sectionPresence: {
|
||||
added: boolean;
|
||||
modified: boolean;
|
||||
removed: boolean;
|
||||
renamed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLineEndings(content: string): string {
|
||||
@@ -119,26 +113,11 @@ function normalizeLineEndings(content: string): string {
|
||||
export function parseDeltaSpec(content: string): DeltaPlan {
|
||||
const normalized = normalizeLineEndings(content);
|
||||
const sections = splitTopLevelSections(normalized);
|
||||
const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
|
||||
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
||||
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
||||
const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
|
||||
const added = parseRequirementBlocksFromSection(addedLookup.body);
|
||||
const modified = parseRequirementBlocksFromSection(modifiedLookup.body);
|
||||
const removedNames = parseRemovedNames(removedLookup.body);
|
||||
const renamedPairs = parseRenamedPairs(renamedLookup.body);
|
||||
return {
|
||||
added,
|
||||
modified,
|
||||
removed: removedNames,
|
||||
renamed: renamedPairs,
|
||||
sectionPresence: {
|
||||
added: addedLookup.found,
|
||||
modified: modifiedLookup.found,
|
||||
removed: removedLookup.found,
|
||||
renamed: renamedLookup.found,
|
||||
},
|
||||
};
|
||||
const added = parseRequirementBlocksFromSection(sections['ADDED Requirements'] || '');
|
||||
const modified = parseRequirementBlocksFromSection(sections['MODIFIED Requirements'] || '');
|
||||
const removedNames = parseRemovedNames(sections['REMOVED Requirements'] || '');
|
||||
const renamedPairs = parseRenamedPairs(sections['RENAMED Requirements'] || '');
|
||||
return { added, modified, removed: removedNames, renamed: renamedPairs };
|
||||
}
|
||||
|
||||
function splitTopLevelSections(content: string): Record<string, string> {
|
||||
@@ -161,14 +140,6 @@ function splitTopLevelSections(content: string): Record<string, string> {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSectionCaseInsensitive(sections: Record<string, string>, desired: string): { body: string; found: boolean } {
|
||||
const target = desired.toLowerCase();
|
||||
for (const [title, body] of Object.entries(sections)) {
|
||||
if (title.toLowerCase() === target) return { body, found: true };
|
||||
}
|
||||
return { body: '', found: false };
|
||||
}
|
||||
|
||||
function parseRequirementBlocksFromSection(sectionBody: string): RequirementBlock[] {
|
||||
if (!sectionBody) return [];
|
||||
const lines = normalizeLineEndings(sectionBody).split('\n');
|
||||
@@ -232,3 +203,5 @@ function parseRenamedPairs(sectionBody: string): Array<{ from: string; to: strin
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { agentsRootStubTemplate as clineTemplate } from './agents-root-stub.js';
|
||||
@@ -1,7 +1,6 @@
|
||||
import { agentsTemplate } from './agents-template.js';
|
||||
import { projectTemplate, ProjectContext } from './project-template.js';
|
||||
import { claudeTemplate } from './claude-template.js';
|
||||
import { clineTemplate } from './cline-template.js';
|
||||
import { agentsRootStubTemplate } from './agents-root-stub.js';
|
||||
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
|
||||
|
||||
@@ -28,10 +27,6 @@ export class TemplateManager {
|
||||
return claudeTemplate;
|
||||
}
|
||||
|
||||
static getClineTemplate(): string {
|
||||
return clineTemplate;
|
||||
}
|
||||
|
||||
static getAgentsStandardTemplate(): string {
|
||||
return agentsRootStubTemplate;
|
||||
}
|
||||
|
||||
@@ -114,8 +114,6 @@ export class Validator {
|
||||
const issues: ValidationIssue[] = [];
|
||||
const specsDir = path.join(changeDir, 'specs');
|
||||
let totalDeltas = 0;
|
||||
const missingHeaderSpecs: string[] = [];
|
||||
const emptySectionSpecs: Array<{ path: string; sections: string[] }> = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
||||
@@ -132,17 +130,6 @@ export class Validator {
|
||||
|
||||
const plan = parseDeltaSpec(content);
|
||||
const entryPath = `${specName}/spec.md`;
|
||||
const sectionNames: string[] = [];
|
||||
if (plan.sectionPresence.added) sectionNames.push('## ADDED Requirements');
|
||||
if (plan.sectionPresence.modified) sectionNames.push('## MODIFIED Requirements');
|
||||
if (plan.sectionPresence.removed) sectionNames.push('## REMOVED Requirements');
|
||||
if (plan.sectionPresence.renamed) sectionNames.push('## RENAMED Requirements');
|
||||
const hasSections = sectionNames.length > 0;
|
||||
const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
|
||||
if (!hasEntries) {
|
||||
if (hasSections) emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
|
||||
else missingHeaderSpecs.push(entryPath);
|
||||
}
|
||||
|
||||
const addedNames = new Set<string>();
|
||||
const modifiedNames = new Set<string>();
|
||||
@@ -249,21 +236,6 @@ export class Validator {
|
||||
// If no specs dir, treat as no deltas
|
||||
}
|
||||
|
||||
for (const { path: specPath, sections } of emptySectionSpecs) {
|
||||
issues.push({
|
||||
level: 'ERROR',
|
||||
path: specPath,
|
||||
message: `Delta sections ${this.formatSectionList(sections)} were found, but no requirement entries parsed. Ensure each section includes at least one "### Requirement:" block (REMOVED may use bullet list syntax).`,
|
||||
});
|
||||
}
|
||||
for (const path of missingHeaderSpecs) {
|
||||
issues.push({
|
||||
level: 'ERROR',
|
||||
path,
|
||||
message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
|
||||
});
|
||||
}
|
||||
|
||||
if (totalDeltas === 0) {
|
||||
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
||||
}
|
||||
@@ -437,12 +409,4 @@ export class Validator {
|
||||
const matches = blockRaw.match(/^####\s+/gm);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
private formatSectionList(sections: string[]): string {
|
||||
if (sections.length === 0) return '';
|
||||
if (sections.length === 1) return sections[0];
|
||||
const head = sections.slice(0, -1);
|
||||
const last = sections[sections.length - 1];
|
||||
return `${head.join(', ')} and ${last}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,39 +136,6 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create CLINE.md when Cline is selected', async () => {
|
||||
queueSelections('cline', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
expect(await fileExists(clinePath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(clinePath, 'utf-8');
|
||||
expect(content).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(content).toContain("@/openspec/AGENTS.md");
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
});
|
||||
|
||||
it('should update existing CLINE.md with markers', async () => {
|
||||
queueSelections('cline', DONE);
|
||||
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
const existingContent =
|
||||
'# My Cline Rules\nCustom Cline instructions here';
|
||||
await fs.writeFile(clinePath, existingContent);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(clinePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
expect(updatedContent).toContain('openspec update');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain('Custom Cline instructions here');
|
||||
});
|
||||
|
||||
it('should create Windsurf workflows when Windsurf is selected', async () => {
|
||||
queueSelections('windsurf', DONE);
|
||||
|
||||
@@ -349,45 +316,6 @@ describe('InitCommand', () => {
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
});
|
||||
|
||||
it('should create Cline rule files with templates', async () => {
|
||||
queueSelections('cline', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const clineProposal = path.join(
|
||||
testDir,
|
||||
'.clinerules/openspec-proposal.md'
|
||||
);
|
||||
const clineApply = path.join(
|
||||
testDir,
|
||||
'.clinerules/openspec-apply.md'
|
||||
);
|
||||
const clineArchive = path.join(
|
||||
testDir,
|
||||
'.clinerules/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(clineProposal)).toBe(true);
|
||||
expect(await fileExists(clineApply)).toBe(true);
|
||||
expect(await fileExists(clineArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(clineProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('# OpenSpec: Proposal');
|
||||
expect(proposalContent).toContain('Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(clineApply, 'utf-8');
|
||||
expect(applyContent).toContain('# OpenSpec: Apply');
|
||||
expect(applyContent).toContain('Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(clineArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('# OpenSpec: Archive');
|
||||
expect(archiveContent).toContain('Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('openspec archive <id>');
|
||||
});
|
||||
|
||||
it('should create Factory slash command files with templates', async () => {
|
||||
queueSelections('factory', DONE);
|
||||
|
||||
@@ -755,120 +683,6 @@ describe('InitCommand', () => {
|
||||
);
|
||||
expect(amazonQChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Auggie slash command files with templates', async () => {
|
||||
queueSelections('auggie', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const auggieProposal = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-proposal.md'
|
||||
);
|
||||
const auggieApply = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-apply.md'
|
||||
);
|
||||
const auggieArchive = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(auggieProposal)).toBe(true);
|
||||
expect(await fileExists(auggieApply)).toBe(true);
|
||||
expect(await fileExists(auggieArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(auggieProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('argument-hint: feature description or request');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(auggieApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('argument-hint: change-id');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(auggieArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('argument-hint: change-id');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
it('should mark Auggie as already configured during extend mode', async () => {
|
||||
queueSelections('auggie', DONE, 'auggie', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const auggieChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'auggie'
|
||||
);
|
||||
expect(auggieChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Crush slash command files with templates', async () => {
|
||||
queueSelections('crush', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const crushProposal = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec/proposal.md'
|
||||
);
|
||||
const crushApply = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec/apply.md'
|
||||
);
|
||||
const crushArchive = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec/archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(crushProposal)).toBe(true);
|
||||
expect(await fileExists(crushApply)).toBe(true);
|
||||
expect(await fileExists(crushArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(crushProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('name: OpenSpec: Proposal');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('category: OpenSpec');
|
||||
expect(proposalContent).toContain('tags: [openspec, change]');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(crushApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('name: OpenSpec: Apply');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('category: OpenSpec');
|
||||
expect(applyContent).toContain('tags: [openspec, apply]');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(crushArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('name: OpenSpec: Archive');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('category: OpenSpec');
|
||||
expect(archiveContent).toContain('tags: [openspec, archive]');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
it('should mark Crush as already configured during extend mode', async () => {
|
||||
queueSelections('crush', DONE, 'crush', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const crushChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'crush'
|
||||
);
|
||||
expect(crushChoice.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
|
||||
@@ -126,95 +126,6 @@ Old slash content
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
|
||||
it('should update only existing CLINE.md file', async () => {
|
||||
// Create CLINE.md file with initial content
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
const initialContent = `# Cline Rules
|
||||
|
||||
Some existing Cline rules here.
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old OpenSpec content
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
More rules after.`;
|
||||
await fs.writeFile(clinePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLINE.md was updated
|
||||
const updatedContent = await fs.readFile(clinePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
expect(updatedContent).toContain('openspec update');
|
||||
expect(updatedContent).toContain('Some existing Cline rules here');
|
||||
expect(updatedContent).toContain('More rules after');
|
||||
|
||||
// Check console output
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain('Updated AI tool files: CLINE.md');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create CLINE.md if it does not exist', async () => {
|
||||
// Ensure CLINE.md does not exist
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLINE.md was not created
|
||||
const fileExists = await FileSystemUtils.fileExists(clinePath);
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Cline rule files', async () => {
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.clinerules/openspec-proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
||||
const initialContent = `# OpenSpec: Proposal
|
||||
|
||||
Scaffold a new OpenSpec change and validate strictly.
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old slash content
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(proposalPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(updated).toContain('# OpenSpec: Proposal');
|
||||
expect(updated).toContain('**Guardrails**');
|
||||
expect(updated).toContain(
|
||||
'Validate with `openspec validate <id> --strict`'
|
||||
);
|
||||
expect(updated).not.toContain('Old slash content');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .clinerules/openspec-proposal.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Cursor slash command files', async () => {
|
||||
const cursorPath = path.join(testDir, '.cursor/commands/openspec-apply.md');
|
||||
await fs.mkdir(path.dirname(cursorPath), { recursive: true });
|
||||
@@ -602,145 +513,6 @@ Old body
|
||||
await expect(FileSystemUtils.fileExists(aqArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Auggie slash command files', async () => {
|
||||
const auggiePath = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(auggiePath), { recursive: true });
|
||||
const initialContent = `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
argument-hint: change-id
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(auggiePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(auggiePath, 'utf-8');
|
||||
expect(updatedContent).toContain('**Guardrails**');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).not.toContain('Old body');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('.augment/commands/openspec-apply.md')
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing Auggie slash command files on update', async () => {
|
||||
const auggieApply = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-apply.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(auggieApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
auggieApply,
|
||||
'---\ndescription: Old\nargument-hint: old\n---\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const auggieProposal = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-proposal.md'
|
||||
);
|
||||
const auggieArchive = path.join(
|
||||
testDir,
|
||||
'.augment/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(auggieProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(auggieArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Crush slash command files', async () => {
|
||||
const crushPath = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec/proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(crushPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
name: OpenSpec: Proposal
|
||||
description: Old description
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old slash content
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(crushPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(crushPath, 'utf-8');
|
||||
expect(updated).toContain('name: OpenSpec: Proposal');
|
||||
expect(updated).toContain('**Guardrails**');
|
||||
expect(updated).toContain(
|
||||
'Validate with `openspec validate <id> --strict`'
|
||||
);
|
||||
expect(updated).not.toContain('Old slash content');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .crush/commands/openspec/proposal.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing Crush slash command files on update', async () => {
|
||||
const crushApply = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec-apply.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(crushApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
crushApply,
|
||||
`---
|
||||
name: OpenSpec: Apply
|
||||
description: Old description
|
||||
category: OpenSpec
|
||||
tags: [openspec, apply]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const crushProposal = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec-proposal.md'
|
||||
);
|
||||
const crushArchive = path.join(
|
||||
testDir,
|
||||
'.crush/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(crushProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(crushArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve Windsurf content outside markers during update', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
|
||||
@@ -456,34 +456,5 @@ The system SHALL implement this feature.
|
||||
expect(report.valid).toBe(true);
|
||||
expect(report.summary.errors).toBe(0);
|
||||
});
|
||||
|
||||
it('should treat delta headers case-insensitively', async () => {
|
||||
const changeDir = path.join(testDir, 'test-change-mixed-case');
|
||||
const specsDir = path.join(changeDir, 'specs', 'test-spec');
|
||||
await fs.mkdir(specsDir, { recursive: true });
|
||||
|
||||
const deltaSpec = `# Test Spec
|
||||
|
||||
## Added Requirements
|
||||
|
||||
### Requirement: Mixed Case Handling
|
||||
The system MUST support mixed case delta headers.
|
||||
|
||||
#### Scenario: Case insensitive parsing
|
||||
**Given** a delta file with mixed case headers
|
||||
**When** validation runs
|
||||
**Then** the delta is detected`;
|
||||
|
||||
const specPath = path.join(specsDir, 'spec.md');
|
||||
await fs.writeFile(specPath, deltaSpec);
|
||||
|
||||
const validator = new Validator(true);
|
||||
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
||||
|
||||
expect(report.valid).toBe(true);
|
||||
expect(report.summary.errors).toBe(0);
|
||||
expect(report.summary.warnings).toBe(0);
|
||||
expect(report.summary.info).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user