simba-skills 0.1.2 → 0.2.0
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/package.json +3 -2
- package/src/commands/adopt.ts +35 -3
- package/src/commands/doctor.ts +12 -1
- package/src/index.ts +6 -1
- package/src/tui/matrix.ts +34 -14
- package/src/utils/symlinks.ts +32 -2
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simba-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI skills manager - central store with symlink-based distribution across 14+ coding agents",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
"access": "public"
|
|
6
|
+
"access": "public",
|
|
7
|
+
"provenance": true
|
|
7
8
|
},
|
|
8
9
|
"type": "module",
|
|
9
10
|
"license": "MIT",
|
package/src/commands/adopt.ts
CHANGED
|
@@ -70,9 +70,12 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const toAdopt: Array<{ name: string; skill: DiscoveredSkill }> = []
|
|
73
|
+
const toTakeover: Array<{ name: string; skills: DiscoveredSkill[] }> = []
|
|
74
|
+
|
|
73
75
|
for (const [name, skills] of byName) {
|
|
74
76
|
if (await skillsStore.hasSkill(name)) {
|
|
75
|
-
|
|
77
|
+
// Already in store - take over rogue copies
|
|
78
|
+
toTakeover.push({ name, skills })
|
|
76
79
|
continue
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -86,12 +89,14 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
if (toAdopt.length === 0) {
|
|
92
|
+
if (toAdopt.length === 0 && toTakeover.length === 0) {
|
|
90
93
|
console.log("\nNo new skills to adopt.")
|
|
91
94
|
return
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
if (toAdopt.length > 0) {
|
|
98
|
+
console.log(`\nAdopting ${toAdopt.length} skills...`)
|
|
99
|
+
}
|
|
95
100
|
|
|
96
101
|
if (options.dryRun) {
|
|
97
102
|
for (const { name, skill } of toAdopt) {
|
|
@@ -117,6 +122,33 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
117
122
|
console.log(` Adopted: ${name} (from ${skill.agentId})`)
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
// Take over rogue copies (skill already in store, but real dirs exist at agents)
|
|
126
|
+
if (toTakeover.length > 0) {
|
|
127
|
+
console.log(`\nTaking over ${toTakeover.length} rogue skills...`)
|
|
128
|
+
|
|
129
|
+
if (!options.dryRun) {
|
|
130
|
+
for (const { name, skills } of toTakeover) {
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
await rm(skill.path, { recursive: true })
|
|
133
|
+
await createSymlink(join(options.skillsDir, name), skill.path)
|
|
134
|
+
|
|
135
|
+
// Update registry assignment
|
|
136
|
+
if (!registry.skills[name].assignments[skill.agentId]) {
|
|
137
|
+
registry.skills[name].assignments[skill.agentId] = { type: "directory" }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(` Replaced: ${name} (${skill.agentId})`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
for (const { name, skills } of toTakeover) {
|
|
145
|
+
for (const skill of skills) {
|
|
146
|
+
console.log(` Would replace: ${name} (${skill.agentId})`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
await registryStore.save(registry)
|
|
121
153
|
console.log("\nAdoption complete!")
|
|
122
154
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from "citty"
|
|
2
|
-
import { access } from "node:fs/promises"
|
|
2
|
+
import { access, rm } from "node:fs/promises"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import * as p from "@clack/prompts"
|
|
5
5
|
import { ConfigStore } from "../core/config-store"
|
|
@@ -164,6 +164,17 @@ export default defineCommand({
|
|
|
164
164
|
console.log(`Fixed: ${broken.skill} (${broken.agent})`)
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
for (const rogue of results.rogue) {
|
|
168
|
+
const agent = detected[rogue.agent]
|
|
169
|
+
if (!agent) continue
|
|
170
|
+
|
|
171
|
+
// Delete rogue directory/file and replace with symlink
|
|
172
|
+
await rm(rogue.path, { recursive: true })
|
|
173
|
+
const expectedTarget = join(getSkillsDir(), rogue.skill)
|
|
174
|
+
await createSymlink(expectedTarget, rogue.path)
|
|
175
|
+
console.log(`Fixed rogue: ${rogue.skill} (${rogue.agent})`)
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
console.log("\nRepairs complete!")
|
|
168
179
|
},
|
|
169
180
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { defineCommand, runMain } from "citty"
|
|
3
|
+
import { defineCommand, runMain, showUsage } from "citty"
|
|
4
4
|
|
|
5
5
|
const main = defineCommand({
|
|
6
6
|
meta: {
|
|
@@ -8,6 +8,11 @@ const main = defineCommand({
|
|
|
8
8
|
version: "0.2.0",
|
|
9
9
|
description: "AI skills manager",
|
|
10
10
|
},
|
|
11
|
+
async run({ cmd, rawArgs }) {
|
|
12
|
+
if (rawArgs.length === 0) {
|
|
13
|
+
await showUsage(cmd)
|
|
14
|
+
}
|
|
15
|
+
},
|
|
11
16
|
subCommands: {
|
|
12
17
|
adopt: () => import("./commands/adopt").then((m) => m.default),
|
|
13
18
|
assign: () => import("./commands/assign").then((m) => m.default),
|
package/src/tui/matrix.ts
CHANGED
|
@@ -6,8 +6,7 @@ import { AgentRegistry } from "../core/agent-registry"
|
|
|
6
6
|
import { getRegistryPath, getSkillsDir, getConfigPath, expandPath } from "../utils/paths"
|
|
7
7
|
import type { Agent, Registry } from "../core/types"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const term = termkit.terminal as typeof termkit.terminal & { showCursor: () => void }
|
|
9
|
+
const term = termkit.terminal
|
|
11
10
|
|
|
12
11
|
interface MatrixState {
|
|
13
12
|
skills: string[]
|
|
@@ -102,7 +101,7 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
102
101
|
|
|
103
102
|
term("\n")
|
|
104
103
|
term("─".repeat(21 + state.agents.length * 10) + "\n")
|
|
105
|
-
term.dim("[Space] Toggle [a] Assign all [n] None [q]
|
|
104
|
+
term.dim("[Space] Toggle [a] Assign all [n] None [Enter] Confirm [q] Cancel\n")
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
const toggle = async () => {
|
|
@@ -110,29 +109,43 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
110
109
|
const agent = state.agents[state.cursorCol]
|
|
111
110
|
const skill = state.registry.skills[skillName]
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
try {
|
|
113
|
+
if (skill.assignments[agent.id]) {
|
|
114
|
+
await skillsStore.unassignSkill(skillName, expandPath(agent.globalPath))
|
|
115
|
+
delete skill.assignments[agent.id]
|
|
116
|
+
} else {
|
|
117
|
+
await skillsStore.assignSkill(skillName, expandPath(agent.globalPath), { type: "directory" })
|
|
118
|
+
skill.assignments[agent.id] = { type: "directory" }
|
|
119
|
+
}
|
|
120
|
+
await registryStore.save(state.registry)
|
|
121
|
+
} catch (err) {
|
|
122
|
+
term.moveTo(1, state.skills.length + 8)
|
|
123
|
+
term.yellow(`Error: ${(err as Error).message}\n`)
|
|
119
124
|
}
|
|
120
|
-
|
|
121
|
-
await registryStore.save(state.registry)
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
const assignAll = async () => {
|
|
125
128
|
const skillName = state.skills[state.cursorRow]
|
|
126
129
|
const skill = state.registry.skills[skillName]
|
|
130
|
+
const errors: string[] = []
|
|
127
131
|
|
|
128
132
|
for (const agent of state.agents) {
|
|
129
133
|
if (!skill.assignments[agent.id]) {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
try {
|
|
135
|
+
await skillsStore.assignSkill(skillName, expandPath(agent.globalPath), { type: "directory" })
|
|
136
|
+
skill.assignments[agent.id] = { type: "directory" }
|
|
137
|
+
} catch (err) {
|
|
138
|
+
errors.push(`${agent.name}: ${(err as Error).message}`)
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
await registryStore.save(state.registry)
|
|
144
|
+
|
|
145
|
+
if (errors.length > 0) {
|
|
146
|
+
term.moveTo(1, state.skills.length + 8)
|
|
147
|
+
term.yellow(`Errors:\n${errors.join("\n")}\n`)
|
|
148
|
+
}
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
const unassignAll = async () => {
|
|
@@ -176,11 +189,18 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
176
189
|
case "n":
|
|
177
190
|
await unassignAll()
|
|
178
191
|
break
|
|
192
|
+
case "ENTER":
|
|
193
|
+
term.clear()
|
|
194
|
+
term.hideCursor(false)
|
|
195
|
+
term.grabInput(false)
|
|
196
|
+
term.green("Changes saved.\n")
|
|
197
|
+
process.exit(0)
|
|
179
198
|
case "q":
|
|
180
199
|
case "CTRL_C":
|
|
181
200
|
term.clear()
|
|
182
|
-
term.
|
|
201
|
+
term.hideCursor(false)
|
|
183
202
|
term.grabInput(false)
|
|
203
|
+
term.yellow("Cancelled.\n")
|
|
184
204
|
process.exit(0)
|
|
185
205
|
}
|
|
186
206
|
|
package/src/utils/symlinks.ts
CHANGED
|
@@ -3,7 +3,29 @@ import { dirname } from "node:path"
|
|
|
3
3
|
|
|
4
4
|
export async function createSymlink(source: string, target: string): Promise<void> {
|
|
5
5
|
await mkdir(dirname(target), { recursive: true })
|
|
6
|
-
|
|
6
|
+
try {
|
|
7
|
+
await symlink(source, target)
|
|
8
|
+
} catch (err) {
|
|
9
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
10
|
+
// Already exists - check if it's a symlink pointing to the right place
|
|
11
|
+
const existing = await getSymlinkTarget(target)
|
|
12
|
+
if (existing === source) return // Already correct symlink
|
|
13
|
+
|
|
14
|
+
// Check what we're dealing with
|
|
15
|
+
const stat = await lstat(target)
|
|
16
|
+
if (stat.isSymbolicLink()) {
|
|
17
|
+
await unlink(target)
|
|
18
|
+
} else if (stat.isDirectory()) {
|
|
19
|
+
// Real directory - don't delete, throw error
|
|
20
|
+
throw new Error(`Cannot create symlink: ${target} is a directory (not managed by simba)`)
|
|
21
|
+
} else {
|
|
22
|
+
await unlink(target)
|
|
23
|
+
}
|
|
24
|
+
await symlink(source, target)
|
|
25
|
+
} else {
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
}
|
|
8
30
|
|
|
9
31
|
export async function isSymlink(path: string): Promise<boolean> {
|
|
@@ -17,7 +39,15 @@ export async function isSymlink(path: string): Promise<boolean> {
|
|
|
17
39
|
|
|
18
40
|
export async function removeSymlink(path: string): Promise<void> {
|
|
19
41
|
try {
|
|
20
|
-
await
|
|
42
|
+
const stat = await lstat(path)
|
|
43
|
+
if (stat.isSymbolicLink()) {
|
|
44
|
+
await unlink(path)
|
|
45
|
+
} else if (stat.isDirectory()) {
|
|
46
|
+
// Real directory - don't delete, it's not managed by simba
|
|
47
|
+
throw new Error(`Cannot remove: ${path} is a directory (not managed by simba)`)
|
|
48
|
+
} else {
|
|
49
|
+
await unlink(path)
|
|
50
|
+
}
|
|
21
51
|
} catch (err) {
|
|
22
52
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
23
53
|
throw err
|