fix(update): only suggest IDE restarts when needed (#1656)

* fix(update): only suggest IDE restarts when needed

* test(update): cover restart hint edge cases
This commit is contained in:
Clay Good
2026-08-19 20:19:06 +00:00
committed by GitHub
parent a2b965aa5e
commit a72a74de65
3 changed files with 87 additions and 3 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": patch
---
### Bug Fixes
- `openspec update` now suggests restarting an IDE only when it updates an IDE-resident tool. CLI tools such as Claude Code, Codex, and Gemini CLI no longer show an unnecessary restart hint.
+16 -1
View File
@@ -267,6 +267,7 @@ export class UpdateCommand {
// 10. Update tools (all if force, otherwise only those needing update)
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
const updatedTools: string[] = [];
const updatedToolIds: string[] = [];
const failedTools: Array<{ name: string; error: string }> = [];
const skillsInvocableCommandSkips: string[] = [];
const zeroArtifactTools: string[] = [];
@@ -361,6 +362,7 @@ export class UpdateCommand {
spinner.succeed(`Updated ${tool.name}`);
updatedTools.push(tool.name);
updatedToolIds.push(tool.value);
for (const migration of migrateLegacyToolDirs(
resolvedProjectPath,
[tool.value],
@@ -484,7 +486,20 @@ export class UpdateCommand {
}
console.log();
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
const affectedToolIds = [...new Set([...newlyConfiguredTools, ...updatedToolIds])];
const shouldRestartIde = affectedToolIds.some((toolId) => {
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
return Boolean(
tool?.requiresIdeRestart &&
(
shouldGenerateCommandsForTool(toolId, delivery) ||
shouldGenerateSkillsForTool(toolId, delivery)
)
);
});
if (shouldRestartIde) {
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
}
if (failedTools.length > 0) {
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
}
+64 -2
View File
@@ -1675,9 +1675,43 @@ metadata:
expect.stringContaining('Failed')
);
// Cursor succeeded, so its IDE process still needs to reload the changes.
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Restart your IDE')
);
writeSpy.mockRestore();
consoleSpy.mockRestore();
});
it('should not suggest an IDE restart when only the IDE tool fails', async () => {
const claudeSkill = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md');
const cursorSkill = path.join(testDir, '.cursor', 'skills', 'openspec-explore', 'SKILL.md');
await fs.mkdir(path.dirname(claudeSkill), { recursive: true });
await fs.mkdir(path.dirname(cursorSkill), { recursive: true });
await fs.writeFile(claudeSkill, 'old');
await fs.writeFile(cursorSkill, 'old');
const originalWriteFile = FileSystemUtils.writeFile.bind(FileSystemUtils);
vi.spyOn(FileSystemUtils, 'writeFile').mockImplementation(async (filePath, content) => {
if (filePath.includes('.cursor') && filePath.includes('SKILL.md')) {
throw new Error('EACCES: permission denied');
}
return originalWriteFile(filePath, content);
});
const consoleSpy = vi.spyOn(console, 'log');
await expect(updateCommand.execute(testDir)).rejects.toThrow(
'OpenSpec update failed for: Cursor'
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Updated: Claude Code')
);
expect(consoleSpy).not.toHaveBeenCalledWith(
expect.stringContaining('Restart your IDE')
);
});
});
describe('tool detection', () => {
@@ -1800,8 +1834,8 @@ metadata:
consoleSpy.mockRestore();
});
it('should suggest IDE restart after update', async () => {
// Set up a configured tool
it('should not suggest an IDE restart for CLI-only tools', async () => {
// Set up a configured CLI tool
const skillsDir = path.join(testDir, '.claude', 'skills');
await fs.mkdir(path.join(skillsDir, 'openspec-explore'), {
recursive: true,
@@ -1815,6 +1849,27 @@ metadata:
await updateCommand.execute(testDir);
expect(consoleSpy).not.toHaveBeenCalledWith(
expect.stringContaining('Restart your IDE')
);
consoleSpy.mockRestore();
});
it('should suggest an IDE restart for IDE-resident tools', async () => {
const skillsDir = path.join(testDir, '.cursor', 'skills');
await fs.mkdir(path.join(skillsDir, 'openspec-explore'), {
recursive: true,
});
await fs.writeFile(
path.join(skillsDir, 'openspec-explore', 'SKILL.md'),
'old'
);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Restart your IDE')
);
@@ -2199,6 +2254,11 @@ metadata:
expect.stringContaining('Already up to date: cursor')
);
// A configured IDE tool that was not affected must not cause the hint.
expect(consoleSpy).not.toHaveBeenCalledWith(
expect.stringContaining('Restart your IDE')
);
consoleSpy.mockRestore();
});
});
@@ -2392,6 +2452,7 @@ ${OPENSPEC_MARKERS.end}
expect(menuLines).toHaveLength(1);
expect(menuLines[0]).toContain('/opsx-propose');
expect(logCalls.some((entry) => entry.includes('/opsx:propose'))).toBe(false);
expect(logCalls.some((entry) => entry.includes('Restart your IDE'))).toBe(true);
});
it('should preserve legacy Codex prompts when a configured Codex tool lacks the replacement workflow', async () => {
@@ -2691,6 +2752,7 @@ More user content after markers.
.join('\n');
expect(gettingStartedCalls).not.toContain('/opsx:new');
expect(gettingStartedCalls).not.toContain('/opsx:continue');
expect(gettingStartedCalls).not.toContain('Restart your IDE');
// Skills should be created
const skillFile = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md');