|
| 1 | +import { spawnSync } from "node:child_process"; |
| 2 | +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 3 | +import { dirname, join } from "node:path"; |
| 4 | +import { homedir } from "node:os"; |
| 5 | +import { print, printError } from "../lib/util/output.js"; |
| 6 | + |
| 7 | +const ZERION_AGENT_REPO = "zeriontech/zerion-agent"; |
| 8 | + |
| 9 | +const ZERION_MCP_SERVER = { |
| 10 | + type: "sse", |
| 11 | + url: "https://developers.zerion.io/mcp", |
| 12 | + headers: { Authorization: "Bearer ${ZERION_API_KEY}" }, |
| 13 | +}; |
| 14 | + |
| 15 | +const HELP = { |
| 16 | + usage: "zerion setup <skills|mcp> [options]", |
| 17 | + subcommands: { |
| 18 | + "setup skills": |
| 19 | + "Install Zerion agent skills via `npx skills add` (delegates to vercel-labs/skills, supports 45+ agents).", |
| 20 | + "setup mcp": |
| 21 | + "Write the Zerion hosted-MCP config fragment into a detected agent's config file.", |
| 22 | + }, |
| 23 | + flags: { |
| 24 | + "--global, -g": "Install globally instead of project scope", |
| 25 | + "--agent <name>": "Target a specific agent (e.g. claude-code, cursor, claude-desktop)", |
| 26 | + "--print": "Print the MCP config fragment to stdout instead of writing (mcp only)", |
| 27 | + "--dry-run": "Print the underlying command/target without executing", |
| 28 | + "--yes, -y": "Skip prompts (skills only — passes through to npx skills)", |
| 29 | + }, |
| 30 | + examples: { |
| 31 | + "zerion setup skills": "Interactive install across detected agents", |
| 32 | + "zerion setup skills --agent claude-code -y": "Non-interactive install into Claude Code", |
| 33 | + "zerion setup skills -g": "Install globally", |
| 34 | + "zerion setup mcp --print": "View the canonical Zerion MCP fragment", |
| 35 | + "zerion setup mcp --agent cursor": "Merge Zerion MCP into project .cursor/mcp.json", |
| 36 | + "zerion setup mcp --agent claude-desktop -g": "Merge into Claude Desktop's global config", |
| 37 | + }, |
| 38 | + source: `https://github.com/${ZERION_AGENT_REPO}`, |
| 39 | +}; |
| 40 | + |
| 41 | +const MCP_TARGETS = { |
| 42 | + cursor: { |
| 43 | + global: () => join(homedir(), ".cursor", "mcp.json"), |
| 44 | + project: () => join(process.cwd(), ".cursor", "mcp.json"), |
| 45 | + }, |
| 46 | + "claude-code": { |
| 47 | + global: () => join(homedir(), ".claude", "settings.json"), |
| 48 | + project: () => join(process.cwd(), ".claude", "settings.json"), |
| 49 | + }, |
| 50 | + "claude-desktop": { |
| 51 | + global: () => { |
| 52 | + if (process.platform === "darwin") { |
| 53 | + return join( |
| 54 | + homedir(), |
| 55 | + "Library", |
| 56 | + "Application Support", |
| 57 | + "Claude", |
| 58 | + "claude_desktop_config.json" |
| 59 | + ); |
| 60 | + } |
| 61 | + if (process.platform === "win32") { |
| 62 | + return join(homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json"); |
| 63 | + } |
| 64 | + return join(homedir(), ".config", "Claude", "claude_desktop_config.json"); |
| 65 | + }, |
| 66 | + }, |
| 67 | +}; |
| 68 | + |
| 69 | +export default async function setup(args, flags) { |
| 70 | + const [subcommand, ...rest] = args; |
| 71 | + |
| 72 | + if (flags.help || flags.h || !subcommand) { |
| 73 | + print(HELP); |
| 74 | + return; |
| 75 | + } |
| 76 | + |
| 77 | + switch (subcommand) { |
| 78 | + case "skills": |
| 79 | + return setupSkills(rest, flags); |
| 80 | + case "mcp": |
| 81 | + return setupMcp(rest, flags); |
| 82 | + default: |
| 83 | + printError( |
| 84 | + "unknown_subcommand", |
| 85 | + `Unknown setup subcommand: ${subcommand}`, |
| 86 | + { suggestion: "Run 'zerion setup --help' for usage", subcommand } |
| 87 | + ); |
| 88 | + process.exit(1); |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +function setupSkills(_args, flags) { |
| 93 | + const npxArgs = ["-y", "skills", "add", ZERION_AGENT_REPO]; |
| 94 | + if (flags.global || flags.g) npxArgs.push("-g"); |
| 95 | + if (flags.agent) npxArgs.push("-a", flags.agent); |
| 96 | + if (flags.yes || flags.y) npxArgs.push("--yes"); |
| 97 | + |
| 98 | + const renderedCommand = `npx ${npxArgs.join(" ")}`; |
| 99 | + |
| 100 | + if (flags["dry-run"]) { |
| 101 | + print({ ok: true, dryRun: true, command: renderedCommand, source: ZERION_AGENT_REPO }); |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + const res = spawnSync("npx", npxArgs, { stdio: "inherit" }); |
| 106 | + if (res.status !== 0) { |
| 107 | + printError("skills_install_failed", "npx skills add returned non-zero", { |
| 108 | + exitCode: res.status, |
| 109 | + command: renderedCommand, |
| 110 | + }); |
| 111 | + process.exit(res.status ?? 1); |
| 112 | + } |
| 113 | + print({ ok: true, action: "setup-skills", source: ZERION_AGENT_REPO }); |
| 114 | +} |
| 115 | + |
| 116 | +function setupMcp(_args, flags) { |
| 117 | + if (flags.print) { |
| 118 | + const fragment = { mcpServers: { zerion: ZERION_MCP_SERVER } }; |
| 119 | + process.stdout.write(JSON.stringify(fragment, null, 2) + "\n"); |
| 120 | + return; |
| 121 | + } |
| 122 | + |
| 123 | + const agent = flags.agent; |
| 124 | + if (!agent) { |
| 125 | + printError( |
| 126 | + "missing_agent", |
| 127 | + "Specify --agent <name> (cursor, claude-code, claude-desktop) or use --print to view the fragment.", |
| 128 | + { supportedAgents: Object.keys(MCP_TARGETS) } |
| 129 | + ); |
| 130 | + process.exit(1); |
| 131 | + } |
| 132 | + |
| 133 | + const target = MCP_TARGETS[agent]; |
| 134 | + if (!target) { |
| 135 | + printError("unknown_agent", `Unknown agent: ${agent}`, { |
| 136 | + supportedAgents: Object.keys(MCP_TARGETS), |
| 137 | + }); |
| 138 | + process.exit(1); |
| 139 | + } |
| 140 | + |
| 141 | + const wantGlobal = flags.global || flags.g; |
| 142 | + const scope = wantGlobal || !target.project ? "global" : "project"; |
| 143 | + const path = target[scope](); |
| 144 | + |
| 145 | + if (flags["dry-run"]) { |
| 146 | + print({ ok: true, dryRun: true, agent, scope, target: path }); |
| 147 | + return; |
| 148 | + } |
| 149 | + |
| 150 | + let config = {}; |
| 151 | + if (existsSync(path)) { |
| 152 | + try { |
| 153 | + config = JSON.parse(readFileSync(path, "utf8")); |
| 154 | + } catch (err) { |
| 155 | + printError("invalid_config", `Existing config at ${path} is not valid JSON`, { |
| 156 | + error: err.message, |
| 157 | + }); |
| 158 | + process.exit(1); |
| 159 | + } |
| 160 | + } else { |
| 161 | + mkdirSync(dirname(path), { recursive: true }); |
| 162 | + } |
| 163 | + |
| 164 | + config.mcpServers = config.mcpServers || {}; |
| 165 | + config.mcpServers.zerion = ZERION_MCP_SERVER; |
| 166 | + writeFileSync(path, JSON.stringify(config, null, 2) + "\n"); |
| 167 | + |
| 168 | + print({ ok: true, action: "setup-mcp", agent, scope, target: path }); |
| 169 | +} |
0 commit comments