Skip to content

Commit 646c516

Browse files
authored
Merge pull request #63 from Fission-AI/feat/add-slash-command-support
feat(cli): add slash command support
2 parents 17e6f71 + 8c1b580 commit 646c516

File tree

14 files changed

+447
-17
lines changed

14 files changed

+447
-17
lines changed

openspec/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,26 @@ Skip proposal for:
4040
- Configuration changes
4141
- Tests for existing behavior
4242

43+
**Workflow**
44+
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
45+
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
46+
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
47+
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
48+
4349
### Stage 2: Implementing Changes
4450
1. **Read proposal.md** - Understand what's being built
4551
2. **Read design.md** (if exists) - Review technical decisions
4652
3. **Read tasks.md** - Get implementation checklist
4753
4. **Implement tasks sequentially** - Complete in order
4854
5. **Mark complete immediately** - Update `- [x]` after each task
49-
6. **Validate strictly** - Run `openspec validate [change] --strict` and address issues
50-
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
55+
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
5156

5257
### Stage 3: Archiving Changes
5358
After deployment, create separate PR to:
5459
- Move `changes/[name]/``changes/archive/YYYY-MM-DD-[name]/`
5560
- Update `specs/` if capabilities changed
5661
- Use `openspec archive [change] --skip-specs` for tooling-only changes
62+
- Run `openspec validate --strict` to confirm the archived change passes checks
5763

5864
## Before Any Task
5965

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Implementation Tasks
22

33
## 1. Templates and Configurators
4-
- [ ] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
5-
- [ ] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
4+
- [x] 1.1 Create shared templates for the Proposal, Apply, and Archive commands with instructions for each workflow stage from `openspec/README.md`.
5+
- [x] 1.2 Implement a `SlashCommandConfigurator` base and tool-specific configurators for Claude Code and Cursor.
66

77
## 2. Claude Code Integration
8-
- [ ] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
9-
- [ ] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
8+
- [x] 2.1 Generate `.claude/commands/openspec/{proposal,apply,archive}.md` during `openspec init` using shared templates.
9+
- [x] 2.2 Update existing `.claude/commands/openspec/*` files during `openspec update`.
1010

1111
## 3. Cursor Integration
12-
- [ ] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
13-
- [ ] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
12+
- [x] 3.1 Generate `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md` during `openspec init` using shared templates.
13+
- [x] 3.2 Update existing `.cursor/commands/*` files during `openspec update`.
1414

1515
## 4. Verification
16-
- [ ] 4.1 Add tests verifying slash command files are created and updated correctly.
16+
- [x] 4.1 Add tests verifying slash command files are created and updated correctly.

src/core/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const OPENSPEC_MARKERS = {
1111

1212
export const AI_TOOLS = [
1313
{ name: 'Claude Code', value: 'claude', available: true },
14-
{ name: 'Cursor', value: 'cursor', available: false },
14+
{ name: 'Cursor', value: 'cursor', available: true },
1515
{ name: 'Aider', value: 'aider', available: false },
1616
{ name: 'Continue', value: 'continue', available: false }
17-
];
17+
];
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import path from 'path';
2+
import { FileSystemUtils } from '../../../utils/file-system.js';
3+
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
4+
import { OPENSPEC_MARKERS } from '../../config.js';
5+
6+
export interface SlashCommandTarget {
7+
id: SlashCommandId;
8+
path: string;
9+
kind: 'slash';
10+
}
11+
12+
const ALL_COMMANDS: SlashCommandId[] = ['proposal', 'apply', 'archive'];
13+
14+
export abstract class SlashCommandConfigurator {
15+
abstract readonly toolId: string;
16+
abstract readonly isAvailable: boolean;
17+
18+
getTargets(): SlashCommandTarget[] {
19+
return ALL_COMMANDS.map((id) => ({
20+
id,
21+
path: this.getRelativePath(id),
22+
kind: 'slash'
23+
}));
24+
}
25+
26+
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
27+
const createdOrUpdated: string[] = [];
28+
29+
for (const target of this.getTargets()) {
30+
const body = TemplateManager.getSlashCommandBody(target.id).trim();
31+
const filePath = path.join(projectPath, target.path);
32+
33+
if (await FileSystemUtils.fileExists(filePath)) {
34+
await this.updateBody(filePath, body);
35+
} else {
36+
const frontmatter = this.getFrontmatter(target.id);
37+
const sections: string[] = [];
38+
if (frontmatter) {
39+
sections.push(frontmatter.trim());
40+
}
41+
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
42+
const content = sections.join('\n') + '\n';
43+
await FileSystemUtils.writeFile(filePath, content);
44+
}
45+
46+
createdOrUpdated.push(target.path);
47+
}
48+
49+
return createdOrUpdated;
50+
}
51+
52+
async updateExisting(projectPath: string, _openspecDir: string): Promise<string[]> {
53+
const updated: string[] = [];
54+
55+
for (const target of this.getTargets()) {
56+
const filePath = path.join(projectPath, target.path);
57+
if (await FileSystemUtils.fileExists(filePath)) {
58+
const body = TemplateManager.getSlashCommandBody(target.id).trim();
59+
await this.updateBody(filePath, body);
60+
updated.push(target.path);
61+
}
62+
}
63+
64+
return updated;
65+
}
66+
67+
protected abstract getRelativePath(id: SlashCommandId): string;
68+
protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
69+
70+
private async updateBody(filePath: string, body: string): Promise<void> {
71+
const content = await FileSystemUtils.readFile(filePath);
72+
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
73+
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
74+
75+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
76+
throw new Error(`Missing OpenSpec markers in ${filePath}`);
77+
}
78+
79+
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
80+
const after = content.slice(endIndex);
81+
const updatedContent = `${before}\n${body}\n${after}`;
82+
83+
await FileSystemUtils.writeFile(filePath, updatedContent);
84+
}
85+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.claude/commands/openspec/proposal.md',
6+
apply: '.claude/commands/openspec/apply.md',
7+
archive: '.claude/commands/openspec/archive.md'
8+
};
9+
10+
const FRONTMATTER: Record<SlashCommandId, string> = {
11+
proposal: `---
12+
name: OpenSpec: Proposal
13+
description: Scaffold a new OpenSpec change and validate strictly.
14+
category: OpenSpec
15+
tags: [openspec, change]
16+
---`,
17+
apply: `---
18+
name: OpenSpec: Apply
19+
description: Implement an approved OpenSpec change and keep tasks in sync.
20+
category: OpenSpec
21+
tags: [openspec, apply]
22+
---`,
23+
archive: `---
24+
name: OpenSpec: Archive
25+
description: Archive a deployed OpenSpec change and update specs.
26+
category: OpenSpec
27+
tags: [openspec, archive]
28+
---`
29+
};
30+
31+
export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
32+
readonly toolId = 'claude';
33+
readonly isAvailable = true;
34+
35+
protected getRelativePath(id: SlashCommandId): string {
36+
return FILE_PATHS[id];
37+
}
38+
39+
protected getFrontmatter(id: SlashCommandId): string {
40+
return FRONTMATTER[id];
41+
}
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.cursor/commands/openspec-proposal.md',
6+
apply: '.cursor/commands/openspec-apply.md',
7+
archive: '.cursor/commands/openspec-archive.md'
8+
};
9+
10+
const FRONTMATTER: Record<SlashCommandId, string> = {
11+
proposal: `---
12+
name: /openspec-proposal
13+
id: openspec-proposal
14+
category: OpenSpec
15+
description: Scaffold a new OpenSpec change and validate strictly.
16+
---`,
17+
apply: `---
18+
name: /openspec-apply
19+
id: openspec-apply
20+
category: OpenSpec
21+
description: Implement an approved OpenSpec change and keep tasks in sync.
22+
---`,
23+
archive: `---
24+
name: /openspec-archive
25+
id: openspec-archive
26+
category: OpenSpec
27+
description: Archive a deployed OpenSpec change and update specs.
28+
---`
29+
};
30+
31+
export class CursorSlashCommandConfigurator extends SlashCommandConfigurator {
32+
readonly toolId = 'cursor';
33+
readonly isAvailable = true;
34+
35+
protected getRelativePath(id: SlashCommandId): string {
36+
return FILE_PATHS[id];
37+
}
38+
39+
protected getFrontmatter(id: SlashCommandId): string {
40+
return FRONTMATTER[id];
41+
}
42+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { ClaudeSlashCommandConfigurator } from './claude.js';
3+
import { CursorSlashCommandConfigurator } from './cursor.js';
4+
5+
export class SlashCommandRegistry {
6+
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
7+
8+
static {
9+
const claude = new ClaudeSlashCommandConfigurator();
10+
const cursor = new CursorSlashCommandConfigurator();
11+
12+
this.configurators.set(claude.toolId, claude);
13+
this.configurators.set(cursor.toolId, cursor);
14+
}
15+
16+
static register(configurator: SlashCommandConfigurator): void {
17+
this.configurators.set(configurator.toolId, configurator);
18+
}
19+
20+
static get(toolId: string): SlashCommandConfigurator | undefined {
21+
return this.configurators.get(toolId);
22+
}
23+
24+
static getAll(): SlashCommandConfigurator[] {
25+
return Array.from(this.configurators.values());
26+
}
27+
}

src/core/init.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ora from 'ora';
44
import { FileSystemUtils } from '../utils/file-system.js';
55
import { TemplateManager, ProjectContext } from './templates/index.js';
66
import { ToolRegistry } from './configurators/registry.js';
7+
import { SlashCommandRegistry } from './configurators/slash/registry.js';
78
import { OpenSpecConfig, AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
89

910
export class InitCommand {
@@ -105,6 +106,11 @@ export class InitCommand {
105106
if (configurator && configurator.isAvailable) {
106107
await configurator.configure(projectPath, openspecDir);
107108
}
109+
110+
const slashConfigurator = SlashCommandRegistry.get(toolId);
111+
if (slashConfigurator && slashConfigurator.isAvailable) {
112+
await slashConfigurator.generateAll(projectPath, openspecDir);
113+
}
108114
}
109115
}
110116

@@ -130,4 +136,4 @@ export class InitCommand {
130136
console.log(' and how I should work with you on this project"');
131137
console.log('────────────────────────────────────────────────────────────\n');
132138
}
133-
}
139+
}

src/core/templates/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readmeTemplate } from './readme-template.js';
22
import { projectTemplate, ProjectContext } from './project-template.js';
33
import { claudeTemplate } from './claude-template.js';
4+
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
45

56
export interface Template {
67
path: string;
@@ -24,6 +25,11 @@ export class TemplateManager {
2425
static getClaudeTemplate(): string {
2526
return claudeTemplate;
2627
}
28+
29+
static getSlashCommandBody(id: SlashCommandId): string {
30+
return getSlashCommandBody(id);
31+
}
2732
}
2833

29-
export { ProjectContext } from './project-template.js';
34+
export { ProjectContext } from './project-template.js';
35+
export type { SlashCommandId } from './slash-command-templates.js';

src/core/templates/readme-template.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,26 @@ Skip proposal for:
4040
- Configuration changes
4141
- Tests for existing behavior
4242
43+
**Workflow**
44+
1. Review \`openspec/project.md\`, \`openspec list\`, and \`openspec list --specs\` to understand current context.
45+
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and spec deltas under \`openspec/changes/<id>/\`.
46+
3. Draft spec deltas using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
47+
4. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.
48+
4349
### Stage 2: Implementing Changes
4450
1. **Read proposal.md** - Understand what's being built
4551
2. **Read design.md** (if exists) - Review technical decisions
4652
3. **Read tasks.md** - Get implementation checklist
4753
4. **Implement tasks sequentially** - Complete in order
4854
5. **Mark complete immediately** - Update \`- [x]\` after each task
49-
6. **Validate strictly** - Run \`openspec validate [change] --strict\` and address issues
50-
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
55+
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
5156
5257
### Stage 3: Archiving Changes
5358
After deployment, create separate PR to:
5459
- Move \`changes/[name]/\` → \`changes/archive/YYYY-MM-DD-[name]/\`
5560
- Update \`specs/\` if capabilities changed
5661
- Use \`openspec archive [change] --skip-specs\` for tooling-only changes
62+
- Run \`openspec validate --strict\` to confirm the archived change passes checks
5763
5864
## Before Any Task
5965

0 commit comments

Comments
 (0)