@dealdeploy/skl 1.0.0 → 1.1.1
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/add-tui.test.ts +338 -0
- package/add-tui.ts +269 -0
- package/add.ts +68 -306
- package/index.ts +30 -54
- package/lib.test.ts +446 -85
- package/lib.ts +116 -0
- package/package.json +1 -1
- package/skills-lock.json +10 -0
- package/update.ts +28 -43
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
package/skills-lock.json
ADDED
package/update.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { rmSync, mkdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
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
|
|
9
|
+
const byRepo = groupByRepo(lock);
|
|
7
10
|
|
|
8
|
-
if (
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
totalErrors += skills.length;
|
|
56
50
|
continue;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
|
|
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
|
-
|
|
61
|
+
totalErrors += skills.length;
|
|
69
62
|
continue;
|
|
70
63
|
}
|
|
71
|
-
const treeData = (await treeResp.json()) as { tree:
|
|
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
|
-
|
|
68
|
+
totalErrors += skills.length;
|
|
76
69
|
continue;
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
console.log("");
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
const remoteEntry = tree.find(
|
|
83
|
-
(e) => e.path === skill.skillPath && e.type === "tree"
|
|
84
|
-
);
|
|
74
|
+
const plan = planUpdates(skills, tree);
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
errors++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
76
|
+
totalUpToDate += plan.upToDate.length;
|
|
77
|
+
totalErrors += plan.notFound.length;
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
79
|
+
for (const name of plan.notFound) {
|
|
80
|
+
console.log(` ${name}: not found in remote tree (skipped)`);
|
|
81
|
+
}
|
|
96
82
|
|
|
97
|
-
|
|
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
|
-
|
|
106
|
-
lock.skills[skill.name]!.treeSHA = remoteEntry.sha;
|
|
91
|
+
lock.skills[skill.name]!.treeSHA = skill.newSHA;
|
|
107
92
|
console.log(` updated (${fileCount} files)`);
|
|
108
|
-
|
|
93
|
+
totalUpdated++;
|
|
109
94
|
}
|
|
110
95
|
}
|
|
111
96
|
|
|
112
|
-
if (
|
|
97
|
+
if (totalUpdated > 0) {
|
|
113
98
|
writeLock(lock);
|
|
114
99
|
}
|
|
115
100
|
|
|
116
101
|
const parts: string[] = [];
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
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(", ")}`);
|