@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/add-tui.test.ts +338 -0
- package/add-tui.ts +269 -0
- package/add.ts +81 -328
- package/index.ts +112 -976
- package/lib.test.ts +470 -40
- package/lib.ts +240 -37
- package/package.json +1 -1
- package/skills-lock.json +10 -0
- package/tui.test.ts +565 -0
- package/tui.ts +612 -0
- package/update.ts +90 -90
package/lib.ts
CHANGED
|
@@ -1,50 +1,253 @@
|
|
|
1
|
-
import { readdirSync,
|
|
1
|
+
import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
function home(): string {
|
|
6
|
+
return process.env.HOME || homedir();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function catalogDir(): string {
|
|
10
|
+
return join(home(), ".skl/catalog");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function lockPath(): string {
|
|
14
|
+
return join(home(), ".skl/catalog.lock.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LockEntry = {
|
|
18
|
+
source: string;
|
|
19
|
+
sourceUrl: string;
|
|
20
|
+
skillPath: string;
|
|
21
|
+
treeSHA: string;
|
|
22
|
+
addedAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type LockFile = {
|
|
26
|
+
version: 1;
|
|
27
|
+
skills: Record<string, LockEntry>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function readLock(): LockFile {
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(readFileSync(lockPath(), "utf-8"));
|
|
33
|
+
if (data.version === 1 && data.skills) return data;
|
|
34
|
+
} catch {}
|
|
35
|
+
return { version: 1, skills: {} };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function writeLock(lock: LockFile): void {
|
|
39
|
+
mkdirSync(join(home(), ".skl"), { recursive: true });
|
|
40
|
+
writeFileSync(lockPath(), JSON.stringify(lock, null, 2) + "\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function addToLock(name: string, entry: LockEntry): void {
|
|
44
|
+
const lock = readLock();
|
|
45
|
+
lock.skills[name] = entry;
|
|
46
|
+
writeLock(lock);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function removeFromLock(name: string): void {
|
|
50
|
+
const lock = readLock();
|
|
51
|
+
delete lock.skills[name];
|
|
52
|
+
writeLock(lock);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getLockEntry(name: string): LockEntry | null {
|
|
56
|
+
const lock = readLock();
|
|
57
|
+
return lock.skills[name] ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getCatalogSkills(): string[] {
|
|
61
|
+
const dir = catalogDir();
|
|
62
|
+
try {
|
|
63
|
+
return readdirSync(dir)
|
|
64
|
+
.filter((name) => {
|
|
15
65
|
const full = join(dir, name);
|
|
16
66
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
67
|
+
return (
|
|
68
|
+
statSync(full).isDirectory() &&
|
|
69
|
+
existsSync(join(full, "SKILL.md"))
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.sort();
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
27
78
|
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Tree / skill discovery ────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export type TreeEntry = { path: string; type: string; sha: string };
|
|
28
84
|
|
|
29
|
-
|
|
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));
|
|
30
104
|
}
|
|
31
105
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
114
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
});
|
|
46
177
|
}
|
|
47
178
|
}
|
|
48
179
|
|
|
49
|
-
return
|
|
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
|
+
|
|
197
|
+
export async function fetchTreeSHA(
|
|
198
|
+
ownerRepo: string,
|
|
199
|
+
skillPath: string,
|
|
200
|
+
token?: string,
|
|
201
|
+
): Promise<string | null> {
|
|
202
|
+
const headers: Record<string, string> = {
|
|
203
|
+
Accept: "application/vnd.github+json",
|
|
204
|
+
};
|
|
205
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
206
|
+
|
|
207
|
+
const repoResp = await fetch(`https://api.github.com/repos/${ownerRepo}`, { headers });
|
|
208
|
+
if (!repoResp.ok) return null;
|
|
209
|
+
const repoData = (await repoResp.json()) as { default_branch: string };
|
|
210
|
+
const branch = repoData.default_branch;
|
|
211
|
+
|
|
212
|
+
const treeResp = await fetch(
|
|
213
|
+
`https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`,
|
|
214
|
+
{ headers },
|
|
215
|
+
);
|
|
216
|
+
if (!treeResp.ok) return null;
|
|
217
|
+
const treeData = (await treeResp.json()) as { tree?: { path: string; type: string; sha: string }[] };
|
|
218
|
+
|
|
219
|
+
const entry = treeData.tree?.find(
|
|
220
|
+
(e: { path: string; type: string; sha: string }) =>
|
|
221
|
+
e.path === skillPath && e.type === "tree",
|
|
222
|
+
);
|
|
223
|
+
return entry?.sha ?? null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function downloadSkillFiles(
|
|
227
|
+
repo: string,
|
|
228
|
+
branch: string,
|
|
229
|
+
prefix: string,
|
|
230
|
+
destDir: string,
|
|
231
|
+
tree: { path: string; type: string }[],
|
|
232
|
+
): Promise<number> {
|
|
233
|
+
const prefixSlash = prefix + "/";
|
|
234
|
+
const files = tree
|
|
235
|
+
.filter((e) => e.type === "blob" && e.path.startsWith(prefixSlash))
|
|
236
|
+
.map((e) => e.path);
|
|
237
|
+
|
|
238
|
+
const baseUrl = `https://raw.githubusercontent.com/${repo}/${branch}`;
|
|
239
|
+
|
|
240
|
+
for (const filePath of files) {
|
|
241
|
+
const url = `${baseUrl}/${filePath}`;
|
|
242
|
+
const resp = await fetch(url);
|
|
243
|
+
if (!resp.ok) continue;
|
|
244
|
+
const content = await resp.arrayBuffer();
|
|
245
|
+
const relativePath = filePath.substring(prefixSlash.length);
|
|
246
|
+
const dest = join(destDir, relativePath);
|
|
247
|
+
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
248
|
+
mkdirSync(dir, { recursive: true });
|
|
249
|
+
writeFileSync(dest, Buffer.from(content));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return files.length;
|
|
50
253
|
}
|
package/package.json
CHANGED