/** * lib/skill-slug.ts — Single source of truth for mapping an authoring path * under `templates/skills/` to the slug it is deployed under in * `~/.claude/skills/` (and therefore the slash-command name it answers to). * * Consumed by: * - `src/lib/installer.ts` at install time (decides what to copy where) * - `scripts/generate-docs/lib/skill-parser.ts` at doc-build time (decides * what slug to print on each skill card) * * Keeping a single implementation prevents the two paths from drifting. */ const BA_GROUP = 'business-analyse/'; /** * Claude Code only registers a skill when its directory is a DIRECT child of * the skills root (the folder name is the slash-command token; nested folders * are not invocable and there is no namespacing for plain skills). The BA * skills are authored grouped under `business-analyse//` for repo * organisation, so at deploy time we FLATTEN + PREFIX them to `ba-/` — * each phase then becomes an individual, grouped command * (`/ba-create-menu`, `/ba-create-actors`, …). * * Returns the remapped relative path, or `null` for files that must NOT be * deployed as commands (the `_workflow/` companions and the `CLAUDE.md` index * — these stay repo-only; each skill inlines what it needs). * * @param file Relative path under `templates/skills/`, e.g. * `business-analyse/create-menu/SKILL.md`. Slashes are normalised. */ export function remapSkillPath(file: string): string | null { const norm = file.replace(/\\/g, '/'); if (!norm.startsWith(BA_GROUP)) { return file; // top-level skills (incl. ba-develop) deploy as-is } const rest = norm.slice(BA_GROUP.length); // 'create-menu/SKILL.md' | 'CLAUDE.md' | '_workflow/ba-files.md' const slash = rest.indexOf('/'); if (slash === -1) { return null; // bare file directly under business-analyse/ (e.g. CLAUDE.md index) — not deployed } const name = rest.slice(0, slash); if (name.startsWith('_')) { return null; // internal companions (_workflow, _fallback-summary, …) — repo-only } return `ba-${rest}`; // business-analyse/create-menu/SKILL.md → ba-create-menu/SKILL.md } /** * Convenience: extract just the deployed slug (top-level folder name) from * an authoring path. Returns `null` for repo-only files. * * Examples: * skillSlugFromAuthoringPath('business-analyse/create-menu/SKILL.md') * => 'ba-create-menu' * skillSlugFromAuthoringPath('efcore/SKILL.md') * => 'efcore' * skillSlugFromAuthoringPath('development/backend/controller/SKILL.md') * => 'development' // first segment * skillSlugFromAuthoringPath('business-analyse/_workflow/ba-files.md') * => null * * NOTE: For nested-but-deployed skills (e.g. `development/backend/controller/`), * the installer does NOT flatten — Claude only registers `development/` as the * slash command. The doc generator therefore uses the SKILL.md `name:` * frontmatter as the canonical slug, not this path-derived value, for any * skill that lives more than one folder deep. */ export function skillSlugFromAuthoringPath(file: string): string | null { const remapped = remapSkillPath(file); if (remapped === null) return null; const norm = remapped.replace(/\\/g, '/'); const slash = norm.indexOf('/'); return slash === -1 ? norm.replace(/\.md$/, '') : norm.slice(0, slash); } /** * Returns the "phase" bucket for a skill, used by the doc generator to group * skill cards on `commands.html`, `business-analyse.html`, etc. Inferred from * folder structure unless overridden by the SKILL.md `phase:` frontmatter. * * Examples: * phaseFromAuthoringPath('efcore/SKILL.md') => 'infrastructure' * phaseFromAuthoringPath('gitflow/SKILL.md') => 'infrastructure' * phaseFromAuthoringPath('business-analyse/audit-actors/SKILL.md') => 'business-analyse' * phaseFromAuthoringPath('development/backend/controller/SKILL.md') => 'development/backend' * phaseFromAuthoringPath('development/frontend/theme/SKILL.md') => 'development/frontend' * phaseFromAuthoringPath('validation/conventions/SKILL.md') => 'validation' * phaseFromAuthoringPath('init/SKILL.md') => 'lifecycle' */ const LIFECYCLE_SLUGS = new Set([ 'init', 'upgrade', 'review', 'check-version', 'cli-app-sync', 'dev-start', 'documentation', 'quick-search', 'smoke-generation', 'ui-components', 'utils', 'validate-feature', 'conventions', ]); const INFRASTRUCTURE_SLUGS = new Set([ 'efcore', 'gitflow', 'external', 'lib', 'ba-develop', 'ba-develop-plan', ]); export function phaseFromAuthoringPath(file: string): string { const norm = file.replace(/\\/g, '/'); const segments = norm.split('/'); const first = segments[0]; if (first === 'business-analyse') return 'business-analyse'; if (first === 'development') { // development/backend//SKILL.md => 'development/backend' // development/frontend//SKILL.md => 'development/frontend' // development/audit-dev-/SKILL.md => 'development/audit' // development/{debug,run,testing,smoke-test,audit}/<...>/SKILL.md => keep const second = segments[1]; if (!second) return 'development'; if (second.startsWith('audit-dev-')) return 'development/audit'; return `development/${second}`; } if (first === 'validation') return 'validation'; if (LIFECYCLE_SLUGS.has(first)) return 'lifecycle'; if (INFRASTRUCTURE_SLUGS.has(first)) return 'infrastructure'; return 'other'; }