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 +181 -0
- package/package.json +59 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/adopt.ts +232 -0
- package/src/commands/assign.ts +110 -0
- package/src/commands/backup.ts +121 -0
- package/src/commands/detect.ts +63 -0
- package/src/commands/doctor.ts +169 -0
- package/src/commands/import.ts +99 -0
- package/src/commands/install.ts +486 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/manage.ts +12 -0
- package/src/commands/migrate.ts +137 -0
- package/src/commands/restore.ts +175 -0
- package/src/commands/snapshots.ts +39 -0
- package/src/commands/status.ts +70 -0
- package/src/commands/sync.ts +108 -0
- package/src/commands/unassign.ts +68 -0
- package/src/commands/undo.ts +57 -0
- package/src/commands/uninstall.ts +134 -0
- package/src/commands/update.ts +251 -0
- package/src/core/agent-registry.ts +105 -0
- package/src/core/config-store.ts +89 -0
- package/src/core/registry-store.ts +28 -0
- package/src/core/skill-manager.ts +99 -0
- package/src/core/skills-store.ts +79 -0
- package/src/core/snapshot.ts +123 -0
- package/src/core/types.ts +75 -0
- package/src/index.ts +33 -0
- package/src/tui/.gitkeep +0 -0
- package/src/tui/matrix.ts +189 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/diff.ts +53 -0
- package/src/utils/hash.ts +47 -0
- package/src/utils/paths.ts +30 -0
- package/src/utils/prompts.ts +85 -0
- package/src/utils/symlinks.ts +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Simba
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/simba-skills)
|
|
4
|
+
[](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
|
+
})
|