mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
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:
@@ -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
@@ -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(', ')}`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user