a11y-devkit-deploy 0.9.0 → 0.9.2

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/README.md CHANGED
@@ -22,6 +22,7 @@ a11y-devkit-deploy
22
22
 
23
23
  - `--local` / `--global`: Skip the scope prompt.
24
24
  - `--yes`: Use defaults (local scope, all IDEs, install skills).
25
+ - `--uninstall`: Remove skills and MCP entries installed by this tool.
25
26
 
26
27
  ## After Installation
27
28
 
package/config/a11y.json CHANGED
@@ -161,10 +161,15 @@
161
161
  "a11y-remediator-skill",
162
162
  "web-standards-skill",
163
163
  "a11y-audit-fix-agent-orchestrator-skill",
164
- "a11y-validator-skill",
165
- "playwright"
164
+ "a11y-validator-skill"
166
165
  ],
167
- "mcpServers": ["wcag", "aria", "magentaa11y", "a11y-personas"]
166
+ "mcpServers": [
167
+ "wcag",
168
+ "aria",
169
+ "magentaa11y",
170
+ "a11y-personas",
171
+ "playwright"
172
+ ]
168
173
  },
169
174
  {
170
175
  "id": "developer-ios",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a11y-devkit-deploy",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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
@@ -25,8 +25,8 @@ import prompts from "prompts";
25
25
 
26
26
  import { header, info, warn, success, startSpinner, formatPath } from "./ui.js";
27
27
  import { getPlatform, getHostApplicationPaths, getTempDir, getMcpRepoDir } from "./paths.js";
28
- import { installSkillsFromNpm, cleanupTemp } from "./installers/skills.js";
29
- import { installMcpConfig } from "./installers/mcp.js";
28
+ import { installSkillsFromNpm, uninstallSkillsFromTargets, cleanupTemp } from "./installers/skills.js";
29
+ import { installMcpConfig, removeMcpConfig } from "./installers/mcp.js";
30
30
  import { getGitMcpPrompts, parseArgsString } from "./prompts/git-mcp.js";
31
31
  import { installGitMcp } from "./installers/git-mcp.js";
32
32
 
@@ -55,6 +55,7 @@ function parseArgs(argv) {
55
55
  ? "local"
56
56
  : null,
57
57
  gitMcp: args.has("--git-mcp"),
58
+ uninstall: args.has("--uninstall"),
58
59
  };
59
60
  }
60
61
 
@@ -80,10 +81,22 @@ async function run() {
80
81
  `A11y Devkit Deploy v${pkg.version}`,
81
82
  args.gitMcp
82
83
  ? "Install MCP server from Git repository"
83
- : "Install skills + MCP servers across host applications",
84
+ : args.uninstall
85
+ ? "Uninstall skills + MCP servers installed by this tool"
86
+ : "Install skills + MCP servers across host applications",
84
87
  );
85
88
  info(`Detected OS: ${formatOs(platformInfo)}`);
86
89
 
90
+ if (args.uninstall && args.gitMcp) {
91
+ warn("--uninstall cannot be used with --git-mcp.");
92
+ process.exit(1);
93
+ }
94
+
95
+ if (args.uninstall) {
96
+ await runUninstall(projectRoot, platformInfo, config, hostPaths, args);
97
+ return;
98
+ }
99
+
87
100
  // Branch to Git MCP installation flow
88
101
  if (args.gitMcp) {
89
102
  await runGitMcpInstallation(projectRoot, platformInfo, config, hostPaths, args);
@@ -340,6 +353,191 @@ async function run() {
340
353
  info("Documentation: https://github.com/joe-watkins/a11y-devkit#readme");
341
354
  }
342
355
 
356
+ async function runUninstall(projectRoot, platformInfo, config, hostPaths, args) {
357
+ console.log("\n");
358
+ info("Removing skills and MCP servers installed by this tool");
359
+ console.log("");
360
+
361
+ let removeSkills = true;
362
+ let removeMcp = true;
363
+ let scope = args.scope;
364
+ let mcpScope = null;
365
+ let hostSelection = config.hostApplications.map((host) => host.id);
366
+
367
+ if (!args.autoYes) {
368
+ const removeResponse = await prompts(
369
+ {
370
+ type: "multiselect",
371
+ name: "targets",
372
+ message: "Remove which items?",
373
+ choices: [
374
+ { title: "Skills", value: "skills" },
375
+ { title: "MCP servers", value: "mcp" },
376
+ ],
377
+ initial: [0, 1],
378
+ },
379
+ {
380
+ onCancel: () => {
381
+ warn("Uninstall cancelled.");
382
+ process.exit(0);
383
+ },
384
+ },
385
+ );
386
+
387
+ const selectedTargets = removeResponse.targets || [];
388
+ removeSkills = selectedTargets.includes("skills");
389
+ removeMcp = selectedTargets.includes("mcp");
390
+
391
+ if (!removeSkills && !removeMcp) {
392
+ warn("No items selected to uninstall.");
393
+ process.exit(0);
394
+ }
395
+ }
396
+
397
+ if (!args.autoYes) {
398
+ const hostChoices = config.hostApplications.map((host) => ({
399
+ title: host.displayName,
400
+ value: host.id,
401
+ }));
402
+
403
+ const questions = [];
404
+
405
+ if (removeSkills && !scope) {
406
+ questions.push({
407
+ type: "select",
408
+ name: "scope",
409
+ message: "Remove skills locally or globally?",
410
+ choices: [
411
+ {
412
+ title: `Local to this project (${formatPath(projectRoot)}) [recommended]`,
413
+ value: "local",
414
+ },
415
+ { title: "Global for this user", value: "global" },
416
+ ],
417
+ initial: 0,
418
+ });
419
+ }
420
+
421
+ if (removeMcp) {
422
+ questions.push({
423
+ type: "select",
424
+ name: "mcpScope",
425
+ message: "Remove MCP configs locally or globally?",
426
+ choices: [
427
+ {
428
+ title: `Local to this project (${formatPath(projectRoot)})`,
429
+ value: "local",
430
+ description:
431
+ "Remove from project-level host application config folders (version-controllable)",
432
+ },
433
+ {
434
+ title: "Global for this user",
435
+ value: "global",
436
+ description: "Remove from user-level host application config folders",
437
+ },
438
+ ],
439
+ initial: 0,
440
+ });
441
+ }
442
+
443
+ questions.push({
444
+ type: "multiselect",
445
+ name: "hosts",
446
+ message: "Remove from which host applications?",
447
+ choices: hostChoices,
448
+ initial: hostChoices.map((_, index) => index),
449
+ });
450
+
451
+ const response = await prompts(questions, {
452
+ onCancel: () => {
453
+ warn("Uninstall cancelled.");
454
+ process.exit(0);
455
+ },
456
+ });
457
+
458
+ scope = scope || response.scope;
459
+ mcpScope = response.mcpScope || mcpScope;
460
+ hostSelection = response.hosts || hostSelection;
461
+ }
462
+
463
+ if (removeSkills && !scope) {
464
+ scope = "local";
465
+ }
466
+
467
+ if (removeMcp && !mcpScope) {
468
+ mcpScope = "local";
469
+ }
470
+
471
+ if (!hostSelection.length) {
472
+ warn("No host applications selected. Uninstall requires at least one host application.");
473
+ process.exit(1);
474
+ }
475
+
476
+ if (removeSkills) {
477
+ info(`Skills scope: ${scope === "local" ? "Local" : "Global"}`);
478
+ }
479
+ if (removeMcp) {
480
+ info(`MCP scope: ${mcpScope === "local" ? "Local" : "Global"}`);
481
+ }
482
+
483
+ if (removeSkills) {
484
+ const skillsSpinner = startSpinner("Removing skills...");
485
+ try {
486
+ const skillTargets =
487
+ scope === "local"
488
+ ? hostSelection.map((host) => hostPaths[host].localSkillsDir)
489
+ : hostSelection.map((host) => hostPaths[host].skillsDir);
490
+
491
+ const skillNames = config.skills.map((skill) =>
492
+ typeof skill === "string" ? skill : skill.npmName,
493
+ );
494
+
495
+ const result = await uninstallSkillsFromTargets(
496
+ skillNames,
497
+ skillTargets,
498
+ config.skillsFolder,
499
+ config.readmeTemplate,
500
+ );
501
+ skillsSpinner.succeed(
502
+ `Removed ${result.removed} skill folder(s) from ${skillTargets.length} host application location(s).`,
503
+ );
504
+ } catch (error) {
505
+ skillsSpinner.fail(`Failed to remove skills: ${error.message}`);
506
+ }
507
+ }
508
+
509
+ if (removeMcp) {
510
+ const mcpSpinner = startSpinner("Removing MCP configurations...");
511
+ const mcpConfigPaths =
512
+ mcpScope === "local"
513
+ ? hostSelection.map((host) => hostPaths[host].localMcpConfig)
514
+ : hostSelection.map((host) => hostPaths[host].mcpConfig);
515
+
516
+ let removedCount = 0;
517
+ const serverNames = config.mcpServers.map((server) => server.name);
518
+
519
+ for (let i = 0; i < hostSelection.length; i++) {
520
+ const host = hostSelection[i];
521
+ const result = await removeMcpConfig(
522
+ mcpConfigPaths[i],
523
+ serverNames,
524
+ hostPaths[host].mcpServerKey,
525
+ );
526
+ removedCount += result.removed;
527
+ }
528
+
529
+ if (removedCount > 0) {
530
+ mcpSpinner.succeed(
531
+ `Removed ${removedCount} MCP entries from ${hostSelection.length} host application(s) (${mcpScope} scope).`,
532
+ );
533
+ } else {
534
+ mcpSpinner.succeed("No matching MCP entries found to remove.");
535
+ }
536
+ }
537
+
538
+ success("Uninstall complete.");
539
+ }
540
+
343
541
  async function runGitMcpInstallation(projectRoot, platformInfo, config, hostPaths, args) {
344
542
  // Check if --yes flag is used with --git-mcp
345
543
  if (args.autoYes) {
@@ -49,6 +49,39 @@ function mergeServers(existing, incoming, serverKey = "servers") {
49
49
  return merged;
50
50
  }
51
51
 
52
+ function removeServers(existing, removeNames, serverKey = "servers") {
53
+ const existingServers = existing[serverKey] && typeof existing[serverKey] === "object"
54
+ ? existing[serverKey]
55
+ : null;
56
+
57
+ if (!existingServers) {
58
+ return { updated: existing, removed: 0 };
59
+ }
60
+
61
+ const updatedServers = { ...existingServers };
62
+ let removed = 0;
63
+
64
+ for (const name of removeNames) {
65
+ if (Object.prototype.hasOwnProperty.call(updatedServers, name)) {
66
+ delete updatedServers[name];
67
+ removed++;
68
+ }
69
+ }
70
+
71
+ if (removed === 0) {
72
+ return { updated: existing, removed: 0 };
73
+ }
74
+
75
+ const updated = { ...existing };
76
+ if (Object.keys(updatedServers).length === 0) {
77
+ delete updated[serverKey];
78
+ } else {
79
+ updated[serverKey] = updatedServers;
80
+ }
81
+
82
+ return { updated, removed };
83
+ }
84
+
52
85
  async function installMcpConfig(configPath, servers, serverKey = "servers") {
53
86
  await fs.mkdir(path.dirname(configPath), { recursive: true });
54
87
  const existing = await loadJson(configPath);
@@ -56,6 +89,23 @@ async function installMcpConfig(configPath, servers, serverKey = "servers") {
56
89
  await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
57
90
  }
58
91
 
92
+ async function removeMcpConfig(configPath, serverNames, serverKey = "servers") {
93
+ if (!(await pathExists(configPath))) {
94
+ return { removed: 0, changed: false };
95
+ }
96
+
97
+ const existing = await loadJson(configPath);
98
+ const { updated, removed } = removeServers(existing, serverNames, serverKey);
99
+
100
+ if (removed === 0) {
101
+ return { removed: 0, changed: false };
102
+ }
103
+
104
+ await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
105
+ return { removed, changed: true };
106
+ }
107
+
59
108
  export {
60
- installMcpConfig
109
+ installMcpConfig,
110
+ removeMcpConfig
61
111
  };
@@ -15,6 +15,10 @@ async function pathExists(target) {
15
15
  }
16
16
  }
17
17
 
18
+ function getSkillDirName(skillPackageName) {
19
+ return skillPackageName.replace(/-skill$/, "");
20
+ }
21
+
18
22
  function run(command, args, options = {}) {
19
23
  return new Promise((resolve, reject) => {
20
24
  const child = spawn(command, args, {
@@ -53,6 +57,17 @@ async function cleanupTemp(tempDir) {
53
57
  }
54
58
  }
55
59
 
60
+ async function removeDirIfEmpty(targetDir) {
61
+ if (!(await pathExists(targetDir))) {
62
+ return;
63
+ }
64
+
65
+ const entries = await fs.readdir(targetDir);
66
+ if (entries.length === 0) {
67
+ await fs.rmdir(targetDir);
68
+ }
69
+ }
70
+
56
71
  /**
57
72
  * Install skills from npm packages into IDE skills directories.
58
73
  *
@@ -116,7 +131,7 @@ async function installSkillsFromNpm(
116
131
 
117
132
  if (await pathExists(skillMdPath)) {
118
133
  // Create skill directory in target (use package name without -skill suffix)
119
- const skillDirName = skill.replace(/-skill$/, "");
134
+ const skillDirName = getSkillDirName(skill);
120
135
  const targetSkillDir = path.join(skillsDir, skillDirName);
121
136
  await fs.mkdir(targetSkillDir, { recursive: true });
122
137
 
@@ -146,4 +161,49 @@ async function installSkillsFromNpm(
146
161
  };
147
162
  }
148
163
 
149
- export { installSkillsFromNpm, cleanupTemp };
164
+ /**
165
+ * Remove skills installed by this tool from target directories.
166
+ *
167
+ * @param {string[]} skills - Array of npm package names
168
+ * @param {string[]} targetDirs - Array of target directories to uninstall from
169
+ * @param {string} skillsFolder - Optional subfolder name used for bundled skills
170
+ * @param {string} readmeTemplate - README template filename from templates folder
171
+ * @returns {Promise<{removed: number}>}
172
+ */
173
+ async function uninstallSkillsFromTargets(
174
+ skills,
175
+ targetDirs,
176
+ skillsFolder = null,
177
+ readmeTemplate = "deploy-README.md",
178
+ ) {
179
+ let removedCount = 0;
180
+
181
+ for (const targetDir of targetDirs) {
182
+ const skillsDir = skillsFolder
183
+ ? path.join(targetDir, skillsFolder)
184
+ : targetDir;
185
+
186
+ for (const skill of skills) {
187
+ const skillDirName = getSkillDirName(skill);
188
+ const targetSkillDir = path.join(skillsDir, skillDirName);
189
+
190
+ if (await pathExists(targetSkillDir)) {
191
+ await fs.rm(targetSkillDir, { recursive: true, force: true });
192
+ removedCount++;
193
+ }
194
+ }
195
+
196
+ const readmePath = path.join(skillsDir, "a11y-devkit-README.md");
197
+ if (await pathExists(readmePath)) {
198
+ await fs.rm(readmePath, { force: true });
199
+ }
200
+
201
+ if (skillsFolder) {
202
+ await removeDirIfEmpty(skillsDir);
203
+ }
204
+ }
205
+
206
+ return { removed: removedCount };
207
+ }
208
+
209
+ export { installSkillsFromNpm, uninstallSkillsFromTargets, cleanupTemp };