From 93ad6f5fd58b09b5693541372511a3b5d7d199a5 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 15 May 2026 16:33:40 -0700 Subject: [PATCH 1/2] fix: only show 'Restart your IDE' message for IDE-embedded tools Closes #1067 --- .changeset/restart-ide-hint.md | 7 +++++++ src/core/config.ts | 23 ++++++++++++----------- src/core/init.ts | 20 +++++++++++++++----- test/core/init.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 .changeset/restart-ide-hint.md diff --git a/.changeset/restart-ide-hint.md b/.changeset/restart-ide-hint.md new file mode 100644 index 000000000..4db7a2866 --- /dev/null +++ b/.changeset/restart-ide-hint.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": patch +--- + +### Bug Fixes + +- `openspec init` now suggests an IDE restart only when an IDE-resident tool such as Cursor, GitHub Copilot, Continue, or Windsurf was configured. diff --git a/src/core/config.ts b/src/core/config.ts index 68f1abd33..f930fe3b6 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -16,28 +16,29 @@ export interface AIToolOption { successLabel?: string; skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection + requiresIdeRestart?: boolean; // True when slash commands are loaded by an IDE/editor process } export const AI_TOOLS: AIToolOption[] = [ - { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' }, + { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true }, { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' }, { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' }, { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' }, { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' }, - { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' }, + { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline', requiresIdeRestart: true }, { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' }, { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' }, { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' }, - { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' }, - { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' }, + { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue', requiresIdeRestart: true }, + { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec', requiresIdeRestart: true }, { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' }, - { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' }, + { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor', requiresIdeRestart: true }, { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' }, { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' }, - { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] }, + { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true }, { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' }, - { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' }, - { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' }, + { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true }, + { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode', requiresIdeRestart: true }, { name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' }, { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, @@ -45,8 +46,8 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, - { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' }, - { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, - { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' }, + { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo', requiresIdeRestart: true }, + { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true }, + { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf', requiresIdeRestart: true }, { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/init.ts b/src/core/init.ts index aa38408f2..fea61264b 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -85,6 +85,14 @@ type InitCommandOptions = { profile?: string; }; +type SelectedTool = { + value: string; + name: string; + skillsDir: string; + wasConfigured: boolean; + requiresIdeRestart?: boolean; +}; + // ----------------------------------------------------------------------------- // Init Command Class // ----------------------------------------------------------------------------- @@ -417,8 +425,8 @@ export class InitCommand { private validateTools( toolIds: string[], toolStates: Map - ): Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> { - const validatedTools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> = []; + ): SelectedTool[] { + const validatedTools: SelectedTool[] = []; for (const toolId of toolIds) { const tool = AI_TOOLS.find((t) => t.value === toolId); @@ -442,6 +450,7 @@ export class InitCommand { name: tool.name, skillsDir: tool.skillsDir, wasConfigured: preState?.configured ?? false, + requiresIdeRestart: tool.requiresIdeRestart, }); } @@ -493,7 +502,7 @@ export class InitCommand { private async generateSkillsAndCommands( projectPath: string, - tools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> + tools: SelectedTool[] ): Promise<{ createdTools: typeof tools; refreshedTools: typeof tools; @@ -716,8 +725,9 @@ export class InitCommand { console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`); console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`); - // Restart instruction if any tools were configured - if (results.createdTools.length > 0 || results.refreshedTools.length > 0) { + // Restart instruction for tools whose slash commands are loaded by an IDE/editor + const configuredTools = [...results.createdTools, ...results.refreshedTools]; + if (configuredTools.some((tool) => tool.requiresIdeRestart)) { console.log(); console.log(chalk.white('Restart your IDE for slash commands to take effect.')); } diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6a436eaed..4c9be4249 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -192,6 +192,22 @@ describe('InitCommand', () => { ).toBe(true); }); + it('should not suggest an IDE restart for CLI-only tools', async () => { + const initCommand = new InitCommand({ tools: 'claude', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).not.toContain('Restart your IDE for slash commands to take effect.'); + }); + + it('should suggest an IDE restart for IDE-resident tools', async () => { + const initCommand = new InitCommand({ tools: 'cursor', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).toContain('Restart your IDE for slash commands to take effect.'); + }); + it('should create skills for multiple tools at once', async () => { const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); @@ -784,3 +800,10 @@ async function directoryExists(dirPath: string): Promise { return false; } } + +function getConsoleOutput(): string { + return (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls + .flat() + .map(String) + .join('\n'); +} From 4c7c8ac564cf5a236542af5bc4dc7cc9de7b5c86 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 15 May 2026 17:34:57 -0700 Subject: [PATCH 2/2] address CodeRabbit review: type tools parameter as SelectedTool[] displaySuccessMessage accessed tool.requiresIdeRestart on objects derived from results.createdTools / results.refreshedTools, but the parameter type was an inline anonymous shape that didn't include the field. Use the existing SelectedTool type so the access is type-safe. --- src/core/init.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/init.ts b/src/core/init.ts index fea61264b..5ec2895c9 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -634,10 +634,10 @@ export class InitCommand { private displaySuccessMessage( projectPath: string, - tools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }>, + tools: SelectedTool[], results: { - createdTools: typeof tools; - refreshedTools: typeof tools; + createdTools: SelectedTool[]; + refreshedTools: SelectedTool[]; failedTools: Array<{ name: string; error: Error }>; commandsSkipped: string[]; removedCommandCount: number;