a11y-devkit-deploy 0.4.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 +101 -0
- package/bin/a11y-skills.js +9 -0
- package/config/a11y.json +73 -0
- package/package.json +49 -0
- package/src/cli.js +194 -0
- package/src/installers/mcp.js +69 -0
- package/src/installers/repo.js +103 -0
- package/src/installers/skills.js +31 -0
- package/src/paths.js +93 -0
- package/src/ui.js +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# A11y Devkit Deploy
|
|
2
|
+
|
|
3
|
+
A cross-platform CLI for deploying accessibility skills and MCP servers across Claude Code, Cursor, Codex, and VSCode. Automatically clones the a11y-skills repo and all required MCP server repositories.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g a11y-devkit-deploy
|
|
9
|
+
# or
|
|
10
|
+
npx a11y-devkit-deploy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
a11y-devkit-deploy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Flags
|
|
20
|
+
|
|
21
|
+
- `--local` / `--global`: Skip the scope prompt.
|
|
22
|
+
- `--yes`: Use defaults (local scope, all IDEs, install skills).
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
This CLI automates the setup of accessibility tooling by:
|
|
27
|
+
|
|
28
|
+
1. **Cloning the a11y-skills repository** - Contains IDE skills for accessibility workflows
|
|
29
|
+
2. **Cloning and building MCP server repositories** - Installs 5 accessibility-focused MCP servers:
|
|
30
|
+
- **wcag-mcp** - WCAG 2.2 guidelines, success criteria, and techniques
|
|
31
|
+
- **aria-mcp** - WAI-ARIA roles, states, properties, and patterns
|
|
32
|
+
- **magentaa11y** - Component accessibility acceptance criteria
|
|
33
|
+
- **a11y-personas-mcp** - Accessibility personas for diverse user needs
|
|
34
|
+
- **a11y-issues-template-mcp** - Format AxeCore violations into standardized issue templates
|
|
35
|
+
3. **Installing skills** - Copies skills to IDE-specific directories based on scope (local/global)
|
|
36
|
+
4. **Installing MCP servers** - Copies built MCP servers to home directory:
|
|
37
|
+
- Single IDE: `~/.{ide}/mcp/servers/` (e.g., `~/.claude/mcp/servers/`)
|
|
38
|
+
- Multiple IDEs: `~/.mcp/servers/` (shared location)
|
|
39
|
+
5. **Configuring MCP servers** - Updates each IDE's MCP config to enable the accessibility tools
|
|
40
|
+
6. **Cleanup** - Removes temporary build directory after installation
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Edit `config/a11y.json` to customize the deployment:
|
|
45
|
+
|
|
46
|
+
- `repo.url` - Main skills repository to clone
|
|
47
|
+
- `mcpRepos` - Array of MCP repositories to clone and build
|
|
48
|
+
- `skillsSearchPaths` - Directories to search for skills in the cloned repo
|
|
49
|
+
- `ideSkillsPaths` - IDE-specific skills directories (configurable per IDE)
|
|
50
|
+
- `mcpServers` - MCP server definitions with placeholders:
|
|
51
|
+
- `{mcpRepoDir}` - Path to the MCP servers directory (e.g., `~/.mcp/servers/` or `~/.claude/mcp/servers/`)
|
|
52
|
+
|
|
53
|
+
## Directory Structure
|
|
54
|
+
|
|
55
|
+
### Local Install (Project-Specific)
|
|
56
|
+
```
|
|
57
|
+
your-project/
|
|
58
|
+
├── .claude/skills/ # Skills copied to Claude Code (if selected)
|
|
59
|
+
├── .cursor/skills/ # Skills copied to Cursor (if selected)
|
|
60
|
+
├── .codex/skills/ # Skills copied to Codex (if selected)
|
|
61
|
+
└── .github/skills/ # Skills copied here for version control
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Global Install (User-Wide)
|
|
65
|
+
```
|
|
66
|
+
~/.claude/skills/ # Claude Code skills
|
|
67
|
+
~/.cursor/skills/ # Cursor skills
|
|
68
|
+
~/.codex/skills/ # Codex skills
|
|
69
|
+
~/.vscode/skills/ # VSCode skills
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### MCP Server Locations
|
|
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
|
+
```
|
|
94
|
+
|
|
95
|
+
MCP configurations are written to each IDE's OS-specific config path:
|
|
96
|
+
- **macOS**: `~/Library/Application Support/{IDE}/mcp.json`
|
|
97
|
+
- **Windows**: `%APPDATA%\{IDE}\mcp.json`
|
|
98
|
+
- **Linux**: `~/.config/{IDE}/mcp.json`
|
|
99
|
+
|
|
100
|
+
### Temporary Build Directory
|
|
101
|
+
During installation, repos are cloned and built in a temporary directory (OS temp folder) which is automatically cleaned up after completion.
|
package/config/a11y.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": {
|
|
3
|
+
"url": "https://github.com/joe-watkins/a11y-skills",
|
|
4
|
+
"dirName": "a11y-skills"
|
|
5
|
+
},
|
|
6
|
+
"mcpRepos": [
|
|
7
|
+
{
|
|
8
|
+
"url": "https://github.com/joe-watkins/wcag-mcp",
|
|
9
|
+
"dirName": "wcag-mcp",
|
|
10
|
+
"buildCommands": ["npm install", "npm run build"]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"url": "https://github.com/joe-watkins/aria-mcp",
|
|
14
|
+
"dirName": "aria-mcp",
|
|
15
|
+
"buildCommands": ["npm install"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"url": "https://github.com/joe-watkins/magentaa11y-mcp",
|
|
19
|
+
"dirName": "magentaa11y-mcp",
|
|
20
|
+
"buildCommands": ["npm install", "npm run update-content"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"url": "https://github.com/joe-watkins/a11y-personas-mcp",
|
|
24
|
+
"dirName": "a11y-personas-mcp",
|
|
25
|
+
"buildCommands": ["npm install", "npm run build"]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"url": "https://github.com/joe-watkins/accessibility-issues-template-mcp",
|
|
29
|
+
"dirName": "a11y-issues-template-mcp",
|
|
30
|
+
"buildCommands": ["npm install", "npm run build"]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"skillsSearchPaths": [
|
|
34
|
+
".",
|
|
35
|
+
"skills",
|
|
36
|
+
".github/skills",
|
|
37
|
+
".codex/skills"
|
|
38
|
+
],
|
|
39
|
+
"ideSkillsPaths": {
|
|
40
|
+
"claude": ".claude/skills",
|
|
41
|
+
"cursor": ".cursor/skills",
|
|
42
|
+
"codex": ".codex/skills",
|
|
43
|
+
"vscode": ".github/skills",
|
|
44
|
+
"local": ".github/skills"
|
|
45
|
+
},
|
|
46
|
+
"mcpServers": [
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "a11y-devkit-deploy",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "CLI to deploy a11y skills and MCP servers across IDEs",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/cli.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"a11y-devkit-deploy": "bin/a11y-skills.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"config",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"accessibility",
|
|
19
|
+
"a11y",
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"copilot",
|
|
23
|
+
"skills",
|
|
24
|
+
"wcag",
|
|
25
|
+
"aria",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/joe-watkins/a11y-skills-npm.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/joe-watkins/a11y-skills-npm/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/joe-watkins/a11y-skills-npm#readme",
|
|
36
|
+
"author": "Joe Watkins",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"start": "node bin/a11y-skills.js"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"boxen": "^5.1.2",
|
|
45
|
+
"ora": "^6.3.1",
|
|
46
|
+
"picocolors": "^1.1.0",
|
|
47
|
+
"prompts": "^2.4.2"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,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, getMcpServerDir, getTempDir } from "./paths.js";
|
|
9
|
+
import { ensureRepo, buildMcp, copyMcpServers, cleanupTemp } from "./installers/repo.js";
|
|
10
|
+
import { findSkillsDir, copySkills } from "./installers/skills.js";
|
|
11
|
+
import { resolveServers, 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
|
+
const homeDir = os.homedir();
|
|
44
|
+
|
|
45
|
+
header("A11y Devkit Deploy", "Install skills + MCP servers across IDEs");
|
|
46
|
+
info(`Detected OS: ${formatOs(platformInfo)}`);
|
|
47
|
+
|
|
48
|
+
const ideChoices = [
|
|
49
|
+
{ title: "Claude Code", value: "claude" },
|
|
50
|
+
{ title: "Cursor", value: "cursor" },
|
|
51
|
+
{ title: "Codex", value: "codex" },
|
|
52
|
+
{ title: "VSCode", value: "vscode" }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
let scope = args.scope;
|
|
56
|
+
let ideSelection = ["claude", "cursor", "codex", "vscode"];
|
|
57
|
+
let installSkills = true;
|
|
58
|
+
|
|
59
|
+
if (!args.autoYes) {
|
|
60
|
+
const response = await prompts(
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
type: scope ? null : "select",
|
|
64
|
+
name: "scope",
|
|
65
|
+
message: "Install skills + repo locally or globally?",
|
|
66
|
+
choices: [
|
|
67
|
+
{ title: "Local to this project", value: "local" },
|
|
68
|
+
{ title: "Global for this user", value: "global" }
|
|
69
|
+
],
|
|
70
|
+
initial: 0
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "multiselect",
|
|
74
|
+
name: "ides",
|
|
75
|
+
message: "Configure MCP for which IDEs?",
|
|
76
|
+
choices: ideChoices,
|
|
77
|
+
initial: ideChoices.map((_, index) => index)
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "toggle",
|
|
81
|
+
name: "installSkills",
|
|
82
|
+
message: "Install skills into IDE skills folders?",
|
|
83
|
+
active: "yes",
|
|
84
|
+
inactive: "no",
|
|
85
|
+
initial: true
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
{
|
|
89
|
+
onCancel: () => {
|
|
90
|
+
warn("Setup cancelled.");
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
scope = scope || response.scope;
|
|
97
|
+
ideSelection = response.ides || ideSelection;
|
|
98
|
+
installSkills = response.installSkills;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!scope) {
|
|
102
|
+
scope = "local";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!ideSelection.length) {
|
|
106
|
+
warn("No IDEs selected. MCP installation requires at least one IDE.");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
info(`Install scope: ${scope === "local" ? "Local" : "Global"}`);
|
|
111
|
+
|
|
112
|
+
// Create temp directory for cloning and building
|
|
113
|
+
const tempDir = path.join(getTempDir(), `.a11y-devkit-${Date.now()}`);
|
|
114
|
+
const tempSkillsDir = path.join(tempDir, "skills");
|
|
115
|
+
const tempMcpDir = path.join(tempDir, "mcp");
|
|
116
|
+
|
|
117
|
+
// Determine MCP server destination based on IDE selection
|
|
118
|
+
const mcpServerDir = getMcpServerDir(homeDir, ideSelection);
|
|
119
|
+
info(`MCP servers: ${formatPath(mcpServerDir)}`);
|
|
120
|
+
|
|
121
|
+
// Clone skills repo into temp
|
|
122
|
+
const repoSpinner = startSpinner("Syncing a11y-skills repo...");
|
|
123
|
+
const repoResult = await ensureRepo({
|
|
124
|
+
url: config.repo.url,
|
|
125
|
+
dir: tempSkillsDir
|
|
126
|
+
});
|
|
127
|
+
repoSpinner.succeed(`Repo ${repoResult.action}: ${formatPath(repoResult.dir)}`);
|
|
128
|
+
|
|
129
|
+
// Clone and build MCP repos in temp
|
|
130
|
+
if (config.mcpRepos && config.mcpRepos.length > 0) {
|
|
131
|
+
const mcpSpinner = startSpinner(`Syncing ${config.mcpRepos.length} MCP repos...`);
|
|
132
|
+
for (const mcpRepo of config.mcpRepos) {
|
|
133
|
+
const mcpDir = path.join(tempMcpDir, mcpRepo.dirName);
|
|
134
|
+
await ensureRepo({
|
|
135
|
+
url: mcpRepo.url,
|
|
136
|
+
dir: mcpDir
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Build MCP if build commands are specified
|
|
140
|
+
if (mcpRepo.buildCommands) {
|
|
141
|
+
mcpSpinner.text = `Building ${mcpRepo.dirName}...`;
|
|
142
|
+
await buildMcp({
|
|
143
|
+
dir: mcpDir,
|
|
144
|
+
buildCommands: mcpRepo.buildCommands
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
mcpSpinner.succeed(`MCP repos synced and built in temp directory`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Copy MCP servers from temp to final location
|
|
152
|
+
const copySpinner = startSpinner("Installing MCP servers...");
|
|
153
|
+
await copyMcpServers(tempMcpDir, mcpServerDir);
|
|
154
|
+
copySpinner.succeed(`MCP servers installed to ${formatPath(mcpServerDir)}`);
|
|
155
|
+
|
|
156
|
+
if (installSkills) {
|
|
157
|
+
const skillsSpinner = startSpinner("Installing skills to IDE folders...");
|
|
158
|
+
const sourceDir = await findSkillsDir(tempSkillsDir, config.skillsSearchPaths);
|
|
159
|
+
if (!sourceDir) {
|
|
160
|
+
skillsSpinner.fail("No skills directory found in repo.");
|
|
161
|
+
} else {
|
|
162
|
+
const skillTargets = scope === "local"
|
|
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
|
+
};
|
|
@@ -0,0 +1,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 resolveServers(servers, repoDir, mcpRepoDir) {
|
|
29
|
+
return servers.map((server) => {
|
|
30
|
+
const args = Array.isArray(server.args) ? server.args : [];
|
|
31
|
+
return {
|
|
32
|
+
...server,
|
|
33
|
+
args: args.map((value) =>
|
|
34
|
+
value.replace("{repoDir}", repoDir).replace("{mcpRepoDir}", mcpRepoDir)
|
|
35
|
+
)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mergeServers(existing, incoming, serverKey = "servers") {
|
|
41
|
+
const existingServers = existing[serverKey] && typeof existing[serverKey] === "object"
|
|
42
|
+
? existing[serverKey]
|
|
43
|
+
: {};
|
|
44
|
+
|
|
45
|
+
const merged = { ...existing, [serverKey]: { ...existingServers } };
|
|
46
|
+
|
|
47
|
+
for (const server of incoming) {
|
|
48
|
+
merged[serverKey][server.name] = {
|
|
49
|
+
command: server.command,
|
|
50
|
+
args: server.args || [],
|
|
51
|
+
env: server.env || {},
|
|
52
|
+
cwd: server.cwd
|
|
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
|
+
};
|
|
@@ -0,0 +1,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 buildMcp({ dir, buildCommands }) {
|
|
47
|
+
if (!buildCommands || buildCommands.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const command of buildCommands) {
|
|
52
|
+
const parts = command.split(" ");
|
|
53
|
+
const cmd = parts[0];
|
|
54
|
+
const args = parts.slice(1);
|
|
55
|
+
await run(cmd, args, { cwd: dir, shell: true });
|
|
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
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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 findSkillsDir(repoDir, candidates) {
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
const fullPath = path.join(repoDir, candidate);
|
|
16
|
+
if (await pathExists(fullPath)) {
|
|
17
|
+
return fullPath;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function copySkills(sourceDir, targetDir) {
|
|
24
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
25
|
+
await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
findSkillsDir,
|
|
30
|
+
copySkills
|
|
31
|
+
};
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
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 getMcpServerDir(home, ideSelection) {
|
|
34
|
+
// Single IDE: use IDE-specific path (~/.claude/mcp/servers/)
|
|
35
|
+
// Multiple IDEs: use shared path (~/.mcp/servers/)
|
|
36
|
+
if (ideSelection.length === 1) {
|
|
37
|
+
return path.join(home, `.${ideSelection[0]}`, "mcp", "servers");
|
|
38
|
+
}
|
|
39
|
+
return path.join(home, ".mcp", "servers");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getIdePaths(projectRoot, platformInfo = getPlatform(), ideSkillsPaths = null) {
|
|
43
|
+
const appSupport = getAppSupportDir(platformInfo);
|
|
44
|
+
const home = os.homedir();
|
|
45
|
+
|
|
46
|
+
// Default paths if not provided via config
|
|
47
|
+
const skillsPaths = ideSkillsPaths || {
|
|
48
|
+
claude: ".claude/skills",
|
|
49
|
+
cursor: ".cursor/skills",
|
|
50
|
+
codex: ".codex/skills",
|
|
51
|
+
vscode: ".vscode/skills",
|
|
52
|
+
local: ".github/skills"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
claude: {
|
|
57
|
+
name: "Claude Code",
|
|
58
|
+
mcpConfig: path.join(appSupport, "Claude", "mcp.json"),
|
|
59
|
+
mcpServerKey: "servers",
|
|
60
|
+
skillsDir: path.join(home, skillsPaths.claude),
|
|
61
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.claude)
|
|
62
|
+
},
|
|
63
|
+
cursor: {
|
|
64
|
+
name: "Cursor",
|
|
65
|
+
mcpConfig: path.join(appSupport, "Cursor", "mcp.json"),
|
|
66
|
+
mcpServerKey: "mcpServers",
|
|
67
|
+
skillsDir: path.join(home, skillsPaths.cursor),
|
|
68
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.cursor)
|
|
69
|
+
},
|
|
70
|
+
codex: {
|
|
71
|
+
name: "Codex",
|
|
72
|
+
mcpConfig: path.join(home, ".codex", "mcp.json"),
|
|
73
|
+
mcpServerKey: "servers",
|
|
74
|
+
skillsDir: path.join(home, skillsPaths.codex),
|
|
75
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.codex)
|
|
76
|
+
},
|
|
77
|
+
vscode: {
|
|
78
|
+
name: "VSCode",
|
|
79
|
+
mcpConfig: path.join(appSupport, "Code", "User", "mcp.json"),
|
|
80
|
+
mcpServerKey: "servers",
|
|
81
|
+
skillsDir: path.join(home, skillsPaths.vscode),
|
|
82
|
+
localSkillsDir: path.join(projectRoot, skillsPaths.vscode)
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
getPlatform,
|
|
89
|
+
getAppSupportDir,
|
|
90
|
+
getIdePaths,
|
|
91
|
+
getMcpServerDir,
|
|
92
|
+
getTempDir
|
|
93
|
+
};
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import boxen from "boxen";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
|
|
5
|
+
const bullets = {
|
|
6
|
+
info: pc.cyan("i"),
|
|
7
|
+
warn: pc.yellow("!"),
|
|
8
|
+
error: pc.red("x"),
|
|
9
|
+
success: pc.green("ok")
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function header(title, subtitle) {
|
|
13
|
+
const line = subtitle ? `${pc.dim(subtitle)}` : "";
|
|
14
|
+
const content = [pc.bold(title), line].filter(Boolean).join("\n");
|
|
15
|
+
console.log(
|
|
16
|
+
boxen(content, {
|
|
17
|
+
padding: 1,
|
|
18
|
+
margin: { top: 1, bottom: 1 },
|
|
19
|
+
borderStyle: "round",
|
|
20
|
+
borderColor: "cyan"
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function info(message) {
|
|
26
|
+
console.log(`${bullets.info} ${message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function warn(message) {
|
|
30
|
+
console.log(`${bullets.warn} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function success(message) {
|
|
34
|
+
console.log(`${bullets.success} ${message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function error(message) {
|
|
38
|
+
console.log(`${bullets.error} ${message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function startSpinner(text) {
|
|
42
|
+
return ora({ text, spinner: "dots" }).start();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatPath(value) {
|
|
46
|
+
return pc.dim(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
header,
|
|
51
|
+
info,
|
|
52
|
+
warn,
|
|
53
|
+
success,
|
|
54
|
+
error,
|
|
55
|
+
startSpinner,
|
|
56
|
+
formatPath
|
|
57
|
+
};
|