simba-skills 0.2.0 → 0.4.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/README.md +4 -0
- package/package.json +3 -4
- package/src/commands/install.ts +40 -15
- package/src/commands/update.ts +60 -8
- package/src/core/config-store.ts +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Simba
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/simba.jpg" alt="Simba the cat" width="600">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
[](https://www.npmjs.com/package/simba-skills)
|
|
4
8
|
[](LICENSE)
|
|
5
9
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simba-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AI skills manager - central store with symlink-based distribution across 14+ coding agents",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
"access": "public"
|
|
7
|
-
"provenance": true
|
|
6
|
+
"access": "public"
|
|
8
7
|
},
|
|
9
8
|
"type": "module",
|
|
10
9
|
"license": "MIT",
|
|
@@ -31,7 +30,7 @@
|
|
|
31
30
|
},
|
|
32
31
|
"files": [
|
|
33
32
|
"src",
|
|
34
|
-
"!
|
|
33
|
+
"!assets"
|
|
35
34
|
],
|
|
36
35
|
"scripts": {
|
|
37
36
|
"dev": "bun run src/index.ts",
|
package/src/commands/install.ts
CHANGED
|
@@ -281,6 +281,8 @@ export interface InstallOptions {
|
|
|
281
281
|
skillsDir: string
|
|
282
282
|
registryPath: string
|
|
283
283
|
useSSH: boolean
|
|
284
|
+
skillName?: string // Install specific skill by name, skip selection
|
|
285
|
+
installAll?: boolean // Install all discovered skills without prompts
|
|
284
286
|
onSelect: (skills: DiscoveredSkill[]) => Promise<string[]>
|
|
285
287
|
}
|
|
286
288
|
|
|
@@ -346,16 +348,33 @@ export async function runInstall(options: InstallOptions): Promise<void> {
|
|
|
346
348
|
return
|
|
347
349
|
}
|
|
348
350
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
let selected: string[]
|
|
352
|
+
|
|
353
|
+
if (options.installAll) {
|
|
354
|
+
selected = discovered.map(s => s.name)
|
|
355
|
+
console.log(`Installing all ${selected.length} skills...`)
|
|
356
|
+
} else if (options.skillName) {
|
|
357
|
+
// Direct install of specific skill
|
|
358
|
+
const skill = discovered.find(s => s.name === options.skillName)
|
|
359
|
+
if (!skill) {
|
|
360
|
+
console.log(`Skill "${options.skillName}" not found in source.`)
|
|
361
|
+
console.log(`Available skills: ${discovered.map(s => s.name).join(", ")}`)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
selected = [options.skillName]
|
|
365
|
+
console.log(`Installing skill: ${options.skillName}`)
|
|
366
|
+
} else {
|
|
367
|
+
console.log(`\nFound ${discovered.length} skills:`)
|
|
368
|
+
for (const skill of discovered) {
|
|
369
|
+
console.log(` * ${skill.name}${skill.description ? ` - ${skill.description}` : ""}`)
|
|
370
|
+
}
|
|
353
371
|
|
|
354
|
-
|
|
372
|
+
selected = await options.onSelect(discovered)
|
|
355
373
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
if (selected.length === 0) {
|
|
375
|
+
console.log("No skills selected.")
|
|
376
|
+
return
|
|
377
|
+
}
|
|
359
378
|
}
|
|
360
379
|
|
|
361
380
|
for (const name of selected) {
|
|
@@ -390,14 +409,16 @@ export async function runInstall(options: InstallOptions): Promise<void> {
|
|
|
390
409
|
renderDiff(comparison.diff, "current", "new")
|
|
391
410
|
}
|
|
392
411
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
412
|
+
if (!options.installAll) {
|
|
413
|
+
const update = await p.confirm({
|
|
414
|
+
message: `Update ${name}?`,
|
|
415
|
+
initialValue: true,
|
|
416
|
+
})
|
|
397
417
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
418
|
+
if (p.isCancel(update) || !update) {
|
|
419
|
+
console.log(` Skipping ${name}`)
|
|
420
|
+
continue
|
|
421
|
+
}
|
|
401
422
|
}
|
|
402
423
|
|
|
403
424
|
// Remove old and add new
|
|
@@ -458,6 +479,8 @@ export default defineCommand({
|
|
|
458
479
|
args: {
|
|
459
480
|
source: { type: "positional", description: "GitHub repo (user/repo) or local path", required: true },
|
|
460
481
|
ssh: { type: "boolean", description: "Use SSH for GitHub repos (for private repos)", default: false },
|
|
482
|
+
skill: { type: "string", description: "Install specific skill by name (skip selection)", required: false },
|
|
483
|
+
all: { type: "boolean", description: "Install all skills without prompts", default: false },
|
|
461
484
|
},
|
|
462
485
|
async run({ args }) {
|
|
463
486
|
await runInstall({
|
|
@@ -465,6 +488,8 @@ export default defineCommand({
|
|
|
465
488
|
skillsDir: getSkillsDir(),
|
|
466
489
|
registryPath: getRegistryPath(),
|
|
467
490
|
useSSH: args.ssh,
|
|
491
|
+
skillName: args.skill,
|
|
492
|
+
installAll: args.all,
|
|
468
493
|
onSelect: async (skills) => {
|
|
469
494
|
const result = await p.multiselect({
|
|
470
495
|
message: "Select skills to install:",
|
package/src/commands/update.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from "citty"
|
|
2
|
-
import { readFile, mkdir, rm } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
2
|
+
import { readFile, readdir, mkdir, rm } from "node:fs/promises"
|
|
3
|
+
import { join, relative } from "node:path"
|
|
4
4
|
import * as p from "@clack/prompts"
|
|
5
5
|
import simpleGit from "simple-git"
|
|
6
6
|
import { tmpdir } from "node:os"
|
|
7
|
+
import { createHash } from "node:crypto"
|
|
7
8
|
import { RegistryStore } from "../core/registry-store"
|
|
8
9
|
import { SkillsStore } from "../core/skills-store"
|
|
9
10
|
import { getSkillsDir, getRegistryPath } from "../utils/paths"
|
|
@@ -12,6 +13,50 @@ import { discoverSkills } from "./install"
|
|
|
12
13
|
import type { ManagedSkill, InstallSource } from "../core/types"
|
|
13
14
|
import matter from "gray-matter"
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Recursively get all files in a directory (sorted for deterministic hashing)
|
|
18
|
+
*/
|
|
19
|
+
async function getAllFiles(dir: string): Promise<string[]> {
|
|
20
|
+
const files: string[] = []
|
|
21
|
+
|
|
22
|
+
async function scan(currentDir: string): Promise<void> {
|
|
23
|
+
const entries = await readdir(currentDir, { withFileTypes: true })
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = join(currentDir, entry.name)
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
await scan(fullPath)
|
|
28
|
+
} else {
|
|
29
|
+
files.push(relative(dir, fullPath))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await scan(dir)
|
|
35
|
+
return files.sort()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute a content hash for an entire skill directory.
|
|
40
|
+
* Hash includes: sorted file paths + their contents
|
|
41
|
+
*/
|
|
42
|
+
async function hashSkillDir(dir: string): Promise<string> {
|
|
43
|
+
const files = await getAllFiles(dir)
|
|
44
|
+
const hash = createHash("sha256")
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
// Include file path in hash (detects renames/additions/deletions)
|
|
48
|
+
hash.update(file)
|
|
49
|
+
hash.update("\0")
|
|
50
|
+
|
|
51
|
+
// Include file content
|
|
52
|
+
const content = await readFile(join(dir, file))
|
|
53
|
+
hash.update(content)
|
|
54
|
+
hash.update("\0")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return hash.digest("hex")
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
interface SkillUpdate {
|
|
16
61
|
skill: ManagedSkill
|
|
17
62
|
newPath: string
|
|
@@ -119,16 +164,23 @@ export async function runUpdate(options: UpdateOptions): Promise<void> {
|
|
|
119
164
|
continue
|
|
120
165
|
}
|
|
121
166
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
const localDir = join(options.skillsDir, skill.name)
|
|
168
|
+
|
|
169
|
+
// Hash entire directories to detect any file changes
|
|
170
|
+
const [localHash, remoteHash] = await Promise.all([
|
|
171
|
+
hashSkillDir(localDir),
|
|
172
|
+
hashSkillDir(remote.path)
|
|
173
|
+
])
|
|
127
174
|
|
|
128
|
-
if (
|
|
175
|
+
if (localHash === remoteHash) {
|
|
129
176
|
console.log(` ✓ ${skill.name}: up to date`)
|
|
130
177
|
} else {
|
|
131
178
|
console.log(` ↑ ${skill.name}: update available`)
|
|
179
|
+
|
|
180
|
+
// Still read SKILL.md for version display purposes
|
|
181
|
+
const existingContent = await readFile(join(localDir, "SKILL.md"), "utf-8")
|
|
182
|
+
const newContent = await readFile(join(remote.path, "SKILL.md"), "utf-8")
|
|
183
|
+
|
|
132
184
|
allUpdates.push({
|
|
133
185
|
skill,
|
|
134
186
|
newPath: remote.path,
|
package/src/core/config-store.ts
CHANGED
|
@@ -13,7 +13,7 @@ const AGENT_DEFINITIONS: [string, string, string, string, string][] = [
|
|
|
13
13
|
["windsurf", "Windsurf", "Windsurf", "~/.codeium/windsurf/skills", ".windsurf/skills"],
|
|
14
14
|
["amp", "Amp", "Amp", "~/.config/agents/skills", ".agents/skills"],
|
|
15
15
|
["goose", "Goose", "Goose", "~/.config/goose/skills", ".goose/skills"],
|
|
16
|
-
["opencode", "OpenCode", "OpenCode", "~/.config/opencode/
|
|
16
|
+
["opencode", "OpenCode", "OpenCode", "~/.config/opencode/skills", ".opencode/skills"],
|
|
17
17
|
["kilo", "Kilo Code", "Kilo", "~/.kilocode/skills", ".kilocode/skills"],
|
|
18
18
|
["roo", "Roo Code", "Roo", "~/.roo/skills", ".roo/skills"],
|
|
19
19
|
["antigravity", "Antigravity", "Antigrav", "~/.gemini/antigravity/skills", ".agent/skills"],
|