From e706876114a3d2cc86995dde4bb40057b526dd39 Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sun, 22 Mar 2026 05:30:29 -0400 Subject: [PATCH] fix(skills): add migration from ~/.gsd/agent/skills/ to ~/.agents/skills/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing GSD users have skills in ~/.gsd/agent/skills/ that would silently vanish after the directory switch. This adds: 1. One-time migration in initResources() — copies skill directories from ~/.gsd/agent/skills/ to ~/.agents/skills/ (collision-safe, writes .migrated-to-agents marker so it runs at most once). 2. Legacy fallback reads in loadSkills() and getSkillSearchDirs() — the old directory is scanned as a low-priority fallback so skills work immediately, even before the migration runs on next restart. The old directory is NOT deleted — users can safely downgrade to a pre-migration GSD version without losing skills. --- packages/pi-coding-agent/src/core/skills.ts | 15 +++++ src/resource-loader.ts | 56 +++++++++++++++++++ .../extensions/gsd/preferences-skills.ts | 9 ++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/pi-coding-agent/src/core/skills.ts b/packages/pi-coding-agent/src/core/skills.ts index f53b78bbc..9ab4df3b7 100644 --- a/packages/pi-coding-agent/src/core/skills.ts +++ b/packages/pi-coding-agent/src/core/skills.ts @@ -5,6 +5,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "pat import { parseFrontmatter } from "../utils/frontmatter.js"; import { toPosixPath } from "../utils/path-display.js"; import type { ResourceDiagnostic } from "./diagnostics.js"; +import { CONFIG_DIR_NAME } from "../config.js"; /** * The standard ecosystem skills directory used by skills.sh and the @@ -18,6 +19,12 @@ export const ECOSYSTEM_SKILLS_DIR = join(homedir(), ".agents", "skills"); */ export const ECOSYSTEM_PROJECT_SKILLS_DIR = ".agents"; +/** + * Legacy skills directory (~/.gsd/agent/skills/ or ~/.pi/agent/skills/). + * Read as a fallback so existing installs don't lose skills before migration runs. + */ +const LEGACY_SKILLS_DIR = join(homedir(), CONFIG_DIR_NAME, "agent", "skills"); + /** Max name length per spec */ const MAX_NAME_LENGTH = 64; @@ -416,6 +423,14 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { addSkills(loadSkillsFromDirInternal(ECOSYSTEM_SKILLS_DIR, "user", true)); // Primary project: .agents/skills/ — standard project-level location addSkills(loadSkillsFromDirInternal(resolve(cwd, ECOSYSTEM_PROJECT_SKILLS_DIR, "skills"), "project", true)); + + // Legacy fallback: read skills from ~/.gsd/agent/skills/ so existing + // installs keep working until the one-time migration in resource-loader + // copies them to ~/.agents/skills/. Collision dedup above means already- + // migrated skills won't load twice. + if (LEGACY_SKILLS_DIR !== ECOSYSTEM_SKILLS_DIR && existsSync(LEGACY_SKILLS_DIR)) { + addSkills(loadSkillsFromDirInternal(LEGACY_SKILLS_DIR, "user", true)); + } } const userSkillsDir = ECOSYSTEM_SKILLS_DIR; diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 63cfe0aac..1ec43d10b 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -393,6 +393,10 @@ export function initResources(agentDir: string): void { // Skills are no longer force-synced here. Users install skills via the // skills.sh CLI (`npx skills add `) into ~/.agents/skills/ which // is the industry-standard Agent Skills ecosystem directory. + // + // Migrate any user-customized skills from the legacy ~/.gsd/agent/skills/ + // directory into ~/.agents/skills/ so they aren't silently lost on upgrade. + migrateSkillsToEcosystemDir(agentDir) // Sync GSD-WORKFLOW.md to agentDir as a fallback for when GSD_WORKFLOW_PATH // env var is not set (e.g. fork/dev builds, alternative entry points). @@ -409,6 +413,58 @@ export function initResources(agentDir: string): void { ensureRegistryEntries(join(agentDir, 'extensions')) } +// ─── Legacy Skill Migration ────────────────────────────────────────────────────── + +/** + * One-time migration: copy user-customized skills from the old + * ~/.gsd/agent/skills/ directory into ~/.agents/skills/. + * + * The migration is conservative: + * - Only skill directories containing a SKILL.md are considered. + * - Copies, does not move — the old directory stays intact so downgrading + * to a pre-migration GSD version still works. + * - Collision-safe — if a skill name already exists in the target, the + * existing ecosystem skill wins (user may have already installed a newer + * version via skills.sh). + * - Writes a `.migrated-to-agents` marker inside the legacy directory so + * the migration runs at most once. + */ +function migrateSkillsToEcosystemDir(agentDir: string): void { + const legacyDir = join(agentDir, 'skills') + const markerPath = join(legacyDir, '.migrated-to-agents') + + // Already migrated or no legacy dir — nothing to do + if (!existsSync(legacyDir) || existsSync(markerPath)) return + + const ecosystemDir = join(homedir(), '.agents', 'skills') + mkdirSync(ecosystemDir, { recursive: true }) + + try { + const entries = readdirSync(legacyDir, { withFileTypes: true }) + let migrated = 0 + for (const entry of entries) { + if (!entry.isDirectory()) continue + const skillMd = join(legacyDir, entry.name, 'SKILL.md') + if (!existsSync(skillMd)) continue + + const target = join(ecosystemDir, entry.name) + if (existsSync(target)) continue // ecosystem version wins + + try { + cpSync(join(legacyDir, entry.name), target, { recursive: true }) + migrated++ + } catch { + // non-fatal — skip this skill + } + } + + // Drop marker whether or not anything was copied, so we don't re-scan + try { writeFileSync(markerPath, `Migrated ${migrated} skill(s) to ${ecosystemDir} on ${new Date().toISOString()}\n`) } catch { /* non-fatal */ } + } catch { + // can't read legacy dir — skip silently + } +} + export function hasStaleCompiledExtensionSiblings(extensionsDir: string): boolean { if (!existsSync(extensionsDir)) return false for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) { diff --git a/src/resources/extensions/gsd/preferences-skills.ts b/src/resources/extensions/gsd/preferences-skills.ts index 4587503ad..59bd4cf69 100644 --- a/src/resources/extensions/gsd/preferences-skills.ts +++ b/src/resources/extensions/gsd/preferences-skills.ts @@ -25,12 +25,19 @@ export type { GSDSkillRule, SkillDiscoveryMode, SkillResolution, SkillResolution /** * Known skill directories, in priority order. * Global skills (~/.agents/skills/) take precedence over project skills. + * Legacy ~/.gsd/agent/skills/ is included as a fallback for pre-migration installs. */ export function getSkillSearchDirs(cwd: string): Array<{ dir: string; method: SkillResolution["method"] }> { - return [ + const dirs: Array<{ dir: string; method: SkillResolution["method"] }> = [ { dir: join(homedir(), ".agents", "skills"), method: "user-skill" }, { dir: join(cwd, ".agents", "skills"), method: "project-skill" }, ]; + // Legacy fallback — read skills from old GSD directory until migration completes + const legacyDir = join(homedir(), ".gsd", "agent", "skills"); + if (existsSync(legacyDir)) { + dirs.push({ dir: legacyDir, method: "user-skill" }); + } + return dirs; } /**