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 +1 -0
- package/config/a11y.json +8 -3
- package/package.json +1 -1
- package/src/cli.js +201 -3
- package/src/installers/mcp.js +51 -1
- package/src/installers/skills.js +62 -2
package/README.md
CHANGED
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": [
|
|
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
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
|
-
:
|
|
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) {
|
package/src/installers/mcp.js
CHANGED
|
@@ -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
|
};
|
package/src/installers/skills.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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 };
|