@codemcp/agentskills-cli 0.0.9 → 1.0.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.
@@ -0,0 +1,2258 @@
1
+ /**
2
+ * Tests for CLI install command with --with-mcp flag
3
+ *
4
+ * TDD RED PHASE - Tests written before implementation
5
+ * These tests define the expected behavior of automatic MCP server
6
+ * installation and configuration via the --with-mcp flag.
7
+ *
8
+ * Task: agent-skills-2.3.18
9
+ * Phase: TDD RED - Writing failing tests
10
+ */
11
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
12
+ import { installCommand } from "../commands/install.js";
13
+ import { PackageConfigManager, SkillInstaller, MCPConfigManager, MCPDependencyChecker, substituteParameters } from "@codemcp/agentskills-core";
14
+ // Mock fs module
15
+ vi.mock("fs", () => ({
16
+ promises: {
17
+ mkdir: vi.fn().mockResolvedValue(undefined),
18
+ access: vi.fn().mockResolvedValue(undefined),
19
+ readFile: vi.fn(),
20
+ writeFile: vi.fn(),
21
+ stat: vi.fn(),
22
+ rm: vi.fn()
23
+ }
24
+ }));
25
+ // Mock inquirer for prompting
26
+ vi.mock("inquirer", () => ({
27
+ default: {
28
+ prompt: vi.fn()
29
+ }
30
+ }));
31
+ // Mock all dependencies
32
+ vi.mock("@codemcp/agentskills-core", async () => {
33
+ const actualCore = await vi.importActual("@codemcp/agentskills-core");
34
+ return {
35
+ ...actualCore,
36
+ PackageConfigManager: vi.fn(),
37
+ SkillInstaller: vi.fn(),
38
+ MCPConfigManager: vi.fn(),
39
+ MCPDependencyChecker: vi.fn(),
40
+ substituteParameters: vi.fn()
41
+ };
42
+ });
43
+ vi.mock("ora", () => ({
44
+ default: vi.fn(() => ({
45
+ start: vi.fn().mockReturnThis(),
46
+ stop: vi.fn().mockReturnThis(),
47
+ succeed: vi.fn().mockReturnThis(),
48
+ fail: vi.fn().mockReturnThis()
49
+ }))
50
+ }));
51
+ describe("Install Command - --with-mcp Flag", () => {
52
+ let mockConfigManager;
53
+ let mockInstaller;
54
+ let mockMCPConfigManager;
55
+ let mockMCPDependencyChecker;
56
+ let mockInquirer;
57
+ let consoleLogSpy;
58
+ let consoleErrorSpy;
59
+ let processExitSpy;
60
+ beforeEach(async () => {
61
+ // Setup mocks
62
+ mockConfigManager = {
63
+ loadConfig: vi.fn()
64
+ };
65
+ mockInstaller = {
66
+ install: vi.fn(),
67
+ generateLockFile: vi.fn(),
68
+ loadInstalledSkills: vi.fn()
69
+ };
70
+ mockMCPConfigManager = {
71
+ isServerConfigured: vi.fn(),
72
+ addServer: vi.fn()
73
+ };
74
+ mockMCPDependencyChecker = {
75
+ collectDependencies: vi.fn(),
76
+ checkDependencies: vi.fn()
77
+ };
78
+ // Get the mocked inquirer
79
+ const inquirer = await import("inquirer");
80
+ mockInquirer = inquirer.default;
81
+ vi.mocked(PackageConfigManager).mockImplementation(() => mockConfigManager);
82
+ vi.mocked(SkillInstaller).mockImplementation(() => mockInstaller);
83
+ vi.mocked(MCPConfigManager).mockImplementation(() => mockMCPConfigManager);
84
+ vi.mocked(MCPDependencyChecker).mockImplementation(() => mockMCPDependencyChecker);
85
+ // Default substituteParameters to pass-through
86
+ vi.mocked(substituteParameters).mockImplementation((template, params) => {
87
+ if (typeof template === "string") {
88
+ let result = template;
89
+ for (const [key, value] of Object.entries(params)) {
90
+ result = result.replace(new RegExp(`{{${key}}}`, "g"), String(value));
91
+ }
92
+ return result;
93
+ }
94
+ if (Array.isArray(template)) {
95
+ return template.map((item) => typeof item === "string"
96
+ ? Object.entries(params).reduce((str, [key, value]) => str.replace(new RegExp(`{{${key}}}`, "g"), String(value)), item)
97
+ : item);
98
+ }
99
+ if (typeof template === "object" && template !== null) {
100
+ const result = {};
101
+ for (const [key, value] of Object.entries(template)) {
102
+ // Recursively substitute parameters in object values
103
+ if (typeof value === "string") {
104
+ let substituted = value;
105
+ for (const [paramKey, paramValue] of Object.entries(params)) {
106
+ substituted = substituted.replace(new RegExp(`{{${paramKey}}}`, "g"), String(paramValue));
107
+ }
108
+ result[key] = substituted;
109
+ }
110
+ else {
111
+ result[key] = value;
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ return template;
117
+ });
118
+ // Setup spies
119
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
120
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
121
+ processExitSpy = vi
122
+ .spyOn(process, "exit")
123
+ .mockImplementation((() => { }));
124
+ // Default mock implementations
125
+ mockConfigManager.loadConfig.mockResolvedValue({
126
+ skills: {},
127
+ config: {
128
+ skillsDirectory: ".agentskills/skills",
129
+ autoDiscover: [],
130
+ maxSkillSize: 5000,
131
+ logLevel: "info"
132
+ },
133
+ source: {
134
+ type: "file",
135
+ path: "/test/package.json"
136
+ }
137
+ });
138
+ });
139
+ afterEach(() => {
140
+ vi.clearAllMocks();
141
+ consoleLogSpy.mockRestore();
142
+ consoleErrorSpy.mockRestore();
143
+ processExitSpy.mockRestore();
144
+ });
145
+ describe("--with-mcp flag with no missing dependencies", () => {
146
+ it("should succeed when all MCP dependencies are already configured", async () => {
147
+ // Setup
148
+ const config = {
149
+ skills: {
150
+ "file-manager": "github:user/file-manager#v1.0.0"
151
+ },
152
+ config: {
153
+ skillsDirectory: ".agentskills/skills",
154
+ autoDiscover: [],
155
+ maxSkillSize: 5000,
156
+ logLevel: "info"
157
+ },
158
+ source: {
159
+ type: "file",
160
+ path: "/test/package.json"
161
+ }
162
+ };
163
+ const installedSkills = [
164
+ {
165
+ metadata: {
166
+ name: "file-manager",
167
+ description: "File management skill",
168
+ requiresMcpServers: [
169
+ {
170
+ name: "filesystem",
171
+ description: "File system access",
172
+ command: "npx",
173
+ args: [
174
+ "-y",
175
+ "@modelcontextprotocol/server-filesystem",
176
+ "{{ROOT_PATH}}"
177
+ ],
178
+ parameters: {
179
+ ROOT_PATH: {
180
+ description: "Root directory for file operations",
181
+ required: true,
182
+ default: "/tmp"
183
+ }
184
+ }
185
+ }
186
+ ]
187
+ },
188
+ body: "Skill content"
189
+ }
190
+ ];
191
+ const dependencies = [
192
+ {
193
+ serverName: "filesystem",
194
+ neededBy: ["file-manager"],
195
+ spec: {
196
+ name: "filesystem",
197
+ description: "File system access",
198
+ command: "npx",
199
+ args: [
200
+ "-y",
201
+ "@modelcontextprotocol/server-filesystem",
202
+ "{{ROOT_PATH}}"
203
+ ],
204
+ parameters: {
205
+ ROOT_PATH: {
206
+ description: "Root directory for file operations",
207
+ required: true,
208
+ default: "/tmp"
209
+ }
210
+ }
211
+ }
212
+ }
213
+ ];
214
+ const checkResult = {
215
+ allConfigured: true,
216
+ missing: [],
217
+ configured: ["filesystem"]
218
+ };
219
+ mockConfigManager.loadConfig.mockResolvedValue(config);
220
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(true); // agentskills already configured
221
+ mockInstaller.install.mockResolvedValue({
222
+ success: true,
223
+ name: "file-manager",
224
+ spec: "github:user/file-manager#v1.0.0",
225
+ resolvedVersion: "1.0.0",
226
+ integrity: "sha512-abc123",
227
+ installPath: "/test/.agentskills/skills/file-manager"
228
+ });
229
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
230
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
231
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
232
+ // Execute with --with-mcp flag
233
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
234
+ // Verify
235
+ expect(processExitSpy).toHaveBeenCalledWith(0);
236
+ expect(mockInquirer.prompt).not.toHaveBeenCalled(); // No prompting needed
237
+ expect(mockMCPConfigManager.addServer).not.toHaveBeenCalled(); // All servers already configured
238
+ });
239
+ it("should succeed when no MCP dependencies are required", async () => {
240
+ // Setup
241
+ const config = {
242
+ skills: {
243
+ "simple-skill": "github:user/simple-skill#v1.0.0"
244
+ },
245
+ config: {
246
+ skillsDirectory: ".agentskills/skills",
247
+ autoDiscover: [],
248
+ maxSkillSize: 5000,
249
+ logLevel: "info"
250
+ },
251
+ source: {
252
+ type: "file",
253
+ path: "/test/package.json"
254
+ }
255
+ };
256
+ const installedSkills = [
257
+ {
258
+ metadata: {
259
+ name: "simple-skill",
260
+ description: "Simple skill without MCP dependencies"
261
+ },
262
+ body: "Skill content"
263
+ }
264
+ ];
265
+ mockConfigManager.loadConfig.mockResolvedValue(config);
266
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(true); // agentskills already configured
267
+ mockInstaller.install.mockResolvedValue({
268
+ success: true,
269
+ name: "simple-skill",
270
+ spec: "github:user/simple-skill#v1.0.0",
271
+ resolvedVersion: "1.0.0",
272
+ integrity: "sha512-abc123",
273
+ installPath: "/test/.agentskills/skills/simple-skill"
274
+ });
275
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
276
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue([]);
277
+ // Execute with --with-mcp flag
278
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
279
+ // Verify
280
+ expect(processExitSpy).toHaveBeenCalledWith(0);
281
+ expect(mockInquirer.prompt).not.toHaveBeenCalled();
282
+ expect(mockMCPConfigManager.addServer).not.toHaveBeenCalled(); // agentskills already configured
283
+ });
284
+ });
285
+ describe("--with-mcp flag with missing dependencies", () => {
286
+ it("should prompt for required parameters when dependencies are missing", async () => {
287
+ // Setup
288
+ const config = {
289
+ skills: {
290
+ "file-manager": "github:user/file-manager#v1.0.0"
291
+ },
292
+ config: {
293
+ skillsDirectory: ".agentskills/skills",
294
+ autoDiscover: [],
295
+ maxSkillSize: 5000,
296
+ logLevel: "info"
297
+ },
298
+ source: {
299
+ type: "file",
300
+ path: "/test/package.json"
301
+ }
302
+ };
303
+ const installedSkills = [
304
+ {
305
+ metadata: {
306
+ name: "file-manager",
307
+ description: "File management skill",
308
+ requiresMcpServers: [
309
+ {
310
+ name: "filesystem",
311
+ description: "File system access",
312
+ command: "npx",
313
+ args: [
314
+ "-y",
315
+ "@modelcontextprotocol/server-filesystem",
316
+ "{{ROOT_PATH}}"
317
+ ],
318
+ parameters: {
319
+ ROOT_PATH: {
320
+ description: "Root directory for file operations",
321
+ required: true,
322
+ default: "/tmp"
323
+ }
324
+ }
325
+ }
326
+ ]
327
+ },
328
+ body: "Skill content"
329
+ }
330
+ ];
331
+ const dependencies = [
332
+ {
333
+ serverName: "filesystem",
334
+ neededBy: ["file-manager"],
335
+ spec: {
336
+ name: "filesystem",
337
+ description: "File system access",
338
+ command: "npx",
339
+ args: [
340
+ "-y",
341
+ "@modelcontextprotocol/server-filesystem",
342
+ "{{ROOT_PATH}}"
343
+ ],
344
+ parameters: {
345
+ ROOT_PATH: {
346
+ description: "Root directory for file operations",
347
+ required: true,
348
+ default: "/tmp"
349
+ }
350
+ }
351
+ }
352
+ }
353
+ ];
354
+ const checkResult = {
355
+ allConfigured: false,
356
+ missing: dependencies,
357
+ configured: []
358
+ };
359
+ mockConfigManager.loadConfig.mockResolvedValue(config);
360
+ mockInstaller.install.mockResolvedValue({
361
+ success: true,
362
+ name: "file-manager",
363
+ spec: "github:user/file-manager#v1.0.0",
364
+ resolvedVersion: "1.0.0",
365
+ integrity: "sha512-abc123",
366
+ installPath: "/test/.agentskills/skills/file-manager"
367
+ });
368
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
369
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
370
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
371
+ // Mock user input
372
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/user/files" });
373
+ // Execute with --with-mcp flag
374
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
375
+ // Verify prompting occurred
376
+ expect(mockInquirer.prompt).toHaveBeenCalled();
377
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalled();
378
+ expect(processExitSpy).toHaveBeenCalledWith(0);
379
+ });
380
+ it("should display parameter description in prompt", async () => {
381
+ // Setup
382
+ const config = {
383
+ skills: {
384
+ "github-helper": "github:user/github-helper#v1.0.0"
385
+ },
386
+ config: {
387
+ skillsDirectory: ".agentskills/skills",
388
+ autoDiscover: [],
389
+ maxSkillSize: 5000,
390
+ logLevel: "info"
391
+ },
392
+ source: {
393
+ type: "file",
394
+ path: "/test/package.json"
395
+ }
396
+ };
397
+ const installedSkills = [
398
+ {
399
+ metadata: {
400
+ name: "github-helper",
401
+ description: "GitHub operations",
402
+ requiresMcpServers: [
403
+ {
404
+ name: "github",
405
+ description: "GitHub API access",
406
+ command: "npx",
407
+ args: ["-y", "@modelcontextprotocol/server-github"],
408
+ env: {
409
+ GITHUB_TOKEN: "{{API_TOKEN}}"
410
+ },
411
+ parameters: {
412
+ API_TOKEN: {
413
+ description: "GitHub personal access token with repo scope",
414
+ required: true,
415
+ sensitive: true,
416
+ example: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
417
+ }
418
+ }
419
+ }
420
+ ]
421
+ },
422
+ body: "Skill content"
423
+ }
424
+ ];
425
+ const dependencies = [
426
+ {
427
+ serverName: "github",
428
+ neededBy: ["github-helper"],
429
+ spec: {
430
+ name: "github",
431
+ description: "GitHub API access",
432
+ command: "npx",
433
+ args: ["-y", "@modelcontextprotocol/server-github"],
434
+ env: {
435
+ GITHUB_TOKEN: "{{API_TOKEN}}"
436
+ },
437
+ parameters: {
438
+ API_TOKEN: {
439
+ description: "GitHub personal access token with repo scope",
440
+ required: true,
441
+ sensitive: true,
442
+ example: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
443
+ }
444
+ }
445
+ }
446
+ }
447
+ ];
448
+ const checkResult = {
449
+ allConfigured: false,
450
+ missing: dependencies,
451
+ configured: []
452
+ };
453
+ mockConfigManager.loadConfig.mockResolvedValue(config);
454
+ mockInstaller.install.mockResolvedValue({
455
+ success: true,
456
+ name: "github-helper",
457
+ spec: "github:user/github-helper#v1.0.0",
458
+ resolvedVersion: "1.0.0",
459
+ integrity: "sha512-abc123",
460
+ installPath: "/test/.agentskills/skills/github-helper"
461
+ });
462
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
463
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
464
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
465
+ // Mock user input
466
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_secret_token" });
467
+ // Execute with --with-mcp flag
468
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
469
+ // Verify prompt contains description
470
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
471
+ expect.objectContaining({
472
+ message: expect.stringContaining("GitHub personal access token with repo scope")
473
+ })
474
+ ]));
475
+ });
476
+ it("should show default value in prompt", async () => {
477
+ // Setup
478
+ const config = {
479
+ skills: {
480
+ "file-manager": "github:user/file-manager#v1.0.0"
481
+ },
482
+ config: {
483
+ skillsDirectory: ".agentskills/skills",
484
+ autoDiscover: [],
485
+ maxSkillSize: 5000,
486
+ logLevel: "info"
487
+ },
488
+ source: {
489
+ type: "file",
490
+ path: "/test/package.json"
491
+ }
492
+ };
493
+ const installedSkills = [
494
+ {
495
+ metadata: {
496
+ name: "file-manager",
497
+ description: "File management skill",
498
+ requiresMcpServers: [
499
+ {
500
+ name: "filesystem",
501
+ description: "File system access",
502
+ command: "npx",
503
+ args: [
504
+ "-y",
505
+ "@modelcontextprotocol/server-filesystem",
506
+ "{{ROOT_PATH}}"
507
+ ],
508
+ parameters: {
509
+ ROOT_PATH: {
510
+ description: "Root directory for file operations",
511
+ required: true,
512
+ default: "/tmp"
513
+ }
514
+ }
515
+ }
516
+ ]
517
+ },
518
+ body: "Skill content"
519
+ }
520
+ ];
521
+ const dependencies = [
522
+ {
523
+ serverName: "filesystem",
524
+ neededBy: ["file-manager"],
525
+ spec: {
526
+ name: "filesystem",
527
+ description: "File system access",
528
+ command: "npx",
529
+ args: [
530
+ "-y",
531
+ "@modelcontextprotocol/server-filesystem",
532
+ "{{ROOT_PATH}}"
533
+ ],
534
+ parameters: {
535
+ ROOT_PATH: {
536
+ description: "Root directory for file operations",
537
+ required: true,
538
+ default: "/tmp"
539
+ }
540
+ }
541
+ }
542
+ }
543
+ ];
544
+ const checkResult = {
545
+ allConfigured: false,
546
+ missing: dependencies,
547
+ configured: []
548
+ };
549
+ mockConfigManager.loadConfig.mockResolvedValue(config);
550
+ mockInstaller.install.mockResolvedValue({
551
+ success: true,
552
+ name: "file-manager",
553
+ spec: "github:user/file-manager#v1.0.0",
554
+ resolvedVersion: "1.0.0",
555
+ integrity: "sha512-abc123",
556
+ installPath: "/test/.agentskills/skills/file-manager"
557
+ });
558
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
559
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
560
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
561
+ // Mock user accepts default
562
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/tmp" });
563
+ // Execute with --with-mcp flag
564
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
565
+ // Verify prompt shows default
566
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
567
+ expect.objectContaining({
568
+ default: "/tmp"
569
+ })
570
+ ]));
571
+ });
572
+ });
573
+ describe("Parameter substitution", () => {
574
+ it("should substitute parameters in args with user values", async () => {
575
+ // Setup
576
+ const config = {
577
+ skills: {
578
+ "file-manager": "github:user/file-manager#v1.0.0"
579
+ },
580
+ config: {
581
+ skillsDirectory: ".agentskills/skills",
582
+ autoDiscover: [],
583
+ maxSkillSize: 5000,
584
+ logLevel: "info"
585
+ },
586
+ source: {
587
+ type: "file",
588
+ path: "/test/package.json"
589
+ }
590
+ };
591
+ const installedSkills = [
592
+ {
593
+ metadata: {
594
+ name: "file-manager",
595
+ description: "File management skill",
596
+ requiresMcpServers: [
597
+ {
598
+ name: "filesystem",
599
+ description: "File system access",
600
+ command: "npx",
601
+ args: [
602
+ "-y",
603
+ "@modelcontextprotocol/server-filesystem",
604
+ "{{ROOT_PATH}}"
605
+ ],
606
+ parameters: {
607
+ ROOT_PATH: {
608
+ description: "Root directory for file operations",
609
+ required: true,
610
+ default: "/tmp"
611
+ }
612
+ }
613
+ }
614
+ ]
615
+ },
616
+ body: "Skill content"
617
+ }
618
+ ];
619
+ const dependencies = [
620
+ {
621
+ serverName: "filesystem",
622
+ neededBy: ["file-manager"],
623
+ spec: {
624
+ name: "filesystem",
625
+ description: "File system access",
626
+ command: "npx",
627
+ args: [
628
+ "-y",
629
+ "@modelcontextprotocol/server-filesystem",
630
+ "{{ROOT_PATH}}"
631
+ ],
632
+ parameters: {
633
+ ROOT_PATH: {
634
+ description: "Root directory for file operations",
635
+ required: true,
636
+ default: "/tmp"
637
+ }
638
+ }
639
+ }
640
+ }
641
+ ];
642
+ const checkResult = {
643
+ allConfigured: false,
644
+ missing: dependencies,
645
+ configured: []
646
+ };
647
+ mockConfigManager.loadConfig.mockResolvedValue(config);
648
+ mockInstaller.install.mockResolvedValue({
649
+ success: true,
650
+ name: "file-manager",
651
+ spec: "github:user/file-manager#v1.0.0",
652
+ resolvedVersion: "1.0.0",
653
+ integrity: "sha512-abc123",
654
+ installPath: "/test/.agentskills/skills/file-manager"
655
+ });
656
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
657
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
658
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
659
+ // Mock user input
660
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/user/files" });
661
+ // Execute with --with-mcp flag
662
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
663
+ // Verify substituteParameters was called with user values
664
+ expect(substituteParameters).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ ROOT_PATH: "/home/user/files" }));
665
+ });
666
+ it("should substitute parameters in env with user values", async () => {
667
+ // Setup
668
+ const config = {
669
+ skills: {
670
+ "github-helper": "github:user/github-helper#v1.0.0"
671
+ },
672
+ config: {
673
+ skillsDirectory: ".agentskills/skills",
674
+ autoDiscover: [],
675
+ maxSkillSize: 5000,
676
+ logLevel: "info"
677
+ },
678
+ source: {
679
+ type: "file",
680
+ path: "/test/package.json"
681
+ }
682
+ };
683
+ const installedSkills = [
684
+ {
685
+ metadata: {
686
+ name: "github-helper",
687
+ description: "GitHub operations",
688
+ requiresMcpServers: [
689
+ {
690
+ name: "github",
691
+ description: "GitHub API access",
692
+ command: "npx",
693
+ args: ["-y", "@modelcontextprotocol/server-github"],
694
+ env: {
695
+ GITHUB_TOKEN: "{{API_TOKEN}}"
696
+ },
697
+ parameters: {
698
+ API_TOKEN: {
699
+ description: "GitHub personal access token",
700
+ required: true,
701
+ sensitive: true
702
+ }
703
+ }
704
+ }
705
+ ]
706
+ },
707
+ body: "Skill content"
708
+ }
709
+ ];
710
+ const dependencies = [
711
+ {
712
+ serverName: "github",
713
+ neededBy: ["github-helper"],
714
+ spec: {
715
+ name: "github",
716
+ description: "GitHub API access",
717
+ command: "npx",
718
+ args: ["-y", "@modelcontextprotocol/server-github"],
719
+ env: {
720
+ GITHUB_TOKEN: "{{API_TOKEN}}"
721
+ },
722
+ parameters: {
723
+ API_TOKEN: {
724
+ description: "GitHub personal access token",
725
+ required: true,
726
+ sensitive: true
727
+ }
728
+ }
729
+ }
730
+ }
731
+ ];
732
+ const checkResult = {
733
+ allConfigured: false,
734
+ missing: dependencies,
735
+ configured: []
736
+ };
737
+ mockConfigManager.loadConfig.mockResolvedValue(config);
738
+ mockInstaller.install.mockResolvedValue({
739
+ success: true,
740
+ name: "github-helper",
741
+ spec: "github:user/github-helper#v1.0.0",
742
+ resolvedVersion: "1.0.0",
743
+ integrity: "sha512-abc123",
744
+ installPath: "/test/.agentskills/skills/github-helper"
745
+ });
746
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
747
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
748
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
749
+ // Mock user input
750
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_secret_token" });
751
+ // Execute with --with-mcp flag
752
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
753
+ // Verify substituteParameters was called for env vars
754
+ expect(substituteParameters).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ API_TOKEN: "ghp_secret_token" }));
755
+ });
756
+ it("should substitute multiple parameters in complex configuration", async () => {
757
+ // Setup
758
+ const config = {
759
+ skills: {
760
+ "database-tool": "github:user/database-tool#v1.0.0"
761
+ },
762
+ config: {
763
+ skillsDirectory: ".agentskills/skills",
764
+ autoDiscover: [],
765
+ maxSkillSize: 5000,
766
+ logLevel: "info"
767
+ },
768
+ source: {
769
+ type: "file",
770
+ path: "/test/package.json"
771
+ }
772
+ };
773
+ const installedSkills = [
774
+ {
775
+ metadata: {
776
+ name: "database-tool",
777
+ description: "Database operations",
778
+ requiresMcpServers: [
779
+ {
780
+ name: "postgres",
781
+ description: "PostgreSQL database access",
782
+ command: "npx",
783
+ args: [
784
+ "-y",
785
+ "@modelcontextprotocol/server-postgres",
786
+ "--host",
787
+ "{{DB_HOST}}",
788
+ "--port",
789
+ "{{DB_PORT}}"
790
+ ],
791
+ env: {
792
+ POSTGRES_USER: "{{DB_USER}}",
793
+ POSTGRES_PASSWORD: "{{DB_PASSWORD}}"
794
+ },
795
+ parameters: {
796
+ DB_HOST: {
797
+ description: "Database host",
798
+ required: true,
799
+ default: "localhost"
800
+ },
801
+ DB_PORT: {
802
+ description: "Database port",
803
+ required: true,
804
+ default: "5432"
805
+ },
806
+ DB_USER: {
807
+ description: "Database user",
808
+ required: true,
809
+ default: "postgres"
810
+ },
811
+ DB_PASSWORD: {
812
+ description: "Database password",
813
+ required: true,
814
+ sensitive: true
815
+ }
816
+ }
817
+ }
818
+ ]
819
+ },
820
+ body: "Skill content"
821
+ }
822
+ ];
823
+ const dependencies = [
824
+ {
825
+ serverName: "postgres",
826
+ neededBy: ["database-tool"],
827
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
828
+ }
829
+ ];
830
+ const checkResult = {
831
+ allConfigured: false,
832
+ missing: dependencies,
833
+ configured: []
834
+ };
835
+ mockConfigManager.loadConfig.mockResolvedValue(config);
836
+ mockInstaller.install.mockResolvedValue({
837
+ success: true,
838
+ name: "database-tool",
839
+ spec: "github:user/database-tool#v1.0.0",
840
+ resolvedVersion: "1.0.0",
841
+ integrity: "sha512-abc123",
842
+ installPath: "/test/.agentskills/skills/database-tool"
843
+ });
844
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
845
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
846
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
847
+ // Mock user input for all parameters
848
+ mockInquirer.prompt.mockResolvedValue({
849
+ DB_HOST: "db.example.com",
850
+ DB_PORT: "5433",
851
+ DB_USER: "admin",
852
+ DB_PASSWORD: "secret123"
853
+ });
854
+ // Execute with --with-mcp flag
855
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
856
+ // Verify all parameters were substituted
857
+ expect(substituteParameters).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({
858
+ DB_HOST: "db.example.com",
859
+ DB_PORT: "5433",
860
+ DB_USER: "admin",
861
+ DB_PASSWORD: "secret123"
862
+ }));
863
+ });
864
+ });
865
+ describe("Adding servers to MCP config", () => {
866
+ it("should add server to MCP config after prompting", async () => {
867
+ // Setup
868
+ const config = {
869
+ skills: {
870
+ "file-manager": "github:user/file-manager#v1.0.0"
871
+ },
872
+ config: {
873
+ skillsDirectory: ".agentskills/skills",
874
+ autoDiscover: [],
875
+ maxSkillSize: 5000,
876
+ logLevel: "info"
877
+ },
878
+ source: {
879
+ type: "file",
880
+ path: "/test/package.json"
881
+ }
882
+ };
883
+ const installedSkills = [
884
+ {
885
+ metadata: {
886
+ name: "file-manager",
887
+ description: "File management skill",
888
+ requiresMcpServers: [
889
+ {
890
+ name: "filesystem",
891
+ description: "File system access",
892
+ command: "npx",
893
+ args: [
894
+ "-y",
895
+ "@modelcontextprotocol/server-filesystem",
896
+ "{{ROOT_PATH}}"
897
+ ],
898
+ parameters: {
899
+ ROOT_PATH: {
900
+ description: "Root directory for file operations",
901
+ required: true,
902
+ default: "/tmp"
903
+ }
904
+ }
905
+ }
906
+ ]
907
+ },
908
+ body: "Skill content"
909
+ }
910
+ ];
911
+ const dependencies = [
912
+ {
913
+ serverName: "filesystem",
914
+ neededBy: ["file-manager"],
915
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
916
+ }
917
+ ];
918
+ const checkResult = {
919
+ allConfigured: false,
920
+ missing: dependencies,
921
+ configured: []
922
+ };
923
+ mockConfigManager.loadConfig.mockResolvedValue(config);
924
+ mockInstaller.install.mockResolvedValue({
925
+ success: true,
926
+ name: "file-manager",
927
+ spec: "github:user/file-manager#v1.0.0",
928
+ resolvedVersion: "1.0.0",
929
+ integrity: "sha512-abc123",
930
+ installPath: "/test/.agentskills/skills/file-manager"
931
+ });
932
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
933
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
934
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
935
+ // Mock user input
936
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/user/files" });
937
+ // Execute with --with-mcp flag
938
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
939
+ // Verify addServer was called with substituted config
940
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "filesystem", expect.objectContaining({
941
+ command: "npx",
942
+ args: expect.arrayContaining([
943
+ "-y",
944
+ "@modelcontextprotocol/server-filesystem",
945
+ "/home/user/files"
946
+ ])
947
+ }), "/test");
948
+ });
949
+ it("should add server with env vars to MCP config", async () => {
950
+ // Setup
951
+ const config = {
952
+ skills: {
953
+ "github-helper": "github:user/github-helper#v1.0.0"
954
+ },
955
+ config: {
956
+ skillsDirectory: ".agentskills/skills",
957
+ autoDiscover: [],
958
+ maxSkillSize: 5000,
959
+ logLevel: "info"
960
+ },
961
+ source: {
962
+ type: "file",
963
+ path: "/test/package.json"
964
+ }
965
+ };
966
+ const installedSkills = [
967
+ {
968
+ metadata: {
969
+ name: "github-helper",
970
+ description: "GitHub operations",
971
+ requiresMcpServers: [
972
+ {
973
+ name: "github",
974
+ description: "GitHub API access",
975
+ command: "npx",
976
+ args: ["-y", "@modelcontextprotocol/server-github"],
977
+ env: {
978
+ GITHUB_TOKEN: "{{API_TOKEN}}"
979
+ },
980
+ parameters: {
981
+ API_TOKEN: {
982
+ description: "GitHub personal access token",
983
+ required: true,
984
+ sensitive: true
985
+ }
986
+ }
987
+ }
988
+ ]
989
+ },
990
+ body: "Skill content"
991
+ }
992
+ ];
993
+ const dependencies = [
994
+ {
995
+ serverName: "github",
996
+ neededBy: ["github-helper"],
997
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
998
+ }
999
+ ];
1000
+ const checkResult = {
1001
+ allConfigured: false,
1002
+ missing: dependencies,
1003
+ configured: []
1004
+ };
1005
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1006
+ mockInstaller.install.mockResolvedValue({
1007
+ success: true,
1008
+ name: "github-helper",
1009
+ spec: "github:user/github-helper#v1.0.0",
1010
+ resolvedVersion: "1.0.0",
1011
+ integrity: "sha512-abc123",
1012
+ installPath: "/test/.agentskills/skills/github-helper"
1013
+ });
1014
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1015
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1016
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1017
+ // Mock user input
1018
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_secret_token" });
1019
+ // Execute with --with-mcp flag
1020
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1021
+ // Verify addServer was called with env vars
1022
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "github", expect.objectContaining({
1023
+ command: "npx",
1024
+ args: ["-y", "@modelcontextprotocol/server-github"],
1025
+ env: expect.objectContaining({
1026
+ GITHUB_TOKEN: "ghp_secret_token"
1027
+ })
1028
+ }), "/test");
1029
+ });
1030
+ });
1031
+ describe("Skip already configured servers", () => {
1032
+ it("should skip prompting for servers that are already configured", async () => {
1033
+ // Setup
1034
+ const config = {
1035
+ skills: {
1036
+ "file-manager": "github:user/file-manager#v1.0.0",
1037
+ "file-reader": "github:user/file-reader#v1.0.0"
1038
+ },
1039
+ config: {
1040
+ skillsDirectory: ".agentskills/skills",
1041
+ autoDiscover: [],
1042
+ maxSkillSize: 5000,
1043
+ logLevel: "info"
1044
+ },
1045
+ source: {
1046
+ type: "file",
1047
+ path: "/test/package.json"
1048
+ }
1049
+ };
1050
+ const installedSkills = [
1051
+ {
1052
+ metadata: {
1053
+ name: "file-manager",
1054
+ description: "File management skill",
1055
+ requiresMcpServers: [
1056
+ {
1057
+ name: "filesystem",
1058
+ description: "File system access",
1059
+ command: "npx",
1060
+ args: [
1061
+ "-y",
1062
+ "@modelcontextprotocol/server-filesystem",
1063
+ "{{ROOT_PATH}}"
1064
+ ],
1065
+ parameters: {
1066
+ ROOT_PATH: {
1067
+ description: "Root directory for file operations",
1068
+ required: true,
1069
+ default: "/tmp"
1070
+ }
1071
+ }
1072
+ }
1073
+ ]
1074
+ },
1075
+ body: "Skill content"
1076
+ },
1077
+ {
1078
+ metadata: {
1079
+ name: "file-reader",
1080
+ description: "File reading skill",
1081
+ requiresMcpServers: [
1082
+ {
1083
+ name: "filesystem",
1084
+ description: "File system access",
1085
+ command: "npx",
1086
+ args: [
1087
+ "-y",
1088
+ "@modelcontextprotocol/server-filesystem",
1089
+ "{{ROOT_PATH}}"
1090
+ ],
1091
+ parameters: {
1092
+ ROOT_PATH: {
1093
+ description: "Root directory for file operations",
1094
+ required: true,
1095
+ default: "/tmp"
1096
+ }
1097
+ }
1098
+ }
1099
+ ]
1100
+ },
1101
+ body: "Skill content"
1102
+ }
1103
+ ];
1104
+ const dependencies = [
1105
+ {
1106
+ serverName: "filesystem",
1107
+ neededBy: ["file-manager", "file-reader"],
1108
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1109
+ }
1110
+ ];
1111
+ // Filesystem is already configured
1112
+ const checkResult = {
1113
+ allConfigured: true,
1114
+ missing: [],
1115
+ configured: ["filesystem"]
1116
+ };
1117
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1118
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(true); // agentskills already configured
1119
+ mockInstaller.install
1120
+ .mockResolvedValueOnce({
1121
+ success: true,
1122
+ name: "file-manager",
1123
+ spec: "github:user/file-manager#v1.0.0",
1124
+ resolvedVersion: "1.0.0",
1125
+ integrity: "sha512-abc123",
1126
+ installPath: "/test/.agentskills/skills/file-manager"
1127
+ })
1128
+ .mockResolvedValueOnce({
1129
+ success: true,
1130
+ name: "file-reader",
1131
+ spec: "github:user/file-reader#v1.0.0",
1132
+ resolvedVersion: "1.0.0",
1133
+ integrity: "sha512-def456",
1134
+ installPath: "/test/.agentskills/skills/file-reader"
1135
+ });
1136
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1137
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1138
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1139
+ // Execute with --with-mcp flag
1140
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1141
+ // Verify no prompting occurred since server is already configured
1142
+ // and agentskills server is already configured too
1143
+ expect(mockInquirer.prompt).not.toHaveBeenCalled();
1144
+ expect(mockMCPConfigManager.addServer).not.toHaveBeenCalled();
1145
+ expect(processExitSpy).toHaveBeenCalledWith(0);
1146
+ });
1147
+ it("should only prompt for missing servers when some are configured", async () => {
1148
+ // Setup
1149
+ const config = {
1150
+ skills: {
1151
+ "devops-tool": "github:user/devops-tool#v1.0.0"
1152
+ },
1153
+ config: {
1154
+ skillsDirectory: ".agentskills/skills",
1155
+ autoDiscover: [],
1156
+ maxSkillSize: 5000,
1157
+ logLevel: "info"
1158
+ },
1159
+ source: {
1160
+ type: "file",
1161
+ path: "/test/package.json"
1162
+ }
1163
+ };
1164
+ const installedSkills = [
1165
+ {
1166
+ metadata: {
1167
+ name: "devops-tool",
1168
+ description: "DevOps operations",
1169
+ requiresMcpServers: [
1170
+ {
1171
+ name: "filesystem",
1172
+ description: "File system access",
1173
+ command: "npx",
1174
+ args: [
1175
+ "-y",
1176
+ "@modelcontextprotocol/server-filesystem",
1177
+ "{{ROOT_PATH}}"
1178
+ ],
1179
+ parameters: {
1180
+ ROOT_PATH: {
1181
+ description: "Root directory",
1182
+ required: true,
1183
+ default: "/tmp"
1184
+ }
1185
+ }
1186
+ },
1187
+ {
1188
+ name: "github",
1189
+ description: "GitHub API access",
1190
+ command: "npx",
1191
+ args: ["-y", "@modelcontextprotocol/server-github"],
1192
+ env: {
1193
+ GITHUB_TOKEN: "{{API_TOKEN}}"
1194
+ },
1195
+ parameters: {
1196
+ API_TOKEN: {
1197
+ description: "GitHub token",
1198
+ required: true,
1199
+ sensitive: true
1200
+ }
1201
+ }
1202
+ }
1203
+ ]
1204
+ },
1205
+ body: "Skill content"
1206
+ }
1207
+ ];
1208
+ const allDependencies = [
1209
+ {
1210
+ serverName: "filesystem",
1211
+ neededBy: ["devops-tool"],
1212
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1213
+ },
1214
+ {
1215
+ serverName: "github",
1216
+ neededBy: ["devops-tool"],
1217
+ spec: installedSkills[0].metadata.requiresMcpServers[1]
1218
+ }
1219
+ ];
1220
+ // filesystem is configured, github is missing
1221
+ const checkResult = {
1222
+ allConfigured: false,
1223
+ missing: [allDependencies[1]], // only github is missing
1224
+ configured: ["filesystem"]
1225
+ };
1226
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1227
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(false); // agentskills NOT configured
1228
+ mockInstaller.install.mockResolvedValue({
1229
+ success: true,
1230
+ name: "devops-tool",
1231
+ spec: "github:user/devops-tool#v1.0.0",
1232
+ resolvedVersion: "1.0.0",
1233
+ integrity: "sha512-abc123",
1234
+ installPath: "/test/.agentskills/skills/devops-tool"
1235
+ });
1236
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1237
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(allDependencies);
1238
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1239
+ // Mock user input for github only
1240
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_token" });
1241
+ // Execute with --with-mcp flag
1242
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1243
+ // Verify only github prompted (not filesystem)
1244
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(1);
1245
+ // addServer called twice: once for github, once for agentskills
1246
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledTimes(2);
1247
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "github", expect.any(Object), "/test");
1248
+ });
1249
+ });
1250
+ describe("Handle multiple missing servers", () => {
1251
+ it("should prompt for each missing server sequentially", async () => {
1252
+ // Setup
1253
+ const config = {
1254
+ skills: {
1255
+ "devops-tool": "github:user/devops-tool#v1.0.0"
1256
+ },
1257
+ config: {
1258
+ skillsDirectory: ".agentskills/skills",
1259
+ autoDiscover: [],
1260
+ maxSkillSize: 5000,
1261
+ logLevel: "info"
1262
+ },
1263
+ source: {
1264
+ type: "file",
1265
+ path: "/test/package.json"
1266
+ }
1267
+ };
1268
+ const installedSkills = [
1269
+ {
1270
+ metadata: {
1271
+ name: "devops-tool",
1272
+ description: "DevOps operations",
1273
+ requiresMcpServers: [
1274
+ {
1275
+ name: "filesystem",
1276
+ description: "File system access",
1277
+ command: "npx",
1278
+ args: [
1279
+ "-y",
1280
+ "@modelcontextprotocol/server-filesystem",
1281
+ "{{ROOT_PATH}}"
1282
+ ],
1283
+ parameters: {
1284
+ ROOT_PATH: {
1285
+ description: "Root directory",
1286
+ required: true,
1287
+ default: "/tmp"
1288
+ }
1289
+ }
1290
+ },
1291
+ {
1292
+ name: "github",
1293
+ description: "GitHub API access",
1294
+ command: "npx",
1295
+ args: ["-y", "@modelcontextprotocol/server-github"],
1296
+ env: {
1297
+ GITHUB_TOKEN: "{{API_TOKEN}}"
1298
+ },
1299
+ parameters: {
1300
+ API_TOKEN: {
1301
+ description: "GitHub token",
1302
+ required: true,
1303
+ sensitive: true
1304
+ }
1305
+ }
1306
+ },
1307
+ {
1308
+ name: "slack",
1309
+ description: "Slack API access",
1310
+ command: "npx",
1311
+ args: ["-y", "@modelcontextprotocol/server-slack"],
1312
+ env: {
1313
+ SLACK_TOKEN: "{{SLACK_TOKEN}}"
1314
+ },
1315
+ parameters: {
1316
+ SLACK_TOKEN: {
1317
+ description: "Slack bot token",
1318
+ required: true,
1319
+ sensitive: true
1320
+ }
1321
+ }
1322
+ }
1323
+ ]
1324
+ },
1325
+ body: "Skill content"
1326
+ }
1327
+ ];
1328
+ const dependencies = [
1329
+ {
1330
+ serverName: "filesystem",
1331
+ neededBy: ["devops-tool"],
1332
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1333
+ },
1334
+ {
1335
+ serverName: "github",
1336
+ neededBy: ["devops-tool"],
1337
+ spec: installedSkills[0].metadata.requiresMcpServers[1]
1338
+ },
1339
+ {
1340
+ serverName: "slack",
1341
+ neededBy: ["devops-tool"],
1342
+ spec: installedSkills[0].metadata.requiresMcpServers[2]
1343
+ }
1344
+ ];
1345
+ const checkResult = {
1346
+ allConfigured: false,
1347
+ missing: dependencies,
1348
+ configured: []
1349
+ };
1350
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1351
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(false); // agentskills NOT configured
1352
+ mockInstaller.install.mockResolvedValue({
1353
+ success: true,
1354
+ name: "devops-tool",
1355
+ spec: "github:user/devops-tool#v1.0.0",
1356
+ resolvedVersion: "1.0.0",
1357
+ integrity: "sha512-abc123",
1358
+ installPath: "/test/.agentskills/skills/devops-tool"
1359
+ });
1360
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1361
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1362
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1363
+ // Mock user input for each server
1364
+ mockInquirer.prompt
1365
+ .mockResolvedValueOnce({ ROOT_PATH: "/home/user" })
1366
+ .mockResolvedValueOnce({ API_TOKEN: "ghp_token" })
1367
+ .mockResolvedValueOnce({ SLACK_TOKEN: "xoxb-token" });
1368
+ // Execute with --with-mcp flag
1369
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1370
+ // Verify prompts for all three dependency servers
1371
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(3);
1372
+ // addServer called 4 times: 3 dependency servers + agentskills
1373
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledTimes(4);
1374
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "filesystem", expect.any(Object), "/test");
1375
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "github", expect.any(Object), "/test");
1376
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "slack", expect.any(Object), "/test");
1377
+ });
1378
+ it("should add all missing servers to config", async () => {
1379
+ // Setup
1380
+ const config = {
1381
+ skills: {
1382
+ "multi-tool": "github:user/multi-tool#v1.0.0"
1383
+ },
1384
+ config: {
1385
+ skillsDirectory: ".agentskills/skills",
1386
+ autoDiscover: [],
1387
+ maxSkillSize: 5000,
1388
+ logLevel: "info"
1389
+ },
1390
+ source: {
1391
+ type: "file",
1392
+ path: "/test/package.json"
1393
+ }
1394
+ };
1395
+ const installedSkills = [
1396
+ {
1397
+ metadata: {
1398
+ name: "multi-tool",
1399
+ description: "Multi-server tool",
1400
+ requiresMcpServers: [
1401
+ {
1402
+ name: "filesystem",
1403
+ description: "File system access",
1404
+ command: "npx",
1405
+ args: [
1406
+ "-y",
1407
+ "@modelcontextprotocol/server-filesystem",
1408
+ "{{ROOT_PATH}}"
1409
+ ],
1410
+ parameters: {
1411
+ ROOT_PATH: {
1412
+ description: "Root directory",
1413
+ required: true,
1414
+ default: "/tmp"
1415
+ }
1416
+ }
1417
+ },
1418
+ {
1419
+ name: "github",
1420
+ description: "GitHub API access",
1421
+ command: "npx",
1422
+ args: ["-y", "@modelcontextprotocol/server-github"],
1423
+ env: {
1424
+ GITHUB_TOKEN: "{{API_TOKEN}}"
1425
+ },
1426
+ parameters: {
1427
+ API_TOKEN: {
1428
+ description: "GitHub token",
1429
+ required: true,
1430
+ sensitive: true
1431
+ }
1432
+ }
1433
+ }
1434
+ ]
1435
+ },
1436
+ body: "Skill content"
1437
+ }
1438
+ ];
1439
+ const dependencies = [
1440
+ {
1441
+ serverName: "filesystem",
1442
+ neededBy: ["multi-tool"],
1443
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1444
+ },
1445
+ {
1446
+ serverName: "github",
1447
+ neededBy: ["multi-tool"],
1448
+ spec: installedSkills[0].metadata.requiresMcpServers[1]
1449
+ }
1450
+ ];
1451
+ const checkResult = {
1452
+ allConfigured: false,
1453
+ missing: dependencies,
1454
+ configured: []
1455
+ };
1456
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1457
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(false); // agentskills NOT configured
1458
+ mockInstaller.install.mockResolvedValue({
1459
+ success: true,
1460
+ name: "multi-tool",
1461
+ spec: "github:user/multi-tool#v1.0.0",
1462
+ resolvedVersion: "1.0.0",
1463
+ integrity: "sha512-abc123",
1464
+ installPath: "/test/.agentskills/skills/multi-tool"
1465
+ });
1466
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1467
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1468
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1469
+ // Mock user input
1470
+ mockInquirer.prompt
1471
+ .mockResolvedValueOnce({ ROOT_PATH: "/home/user" })
1472
+ .mockResolvedValueOnce({ API_TOKEN: "ghp_token" });
1473
+ // Execute with --with-mcp flag
1474
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1475
+ // Verify both dependency servers + agentskills server were added
1476
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledTimes(3);
1477
+ expect(processExitSpy).toHaveBeenCalledWith(0);
1478
+ });
1479
+ });
1480
+ describe("Handle user cancellation", () => {
1481
+ it("should handle Ctrl+C gracefully during prompting", async () => {
1482
+ // Setup
1483
+ const config = {
1484
+ skills: {
1485
+ "file-manager": "github:user/file-manager#v1.0.0"
1486
+ },
1487
+ config: {
1488
+ skillsDirectory: ".agentskills/skills",
1489
+ autoDiscover: [],
1490
+ maxSkillSize: 5000,
1491
+ logLevel: "info"
1492
+ },
1493
+ source: {
1494
+ type: "file",
1495
+ path: "/test/package.json"
1496
+ }
1497
+ };
1498
+ const installedSkills = [
1499
+ {
1500
+ metadata: {
1501
+ name: "file-manager",
1502
+ description: "File management skill",
1503
+ requiresMcpServers: [
1504
+ {
1505
+ name: "filesystem",
1506
+ description: "File system access",
1507
+ command: "npx",
1508
+ args: [
1509
+ "-y",
1510
+ "@modelcontextprotocol/server-filesystem",
1511
+ "{{ROOT_PATH}}"
1512
+ ],
1513
+ parameters: {
1514
+ ROOT_PATH: {
1515
+ description: "Root directory for file operations",
1516
+ required: true,
1517
+ default: "/tmp"
1518
+ }
1519
+ }
1520
+ }
1521
+ ]
1522
+ },
1523
+ body: "Skill content"
1524
+ }
1525
+ ];
1526
+ const dependencies = [
1527
+ {
1528
+ serverName: "filesystem",
1529
+ neededBy: ["file-manager"],
1530
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1531
+ }
1532
+ ];
1533
+ const checkResult = {
1534
+ allConfigured: false,
1535
+ missing: dependencies,
1536
+ configured: []
1537
+ };
1538
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1539
+ mockInstaller.install.mockResolvedValue({
1540
+ success: true,
1541
+ name: "file-manager",
1542
+ spec: "github:user/file-manager#v1.0.0",
1543
+ resolvedVersion: "1.0.0",
1544
+ integrity: "sha512-abc123",
1545
+ installPath: "/test/.agentskills/skills/file-manager"
1546
+ });
1547
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1548
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1549
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1550
+ // Simulate user cancellation (Ctrl+C)
1551
+ mockInquirer.prompt.mockRejectedValue(new Error("User cancelled"));
1552
+ // Execute with --with-mcp flag
1553
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1554
+ // Verify graceful handling
1555
+ expect(consoleErrorSpy).toHaveBeenCalled();
1556
+ expect(processExitSpy).toHaveBeenCalledWith(1);
1557
+ });
1558
+ it("should show helpful message on cancellation", async () => {
1559
+ // Setup
1560
+ const config = {
1561
+ skills: {
1562
+ "file-manager": "github:user/file-manager#v1.0.0"
1563
+ },
1564
+ config: {
1565
+ skillsDirectory: ".agentskills/skills",
1566
+ autoDiscover: [],
1567
+ maxSkillSize: 5000,
1568
+ logLevel: "info"
1569
+ },
1570
+ source: {
1571
+ type: "file",
1572
+ path: "/test/package.json"
1573
+ }
1574
+ };
1575
+ const installedSkills = [
1576
+ {
1577
+ metadata: {
1578
+ name: "file-manager",
1579
+ description: "File management skill",
1580
+ requiresMcpServers: [
1581
+ {
1582
+ name: "filesystem",
1583
+ description: "File system access",
1584
+ command: "npx",
1585
+ args: [
1586
+ "-y",
1587
+ "@modelcontextprotocol/server-filesystem",
1588
+ "{{ROOT_PATH}}"
1589
+ ],
1590
+ parameters: {
1591
+ ROOT_PATH: {
1592
+ description: "Root directory",
1593
+ required: true,
1594
+ default: "/tmp"
1595
+ }
1596
+ }
1597
+ }
1598
+ ]
1599
+ },
1600
+ body: "Skill content"
1601
+ }
1602
+ ];
1603
+ const dependencies = [
1604
+ {
1605
+ serverName: "filesystem",
1606
+ neededBy: ["file-manager"],
1607
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1608
+ }
1609
+ ];
1610
+ const checkResult = {
1611
+ allConfigured: false,
1612
+ missing: dependencies,
1613
+ configured: []
1614
+ };
1615
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1616
+ mockInstaller.install.mockResolvedValue({
1617
+ success: true,
1618
+ name: "file-manager",
1619
+ spec: "github:user/file-manager#v1.0.0",
1620
+ resolvedVersion: "1.0.0",
1621
+ integrity: "sha512-abc123",
1622
+ installPath: "/test/.agentskills/skills/file-manager"
1623
+ });
1624
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1625
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1626
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1627
+ // Simulate user cancellation
1628
+ mockInquirer.prompt.mockRejectedValue(new Error("User cancelled"));
1629
+ // Execute with --with-mcp flag
1630
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1631
+ // Verify helpful message
1632
+ const errorCalls = consoleErrorSpy.mock.calls
1633
+ .map((call) => call[0])
1634
+ .join(" ");
1635
+ expect(errorCalls).toContain("cancelled");
1636
+ });
1637
+ });
1638
+ describe("Parameter validation", () => {
1639
+ it("should handle required parameters", async () => {
1640
+ // Setup
1641
+ const config = {
1642
+ skills: {
1643
+ "file-manager": "github:user/file-manager#v1.0.0"
1644
+ },
1645
+ config: {
1646
+ skillsDirectory: ".agentskills/skills",
1647
+ autoDiscover: [],
1648
+ maxSkillSize: 5000,
1649
+ logLevel: "info"
1650
+ },
1651
+ source: {
1652
+ type: "file",
1653
+ path: "/test/package.json"
1654
+ }
1655
+ };
1656
+ const installedSkills = [
1657
+ {
1658
+ metadata: {
1659
+ name: "file-manager",
1660
+ description: "File management skill",
1661
+ requiresMcpServers: [
1662
+ {
1663
+ name: "filesystem",
1664
+ description: "File system access",
1665
+ command: "npx",
1666
+ args: [
1667
+ "-y",
1668
+ "@modelcontextprotocol/server-filesystem",
1669
+ "{{ROOT_PATH}}"
1670
+ ],
1671
+ parameters: {
1672
+ ROOT_PATH: {
1673
+ description: "Root directory for file operations",
1674
+ required: true, // Required parameter
1675
+ default: "/tmp"
1676
+ }
1677
+ }
1678
+ }
1679
+ ]
1680
+ },
1681
+ body: "Skill content"
1682
+ }
1683
+ ];
1684
+ const dependencies = [
1685
+ {
1686
+ serverName: "filesystem",
1687
+ neededBy: ["file-manager"],
1688
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1689
+ }
1690
+ ];
1691
+ const checkResult = {
1692
+ allConfigured: false,
1693
+ missing: dependencies,
1694
+ configured: []
1695
+ };
1696
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1697
+ mockInstaller.install.mockResolvedValue({
1698
+ success: true,
1699
+ name: "file-manager",
1700
+ spec: "github:user/file-manager#v1.0.0",
1701
+ resolvedVersion: "1.0.0",
1702
+ integrity: "sha512-abc123",
1703
+ installPath: "/test/.agentskills/skills/file-manager"
1704
+ });
1705
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1706
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1707
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1708
+ // Mock user input
1709
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/user/files" });
1710
+ // Execute with --with-mcp flag
1711
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1712
+ // Verify required parameter was prompted
1713
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
1714
+ expect.objectContaining({
1715
+ name: "ROOT_PATH",
1716
+ type: "input"
1717
+ })
1718
+ ]));
1719
+ });
1720
+ it("should handle optional parameters with defaults", async () => {
1721
+ // Setup
1722
+ const config = {
1723
+ skills: {
1724
+ "web-server": "github:user/web-server#v1.0.0"
1725
+ },
1726
+ config: {
1727
+ skillsDirectory: ".agentskills/skills",
1728
+ autoDiscover: [],
1729
+ maxSkillSize: 5000,
1730
+ logLevel: "info"
1731
+ },
1732
+ source: {
1733
+ type: "file",
1734
+ path: "/test/package.json"
1735
+ }
1736
+ };
1737
+ const installedSkills = [
1738
+ {
1739
+ metadata: {
1740
+ name: "web-server",
1741
+ description: "Web server skill",
1742
+ requiresMcpServers: [
1743
+ {
1744
+ name: "http",
1745
+ description: "HTTP server",
1746
+ command: "npx",
1747
+ args: [
1748
+ "-y",
1749
+ "@modelcontextprotocol/server-http",
1750
+ "--port",
1751
+ "{{PORT}}"
1752
+ ],
1753
+ parameters: {
1754
+ PORT: {
1755
+ description: "Server port",
1756
+ required: false, // Optional parameter
1757
+ default: "8080"
1758
+ }
1759
+ }
1760
+ }
1761
+ ]
1762
+ },
1763
+ body: "Skill content"
1764
+ }
1765
+ ];
1766
+ const dependencies = [
1767
+ {
1768
+ serverName: "http",
1769
+ neededBy: ["web-server"],
1770
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1771
+ }
1772
+ ];
1773
+ const checkResult = {
1774
+ allConfigured: false,
1775
+ missing: dependencies,
1776
+ configured: []
1777
+ };
1778
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1779
+ mockInstaller.install.mockResolvedValue({
1780
+ success: true,
1781
+ name: "web-server",
1782
+ spec: "github:user/web-server#v1.0.0",
1783
+ resolvedVersion: "1.0.0",
1784
+ integrity: "sha512-abc123",
1785
+ installPath: "/test/.agentskills/skills/web-server"
1786
+ });
1787
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1788
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1789
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1790
+ // Mock user accepts default
1791
+ mockInquirer.prompt.mockResolvedValue({ PORT: "8080" });
1792
+ // Execute with --with-mcp flag
1793
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1794
+ // Verify optional parameter has default
1795
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
1796
+ expect.objectContaining({
1797
+ name: "PORT",
1798
+ default: "8080"
1799
+ })
1800
+ ]));
1801
+ });
1802
+ it("should mark sensitive parameters appropriately", async () => {
1803
+ // Setup
1804
+ const config = {
1805
+ skills: {
1806
+ "github-helper": "github:user/github-helper#v1.0.0"
1807
+ },
1808
+ config: {
1809
+ skillsDirectory: ".agentskills/skills",
1810
+ autoDiscover: [],
1811
+ maxSkillSize: 5000,
1812
+ logLevel: "info"
1813
+ },
1814
+ source: {
1815
+ type: "file",
1816
+ path: "/test/package.json"
1817
+ }
1818
+ };
1819
+ const installedSkills = [
1820
+ {
1821
+ metadata: {
1822
+ name: "github-helper",
1823
+ description: "GitHub operations",
1824
+ requiresMcpServers: [
1825
+ {
1826
+ name: "github",
1827
+ description: "GitHub API access",
1828
+ command: "npx",
1829
+ args: ["-y", "@modelcontextprotocol/server-github"],
1830
+ env: {
1831
+ GITHUB_TOKEN: "{{API_TOKEN}}"
1832
+ },
1833
+ parameters: {
1834
+ API_TOKEN: {
1835
+ description: "GitHub personal access token",
1836
+ required: true,
1837
+ sensitive: true // Sensitive parameter (password)
1838
+ }
1839
+ }
1840
+ }
1841
+ ]
1842
+ },
1843
+ body: "Skill content"
1844
+ }
1845
+ ];
1846
+ const dependencies = [
1847
+ {
1848
+ serverName: "github",
1849
+ neededBy: ["github-helper"],
1850
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1851
+ }
1852
+ ];
1853
+ const checkResult = {
1854
+ allConfigured: false,
1855
+ missing: dependencies,
1856
+ configured: []
1857
+ };
1858
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1859
+ mockInstaller.install.mockResolvedValue({
1860
+ success: true,
1861
+ name: "github-helper",
1862
+ spec: "github:user/github-helper#v1.0.0",
1863
+ resolvedVersion: "1.0.0",
1864
+ integrity: "sha512-abc123",
1865
+ installPath: "/test/.agentskills/skills/github-helper"
1866
+ });
1867
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1868
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1869
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1870
+ // Mock user input
1871
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_secret_token" });
1872
+ // Execute with --with-mcp flag
1873
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1874
+ // Verify sensitive parameter uses password type
1875
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
1876
+ expect.objectContaining({
1877
+ name: "API_TOKEN",
1878
+ type: "password"
1879
+ })
1880
+ ]));
1881
+ });
1882
+ });
1883
+ describe("Environment variable defaults", () => {
1884
+ it("should support {{ENV:VAR}} syntax in defaults", async () => {
1885
+ // Setup
1886
+ const config = {
1887
+ skills: {
1888
+ "file-manager": "github:user/file-manager#v1.0.0"
1889
+ },
1890
+ config: {
1891
+ skillsDirectory: ".agentskills/skills",
1892
+ autoDiscover: [],
1893
+ maxSkillSize: 5000,
1894
+ logLevel: "info"
1895
+ },
1896
+ source: {
1897
+ type: "file",
1898
+ path: "/test/package.json"
1899
+ }
1900
+ };
1901
+ const installedSkills = [
1902
+ {
1903
+ metadata: {
1904
+ name: "file-manager",
1905
+ description: "File management skill",
1906
+ requiresMcpServers: [
1907
+ {
1908
+ name: "filesystem",
1909
+ description: "File system access",
1910
+ command: "npx",
1911
+ args: [
1912
+ "-y",
1913
+ "@modelcontextprotocol/server-filesystem",
1914
+ "{{ROOT_PATH}}"
1915
+ ],
1916
+ parameters: {
1917
+ ROOT_PATH: {
1918
+ description: "Root directory for file operations",
1919
+ required: true,
1920
+ default: "{{ENV:HOME}}" // Use environment variable
1921
+ }
1922
+ }
1923
+ }
1924
+ ]
1925
+ },
1926
+ body: "Skill content"
1927
+ }
1928
+ ];
1929
+ const dependencies = [
1930
+ {
1931
+ serverName: "filesystem",
1932
+ neededBy: ["file-manager"],
1933
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
1934
+ }
1935
+ ];
1936
+ const checkResult = {
1937
+ allConfigured: false,
1938
+ missing: dependencies,
1939
+ configured: []
1940
+ };
1941
+ mockConfigManager.loadConfig.mockResolvedValue(config);
1942
+ mockInstaller.install.mockResolvedValue({
1943
+ success: true,
1944
+ name: "file-manager",
1945
+ spec: "github:user/file-manager#v1.0.0",
1946
+ resolvedVersion: "1.0.0",
1947
+ integrity: "sha512-abc123",
1948
+ installPath: "/test/.agentskills/skills/file-manager"
1949
+ });
1950
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
1951
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
1952
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
1953
+ // Set environment variable
1954
+ process.env.HOME = "/home/testuser";
1955
+ // Mock user accepts env default
1956
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/testuser" });
1957
+ // Execute with --with-mcp flag
1958
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
1959
+ // Verify env var was resolved in default
1960
+ expect(mockInquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
1961
+ expect.objectContaining({
1962
+ default: "/home/testuser"
1963
+ })
1964
+ ]));
1965
+ // Cleanup
1966
+ delete process.env.HOME;
1967
+ });
1968
+ it("should handle missing environment variables gracefully", async () => {
1969
+ // Setup
1970
+ const config = {
1971
+ skills: {
1972
+ "api-tool": "github:user/api-tool#v1.0.0"
1973
+ },
1974
+ config: {
1975
+ skillsDirectory: ".agentskills/skills",
1976
+ autoDiscover: [],
1977
+ maxSkillSize: 5000,
1978
+ logLevel: "info"
1979
+ },
1980
+ source: {
1981
+ type: "file",
1982
+ path: "/test/package.json"
1983
+ }
1984
+ };
1985
+ const installedSkills = [
1986
+ {
1987
+ metadata: {
1988
+ name: "api-tool",
1989
+ description: "API tool",
1990
+ requiresMcpServers: [
1991
+ {
1992
+ name: "api",
1993
+ description: "API server",
1994
+ command: "npx",
1995
+ args: ["-y", "@modelcontextprotocol/server-api"],
1996
+ env: {
1997
+ API_KEY: "{{API_KEY}}"
1998
+ },
1999
+ parameters: {
2000
+ API_KEY: {
2001
+ description: "API key",
2002
+ required: true,
2003
+ sensitive: true,
2004
+ default: "{{ENV:NONEXISTENT_VAR}}" // Missing env var
2005
+ }
2006
+ }
2007
+ }
2008
+ ]
2009
+ },
2010
+ body: "Skill content"
2011
+ }
2012
+ ];
2013
+ const dependencies = [
2014
+ {
2015
+ serverName: "api",
2016
+ neededBy: ["api-tool"],
2017
+ spec: installedSkills[0].metadata.requiresMcpServers[0]
2018
+ }
2019
+ ];
2020
+ const checkResult = {
2021
+ allConfigured: false,
2022
+ missing: dependencies,
2023
+ configured: []
2024
+ };
2025
+ mockConfigManager.loadConfig.mockResolvedValue(config);
2026
+ mockInstaller.install.mockResolvedValue({
2027
+ success: true,
2028
+ name: "api-tool",
2029
+ spec: "github:user/api-tool#v1.0.0",
2030
+ resolvedVersion: "1.0.0",
2031
+ integrity: "sha512-abc123",
2032
+ installPath: "/test/.agentskills/skills/api-tool"
2033
+ });
2034
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
2035
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
2036
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
2037
+ // Mock user provides value
2038
+ mockInquirer.prompt.mockResolvedValue({ API_KEY: "user-provided-key" });
2039
+ // Execute with --with-mcp flag
2040
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
2041
+ // Verify graceful handling (no default or warning about missing env var)
2042
+ expect(mockInquirer.prompt).toHaveBeenCalled();
2043
+ expect(processExitSpy).toHaveBeenCalledWith(0);
2044
+ });
2045
+ });
2046
+ describe("Multiple skills needing same server", () => {
2047
+ it("should only prompt once for server needed by multiple skills", async () => {
2048
+ // Setup
2049
+ const config = {
2050
+ skills: {
2051
+ "file-manager": "github:user/file-manager#v1.0.0",
2052
+ "file-reader": "github:user/file-reader#v1.0.0",
2053
+ "file-writer": "github:user/file-writer#v1.0.0"
2054
+ },
2055
+ config: {
2056
+ skillsDirectory: ".agentskills/skills",
2057
+ autoDiscover: [],
2058
+ maxSkillSize: 5000,
2059
+ logLevel: "info"
2060
+ },
2061
+ source: {
2062
+ type: "file",
2063
+ path: "/test/package.json"
2064
+ }
2065
+ };
2066
+ const fileServerSpec = {
2067
+ name: "filesystem",
2068
+ description: "File system access",
2069
+ command: "npx",
2070
+ args: [
2071
+ "-y",
2072
+ "@modelcontextprotocol/server-filesystem",
2073
+ "{{ROOT_PATH}}"
2074
+ ],
2075
+ parameters: {
2076
+ ROOT_PATH: {
2077
+ description: "Root directory for file operations",
2078
+ required: true,
2079
+ default: "/tmp"
2080
+ }
2081
+ }
2082
+ };
2083
+ const installedSkills = [
2084
+ {
2085
+ metadata: {
2086
+ name: "file-manager",
2087
+ description: "File management skill",
2088
+ requiresMcpServers: [fileServerSpec]
2089
+ },
2090
+ body: "Skill content"
2091
+ },
2092
+ {
2093
+ metadata: {
2094
+ name: "file-reader",
2095
+ description: "File reading skill",
2096
+ requiresMcpServers: [fileServerSpec]
2097
+ },
2098
+ body: "Skill content"
2099
+ },
2100
+ {
2101
+ metadata: {
2102
+ name: "file-writer",
2103
+ description: "File writing skill",
2104
+ requiresMcpServers: [fileServerSpec]
2105
+ },
2106
+ body: "Skill content"
2107
+ }
2108
+ ];
2109
+ // All three skills need the same filesystem server
2110
+ const dependencies = [
2111
+ {
2112
+ serverName: "filesystem",
2113
+ neededBy: ["file-manager", "file-reader", "file-writer"],
2114
+ spec: fileServerSpec
2115
+ }
2116
+ ];
2117
+ const checkResult = {
2118
+ allConfigured: false,
2119
+ missing: dependencies,
2120
+ configured: []
2121
+ };
2122
+ mockConfigManager.loadConfig.mockResolvedValue(config);
2123
+ mockMCPConfigManager.isServerConfigured.mockResolvedValue(false); // agentskills NOT configured
2124
+ mockInstaller.install
2125
+ .mockResolvedValueOnce({
2126
+ success: true,
2127
+ name: "file-manager",
2128
+ spec: "github:user/file-manager#v1.0.0",
2129
+ resolvedVersion: "1.0.0",
2130
+ integrity: "sha512-abc123",
2131
+ installPath: "/test/.agentskills/skills/file-manager"
2132
+ })
2133
+ .mockResolvedValueOnce({
2134
+ success: true,
2135
+ name: "file-reader",
2136
+ spec: "github:user/file-reader#v1.0.0",
2137
+ resolvedVersion: "1.0.0",
2138
+ integrity: "sha512-def456",
2139
+ installPath: "/test/.agentskills/skills/file-reader"
2140
+ })
2141
+ .mockResolvedValueOnce({
2142
+ success: true,
2143
+ name: "file-writer",
2144
+ spec: "github:user/file-writer#v1.0.0",
2145
+ resolvedVersion: "1.0.0",
2146
+ integrity: "sha512-ghi789",
2147
+ installPath: "/test/.agentskills/skills/file-writer"
2148
+ });
2149
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
2150
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
2151
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
2152
+ // Mock user input
2153
+ mockInquirer.prompt.mockResolvedValue({ ROOT_PATH: "/home/user/files" });
2154
+ // Execute with --with-mcp flag
2155
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
2156
+ // Verify prompted only ONCE despite three skills needing it
2157
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(1);
2158
+ // addServer called twice: once for filesystem, once for agentskills
2159
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledTimes(2);
2160
+ expect(mockMCPConfigManager.addServer).toHaveBeenCalledWith("claude-desktop", "filesystem", expect.any(Object), "/test");
2161
+ });
2162
+ it("should show all dependent skills in prompt message", async () => {
2163
+ // Setup
2164
+ const config = {
2165
+ skills: {
2166
+ "skill-a": "github:user/skill-a#v1.0.0",
2167
+ "skill-b": "github:user/skill-b#v1.0.0"
2168
+ },
2169
+ config: {
2170
+ skillsDirectory: ".agentskills/skills",
2171
+ autoDiscover: [],
2172
+ maxSkillSize: 5000,
2173
+ logLevel: "info"
2174
+ },
2175
+ source: {
2176
+ type: "file",
2177
+ path: "/test/package.json"
2178
+ }
2179
+ };
2180
+ const githubSpec = {
2181
+ name: "github",
2182
+ description: "GitHub API access",
2183
+ command: "npx",
2184
+ args: ["-y", "@modelcontextprotocol/server-github"],
2185
+ env: {
2186
+ GITHUB_TOKEN: "{{API_TOKEN}}"
2187
+ },
2188
+ parameters: {
2189
+ API_TOKEN: {
2190
+ description: "GitHub personal access token",
2191
+ required: true,
2192
+ sensitive: true
2193
+ }
2194
+ }
2195
+ };
2196
+ const installedSkills = [
2197
+ {
2198
+ metadata: {
2199
+ name: "skill-a",
2200
+ description: "Skill A",
2201
+ requiresMcpServers: [githubSpec]
2202
+ },
2203
+ body: "Skill content"
2204
+ },
2205
+ {
2206
+ metadata: {
2207
+ name: "skill-b",
2208
+ description: "Skill B",
2209
+ requiresMcpServers: [githubSpec]
2210
+ },
2211
+ body: "Skill content"
2212
+ }
2213
+ ];
2214
+ const dependencies = [
2215
+ {
2216
+ serverName: "github",
2217
+ neededBy: ["skill-a", "skill-b"],
2218
+ spec: githubSpec
2219
+ }
2220
+ ];
2221
+ const checkResult = {
2222
+ allConfigured: false,
2223
+ missing: dependencies,
2224
+ configured: []
2225
+ };
2226
+ mockConfigManager.loadConfig.mockResolvedValue(config);
2227
+ mockInstaller.install
2228
+ .mockResolvedValueOnce({
2229
+ success: true,
2230
+ name: "skill-a",
2231
+ spec: "github:user/skill-a#v1.0.0",
2232
+ resolvedVersion: "1.0.0",
2233
+ integrity: "sha512-abc123",
2234
+ installPath: "/test/.agentskills/skills/skill-a"
2235
+ })
2236
+ .mockResolvedValueOnce({
2237
+ success: true,
2238
+ name: "skill-b",
2239
+ spec: "github:user/skill-b#v1.0.0",
2240
+ resolvedVersion: "1.0.0",
2241
+ integrity: "sha512-def456",
2242
+ installPath: "/test/.agentskills/skills/skill-b"
2243
+ });
2244
+ mockInstaller.loadInstalledSkills.mockResolvedValue(installedSkills);
2245
+ mockMCPDependencyChecker.collectDependencies.mockReturnValue(dependencies);
2246
+ mockMCPDependencyChecker.checkDependencies.mockResolvedValue(checkResult);
2247
+ // Mock user input
2248
+ mockInquirer.prompt.mockResolvedValue({ API_TOKEN: "ghp_token" });
2249
+ // Execute with --with-mcp flag
2250
+ await installCommand({ cwd: "/test", withMcp: true, agent: "claude" });
2251
+ // Verify prompt mentions both skills
2252
+ // Note: Implementation should show "needed by: skill-a, skill-b"
2253
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("skill-a"));
2254
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("skill-b"));
2255
+ });
2256
+ });
2257
+ });
2258
+ //# sourceMappingURL=install-with-mcp.test.js.map