@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/lib.test.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { test, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "fs";
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, cpSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { mkdtempSync } from "fs";
5
5
  import { tmpdir } from "os";
@@ -12,8 +12,16 @@ import {
12
12
  getCatalogSkills,
13
13
  getLockEntry,
14
14
  catalogDir,
15
+ lockPath,
16
+ findSkillEntries,
17
+ parseRepoArg,
18
+ buildAddArgs,
19
+ parseSkillsListOutput,
20
+ planUpdates,
21
+ groupByRepo,
15
22
  type LockFile,
16
23
  type LockEntry,
24
+ type TreeEntry,
17
25
  } from "./lib.ts";
18
26
 
19
27
  let tmp: string;
@@ -30,110 +38,463 @@ afterEach(() => {
30
38
  rmSync(tmp, { recursive: true, force: true });
31
39
  });
32
40
 
33
- test("readLock returns empty structure when file doesn't exist", () => {
34
- const lock = readLock();
35
- expect(lock).toEqual({ version: 1, skills: {} });
36
- });
41
+ // ── Lock file ───────────────────────────────────────────────────────
42
+
43
+ describe("lock file", () => {
44
+ test("readLock returns empty structure when file doesn't exist", () => {
45
+ const lock = readLock();
46
+ expect(lock).toEqual({ version: 1, skills: {} });
47
+ });
37
48
 
38
- test("writeLock and readLock round-trip", () => {
39
- const lock: LockFile = {
40
- version: 1,
41
- skills: {
42
- "test-skill": {
43
- source: "owner/repo",
44
- sourceUrl: "https://github.com/owner/repo",
45
- skillPath: "skills/test-skill",
46
- treeSHA: "abc123",
47
- addedAt: "2026-03-12T00:00:00.000Z",
49
+ test("writeLock and readLock round-trip", () => {
50
+ const lock: LockFile = {
51
+ version: 1,
52
+ skills: {
53
+ "test-skill": {
54
+ source: "owner/repo",
55
+ sourceUrl: "https://github.com/owner/repo",
56
+ skillPath: "skills/test-skill",
57
+ treeSHA: "abc123",
58
+ addedAt: "2026-03-12T00:00:00.000Z",
59
+ },
48
60
  },
49
- },
50
- };
51
- writeLock(lock);
52
- const read = readLock();
53
- expect(read).toEqual(lock);
54
- });
61
+ };
62
+ writeLock(lock);
63
+ const read = readLock();
64
+ expect(read).toEqual(lock);
65
+ });
55
66
 
56
- test("addToLock adds a new entry", () => {
57
- writeLock({ version: 1, skills: {} });
67
+ test("writeLock creates .skl directory if missing", () => {
68
+ expect(existsSync(join(tmp, ".skl"))).toBe(false);
69
+ writeLock({ version: 1, skills: {} });
70
+ expect(existsSync(join(tmp, ".skl"))).toBe(true);
71
+ });
58
72
 
59
- const entry: LockEntry = {
60
- source: "owner/repo",
61
- sourceUrl: "https://github.com/owner/repo",
62
- skillPath: "skills/my-skill",
63
- treeSHA: "def456",
64
- addedAt: "2026-03-12T00:00:00.000Z",
65
- };
66
- addToLock("my-skill", entry);
73
+ test("readLock returns empty on corrupted JSON", () => {
74
+ mkdirSync(join(tmp, ".skl"), { recursive: true });
75
+ writeFileSync(lockPath(), "not json");
76
+ expect(readLock()).toEqual({ version: 1, skills: {} });
77
+ });
67
78
 
68
- const lock = readLock();
69
- expect(lock.skills["my-skill"]).toEqual(entry);
70
- });
79
+ test("readLock returns empty on wrong version", () => {
80
+ mkdirSync(join(tmp, ".skl"), { recursive: true });
81
+ writeFileSync(lockPath(), JSON.stringify({ version: 99, skills: {} }));
82
+ expect(readLock()).toEqual({ version: 1, skills: {} });
83
+ });
84
+
85
+ test("addToLock adds a new entry", () => {
86
+ writeLock({ version: 1, skills: {} });
87
+ const entry: LockEntry = {
88
+ source: "owner/repo",
89
+ sourceUrl: "https://github.com/owner/repo",
90
+ skillPath: "skills/my-skill",
91
+ treeSHA: "def456",
92
+ addedAt: "2026-03-12T00:00:00.000Z",
93
+ };
94
+ addToLock("my-skill", entry);
95
+ const lock = readLock();
96
+ expect(lock.skills["my-skill"]).toEqual(entry);
97
+ });
98
+
99
+ test("addToLock overwrites existing entry", () => {
100
+ const entry1: LockEntry = {
101
+ source: "a/b", sourceUrl: "https://github.com/a/b",
102
+ skillPath: "skills/x", treeSHA: "old", addedAt: "2026-01-01",
103
+ };
104
+ const entry2: LockEntry = {
105
+ source: "a/b", sourceUrl: "https://github.com/a/b",
106
+ skillPath: "skills/x", treeSHA: "new", addedAt: "2026-01-02",
107
+ };
108
+ addToLock("x", entry1);
109
+ addToLock("x", entry2);
110
+ expect(getLockEntry("x")!.treeSHA).toBe("new");
111
+ });
71
112
 
72
- test("removeFromLock removes an entry", () => {
73
- writeLock({
74
- version: 1,
75
- skills: {
76
- "keep-me": {
77
- source: "a/b",
78
- sourceUrl: "https://github.com/a/b",
79
- skillPath: "skills/keep-me",
80
- treeSHA: "111",
81
- addedAt: "2026-03-12T00:00:00.000Z",
113
+ test("removeFromLock removes an entry", () => {
114
+ writeLock({
115
+ version: 1,
116
+ skills: {
117
+ "keep-me": {
118
+ source: "a/b", sourceUrl: "https://github.com/a/b",
119
+ skillPath: "skills/keep-me", treeSHA: "111", addedAt: "2026-03-12",
120
+ },
121
+ "remove-me": {
122
+ source: "a/b", sourceUrl: "https://github.com/a/b",
123
+ skillPath: "skills/remove-me", treeSHA: "222", addedAt: "2026-03-12",
124
+ },
82
125
  },
83
- "remove-me": {
84
- source: "a/b",
85
- sourceUrl: "https://github.com/a/b",
86
- skillPath: "skills/remove-me",
87
- treeSHA: "222",
88
- addedAt: "2026-03-12T00:00:00.000Z",
126
+ });
127
+ removeFromLock("remove-me");
128
+ const lock = readLock();
129
+ expect(lock.skills["keep-me"]).toBeDefined();
130
+ expect(lock.skills["remove-me"]).toBeUndefined();
131
+ });
132
+
133
+ test("removeFromLock is no-op for missing entry", () => {
134
+ writeLock({ version: 1, skills: {} });
135
+ removeFromLock("nonexistent");
136
+ expect(readLock()).toEqual({ version: 1, skills: {} });
137
+ });
138
+
139
+ test("getLockEntry returns entry or null", () => {
140
+ writeLock({
141
+ version: 1,
142
+ skills: {
143
+ "exists": {
144
+ source: "a/b", sourceUrl: "https://github.com/a/b",
145
+ skillPath: "skills/exists", treeSHA: "333", addedAt: "2026-03-12",
146
+ },
89
147
  },
90
- },
148
+ });
149
+ expect(getLockEntry("exists")).not.toBeNull();
150
+ expect(getLockEntry("exists")!.treeSHA).toBe("333");
151
+ expect(getLockEntry("nope")).toBeNull();
152
+ });
153
+ });
154
+
155
+ // ── Catalog ─────────────────────────────────────────────────────────
156
+
157
+ describe("catalog", () => {
158
+ test("getCatalogSkills finds directories with SKILL.md", () => {
159
+ const catalog = catalogDir();
160
+ mkdirSync(catalog, { recursive: true });
161
+
162
+ mkdirSync(join(catalog, "good-skill"));
163
+ writeFileSync(join(catalog, "good-skill", "SKILL.md"), "# Good");
164
+
165
+ mkdirSync(join(catalog, "no-manifest"));
166
+ writeFileSync(join(catalog, "just-a-file"), "nope");
167
+
168
+ mkdirSync(join(catalog, "another-skill"));
169
+ writeFileSync(join(catalog, "another-skill", "SKILL.md"), "# Another");
170
+
171
+ const skills = getCatalogSkills();
172
+ expect(skills).toEqual(["another-skill", "good-skill"]);
91
173
  });
92
174
 
93
- removeFromLock("remove-me");
94
- const lock = readLock();
95
- expect(lock.skills["keep-me"]).toBeDefined();
96
- expect(lock.skills["remove-me"]).toBeUndefined();
175
+ test("getCatalogSkills returns empty array when catalog doesn't exist", () => {
176
+ expect(getCatalogSkills()).toEqual([]);
177
+ });
178
+
179
+ test("getCatalogSkills returns sorted list", () => {
180
+ const catalog = catalogDir();
181
+ mkdirSync(catalog, { recursive: true });
182
+ for (const name of ["zebra", "apple", "mango"]) {
183
+ mkdirSync(join(catalog, name));
184
+ writeFileSync(join(catalog, name, "SKILL.md"), "# Skill");
185
+ }
186
+ expect(getCatalogSkills()).toEqual(["apple", "mango", "zebra"]);
187
+ });
97
188
  });
98
189
 
99
- test("getLockEntry returns entry or null", () => {
100
- writeLock({
101
- version: 1,
102
- skills: {
103
- "exists": {
104
- source: "a/b",
105
- sourceUrl: "https://github.com/a/b",
106
- skillPath: "skills/exists",
107
- treeSHA: "333",
108
- addedAt: "2026-03-12T00:00:00.000Z",
109
- },
110
- },
190
+ // ── findSkillEntries ────────────────────────────────────────────────
191
+
192
+ describe("findSkillEntries", () => {
193
+ test("finds flat skills (name/SKILL.md)", () => {
194
+ const tree: TreeEntry[] = [
195
+ { path: "commit", type: "tree", sha: "aaa" },
196
+ { path: "commit/SKILL.md", type: "blob", sha: "bbb" },
197
+ { path: "commit/prompt.md", type: "blob", sha: "ccc" },
198
+ { path: "opentui", type: "tree", sha: "ddd" },
199
+ { path: "opentui/SKILL.md", type: "blob", sha: "eee" },
200
+ ];
201
+ const entries = findSkillEntries(tree);
202
+ expect(entries).toEqual([
203
+ { prefix: "commit", name: "commit", treeSHA: "aaa" },
204
+ { prefix: "opentui", name: "opentui", treeSHA: "ddd" },
205
+ ]);
206
+ });
207
+
208
+ test("finds nested skills (dir/name/SKILL.md)", () => {
209
+ const tree: TreeEntry[] = [
210
+ { path: "skills", type: "tree", sha: "000" },
211
+ { path: "skills/commit", type: "tree", sha: "aaa" },
212
+ { path: "skills/commit/SKILL.md", type: "blob", sha: "bbb" },
213
+ { path: "skills/opentui", type: "tree", sha: "ddd" },
214
+ { path: "skills/opentui/SKILL.md", type: "blob", sha: "eee" },
215
+ ];
216
+ const entries = findSkillEntries(tree);
217
+ expect(entries).toEqual([
218
+ { prefix: "skills/commit", name: "commit", treeSHA: "aaa" },
219
+ { prefix: "skills/opentui", name: "opentui", treeSHA: "ddd" },
220
+ ]);
221
+ });
222
+
223
+ test("returns empty for tree with no SKILL.md", () => {
224
+ const tree: TreeEntry[] = [
225
+ { path: "README.md", type: "blob", sha: "aaa" },
226
+ { path: "src", type: "tree", sha: "bbb" },
227
+ ];
228
+ expect(findSkillEntries(tree)).toEqual([]);
229
+ });
230
+
231
+ test("sorts by name", () => {
232
+ const tree: TreeEntry[] = [
233
+ { path: "zeta", type: "tree", sha: "z" },
234
+ { path: "zeta/SKILL.md", type: "blob", sha: "z1" },
235
+ { path: "alpha", type: "tree", sha: "a" },
236
+ { path: "alpha/SKILL.md", type: "blob", sha: "a1" },
237
+ ];
238
+ const entries = findSkillEntries(tree);
239
+ expect(entries[0]!.name).toBe("alpha");
240
+ expect(entries[1]!.name).toBe("zeta");
241
+ });
242
+
243
+ test("handles missing tree entry for directory (empty SHA)", () => {
244
+ const tree: TreeEntry[] = [
245
+ { path: "orphan/SKILL.md", type: "blob", sha: "ooo" },
246
+ // No tree entry for "orphan"
247
+ ];
248
+ const entries = findSkillEntries(tree);
249
+ expect(entries).toEqual([
250
+ { prefix: "orphan", name: "orphan", treeSHA: "" },
251
+ ]);
252
+ });
253
+ });
254
+
255
+ // ── parseRepoArg ────────────────────────────────────────────────────
256
+
257
+ describe("parseRepoArg", () => {
258
+ test("accepts owner/repo format", () => {
259
+ expect(parseRepoArg("vercel-labs/agent-skills")).toBe("vercel-labs/agent-skills");
260
+ });
261
+
262
+ test("extracts from GitHub URL", () => {
263
+ expect(parseRepoArg("https://github.com/vercel-labs/agent-skills")).toBe("vercel-labs/agent-skills");
264
+ });
265
+
266
+ test("strips .git suffix from URL", () => {
267
+ expect(parseRepoArg("https://github.com/owner/repo.git")).toBe("owner/repo");
268
+ });
269
+
270
+ test("extracts from URL with trailing path", () => {
271
+ expect(parseRepoArg("https://github.com/owner/repo/tree/main")).toBe("owner/repo");
272
+ });
273
+
274
+ test("returns null for empty string", () => {
275
+ expect(parseRepoArg("")).toBeNull();
276
+ });
277
+
278
+ test("returns null for invalid format", () => {
279
+ expect(parseRepoArg("just-a-name")).toBeNull();
111
280
  });
112
281
 
113
- expect(getLockEntry("exists")).not.toBeNull();
114
- expect(getLockEntry("exists")!.treeSHA).toBe("333");
115
- expect(getLockEntry("nope")).toBeNull();
282
+ test("returns null for triple-segment path", () => {
283
+ expect(parseRepoArg("a/b/c")).toBeNull();
284
+ });
116
285
  });
117
286
 
118
- test("getCatalogSkills finds directories with SKILL.md", () => {
119
- const catalog = catalogDir();
120
- mkdirSync(catalog, { recursive: true });
287
+ // ── buildAddArgs ────────────────────────────────────────────────────
121
288
 
122
- mkdirSync(join(catalog, "good-skill"));
123
- writeFileSync(join(catalog, "good-skill", "SKILL.md"), "# Good");
289
+ describe("buildAddArgs", () => {
290
+ test("uses lock entry sourceUrl for remote skill", () => {
291
+ const entry: LockEntry = {
292
+ source: "owner/repo",
293
+ sourceUrl: "https://github.com/owner/repo",
294
+ skillPath: "skills/myskill",
295
+ treeSHA: "abc",
296
+ addedAt: "2026-01-01",
297
+ };
298
+ addToLock("myskill", entry);
124
299
 
125
- mkdirSync(join(catalog, "no-manifest"));
300
+ const args = buildAddArgs("/catalog", "myskill", false);
301
+ expect(args).toEqual(["add", "https://github.com/owner/repo", "--skill", "myskill", "-y"]);
302
+ });
126
303
 
127
- writeFileSync(join(catalog, "just-a-file"), "nope");
304
+ test("adds -g flag for global remote skill", () => {
305
+ const entry: LockEntry = {
306
+ source: "owner/repo",
307
+ sourceUrl: "https://github.com/owner/repo",
308
+ skillPath: "skills/myskill",
309
+ treeSHA: "abc",
310
+ addedAt: "2026-01-01",
311
+ };
312
+ addToLock("myskill", entry);
128
313
 
129
- mkdirSync(join(catalog, "another-skill"));
130
- writeFileSync(join(catalog, "another-skill", "SKILL.md"), "# Another");
314
+ const args = buildAddArgs("/catalog", "myskill", true);
315
+ expect(args).toContain("-g");
316
+ });
131
317
 
132
- const skills = getCatalogSkills();
133
- expect(skills).toEqual(["another-skill", "good-skill"]);
318
+ test("uses catalog path for hand-authored skill", () => {
319
+ // No lock entry for this skill
320
+ const args = buildAddArgs("/my/catalog", "custom-skill", false);
321
+ expect(args).toEqual(["add", "/my/catalog/custom-skill", "-y"]);
322
+ });
323
+
324
+ test("adds -g flag for global hand-authored skill", () => {
325
+ const args = buildAddArgs("/my/catalog", "custom-skill", true);
326
+ expect(args).toEqual(["add", "/my/catalog/custom-skill", "-y", "-g"]);
327
+ });
134
328
  });
135
329
 
136
- test("getCatalogSkills returns empty array when catalog doesn't exist", () => {
137
- const skills = getCatalogSkills();
138
- expect(skills).toEqual([]);
330
+ // ── parseSkillsListOutput ───────────────────────────────────────────
331
+
332
+ describe("parseSkillsListOutput", () => {
333
+ test("parses valid JSON array", () => {
334
+ const json = JSON.stringify([
335
+ { name: "opentui", path: "/some/path", scope: "project", agents: ["Claude Code"] },
336
+ { name: "commit", path: "/other/path", scope: "global", agents: ["Cursor"] },
337
+ ]);
338
+ expect(parseSkillsListOutput(json)).toEqual(["opentui", "commit"]);
339
+ });
340
+
341
+ test("returns empty for empty array", () => {
342
+ expect(parseSkillsListOutput("[]")).toEqual([]);
343
+ });
344
+
345
+ test("returns empty for invalid JSON", () => {
346
+ expect(parseSkillsListOutput("not json")).toEqual([]);
347
+ });
348
+
349
+ test("returns empty for non-array JSON", () => {
350
+ expect(parseSkillsListOutput('{"foo": "bar"}')).toEqual([]);
351
+ });
352
+
353
+ test("returns empty for empty string", () => {
354
+ expect(parseSkillsListOutput("")).toEqual([]);
355
+ });
356
+ });
357
+
358
+ // ── planUpdates ─────────────────────────────────────────────────────
359
+
360
+ describe("planUpdates", () => {
361
+ const remoteTree: TreeEntry[] = [
362
+ { path: "skills", type: "tree", sha: "root" },
363
+ { path: "skills/alpha", type: "tree", sha: "new-alpha-sha" },
364
+ { path: "skills/alpha/SKILL.md", type: "blob", sha: "f1" },
365
+ { path: "skills/beta", type: "tree", sha: "same-beta-sha" },
366
+ { path: "skills/beta/SKILL.md", type: "blob", sha: "f2" },
367
+ ];
368
+
369
+ test("detects skills that need updating (SHA changed)", () => {
370
+ const skills = [
371
+ { name: "alpha", skillPath: "skills/alpha", treeSHA: "old-alpha-sha" },
372
+ ];
373
+ const plan = planUpdates(skills, remoteTree);
374
+ expect(plan.updated).toEqual([{
375
+ name: "alpha",
376
+ skillPath: "skills/alpha",
377
+ oldSHA: "old-alpha-sha",
378
+ newSHA: "new-alpha-sha",
379
+ }]);
380
+ expect(plan.upToDate).toEqual([]);
381
+ expect(plan.notFound).toEqual([]);
382
+ });
383
+
384
+ test("detects skills that are up to date (SHA matches)", () => {
385
+ const skills = [
386
+ { name: "beta", skillPath: "skills/beta", treeSHA: "same-beta-sha" },
387
+ ];
388
+ const plan = planUpdates(skills, remoteTree);
389
+ expect(plan.updated).toEqual([]);
390
+ expect(plan.upToDate).toEqual(["beta"]);
391
+ expect(plan.notFound).toEqual([]);
392
+ });
393
+
394
+ test("detects skills not found in remote tree", () => {
395
+ const skills = [
396
+ { name: "gamma", skillPath: "skills/gamma", treeSHA: "whatever" },
397
+ ];
398
+ const plan = planUpdates(skills, remoteTree);
399
+ expect(plan.updated).toEqual([]);
400
+ expect(plan.upToDate).toEqual([]);
401
+ expect(plan.notFound).toEqual(["gamma"]);
402
+ });
403
+
404
+ test("handles mix of updated, up-to-date, and missing", () => {
405
+ const skills = [
406
+ { name: "alpha", skillPath: "skills/alpha", treeSHA: "old-alpha-sha" },
407
+ { name: "beta", skillPath: "skills/beta", treeSHA: "same-beta-sha" },
408
+ { name: "gamma", skillPath: "skills/gamma", treeSHA: "missing" },
409
+ ];
410
+ const plan = planUpdates(skills, remoteTree);
411
+ expect(plan.updated.length).toBe(1);
412
+ expect(plan.upToDate.length).toBe(1);
413
+ expect(plan.notFound.length).toBe(1);
414
+ });
415
+
416
+ test("empty skills list returns empty plan", () => {
417
+ const plan = planUpdates([], remoteTree);
418
+ expect(plan.updated).toEqual([]);
419
+ expect(plan.upToDate).toEqual([]);
420
+ expect(plan.notFound).toEqual([]);
421
+ });
422
+ });
423
+
424
+ // ── groupByRepo ─────────────────────────────────────────────────────
425
+
426
+ describe("groupByRepo", () => {
427
+ test("groups skills by source repo", () => {
428
+ const lock: LockFile = {
429
+ version: 1,
430
+ skills: {
431
+ "skill-a": {
432
+ source: "owner/repo1", sourceUrl: "https://github.com/owner/repo1",
433
+ skillPath: "skills/skill-a", treeSHA: "aaa", addedAt: "2026-01-01",
434
+ },
435
+ "skill-b": {
436
+ source: "owner/repo1", sourceUrl: "https://github.com/owner/repo1",
437
+ skillPath: "skills/skill-b", treeSHA: "bbb", addedAt: "2026-01-01",
438
+ },
439
+ "skill-c": {
440
+ source: "other/repo2", sourceUrl: "https://github.com/other/repo2",
441
+ skillPath: "skills/skill-c", treeSHA: "ccc", addedAt: "2026-01-01",
442
+ },
443
+ },
444
+ };
445
+ const byRepo = groupByRepo(lock);
446
+ expect(byRepo.size).toBe(2);
447
+ expect(byRepo.get("owner/repo1")!.length).toBe(2);
448
+ expect(byRepo.get("other/repo2")!.length).toBe(1);
449
+ });
450
+
451
+ test("skips skills without source or treeSHA", () => {
452
+ const lock: LockFile = {
453
+ version: 1,
454
+ skills: {
455
+ "has-source": {
456
+ source: "a/b", sourceUrl: "https://github.com/a/b",
457
+ skillPath: "skills/x", treeSHA: "aaa", addedAt: "2026-01-01",
458
+ },
459
+ "no-source": {
460
+ source: "", sourceUrl: "",
461
+ skillPath: "skills/y", treeSHA: "bbb", addedAt: "2026-01-01",
462
+ },
463
+ "no-sha": {
464
+ source: "a/b", sourceUrl: "https://github.com/a/b",
465
+ skillPath: "skills/z", treeSHA: "", addedAt: "2026-01-01",
466
+ },
467
+ },
468
+ };
469
+ const byRepo = groupByRepo(lock);
470
+ expect(byRepo.size).toBe(1);
471
+ expect(byRepo.get("a/b")!.length).toBe(1);
472
+ });
473
+
474
+ test("empty lock file returns empty map", () => {
475
+ const byRepo = groupByRepo({ version: 1, skills: {} });
476
+ expect(byRepo.size).toBe(0);
477
+ });
478
+ });
479
+
480
+ // ── Migration (integration test) ────────────────────────────────────
481
+
482
+ describe("migration", () => {
483
+ test("copies skills from old library to catalog", () => {
484
+ const oldLib = join(tmp, "dotfiles/skills");
485
+ mkdirSync(join(oldLib, "skill-one"), { recursive: true });
486
+ writeFileSync(join(oldLib, "skill-one", "SKILL.md"), "# One");
487
+ mkdirSync(join(oldLib, "skill-two"), { recursive: true });
488
+ writeFileSync(join(oldLib, "skill-two", "SKILL.md"), "# Two");
489
+
490
+ const catalog = catalogDir();
491
+ mkdirSync(catalog, { recursive: true });
492
+
493
+ // Simulate migration
494
+ for (const name of readdirSync(oldLib)) {
495
+ cpSync(join(oldLib, name), join(catalog, name), { recursive: true });
496
+ }
497
+
498
+ expect(getCatalogSkills()).toEqual(["skill-one", "skill-two"]);
499
+ });
139
500
  });