a11y-devkit-deploy 0.4.1 → 0.5.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/README.md +43 -39
- package/config/a11y.json +46 -73
- package/package.json +1 -1
- package/src/cli.js +162 -194
- package/src/installers/mcp.js +54 -69
- package/src/installers/repo.js +55 -103
- package/src/paths.js +82 -92
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# A11y Devkit Deploy
|
|
2
2
|
|
|
3
|
-
A cross-platform CLI for deploying accessibility skills and MCP servers across Claude Code, Cursor, Codex, and VSCode.
|
|
3
|
+
A cross-platform CLI for deploying accessibility skills and MCP servers across Claude Code, Cursor, Codex, and VSCode.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -26,29 +26,47 @@ a11y-devkit-deploy
|
|
|
26
26
|
This CLI automates the setup of accessibility tooling by:
|
|
27
27
|
|
|
28
28
|
1. **Cloning the a11y-skills repository** - Contains IDE skills for accessibility workflows
|
|
29
|
-
2. **
|
|
30
|
-
|
|
31
|
-
- **
|
|
29
|
+
2. **Installing skills** - Copies skills to IDE-specific directories based on scope (local/global)
|
|
30
|
+
3. **Configuring MCP servers** - Updates each IDE's MCP config to enable 5 accessibility-focused MCP servers:
|
|
31
|
+
- **wcag** - WCAG 2.2 guidelines, success criteria, and techniques
|
|
32
|
+
- **aria** - WAI-ARIA roles, states, properties, and patterns
|
|
32
33
|
- **magentaa11y** - Component accessibility acceptance criteria
|
|
33
|
-
- **a11y-personas
|
|
34
|
-
- **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
- **a11y-personas** - Accessibility personas for diverse user needs
|
|
35
|
+
- **arc-issues** - Format AxeCore violations into standardized issue templates
|
|
36
|
+
|
|
37
|
+
### No Local MCP Installation Required!
|
|
38
|
+
|
|
39
|
+
MCP servers are configured to use `npx`, which means:
|
|
40
|
+
- **No cloning** of MCP server repositories
|
|
41
|
+
- **No building** or `npm install` steps
|
|
42
|
+
- **No disk space** used for local copies
|
|
43
|
+
- **Always up-to-date** - npx fetches the latest version automatically
|
|
44
|
+
|
|
45
|
+
The generated MCP config looks like this:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"wcag": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "wcag-mcp"]
|
|
53
|
+
},
|
|
54
|
+
"aria": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "aria-mcp"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
41
61
|
|
|
42
62
|
## Configuration
|
|
43
63
|
|
|
44
64
|
Edit `config/a11y.json` to customize the deployment:
|
|
45
65
|
|
|
46
66
|
- `repo.url` - Main skills repository to clone
|
|
47
|
-
- `mcpRepos` - Array of MCP repositories to clone and build
|
|
48
67
|
- `skillsSearchPaths` - Directories to search for skills in the cloned repo
|
|
49
68
|
- `ideSkillsPaths` - IDE-specific skills directories (configurable per IDE)
|
|
50
|
-
- `mcpServers` - MCP server definitions
|
|
51
|
-
- `{mcpRepoDir}` - Path to the MCP servers directory (e.g., `~/.mcp/servers/` or `~/.claude/mcp/servers/`)
|
|
69
|
+
- `mcpServers` - MCP server definitions using npx
|
|
52
70
|
|
|
53
71
|
## Directory Structure
|
|
54
72
|
|
|
@@ -69,33 +87,19 @@ your-project/
|
|
|
69
87
|
~/.vscode/skills/ # VSCode skills
|
|
70
88
|
```
|
|
71
89
|
|
|
72
|
-
### MCP
|
|
73
|
-
MCP servers are always installed to the home directory:
|
|
74
|
-
|
|
75
|
-
**Single IDE Selection:**
|
|
76
|
-
```
|
|
77
|
-
~/.claude/mcp/servers/ # If only Claude Code is selected
|
|
78
|
-
│ ├── wcag-mcp/
|
|
79
|
-
│ ├── aria-mcp/
|
|
80
|
-
│ ├── magentaa11y-mcp/
|
|
81
|
-
│ ├── a11y-personas-mcp/
|
|
82
|
-
│ └── a11y-issues-template-mcp/
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Multiple IDE Selection:**
|
|
86
|
-
```
|
|
87
|
-
~/.mcp/servers/ # Shared location for all selected IDEs
|
|
88
|
-
│ ├── wcag-mcp/
|
|
89
|
-
│ ├── aria-mcp/
|
|
90
|
-
│ ├── magentaa11y-mcp/
|
|
91
|
-
│ ├── a11y-personas-mcp/
|
|
92
|
-
│ └── a11y-issues-template-mcp/
|
|
93
|
-
```
|
|
90
|
+
### MCP Configuration Locations
|
|
94
91
|
|
|
95
92
|
MCP configurations are written to each IDE's OS-specific config path:
|
|
96
93
|
- **macOS**: `~/Library/Application Support/{IDE}/mcp.json`
|
|
97
94
|
- **Windows**: `%APPDATA%\{IDE}\mcp.json`
|
|
98
95
|
- **Linux**: `~/.config/{IDE}/mcp.json`
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
## MCP Servers Included
|
|
98
|
+
|
|
99
|
+
| Server | Package | Description |
|
|
100
|
+
|--------|---------|-------------|
|
|
101
|
+
| wcag | `wcag-mcp` | WCAG 2.2 guidelines, success criteria, techniques |
|
|
102
|
+
| aria | `aria-mcp` | WAI-ARIA roles, states, properties |
|
|
103
|
+
| magentaa11y | `magentaa11y-mcp` | Component accessibility acceptance criteria |
|
|
104
|
+
| a11y-personas | `a11y-personas-mcp` | Accessibility personas for diverse users |
|
|
105
|
+
| arc-issues | `arc-issues-mcp` | AxeCore violation formatting |
|
package/config/a11y.json
CHANGED
|
@@ -1,73 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"repo": {
|
|
3
|
-
"url": "https://github.com/joe-watkins/a11y-skills",
|
|
4
|
-
"dirName": "a11y-skills"
|
|
5
|
-
},
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
"name": "wcag-mcp",
|
|
49
|
-
"command": "node",
|
|
50
|
-
"args": ["{mcpRepoDir}/wcag-mcp/src/index.js"]
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"name": "aria-mcp",
|
|
54
|
-
"command": "node",
|
|
55
|
-
"args": ["{mcpRepoDir}/aria-mcp/src/index.js"]
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"name": "magentaa11y",
|
|
59
|
-
"command": "node",
|
|
60
|
-
"args": ["{mcpRepoDir}/magentaa11y-mcp/src/index.js"]
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"name": "a11y-personas-mcp",
|
|
64
|
-
"command": "node",
|
|
65
|
-
"args": ["{mcpRepoDir}/a11y-personas-mcp/src/index.js"]
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"name": "a11y-issues-template-mcp",
|
|
69
|
-
"command": "node",
|
|
70
|
-
"args": ["{mcpRepoDir}/accessibility-issues-template-mcp/build/index.js"]
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"repo": {
|
|
3
|
+
"url": "https://github.com/joe-watkins/a11y-skills",
|
|
4
|
+
"dirName": "a11y-skills"
|
|
5
|
+
},
|
|
6
|
+
"skillsSearchPaths": [
|
|
7
|
+
".",
|
|
8
|
+
"skills",
|
|
9
|
+
".github/skills",
|
|
10
|
+
".codex/skills"
|
|
11
|
+
],
|
|
12
|
+
"ideSkillsPaths": {
|
|
13
|
+
"claude": ".claude/skills",
|
|
14
|
+
"cursor": ".cursor/skills",
|
|
15
|
+
"codex": ".codex/skills",
|
|
16
|
+
"vscode": ".github/skills",
|
|
17
|
+
"local": ".github/skills"
|
|
18
|
+
},
|
|
19
|
+
"mcpServers": [
|
|
20
|
+
{
|
|
21
|
+
"name": "wcag",
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "wcag-mcp"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "aria",
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "aria-mcp"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "magentaa11y",
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "magentaa11y-mcp"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "a11y-personas",
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "a11y-personas-mcp"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "arc-issues",
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "arc-issues-mcp"]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,194 +1,162 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import prompts from "prompts";
|
|
6
|
-
|
|
7
|
-
import { header, info, warn, success, startSpinner, formatPath } from "./ui.js";
|
|
8
|
-
import { getPlatform, getIdePaths,
|
|
9
|
-
import { ensureRepo,
|
|
10
|
-
import { findSkillsDir, copySkills } from "./installers/skills.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
|
|
16
|
-
async function loadConfig() {
|
|
17
|
-
const configPath = path.join(__dirname, "..", "config", "a11y.json");
|
|
18
|
-
const raw = await fs.readFile(configPath, "utf8");
|
|
19
|
-
return JSON.parse(raw);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function parseArgs(argv) {
|
|
23
|
-
const args = new Set(argv.slice(2));
|
|
24
|
-
return {
|
|
25
|
-
autoYes: args.has("--yes") || args.has("-y"),
|
|
26
|
-
scope: args.has("--global") ? "global" : args.has("--local") ? "local" : null
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function formatOs(platformInfo) {
|
|
31
|
-
if (platformInfo.isWindows) return "Windows";
|
|
32
|
-
if (platformInfo.isMac) return "macOS";
|
|
33
|
-
if (platformInfo.isLinux) return "Linux";
|
|
34
|
-
return platformInfo.platform;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function run() {
|
|
38
|
-
const projectRoot = process.cwd();
|
|
39
|
-
const platformInfo = getPlatform();
|
|
40
|
-
const config = await loadConfig();
|
|
41
|
-
const idePaths = getIdePaths(projectRoot, platformInfo, config.ideSkillsPaths);
|
|
42
|
-
const args = parseArgs(process.argv);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{ title: "
|
|
50
|
-
{ title: "
|
|
51
|
-
{ title: "
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{ title: "
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? ideSelection.map((ide) => idePaths[ide].localSkillsDir)
|
|
164
|
-
: ideSelection.map((ide) => idePaths[ide].skillsDir);
|
|
165
|
-
|
|
166
|
-
for (const target of skillTargets) {
|
|
167
|
-
await copySkills(sourceDir, target);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
skillsSpinner.succeed(`Skills installed to ${skillTargets.length} IDE location(s).`);
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
warn("Skipping skills install to IDE folders.");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const serverDefs = resolveServers(config.mcpServers, mcpServerDir, mcpServerDir);
|
|
177
|
-
const mcpSpinner = startSpinner("Updating MCP configurations...");
|
|
178
|
-
for (const ide of ideSelection) {
|
|
179
|
-
await installMcpConfig(idePaths[ide].mcpConfig, serverDefs, idePaths[ide].mcpServerKey);
|
|
180
|
-
}
|
|
181
|
-
mcpSpinner.succeed(`MCP configs updated for ${ideSelection.length} IDE(s).`);
|
|
182
|
-
|
|
183
|
-
// Clean up temporary directory
|
|
184
|
-
const cleanupSpinner = startSpinner("Cleaning up temporary files...");
|
|
185
|
-
await cleanupTemp(tempDir);
|
|
186
|
-
cleanupSpinner.succeed("Temporary files removed");
|
|
187
|
-
|
|
188
|
-
success("All done. Your skills and MCP servers are ready.");
|
|
189
|
-
info("You can re-run this CLI any time to update the repo and configs.");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export {
|
|
193
|
-
run
|
|
194
|
-
};
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
|
|
7
|
+
import { header, info, warn, success, startSpinner, formatPath } from "./ui.js";
|
|
8
|
+
import { getPlatform, getIdePaths, getTempDir } from "./paths.js";
|
|
9
|
+
import { ensureRepo, cleanupTemp } from "./installers/repo.js";
|
|
10
|
+
import { findSkillsDir, copySkills } from "./installers/skills.js";
|
|
11
|
+
import { installMcpConfig } from "./installers/mcp.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
async function loadConfig() {
|
|
17
|
+
const configPath = path.join(__dirname, "..", "config", "a11y.json");
|
|
18
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = new Set(argv.slice(2));
|
|
24
|
+
return {
|
|
25
|
+
autoYes: args.has("--yes") || args.has("-y"),
|
|
26
|
+
scope: args.has("--global") ? "global" : args.has("--local") ? "local" : null
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatOs(platformInfo) {
|
|
31
|
+
if (platformInfo.isWindows) return "Windows";
|
|
32
|
+
if (platformInfo.isMac) return "macOS";
|
|
33
|
+
if (platformInfo.isLinux) return "Linux";
|
|
34
|
+
return platformInfo.platform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function run() {
|
|
38
|
+
const projectRoot = process.cwd();
|
|
39
|
+
const platformInfo = getPlatform();
|
|
40
|
+
const config = await loadConfig();
|
|
41
|
+
const idePaths = getIdePaths(projectRoot, platformInfo, config.ideSkillsPaths);
|
|
42
|
+
const args = parseArgs(process.argv);
|
|
43
|
+
|
|
44
|
+
header("A11y Devkit Deploy", "Install skills + MCP servers across IDEs");
|
|
45
|
+
info(`Detected OS: ${formatOs(platformInfo)}`);
|
|
46
|
+
|
|
47
|
+
const ideChoices = [
|
|
48
|
+
{ title: "Claude Code", value: "claude" },
|
|
49
|
+
{ title: "Cursor", value: "cursor" },
|
|
50
|
+
{ title: "Codex", value: "codex" },
|
|
51
|
+
{ title: "VSCode", value: "vscode" }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let scope = args.scope;
|
|
55
|
+
let ideSelection = ["claude", "cursor", "codex", "vscode"];
|
|
56
|
+
let installSkills = true;
|
|
57
|
+
|
|
58
|
+
if (!args.autoYes) {
|
|
59
|
+
const response = await prompts(
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
type: scope ? null : "select",
|
|
63
|
+
name: "scope",
|
|
64
|
+
message: "Install skills + repo locally or globally?",
|
|
65
|
+
choices: [
|
|
66
|
+
{ title: "Local to this project", value: "local" },
|
|
67
|
+
{ title: "Global for this user", value: "global" }
|
|
68
|
+
],
|
|
69
|
+
initial: 0
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "multiselect",
|
|
73
|
+
name: "ides",
|
|
74
|
+
message: "Configure MCP for which IDEs?",
|
|
75
|
+
choices: ideChoices,
|
|
76
|
+
initial: ideChoices.map((_, index) => index)
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "toggle",
|
|
80
|
+
name: "installSkills",
|
|
81
|
+
message: "Install skills into IDE skills folders?",
|
|
82
|
+
active: "yes",
|
|
83
|
+
inactive: "no",
|
|
84
|
+
initial: true
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
{
|
|
88
|
+
onCancel: () => {
|
|
89
|
+
warn("Setup cancelled.");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
scope = scope || response.scope;
|
|
96
|
+
ideSelection = response.ides || ideSelection;
|
|
97
|
+
installSkills = response.installSkills;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!scope) {
|
|
101
|
+
scope = "local";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!ideSelection.length) {
|
|
105
|
+
warn("No IDEs selected. MCP installation requires at least one IDE.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
info(`Install scope: ${scope === "local" ? "Local" : "Global"}`);
|
|
110
|
+
|
|
111
|
+
// Create temp directory for cloning skills repo
|
|
112
|
+
const tempDir = path.join(getTempDir(), `.a11y-devkit-${Date.now()}`);
|
|
113
|
+
const tempSkillsDir = path.join(tempDir, "skills");
|
|
114
|
+
|
|
115
|
+
// Clone skills repo into temp
|
|
116
|
+
const repoSpinner = startSpinner("Syncing a11y-skills repo...");
|
|
117
|
+
const repoResult = await ensureRepo({
|
|
118
|
+
url: config.repo.url,
|
|
119
|
+
dir: tempSkillsDir
|
|
120
|
+
});
|
|
121
|
+
repoSpinner.succeed(`Repo ${repoResult.action}: ${formatPath(repoResult.dir)}`);
|
|
122
|
+
|
|
123
|
+
if (installSkills) {
|
|
124
|
+
const skillsSpinner = startSpinner("Installing skills to IDE folders...");
|
|
125
|
+
const sourceDir = await findSkillsDir(tempSkillsDir, config.skillsSearchPaths);
|
|
126
|
+
if (!sourceDir) {
|
|
127
|
+
skillsSpinner.fail("No skills directory found in repo.");
|
|
128
|
+
} else {
|
|
129
|
+
const skillTargets = scope === "local"
|
|
130
|
+
? ideSelection.map((ide) => idePaths[ide].localSkillsDir)
|
|
131
|
+
: ideSelection.map((ide) => idePaths[ide].skillsDir);
|
|
132
|
+
|
|
133
|
+
for (const target of skillTargets) {
|
|
134
|
+
await copySkills(sourceDir, target);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
skillsSpinner.succeed(`Skills installed to ${skillTargets.length} IDE location(s).`);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
warn("Skipping skills install to IDE folders.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Configure MCP servers using npx (no local installation needed!)
|
|
144
|
+
const mcpSpinner = startSpinner("Updating MCP configurations...");
|
|
145
|
+
for (const ide of ideSelection) {
|
|
146
|
+
await installMcpConfig(idePaths[ide].mcpConfig, config.mcpServers, idePaths[ide].mcpServerKey);
|
|
147
|
+
}
|
|
148
|
+
mcpSpinner.succeed(`MCP configs updated for ${ideSelection.length} IDE(s).`);
|
|
149
|
+
|
|
150
|
+
// Clean up temporary directory
|
|
151
|
+
const cleanupSpinner = startSpinner("Cleaning up temporary files...");
|
|
152
|
+
await cleanupTemp(tempDir);
|
|
153
|
+
cleanupSpinner.succeed("Temporary files removed");
|
|
154
|
+
|
|
155
|
+
success("All done. Your skills and MCP servers are ready.");
|
|
156
|
+
info("MCP servers use npx - no local installation needed!");
|
|
157
|
+
info("You can re-run this CLI any time to update the repo and configs.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
run
|
|
162
|
+
};
|
package/src/installers/mcp.js
CHANGED
|
@@ -1,69 +1,54 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
async function pathExists(target) {
|
|
5
|
-
try {
|
|
6
|
-
await fs.access(target);
|
|
7
|
-
return true;
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function loadJson(filePath) {
|
|
14
|
-
if (!(await pathExists(filePath))) {
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
20
|
-
return raw.trim() ? JSON.parse(raw) : {};
|
|
21
|
-
} catch (error) {
|
|
22
|
-
const backupPath = `${filePath}.bak`;
|
|
23
|
-
await fs.copyFile(filePath, backupPath);
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return merged;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function installMcpConfig(configPath, servers, serverKey = "servers") {
|
|
60
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
61
|
-
const existing = await loadJson(configPath);
|
|
62
|
-
const updated = mergeServers(existing, servers, serverKey);
|
|
63
|
-
await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export {
|
|
67
|
-
resolveServers,
|
|
68
|
-
installMcpConfig
|
|
69
|
-
};
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
async function pathExists(target) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(target);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function loadJson(filePath) {
|
|
14
|
+
if (!(await pathExists(filePath))) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
20
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const backupPath = `${filePath}.bak`;
|
|
23
|
+
await fs.copyFile(filePath, backupPath);
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mergeServers(existing, incoming, serverKey = "servers") {
|
|
29
|
+
const existingServers = existing[serverKey] && typeof existing[serverKey] === "object"
|
|
30
|
+
? existing[serverKey]
|
|
31
|
+
: {};
|
|
32
|
+
|
|
33
|
+
const merged = { ...existing, [serverKey]: { ...existingServers } };
|
|
34
|
+
|
|
35
|
+
for (const server of incoming) {
|
|
36
|
+
merged[serverKey][server.name] = {
|
|
37
|
+
command: server.command,
|
|
38
|
+
args: server.args || []
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return merged;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function installMcpConfig(configPath, servers, serverKey = "servers") {
|
|
46
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
47
|
+
const existing = await loadJson(configPath);
|
|
48
|
+
const updated = mergeServers(existing, servers, serverKey);
|
|
49
|
+
await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
installMcpConfig
|
|
54
|
+
};
|
package/src/installers/repo.js
CHANGED
|
@@ -1,103 +1,55 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { spawn } from "child_process";
|
|
4
|
-
|
|
5
|
-
async function pathExists(target) {
|
|
6
|
-
try {
|
|
7
|
-
await fs.access(target);
|
|
8
|
-
return true;
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function run(command, args, options = {}) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const child = spawn(command, args, { stdio: "inherit", ...options });
|
|
17
|
-
child.on("error", reject);
|
|
18
|
-
child.on("close", (code) => {
|
|
19
|
-
if (code === 0) {
|
|
20
|
-
resolve();
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
reject(new Error(`${command} ${args.join(" ")} failed with code ${code}`));
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function ensureRepo({ url, dir }) {
|
|
29
|
-
const hasDir = await pathExists(dir);
|
|
30
|
-
const gitDir = path.join(dir, ".git");
|
|
31
|
-
|
|
32
|
-
if (hasDir) {
|
|
33
|
-
const isGitRepo = await pathExists(gitDir);
|
|
34
|
-
if (!isGitRepo) {
|
|
35
|
-
throw new Error(`Target exists but is not a git repo: ${dir}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await run("git", ["-C", dir, "pull", "--ff-only"]);
|
|
39
|
-
return { action: "updated", dir };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
await run("git", ["clone", "--depth", "1", url, dir]);
|
|
43
|
-
return { action: "cloned", dir };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function copyDirectory(source, dest) {
|
|
60
|
-
await fs.mkdir(dest, { recursive: true });
|
|
61
|
-
const entries = await fs.readdir(source, { withFileTypes: true });
|
|
62
|
-
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
const srcPath = path.join(source, entry.name);
|
|
65
|
-
const destPath = path.join(dest, entry.name);
|
|
66
|
-
|
|
67
|
-
if (entry.isDirectory()) {
|
|
68
|
-
await copyDirectory(srcPath, destPath);
|
|
69
|
-
} else {
|
|
70
|
-
await fs.copyFile(srcPath, destPath);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function copyMcpServers(tempMcpDir, finalMcpDir) {
|
|
76
|
-
if (!(await pathExists(tempMcpDir))) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
await fs.mkdir(finalMcpDir, { recursive: true });
|
|
81
|
-
const entries = await fs.readdir(tempMcpDir, { withFileTypes: true });
|
|
82
|
-
|
|
83
|
-
for (const entry of entries) {
|
|
84
|
-
if (entry.isDirectory()) {
|
|
85
|
-
const srcPath = path.join(tempMcpDir, entry.name);
|
|
86
|
-
const destPath = path.join(finalMcpDir, entry.name);
|
|
87
|
-
await copyDirectory(srcPath, destPath);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function cleanupTemp(tempDir) {
|
|
93
|
-
if (await pathExists(tempDir)) {
|
|
94
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export {
|
|
99
|
-
ensureRepo,
|
|
100
|
-
buildMcp,
|
|
101
|
-
copyMcpServers,
|
|
102
|
-
cleanupTemp
|
|
103
|
-
};
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
|
|
5
|
+
async function pathExists(target) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.access(target);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function run(command, args, options = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn(command, args, { stdio: "inherit", ...options });
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
child.on("close", (code) => {
|
|
19
|
+
if (code === 0) {
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
reject(new Error(`${command} ${args.join(" ")} failed with code ${code}`));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureRepo({ url, dir }) {
|
|
29
|
+
const hasDir = await pathExists(dir);
|
|
30
|
+
const gitDir = path.join(dir, ".git");
|
|
31
|
+
|
|
32
|
+
if (hasDir) {
|
|
33
|
+
const isGitRepo = await pathExists(gitDir);
|
|
34
|
+
if (!isGitRepo) {
|
|
35
|
+
throw new Error(`Target exists but is not a git repo: ${dir}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await run("git", ["-C", dir, "pull", "--ff-only"]);
|
|
39
|
+
return { action: "updated", dir };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await run("git", ["clone", "--depth", "1", url, dir]);
|
|
43
|
+
return { action: "cloned", dir };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function cleanupTemp(tempDir) {
|
|
47
|
+
if (await pathExists(tempDir)) {
|
|
48
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
ensureRepo,
|
|
54
|
+
cleanupTemp
|
|
55
|
+
};
|
package/src/paths.js
CHANGED
|
@@ -1,93 +1,83 @@
|
|
|
1
|
-
import os from "os";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
function getPlatform() {
|
|
5
|
-
const platform = os.platform();
|
|
6
|
-
return {
|
|
7
|
-
platform,
|
|
8
|
-
isWindows: platform === "win32",
|
|
9
|
-
isMac: platform === "darwin",
|
|
10
|
-
isLinux: platform === "linux"
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function getTempDir() {
|
|
15
|
-
return os.tmpdir();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getAppSupportDir(platformInfo = getPlatform()) {
|
|
19
|
-
if (platformInfo.isWindows) {
|
|
20
|
-
return (
|
|
21
|
-
process.env.APPDATA ||
|
|
22
|
-
path.join(os.homedir(), "AppData", "Roaming")
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (platformInfo.isMac) {
|
|
27
|
-
return path.join(os.homedir(), "Library", "Application Support");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export {
|
|
88
|
-
getPlatform,
|
|
89
|
-
getAppSupportDir,
|
|
90
|
-
getIdePaths,
|
|
91
|
-
getMcpServerDir,
|
|
92
|
-
getTempDir
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
function getPlatform() {
|
|
5
|
+
const platform = os.platform();
|
|
6
|
+
return {
|
|
7
|
+
platform,
|
|
8
|
+
isWindows: platform === "win32",
|
|
9
|
+
isMac: platform === "darwin",
|
|
10
|
+
isLinux: platform === "linux"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getTempDir() {
|
|
15
|
+
return os.tmpdir();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAppSupportDir(platformInfo = getPlatform()) {
|
|
19
|
+
if (platformInfo.isWindows) {
|
|
20
|
+
return (
|
|
21
|
+
process.env.APPDATA ||
|
|
22
|
+
path.join(os.homedir(), "AppData", "Roaming")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (platformInfo.isMac) {
|
|
27
|
+
return path.join(os.homedir(), "Library", "Application Support");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getIdePaths(projectRoot, platformInfo = getPlatform(), ideSkillsPaths = null) {
|
|
34
|
+
const appSupport = getAppSupportDir(platformInfo);
|
|
35
|
+
const home = os.homedir();
|
|
36
|
+
|
|
37
|
+
// Default paths if not provided via config
|
|
38
|
+
const skillsPaths = ideSkillsPaths || {
|
|
39
|
+
claude: ".claude/skills",
|
|
40
|
+
cursor: ".cursor/skills",
|
|
41
|
+
codex: ".codex/skills",
|
|
42
|
+
vscode: ".vscode/skills",
|
|
43
|
+
local: ".github/skills"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
claude: {
|
|
48
|
+
name: "Claude Code",
|
|
49
|
+
mcpConfig: path.join(appSupport, "Claude", "mcp.json"),
|
|
50
|
+
mcpServerKey: "servers",
|
|
51
|
+
skillsDir: path.join(home, skillsPaths.claude),
|
|
52
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.claude)
|
|
53
|
+
},
|
|
54
|
+
cursor: {
|
|
55
|
+
name: "Cursor",
|
|
56
|
+
mcpConfig: path.join(appSupport, "Cursor", "mcp.json"),
|
|
57
|
+
mcpServerKey: "mcpServers",
|
|
58
|
+
skillsDir: path.join(home, skillsPaths.cursor),
|
|
59
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.cursor)
|
|
60
|
+
},
|
|
61
|
+
codex: {
|
|
62
|
+
name: "Codex",
|
|
63
|
+
mcpConfig: path.join(home, ".codex", "mcp.json"),
|
|
64
|
+
mcpServerKey: "servers",
|
|
65
|
+
skillsDir: path.join(home, skillsPaths.codex),
|
|
66
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.codex)
|
|
67
|
+
},
|
|
68
|
+
vscode: {
|
|
69
|
+
name: "VSCode",
|
|
70
|
+
mcpConfig: path.join(appSupport, "Code", "User", "mcp.json"),
|
|
71
|
+
mcpServerKey: "servers",
|
|
72
|
+
skillsDir: path.join(home, skillsPaths.vscode),
|
|
73
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.vscode)
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
getPlatform,
|
|
80
|
+
getAppSupportDir,
|
|
81
|
+
getIdePaths,
|
|
82
|
+
getTempDir
|
|
93
83
|
};
|