diff --git a/src/cli/workspace-seed.test.ts b/src/cli/workspace-seed.test.ts index d241cfed9e35b..d879d280b16f4 100644 --- a/src/cli/workspace-seed.test.ts +++ b/src/cli/workspace-seed.test.ts @@ -33,6 +33,15 @@ function createPackageRoot(tempDir: string): string { return pkgRoot; } +const LEGACY_BROWSER_SKILL_CONTENT = `--- +name: browser-automation +--- + +# Browser Automation + +- **COPY THAT USER'S DEFAULT CHROME PROFILE, INTO YOUR OWN CHROME PROFILE** +`; + describe("seedWorkspaceFromAssets", () => { let tempDir: string; @@ -178,6 +187,36 @@ describe("syncManagedSkills", () => { expect(existsSync(workspaceDir)).toBe(true); }); + it("removes the retired bundled browser skill from existing workspaces", () => { + const packageRoot = createPackageRoot(tempDir); + const workspaceDir = path.join(tempDir, "workspace-prune"); + const browserSkillDir = path.join(workspaceDir, "skills", "browser"); + mkdirSync(browserSkillDir, { recursive: true }); + writeFileSync( + path.join(browserSkillDir, "SKILL.md"), + LEGACY_BROWSER_SKILL_CONTENT, + "utf-8", + ); + + syncManagedSkills({ workspaceDirs: [workspaceDir], packageRoot }); + + expect(existsSync(browserSkillDir)).toBe(false); + }); + + it("preserves user-authored browser skills in existing workspaces", () => { + const packageRoot = createPackageRoot(tempDir); + const workspaceDir = path.join(tempDir, "workspace-custom-browser"); + const browserSkillDir = path.join(workspaceDir, "skills", "browser"); + const customSkillPath = path.join(browserSkillDir, "SKILL.md"); + mkdirSync(browserSkillDir, { recursive: true }); + writeFileSync(customSkillPath, "# My custom browser skill\n", "utf-8"); + + syncManagedSkills({ workspaceDirs: [workspaceDir], packageRoot }); + + expect(existsSync(customSkillPath)).toBe(true); + expect(readFileSync(customSkillPath, "utf-8")).toBe("# My custom browser skill\n"); + }); + it("syncs skills into multiple workspace directories", () => { const packageRoot = createPackageRoot(tempDir); const wsA = path.join(tempDir, "workspace-a"); diff --git a/src/cli/workspace-seed.ts b/src/cli/workspace-seed.ts index cbdc5895c32e6..121bfde1083a5 100644 --- a/src/cli/workspace-seed.ts +++ b/src/cli/workspace-seed.ts @@ -1,4 +1,13 @@ -import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { + copyFileSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import path from "node:path"; export type SeedField = { @@ -143,6 +152,46 @@ export const MANAGED_SKILLS: ReadonlyArray<{ name: string; templatePaths?: boole { name: "gstack" }, ]; +const RETIRED_MANAGED_SKILLS: ReadonlyArray<{ + name: string; + matchesLegacySkill: (content: string) => boolean; +}> = [ + { + name: "browser", + matchesLegacySkill: (content) => + content.includes("name: browser-automation") && + content.includes("COPY THAT USER'S DEFAULT CHROME PROFILE"), + }, +]; + +function pruneRetiredManagedSkills(workspaceDir: string): void { + for (const skill of RETIRED_MANAGED_SKILLS) { + const skillDir = path.join(workspaceDir, "skills", skill.name); + const skillFile = path.join(skillDir, "SKILL.md"); + if (!existsSync(skillFile)) { + continue; + } + + try { + const visibleEntries = readdirSync(skillDir).filter( + (entry) => entry !== ".DS_Store" && entry !== "Thumbs.db", + ); + if (visibleEntries.length !== 1 || visibleEntries[0] !== "SKILL.md") { + continue; + } + + const content = readFileSync(skillFile, "utf-8"); + if (!skill.matchesLegacySkill(content)) { + continue; + } + + rmSync(skillDir, { recursive: true, force: true }); + } catch { + // Best-effort cleanup for retired managed skills. + } + } +} + export function seedSkill( params: { workspaceDir: string; packageRoot: string }, skill: { name: string; templatePaths?: boolean }, @@ -226,6 +275,7 @@ export function syncManagedSkills(params: { const synced: string[] = []; for (const workspaceDir of params.workspaceDirs) { mkdirSync(workspaceDir, { recursive: true }); + pruneRetiredManagedSkills(workspaceDir); for (const skill of MANAGED_SKILLS) { seedSkill({ workspaceDir, packageRoot: params.packageRoot }, skill); }