@codemcp/agentskills-cli 0.0.7 → 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.
- package/dist/__tests__/install-agentskills-mcp.test.d.ts +12 -0
- package/dist/__tests__/install-agentskills-mcp.test.d.ts.map +1 -0
- package/dist/__tests__/install-agentskills-mcp.test.js +848 -0
- package/dist/__tests__/install-agentskills-mcp.test.js.map +1 -0
- package/dist/__tests__/install-mcp-validation.test.d.ts +9 -0
- package/dist/__tests__/install-mcp-validation.test.d.ts.map +1 -0
- package/dist/__tests__/install-mcp-validation.test.js +1495 -0
- package/dist/__tests__/install-mcp-validation.test.js.map +1 -0
- package/dist/__tests__/install-with-mcp.test.d.ts +12 -0
- package/dist/__tests__/install-with-mcp.test.d.ts.map +1 -0
- package/dist/__tests__/install-with-mcp.test.js +2258 -0
- package/dist/__tests__/install-with-mcp.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +305 -9
- package/dist/commands/install.js.map +1 -1
- package/package.json +7 -2
|
@@ -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
|