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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
4
|
+
import { getConfigPath } from "../utils/paths"
|
|
5
|
+
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "detect",
|
|
9
|
+
description: "Scan for installed agents and skills",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
refresh: {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
description: "Force rescan even if already detected",
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
20
|
+
const config = await configStore.load()
|
|
21
|
+
|
|
22
|
+
const registry = new AgentRegistry(config.agents)
|
|
23
|
+
const detected = await registry.detectAgents()
|
|
24
|
+
|
|
25
|
+
// Update config with detection results
|
|
26
|
+
for (const [id, agent] of Object.entries(detected)) {
|
|
27
|
+
config.agents[id] = agent
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Scan skills from detected agents
|
|
31
|
+
let totalSkills = 0
|
|
32
|
+
for (const [id, agent] of Object.entries(detected)) {
|
|
33
|
+
if (!agent.detected) continue
|
|
34
|
+
|
|
35
|
+
const skills = await registry.listSkills(id)
|
|
36
|
+
totalSkills += skills.length
|
|
37
|
+
|
|
38
|
+
for (const skill of skills) {
|
|
39
|
+
config.skills[skill.name] = {
|
|
40
|
+
...config.skills[skill.name],
|
|
41
|
+
...skill,
|
|
42
|
+
agents: [
|
|
43
|
+
...new Set([
|
|
44
|
+
...(config.skills[skill.name]?.agents ?? []),
|
|
45
|
+
id,
|
|
46
|
+
]),
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await configStore.save(config)
|
|
53
|
+
|
|
54
|
+
// Output results
|
|
55
|
+
console.log("\nDetected agents:")
|
|
56
|
+
for (const [id, agent] of Object.entries(detected)) {
|
|
57
|
+
const status = agent.detected ? "✓" : "─"
|
|
58
|
+
console.log(` ${status} ${agent.name}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\nTotal skills found: ${totalSkills}`)
|
|
62
|
+
},
|
|
63
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { access } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import * as p from "@clack/prompts"
|
|
5
|
+
import { ConfigStore } from "../core/config-store"
|
|
6
|
+
import { RegistryStore } from "../core/registry-store"
|
|
7
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
8
|
+
import { getConfigPath, getSkillsDir, getRegistryPath, expandPath } from "../utils/paths"
|
|
9
|
+
import { isSymlink, getSymlinkTarget, createSymlink, removeSymlink } from "../utils/symlinks"
|
|
10
|
+
import type { Agent } from "../core/types"
|
|
11
|
+
|
|
12
|
+
interface BrokenLink {
|
|
13
|
+
skill: string
|
|
14
|
+
agent: string
|
|
15
|
+
path: string
|
|
16
|
+
reason: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RogueFile {
|
|
20
|
+
skill: string
|
|
21
|
+
agent: string
|
|
22
|
+
path: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DoctorResults {
|
|
26
|
+
healthy: string[]
|
|
27
|
+
broken: BrokenLink[]
|
|
28
|
+
rogue: RogueFile[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DoctorOptions {
|
|
32
|
+
skillsDir: string
|
|
33
|
+
registryPath: string
|
|
34
|
+
agents: Record<string, Agent>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runDoctor(options: DoctorOptions): Promise<DoctorResults> {
|
|
38
|
+
const registryStore = new RegistryStore(options.registryPath)
|
|
39
|
+
const registry = await registryStore.load()
|
|
40
|
+
|
|
41
|
+
const results: DoctorResults = {
|
|
42
|
+
healthy: [],
|
|
43
|
+
broken: [],
|
|
44
|
+
rogue: [],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const [skillName, skill] of Object.entries(registry.skills)) {
|
|
48
|
+
let skillHealthy = true
|
|
49
|
+
|
|
50
|
+
for (const [agentId, assignment] of Object.entries(skill.assignments)) {
|
|
51
|
+
const agent = options.agents[agentId]
|
|
52
|
+
if (!agent || !agent.detected) continue
|
|
53
|
+
|
|
54
|
+
const agentSkillsDir = expandPath(agent.globalPath)
|
|
55
|
+
const expectedPath = join(agentSkillsDir, skillName)
|
|
56
|
+
const expectedTarget = join(options.skillsDir, skillName)
|
|
57
|
+
|
|
58
|
+
const pathIsSymlink = await isSymlink(expectedPath)
|
|
59
|
+
|
|
60
|
+
if (!pathIsSymlink) {
|
|
61
|
+
try {
|
|
62
|
+
await access(expectedPath)
|
|
63
|
+
results.rogue.push({ skill: skillName, agent: agentId, path: expectedPath })
|
|
64
|
+
skillHealthy = false
|
|
65
|
+
} catch {
|
|
66
|
+
results.broken.push({
|
|
67
|
+
skill: skillName,
|
|
68
|
+
agent: agentId,
|
|
69
|
+
path: expectedPath,
|
|
70
|
+
reason: "symlink missing",
|
|
71
|
+
})
|
|
72
|
+
skillHealthy = false
|
|
73
|
+
}
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const target = await getSymlinkTarget(expectedPath)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await access(target!)
|
|
81
|
+
} catch {
|
|
82
|
+
results.broken.push({
|
|
83
|
+
skill: skillName,
|
|
84
|
+
agent: agentId,
|
|
85
|
+
path: expectedPath,
|
|
86
|
+
reason: "target missing",
|
|
87
|
+
})
|
|
88
|
+
skillHealthy = false
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (target !== expectedTarget) {
|
|
93
|
+
results.broken.push({
|
|
94
|
+
skill: skillName,
|
|
95
|
+
agent: agentId,
|
|
96
|
+
path: expectedPath,
|
|
97
|
+
reason: `wrong target: ${target}`,
|
|
98
|
+
})
|
|
99
|
+
skillHealthy = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (skillHealthy) {
|
|
104
|
+
results.healthy.push(skillName)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default defineCommand({
|
|
112
|
+
meta: { name: "doctor", description: "Verify symlink integrity" },
|
|
113
|
+
args: {
|
|
114
|
+
fix: { type: "boolean", description: "Automatically fix issues", default: false },
|
|
115
|
+
},
|
|
116
|
+
async run({ args }) {
|
|
117
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
118
|
+
const config = await configStore.load()
|
|
119
|
+
|
|
120
|
+
const agentRegistry = new AgentRegistry(config.agents)
|
|
121
|
+
const detected = await agentRegistry.detectAgents()
|
|
122
|
+
|
|
123
|
+
console.log("\nChecking symlink integrity...\n")
|
|
124
|
+
|
|
125
|
+
const results = await runDoctor({
|
|
126
|
+
skillsDir: getSkillsDir(),
|
|
127
|
+
registryPath: getRegistryPath(),
|
|
128
|
+
agents: detected,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
for (const skill of results.healthy) {
|
|
132
|
+
console.log(`✓ ${skill}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const broken of results.broken) {
|
|
136
|
+
console.log(`✗ ${broken.skill}`)
|
|
137
|
+
console.log(` └─ ${broken.agent}: BROKEN (${broken.reason})`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const rogue of results.rogue) {
|
|
141
|
+
console.log(`⚠ ${rogue.skill}`)
|
|
142
|
+
console.log(` └─ ${rogue.agent}: ROGUE (real file, not symlink)`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`\nSummary: ${results.broken.length} broken, ${results.rogue.length} rogue, ${results.healthy.length} healthy`)
|
|
146
|
+
|
|
147
|
+
if (results.broken.length === 0 && results.rogue.length === 0) {
|
|
148
|
+
console.log("\nAll symlinks healthy!")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!args.fix) {
|
|
153
|
+
const shouldFix = await p.confirm({ message: "Fix issues?" })
|
|
154
|
+
if (p.isCancel(shouldFix) || !shouldFix) return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const broken of results.broken) {
|
|
158
|
+
const agent = detected[broken.agent]
|
|
159
|
+
if (!agent) continue
|
|
160
|
+
|
|
161
|
+
const expectedTarget = join(getSkillsDir(), broken.skill)
|
|
162
|
+
await removeSymlink(broken.path)
|
|
163
|
+
await createSymlink(expectedTarget, broken.path)
|
|
164
|
+
console.log(`Fixed: ${broken.skill} (${broken.agent})`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log("\nRepairs complete!")
|
|
168
|
+
},
|
|
169
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
4
|
+
import { getConfigPath } from "../utils/paths"
|
|
5
|
+
import { selectFromList, selectAgent } from "../utils/prompts"
|
|
6
|
+
import { mkdir, access } from "node:fs/promises"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "import",
|
|
12
|
+
description: "Copy a global skill into current project",
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
skill: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "Skill name to import",
|
|
18
|
+
},
|
|
19
|
+
to: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Target directory (defaults to detected agent's project path)",
|
|
22
|
+
},
|
|
23
|
+
agent: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Source agent (defaults to first detected with skill)",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async run({ args }) {
|
|
29
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
30
|
+
const config = await configStore.load()
|
|
31
|
+
const registry = new AgentRegistry(config.agents)
|
|
32
|
+
|
|
33
|
+
let skillName = args.skill
|
|
34
|
+
let agentId = args.agent
|
|
35
|
+
|
|
36
|
+
// Interactive agent selection if not provided
|
|
37
|
+
if (!agentId) {
|
|
38
|
+
agentId = await selectAgent(config.agents, "Select source agent")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const agent = config.agents[agentId]
|
|
42
|
+
if (!agent || !agent.detected) {
|
|
43
|
+
console.error(`Agent not found or not detected: ${agentId}`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get available skills from selected agent
|
|
48
|
+
const availableSkills = await registry.listSkills(agentId)
|
|
49
|
+
|
|
50
|
+
if (availableSkills.length === 0) {
|
|
51
|
+
console.log(`No skills found in ${agent.name}.`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Interactive skill selection if not provided
|
|
56
|
+
if (!skillName) {
|
|
57
|
+
skillName = await selectFromList(
|
|
58
|
+
"Select skill to import:",
|
|
59
|
+
availableSkills.map((s) => ({ value: s.name, label: s.name }))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Find skill source
|
|
64
|
+
const sourcePath = registry.getSkillPath(skillName, agentId)
|
|
65
|
+
try {
|
|
66
|
+
await access(join(sourcePath, "SKILL.md"))
|
|
67
|
+
} catch {
|
|
68
|
+
console.error(`Skill not found in ${agentId}: ${skillName}`)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine target path
|
|
73
|
+
let targetPath: string
|
|
74
|
+
|
|
75
|
+
if (args.to) {
|
|
76
|
+
targetPath = join(args.to, skillName)
|
|
77
|
+
} else {
|
|
78
|
+
// Use project path of source agent
|
|
79
|
+
targetPath = join(process.cwd(), agent.projectPath, skillName)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if target exists
|
|
83
|
+
try {
|
|
84
|
+
await access(targetPath)
|
|
85
|
+
console.error(`Skill already exists at: ${targetPath}`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
} catch {
|
|
88
|
+
// Good, doesn't exist
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Copy skill
|
|
92
|
+
await mkdir(join(targetPath, ".."), { recursive: true })
|
|
93
|
+
await Bun.$`cp -r ${sourcePath} ${targetPath}`
|
|
94
|
+
|
|
95
|
+
console.log(`\nImported: ${skillName}`)
|
|
96
|
+
console.log(`From: ${agent.name}`)
|
|
97
|
+
console.log(`To: ${targetPath}`)
|
|
98
|
+
},
|
|
99
|
+
})
|