From b1a1c68c5f74a525d58b8c2b0733f4effbb9ff84 Mon Sep 17 00:00:00 2001 From: dkmos2016 Date: Sat, 1 Nov 2025 23:25:02 +0800 Subject: [PATCH 1/3] support iflow-cli --- src/core/config.ts | 1 + src/core/configurators/iflow.ts | 23 +++++++++++++ src/core/configurators/registry.ts | 3 ++ src/core/configurators/slash/iflow.ts | 42 ++++++++++++++++++++++++ src/core/configurators/slash/registry.ts | 3 ++ 5 files changed, 72 insertions(+) create mode 100644 src/core/configurators/iflow.ts create mode 100644 src/core/configurators/slash/iflow.ts diff --git a/src/core/config.ts b/src/core/config.ts index d0bc2288..20218821 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -27,6 +27,7 @@ export const AI_TOOLS: AIToolOption[] = [ { 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: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' }, { 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' }, diff --git a/src/core/configurators/iflow.ts b/src/core/configurators/iflow.ts new file mode 100644 index 00000000..1ca97442 --- /dev/null +++ b/src/core/configurators/iflow.ts @@ -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 { + const filePath = path.join(projectPath, this.configFileName); + const content = TemplateManager.getClaudeTemplate(); + + await FileSystemUtils.updateFileWithMarkers( + filePath, + content, + OPENSPEC_MARKERS.start, + OPENSPEC_MARKERS.end + ); + } +} diff --git a/src/core/configurators/registry.ts b/src/core/configurators/registry.ts index b1be2072..70a1a207 100644 --- a/src/core/configurators/registry.ts +++ b/src/core/configurators/registry.ts @@ -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); } diff --git a/src/core/configurators/slash/iflow.ts b/src/core/configurators/slash/iflow.ts new file mode 100644 index 00000000..c7c79618 --- /dev/null +++ b/src/core/configurators/slash/iflow.ts @@ -0,0 +1,42 @@ +import { SlashCommandConfigurator } from './base.js'; +import { SlashCommandId } from '../../templates/index.js'; + +const FILE_PATHS: Record = { + proposal: '.iflow/commands/openspec-proposal.md', + apply: '.iflow/commands/openspec-apply.md', + archive: '.iflow/commands/openspec-archive.md' +}; + +const FRONTMATTER: Record = { + 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]; + } +} diff --git a/src/core/configurators/slash/registry.ts b/src/core/configurators/slash/registry.ts index 45f9dae5..8020940e 100644 --- a/src/core/configurators/slash/registry.ts +++ b/src/core/configurators/slash/registry.ts @@ -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 = 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 { From c946706fda7df4220dbbb33db0406056a9114623 Mon Sep 17 00:00:00 2001 From: dkmos2016 Date: Tue, 4 Nov 2025 14:21:50 +0800 Subject: [PATCH 2/3] 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. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2590c7d0..ecaebf4e 100644 --- a/README.md +++ b/README.md @@ -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`. From 705c34a04442cbfe5394d9d0a6804bbc24220403 Mon Sep 17 00:00:00 2001 From: dkmos2016 Date: Thu, 20 Nov 2025 17:30:24 +0800 Subject: [PATCH 3/3] add UTs for iflow-cli --- test/core/init.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++ test/core/update.test.ts | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 1ebe4471..43fc6fb2 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -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(''); + expect(proposalContent).toContain('**Guardrails**'); + expect(proposalContent).toContain(''); + + 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 '); + }); + + 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(''); + expect(updatedContent).toContain("@/openspec/AGENTS.md"); + expect(updatedContent).toContain('openspec update'); + expect(updatedContent).toContain(''); + expect(updatedContent).toContain('Custom instructions here'); + }); + it('should create OpenCode slash command files with templates', async () => { queueSelections('opencode', DONE); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index b53cca9c..b6fe974c 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -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 = """ + +Old IFlow body + +""" +`; + 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(''); + expect(updated).toContain('**Guardrails**'); + expect(updated).toContain(''); + 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(