From e3805a2a23da71e9fa9744fd9ec87c1885f3fcc6 Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Wed, 13 May 2026 15:22:03 +0200 Subject: [PATCH 1/2] fix: allow uninstalling managed skills --- src/main/core/skills/SkillsService.ts | 39 ++++++++++++++----- .../skills/components/SkillDetailModal.tsx | 4 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/main/core/skills/SkillsService.ts b/src/main/core/skills/SkillsService.ts index e8d7af87b..c628e1c94 100644 --- a/src/main/core/skills/SkillsService.ts +++ b/src/main/core/skills/SkillsService.ts @@ -287,15 +287,23 @@ export class SkillsService { async uninstallSkill(skillId: string): Promise { const skillDir = path.join(SKILLS_ROOT, skillId); - // Remove agent symlinks first + // Remove agent symlinks first. Never delete real directories from agent config paths — + // those may be user-managed skills that Emdash only discovered. await this.unsyncFromAgents(skillId); - // Remove skill directory try { - await fs.promises.rm(skillDir, { recursive: true, force: true }); + const stat = await fs.promises.lstat(skillDir); + if (stat.isSymbolicLink()) { + await fs.promises.unlink(skillDir); + } else if (stat.isDirectory()) { + await fs.promises.rm(skillDir, { recursive: true, force: true }); + } } catch (error) { - log.error(`Failed to remove skill directory ${skillDir}:`, error); - throw error; + const code = (error as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') { + log.error(`Failed to remove skill directory ${skillDir}:`, error); + throw error; + } } // Invalidate cache @@ -376,19 +384,23 @@ export class SkillsService { } async unsyncFromAgents(skillId: string): Promise { - for (const target of agentTargets) { + const syncPaths = [ + ...agentTargets.map((target) => target.getSkillDir(skillId)), + ...skillScanPaths.map((scanPath) => path.join(scanPath, skillId)), + ]; + + for (const targetDir of new Set(syncPaths)) { try { - const targetDir = target.getSkillDir(skillId); const stat = await fs.promises.lstat(targetDir); if (stat.isSymbolicLink()) { - // Only remove symlinks that point into our central skills root + // Only remove symlinks that point into our central skills root. const linkTarget = await fs.promises.readlink(targetDir); const resolved = path.resolve(path.dirname(targetDir), linkTarget); - if (resolved.startsWith(SKILLS_ROOT)) { + if (this.isPathInsideSkillsRoot(resolved)) { await fs.promises.unlink(targetDir); } } - // Never rm -rf real directories in agent config — they may be user-managed + // Never rm -rf real directories in agent config — they may be user-managed. } catch { // Doesn't exist or can't remove — skip } @@ -417,6 +429,13 @@ export class SkillsService { // --- Private helpers --- + private isPathInsideSkillsRoot(candidatePath: string): boolean { + const relativePath = path.relative(SKILLS_ROOT, candidatePath); + return ( + relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) + ); + } + private loadBundledCatalog(): CatalogIndex { return bundledCatalog as CatalogIndex; } diff --git a/src/renderer/features/skills/components/SkillDetailModal.tsx b/src/renderer/features/skills/components/SkillDetailModal.tsx index e5a83291e..6aef5ae4e 100644 --- a/src/renderer/features/skills/components/SkillDetailModal.tsx +++ b/src/renderer/features/skills/components/SkillDetailModal.tsx @@ -49,8 +49,8 @@ const SkillDetailModal: React.FC = ({ if (!skill) return; setIsProcessing(true); try { - await onUninstall(skill.id); - onClose(); + const success = await onUninstall(skill.id); + if (success) onClose(); } finally { setIsProcessing(false); } From 2a8c4ec9fcd067359964f955b2f2cfbcd048685f Mon Sep 17 00:00:00 2001 From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com> Date: Wed, 13 May 2026 19:16:00 +0200 Subject: [PATCH 2/2] fix: address skill uninstall review feedback --- src/main/core/skills/SkillsService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/core/skills/SkillsService.ts b/src/main/core/skills/SkillsService.ts index c628e1c94..4a4424529 100644 --- a/src/main/core/skills/SkillsService.ts +++ b/src/main/core/skills/SkillsService.ts @@ -297,6 +297,8 @@ export class SkillsService { await fs.promises.unlink(skillDir); } else if (stat.isDirectory()) { await fs.promises.rm(skillDir, { recursive: true, force: true }); + } else { + log.warn(`Unexpected entry type at ${skillDir} during uninstall — skipping`); } } catch (error) { const code = (error as NodeJS.ErrnoException).code; @@ -431,9 +433,7 @@ export class SkillsService { private isPathInsideSkillsRoot(candidatePath: string): boolean { const relativePath = path.relative(SKILLS_ROOT, candidatePath); - return ( - relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) - ); + return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); } private loadBundledCatalog(): CatalogIndex {