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 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.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../src/cli.js";
4
+
5
+ run().catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ console.error(`\n[Error] ${message}`);
8
+ process.exitCode = 1;
9
+ });
@@ -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
+ };