a11y-devkit-deploy 0.9.1 → 0.9.3

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
 
@@ -63,7 +64,7 @@ All MCP servers are configured to run via `npx`, which means:
63
64
 
64
65
  This CLI automates the setup of accessibility tooling by:
65
66
 
66
- 1. **Installing skills from npm** - Downloads and installs accessibility skill packages (configurable in `config/a11y.json`)
67
+ 1. **Installing skills from npm** - Downloads and installs accessibility skill packages (configurable in `config/settings.json`)
67
68
  2. **Configuring MCP servers** - Updates each IDE's MCP config to enable accessibility-focused MCP servers (also configurable)
68
69
 
69
70
  **Default configuration includes:**
@@ -79,7 +80,7 @@ This CLI automates the setup of accessibility tooling by:
79
80
 
80
81
  ## Why This Tool?
81
82
 
82
- **Zero Hardcoded Values** - Every aspect of the tool is driven by `config/a11y.json`:
83
+ **Zero Hardcoded Values** - Every aspect of the tool is driven by `config/settings.json`:
83
84
  - IDE paths and configuration files
84
85
  - Skills to install
85
86
  - MCP servers to configure
@@ -103,7 +104,7 @@ This CLI automates the setup of accessibility tooling by:
103
104
 
104
105
  ### Skills Installed (Default)
105
106
 
106
- The following skill packages are installed from npm by default. **Add your own by editing `config/a11y.json`**:
107
+ The following skill packages are installed from npm by default. **Add your own by editing `config/settings.json`**:
107
108
 
108
109
  | Skill | Package | Description |
109
110
  |-------|---------|-------------|
@@ -142,7 +143,7 @@ The generated MCP config looks like this:
142
143
 
143
144
  ## Configuration
144
145
 
145
- The entire tool is **fully config-driven**. Edit `config/a11y.json` to customize everything without touching code.
146
+ The entire tool is **fully config-driven**. Edit `config/settings.json` to customize everything without touching code.
146
147
 
147
148
  ### Adding a New Skill
148
149
 
@@ -292,11 +293,11 @@ your-project/
292
293
  # macOS: ~/Library/Application Support/Code/User/mcp.json
293
294
  ```
294
295
 
295
- **Note:** Paths are fully customizable per IDE in `config/a11y.json`
296
+ **Note:** Paths are fully customizable per IDE in `config/settings.json`
296
297
 
297
298
  ## MCP Servers Included (Default)
298
299
 
299
- **Add your own by editing `config/a11y.json`**:
300
+ **Add your own by editing `config/settings.json`**:
300
301
 
301
302
  | Server | Package | Description |
302
303
  |--------|---------|-------------|
@@ -308,7 +309,7 @@ your-project/
308
309
 
309
310
  ## Complete Config Example
310
311
 
311
- Here's what a complete `config/a11y.json` looks like:
312
+ Here's what a complete `config/settings.json` looks like:
312
313
 
313
314
  ```json
314
315
  {
@@ -365,3 +366,4 @@ Here's what a complete `config/a11y.json` looks like:
365
366
  ```
366
367
 
367
368
  Everything is customizable - add, remove, or modify any section to match your needs.
369
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a11y-devkit-deploy",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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
 
@@ -34,7 +34,7 @@ const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = path.dirname(__filename);
35
35
 
36
36
  async function loadConfig() {
37
- const configPath = path.join(__dirname, "..", "config", "a11y.json");
37
+ const configPath = path.join(__dirname, "..", "config", "settings.json");
38
38
  const raw = await fs.readFile(configPath, "utf8");
39
39
  return JSON.parse(raw);
40
40
  }
@@ -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) {
@@ -535,3 +733,4 @@ async function runGitMcpInstallation(projectRoot, platformInfo, config, hostPath
535
733
  }
536
734
 
537
735
  export { run };
736
+
@@ -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 };
File without changes