@dealdeploy/skl 0.2.0 → 0.3.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/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  } from "fs";
24
24
  import { join, resolve } from "path";
25
25
  import { homedir } from "os";
26
+ import { findOrphanSkills, adoptSkills } from "./lib.ts";
26
27
 
27
28
  // @ts-ignore - bun supports JSON imports
28
29
  const { version: VERSION } = await import("./package.json");
@@ -292,6 +293,39 @@ function ensureLocalClaudeDir(): string {
292
293
  }
293
294
  }
294
295
 
296
+ // ── Adopt local-only skills into library ────────────────────────────
297
+ {
298
+ const localDirsToScan: string[] = [];
299
+ const ld = findLocalDir();
300
+ if (ld) localDirsToScan.push(ld);
301
+ const lcd = findLocalClaudeDir();
302
+ if (lcd) localDirsToScan.push(lcd);
303
+
304
+ const orphans = findOrphanSkills(localDirsToScan, LIBRARY);
305
+
306
+ if (orphans.length > 0) {
307
+ console.log("\nFound skills in this repo not in your library:");
308
+ for (const o of orphans) {
309
+ console.log(` ${o.dir.replace(homedir(), "~")}/${o.name}`);
310
+ }
311
+ process.stdout.write(`\nCopy to ${LIBRARY.replace(homedir(), "~")}? [y/N] `);
312
+
313
+ const buf = new Uint8Array(100);
314
+ const n = readSync(0, buf);
315
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
316
+
317
+ if (answer === "y" || answer === "yes") {
318
+ const results = adoptSkills(orphans, LIBRARY);
319
+ for (const r of results) {
320
+ console.log(` copied ${r.name}`);
321
+ }
322
+ console.log(`\n${results.filter(r => r.copied).length} added to library\n`);
323
+ } else {
324
+ console.log("Skipping.\n");
325
+ }
326
+ }
327
+ }
328
+
295
329
  const allSkills = readdirSync(LIBRARY).sort();
296
330
 
297
331
  // ── State ───────────────────────────────────────────────────────────
package/lib.test.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { mkdirSync, rmSync, existsSync, writeFileSync, symlinkSync } from "fs";
3
+ import { join } from "path";
4
+ import { mkdtempSync } from "fs";
5
+ import { tmpdir } from "os";
6
+
7
+ import { findOrphanSkills, adoptSkills } from "./lib.ts";
8
+
9
+ let tmp: string;
10
+ let library: string;
11
+ let localDir: string;
12
+
13
+ beforeEach(() => {
14
+ tmp = mkdtempSync(join(tmpdir(), "skl-test-"));
15
+ library = join(tmp, "library");
16
+ localDir = join(tmp, "local");
17
+ mkdirSync(library);
18
+ mkdirSync(localDir);
19
+ });
20
+
21
+ afterEach(() => {
22
+ rmSync(tmp, { recursive: true, force: true });
23
+ });
24
+
25
+ test("detects a local skill not in the library", () => {
26
+ mkdirSync(join(localDir, "my-skill"));
27
+ mkdirSync(join(library, "other-skill"));
28
+
29
+ const orphans = findOrphanSkills([localDir], library);
30
+
31
+ expect(orphans).toEqual([{ dir: localDir, name: "my-skill" }]);
32
+ });
33
+
34
+ test("deduplicates when same skill exists in multiple local dirs", () => {
35
+ const localDir2 = join(tmp, "local2");
36
+ mkdirSync(localDir2);
37
+ mkdirSync(join(localDir, "shared-skill"));
38
+ mkdirSync(join(localDir2, "shared-skill"));
39
+
40
+ const orphans = findOrphanSkills([localDir, localDir2], library);
41
+
42
+ expect(orphans).toHaveLength(1);
43
+ expect(orphans[0]!.name).toBe("shared-skill");
44
+ });
45
+
46
+ test("adoptSkills copies orphan skills into the library", () => {
47
+ // Create a local skill with a file inside
48
+ mkdirSync(join(localDir, "cool-skill"));
49
+ writeFileSync(join(localDir, "cool-skill", "prompt.md"), "# Cool Skill\nDo stuff");
50
+
51
+ const orphans = [{ dir: localDir, name: "cool-skill" }];
52
+ const results = adoptSkills(orphans, library);
53
+
54
+ expect(results).toEqual([{ name: "cool-skill", copied: true }]);
55
+ expect(existsSync(join(library, "cool-skill", "prompt.md"))).toBe(true);
56
+ });
57
+
58
+ test("ignores symlinks and plain files in local dirs", () => {
59
+ // A regular file (not a directory)
60
+ writeFileSync(join(localDir, "not-a-skill"), "just a file");
61
+ // A symlink
62
+ mkdirSync(join(tmp, "symlink-target"));
63
+ symlinkSync(join(tmp, "symlink-target"), join(localDir, "linked-skill"));
64
+ // A real skill directory (should be detected)
65
+ mkdirSync(join(localDir, "real-skill"));
66
+
67
+ const orphans = findOrphanSkills([localDir], library);
68
+
69
+ expect(orphans).toEqual([{ dir: localDir, name: "real-skill" }]);
70
+ });
package/lib.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { readdirSync, lstatSync, existsSync, cpSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ export function findOrphanSkills(
5
+ localDirs: string[],
6
+ library: string,
7
+ ): { dir: string; name: string }[] {
8
+ const orphans: { dir: string; name: string }[] = [];
9
+ const seen = new Set<string>();
10
+
11
+ for (const dir of localDirs) {
12
+ try {
13
+ for (const name of readdirSync(dir)) {
14
+ if (seen.has(name)) continue;
15
+ const full = join(dir, name);
16
+ try {
17
+ const stat = lstatSync(full);
18
+ if (stat.isSymbolicLink()) continue;
19
+ if (!stat.isDirectory()) continue;
20
+ if (!existsSync(join(library, name))) {
21
+ seen.add(name);
22
+ orphans.push({ dir, name });
23
+ }
24
+ } catch {}
25
+ }
26
+ } catch {}
27
+ }
28
+
29
+ return orphans;
30
+ }
31
+
32
+ export function adoptSkills(
33
+ orphans: { dir: string; name: string }[],
34
+ library: string,
35
+ ): { name: string; copied: boolean }[] {
36
+ const results: { name: string; copied: boolean }[] = [];
37
+
38
+ for (const { dir, name } of orphans) {
39
+ const src = join(dir, name);
40
+ const dest = join(library, name);
41
+ try {
42
+ cpSync(src, dest, { recursive: true });
43
+ results.push({ name, copied: true });
44
+ } catch {
45
+ results.push({ name, copied: false });
46
+ }
47
+ }
48
+
49
+ return results;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
package/update.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { readdirSync, existsSync, cpSync, rmSync, lstatSync, readlinkSync, readSync, unlinkSync } from "fs";
4
4
  import { join, resolve } from "path";
5
5
  import { homedir } from "os";
6
+ import { findOrphanSkills, adoptSkills } from "./lib.ts";
6
7
 
7
8
  const LIBRARY = join(homedir(), "dotfiles/skills");
8
9
 
@@ -27,16 +28,11 @@ type Entry = { dir: string; label: string; name: string; kind: "symlink" | "copy
27
28
 
28
29
  const symlinks: Entry[] = [];
29
30
  const copies: Entry[] = [];
30
- let skipped = 0;
31
-
32
31
  for (const [dir, label] of localDirs) {
33
32
  for (const name of readdirSync(dir)) {
34
33
  const localPath = join(dir, name);
35
34
  const libPath = join(LIBRARY, name);
36
- if (!existsSync(libPath)) {
37
- skipped++;
38
- continue;
39
- }
35
+ if (!existsSync(libPath)) continue;
40
36
  const isSym = lstatSync(localPath).isSymbolicLink();
41
37
  const entry: Entry = { dir, label, name, kind: isSym ? "symlink" : "copy" };
42
38
  if (isSym) symlinks.push(entry);
@@ -44,6 +40,8 @@ for (const [dir, label] of localDirs) {
44
40
  }
45
41
  }
46
42
 
43
+ const notInLibrary = findOrphanSkills(localDirs.map(([dir]) => dir), LIBRARY);
44
+
47
45
  // Handle symlinks → copies (requires confirmation)
48
46
  if (symlinks.length > 0) {
49
47
  console.log("\nSymlinks to convert to local copies:");
@@ -79,9 +77,29 @@ if (copies.length > 0) {
79
77
  }
80
78
  }
81
79
 
82
- if (symlinks.length === 0 && copies.length === 0) {
80
+ if (symlinks.length === 0 && copies.length === 0 && notInLibrary.length === 0) {
83
81
  console.log("Nothing to update.");
84
82
  }
85
- if (skipped > 0) {
86
- console.log(`${skipped} skipped (not in library)`);
83
+
84
+ // Offer to copy local-only skills into library
85
+ if (notInLibrary.length > 0) {
86
+ console.log("\nFound skills in this repo not in your library:");
87
+ for (const o of notInLibrary) {
88
+ console.log(` ${o.dir.replace(homedir(), "~")}/${o.name}`);
89
+ }
90
+ process.stdout.write(`\nCopy to ${LIBRARY.replace(homedir(), "~")}? [y/N] `);
91
+
92
+ const buf = new Uint8Array(100);
93
+ const n = readSync(0, buf);
94
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
95
+
96
+ if (answer === "y" || answer === "yes") {
97
+ const results = adoptSkills(notInLibrary, LIBRARY);
98
+ for (const r of results) {
99
+ console.log(` copied ${r.name}`);
100
+ }
101
+ console.log(`\n${results.filter(r => r.copied).length} added to library`);
102
+ } else {
103
+ console.log("Skipping.");
104
+ }
87
105
  }