@dealdeploy/skl 0.4.0 → 1.0.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.
Files changed (8) hide show
  1. package/add.ts +55 -64
  2. package/index.ts +110 -972
  3. package/lib.test.ts +110 -41
  4. package/lib.ts +125 -38
  5. package/package.json +1 -1
  6. package/tui.test.ts +565 -0
  7. package/tui.ts +612 -0
  8. package/update.ts +102 -87
package/update.ts CHANGED
@@ -1,105 +1,120 @@
1
- #!/usr/bin/env bun
1
+ import { rmSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { readLock, writeLock, catalogDir, downloadSkillFiles } from "./lib.ts";
2
4
 
3
- import { readdirSync, existsSync, cpSync, rmSync, lstatSync, readlinkSync, readSync, unlinkSync } from "fs";
4
- import { join, resolve } from "path";
5
- import { homedir } from "os";
6
- import { findOrphanSkills, adoptSkills } from "./lib.ts";
5
+ const lock = readLock();
6
+ const remoteSkills = Object.entries(lock.skills).filter(([, e]) => e.source && e.treeSHA);
7
7
 
8
- const LIBRARY = join(homedir(), "dotfiles/skills");
9
-
10
- if (!existsSync(LIBRARY)) {
11
- console.error(`skl: library not found at ${LIBRARY}`);
12
- process.exit(1);
13
- }
14
-
15
- const localDirs: [string, string][] = [];
16
- const agentsDir = join(process.cwd(), ".agents/skills");
17
- if (existsSync(agentsDir)) localDirs.push([agentsDir, ".agents/skills"]);
18
-
19
- const claudeDir = join(process.cwd(), ".claude/skills");
20
- if (existsSync(claudeDir)) localDirs.push([claudeDir, ".claude/skills"]);
21
-
22
- if (localDirs.length === 0) {
23
- console.log("No local skill directories found.");
8
+ if (remoteSkills.length === 0) {
9
+ console.log("No remote skills to update.");
24
10
  process.exit(0);
25
11
  }
26
12
 
27
- type Entry = { dir: string; label: string; name: string; kind: "symlink" | "copy" };
28
-
29
- const symlinks: Entry[] = [];
30
- const copies: Entry[] = [];
31
- for (const [dir, label] of localDirs) {
32
- for (const name of readdirSync(dir)) {
33
- const localPath = join(dir, name);
34
- const libPath = join(LIBRARY, name);
35
- if (!existsSync(libPath)) continue;
36
- const isSym = lstatSync(localPath).isSymbolicLink();
37
- const entry: Entry = { dir, label, name, kind: isSym ? "symlink" : "copy" };
38
- if (isSym) symlinks.push(entry);
39
- else copies.push(entry);
40
- }
13
+ // Get GitHub token
14
+ let token = "";
15
+ try {
16
+ const proc = Bun.spawn(["gh", "auth", "token"], { stdout: "pipe", stderr: "pipe" });
17
+ token = (await new Response(proc.stdout).text()).trim();
18
+ await proc.exited;
19
+ } catch {}
20
+
21
+ const headers: Record<string, string> = {
22
+ Accept: "application/vnd.github+json",
23
+ };
24
+ if (token) headers.Authorization = `Bearer ${token}`;
25
+
26
+ // Group skills by source repo
27
+ const byRepo = new Map<string, { name: string; skillPath: string; treeSHA: string }[]>();
28
+ for (const [name, entry] of remoteSkills) {
29
+ const list = byRepo.get(entry.source) ?? [];
30
+ list.push({ name, skillPath: entry.skillPath, treeSHA: entry.treeSHA });
31
+ byRepo.set(entry.source, list);
41
32
  }
42
33
 
43
- const notInLibrary = findOrphanSkills(localDirs.map(([dir]) => dir), LIBRARY);
44
-
45
- // Handle symlinks → copies (requires confirmation)
46
- if (symlinks.length > 0) {
47
- console.log("\nSymlinks to convert to local copies:");
48
- for (const s of symlinks) {
49
- console.log(` ${s.label}/${s.name} ${readlinkSync(join(s.dir, s.name))}`);
50
- }
51
- process.stdout.write(`\nReplace ${symlinks.length} symlink(s) with copies from library? [y/N] `);
52
-
53
- const buf = new Uint8Array(100);
54
- const n = readSync(0, buf);
55
- const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
56
-
57
- if (answer === "y" || answer === "yes") {
58
- for (const s of symlinks) {
59
- const localPath = join(s.dir, s.name);
60
- unlinkSync(localPath);
61
- cpSync(join(LIBRARY, s.name), localPath, { recursive: true });
62
- console.log(` converted ${s.label}/${s.name}`);
34
+ let updated = 0;
35
+ let upToDate = 0;
36
+ let errors = 0;
37
+ const CATALOG = catalogDir();
38
+
39
+ for (const [repo, skills] of byRepo) {
40
+ process.stdout.write(`Checking ${repo}...`);
41
+
42
+ // Get default branch
43
+ let branch: string;
44
+ try {
45
+ const repoResp = await fetch(`https://api.github.com/repos/${repo}`, { headers });
46
+ if (!repoResp.ok) {
47
+ console.log(` failed (${repoResp.status})`);
48
+ errors += skills.length;
49
+ continue;
63
50
  }
64
- } else {
65
- console.log("Skipping symlink conversion.");
51
+ const repoData = (await repoResp.json()) as { default_branch: string };
52
+ branch = repoData.default_branch;
53
+ } catch (e: any) {
54
+ console.log(` failed (${e.message})`);
55
+ errors += skills.length;
56
+ continue;
66
57
  }
67
- }
68
58
 
69
- // Update existing copies
70
- if (copies.length > 0) {
71
- console.log(`\nUpdating ${copies.length} local copies from library...`);
72
- for (const c of copies) {
73
- const localPath = join(c.dir, c.name);
74
- rmSync(localPath, { recursive: true, force: true });
75
- cpSync(join(LIBRARY, c.name), localPath, { recursive: true });
76
- console.log(` updated ${c.label}/${c.name}`);
59
+ // Fetch full tree
60
+ let tree: { path: string; type: string; sha: string }[];
61
+ try {
62
+ const treeResp = await fetch(
63
+ `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`,
64
+ { headers },
65
+ );
66
+ if (!treeResp.ok) {
67
+ console.log(` failed (${treeResp.status})`);
68
+ errors += skills.length;
69
+ continue;
70
+ }
71
+ const treeData = (await treeResp.json()) as { tree: { path: string; type: string; sha: string }[] };
72
+ tree = treeData.tree;
73
+ } catch (e: any) {
74
+ console.log(` failed (${e.message})`);
75
+ errors += skills.length;
76
+ continue;
77
77
  }
78
- }
79
78
 
80
- if (symlinks.length === 0 && copies.length === 0 && notInLibrary.length === 0) {
81
- console.log("Nothing to update.");
82
- }
79
+ console.log("");
83
80
 
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] `);
81
+ for (const skill of skills) {
82
+ const remoteEntry = tree.find(
83
+ (e) => e.path === skill.skillPath && e.type === "tree"
84
+ );
91
85
 
92
- const buf = new Uint8Array(100);
93
- const n = readSync(0, buf);
94
- const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
86
+ if (!remoteEntry) {
87
+ console.log(` ${skill.name}: not found in remote tree (skipped)`);
88
+ errors++;
89
+ continue;
90
+ }
95
91
 
96
- if (answer === "y" || answer === "yes") {
97
- const results = adoptSkills(notInLibrary, LIBRARY);
98
- for (const r of results) {
99
- console.log(` copied ${r.name}`);
92
+ if (remoteEntry.sha === skill.treeSHA) {
93
+ upToDate++;
94
+ continue;
100
95
  }
101
- console.log(`\n${results.filter(r => r.copied).length} added to library`);
102
- } else {
103
- console.log("Skipping.");
96
+
97
+ // SHA differs — re-download
98
+ const skillDir = join(CATALOG, skill.name);
99
+ rmSync(skillDir, { recursive: true, force: true });
100
+ mkdirSync(skillDir, { recursive: true });
101
+
102
+ process.stdout.write(` ${skill.name}...`);
103
+ const fileCount = await downloadSkillFiles(repo, branch, skill.skillPath, skillDir, tree);
104
+
105
+ // Update lock entry
106
+ lock.skills[skill.name]!.treeSHA = remoteEntry.sha;
107
+ console.log(` updated (${fileCount} files)`);
108
+ updated++;
104
109
  }
105
110
  }
111
+
112
+ if (updated > 0) {
113
+ writeLock(lock);
114
+ }
115
+
116
+ const parts: string[] = [];
117
+ if (updated > 0) parts.push(`Updated ${updated} skill(s)`);
118
+ if (upToDate > 0) parts.push(`${upToDate} already up to date`);
119
+ if (errors > 0) parts.push(`${errors} error(s)`);
120
+ console.log(`\n${parts.join(", ")}`);