@dealdeploy/skl 0.4.0 → 1.1.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/update.ts CHANGED
@@ -1,105 +1,105 @@
1
- #!/usr/bin/env bun
2
-
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";
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.");
1
+ import { rmSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import {
4
+ readLock, writeLock, catalogDir, downloadSkillFiles,
5
+ groupByRepo, planUpdates, type TreeEntry,
6
+ } from "./lib.ts";
7
+
8
+ const lock = readLock();
9
+ const byRepo = groupByRepo(lock);
10
+
11
+ if (byRepo.size === 0) {
12
+ console.log("No remote skills to update.");
24
13
  process.exit(0);
25
14
  }
26
15
 
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);
16
+ // Get GitHub token
17
+ let token = "";
18
+ try {
19
+ const proc = Bun.spawn(["gh", "auth", "token"], { stdout: "pipe", stderr: "pipe" });
20
+ token = (await new Response(proc.stdout).text()).trim();
21
+ await proc.exited;
22
+ } catch {}
23
+
24
+ const headers: Record<string, string> = {
25
+ Accept: "application/vnd.github+json",
26
+ };
27
+ if (token) headers.Authorization = `Bearer ${token}`;
28
+
29
+ let totalUpdated = 0;
30
+ let totalUpToDate = 0;
31
+ let totalErrors = 0;
32
+ const CATALOG = catalogDir();
33
+
34
+ for (const [repo, skills] of byRepo) {
35
+ process.stdout.write(`Checking ${repo}...`);
36
+
37
+ let branch: string;
38
+ try {
39
+ const repoResp = await fetch(`https://api.github.com/repos/${repo}`, { headers });
40
+ if (!repoResp.ok) {
41
+ console.log(` failed (${repoResp.status})`);
42
+ totalErrors += skills.length;
43
+ continue;
44
+ }
45
+ const repoData = (await repoResp.json()) as { default_branch: string };
46
+ branch = repoData.default_branch;
47
+ } catch (e: any) {
48
+ console.log(` failed (${e.message})`);
49
+ totalErrors += skills.length;
50
+ continue;
40
51
  }
41
- }
42
52
 
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}`);
53
+ let tree: TreeEntry[];
54
+ try {
55
+ const treeResp = await fetch(
56
+ `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`,
57
+ { headers },
58
+ );
59
+ if (!treeResp.ok) {
60
+ console.log(` failed (${treeResp.status})`);
61
+ totalErrors += skills.length;
62
+ continue;
63
63
  }
64
- } else {
65
- console.log("Skipping symlink conversion.");
64
+ const treeData = (await treeResp.json()) as { tree: TreeEntry[] };
65
+ tree = treeData.tree;
66
+ } catch (e: any) {
67
+ console.log(` failed (${e.message})`);
68
+ totalErrors += skills.length;
69
+ continue;
66
70
  }
67
- }
68
71
 
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}`);
77
- }
78
- }
72
+ console.log("");
79
73
 
80
- if (symlinks.length === 0 && copies.length === 0 && notInLibrary.length === 0) {
81
- console.log("Nothing to update.");
82
- }
74
+ const plan = planUpdates(skills, tree);
75
+
76
+ totalUpToDate += plan.upToDate.length;
77
+ totalErrors += plan.notFound.length;
83
78
 
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}`);
79
+ for (const name of plan.notFound) {
80
+ console.log(` ${name}: not found in remote tree (skipped)`);
89
81
  }
90
- process.stdout.write(`\nCopy to ${LIBRARY.replace(homedir(), "~")}? [y/N] `);
91
82
 
92
- const buf = new Uint8Array(100);
93
- const n = readSync(0, buf);
94
- const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
83
+ for (const skill of plan.updated) {
84
+ const skillDir = join(CATALOG, skill.name);
85
+ rmSync(skillDir, { recursive: true, force: true });
86
+ mkdirSync(skillDir, { recursive: true });
95
87
 
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.");
88
+ process.stdout.write(` ${skill.name}...`);
89
+ const fileCount = await downloadSkillFiles(repo, branch, skill.skillPath, skillDir, tree);
90
+
91
+ lock.skills[skill.name]!.treeSHA = skill.newSHA;
92
+ console.log(` updated (${fileCount} files)`);
93
+ totalUpdated++;
104
94
  }
105
95
  }
96
+
97
+ if (totalUpdated > 0) {
98
+ writeLock(lock);
99
+ }
100
+
101
+ const parts: string[] = [];
102
+ if (totalUpdated > 0) parts.push(`Updated ${totalUpdated} skill(s)`);
103
+ if (totalUpToDate > 0) parts.push(`${totalUpToDate} already up to date`);
104
+ if (totalErrors > 0) parts.push(`${totalErrors} error(s)`);
105
+ console.log(`\n${parts.join(", ")}`);