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.
@@ -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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue({
464
- skillName: "evil-repo",
465
- threatType: "credential-theft",
466
- severity: "critical",
467
- reason: "Steals AWS credentials",
468
- evidenceUrls: [],
469
- discoveredAt: "2026-02-01T00:00:00Z",
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(mockCheckBlocklist).toHaveBeenCalledWith("evil-repo", undefined);
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
- mockCheckBlocklist.mockResolvedValue(null);
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(mockCheckBlocklist).toHaveBeenCalledWith("safe-repo", undefined);
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
- mockCheckBlocklist.mockResolvedValue(null);
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(mockCheckBlocklist).toHaveBeenCalledWith("my-skill", undefined);
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
- mockCheckBlocklist.mockResolvedValue({
522
- skillName: "evil-plugin",
523
- threatType: "prompt-injection",
524
- severity: "critical",
525
- reason: "Injects malicious prompts",
526
- evidenceUrls: [],
527
- discoveredAt: "2026-02-01T00:00:00Z",
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(mockCheckBlocklist).toHaveBeenCalledWith("evil-plugin");
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
- mockCheckBlocklist.mockResolvedValue(null);
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(mockCheckBlocklist).toHaveBeenCalledWith("safe-plugin");
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
- mockCheckBlocklist.mockResolvedValue({
596
- skillName: "evil-skill",
597
- threatType: "credential-theft",
598
- severity: "critical",
599
- reason: "Base64-encoded AWS credential exfil",
600
- evidenceUrls: [],
601
- discoveredAt: "2026-02-01T00:00:00Z",
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
- mockCheckBlocklist.mockResolvedValue({
624
- skillName: "evil-plugin",
625
- threatType: "prompt-injection",
626
- severity: "critical",
627
- reason: "Injects malicious prompts",
628
- evidenceUrls: [],
629
- discoveredAt: "2026-02-01T00:00:00Z",
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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
- mockCheckBlocklist.mockResolvedValue(null);
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