fix(skills): add migration from ~/.gsd/agent/skills/ to ~/.agents/skills/
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.
This commit is contained in:
parent
4020828260
commit
e706876114
3 changed files with 79 additions and 1 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <repo>`) 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 })) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue