a11y-devkit-deploy 0.7.2 → 0.8.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/config/a11y.json CHANGED
@@ -3,31 +3,45 @@
3
3
  "readmeTemplate": "deploy-README.md",
4
4
  "skills": [
5
5
  {
6
- "name": "a11y-base-web-skill",
6
+ "name": "A11y Base Web Skill",
7
+ "npmName": "a11y-base-web-skill",
8
+ "npmURL": "https://www.npmjs.com/package/a11y-base-web-skill",
7
9
  "description": "Core accessibility testing utilities"
8
10
  },
9
11
  {
10
- "name": "a11y-issue-writer-skill",
12
+ "name": "A11y Issue Writer Skill",
13
+ "npmName": "a11y-issue-writer-skill",
14
+ "npmURL": "https://www.npmjs.com/package/a11y-issue-writer-skill",
11
15
  "description": "Document accessibility issues"
12
16
  },
13
17
  {
14
- "name": "a11y-tester-skill",
18
+ "name": "A11y Tester Skill",
19
+ "npmName": "a11y-tester-skill",
20
+ "npmURL": "https://www.npmjs.com/package/a11y-tester-skill",
15
21
  "description": "Run accessibility tests"
16
22
  },
17
23
  {
18
- "name": "a11y-remediator-skill",
24
+ "name": "A11y Remediator Skill",
25
+ "npmName": "a11y-remediator-skill",
26
+ "npmURL": "https://www.npmjs.com/package/a11y-remediator-skill",
19
27
  "description": "Fix accessibility issues"
20
28
  },
21
29
  {
22
- "name": "a11y-validator-skill",
30
+ "name": "A11y Validator Skill",
31
+ "npmName": "a11y-validator-skill",
32
+ "npmURL": "https://www.npmjs.com/package/a11y-validator-skill",
23
33
  "description": "Validate accessibility compliance"
24
34
  },
25
35
  {
26
- "name": "web-standards-skill",
36
+ "name": "Web Standards Skill",
37
+ "npmName": "web-standards-skill",
38
+ "npmURL": "https://www.npmjs.com/package/web-standards-skill",
27
39
  "description": "Web standards reference"
28
40
  },
29
41
  {
30
- "name": "a11y-audit-fix-agent-orchestrator-skill",
42
+ "name": "A11y Audit Fix Agent Orchestrator Skill",
43
+ "npmName": "a11y-audit-fix-agent-orchestrator-skill",
44
+ "npmURL": "https://www.npmjs.com/package/a11y-audit-fix-agent-orchestrator-skill",
31
45
  "description": "Orchestrate accessibility audits"
32
46
  }
33
47
  ],
@@ -58,7 +72,17 @@
58
72
  "displayName": "VSCode",
59
73
  "mcpServerKey": "servers",
60
74
  "skillsFolder": ".github/skills",
61
- "mcpConfigFile": ".github/mcp.json"
75
+ "mcpConfigFile": ".github/mcp.json",
76
+ "globalPaths": {
77
+ "Win": {
78
+ "mcpConfigFile": "Code/User/mcp.json",
79
+ "skillsFolder": "Code/User/skills"
80
+ },
81
+ "macOS": {
82
+ "mcpConfigFile": "Code/User/mcp.json",
83
+ "skillsFolder": "Code/User/skills"
84
+ }
85
+ }
62
86
  },
63
87
  {
64
88
  "id": "windsurf",
@@ -106,5 +130,63 @@
106
130
  "command": "npx",
107
131
  "args": ["-y", "arc-issues-mcp"]
108
132
  }
133
+ ],
134
+ "profiles": [
135
+ {
136
+ "id": "developer",
137
+ "displayName": "Developer",
138
+ "description": "For developers building accessible applications",
139
+ "skills": ["a11y-base-web-skill"],
140
+ "mcpServers": ["magentaa11y"]
141
+ },
142
+ {
143
+ "id": "tester",
144
+ "displayName": "Tester/QA",
145
+ "description": "For QA engineers testing accessibility",
146
+ "skills": ["a11y-tester-skill"],
147
+ "mcpServers": ["arc-issues"]
148
+ },
149
+ {
150
+ "id": "a11y-sme",
151
+ "displayName": "Accessibility SME",
152
+ "description": "For accessibility subject matter experts",
153
+ "skills": ["web-standards-skill"],
154
+ "mcpServers": ["magentaa11y"]
155
+ },
156
+ {
157
+ "id": "product",
158
+ "displayName": "Product",
159
+ "description": "For product managers and owners",
160
+ "skills": ["a11y-base-web-skill"],
161
+ "mcpServers": ["magentaa11y"]
162
+ },
163
+ {
164
+ "id": "design",
165
+ "displayName": "Design",
166
+ "description": "For designers creating accessible experiences",
167
+ "skills": ["web-standards-skill"],
168
+ "mcpServers": ["aria"]
169
+ },
170
+ {
171
+ "id": "hybrid",
172
+ "displayName": "Hybrid (I want all the things)",
173
+ "description": "All skills and MCP servers for comprehensive accessibility support",
174
+ "skills": [
175
+ "a11y-base-web-skill",
176
+ "a11y-issue-writer-skill",
177
+ "a11y-tester-skill",
178
+ "a11y-remediator-skill",
179
+ "a11y-validator-skill",
180
+ "web-standards-skill",
181
+ "a11y-audit-fix-agent-orchestrator-skill"
182
+ ],
183
+ "mcpServers": [
184
+ "wcag",
185
+ "aria",
186
+ "magentaa11y",
187
+ "a11y-personas",
188
+ "arc-issues"
189
+ ]
190
+ }
109
191
  ]
110
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a11y-devkit-deploy",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "CLI to deploy a11y skills and MCP servers across IDEs",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -4,9 +4,11 @@ import { fileURLToPath } from "url";
4
4
  import prompts from "prompts";
5
5
 
6
6
  import { header, info, warn, success, startSpinner, formatPath } from "./ui.js";
7
- import { getPlatform, getIdePaths, getTempDir } from "./paths.js";
7
+ import { getPlatform, getIdePaths, getTempDir, getMcpRepoDir } from "./paths.js";
8
8
  import { installSkillsFromNpm, cleanupTemp } from "./installers/skills.js";
9
9
  import { installMcpConfig } from "./installers/mcp.js";
10
+ import { getGitMcpPrompts, parseArgsString } from "./prompts/git-mcp.js";
11
+ import { installGitMcp } from "./installers/git-mcp.js";
10
12
 
11
13
  const __filename = fileURLToPath(import.meta.url);
12
14
  const __dirname = path.dirname(__filename);
@@ -32,6 +34,7 @@ function parseArgs(argv) {
32
34
  : args.has("--local")
33
35
  ? "local"
34
36
  : null,
37
+ gitMcp: args.has("--git-mcp"),
35
38
  };
36
39
  }
37
40
 
@@ -52,26 +55,115 @@ async function run() {
52
55
 
53
56
  header(
54
57
  `A11y Devkit Deploy v${pkg.version}`,
55
- "Install skills + MCP servers across IDEs",
58
+ args.gitMcp
59
+ ? "Install MCP server from Git repository"
60
+ : "Install skills + MCP servers across IDEs",
56
61
  );
57
62
  info(`Detected OS: ${formatOs(platformInfo)}`);
58
63
 
59
- console.log("\nSkills to install:");
60
- config.skills.forEach((skill) => {
61
- const name = typeof skill === "string" ? skill : skill.name;
62
- const description =
63
- typeof skill === "string"
64
- ? "No description"
65
- : skill.description || "No description";
66
- console.log(`- ${name}: ${description}`);
67
- });
64
+ // Branch to Git MCP installation flow
65
+ if (args.gitMcp) {
66
+ await runGitMcpInstallation(projectRoot, platformInfo, config, idePaths, args);
67
+ return;
68
+ }
68
69
 
69
- console.log("\nMCP Servers to install:");
70
- config.mcpServers.forEach((server) => {
71
- const description = server.description || "No description";
72
- console.log(`${server.name} - ${description}`);
73
- });
74
- console.log("");
70
+ // Prompt for profile selection
71
+ let selectedProfile = null;
72
+ let skillsToInstall = [];
73
+ let mcpServersToInstall = [];
74
+ let profileConfirmed = false;
75
+
76
+ while (!profileConfirmed) {
77
+ if (!args.autoYes && config.profiles) {
78
+ const profileChoices = config.profiles.map((profile) => ({
79
+ title: profile.displayName,
80
+ description: profile.description,
81
+ value: profile.id,
82
+ }));
83
+
84
+ const profileResponse = await prompts(
85
+ {
86
+ type: "select",
87
+ name: "profile",
88
+ message: "Select your profile:",
89
+ choices: profileChoices,
90
+ },
91
+ {
92
+ onCancel: () => {
93
+ warn("Setup cancelled.");
94
+ process.exit(0);
95
+ },
96
+ },
97
+ );
98
+
99
+ selectedProfile = config.profiles.find((p) => p.id === profileResponse.profile);
100
+
101
+ if (selectedProfile) {
102
+ // Filter skills based on profile
103
+ skillsToInstall = config.skills.filter((skill) => {
104
+ const skillNpmName = typeof skill === "string" ? skill : skill.npmName;
105
+ return selectedProfile.skills.includes(skillNpmName);
106
+ });
107
+
108
+ // Filter MCP servers based on profile
109
+ mcpServersToInstall = config.mcpServers.filter((server) =>
110
+ selectedProfile.mcpServers.includes(server.name),
111
+ );
112
+
113
+ console.log(`\n${selectedProfile.displayName} profile selected`);
114
+ }
115
+ } else {
116
+ // If no profiles or auto-yes, use all skills and servers
117
+ skillsToInstall = config.skills;
118
+ mcpServersToInstall = config.mcpServers;
119
+ profileConfirmed = true; // Skip confirmation for auto-yes
120
+ }
121
+
122
+ // Show what will be installed
123
+ if (!args.autoYes) {
124
+ console.log("\nSkills to install:");
125
+ skillsToInstall.forEach((skill) => {
126
+ const name = typeof skill === "string" ? skill : skill.name;
127
+ const description =
128
+ typeof skill === "string"
129
+ ? "No description"
130
+ : skill.description || "No description";
131
+ console.log(` • ${name}`);
132
+ console.log(` ${description}`);
133
+ });
134
+
135
+ console.log("\nMCP Servers to install:");
136
+ mcpServersToInstall.forEach((server) => {
137
+ const description = server.description || "No description";
138
+ console.log(` • ${server.name}`);
139
+ console.log(` ${description}`);
140
+ });
141
+ console.log("");
142
+
143
+ // Confirmation prompt
144
+ const confirmResponse = await prompts(
145
+ {
146
+ type: "confirm",
147
+ name: "continue",
148
+ message: "Continue with this configuration?",
149
+ initial: true,
150
+ },
151
+ {
152
+ onCancel: () => {
153
+ warn("Setup cancelled.");
154
+ process.exit(0);
155
+ },
156
+ },
157
+ );
158
+
159
+ if (confirmResponse.continue) {
160
+ profileConfirmed = true;
161
+ } else {
162
+ // User wants to go back - loop will restart profile selection
163
+ console.log("");
164
+ }
165
+ }
166
+ }
75
167
 
76
168
  const ideChoices = config.ides.map((ide) => ({
77
169
  title: ide.displayName,
@@ -81,7 +173,6 @@ async function run() {
81
173
  let scope = args.scope;
82
174
  let mcpScope = null;
83
175
  let ideSelection = config.ides.map((ide) => ide.id);
84
- let installSkills = true;
85
176
 
86
177
  if (!args.autoYes) {
87
178
  const response = await prompts(
@@ -89,7 +180,7 @@ async function run() {
89
180
  {
90
181
  type: scope ? null : "select",
91
182
  name: "scope",
92
- message: "Install skills + repo locally or globally?",
183
+ message: "Install skills locally or globally?",
93
184
  choices: [
94
185
  {
95
186
  title: `Local to this project (${formatPath(projectRoot)})`,
@@ -121,18 +212,10 @@ async function run() {
121
212
  {
122
213
  type: "multiselect",
123
214
  name: "ides",
124
- message: "Configure MCP for which IDEs?",
215
+ message: "Configure for which IDEs?",
125
216
  choices: ideChoices,
126
217
  initial: ideChoices.map((_, index) => index),
127
218
  },
128
- {
129
- type: "toggle",
130
- name: "installSkills",
131
- message: "Install skills into IDE skills folders?",
132
- active: "yes",
133
- inactive: "no",
134
- initial: true,
135
- },
136
219
  ],
137
220
  {
138
221
  onCancel: () => {
@@ -145,7 +228,6 @@ async function run() {
145
228
  scope = scope || response.scope;
146
229
  mcpScope = response.mcpScope || "local";
147
230
  ideSelection = response.ides || ideSelection;
148
- installSkills = response.installSkills;
149
231
  }
150
232
 
151
233
  if (!scope) {
@@ -166,33 +248,29 @@ async function run() {
166
248
  // Create temp directory for npm install
167
249
  const tempDir = path.join(getTempDir(), `.a11y-devkit-${Date.now()}`);
168
250
 
169
- if (installSkills) {
170
- const skillsSpinner = startSpinner("Installing skills from npm...");
251
+ const skillsSpinner = startSpinner("Installing skills from npm...");
171
252
 
172
- try {
173
- const skillTargets =
174
- scope === "local"
175
- ? ideSelection.map((ide) => idePaths[ide].localSkillsDir)
176
- : ideSelection.map((ide) => idePaths[ide].skillsDir);
253
+ try {
254
+ const skillTargets =
255
+ scope === "local"
256
+ ? ideSelection.map((ide) => idePaths[ide].localSkillsDir)
257
+ : ideSelection.map((ide) => idePaths[ide].skillsDir);
177
258
 
178
- const skillNames = config.skills.map((skill) =>
179
- typeof skill === "string" ? skill : skill.name,
180
- );
181
- const result = await installSkillsFromNpm(
182
- skillNames,
183
- skillTargets,
184
- tempDir,
185
- config.skillsFolder,
186
- config.readmeTemplate,
187
- );
188
- skillsSpinner.succeed(
189
- `${result.installed} skills installed to ${skillTargets.length} IDE location(s).`,
190
- );
191
- } catch (error) {
192
- skillsSpinner.fail(`Failed to install skills: ${error.message}`);
193
- }
194
- } else {
195
- warn("Skipping skills install to IDE folders.");
259
+ const skillNames = skillsToInstall.map((skill) =>
260
+ typeof skill === "string" ? skill : skill.npmName,
261
+ );
262
+ const result = await installSkillsFromNpm(
263
+ skillNames,
264
+ skillTargets,
265
+ tempDir,
266
+ config.skillsFolder,
267
+ config.readmeTemplate,
268
+ );
269
+ skillsSpinner.succeed(
270
+ `${result.installed} skills installed to ${skillTargets.length} IDE location(s).`,
271
+ );
272
+ } catch (error) {
273
+ skillsSpinner.fail(`Failed to install skills: ${error.message}`);
196
274
  }
197
275
 
198
276
  // Configure MCP servers using npx (no local installation needed!)
@@ -206,7 +284,7 @@ async function run() {
206
284
  const ide = ideSelection[i];
207
285
  await installMcpConfig(
208
286
  mcpConfigPaths[i],
209
- config.mcpServers,
287
+ mcpServersToInstall,
210
288
  idePaths[ide].mcpServerKey,
211
289
  );
212
290
  }
@@ -239,4 +317,198 @@ async function run() {
239
317
  info("Documentation: https://github.com/joe-watkins/a11y-devkit#readme");
240
318
  }
241
319
 
320
+ async function runGitMcpInstallation(projectRoot, platformInfo, config, idePaths, args) {
321
+ // Check if --yes flag is used with --git-mcp
322
+ if (args.autoYes) {
323
+ warn("--yes flag not supported for Git MCP installation");
324
+ info("Interactive prompts required for Git MCP configuration");
325
+ process.exit(1);
326
+ }
327
+
328
+ console.log("\n");
329
+ info("Installing MCP server from Git repository");
330
+ console.log("");
331
+
332
+ // Collect Git MCP information
333
+ const gitMcpPrompts = getGitMcpPrompts();
334
+ const mcpInfo = await prompts(gitMcpPrompts, {
335
+ onCancel: () => {
336
+ warn("Git MCP installation cancelled.");
337
+ process.exit(0);
338
+ },
339
+ });
340
+
341
+ // Parse args string into array
342
+ const argsArray = parseArgsString(mcpInfo.args);
343
+
344
+ // Prompt for Repo Clone Scope (where to clone the Git repository)
345
+ const repoScopeResponse = await prompts(
346
+ {
347
+ type: "select",
348
+ name: "repoScope",
349
+ message: "Where to clone the Git repository?",
350
+ choices: [
351
+ {
352
+ title: `Local to this project (${formatPath(projectRoot)})`,
353
+ value: "local",
354
+ description: "Clone to .a11y-devkit/mcp-repos/ in project",
355
+ },
356
+ {
357
+ title: "Global for this user",
358
+ value: "global",
359
+ description: "Clone to user-level app support directory",
360
+ },
361
+ ],
362
+ initial: 0,
363
+ },
364
+ {
365
+ onCancel: () => {
366
+ warn("Git MCP installation cancelled.");
367
+ process.exit(0);
368
+ },
369
+ },
370
+ );
371
+
372
+ // Prompt for MCP Config Scope (where to write MCP configurations)
373
+ const mcpScopeResponse = await prompts(
374
+ {
375
+ type: "select",
376
+ name: "mcpScope",
377
+ message: "Where to write MCP configurations?",
378
+ choices: [
379
+ {
380
+ title: `Local to this project (${formatPath(projectRoot)})`,
381
+ value: "local",
382
+ description: "Write to project-level IDE config folders",
383
+ },
384
+ {
385
+ title: "Global for this user",
386
+ value: "global",
387
+ description: "Write to user-level IDE config folders",
388
+ },
389
+ ],
390
+ initial: 0,
391
+ },
392
+ {
393
+ onCancel: () => {
394
+ warn("Git MCP installation cancelled.");
395
+ process.exit(0);
396
+ },
397
+ },
398
+ );
399
+
400
+ // Prompt for IDE selection
401
+ const ideChoices = config.ides.map((ide) => ({
402
+ title: ide.displayName,
403
+ value: ide.id,
404
+ }));
405
+
406
+ const ideResponse = await prompts(
407
+ {
408
+ type: "multiselect",
409
+ name: "ides",
410
+ message: "Configure MCP for which IDEs?",
411
+ choices: ideChoices,
412
+ initial: ideChoices.map((_, index) => index),
413
+ },
414
+ {
415
+ onCancel: () => {
416
+ warn("Git MCP installation cancelled.");
417
+ process.exit(0);
418
+ },
419
+ },
420
+ );
421
+
422
+ const ideSelection = ideResponse.ides || [];
423
+
424
+ if (!ideSelection.length) {
425
+ warn("No IDEs selected. MCP installation requires at least one IDE.");
426
+ process.exit(1);
427
+ }
428
+
429
+ const repoScope = repoScopeResponse.repoScope;
430
+ const mcpScope = mcpScopeResponse.mcpScope;
431
+
432
+ info(`Repository clone scope: ${repoScope === "local" ? "Local" : "Global"}`);
433
+ info(`MCP config scope: ${mcpScope === "local" ? "Local" : "Global"}`);
434
+
435
+ // Install Git MCP
436
+ const gitSpinner = startSpinner("Cloning Git repository...");
437
+
438
+ let mcpServer;
439
+ try {
440
+ mcpServer = await installGitMcp(
441
+ {
442
+ name: mcpInfo.name,
443
+ repoUrl: mcpInfo.repoUrl,
444
+ type: mcpInfo.type,
445
+ command: mcpInfo.command,
446
+ args: argsArray,
447
+ buildCommand: mcpInfo.buildCommand,
448
+ },
449
+ repoScope,
450
+ projectRoot,
451
+ platformInfo,
452
+ getMcpRepoDir,
453
+ );
454
+ gitSpinner.succeed(`Repository cloned to ${mcpServer.repoPath}`);
455
+ } catch (error) {
456
+ gitSpinner.fail(`Failed to install Git MCP: ${error.message}`);
457
+ process.exit(1);
458
+ }
459
+
460
+ // Install MCP configurations to selected IDEs
461
+ const mcpConfigSpinner = startSpinner("Updating MCP configurations...");
462
+
463
+ const mcpConfigPaths =
464
+ mcpScope === "local"
465
+ ? ideSelection.map((ide) => idePaths[ide].localMcpConfig)
466
+ : ideSelection.map((ide) => idePaths[ide].mcpConfig);
467
+
468
+ // Construct the MCP server configuration with absolute path
469
+ const mcpServerConfig = {
470
+ name: mcpServer.name,
471
+ type: mcpServer.type,
472
+ command: mcpServer.command,
473
+ args: mcpServer.args.length > 0 ? mcpServer.args : undefined,
474
+ };
475
+
476
+ // If args are provided, prepend the repo path to the first argument
477
+ // Otherwise, use the repo path as the only argument
478
+ if (mcpServerConfig.args && mcpServerConfig.args.length > 0) {
479
+ // Assume first arg is a relative path within the repo
480
+ mcpServerConfig.args = [
481
+ path.join(mcpServer.repoPath, mcpServerConfig.args[0]),
482
+ ...mcpServerConfig.args.slice(1),
483
+ ];
484
+ } else {
485
+ // No args provided - this might need to be handled differently
486
+ // For now, don't add any args
487
+ delete mcpServerConfig.args;
488
+ }
489
+
490
+ for (let i = 0; i < ideSelection.length; i++) {
491
+ const ide = ideSelection[i];
492
+ await installMcpConfig(
493
+ mcpConfigPaths[i],
494
+ [mcpServerConfig],
495
+ idePaths[ide].mcpServerKey,
496
+ );
497
+ }
498
+
499
+ mcpConfigSpinner.succeed(
500
+ `MCP configs updated for ${ideSelection.length} IDE(s) (${mcpScope} scope).`,
501
+ );
502
+
503
+ // Display success message
504
+ success("Git MCP installation complete!");
505
+ info(`Repository location: ${mcpServer.repoPath}`);
506
+ info(`MCP server '${mcpServer.name}' configured in ${ideSelection.length} IDE(s)`);
507
+ console.log("");
508
+ success("Next Steps:");
509
+ info("Restart your IDE to load the new MCP server");
510
+ info(`Repository cloned to: ${mcpServer.repoPath}`);
511
+ info("You can manually edit the MCP configuration files if needed");
512
+ }
513
+
242
514
  export { run };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Git repository operations and MCP installation orchestration
3
+ */
4
+
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+ import { spawn } from "child_process";
8
+ import prompts from "prompts";
9
+ import { warn, error as errorMsg } from "../ui.js";
10
+
11
+ /**
12
+ * Helper function to check if a path exists
13
+ * @param {string} target - Path to check
14
+ * @returns {Promise<boolean>}
15
+ */
16
+ async function pathExists(target) {
17
+ try {
18
+ await fs.access(target);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Runs a command using spawn
27
+ * @param {string} command - Command to run
28
+ * @param {string[]} args - Command arguments
29
+ * @param {object} options - Spawn options
30
+ * @returns {Promise<{stdout: string, stderr: string}>}
31
+ */
32
+ function run(command, args, options = {}) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ stdio: "pipe",
36
+ shell: true,
37
+ ...options,
38
+ });
39
+ let stdout = "";
40
+ let stderr = "";
41
+
42
+ child.stdout?.on("data", (data) => {
43
+ stdout += data;
44
+ });
45
+ child.stderr?.on("data", (data) => {
46
+ stderr += data;
47
+ });
48
+
49
+ child.on("error", reject);
50
+ child.on("close", (code) => {
51
+ if (code === 0) {
52
+ resolve({ stdout, stderr });
53
+ return;
54
+ }
55
+ reject(
56
+ new Error(
57
+ `${command} ${args.join(" ")} failed with code ${code}: ${stderr}`,
58
+ ),
59
+ );
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Clones a Git repository to the target directory
66
+ * @param {string} repoUrl - Git repository URL
67
+ * @param {string} targetDir - Directory to clone into
68
+ * @returns {Promise<{success: boolean, stdout: string, stderr: string}>}
69
+ */
70
+ export async function cloneGitRepo(repoUrl, targetDir) {
71
+ try {
72
+ const result = await run("git", ["clone", repoUrl, targetDir]);
73
+ return {
74
+ success: true,
75
+ stdout: result.stdout,
76
+ stderr: result.stderr,
77
+ };
78
+ } catch (err) {
79
+ // Provide helpful error messages based on error type
80
+ const errorMessage = err.message.toLowerCase();
81
+
82
+ if (errorMessage.includes("authentication") || errorMessage.includes("fatal: could not read")) {
83
+ throw new Error(
84
+ `Authentication required for ${repoUrl}. Please ensure the repository is public or configure Git credentials.`,
85
+ );
86
+ }
87
+
88
+ if (errorMessage.includes("repository not found") || errorMessage.includes("not found")) {
89
+ throw new Error(
90
+ `Repository not found: ${repoUrl}. Please verify the URL is correct.`,
91
+ );
92
+ }
93
+
94
+ if (errorMessage.includes("git: command not found") || errorMessage.includes("'git' is not recognized")) {
95
+ throw new Error(
96
+ "Git is not installed. Please install Git and try again.",
97
+ );
98
+ }
99
+
100
+ // Generic error
101
+ throw new Error(`Failed to clone repository: ${err.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Executes build command in the repository directory
107
+ * @param {string} repoDir - Repository directory path
108
+ * @param {string} buildCommand - Build command to execute
109
+ * @returns {Promise<{success: boolean, stdout: string, stderr: string}>}
110
+ */
111
+ export async function buildGitRepo(repoDir, buildCommand) {
112
+ try {
113
+ // Parse build command (handle multi-command strings like "npm install && npm run build")
114
+ // We'll execute the entire command string via shell
115
+ const result = await run(buildCommand, [], { cwd: repoDir });
116
+ return {
117
+ success: true,
118
+ stdout: result.stdout,
119
+ stderr: result.stderr,
120
+ };
121
+ } catch (err) {
122
+ return {
123
+ success: false,
124
+ stdout: err.stdout || "",
125
+ stderr: err.stderr || err.message,
126
+ };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Removes a cloned repository directory
132
+ * @param {string} repoPath - Path to repository to remove
133
+ * @returns {Promise<void>}
134
+ */
135
+ export async function cleanupGitRepo(repoPath) {
136
+ if (await pathExists(repoPath)) {
137
+ await fs.rm(repoPath, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Main orchestrator function for Git MCP installation
143
+ * @param {object} mcpConfig - MCP configuration {name, repoUrl, type, command, args, buildCommand}
144
+ * @param {string} repoScope - Where to clone repo ('local' | 'global')
145
+ * @param {string} projectRoot - Project root directory
146
+ * @param {object} platformInfo - Platform info object
147
+ * @param {Function} getMcpRepoDir - Function to get MCP repo directory
148
+ * @returns {Promise<{name: string, type: string, command: string, args: string[], repoPath: string}>}
149
+ */
150
+ export async function installGitMcp(
151
+ mcpConfig,
152
+ repoScope,
153
+ projectRoot,
154
+ platformInfo,
155
+ getMcpRepoDir,
156
+ ) {
157
+ const { name, repoUrl, type, command, args, buildCommand } = mcpConfig;
158
+
159
+ // Determine repo storage location
160
+ const repoPath = getMcpRepoDir(repoScope, projectRoot, platformInfo, name);
161
+
162
+ // Check for existing MCP with same name
163
+ if (await pathExists(repoPath)) {
164
+ warn(`MCP server '${name}' already exists at ${repoPath}`);
165
+
166
+ const overwriteResponse = await prompts(
167
+ {
168
+ type: "confirm",
169
+ name: "overwrite",
170
+ message: "Overwrite existing installation?",
171
+ initial: false,
172
+ },
173
+ {
174
+ onCancel: () => {
175
+ throw new Error("Installation cancelled by user");
176
+ },
177
+ },
178
+ );
179
+
180
+ if (!overwriteResponse.overwrite) {
181
+ throw new Error("Installation cancelled - existing MCP not overwritten");
182
+ }
183
+
184
+ // Remove old directory
185
+ await cleanupGitRepo(repoPath);
186
+ }
187
+
188
+ // Clone repository
189
+ await cloneGitRepo(repoUrl, repoPath);
190
+
191
+ // Run build command (continue with warning if fails)
192
+ const buildResult = await buildGitRepo(repoPath, buildCommand);
193
+ if (!buildResult.success) {
194
+ warn("Build command failed but continuing with installation");
195
+ if (buildResult.stderr) {
196
+ console.error("Build error output:");
197
+ console.error(buildResult.stderr);
198
+ }
199
+ }
200
+
201
+ // Return MCP server object for config installation
202
+ return {
203
+ name,
204
+ type,
205
+ command,
206
+ args,
207
+ repoPath,
208
+ };
209
+ }
@@ -33,10 +33,17 @@ function mergeServers(existing, incoming, serverKey = "servers") {
33
33
  const merged = { ...existing, [serverKey]: { ...existingServers } };
34
34
 
35
35
  for (const server of incoming) {
36
- merged[serverKey][server.name] = {
36
+ const serverConfig = {
37
37
  command: server.command,
38
38
  args: server.args || []
39
39
  };
40
+
41
+ // Include type if provided
42
+ if (server.type) {
43
+ serverConfig.type = server.type;
44
+ }
45
+
46
+ merged[serverKey][server.name] = serverConfig;
40
47
  }
41
48
 
42
49
  return merged;
package/src/paths.js CHANGED
@@ -32,19 +32,45 @@ function getAppSupportDir(platformInfo = getPlatform()) {
32
32
 
33
33
  function getIdePaths(projectRoot, platformInfo = getPlatform(), ideConfigs = []) {
34
34
  const home = os.homedir();
35
+ const appSupport = getAppSupportDir(platformInfo);
35
36
  const paths = {};
36
37
 
37
38
  for (const ide of ideConfigs) {
38
- // Use custom paths from config, or fall back to default pattern: ~/.{id}/
39
- const skillsFolder = ide.skillsFolder || `.${ide.id}/skills`;
40
- const mcpConfigFile = ide.mcpConfigFile || `.${ide.id}/mcp.json`;
39
+ // Default paths (used for local scope, and global scope if no overrides)
40
+ let skillsFolder = ide.skillsFolder || `.${ide.id}/skills`;
41
+ let mcpConfigFile = ide.mcpConfigFile || `.${ide.id}/mcp.json`;
42
+
43
+ // Check for platform-specific global path overrides
44
+ let globalSkillsFolder = skillsFolder;
45
+ let globalMcpConfigFile = mcpConfigFile;
46
+
47
+ if (ide.globalPaths) {
48
+ const platformKey = platformInfo.isWindows ? 'Win' : platformInfo.isMac ? 'macOS' : null;
49
+
50
+ if (platformKey && ide.globalPaths[platformKey]) {
51
+ const globalOverrides = ide.globalPaths[platformKey];
52
+
53
+ // Use app support directory + override path for global
54
+ if (globalOverrides.skillsFolder) {
55
+ globalSkillsFolder = globalOverrides.skillsFolder;
56
+ }
57
+ if (globalOverrides.mcpConfigFile) {
58
+ globalMcpConfigFile = globalOverrides.mcpConfigFile;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Determine base directory for global paths
64
+ const useAppSupport = ide.globalPaths &&
65
+ (platformInfo.isWindows || platformInfo.isMac);
66
+ const globalBase = useAppSupport ? appSupport : home;
41
67
 
42
68
  paths[ide.id] = {
43
69
  name: ide.displayName,
44
- mcpConfig: path.join(home, mcpConfigFile),
70
+ mcpConfig: path.join(globalBase, globalMcpConfigFile),
45
71
  localMcpConfig: path.join(projectRoot, mcpConfigFile),
46
72
  mcpServerKey: ide.mcpServerKey,
47
- skillsDir: path.join(home, skillsFolder),
73
+ skillsDir: path.join(globalBase, globalSkillsFolder),
48
74
  localSkillsDir: path.join(projectRoot, skillsFolder)
49
75
  };
50
76
  }
@@ -52,9 +78,19 @@ function getIdePaths(projectRoot, platformInfo = getPlatform(), ideConfigs = [])
52
78
  return paths;
53
79
  }
54
80
 
81
+ function getMcpRepoDir(scope, projectRoot, platformInfo, mcpName) {
82
+ if (scope === 'local') {
83
+ return path.join(projectRoot, '.a11y-devkit', 'mcp-repos', mcpName);
84
+ }
85
+
86
+ const appSupport = getAppSupportDir(platformInfo);
87
+ return path.join(appSupport, 'a11y-devkit', 'mcp-repos', mcpName);
88
+ }
89
+
55
90
  export {
56
91
  getPlatform,
57
92
  getAppSupportDir,
58
93
  getIdePaths,
59
- getTempDir
94
+ getTempDir,
95
+ getMcpRepoDir
60
96
  };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Git MCP-specific prompt definitions and validation logic
3
+ */
4
+
5
+ /**
6
+ * Validates MCP name according to rules:
7
+ * - Not empty
8
+ * - No spaces
9
+ * - Only alphanumeric characters and hyphens
10
+ * @param {string} name - MCP name to validate
11
+ * @returns {true | string} - true if valid, error message if invalid
12
+ */
13
+ export function validateMcpName(name) {
14
+ if (!name || name.trim() === "") {
15
+ return "MCP name is required";
16
+ }
17
+
18
+ if (name.includes(" ")) {
19
+ return "MCP name cannot contain spaces";
20
+ }
21
+
22
+ const validPattern = /^[a-z0-9-]+$/i;
23
+ if (!validPattern.test(name)) {
24
+ return "MCP name can only contain letters, numbers, and hyphens";
25
+ }
26
+
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * Validates Git URL format (GitHub or GitLab)
32
+ * @param {string} url - Git repository URL to validate
33
+ * @returns {{ valid: boolean, provider: 'github'|'gitlab'|null, error: string|null }}
34
+ */
35
+ export function validateGitUrl(url) {
36
+ if (!url || url.trim() === "") {
37
+ return {
38
+ valid: false,
39
+ provider: null,
40
+ error: "Git repository URL is required",
41
+ };
42
+ }
43
+
44
+ const gitUrlPattern =
45
+ /^https:\/\/(github\.com|gitlab\.com)\/[\w-]+\/[\w-]+(\.git)?$/;
46
+ const match = url.match(gitUrlPattern);
47
+
48
+ if (!match) {
49
+ if (url.includes("github.com") || url.includes("gitlab.com")) {
50
+ return {
51
+ valid: false,
52
+ provider: null,
53
+ error:
54
+ "Invalid Git URL format. Must be: https://github.com/user/repo.git or https://gitlab.com/user/repo.git",
55
+ };
56
+ }
57
+ return {
58
+ valid: false,
59
+ provider: null,
60
+ error: "Only GitHub and GitLab repositories are supported",
61
+ };
62
+ }
63
+
64
+ const provider = match[1].includes("github") ? "github" : "gitlab";
65
+
66
+ return {
67
+ valid: true,
68
+ provider,
69
+ error: null,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Parses comma-separated arguments string into array
75
+ * @param {string} argsString - Comma-separated arguments
76
+ * @returns {string[]} - Array of trimmed argument strings
77
+ */
78
+ export function parseArgsString(argsString) {
79
+ if (!argsString || argsString.trim() === "") {
80
+ return [];
81
+ }
82
+
83
+ return argsString
84
+ .split(",")
85
+ .map((arg) => arg.trim())
86
+ .filter((arg) => arg !== "");
87
+ }
88
+
89
+ /**
90
+ * Returns prompts array for Git MCP configuration
91
+ * @returns {Array} - Array of prompt objects for use with prompts library
92
+ */
93
+ export function getGitMcpPrompts() {
94
+ return [
95
+ {
96
+ type: "text",
97
+ name: "name",
98
+ message: "MCP Name (no spaces, alphanumeric with hyphens):",
99
+ validate: (value) => validateMcpName(value),
100
+ },
101
+ {
102
+ type: "text",
103
+ name: "repoUrl",
104
+ message: "Git Repository URL (GitHub or GitLab):",
105
+ validate: (value) => {
106
+ const result = validateGitUrl(value);
107
+ return result.valid ? true : result.error;
108
+ },
109
+ },
110
+ {
111
+ type: "select",
112
+ name: "type",
113
+ message: "MCP server transport type:",
114
+ choices: [
115
+ { title: "stdio", value: "stdio" },
116
+ { title: "http", value: "http" },
117
+ { title: "sse", value: "sse" },
118
+ ],
119
+ initial: 0,
120
+ },
121
+ {
122
+ type: "text",
123
+ name: "command",
124
+ message: "Command to run the MCP server:",
125
+ initial: "node",
126
+ },
127
+ {
128
+ type: "text",
129
+ name: "args",
130
+ message: "Arguments for the command (typically relative path to mcp .js example: js/index.js):",
131
+ initial: "",
132
+ },
133
+ {
134
+ type: "text",
135
+ name: "buildCommand",
136
+ message: "Build command:",
137
+ initial: "npm install",
138
+ },
139
+ ];
140
+ }