a11y-devkit-deploy 0.7.1 → 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/README.md +165 -25
- package/config/a11y.json +90 -8
- package/package.json +1 -1
- package/src/cli.js +327 -55
- package/src/installers/git-mcp.js +209 -0
- package/src/installers/mcp.js +8 -1
- package/src/paths.js +42 -6
- package/src/prompts/git-mcp.js +140 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# A11y Devkit Deploy
|
|
2
2
|
|
|
3
|
-
A **fully config-driven**, cross-platform CLI for deploying accessibility skills and MCP servers across Claude Code, Cursor, Codex, and
|
|
3
|
+
A **fully config-driven**, cross-platform CLI for deploying accessibility skills and MCP servers across multiple IDEs: Claude Code, Cursor, Codex, VSCode, Windsurf, and Factory.
|
|
4
4
|
|
|
5
|
-
**Add new skills
|
|
5
|
+
**Add new skills, MCP servers, or entire IDEs without writing code** - just edit the JSON config and re-run.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -27,8 +27,8 @@ a11y-devkit-deploy
|
|
|
27
27
|
|
|
28
28
|
Once installation completes, you'll find a comprehensive usage guide in your IDE's skills directory:
|
|
29
29
|
|
|
30
|
-
- **Local install**: `.claude/skills/README.md` (or `.cursor/skills/`, `.codex/skills/` depending on your IDE)
|
|
31
|
-
- **Global install**: `~/.claude/skills/README.md` (or your IDE's global skills directory)
|
|
30
|
+
- **Local install**: `.claude/skills/a11y/a11y-devkit-README.md` (or `.cursor/skills/a11y/`, `.codex/skills/a11y/` depending on your IDE)
|
|
31
|
+
- **Global install**: `~/.claude/skills/a11y/a11y-devkit-README.md` (or your IDE's global skills directory)
|
|
32
32
|
|
|
33
33
|
### What's in the Guide
|
|
34
34
|
|
|
@@ -42,7 +42,7 @@ The bundled README includes:
|
|
|
42
42
|
|
|
43
43
|
### Preview the Guide
|
|
44
44
|
|
|
45
|
-
You can preview the guide here: [templates/
|
|
45
|
+
You can preview the guide here: [templates/deploy-README.md](templates/deploy-README.md)
|
|
46
46
|
|
|
47
47
|
### Next Steps
|
|
48
48
|
|
|
@@ -75,7 +75,31 @@ This CLI automates the setup of accessibility tooling by:
|
|
|
75
75
|
- **a11y-personas** - Accessibility personas for diverse user needs
|
|
76
76
|
- **arc-issues** - Format AxeCore violations into standardized issue templates
|
|
77
77
|
|
|
78
|
-
**Fully customizable** - add/remove skills
|
|
78
|
+
**Fully customizable** - add/remove skills, MCP servers, or entire IDEs by editing the config file.
|
|
79
|
+
|
|
80
|
+
## Why This Tool?
|
|
81
|
+
|
|
82
|
+
**Zero Hardcoded Values** - Every aspect of the tool is driven by `config/a11y.json`:
|
|
83
|
+
- IDE paths and configuration files
|
|
84
|
+
- Skills to install
|
|
85
|
+
- MCP servers to configure
|
|
86
|
+
- Even the IDE list itself
|
|
87
|
+
|
|
88
|
+
**Adding Support for a New IDE** takes just 5 lines of JSON:
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"id": "new-ide",
|
|
92
|
+
"displayName": "New IDE",
|
|
93
|
+
"mcpServerKey": "servers",
|
|
94
|
+
"skillsFolder": ".new-ide/skills",
|
|
95
|
+
"mcpConfigFile": ".new-ide/mcp.json"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Safe by Default** - Won't overwrite your existing:
|
|
100
|
+
- Custom MCP servers in your IDE configs
|
|
101
|
+
- Other skills in your skills directories
|
|
102
|
+
- Creates backups if it encounters JSON parsing errors
|
|
79
103
|
|
|
80
104
|
### Skills Installed (Default)
|
|
81
105
|
|
|
@@ -118,9 +142,9 @@ The generated MCP config looks like this:
|
|
|
118
142
|
|
|
119
143
|
## Configuration
|
|
120
144
|
|
|
121
|
-
The entire tool is **fully config-driven**. Edit `config/a11y.json` to customize everything without touching code
|
|
145
|
+
The entire tool is **fully config-driven**. Edit `config/a11y.json` to customize everything without touching code.
|
|
122
146
|
|
|
123
|
-
###
|
|
147
|
+
### Adding a New Skill
|
|
124
148
|
|
|
125
149
|
Simply add an object to the `skills` array with a `name` (npm package) and `description`:
|
|
126
150
|
|
|
@@ -139,7 +163,7 @@ Simply add an object to the `skills` array with a `name` (npm package) and `desc
|
|
|
139
163
|
}
|
|
140
164
|
```
|
|
141
165
|
|
|
142
|
-
###
|
|
166
|
+
### Adding a New MCP Server
|
|
143
167
|
|
|
144
168
|
Add an object to the `mcpServers` array with name, description, command, and args:
|
|
145
169
|
|
|
@@ -162,40 +186,96 @@ Add an object to the `mcpServers` array with name, description, command, and arg
|
|
|
162
186
|
}
|
|
163
187
|
```
|
|
164
188
|
|
|
189
|
+
### Adding a New IDE
|
|
190
|
+
|
|
191
|
+
Add an object to the `ides` array with the IDE's configuration:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"ides": [
|
|
196
|
+
{
|
|
197
|
+
"id": "windsurf",
|
|
198
|
+
"displayName": "Windsurf",
|
|
199
|
+
"mcpServerKey": "servers",
|
|
200
|
+
"skillsFolder": ".codeium/windsurf/skills",
|
|
201
|
+
"mcpConfigFile": ".codeium/windsurf/mcp_config.json"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**IDE Configuration Properties:**
|
|
208
|
+
- `id` - Unique identifier for the IDE
|
|
209
|
+
- `displayName` - Human-readable name shown in prompts
|
|
210
|
+
- `mcpServerKey` - MCP config key name (`"servers"` or `"mcpServers"`)
|
|
211
|
+
- `skillsFolder` - Path to skills directory (relative to home/project root)
|
|
212
|
+
- `mcpConfigFile` - Path to MCP config file (relative to home/project root)
|
|
213
|
+
|
|
165
214
|
### Config Structure
|
|
166
215
|
|
|
216
|
+
- `skillsFolder` - Subfolder name to bundle skills under (e.g., "a11y")
|
|
217
|
+
- `readmeTemplate` - README template file to copy into skills directories
|
|
167
218
|
- `skills` - Array of skill objects with `name` (npm package) and `description`
|
|
168
|
-
- `
|
|
169
|
-
- `ideMcpPaths` - IDE-specific MCP config file paths
|
|
219
|
+
- `ides` - Array of IDE configuration objects
|
|
170
220
|
- `mcpServers` - MCP server definitions with name, description, command, and args
|
|
171
221
|
|
|
172
222
|
All changes take effect immediately - just re-run the CLI to deploy your updated config.
|
|
173
223
|
|
|
224
|
+
### Safe Merging
|
|
225
|
+
|
|
226
|
+
The CLI **safely merges** with existing configurations:
|
|
227
|
+
- **MCP configs** - Adds/updates only the specified servers, preserves others
|
|
228
|
+
- **Skills** - Installs only the configured skills, preserves other skills in the directory
|
|
229
|
+
- **Backups** - Creates `.bak` files if JSON parsing fails
|
|
230
|
+
|
|
174
231
|
## Directory Structure
|
|
175
232
|
|
|
176
233
|
### Local Install (Project-Specific)
|
|
177
234
|
```
|
|
178
235
|
your-project/
|
|
179
|
-
├── .claude/
|
|
180
|
-
├── .
|
|
181
|
-
|
|
182
|
-
|
|
236
|
+
├── .claude/
|
|
237
|
+
│ ├── mcp.json # Claude Code MCP config
|
|
238
|
+
│ └── skills/ # Claude Code skills
|
|
239
|
+
├── .cursor/
|
|
240
|
+
│ ├── mcp.json # Cursor MCP config
|
|
241
|
+
│ └── skills/ # Cursor skills
|
|
242
|
+
├── .codex/
|
|
243
|
+
│ ├── mcp.json # Codex MCP config
|
|
244
|
+
│ └── skills/ # Codex skills
|
|
245
|
+
├── .github/
|
|
246
|
+
│ ├── mcp.json # VSCode MCP config
|
|
247
|
+
│ └── skills/ # VSCode skills
|
|
248
|
+
├── .codeium/windsurf/
|
|
249
|
+
│ ├── mcp_config.json # Windsurf MCP config
|
|
250
|
+
│ └── skills/ # Windsurf skills
|
|
251
|
+
└── .factory/
|
|
252
|
+
├── mcp.json # Factory MCP config
|
|
253
|
+
└── skills/ # Factory skills
|
|
183
254
|
```
|
|
184
255
|
|
|
185
256
|
### Global Install (User-Wide)
|
|
186
257
|
```
|
|
187
|
-
~/.claude/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
~/.
|
|
258
|
+
~/.claude/
|
|
259
|
+
├── mcp.json # Claude Code global MCP config
|
|
260
|
+
└── skills/ # Claude Code global skills
|
|
261
|
+
~/.cursor/
|
|
262
|
+
├── mcp.json # Cursor global MCP config
|
|
263
|
+
└── skills/ # Cursor global skills
|
|
264
|
+
~/.codex/
|
|
265
|
+
├── mcp.json # Codex global MCP config
|
|
266
|
+
└── skills/ # Codex global skills
|
|
267
|
+
~/.github/
|
|
268
|
+
├── mcp.json # VSCode global MCP config
|
|
269
|
+
└── skills/ # VSCode global skills
|
|
270
|
+
~/.codeium/windsurf/
|
|
271
|
+
├── mcp_config.json # Windsurf global MCP config
|
|
272
|
+
└── skills/ # Windsurf global skills
|
|
273
|
+
~/.factory/
|
|
274
|
+
├── mcp.json # Factory global MCP config
|
|
275
|
+
└── skills/ # Factory global skills
|
|
191
276
|
```
|
|
192
277
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
MCP configurations are written to each IDE's OS-specific config path:
|
|
196
|
-
- **macOS**: `~/Library/Application Support/{IDE}/mcp.json`
|
|
197
|
-
- **Windows**: `%APPDATA%\{IDE}\mcp.json`
|
|
198
|
-
- **Linux**: `~/.config/{IDE}/mcp.json`
|
|
278
|
+
**Note:** Paths are fully customizable per IDE in `config/a11y.json`
|
|
199
279
|
|
|
200
280
|
## MCP Servers Included (Default)
|
|
201
281
|
|
|
@@ -208,3 +288,63 @@ MCP configurations are written to each IDE's OS-specific config path:
|
|
|
208
288
|
| magentaa11y | `magentaa11y-mcp` | Component accessibility acceptance criteria |
|
|
209
289
|
| a11y-personas | `a11y-personas-mcp` | Accessibility personas for diverse users |
|
|
210
290
|
| arc-issues | `arc-issues-mcp` | AxeCore violation formatting |
|
|
291
|
+
|
|
292
|
+
## Complete Config Example
|
|
293
|
+
|
|
294
|
+
Here's what a complete `config/a11y.json` looks like:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"skillsFolder": "a11y",
|
|
299
|
+
"readmeTemplate": "deploy-README.md",
|
|
300
|
+
"skills": [
|
|
301
|
+
{
|
|
302
|
+
"name": "a11y-base-web-skill",
|
|
303
|
+
"description": "Core accessibility testing utilities"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "a11y-tester-skill",
|
|
307
|
+
"description": "Run accessibility tests"
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
"ides": [
|
|
311
|
+
{
|
|
312
|
+
"id": "claude",
|
|
313
|
+
"displayName": "Claude Code",
|
|
314
|
+
"mcpServerKey": "servers",
|
|
315
|
+
"skillsFolder": ".claude/skills",
|
|
316
|
+
"mcpConfigFile": ".claude/mcp.json"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"id": "cursor",
|
|
320
|
+
"displayName": "Cursor",
|
|
321
|
+
"mcpServerKey": "mcpServers",
|
|
322
|
+
"skillsFolder": ".cursor/skills",
|
|
323
|
+
"mcpConfigFile": ".cursor/mcp.json"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"id": "windsurf",
|
|
327
|
+
"displayName": "Windsurf",
|
|
328
|
+
"mcpServerKey": "servers",
|
|
329
|
+
"skillsFolder": ".codeium/windsurf/skills",
|
|
330
|
+
"mcpConfigFile": ".codeium/windsurf/mcp_config.json"
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
"mcpServers": [
|
|
334
|
+
{
|
|
335
|
+
"name": "wcag",
|
|
336
|
+
"description": "WCAG guidelines reference",
|
|
337
|
+
"command": "npx",
|
|
338
|
+
"args": ["-y", "wcag-mcp"]
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "aria",
|
|
342
|
+
"description": "ARIA specification reference",
|
|
343
|
+
"command": "npx",
|
|
344
|
+
"args": ["-y", "aria-mcp"]
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Everything is customizable - add, remove, or modify any section to match your needs.
|
package/config/a11y.json
CHANGED
|
@@ -3,31 +3,45 @@
|
|
|
3
3
|
"readmeTemplate": "deploy-README.md",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
|
-
"name": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
170
|
-
const skillsSpinner = startSpinner("Installing skills from npm...");
|
|
251
|
+
const skillsSpinner = startSpinner("Installing skills from npm...");
|
|
171
252
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/installers/mcp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
70
|
+
mcpConfig: path.join(globalBase, globalMcpConfigFile),
|
|
45
71
|
localMcpConfig: path.join(projectRoot, mcpConfigFile),
|
|
46
72
|
mcpServerKey: ide.mcpServerKey,
|
|
47
|
-
skillsDir: path.join(
|
|
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
|
+
}
|