Skip to content

Commit 19ccaab

Browse files
authored
feat(iflow-cli): add iFlow-cli integration (#268)
* support iflow-cli * docs: add iFlow to supported AI tools in README ([#268](#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
1 parent 2e382b9 commit 19ccaab

File tree

8 files changed

+173
-0
lines changed

8 files changed

+173
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
109109
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
110110
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
111111
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
112+
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |
112113

113114

114115
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`.

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const AI_TOOLS: AIToolOption[] = [
2727
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
2828
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
2929
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
30+
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
3031
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
3132
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
3233
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },

src/core/configurators/iflow.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import path from "path";
2+
import { ToolConfigurator } from "./base.js";
3+
import { FileSystemUtils } from "../../utils/file-system.js";
4+
import { TemplateManager } from "../templates/index.js";
5+
import { OPENSPEC_MARKERS } from "../config.js";
6+
7+
export class IflowConfigurator implements ToolConfigurator {
8+
name = "iFlow";
9+
configFileName = "IFLOW.md";
10+
isAvailable = true;
11+
12+
async configure(projectPath: string, openspecDir: string): Promise<void> {
13+
const filePath = path.join(projectPath, this.configFileName);
14+
const content = TemplateManager.getClaudeTemplate();
15+
16+
await FileSystemUtils.updateFileWithMarkers(
17+
filePath,
18+
content,
19+
OPENSPEC_MARKERS.start,
20+
OPENSPEC_MARKERS.end
21+
);
22+
}
23+
}

src/core/configurators/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ClineConfigurator } from './cline.js';
44
import { CodeBuddyConfigurator } from './codebuddy.js';
55
import { CostrictConfigurator } from './costrict.js';
66
import { QoderConfigurator } from './qoder.js';
7+
import { IflowConfigurator } from './iflow.js';
78
import { AgentsStandardConfigurator } from './agents.js';
89
import { QwenConfigurator } from './qwen.js';
910

@@ -16,6 +17,7 @@ export class ToolRegistry {
1617
const codeBuddyConfigurator = new CodeBuddyConfigurator();
1718
const costrictConfigurator = new CostrictConfigurator();
1819
const qoderConfigurator = new QoderConfigurator();
20+
const iflowConfigurator = new IflowConfigurator();
1921
const agentsConfigurator = new AgentsStandardConfigurator();
2022
const qwenConfigurator = new QwenConfigurator();
2123
// Register with the ID that matches the checkbox value
@@ -24,6 +26,7 @@ export class ToolRegistry {
2426
this.tools.set('codebuddy', codeBuddyConfigurator);
2527
this.tools.set('costrict', costrictConfigurator);
2628
this.tools.set('qoder', qoderConfigurator);
29+
this.tools.set('iflow', iflowConfigurator);
2730
this.tools.set('agents', agentsConfigurator);
2831
this.tools.set('qwen', qwenConfigurator);
2932
}
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: '.iflow/commands/openspec-proposal.md',
6+
apply: '.iflow/commands/openspec-apply.md',
7+
archive: '.iflow/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 IflowSlashCommandConfigurator extends SlashCommandConfigurator {
32+
readonly toolId = 'iflow';
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+
}

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CostrictSlashCommandConfigurator } from './costrict.js';
1818
import { QwenSlashCommandConfigurator } from './qwen.js';
1919
import { RooCodeSlashCommandConfigurator } from './roocode.js';
2020
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
21+
import { IflowSlashCommandConfigurator } from './iflow.js';
2122

2223
export class SlashCommandRegistry {
2324
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -42,6 +43,7 @@ export class SlashCommandRegistry {
4243
const qwen = new QwenSlashCommandConfigurator();
4344
const roocode = new RooCodeSlashCommandConfigurator();
4445
const antigravity = new AntigravitySlashCommandConfigurator();
46+
const iflow = new IflowSlashCommandConfigurator();
4547

4648
this.configurators.set(claude.toolId, claude);
4749
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -62,6 +64,7 @@ export class SlashCommandRegistry {
6264
this.configurators.set(qwen.toolId, qwen);
6365
this.configurators.set(roocode.toolId, roocode);
6466
this.configurators.set(antigravity.toolId, antigravity);
67+
this.configurators.set(iflow.toolId, iflow);
6568
}
6669

6770
static register(configurator: SlashCommandConfigurator): void {

test/core/init.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,59 @@ describe('InitCommand', () => {
416416
expect(updatedContent).not.toContain('Custom instruction added by user');
417417
});
418418

419+
it('should create IFlow CLI slash command files with templates', async () => {
420+
queueSelections('iflow', DONE);
421+
await initCommand.execute(testDir);
422+
423+
const iflowProposal = path.join(
424+
testDir,
425+
'.iflow/commands/openspec-proposal.md'
426+
);
427+
const iflowApply = path.join(
428+
testDir,
429+
'.iflow/commands/openspec-apply.md'
430+
);
431+
const iflowArchive = path.join(
432+
testDir,
433+
'.iflow/commands/openspec-archive.md'
434+
);
435+
436+
expect(await fileExists(iflowProposal)).toBe(true);
437+
expect(await fileExists(iflowApply)).toBe(true);
438+
expect(await fileExists(iflowArchive)).toBe(true);
439+
440+
const proposalContent = await fs.readFile(iflowProposal, 'utf-8');
441+
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
442+
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
443+
expect(proposalContent).toContain('**Guardrails**');
444+
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
445+
446+
const applyContent = await fs.readFile(iflowApply, 'utf-8');
447+
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
448+
expect(applyContent).toContain('Work through tasks sequentially');
449+
450+
const archiveContent = await fs.readFile(iflowArchive, 'utf-8');
451+
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
452+
expect(archiveContent).toContain('openspec archive <id>');
453+
});
454+
455+
it('should update existing IFLOW.md with markers', async () => {
456+
queueSelections('iflow', DONE);
457+
458+
const iflowPath = path.join(testDir, 'IFLOW.md');
459+
const existingContent = '# My IFLOW Instructions\nCustom instructions here';
460+
await fs.writeFile(iflowPath, existingContent);
461+
462+
await initCommand.execute(testDir);
463+
464+
const updatedContent = await fs.readFile(iflowPath, 'utf-8');
465+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
466+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
467+
expect(updatedContent).toContain('openspec update');
468+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
469+
expect(updatedContent).toContain('Custom instructions here');
470+
});
471+
419472
it('should create OpenCode slash command files with templates', async () => {
420473
queueSelections('opencode', DONE);
421474

test/core/update.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,53 @@ Old Gemini body
663663

664664
consoleSpy.mockRestore();
665665
});
666+
667+
it('should refresh existing IFLOW slash commands', async () => {
668+
const iflowProposal = path.join(
669+
testDir,
670+
'.iflow/commands/openspec-proposal.md'
671+
);
672+
await fs.mkdir(path.dirname(iflowProposal), { recursive: true });
673+
const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."
674+
675+
prompt = """
676+
<!-- OPENSPEC:START -->
677+
Old IFlow body
678+
<!-- OPENSPEC:END -->
679+
"""
680+
`;
681+
await fs.writeFile(iflowProposal, initialContent);
682+
683+
const consoleSpy = vi.spyOn(console, 'log');
684+
685+
await updateCommand.execute(testDir);
686+
687+
const updated = await fs.readFile(iflowProposal, 'utf-8');
688+
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
689+
expect(updated).toContain('<!-- OPENSPEC:START -->');
690+
expect(updated).toContain('**Guardrails**');
691+
expect(updated).toContain('<!-- OPENSPEC:END -->');
692+
expect(updated).not.toContain('Old IFlow body');
693+
694+
const iflowApply = path.join(
695+
testDir,
696+
'.iflow/commands/openspec-apply.md'
697+
);
698+
const iflowArchive = path.join(
699+
testDir,
700+
'.iflow/commands/openspec-archive.md'
701+
);
702+
703+
await expect(FileSystemUtils.fileExists(iflowApply)).resolves.toBe(false);
704+
await expect(FileSystemUtils.fileExists(iflowArchive)).resolves.toBe(false);
705+
706+
const [logMessage] = consoleSpy.mock.calls[0];
707+
expect(logMessage).toContain(
708+
'Updated slash commands: .iflow/commands/openspec-proposal.md'
709+
);
710+
711+
consoleSpy.mockRestore();
712+
});
666713

667714
it('should refresh existing Factory slash commands', async () => {
668715
const factoryPath = path.join(

0 commit comments

Comments
 (0)