Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale fdcf9621db feat(init): add IDE restart instruction after init
Add prominent restart instruction in success message to inform users
they need to restart their IDE/coding tool for slash commands to appear.
Applies to all tools when slash commands are created or refreshed.

Also updates cli-init spec to document the restart instruction requirement.
2025-11-21 00:28:21 +11:00
dkmos2016 19ccaabfc7 feat(iflow-cli): add iFlow-cli integration (#268)
* support iflow-cli

* docs: add iFlow to supported AI tools in README ([#268](https://github.com/Fission-AI/OpenSpec/pull/268))

Add iFlow to the Native Slash Commands table in the README. iFlow support was implemented but was missing from the documentation.

* add UTs for iflow-cli
2025-11-20 21:57:58 +11:00
Tabish Bidiwale 2e382b9898 Add Antigravity slash command support (#318) 2025-11-19 17:18:52 +11:00
11 changed files with 224 additions and 8 deletions
+1
View File
@@ -109,6 +109,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/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`.
+20
View File
@@ -76,6 +76,12 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Configuring iFlow CLI
- **WHEN** iFlow CLI is selected
- **THEN** create or update `IFLOW.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
@@ -118,6 +124,12 @@ The command SHALL provide clear, actionable next steps upon successful initializ
- **WHEN** initialization completes successfully
- **THEN** include prompt: "Please explain the OpenSpec workflow from openspec/AGENTS.md and how I should work with you on this project"
#### Scenario: Displaying restart instruction
- **WHEN** initialization completes successfully and tools were created or refreshed
- **THEN** display a prominent restart instruction before the "Next steps" section
- **AND** inform users that slash commands are loaded at startup
- **AND** instruct users to restart their coding assistant to ensure /openspec commands appear
### Requirement: Exit Codes
The command SHALL use consistent exit codes to indicate different failure modes.
@@ -238,6 +250,14 @@ The init command SHALL generate slash command files for supported editors using
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
#### Scenario: Generating slash commands for iFlow CLI
- **WHEN** the user selects iFlow CLI during initialization
- **THEN** create `.iflow/commands/openspec-proposal.md`, `.iflow/commands/openspec-apply.md`, and `.iflow/commands/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include YAML frontmatter with `name`, `id`, `category`, and `description` fields for each command
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for RooCode
- **WHEN** the user selects RooCode during initialization
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
+7
View File
@@ -123,6 +123,13 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
#### Scenario: Updating slash commands for iFlow CLI
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
+9 -8
View File
@@ -17,24 +17,25 @@ export interface AIToolOption {
}
export const AI_TOOLS: AIToolOption[] = [
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' },
{ 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: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
{ 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: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
+23
View File
@@ -0,0 +1,23 @@
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 IflowConfigurator implements ToolConfigurator {
name = "iFlow";
configFileName = "IFLOW.md";
isAvailable = true;
async configure(projectPath: string, openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getClaudeTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}
+3
View File
@@ -4,6 +4,7 @@ import { ClineConfigurator } from './cline.js';
import { CodeBuddyConfigurator } from './codebuddy.js';
import { CostrictConfigurator } from './costrict.js';
import { QoderConfigurator } from './qoder.js';
import { IflowConfigurator } from './iflow.js';
import { AgentsStandardConfigurator } from './agents.js';
import { QwenConfigurator } from './qwen.js';
@@ -16,6 +17,7 @@ export class ToolRegistry {
const codeBuddyConfigurator = new CodeBuddyConfigurator();
const costrictConfigurator = new CostrictConfigurator();
const qoderConfigurator = new QoderConfigurator();
const iflowConfigurator = new IflowConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
const qwenConfigurator = new QwenConfigurator();
// Register with the ID that matches the checkbox value
@@ -24,6 +26,7 @@ export class ToolRegistry {
this.tools.set('codebuddy', codeBuddyConfigurator);
this.tools.set('costrict', costrictConfigurator);
this.tools.set('qoder', qoderConfigurator);
this.tools.set('iflow', iflowConfigurator);
this.tools.set('agents', agentsConfigurator);
this.tools.set('qwen', qwenConfigurator);
}
+42
View File
@@ -0,0 +1,42 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.iflow/commands/openspec-proposal.md',
apply: '.iflow/commands/openspec-apply.md',
archive: '.iflow/commands/openspec-archive.md'
};
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---`
};
export class IflowSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'iflow';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+3
View File
@@ -18,6 +18,7 @@ import { CostrictSlashCommandConfigurator } from './costrict.js';
import { QwenSlashCommandConfigurator } from './qwen.js';
import { RooCodeSlashCommandConfigurator } from './roocode.js';
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
import { IflowSlashCommandConfigurator } from './iflow.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -42,6 +43,7 @@ export class SlashCommandRegistry {
const qwen = new QwenSlashCommandConfigurator();
const roocode = new RooCodeSlashCommandConfigurator();
const antigravity = new AntigravitySlashCommandConfigurator();
const iflow = new IflowSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -62,6 +64,7 @@ export class SlashCommandRegistry {
this.configurators.set(qwen.toolId, qwen);
this.configurators.set(roocode.toolId, roocode);
this.configurators.set(antigravity.toolId, antigravity);
this.configurators.set(iflow.toolId, iflow);
}
static register(configurator: SlashCommandConfigurator): void {
+16
View File
@@ -862,6 +862,22 @@ export class InitCommand {
)
);
// Show restart instruction if any tools were configured
if (created.length > 0 || refreshed.length > 0) {
console.log();
console.log(PALETTE.white('Important: Restart your IDE'));
console.log(
PALETTE.midGray(
'Slash commands are loaded at startup. Please restart your coding assistant'
)
);
console.log(
PALETTE.midGray(
'to ensure the new /openspec commands appear in your command palette.'
)
);
}
// Get the selected tool name(s) for display
const toolName = this.formatToolNames(selectedTools);
+53
View File
@@ -416,6 +416,59 @@ describe('InitCommand', () => {
expect(updatedContent).not.toContain('Custom instruction added by user');
});
it('should create IFlow CLI slash command files with templates', async () => {
queueSelections('iflow', DONE);
await initCommand.execute(testDir);
const iflowProposal = path.join(
testDir,
'.iflow/commands/openspec-proposal.md'
);
const iflowApply = path.join(
testDir,
'.iflow/commands/openspec-apply.md'
);
const iflowArchive = path.join(
testDir,
'.iflow/commands/openspec-archive.md'
);
expect(await fileExists(iflowProposal)).toBe(true);
expect(await fileExists(iflowApply)).toBe(true);
expect(await fileExists(iflowArchive)).toBe(true);
const proposalContent = await fs.readFile(iflowProposal, 'utf-8');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
const applyContent = await fs.readFile(iflowApply, 'utf-8');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(iflowArchive, 'utf-8');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should update existing IFLOW.md with markers', async () => {
queueSelections('iflow', DONE);
const iflowPath = path.join(testDir, 'IFLOW.md');
const existingContent = '# My IFLOW Instructions\nCustom instructions here';
await fs.writeFile(iflowPath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(iflowPath, '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 instructions here');
});
it('should create OpenCode slash command files with templates', async () => {
queueSelections('opencode', DONE);
+47
View File
@@ -663,6 +663,53 @@ Old Gemini body
consoleSpy.mockRestore();
});
it('should refresh existing IFLOW slash commands', async () => {
const iflowProposal = path.join(
testDir,
'.iflow/commands/openspec-proposal.md'
);
await fs.mkdir(path.dirname(iflowProposal), { recursive: true });
const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."
prompt = """
<!-- OPENSPEC:START -->
Old IFlow body
<!-- OPENSPEC:END -->
"""
`;
await fs.writeFile(iflowProposal, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(iflowProposal, 'utf-8');
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(updated).toContain('<!-- OPENSPEC:START -->');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain('<!-- OPENSPEC:END -->');
expect(updated).not.toContain('Old IFlow body');
const iflowApply = path.join(
testDir,
'.iflow/commands/openspec-apply.md'
);
const iflowArchive = path.join(
testDir,
'.iflow/commands/openspec-archive.md'
);
await expect(FileSystemUtils.fileExists(iflowApply)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(iflowArchive)).resolves.toBe(false);
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .iflow/commands/openspec-proposal.md'
);
consoleSpy.mockRestore();
});
it('should refresh existing Factory slash commands', async () => {
const factoryPath = path.join(