| 19 | export type { SkillMetadata, SkillContent } |
| 20 | |
| 21 | export class SkillsManager { |
| 22 | private skills: Map<string, SkillMetadata> = new Map() |
| 23 | private providerRef: WeakRef<ClineProvider> |
| 24 | private disposables: vscode.Disposable[] = [] |
| 25 | private isDisposed = false |
| 26 | |
| 27 | constructor(provider: ClineProvider) { |
| 28 | this.providerRef = new WeakRef(provider) |
| 29 | } |
| 30 | |
| 31 | async initialize(): Promise<void> { |
| 32 | await this.discoverSkills() |
| 33 | await this.setupFileWatchers() |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Discover all skills from global and project directories. |
| 38 | * Supports both generic skills (skills/) and mode-specific skills (skills-{mode}/). |
| 39 | * Also supports symlinks: |
| 40 | * - .roo/skills can be a symlink to a directory containing skill subdirectories |
| 41 | * - .roo/skills/[dirname] can be a symlink to a skill directory |
| 42 | */ |
| 43 | async discoverSkills(): Promise<void> { |
| 44 | this.skills.clear() |
| 45 | const skillsDirs = await this.getSkillsDirectories() |
| 46 | |
| 47 | for (const { dir, source, mode } of skillsDirs) { |
| 48 | await this.scanSkillsDirectory(dir, source, mode) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Scan a skills directory for skill subdirectories. |
| 54 | * Handles two symlink cases: |
| 55 | * 1. The skills directory itself is a symlink (resolved by directoryExists using realpath) |
| 56 | * 2. Individual skill subdirectories are symlinks |
| 57 | */ |
| 58 | private async scanSkillsDirectory(dirPath: string, source: "global" | "project", mode?: string): Promise<void> { |
| 59 | if (!(await directoryExists(dirPath))) { |
| 60 | return |
| 61 | } |
| 62 | |
| 63 | try { |
| 64 | // Get the real path (resolves if dirPath is a symlink) |
| 65 | const realDirPath = await fs.realpath(dirPath) |
| 66 | |
| 67 | // Read directory entries |
| 68 | const entries = await fs.readdir(realDirPath) |
| 69 | |
| 70 | for (const entryName of entries) { |
| 71 | const entryPath = path.join(realDirPath, entryName) |
| 72 | |
| 73 | // Check if this entry is a directory (follows symlinks automatically) |
| 74 | const stats = await fs.stat(entryPath).catch(() => null) |
| 75 | if (!stats?.isDirectory()) continue |
| 76 | |
| 77 | // Load skill metadata - the skill name comes from the entry name (symlink name if symlinked) |
| 78 | await this.loadSkillMetadata(entryPath, source, mode, entryName) |
nothing calls this directly
no outgoing calls
no test coverage detected