vskill 0.1.33 → 0.1.35
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/dist/blocklist/blocklist.d.ts +9 -1
- package/dist/blocklist/blocklist.js +28 -0
- package/dist/blocklist/blocklist.js.map +1 -1
- package/dist/blocklist/types.d.ts +13 -0
- package/dist/commands/add-blocklist-e2e.test.js +13 -6
- package/dist/commands/add-blocklist-e2e.test.js.map +1 -1
- package/dist/commands/add-wizard.test.js +3 -0
- package/dist/commands/add-wizard.test.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +99 -36
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +188 -54
- package/dist/commands/add.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -73,8 +73,10 @@ vi.mock("../scanner/index.js", () => ({
|
|
|
73
73
|
// Mock blocklist
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
75
|
const mockCheckBlocklist = vi.fn();
|
|
76
|
+
const mockCheckInstallSafety = vi.fn();
|
|
76
77
|
vi.mock("../blocklist/blocklist.js", () => ({
|
|
77
78
|
checkBlocklist: (...args) => mockCheckBlocklist(...args),
|
|
79
|
+
checkInstallSafety: (...args) => mockCheckInstallSafety(...args),
|
|
78
80
|
}));
|
|
79
81
|
// ---------------------------------------------------------------------------
|
|
80
82
|
// Mock security (platform security check)
|
|
@@ -204,7 +206,7 @@ beforeEach(() => {
|
|
|
204
206
|
describe("addCommand with --plugin option (plugin directory support)", () => {
|
|
205
207
|
beforeEach(() => {
|
|
206
208
|
// Plugin path hits blocklist check first — return null (not blocked)
|
|
207
|
-
|
|
209
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
208
210
|
});
|
|
209
211
|
// TC-001: --plugin <name> flag selects sub-plugin from multi-plugin repo
|
|
210
212
|
describe("TC-001: plugin flag selects sub-plugin directory", () => {
|
|
@@ -460,19 +462,23 @@ describe("addCommand blocklist check (GitHub path)", () => {
|
|
|
460
462
|
globalThis.fetch = originalFetch;
|
|
461
463
|
});
|
|
462
464
|
it("blocks installation when skill is on the blocklist", async () => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
465
|
+
mockCheckInstallSafety.mockResolvedValue({
|
|
466
|
+
blocked: true,
|
|
467
|
+
entry: {
|
|
468
|
+
skillName: "evil-repo",
|
|
469
|
+
threatType: "credential-theft",
|
|
470
|
+
severity: "critical",
|
|
471
|
+
reason: "Steals AWS credentials",
|
|
472
|
+
evidenceUrls: [],
|
|
473
|
+
discoveredAt: "2026-02-01T00:00:00Z",
|
|
474
|
+
},
|
|
475
|
+
rejected: false,
|
|
470
476
|
});
|
|
471
477
|
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
472
478
|
throw new Error("process.exit");
|
|
473
479
|
});
|
|
474
480
|
await expect(addCommand("owner/evil-repo", {})).rejects.toThrow("process.exit");
|
|
475
|
-
expect(
|
|
481
|
+
expect(mockCheckInstallSafety).toHaveBeenCalledWith("evil-repo");
|
|
476
482
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
477
483
|
// Tier 1 scan should NOT have been called (blocked before scan)
|
|
478
484
|
expect(mockRunTier1Scan).not.toHaveBeenCalled();
|
|
@@ -484,7 +490,7 @@ describe("addCommand blocklist check (GitHub path)", () => {
|
|
|
484
490
|
mockExit.mockRestore();
|
|
485
491
|
});
|
|
486
492
|
it("proceeds normally when skill is NOT on the blocklist", async () => {
|
|
487
|
-
|
|
493
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
488
494
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
489
495
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
490
496
|
mockEnsureLockfile.mockReturnValue({
|
|
@@ -495,11 +501,11 @@ describe("addCommand blocklist check (GitHub path)", () => {
|
|
|
495
501
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
496
502
|
});
|
|
497
503
|
await addCommand("owner/safe-repo", {});
|
|
498
|
-
expect(
|
|
504
|
+
expect(mockCheckInstallSafety).toHaveBeenCalledWith("safe-repo");
|
|
499
505
|
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
500
506
|
});
|
|
501
507
|
it("uses --skill name for blocklist check when provided", async () => {
|
|
502
|
-
|
|
508
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
503
509
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
504
510
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
505
511
|
mockEnsureLockfile.mockReturnValue({
|
|
@@ -510,7 +516,7 @@ describe("addCommand blocklist check (GitHub path)", () => {
|
|
|
510
516
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
511
517
|
});
|
|
512
518
|
await addCommand("owner/repo", { skill: "my-skill" });
|
|
513
|
-
expect(
|
|
519
|
+
expect(mockCheckInstallSafety).toHaveBeenCalledWith("my-skill");
|
|
514
520
|
});
|
|
515
521
|
});
|
|
516
522
|
// ---------------------------------------------------------------------------
|
|
@@ -518,13 +524,17 @@ describe("addCommand blocklist check (GitHub path)", () => {
|
|
|
518
524
|
// ---------------------------------------------------------------------------
|
|
519
525
|
describe("addCommand blocklist check (plugin path)", () => {
|
|
520
526
|
it("blocks plugin installation when plugin is on the blocklist", async () => {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
527
|
+
mockCheckInstallSafety.mockResolvedValue({
|
|
528
|
+
blocked: true,
|
|
529
|
+
entry: {
|
|
530
|
+
skillName: "evil-plugin",
|
|
531
|
+
threatType: "prompt-injection",
|
|
532
|
+
severity: "critical",
|
|
533
|
+
reason: "Injects malicious prompts",
|
|
534
|
+
evidenceUrls: [],
|
|
535
|
+
discoveredAt: "2026-02-01T00:00:00Z",
|
|
536
|
+
},
|
|
537
|
+
rejected: false,
|
|
528
538
|
});
|
|
529
539
|
mockExistsSync.mockReturnValue(true);
|
|
530
540
|
mockReadFileSync.mockImplementation((p) => {
|
|
@@ -543,12 +553,12 @@ describe("addCommand blocklist check (plugin path)", () => {
|
|
|
543
553
|
throw new Error("process.exit");
|
|
544
554
|
});
|
|
545
555
|
await expect(addCommand("source", { plugin: "evil-plugin", pluginDir: "/tmp/test" })).rejects.toThrow("process.exit");
|
|
546
|
-
expect(
|
|
556
|
+
expect(mockCheckInstallSafety).toHaveBeenCalledWith("evil-plugin");
|
|
547
557
|
expect(mockRunTier1Scan).not.toHaveBeenCalled();
|
|
548
558
|
mockExit.mockRestore();
|
|
549
559
|
});
|
|
550
560
|
it("proceeds with plugin installation when not blocklisted", async () => {
|
|
551
|
-
|
|
561
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
552
562
|
mockExistsSync.mockReturnValue(true);
|
|
553
563
|
mockReadFileSync.mockImplementation((p) => {
|
|
554
564
|
if (p.includes("marketplace.json")) {
|
|
@@ -573,7 +583,7 @@ describe("addCommand blocklist check (plugin path)", () => {
|
|
|
573
583
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
574
584
|
});
|
|
575
585
|
await addCommand("source", { plugin: "safe-plugin", pluginDir: "/tmp/test" });
|
|
576
|
-
expect(
|
|
586
|
+
expect(mockCheckInstallSafety).toHaveBeenCalledWith("safe-plugin");
|
|
577
587
|
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
578
588
|
});
|
|
579
589
|
});
|
|
@@ -592,13 +602,17 @@ describe("addCommand --force with blocked skill", () => {
|
|
|
592
602
|
globalThis.fetch = originalFetch;
|
|
593
603
|
});
|
|
594
604
|
it("shows warning box and continues when --force + blocked (GitHub path)", async () => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
605
|
+
mockCheckInstallSafety.mockResolvedValue({
|
|
606
|
+
blocked: true,
|
|
607
|
+
entry: {
|
|
608
|
+
skillName: "evil-skill",
|
|
609
|
+
threatType: "credential-theft",
|
|
610
|
+
severity: "critical",
|
|
611
|
+
reason: "Base64-encoded AWS credential exfil",
|
|
612
|
+
evidenceUrls: [],
|
|
613
|
+
discoveredAt: "2026-02-01T00:00:00Z",
|
|
614
|
+
},
|
|
615
|
+
rejected: false,
|
|
602
616
|
});
|
|
603
617
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
604
618
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -620,13 +634,17 @@ describe("addCommand --force with blocked skill", () => {
|
|
|
620
634
|
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
621
635
|
});
|
|
622
636
|
it("shows warning box and continues when --force + blocked (plugin path)", async () => {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
637
|
+
mockCheckInstallSafety.mockResolvedValue({
|
|
638
|
+
blocked: true,
|
|
639
|
+
entry: {
|
|
640
|
+
skillName: "evil-plugin",
|
|
641
|
+
threatType: "prompt-injection",
|
|
642
|
+
severity: "critical",
|
|
643
|
+
reason: "Injects malicious prompts",
|
|
644
|
+
evidenceUrls: [],
|
|
645
|
+
discoveredAt: "2026-02-01T00:00:00Z",
|
|
646
|
+
},
|
|
647
|
+
rejected: false,
|
|
630
648
|
});
|
|
631
649
|
mockExistsSync.mockReturnValue(true);
|
|
632
650
|
mockReadFileSync.mockImplementation((p) => {
|
|
@@ -669,7 +687,7 @@ describe("addCommand platform security check (GitHub path)", () => {
|
|
|
669
687
|
ok: true,
|
|
670
688
|
text: async () => "# Safe Skill\nNormal content",
|
|
671
689
|
});
|
|
672
|
-
|
|
690
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
673
691
|
});
|
|
674
692
|
afterEach(() => {
|
|
675
693
|
globalThis.fetch = originalFetch;
|
|
@@ -775,7 +793,7 @@ describe("addCommand platform security check (GitHub path)", () => {
|
|
|
775
793
|
describe("addCommand multi-skill discovery (GitHub path)", () => {
|
|
776
794
|
const originalFetch = globalThis.fetch;
|
|
777
795
|
beforeEach(() => {
|
|
778
|
-
|
|
796
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
779
797
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
780
798
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
781
799
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -886,7 +904,7 @@ describe("addCommand multi-skill discovery (GitHub path)", () => {
|
|
|
886
904
|
describe("addCommand source format routing", () => {
|
|
887
905
|
const originalFetch = globalThis.fetch;
|
|
888
906
|
beforeEach(() => {
|
|
889
|
-
|
|
907
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
890
908
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
891
909
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
892
910
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -977,7 +995,7 @@ describe("addCommand source format routing", () => {
|
|
|
977
995
|
// Reset fetch mock
|
|
978
996
|
globalThis.fetch.mockClear();
|
|
979
997
|
vi.clearAllMocks();
|
|
980
|
-
|
|
998
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
981
999
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
982
1000
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
983
1001
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -998,7 +1016,7 @@ describe("addCommand source format routing", () => {
|
|
|
998
1016
|
// ---------------------------------------------------------------------------
|
|
999
1017
|
describe("addCommand registry install (no slash in source)", () => {
|
|
1000
1018
|
beforeEach(() => {
|
|
1001
|
-
|
|
1019
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1002
1020
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
1003
1021
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1004
1022
|
mockEnsureLockfile.mockReturnValue({
|
|
@@ -1120,7 +1138,7 @@ describe("addCommand smart project root resolution", () => {
|
|
|
1120
1138
|
ok: true,
|
|
1121
1139
|
text: async () => "# Skill content",
|
|
1122
1140
|
});
|
|
1123
|
-
|
|
1141
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1124
1142
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1125
1143
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
1126
1144
|
mockEnsureLockfile.mockReturnValue({
|
|
@@ -1164,7 +1182,7 @@ describe("addCommand --agent filter", () => {
|
|
|
1164
1182
|
ok: true,
|
|
1165
1183
|
text: async () => "# Skill content",
|
|
1166
1184
|
});
|
|
1167
|
-
|
|
1185
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1168
1186
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1169
1187
|
mockEnsureLockfile.mockReturnValue({
|
|
1170
1188
|
version: 1,
|
|
@@ -1225,7 +1243,7 @@ describe("addCommand nested directory fix (TC-016)", () => {
|
|
|
1225
1243
|
ok: true,
|
|
1226
1244
|
text: async () => "# Skill content",
|
|
1227
1245
|
});
|
|
1228
|
-
|
|
1246
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1229
1247
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1230
1248
|
mockEnsureLockfile.mockReturnValue({
|
|
1231
1249
|
version: 1,
|
|
@@ -1294,7 +1312,7 @@ describe("addCommand with --repo flag (remote plugin install)", () => {
|
|
|
1294
1312
|
],
|
|
1295
1313
|
});
|
|
1296
1314
|
function setupHappyPath() {
|
|
1297
|
-
|
|
1315
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1298
1316
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1299
1317
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
1300
1318
|
mockFindProjectRoot.mockReturnValue("/project");
|
|
@@ -1381,7 +1399,7 @@ describe("addCommand with --repo flag (remote plugin install)", () => {
|
|
|
1381
1399
|
exitSpy.mockRestore();
|
|
1382
1400
|
});
|
|
1383
1401
|
it("exits with error when plugin not found in marketplace", async () => {
|
|
1384
|
-
|
|
1402
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1385
1403
|
globalThis.fetch = vi.fn().mockImplementation(async (url) => {
|
|
1386
1404
|
if (url.includes("marketplace.json")) {
|
|
1387
1405
|
return { ok: true, text: async () => marketplaceJson };
|
|
@@ -1418,7 +1436,7 @@ describe("addCommand native Claude Code plugin install", () => {
|
|
|
1418
1436
|
plugins: [{ name: "sw-test", source: "./plugins/test", version: "1.0.0" }],
|
|
1419
1437
|
});
|
|
1420
1438
|
beforeEach(() => {
|
|
1421
|
-
|
|
1439
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1422
1440
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1423
1441
|
mockEnsureLockfile.mockReturnValue({
|
|
1424
1442
|
version: 1,
|
|
@@ -1508,7 +1526,7 @@ describe("addCommand with --repo --all flag (bulk install)", () => {
|
|
|
1508
1526
|
],
|
|
1509
1527
|
});
|
|
1510
1528
|
function setupAllHappyPath() {
|
|
1511
|
-
|
|
1529
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1512
1530
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1513
1531
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
1514
1532
|
mockFindProjectRoot.mockReturnValue("/project");
|
|
@@ -1623,7 +1641,7 @@ describe("addCommand project root consistency", () => {
|
|
|
1623
1641
|
ok: true,
|
|
1624
1642
|
text: async () => "# Skill content",
|
|
1625
1643
|
});
|
|
1626
|
-
|
|
1644
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1627
1645
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
1628
1646
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1629
1647
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -1692,7 +1710,7 @@ describe("addCommand project root consistency", () => {
|
|
|
1692
1710
|
describe("addCommand error handling in discovery flow", () => {
|
|
1693
1711
|
const originalFetch = globalThis.fetch;
|
|
1694
1712
|
beforeEach(() => {
|
|
1695
|
-
|
|
1713
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1696
1714
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
1697
1715
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1698
1716
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -1739,7 +1757,7 @@ describe("addCommand error handling in discovery flow", () => {
|
|
|
1739
1757
|
describe("addCommand flat name identifier guidance", () => {
|
|
1740
1758
|
const originalFetch = globalThis.fetch;
|
|
1741
1759
|
beforeEach(() => {
|
|
1742
|
-
|
|
1760
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1743
1761
|
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
1744
1762
|
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1745
1763
|
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
@@ -1790,8 +1808,124 @@ describe("addCommand flat name identifier guidance", () => {
|
|
|
1790
1808
|
const logOutput = console.log.mock.calls
|
|
1791
1809
|
.map((c) => String(c[0]))
|
|
1792
1810
|
.join("\n");
|
|
1793
|
-
// Should suggest the direct format
|
|
1794
|
-
expect(logOutput).toContain("remotion-dev/skills");
|
|
1811
|
+
// Should suggest the direct format including skill name
|
|
1812
|
+
expect(logOutput).toContain("remotion-dev/skills/remotion-dev-skills-remotion");
|
|
1813
|
+
});
|
|
1814
|
+
});
|
|
1815
|
+
// ---------------------------------------------------------------------------
|
|
1816
|
+
// Registry → GitHub fallback: auto-select matching skill (_targetSkill)
|
|
1817
|
+
// ---------------------------------------------------------------------------
|
|
1818
|
+
describe("addCommand registry fallback auto-selects matching skill", () => {
|
|
1819
|
+
const originalFetch = globalThis.fetch;
|
|
1820
|
+
beforeEach(() => {
|
|
1821
|
+
mockCheckInstallSafety.mockResolvedValue({ blocked: false, rejected: false });
|
|
1822
|
+
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
1823
|
+
mockRunTier1Scan.mockReturnValue(makeScanResult());
|
|
1824
|
+
mockDetectInstalledAgents.mockResolvedValue([makeAgent()]);
|
|
1825
|
+
mockFindProjectRoot.mockReturnValue(process.cwd());
|
|
1826
|
+
// Reset canonical installer (earlier test overrides with throwing impl)
|
|
1827
|
+
mockInstallSymlink.mockReturnValue([]);
|
|
1828
|
+
mockInstallCopy.mockReturnValue([]);
|
|
1829
|
+
mockEnsureLockfile.mockReturnValue({
|
|
1830
|
+
version: 1,
|
|
1831
|
+
agents: [],
|
|
1832
|
+
skills: {},
|
|
1833
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
1834
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
afterEach(() => {
|
|
1838
|
+
globalThis.fetch = originalFetch;
|
|
1839
|
+
});
|
|
1840
|
+
it("auto-selects matching skill from multi-skill repo (installs 1, not all)", async () => {
|
|
1841
|
+
mockGetSkill.mockResolvedValue({
|
|
1842
|
+
name: "excalidraw-diagram-generator",
|
|
1843
|
+
author: "github",
|
|
1844
|
+
content: undefined,
|
|
1845
|
+
repoUrl: "https://github.com/github/awesome-copilot",
|
|
1846
|
+
installs: 100,
|
|
1847
|
+
updatedAt: "2026-02-20T00:00:00Z",
|
|
1848
|
+
});
|
|
1849
|
+
mockDiscoverSkills.mockResolvedValue([
|
|
1850
|
+
{ name: "code-review", path: "skills/code-review/SKILL.md", rawUrl: "https://raw.githubusercontent.com/github/awesome-copilot/main/skills/code-review/SKILL.md" },
|
|
1851
|
+
{ name: "excalidraw-diagram-generator", path: "skills/excalidraw-diagram-generator/SKILL.md", rawUrl: "https://raw.githubusercontent.com/github/awesome-copilot/main/skills/excalidraw-diagram-generator/SKILL.md" },
|
|
1852
|
+
{ name: "unit-test-writer", path: "skills/unit-test-writer/SKILL.md", rawUrl: "https://raw.githubusercontent.com/github/awesome-copilot/main/skills/unit-test-writer/SKILL.md" },
|
|
1853
|
+
]);
|
|
1854
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
1855
|
+
ok: true,
|
|
1856
|
+
text: async () => "# Excalidraw Diagram Generator",
|
|
1857
|
+
});
|
|
1858
|
+
await addCommand("excalidraw-diagram-generator", {});
|
|
1859
|
+
// Should only install 1 skill, not all 3
|
|
1860
|
+
expect(mockRunTier1Scan).toHaveBeenCalledTimes(1);
|
|
1861
|
+
const lockArg = mockWriteLockfile.mock.calls[0][0];
|
|
1862
|
+
expect(Object.keys(lockArg.skills)).toHaveLength(1);
|
|
1863
|
+
expect(lockArg.skills).toHaveProperty("excalidraw-diagram-generator");
|
|
1864
|
+
expect(lockArg.skills).not.toHaveProperty("code-review");
|
|
1865
|
+
expect(lockArg.skills).not.toHaveProperty("unit-test-writer");
|
|
1866
|
+
});
|
|
1867
|
+
it("falls through to install all when registry name does not match any discovered skill", async () => {
|
|
1868
|
+
mockGetSkill.mockResolvedValue({
|
|
1869
|
+
name: "remotion-dev-skills-remotion",
|
|
1870
|
+
author: "remotion-dev",
|
|
1871
|
+
content: undefined,
|
|
1872
|
+
repoUrl: "https://github.com/remotion-dev/skills",
|
|
1873
|
+
installs: 0,
|
|
1874
|
+
updatedAt: "2026-02-20T00:00:00Z",
|
|
1875
|
+
});
|
|
1876
|
+
// Registry name "remotion-dev-skills-remotion" does NOT match dir names
|
|
1877
|
+
mockDiscoverSkills.mockResolvedValue([
|
|
1878
|
+
{ name: "remotion", path: "skills/remotion/SKILL.md", rawUrl: "https://raw.githubusercontent.com/remotion-dev/skills/main/skills/remotion/SKILL.md" },
|
|
1879
|
+
{ name: "video-editor", path: "skills/video-editor/SKILL.md", rawUrl: "https://raw.githubusercontent.com/remotion-dev/skills/main/skills/video-editor/SKILL.md" },
|
|
1880
|
+
]);
|
|
1881
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
1882
|
+
ok: true,
|
|
1883
|
+
text: async () => "# Skill content",
|
|
1884
|
+
});
|
|
1885
|
+
// Non-TTY → installs all discovered skills (no prompt)
|
|
1886
|
+
await addCommand("remotion-dev-skills-remotion", {});
|
|
1887
|
+
// Both skills should be installed (no auto-filter match)
|
|
1888
|
+
expect(mockRunTier1Scan).toHaveBeenCalledTimes(2);
|
|
1889
|
+
const lockArg = mockWriteLockfile.mock.calls[0][0];
|
|
1890
|
+
expect(Object.keys(lockArg.skills)).toHaveLength(2);
|
|
1891
|
+
});
|
|
1892
|
+
it("tip message suggests 3-part owner/repo/skill format", async () => {
|
|
1893
|
+
mockGetSkill.mockResolvedValue({
|
|
1894
|
+
name: "excalidraw-diagram-generator",
|
|
1895
|
+
author: "github",
|
|
1896
|
+
content: undefined,
|
|
1897
|
+
repoUrl: "https://github.com/github/awesome-copilot",
|
|
1898
|
+
installs: 0,
|
|
1899
|
+
updatedAt: "2026-02-20T00:00:00Z",
|
|
1900
|
+
});
|
|
1901
|
+
mockDiscoverSkills.mockResolvedValue([
|
|
1902
|
+
{ name: "excalidraw-diagram-generator", path: "skills/excalidraw-diagram-generator/SKILL.md", rawUrl: "https://raw.githubusercontent.com/github/awesome-copilot/main/skills/excalidraw-diagram-generator/SKILL.md" },
|
|
1903
|
+
]);
|
|
1904
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
1905
|
+
ok: true,
|
|
1906
|
+
text: async () => "# Skill content",
|
|
1907
|
+
});
|
|
1908
|
+
await addCommand("excalidraw-diagram-generator", {});
|
|
1909
|
+
const logOutput = console.log.mock.calls
|
|
1910
|
+
.map((c) => String(c[0]))
|
|
1911
|
+
.join("\n");
|
|
1912
|
+
expect(logOutput).toContain("github/awesome-copilot/excalidraw-diagram-generator");
|
|
1913
|
+
});
|
|
1914
|
+
it("does not auto-filter when _targetSkill is not set (direct owner/repo)", async () => {
|
|
1915
|
+
mockDiscoverSkills.mockResolvedValue([
|
|
1916
|
+
{ name: "skill-a", path: "skills/skill-a/SKILL.md", rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/skill-a/SKILL.md" },
|
|
1917
|
+
{ name: "skill-b", path: "skills/skill-b/SKILL.md", rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/skill-b/SKILL.md" },
|
|
1918
|
+
]);
|
|
1919
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
1920
|
+
ok: true,
|
|
1921
|
+
text: async () => "# Skill content",
|
|
1922
|
+
});
|
|
1923
|
+
// Direct install without registry — no _targetSkill
|
|
1924
|
+
await addCommand("owner/repo", {});
|
|
1925
|
+
// Both skills should be installed
|
|
1926
|
+
expect(mockRunTier1Scan).toHaveBeenCalledTimes(2);
|
|
1927
|
+
const lockArg = mockWriteLockfile.mock.calls[0][0];
|
|
1928
|
+
expect(Object.keys(lockArg.skills)).toHaveLength(2);
|
|
1795
1929
|
});
|
|
1796
1930
|
});
|
|
1797
1931
|
//# sourceMappingURL=add.test.js.map
|