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.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # Simba
2
+
3
+ [![npm version](https://img.shields.io/npm/v/simba-skills)](https://www.npmjs.com/package/simba-skills)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ AI skills manager with a central store and symlink-based distribution across 14+ coding agents.
7
+
8
+ ## Why Simba?
9
+
10
+ Most skill installers are one-shot: they clone a repo and copy files. Simba is a **skill lifecycle manager**:
11
+
12
+ - **Central store** → One source of truth at `~/.config/simba/skills/`
13
+ - **Registry tracking** → Records install sources, enabling one-command updates
14
+ - **Symlink distribution** → No file duplication; changes propagate instantly
15
+ - **Multi-agent sync** → Keep Claude, Cursor, Copilot, and others in sync
16
+ - **Rollback support** → Automatic snapshots before destructive operations
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ # Requires Bun runtime
22
+ bunx simba-skills detect
23
+ ```
24
+
25
+ Or install globally:
26
+
27
+ ```bash
28
+ bun install -g simba-skills
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Detect installed agents
35
+ simba detect
36
+
37
+ # Adopt existing skills into the central store
38
+ simba adopt
39
+
40
+ # Install skills from GitHub
41
+ simba install vercel-labs/agent-skills
42
+
43
+ # Assign skills to specific agents
44
+ simba assign my-skill claude,cursor
45
+
46
+ # Check for updates (uses tracked install sources)
47
+ simba update
48
+
49
+ # View skill matrix across all agents
50
+ simba status
51
+ ```
52
+
53
+ ## Key Features
54
+
55
+ ### Install & Update
56
+
57
+ ```bash
58
+ # Install from GitHub (HTTPS)
59
+ simba install user/repo
60
+
61
+ # Install from private repos (SSH)
62
+ simba install user/repo --ssh
63
+
64
+ # Install from local path (creates symlinks, auto-syncs)
65
+ simba install ~/my-skills
66
+
67
+ # Update all installed skills from their sources
68
+ simba update
69
+ ```
70
+
71
+ Simba records the source repository and path during installation, enabling `simba update` to fetch and compare changes with diffs.
72
+
73
+ ### Assign & Manage
74
+
75
+ ```bash
76
+ # Assign skill to multiple agents
77
+ simba assign my-skill claude,cursor,copilot
78
+
79
+ # Interactive TUI for bulk management
80
+ simba manage
81
+
82
+ # Remove skill from agents
83
+ simba unassign my-skill claude
84
+ ```
85
+
86
+ ### Health & Recovery
87
+
88
+ ```bash
89
+ # Check symlink integrity
90
+ simba doctor
91
+
92
+ # Auto-repair broken symlinks
93
+ simba doctor --fix
94
+
95
+ # Backup all skills
96
+ simba backup ./skills.tar.gz --includeConfig
97
+
98
+ # Restore from backup
99
+ simba restore ./skills.tar.gz
100
+
101
+ # Undo last operation
102
+ simba undo
103
+ ```
104
+
105
+ ## Supported Agents
106
+
107
+ | Agent | Global Path | Project Path |
108
+ |-------|-------------|--------------|
109
+ | Claude Code | `~/.claude/skills` | `.claude/skills` |
110
+ | Cursor | `~/.cursor/skills` | `.cursor/skills` |
111
+ | Codex | `~/.codex/skills` | `.codex/skills` |
112
+ | GitHub Copilot | `~/.copilot/skills` | `.github/skills` |
113
+ | Gemini CLI | `~/.gemini/skills` | `.gemini/skills` |
114
+ | Windsurf | `~/.codeium/windsurf/skills` | `.windsurf/skills` |
115
+ | Amp | `~/.config/agents/skills` | `.agents/skills` |
116
+ | Goose | `~/.config/goose/skills` | `.goose/skills` |
117
+ | OpenCode | `~/.config/opencode/skill` | `.opencode/skill` |
118
+ | Kilo Code | `~/.kilocode/skills` | `.kilocode/skills` |
119
+ | Roo Code | `~/.roo/skills` | `.roo/skills` |
120
+ | Antigravity | `~/.gemini/antigravity/skills` | `.agent/skills` |
121
+ | Clawdbot | `~/.clawdbot/skills` | `skills` |
122
+ | Droid | `~/.factory/skills` | `.factory/skills` |
123
+
124
+ ## Architecture
125
+
126
+ ```
127
+ ~/.config/simba/
128
+ ├── config.toml # Settings
129
+ ├── registry.json # Skill metadata, sources & assignments
130
+ ├── skills/ # Central store
131
+ │ └── my-skill/
132
+ │ └── SKILL.md
133
+ └── snapshots/ # Automatic rollback points
134
+
135
+ ~/.claude/skills/
136
+ └── my-skill → ~/.config/simba/skills/my-skill (symlink)
137
+
138
+ ~/.cursor/skills/
139
+ └── my-skill → ~/.config/simba/skills/my-skill (symlink)
140
+ ```
141
+
142
+ ## All Commands
143
+
144
+ | Command | Description |
145
+ |---------|-------------|
146
+ | `detect` | Scan for installed agents |
147
+ | `adopt` | Move existing skills into central store |
148
+ | `install` | Install from GitHub or local path |
149
+ | `uninstall` | Remove skill from store and agents |
150
+ | `update` | Check and apply updates from sources |
151
+ | `list` | List managed skills |
152
+ | `status` | Skill matrix across agents |
153
+ | `assign` | Symlink skill to agents |
154
+ | `unassign` | Remove skill from agents |
155
+ | `manage` | Interactive TUI |
156
+ | `sync` | Union merge across agents |
157
+ | `migrate` | Copy all skills from one agent to another |
158
+ | `doctor` | Verify and repair symlinks |
159
+ | `backup` | Export skills to archive |
160
+ | `restore` | Restore from backup |
161
+ | `snapshots` | List rollback points |
162
+ | `undo` | Restore from last snapshot |
163
+ | `import` | Copy global skill to project for customization |
164
+
165
+ ## Configuration
166
+
167
+ Config at `~/.config/simba/config.toml`:
168
+
169
+ ```toml
170
+ [snapshots]
171
+ maxCount = 10
172
+ autoSnapshot = true
173
+
174
+ [sync]
175
+ strategy = "union" # or "source"
176
+ sourceAgent = "" # for source strategy
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "simba-skills",
3
+ "version": "0.1.2",
4
+ "description": "AI skills manager - central store with symlink-based distribution across 14+ coding agents",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "author": "Ethan Fann",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/ethanfann/simba"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "skills",
18
+ "claude",
19
+ "cursor",
20
+ "copilot",
21
+ "codex",
22
+ "amp",
23
+ "gemini",
24
+ "windsurf",
25
+ "agent",
26
+ "cli"
27
+ ],
28
+ "bin": {
29
+ "simba": "./src/index.ts"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "!src/**/*.test.ts"
34
+ ],
35
+ "scripts": {
36
+ "dev": "bun run src/index.ts",
37
+ "test": "bun test",
38
+ "lint": "bunx tsc --noEmit"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "latest",
42
+ "@types/tar": "^6.1.13",
43
+ "@types/terminal-kit": "^2.5.7",
44
+ "typescript": "^5.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@clack/prompts": "^0.11.0",
48
+ "@pierre/diffs": "^1.0.6",
49
+ "citty": "^0.1.6",
50
+ "gray-matter": "^4.0.3",
51
+ "simple-git": "^3.30.0",
52
+ "smol-toml": "^1.3.0",
53
+ "tar": "^7.5.3",
54
+ "terminal-kit": "^3.1.2"
55
+ },
56
+ "engines": {
57
+ "bun": ">=1.0.0"
58
+ }
59
+ }
File without changes
@@ -0,0 +1,232 @@
1
+ import { defineCommand } from "citty"
2
+ import { readdir, rm, access, readFile, stat } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import * as p from "@clack/prompts"
5
+ import matter from "gray-matter"
6
+ import { ConfigStore } from "../core/config-store"
7
+ import { RegistryStore } from "../core/registry-store"
8
+ import { SkillsStore } from "../core/skills-store"
9
+ import { AgentRegistry } from "../core/agent-registry"
10
+ import { getConfigPath, getSkillsDir, getRegistryPath, expandPath } from "../utils/paths"
11
+ import { isSymlink, createSymlink } from "../utils/symlinks"
12
+ import { compareFiles, renderDiff, renderIdenticalMessage } from "../utils/diff"
13
+ import type { Agent, ManagedSkill } from "../core/types"
14
+
15
+ export interface ConflictingSkill {
16
+ agentId: string
17
+ path: string
18
+ }
19
+
20
+ export interface AdoptOptions {
21
+ skillsDir: string
22
+ registryPath: string
23
+ configPath: string
24
+ agents: Record<string, Agent>
25
+ dryRun: boolean
26
+ onConflict: (skillName: string, skills: ConflictingSkill[]) => Promise<string>
27
+ }
28
+
29
+ interface DiscoveredSkill {
30
+ name: string
31
+ agentId: string
32
+ path: string
33
+ }
34
+
35
+ export async function runAdopt(options: AdoptOptions): Promise<void> {
36
+ const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
37
+ const registryStore = new RegistryStore(options.registryPath)
38
+ const registry = await registryStore.load()
39
+
40
+ const discovered: DiscoveredSkill[] = []
41
+ for (const [agentId, agent] of Object.entries(options.agents)) {
42
+ if (!agent.detected) continue
43
+
44
+ const agentPath = expandPath(agent.globalPath)
45
+ try {
46
+ const entries = await readdir(agentPath, { withFileTypes: true })
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory()) continue
49
+
50
+ const skillPath = join(agentPath, entry.name)
51
+ if (await isSymlink(skillPath)) continue
52
+
53
+ try {
54
+ await access(join(skillPath, "SKILL.md"))
55
+ } catch {
56
+ continue
57
+ }
58
+
59
+ discovered.push({ name: entry.name, agentId, path: skillPath })
60
+ }
61
+ } catch (err) {
62
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err
63
+ }
64
+ }
65
+
66
+ const byName = new Map<string, DiscoveredSkill[]>()
67
+ for (const skill of discovered) {
68
+ if (!byName.has(skill.name)) byName.set(skill.name, [])
69
+ byName.get(skill.name)!.push(skill)
70
+ }
71
+
72
+ const toAdopt: Array<{ name: string; skill: DiscoveredSkill }> = []
73
+ for (const [name, skills] of byName) {
74
+ if (await skillsStore.hasSkill(name)) {
75
+ console.log(` Skipping ${name} (already in store)`)
76
+ continue
77
+ }
78
+
79
+ if (skills.length === 1) {
80
+ toAdopt.push({ name, skill: skills[0] })
81
+ } else {
82
+ const conflictingSkills = skills.map(s => ({ agentId: s.agentId, path: s.path }))
83
+ const chosenAgent = await options.onConflict(name, conflictingSkills)
84
+ const chosen = skills.find(s => s.agentId === chosenAgent)!
85
+ toAdopt.push({ name, skill: chosen })
86
+ }
87
+ }
88
+
89
+ if (toAdopt.length === 0) {
90
+ console.log("\nNo new skills to adopt.")
91
+ return
92
+ }
93
+
94
+ console.log(`\nAdopting ${toAdopt.length} skills...`)
95
+
96
+ if (options.dryRun) {
97
+ for (const { name, skill } of toAdopt) {
98
+ console.log(` Would adopt: ${name} (from ${skill.agentId})`)
99
+ }
100
+ console.log("\n(dry run - no changes made)")
101
+ return
102
+ }
103
+
104
+ for (const { name, skill } of toAdopt) {
105
+ await skillsStore.addSkill(name, skill.path)
106
+ await rm(skill.path, { recursive: true })
107
+ await createSymlink(join(options.skillsDir, name), skill.path)
108
+
109
+ const managedSkill: ManagedSkill = {
110
+ name,
111
+ source: `adopted:${skill.agentId}`,
112
+ installedAt: new Date().toISOString(),
113
+ assignments: { [skill.agentId]: { type: "directory" } }
114
+ }
115
+ registry.skills[name] = managedSkill
116
+
117
+ console.log(` Adopted: ${name} (from ${skill.agentId})`)
118
+ }
119
+
120
+ await registryStore.save(registry)
121
+ console.log("\nAdoption complete!")
122
+ }
123
+
124
+ export default defineCommand({
125
+ meta: { name: "adopt", description: "Adopt skills from agents into Simba's store" },
126
+ args: {
127
+ dryRun: { type: "boolean", alias: "n", description: "Preview changes without applying", default: false },
128
+ },
129
+ async run({ args }) {
130
+ const configStore = new ConfigStore(getConfigPath())
131
+ const config = await configStore.load()
132
+
133
+ const agentRegistry = new AgentRegistry(config.agents)
134
+ const detected = await agentRegistry.detectAgents()
135
+
136
+ const detectedAgents = Object.fromEntries(
137
+ Object.entries(detected).filter(([, a]) => a.detected)
138
+ )
139
+
140
+ if (Object.keys(detectedAgents).length === 0) {
141
+ console.log("No agents detected. Run 'simba detect' first.")
142
+ return
143
+ }
144
+
145
+ console.log("\nScanning agents for skills...")
146
+ for (const [id, agent] of Object.entries(detectedAgents)) {
147
+ console.log(` ${agent.name}`)
148
+ }
149
+
150
+ await runAdopt({
151
+ skillsDir: getSkillsDir(),
152
+ registryPath: getRegistryPath(),
153
+ configPath: getConfigPath(),
154
+ agents: detectedAgents,
155
+ dryRun: args.dryRun,
156
+ onConflict: async (skillName, skills) => {
157
+ // Read SKILL.md content and metadata from each conflicting skill
158
+ const contents = await Promise.all(
159
+ skills.map(async (s) => {
160
+ const skillPath = join(s.path, "SKILL.md")
161
+ const [content, stats] = await Promise.all([
162
+ readFile(skillPath, "utf-8"),
163
+ stat(skillPath),
164
+ ])
165
+ const parsed = matter(content)
166
+ return {
167
+ agentId: s.agentId,
168
+ content,
169
+ mtime: stats.mtime,
170
+ version: parsed.data.version as string | undefined,
171
+ }
172
+ })
173
+ )
174
+
175
+ // Check if all versions are identical
176
+ const firstContent = contents[0].content
177
+ const allIdentical = contents.every((c) => c.content === firstContent)
178
+
179
+ if (allIdentical) {
180
+ renderIdenticalMessage(skillName)
181
+ return contents[0].agentId
182
+ }
183
+
184
+ // Compare semver strings (basic: split by dots, compare numerically)
185
+ const compareSemver = (a: string, b: string): number => {
186
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0)
187
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0)
188
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
189
+ const diff = (pa[i] || 0) - (pb[i] || 0)
190
+ if (diff !== 0) return diff
191
+ }
192
+ return 0
193
+ }
194
+
195
+ // Find newest: prefer version comparison if all have versions
196
+ const allHaveVersions = contents.every((c) => c.version)
197
+ const newest = allHaveVersions
198
+ ? contents.reduce((a, b) => (compareSemver(a.version!, b.version!) >= 0 ? a : b))
199
+ : contents.reduce((a, b) => (a.mtime > b.mtime ? a : b))
200
+
201
+ // Show diff between first two versions (most common case)
202
+ const comparison = compareFiles(
203
+ contents[0].content,
204
+ contents[1].content,
205
+ "SKILL.md"
206
+ )
207
+
208
+ if (comparison.diff) {
209
+ renderDiff(comparison.diff, contents[0].agentId, contents[1].agentId)
210
+ }
211
+
212
+ // Format options with version or date
213
+ const formatDate = (d: Date) =>
214
+ d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
215
+
216
+ const result = await p.select({
217
+ message: `Conflict: "${skillName}" exists in multiple agents. Which version?`,
218
+ options: contents.map((c) => {
219
+ const info = c.version ? `v${c.version}` : formatDate(c.mtime)
220
+ const isNewest = c.agentId === newest.agentId
221
+ return {
222
+ value: c.agentId,
223
+ label: `${c.agentId} (${info})${isNewest ? " ← newest" : ""}`,
224
+ }
225
+ }),
226
+ })
227
+ if (p.isCancel(result)) process.exit(0)
228
+ return result as string
229
+ },
230
+ })
231
+ },
232
+ })
@@ -0,0 +1,110 @@
1
+ import { defineCommand } from "citty"
2
+ import * as p from "@clack/prompts"
3
+ import { RegistryStore } from "../core/registry-store"
4
+ import { SkillsStore } from "../core/skills-store"
5
+ import { ConfigStore } from "../core/config-store"
6
+ import { AgentRegistry } from "../core/agent-registry"
7
+ import { getSkillsDir, getRegistryPath, getConfigPath, expandPath } from "../utils/paths"
8
+
9
+ export interface AssignOptions {
10
+ skill: string
11
+ agents: string[]
12
+ skillsDir: string
13
+ registryPath: string
14
+ agentPaths: Record<string, string>
15
+ }
16
+
17
+ export async function runAssign(options: AssignOptions): Promise<void> {
18
+ const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
19
+ const registryStore = new RegistryStore(options.registryPath)
20
+ const registry = await registryStore.load()
21
+
22
+ const skill = registry.skills[options.skill]
23
+ if (!skill) {
24
+ console.error(`Skill not found: ${options.skill}`)
25
+ process.exit(1)
26
+ }
27
+
28
+ for (const agentId of options.agents) {
29
+ const agentPath = options.agentPaths[agentId]
30
+ if (!agentPath) {
31
+ console.error(`Unknown agent: ${agentId}`)
32
+ continue
33
+ }
34
+
35
+ await skillsStore.assignSkill(options.skill, agentPath, { type: "directory" })
36
+ skill.assignments[agentId] = { type: "directory" }
37
+ console.log(`Assigned ${options.skill} to ${agentId}`)
38
+ }
39
+
40
+ await registryStore.save(registry)
41
+ }
42
+
43
+ export default defineCommand({
44
+ meta: { name: "assign", description: "Assign a skill to agents" },
45
+ args: {
46
+ skill: { type: "positional", description: "Skill name", required: false },
47
+ agents: { type: "positional", description: "Agent IDs (comma-separated)", required: false },
48
+ },
49
+ async run({ args }) {
50
+ const configStore = new ConfigStore(getConfigPath())
51
+ const config = await configStore.load()
52
+
53
+ const registryStore = new RegistryStore(getRegistryPath())
54
+ const registry = await registryStore.load()
55
+
56
+ // Get detected agents
57
+ const agentRegistry = new AgentRegistry(config.agents)
58
+ const detected = await agentRegistry.detectAgents()
59
+ const detectedAgents = Object.entries(detected).filter(([, a]) => a.detected)
60
+
61
+ const agentPaths: Record<string, string> = {}
62
+ for (const [id, agent] of detectedAgents) {
63
+ agentPaths[id] = expandPath(agent.globalPath)
64
+ }
65
+
66
+ // Interactive mode if args missing
67
+ let skill = args.skill as string | undefined
68
+ let agents: string[]
69
+
70
+ if (!skill) {
71
+ const skills = Object.keys(registry.skills)
72
+ if (skills.length === 0) {
73
+ console.log("No skills managed. Run 'simba adopt' first.")
74
+ return
75
+ }
76
+
77
+ const result = await p.select({
78
+ message: "Select a skill to assign",
79
+ options: skills.map((s) => ({ value: s, label: s })),
80
+ })
81
+ if (p.isCancel(result)) process.exit(0)
82
+ skill = result as string
83
+ }
84
+
85
+ if (!args.agents) {
86
+ if (detectedAgents.length === 0) {
87
+ console.log("No agents detected.")
88
+ return
89
+ }
90
+
91
+ const result = await p.multiselect({
92
+ message: "Select agents to assign to",
93
+ options: detectedAgents.map(([id, a]) => ({ value: id, label: a.name })),
94
+ required: true,
95
+ })
96
+ if (p.isCancel(result)) process.exit(0)
97
+ agents = result as string[]
98
+ } else {
99
+ agents = (args.agents as string).split(",").map((a) => a.trim())
100
+ }
101
+
102
+ await runAssign({
103
+ skill,
104
+ agents,
105
+ skillsDir: getSkillsDir(),
106
+ registryPath: getRegistryPath(),
107
+ agentPaths,
108
+ })
109
+ },
110
+ })
@@ -0,0 +1,121 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { getConfigPath, expandPath } from "../utils/paths"
5
+ import { inputText } from "../utils/prompts"
6
+ import { mkdir, writeFile, readFile } from "node:fs/promises"
7
+ import { join, dirname } from "node:path"
8
+ import * as tar from "tar"
9
+
10
+ export default defineCommand({
11
+ meta: {
12
+ name: "backup",
13
+ description: "Export all skills to archive",
14
+ },
15
+ args: {
16
+ path: {
17
+ type: "positional",
18
+ description: "Output path (.tar.gz)",
19
+ },
20
+ includeConfig: {
21
+ type: "boolean",
22
+ description: "Include simba config in backup",
23
+ default: false,
24
+ },
25
+ },
26
+ async run({ args }) {
27
+ const configStore = new ConfigStore(getConfigPath())
28
+ const config = await configStore.load()
29
+ const registry = new AgentRegistry(config.agents)
30
+
31
+ // Prompt for path if not provided
32
+ const defaultPath = `./simba-backup-${new Date().toISOString().slice(0, 10)}.tar.gz`
33
+ let outputPath = args.path
34
+ if (!outputPath) {
35
+ outputPath = await inputText("Output path:", {
36
+ placeholder: defaultPath,
37
+ defaultValue: defaultPath,
38
+ })
39
+ }
40
+
41
+ // Collect all unique skills
42
+ const allSkills = new Map<string, { path: string; origin: string }>()
43
+
44
+ for (const [agentId, agent] of Object.entries(config.agents)) {
45
+ if (!agent.detected) continue
46
+
47
+ const skills = await registry.listSkills(agentId)
48
+ for (const skill of skills) {
49
+ if (!allSkills.has(skill.name)) {
50
+ allSkills.set(skill.name, {
51
+ path: registry.getSkillPath(skill.name, agentId),
52
+ origin: agentId,
53
+ })
54
+ }
55
+ }
56
+ }
57
+
58
+ if (allSkills.size === 0) {
59
+ console.log("No skills to backup.")
60
+ return
61
+ }
62
+
63
+ // Create temp directory for backup structure
64
+ const tempDir = join(dirname(outputPath), `.simba-backup-${Date.now()}`)
65
+ const skillsDir = join(tempDir, "skills")
66
+ await mkdir(skillsDir, { recursive: true })
67
+
68
+ // Copy skills to temp structure
69
+ const manifest = {
70
+ version: "1",
71
+ created: new Date().toISOString(),
72
+ simba_version: "0.1.0",
73
+ source_agents: [...new Set(Array.from(allSkills.values()).map((s) => s.origin))],
74
+ skills: {} as Record<string, { hash: string; origin: string; files: string[] }>,
75
+ includes_config: args.includeConfig,
76
+ }
77
+
78
+ for (const [name, { path, origin }] of allSkills) {
79
+ const destPath = join(skillsDir, name)
80
+ await Bun.$`cp -r ${path} ${destPath}`
81
+
82
+ const skill = (await registry.listSkills(origin)).find((s) => s.name === name)
83
+ if (skill) {
84
+ manifest.skills[name] = {
85
+ hash: skill.treeHash,
86
+ origin,
87
+ files: skill.files.map((f) => f.path),
88
+ }
89
+ }
90
+ }
91
+
92
+ // Write manifest
93
+ await writeFile(
94
+ join(tempDir, "manifest.json"),
95
+ JSON.stringify(manifest, null, 2)
96
+ )
97
+
98
+ // Include config if requested
99
+ if (args.includeConfig) {
100
+ const configContent = await readFile(getConfigPath(), "utf-8")
101
+ await writeFile(join(tempDir, "config.toml"), configContent)
102
+ }
103
+
104
+ // Create tar.gz
105
+ await tar.create(
106
+ {
107
+ gzip: true,
108
+ file: outputPath,
109
+ cwd: tempDir,
110
+ },
111
+ ["manifest.json", "skills", ...(args.includeConfig ? ["config.toml"] : [])]
112
+ )
113
+
114
+ // Cleanup temp
115
+ await Bun.$`rm -rf ${tempDir}`
116
+
117
+ console.log(`\nBackup created: ${outputPath}`)
118
+ console.log(`Skills: ${allSkills.size}`)
119
+ console.log(`Config included: ${args.includeConfig}`)
120
+ },
121
+ })