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 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
  [![npm version](https://img.shields.io/npm/v/simba-skills)](https://www.npmjs.com/package/simba-skills)
4
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
9
 
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "simba-skills",
3
- "version": "0.2.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
- "!src/**/*.test.ts"
33
+ "!assets"
35
34
  ],
36
35
  "scripts": {
37
36
  "dev": "bun run src/index.ts",
@@ -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
- console.log(`\nFound ${discovered.length} skills:`)
350
- for (const skill of discovered) {
351
- console.log(` * ${skill.name}${skill.description ? ` - ${skill.description}` : ""}`)
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
- const selected = await options.onSelect(discovered)
372
+ selected = await options.onSelect(discovered)
355
373
 
356
- if (selected.length === 0) {
357
- console.log("No skills selected.")
358
- return
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
- const update = await p.confirm({
394
- message: `Update ${name}?`,
395
- initialValue: true,
396
- })
412
+ if (!options.installAll) {
413
+ const update = await p.confirm({
414
+ message: `Update ${name}?`,
415
+ initialValue: true,
416
+ })
397
417
 
398
- if (p.isCancel(update) || !update) {
399
- console.log(` Skipping ${name}`)
400
- continue
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:",
@@ -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 existingPath = join(options.skillsDir, skill.name, "SKILL.md")
123
- const existingContent = await readFile(existingPath, "utf-8")
124
- const newContent = await readFile(join(remote.path, "SKILL.md"), "utf-8")
125
-
126
- const comparison = compareFiles(existingContent, newContent, "SKILL.md")
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 (comparison.identical) {
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,
@@ -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/skill", ".opencode/skill"],
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"],