Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/restart-ide-hint.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 12 additions & 11 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,38 @@ 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' },
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
{ 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' }
];
26 changes: 18 additions & 8 deletions src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ type InitCommandOptions = {
profile?: string;
};

type SelectedTool = {
value: string;
name: string;
skillsDir: string;
wasConfigured: boolean;
requiresIdeRestart?: boolean;
};

// -----------------------------------------------------------------------------
// Init Command Class
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -417,8 +425,8 @@ export class InitCommand {
private validateTools(
toolIds: string[],
toolStates: Map<string, ToolSkillStatus>
): 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);
Expand All @@ -442,6 +450,7 @@ export class InitCommand {
name: tool.name,
skillsDir: tool.skillsDir,
wasConfigured: preState?.configured ?? false,
requiresIdeRestart: tool.requiresIdeRestart,
});
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -625,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;
Expand Down Expand Up @@ -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.'));
}
Expand Down
23 changes: 23 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -784,3 +800,10 @@ async function directoryExists(dirPath: string): Promise<boolean> {
return false;
}
}

function getConsoleOutput(): string {
return (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls
.flat()
.map(String)
.join('\n');
}