@dealdeploy/skl 1.0.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/lib.ts CHANGED
@@ -78,6 +78,122 @@ export function getCatalogSkills(): string[] {
78
78
  }
79
79
  }
80
80
 
81
+ // ── Tree / skill discovery ────────────────────────────────────────────
82
+
83
+ export type TreeEntry = { path: string; type: string; sha: string };
84
+
85
+ export type SkillEntry = {
86
+ name: string;
87
+ prefix: string;
88
+ treeSHA: string;
89
+ };
90
+
91
+ /** Find all skill directories (containing SKILL.md) in a GitHub tree. */
92
+ export function findSkillEntries(tree: TreeEntry[]): SkillEntry[] {
93
+ return tree
94
+ .filter((e) => e.type === "blob" && e.path.endsWith("/SKILL.md"))
95
+ .map((e) => {
96
+ const prefix = e.path.replace(/\/SKILL\.md$/, "");
97
+ const name = prefix.split("/").pop()!;
98
+ const dirEntry = tree.find(
99
+ (t) => t.path === prefix && t.type === "tree"
100
+ );
101
+ return { prefix, name, treeSHA: dirEntry?.sha ?? "" };
102
+ })
103
+ .sort((a, b) => a.name.localeCompare(b.name));
104
+ }
105
+
106
+ /** Parse a repo argument: "owner/repo" or GitHub URL → "owner/repo". Returns null if invalid. */
107
+ export function parseRepoArg(input: string): string | null {
108
+ let repo = input;
109
+ const urlMatch = repo.match(/github\.com\/([^/]+\/[^/]+)/);
110
+ if (urlMatch) repo = urlMatch[1]!.replace(/\.git$/, "");
111
+ if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) return null;
112
+ return repo;
113
+ }
114
+
115
+ /** Build npx skills add args for a skill. */
116
+ export function buildAddArgs(
117
+ catalogPath: string,
118
+ name: string,
119
+ isGlobal: boolean,
120
+ ): string[] {
121
+ const lockEntry = getLockEntry(name);
122
+ if (lockEntry) {
123
+ const args = ["add", lockEntry.sourceUrl, "--skill", name, "-y"];
124
+ if (isGlobal) args.push("-g");
125
+ return args;
126
+ }
127
+ const args = ["add", join(catalogPath, name), "-y"];
128
+ if (isGlobal) args.push("-g");
129
+ return args;
130
+ }
131
+
132
+ /** Parse JSON output from `npx skills list --json`. */
133
+ export function parseSkillsListOutput(jsonStr: string): string[] {
134
+ try {
135
+ const data = JSON.parse(jsonStr);
136
+ if (Array.isArray(data)) {
137
+ return data.map((s: { name: string }) => s.name);
138
+ }
139
+ } catch {}
140
+ return [];
141
+ }
142
+
143
+ // ── Update planning ─────────────────────────────────────────────────
144
+
145
+ export type UpdatePlan = {
146
+ updated: { name: string; skillPath: string; oldSHA: string; newSHA: string }[];
147
+ upToDate: string[];
148
+ notFound: string[];
149
+ };
150
+
151
+ /** Compare lock entries against a remote tree and plan which skills need updating. */
152
+ export function planUpdates(
153
+ skills: { name: string; skillPath: string; treeSHA: string }[],
154
+ remoteTree: TreeEntry[],
155
+ ): UpdatePlan {
156
+ const updated: UpdatePlan["updated"] = [];
157
+ const upToDate: string[] = [];
158
+ const notFound: string[] = [];
159
+
160
+ for (const skill of skills) {
161
+ const remoteEntry = remoteTree.find(
162
+ (e) => e.path === skill.skillPath && e.type === "tree"
163
+ );
164
+ if (!remoteEntry) {
165
+ notFound.push(skill.name);
166
+ continue;
167
+ }
168
+ if (remoteEntry.sha === skill.treeSHA) {
169
+ upToDate.push(skill.name);
170
+ } else {
171
+ updated.push({
172
+ name: skill.name,
173
+ skillPath: skill.skillPath,
174
+ oldSHA: skill.treeSHA,
175
+ newSHA: remoteEntry.sha,
176
+ });
177
+ }
178
+ }
179
+
180
+ return { updated, upToDate, notFound };
181
+ }
182
+
183
+ /** Group lock file skills by source repo. */
184
+ export function groupByRepo(
185
+ lock: LockFile,
186
+ ): Map<string, { name: string; skillPath: string; treeSHA: string }[]> {
187
+ const byRepo = new Map<string, { name: string; skillPath: string; treeSHA: string }[]>();
188
+ for (const [name, entry] of Object.entries(lock.skills)) {
189
+ if (!entry.source || !entry.treeSHA) continue;
190
+ const list = byRepo.get(entry.source) ?? [];
191
+ list.push({ name, skillPath: entry.skillPath, treeSHA: entry.treeSHA });
192
+ byRepo.set(entry.source, list);
193
+ }
194
+ return byRepo;
195
+ }
196
+
81
197
  export async function fetchTreeSHA(
82
198
  ownerRepo: string,
83
199
  skillPath: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "agent-browser": {
5
+ "source": "vercel-labs/agent-browser",
6
+ "sourceType": "github",
7
+ "computedHash": "46eddb8bcb7de8e7c75ff53ee54bf764a576a923d19765b4ef6a71bb04755db6"
8
+ }
9
+ }
10
+ }
package/update.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { rmSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
- import { readLock, writeLock, catalogDir, downloadSkillFiles } from "./lib.ts";
3
+ import {
4
+ readLock, writeLock, catalogDir, downloadSkillFiles,
5
+ groupByRepo, planUpdates, type TreeEntry,
6
+ } from "./lib.ts";
4
7
 
5
8
  const lock = readLock();
6
- const remoteSkills = Object.entries(lock.skills).filter(([, e]) => e.source && e.treeSHA);
9
+ const byRepo = groupByRepo(lock);
7
10
 
8
- if (remoteSkills.length === 0) {
11
+ if (byRepo.size === 0) {
9
12
  console.log("No remote skills to update.");
10
13
  process.exit(0);
11
14
  }
@@ -23,41 +26,31 @@ const headers: Record<string, string> = {
23
26
  };
24
27
  if (token) headers.Authorization = `Bearer ${token}`;
25
28
 
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);
32
- }
33
-
34
- let updated = 0;
35
- let upToDate = 0;
36
- let errors = 0;
29
+ let totalUpdated = 0;
30
+ let totalUpToDate = 0;
31
+ let totalErrors = 0;
37
32
  const CATALOG = catalogDir();
38
33
 
39
34
  for (const [repo, skills] of byRepo) {
40
35
  process.stdout.write(`Checking ${repo}...`);
41
36
 
42
- // Get default branch
43
37
  let branch: string;
44
38
  try {
45
39
  const repoResp = await fetch(`https://api.github.com/repos/${repo}`, { headers });
46
40
  if (!repoResp.ok) {
47
41
  console.log(` failed (${repoResp.status})`);
48
- errors += skills.length;
42
+ totalErrors += skills.length;
49
43
  continue;
50
44
  }
51
45
  const repoData = (await repoResp.json()) as { default_branch: string };
52
46
  branch = repoData.default_branch;
53
47
  } catch (e: any) {
54
48
  console.log(` failed (${e.message})`);
55
- errors += skills.length;
49
+ totalErrors += skills.length;
56
50
  continue;
57
51
  }
58
52
 
59
- // Fetch full tree
60
- let tree: { path: string; type: string; sha: string }[];
53
+ let tree: TreeEntry[];
61
54
  try {
62
55
  const treeResp = await fetch(
63
56
  `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`,
@@ -65,36 +58,29 @@ for (const [repo, skills] of byRepo) {
65
58
  );
66
59
  if (!treeResp.ok) {
67
60
  console.log(` failed (${treeResp.status})`);
68
- errors += skills.length;
61
+ totalErrors += skills.length;
69
62
  continue;
70
63
  }
71
- const treeData = (await treeResp.json()) as { tree: { path: string; type: string; sha: string }[] };
64
+ const treeData = (await treeResp.json()) as { tree: TreeEntry[] };
72
65
  tree = treeData.tree;
73
66
  } catch (e: any) {
74
67
  console.log(` failed (${e.message})`);
75
- errors += skills.length;
68
+ totalErrors += skills.length;
76
69
  continue;
77
70
  }
78
71
 
79
72
  console.log("");
80
73
 
81
- for (const skill of skills) {
82
- const remoteEntry = tree.find(
83
- (e) => e.path === skill.skillPath && e.type === "tree"
84
- );
74
+ const plan = planUpdates(skills, tree);
85
75
 
86
- if (!remoteEntry) {
87
- console.log(` ${skill.name}: not found in remote tree (skipped)`);
88
- errors++;
89
- continue;
90
- }
76
+ totalUpToDate += plan.upToDate.length;
77
+ totalErrors += plan.notFound.length;
91
78
 
92
- if (remoteEntry.sha === skill.treeSHA) {
93
- upToDate++;
94
- continue;
95
- }
79
+ for (const name of plan.notFound) {
80
+ console.log(` ${name}: not found in remote tree (skipped)`);
81
+ }
96
82
 
97
- // SHA differs re-download
83
+ for (const skill of plan.updated) {
98
84
  const skillDir = join(CATALOG, skill.name);
99
85
  rmSync(skillDir, { recursive: true, force: true });
100
86
  mkdirSync(skillDir, { recursive: true });
@@ -102,19 +88,18 @@ for (const [repo, skills] of byRepo) {
102
88
  process.stdout.write(` ${skill.name}...`);
103
89
  const fileCount = await downloadSkillFiles(repo, branch, skill.skillPath, skillDir, tree);
104
90
 
105
- // Update lock entry
106
- lock.skills[skill.name]!.treeSHA = remoteEntry.sha;
91
+ lock.skills[skill.name]!.treeSHA = skill.newSHA;
107
92
  console.log(` updated (${fileCount} files)`);
108
- updated++;
93
+ totalUpdated++;
109
94
  }
110
95
  }
111
96
 
112
- if (updated > 0) {
97
+ if (totalUpdated > 0) {
113
98
  writeLock(lock);
114
99
  }
115
100
 
116
101
  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)`);
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)`);
120
105
  console.log(`\n${parts.join(", ")}`);