simba-skills 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ import { defineCommand } from "citty"
2
+ import { readFile, mkdir, rm } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import * as p from "@clack/prompts"
5
+ import simpleGit from "simple-git"
6
+ import { tmpdir } from "node:os"
7
+ import { RegistryStore } from "../core/registry-store"
8
+ import { SkillsStore } from "../core/skills-store"
9
+ import { getSkillsDir, getRegistryPath } from "../utils/paths"
10
+ import { compareFiles, renderDiff } from "../utils/diff"
11
+ import { discoverSkills } from "./install"
12
+ import type { ManagedSkill, InstallSource } from "../core/types"
13
+ import matter from "gray-matter"
14
+
15
+ interface SkillUpdate {
16
+ skill: ManagedSkill
17
+ newPath: string
18
+ newContent: string
19
+ existingContent: string
20
+ }
21
+
22
+ interface RepoGroup {
23
+ repo: string
24
+ protocol: "https" | "ssh"
25
+ skills: ManagedSkill[]
26
+ }
27
+
28
+ function groupByRepo(skills: ManagedSkill[]): RepoGroup[] {
29
+ const groups = new Map<string, RepoGroup>()
30
+
31
+ for (const skill of skills) {
32
+ if (!skill.installSource) continue
33
+ // Skip local skills - they're symlinked and always up to date
34
+ if (skill.installSource.protocol === "local") continue
35
+
36
+ const key = `${skill.installSource.protocol}:${skill.installSource.repo}`
37
+ if (!groups.has(key)) {
38
+ groups.set(key, {
39
+ repo: skill.installSource.repo,
40
+ protocol: skill.installSource.protocol as "https" | "ssh",
41
+ skills: []
42
+ })
43
+ }
44
+ groups.get(key)!.skills.push(skill)
45
+ }
46
+
47
+ return Array.from(groups.values())
48
+ }
49
+
50
+ export interface UpdateOptions {
51
+ skillsDir: string
52
+ registryPath: string
53
+ onConfirm: (updates: Array<{ name: string; hasChanges: boolean }>) => Promise<string[]>
54
+ }
55
+
56
+ export async function runUpdate(options: UpdateOptions): Promise<void> {
57
+ const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
58
+ const registryStore = new RegistryStore(options.registryPath)
59
+ const registry = await registryStore.load()
60
+
61
+ // Find skills with installSource (excluding local symlinked skills)
62
+ const allSkillsWithSource = Object.values(registry.skills).filter(s => s.installSource)
63
+ const localSkills = allSkillsWithSource.filter(s => s.installSource?.protocol === "local")
64
+ const remoteSkills = allSkillsWithSource.filter(s => s.installSource?.protocol !== "local")
65
+
66
+ if (allSkillsWithSource.length === 0) {
67
+ console.log("No updatable skills found. Skills need installSource to be updated.")
68
+ return
69
+ }
70
+
71
+ if (localSkills.length > 0) {
72
+ console.log(`Skipping ${localSkills.length} local symlinked skills (always up to date)`)
73
+ }
74
+
75
+ if (remoteSkills.length === 0) {
76
+ console.log("No remote skills to update.")
77
+ return
78
+ }
79
+
80
+ console.log(`Found ${remoteSkills.length} remote skills to check`)
81
+
82
+ // Group by repo
83
+ const repoGroups = groupByRepo(remoteSkills)
84
+ const allUpdates: SkillUpdate[] = []
85
+
86
+ for (const group of repoGroups) {
87
+ console.log(`\nChecking ${group.repo}...`)
88
+
89
+ // Clone the repo
90
+ let url: string
91
+ if (group.repo.includes("://") || group.repo.startsWith("git@")) {
92
+ url = group.repo
93
+ } else if (group.protocol === "ssh") {
94
+ url = `git@github.com:${group.repo}.git`
95
+ } else {
96
+ url = `https://github.com/${group.repo}`
97
+ }
98
+
99
+ const tempDir = join(tmpdir(), `simba-update-${Date.now()}`)
100
+ await mkdir(tempDir, { recursive: true })
101
+
102
+ try {
103
+ const git = simpleGit()
104
+ await git.clone(url, tempDir, ["--depth", "1"])
105
+
106
+ // Discover skills in the cloned repo
107
+ const discovered = await discoverSkills(tempDir)
108
+ const discoveredByPath = new Map(
109
+ discovered.map(s => [s.relativePath, s])
110
+ )
111
+
112
+ // Check each skill for updates
113
+ for (const skill of group.skills) {
114
+ const skillPath = skill.installSource!.skillPath
115
+ const remote = discoveredByPath.get(skillPath)
116
+
117
+ if (!remote) {
118
+ console.log(` ⚠ ${skill.name}: skill path not found in repo (${skillPath})`)
119
+ continue
120
+ }
121
+
122
+ const existingPath = join(options.skillsDir, skill.name, "SKILL.md")
123
+ const existingContent = await readFile(existingPath, "utf-8")
124
+ const newContent = await readFile(join(remote.path, "SKILL.md"), "utf-8")
125
+
126
+ const comparison = compareFiles(existingContent, newContent, "SKILL.md")
127
+
128
+ if (comparison.identical) {
129
+ console.log(` ✓ ${skill.name}: up to date`)
130
+ } else {
131
+ console.log(` ↑ ${skill.name}: update available`)
132
+ allUpdates.push({
133
+ skill,
134
+ newPath: remote.path,
135
+ newContent,
136
+ existingContent
137
+ })
138
+ }
139
+ }
140
+ } finally {
141
+ await rm(tempDir, { recursive: true, force: true })
142
+ }
143
+ }
144
+
145
+ if (allUpdates.length === 0) {
146
+ console.log("\nAll skills are up to date!")
147
+ return
148
+ }
149
+
150
+ console.log(`\n${allUpdates.length} updates available:`)
151
+
152
+ // Show updates and prompt for selection
153
+ const updateInfo = allUpdates.map(u => ({
154
+ name: u.skill.name,
155
+ hasChanges: true
156
+ }))
157
+
158
+ const selectedNames = await options.onConfirm(updateInfo)
159
+
160
+ if (selectedNames.length === 0) {
161
+ console.log("No updates selected.")
162
+ return
163
+ }
164
+
165
+ // Apply selected updates
166
+ for (const name of selectedNames) {
167
+ const update = allUpdates.find(u => u.skill.name === name)!
168
+ const existingParsed = matter(update.existingContent)
169
+ const newParsed = matter(update.newContent)
170
+
171
+ const existingVersion = existingParsed.data.version as string | undefined
172
+ const newVersion = newParsed.data.version as string | undefined
173
+
174
+ if (existingVersion && newVersion) {
175
+ console.log(`\n${name}: v${existingVersion} → v${newVersion}`)
176
+ } else {
177
+ console.log(`\n${name}: changes detected`)
178
+ }
179
+
180
+ const comparison = compareFiles(update.existingContent, update.newContent, "SKILL.md")
181
+ if (comparison.diff) {
182
+ renderDiff(comparison.diff, "current", "new")
183
+ }
184
+
185
+ // Re-clone to get the files (since we deleted tempDir)
186
+ const source = update.skill.installSource!
187
+ let url: string
188
+ if (source.repo.includes("://") || source.repo.startsWith("git@")) {
189
+ url = source.repo
190
+ } else if (source.protocol === "ssh") {
191
+ url = `git@github.com:${source.repo}.git`
192
+ } else {
193
+ url = `https://github.com/${source.repo}`
194
+ }
195
+
196
+ const tempDir = join(tmpdir(), `simba-update-apply-${Date.now()}`)
197
+ await mkdir(tempDir, { recursive: true })
198
+
199
+ try {
200
+ const git = simpleGit()
201
+ await git.clone(url, tempDir, ["--depth", "1"])
202
+
203
+ const discovered = await discoverSkills(tempDir)
204
+ const remote = discovered.find(s => s.relativePath === source.skillPath)
205
+
206
+ if (!remote) {
207
+ console.log(` Error: Could not find skill at ${source.skillPath}`)
208
+ continue
209
+ }
210
+
211
+ // Remove old and add new
212
+ await rm(join(options.skillsDir, name), { recursive: true })
213
+ await skillsStore.addSkill(name, remote.path)
214
+
215
+ registry.skills[name].installedAt = new Date().toISOString()
216
+ console.log(` Updated: ${name}`)
217
+ } finally {
218
+ await rm(tempDir, { recursive: true, force: true })
219
+ }
220
+ }
221
+
222
+ await registryStore.save(registry)
223
+ console.log("\nUpdate complete!")
224
+ }
225
+
226
+ export default defineCommand({
227
+ meta: { name: "update", description: "Update installed skills from their sources" },
228
+ args: {},
229
+ async run() {
230
+ await runUpdate({
231
+ skillsDir: getSkillsDir(),
232
+ registryPath: getRegistryPath(),
233
+ onConfirm: async (updates) => {
234
+ const result = await p.multiselect({
235
+ message: "Select skills to update:",
236
+ options: updates.map(u => ({
237
+ value: u.name,
238
+ label: u.name,
239
+ })),
240
+ initialValues: updates.map(u => u.name), // Select all by default
241
+ })
242
+
243
+ if (p.isCancel(result)) {
244
+ process.exit(0)
245
+ }
246
+
247
+ return result as string[]
248
+ },
249
+ })
250
+ },
251
+ })
@@ -0,0 +1,105 @@
1
+ import { access, readdir, mkdir, cp, rm } from "node:fs/promises"
2
+ import { join, dirname } from "node:path"
3
+ import { expandPath } from "../utils/paths"
4
+ import { hashTree } from "../utils/hash"
5
+ import type { Agent, SkillInfo } from "./types"
6
+
7
+ export class AgentRegistry {
8
+ constructor(private agents: Record<string, Agent>) {}
9
+
10
+ async detectAgents(): Promise<Record<string, Agent>> {
11
+ const results: Record<string, Agent> = {}
12
+
13
+ for (const [id, agent] of Object.entries(this.agents)) {
14
+ const globalPath = expandPath(agent.globalPath)
15
+ const parentDir = dirname(globalPath)
16
+
17
+ let detected = false
18
+ try {
19
+ await access(parentDir)
20
+ detected = true
21
+ } catch {
22
+ detected = false
23
+ }
24
+
25
+ results[id] = { ...agent, detected }
26
+ }
27
+
28
+ return results
29
+ }
30
+
31
+ async listSkills(agentId: string): Promise<SkillInfo[]> {
32
+ const agent = this.agents[agentId]
33
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`)
34
+
35
+ const skillsPath = expandPath(agent.globalPath)
36
+ const skills: SkillInfo[] = []
37
+
38
+ try {
39
+ const entries = await readdir(skillsPath, { withFileTypes: true })
40
+
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory()) continue
43
+
44
+ const skillPath = join(skillsPath, entry.name)
45
+ const skillMdPath = join(skillPath, "SKILL.md")
46
+
47
+ try {
48
+ await access(skillMdPath)
49
+ } catch {
50
+ continue // Skip directories without SKILL.md
51
+ }
52
+
53
+ const { treeHash, files } = await hashTree(skillPath)
54
+
55
+ skills.push({
56
+ name: entry.name,
57
+ treeHash,
58
+ files,
59
+ origin: agentId,
60
+ lastSeen: new Date(),
61
+ agents: [agentId],
62
+ })
63
+ }
64
+ } catch (err) {
65
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
66
+ throw err
67
+ }
68
+ }
69
+
70
+ return skills
71
+ }
72
+
73
+ async copySkill(
74
+ skillName: string,
75
+ fromAgent: string,
76
+ toAgent: string
77
+ ): Promise<void> {
78
+ const from = this.agents[fromAgent]
79
+ const to = this.agents[toAgent]
80
+
81
+ if (!from || !to) {
82
+ throw new Error(`Unknown agent: ${fromAgent} or ${toAgent}`)
83
+ }
84
+
85
+ const sourcePath = join(expandPath(from.globalPath), skillName)
86
+ const destPath = join(expandPath(to.globalPath), skillName)
87
+
88
+ await mkdir(dirname(destPath), { recursive: true })
89
+ await cp(sourcePath, destPath, { recursive: true })
90
+ }
91
+
92
+ async deleteSkill(skillName: string, agentId: string): Promise<void> {
93
+ const agent = this.agents[agentId]
94
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`)
95
+
96
+ const skillPath = join(expandPath(agent.globalPath), skillName)
97
+ await rm(skillPath, { recursive: true })
98
+ }
99
+
100
+ getSkillPath(skillName: string, agentId: string): string {
101
+ const agent = this.agents[agentId]
102
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`)
103
+ return join(expandPath(agent.globalPath), skillName)
104
+ }
105
+ }
@@ -0,0 +1,89 @@
1
+ import { parse, stringify } from "smol-toml";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import type { Config, Agent } from "./types";
5
+
6
+ // Add new agents here: [id, name, shortName, globalPath, projectPath]
7
+ const AGENT_DEFINITIONS: [string, string, string, string, string][] = [
8
+ ["claude", "Claude Code", "Claude", "~/.claude/skills", ".claude/skills"],
9
+ ["cursor", "Cursor", "Cursor", "~/.cursor/skills", ".cursor/skills"],
10
+ ["codex", "Codex", "Codex", "~/.codex/skills", ".codex/skills"],
11
+ ["copilot", "GitHub Copilot", "Copilot", "~/.copilot/skills", ".github/skills"],
12
+ ["gemini", "Gemini CLI", "Gemini", "~/.gemini/skills", ".gemini/skills"],
13
+ ["windsurf", "Windsurf", "Windsurf", "~/.codeium/windsurf/skills", ".windsurf/skills"],
14
+ ["amp", "Amp", "Amp", "~/.config/agents/skills", ".agents/skills"],
15
+ ["goose", "Goose", "Goose", "~/.config/goose/skills", ".goose/skills"],
16
+ ["opencode", "OpenCode", "OpenCode", "~/.config/opencode/skill", ".opencode/skill"],
17
+ ["kilo", "Kilo Code", "Kilo", "~/.kilocode/skills", ".kilocode/skills"],
18
+ ["roo", "Roo Code", "Roo", "~/.roo/skills", ".roo/skills"],
19
+ ["antigravity", "Antigravity", "Antigrav", "~/.gemini/antigravity/skills", ".agent/skills"],
20
+ ["clawdbot", "Clawdbot", "Clawdbot", "~/.clawdbot/skills", "skills"],
21
+ ["droid", "Droid", "Droid", "~/.factory/skills", ".factory/skills"],
22
+ ];
23
+
24
+ const DEFAULT_AGENTS: Record<string, Agent> = Object.fromEntries(
25
+ AGENT_DEFINITIONS.map(([id, name, shortName, globalPath, projectPath]) => [
26
+ id,
27
+ { id, name, shortName, globalPath, projectPath, detected: false },
28
+ ]),
29
+ );
30
+
31
+ function createDefaultConfig(): Config {
32
+ return {
33
+ agents: { ...DEFAULT_AGENTS },
34
+ sync: {
35
+ strategy: "union",
36
+ sourceAgent: "",
37
+ },
38
+ snapshots: {
39
+ maxCount: 10,
40
+ autoSnapshot: true,
41
+ },
42
+ skills: {},
43
+ };
44
+ }
45
+
46
+ export class ConfigStore {
47
+ constructor(private configPath: string) {}
48
+
49
+ async load(): Promise<Config> {
50
+ try {
51
+ const content = await readFile(this.configPath, "utf-8");
52
+ const parsed = parse(content) as unknown as Config;
53
+ return this.mergeWithDefaults(parsed);
54
+ } catch (err) {
55
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
56
+ return createDefaultConfig();
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+
62
+ async save(config: Config): Promise<void> {
63
+ await mkdir(dirname(this.configPath), { recursive: true });
64
+ const toml = stringify(config as unknown as Record<string, unknown>);
65
+ await writeFile(this.configPath, toml);
66
+ }
67
+
68
+ private mergeWithDefaults(parsed: Partial<Config>): Config {
69
+ const defaults = createDefaultConfig();
70
+
71
+ // Merge agents and ensure shortName exists for each
72
+ const mergedAgents = { ...defaults.agents };
73
+ for (const [id, agent] of Object.entries(parsed.agents ?? {})) {
74
+ mergedAgents[id] = {
75
+ ...defaults.agents[id],
76
+ ...agent,
77
+ // Ensure shortName exists, fallback to first word of name or id
78
+ shortName: agent.shortName ?? defaults.agents[id]?.shortName ?? agent.name?.split(" ")[0] ?? id,
79
+ };
80
+ }
81
+
82
+ return {
83
+ agents: mergedAgents,
84
+ sync: { ...defaults.sync, ...parsed.sync },
85
+ snapshots: { ...defaults.snapshots, ...parsed.snapshots },
86
+ skills: parsed.skills ?? {},
87
+ };
88
+ }
89
+ }
@@ -0,0 +1,28 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises"
2
+ import { dirname } from "node:path"
3
+ import type { Registry } from "./types"
4
+
5
+ function createEmptyRegistry(): Registry {
6
+ return { version: 1, skills: {} }
7
+ }
8
+
9
+ export class RegistryStore {
10
+ constructor(private registryPath: string) {}
11
+
12
+ async load(): Promise<Registry> {
13
+ try {
14
+ const content = await readFile(this.registryPath, "utf-8")
15
+ return JSON.parse(content) as Registry
16
+ } catch (err) {
17
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
18
+ return createEmptyRegistry()
19
+ }
20
+ throw err
21
+ }
22
+ }
23
+
24
+ async save(registry: Registry): Promise<void> {
25
+ await mkdir(dirname(this.registryPath), { recursive: true })
26
+ await writeFile(this.registryPath, JSON.stringify(registry, null, 2))
27
+ }
28
+ }
@@ -0,0 +1,99 @@
1
+ import type { Agent, SkillMatrix, SkillStatus } from "./types"
2
+ import type { AgentRegistry } from "./agent-registry"
3
+
4
+ export class SkillManager {
5
+ constructor(
6
+ private registry: AgentRegistry,
7
+ private agents: Record<string, Agent>
8
+ ) {}
9
+
10
+ async buildMatrix(): Promise<SkillMatrix[]> {
11
+ const detectedAgents = Object.entries(this.agents).filter(
12
+ ([_, agent]) => agent.detected
13
+ )
14
+
15
+ // Collect all skills from all agents
16
+ const skillMap = new Map<
17
+ string,
18
+ Map<string, { present: boolean; hash: string | null }>
19
+ >()
20
+
21
+ for (const [agentId] of detectedAgents) {
22
+ const skills = await this.registry.listSkills(agentId)
23
+
24
+ for (const skill of skills) {
25
+ if (!skillMap.has(skill.name)) {
26
+ skillMap.set(skill.name, new Map())
27
+ }
28
+ skillMap.get(skill.name)!.set(agentId, {
29
+ present: true,
30
+ hash: skill.treeHash,
31
+ })
32
+ }
33
+ }
34
+
35
+ // Build matrix with status
36
+ const matrix: SkillMatrix[] = []
37
+
38
+ for (const [skillName, agentHashes] of skillMap) {
39
+ const agents: Record<string, { present: boolean; hash: string | null }> =
40
+ {}
41
+
42
+ // Initialize all agents as not present
43
+ for (const [agentId] of detectedAgents) {
44
+ agents[agentId] = agentHashes.get(agentId) ?? {
45
+ present: false,
46
+ hash: null,
47
+ }
48
+ }
49
+
50
+ const status = this.computeStatus(agentHashes)
51
+
52
+ matrix.push({ skillName, agents, status })
53
+ }
54
+
55
+ return matrix.sort((a, b) => a.skillName.localeCompare(b.skillName))
56
+ }
57
+
58
+ private computeStatus(
59
+ agentHashes: Map<string, { present: boolean; hash: string | null }>
60
+ ): SkillStatus {
61
+ const presentAgents = Array.from(agentHashes.entries()).filter(
62
+ ([_, v]) => v.present
63
+ )
64
+
65
+ if (presentAgents.length === 0) {
66
+ return "missing"
67
+ }
68
+
69
+ if (presentAgents.length === 1) {
70
+ return "unique"
71
+ }
72
+
73
+ const hashes = new Set(presentAgents.map(([_, v]) => v.hash))
74
+ return hashes.size === 1 ? "synced" : "conflict"
75
+ }
76
+
77
+ async syncUnique(skillName: string, sourceAgent: string): Promise<string[]> {
78
+ const targetAgents = Object.entries(this.agents)
79
+ .filter(([id, agent]) => agent.detected && id !== sourceAgent)
80
+ .map(([id]) => id)
81
+
82
+ for (const targetAgent of targetAgents) {
83
+ await this.registry.copySkill(skillName, sourceAgent, targetAgent)
84
+ }
85
+
86
+ return targetAgents
87
+ }
88
+
89
+ async resolveConflict(
90
+ skillName: string,
91
+ winnerAgent: string,
92
+ loserAgents: string[]
93
+ ): Promise<void> {
94
+ for (const loserAgent of loserAgents) {
95
+ await this.registry.deleteSkill(skillName, loserAgent)
96
+ await this.registry.copySkill(skillName, winnerAgent, loserAgent)
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,79 @@
1
+ import { readdir, access, mkdir, cp, rm } from "node:fs/promises"
2
+ import { join } from "node:path"
3
+ import { createSymlink, removeSymlink } from "../utils/symlinks"
4
+ import type { SkillAssignment } from "./types"
5
+
6
+ export class SkillsStore {
7
+ constructor(
8
+ private skillsDir: string,
9
+ private registryPath: string
10
+ ) {}
11
+
12
+ async ensureDir(): Promise<void> {
13
+ await mkdir(this.skillsDir, { recursive: true })
14
+ }
15
+
16
+ async listSkills(): Promise<string[]> {
17
+ try {
18
+ const entries = await readdir(this.skillsDir, { withFileTypes: true })
19
+ return entries.filter(e => e.isDirectory()).map(e => e.name)
20
+ } catch (err) {
21
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
22
+ return []
23
+ }
24
+ throw err
25
+ }
26
+ }
27
+
28
+ async hasSkill(name: string): Promise<boolean> {
29
+ try {
30
+ await access(join(this.skillsDir, name))
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ async addSkill(name: string, sourcePath: string): Promise<void> {
38
+ await this.ensureDir()
39
+ const destPath = join(this.skillsDir, name)
40
+ await cp(sourcePath, destPath, { recursive: true })
41
+ }
42
+
43
+ async linkSkill(name: string, sourcePath: string): Promise<void> {
44
+ await this.ensureDir()
45
+ const destPath = join(this.skillsDir, name)
46
+ await createSymlink(sourcePath, destPath)
47
+ }
48
+
49
+ async removeSkill(name: string): Promise<void> {
50
+ const skillPath = join(this.skillsDir, name)
51
+ await rm(skillPath, { recursive: true })
52
+ }
53
+
54
+ async assignSkill(
55
+ name: string,
56
+ agentSkillsDir: string,
57
+ assignment: SkillAssignment
58
+ ): Promise<void> {
59
+ const sourcePath = join(this.skillsDir, name)
60
+
61
+ if (assignment.type === "directory") {
62
+ const targetPath = join(agentSkillsDir, name)
63
+ await createSymlink(sourcePath, targetPath)
64
+ } else {
65
+ const sourceFile = join(sourcePath, assignment.target!)
66
+ const targetPath = join(agentSkillsDir, `${name}.${assignment.target!.split(".").pop()}`)
67
+ await createSymlink(sourceFile, targetPath)
68
+ }
69
+ }
70
+
71
+ async unassignSkill(name: string, agentSkillsDir: string): Promise<void> {
72
+ const targetPath = join(agentSkillsDir, name)
73
+ await removeSymlink(targetPath)
74
+ }
75
+
76
+ getSkillPath(name: string): string {
77
+ return join(this.skillsDir, name)
78
+ }
79
+ }