@jaybeeuu/agent-uplink 1.0.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.
@@ -0,0 +1,98 @@
1
+ import { readFileSync } from "fs";
2
+ import { Command } from "commander";
3
+ import {
4
+ createCapability,
5
+ listCapabilities,
6
+ deleteCapability,
7
+ getCapabilityPath,
8
+ capabilityExists,
9
+ } from "../capabilities.js";
10
+ import { getCapabilitiesDir, getConfigValue } from "../config.js";
11
+ import { openInEditor } from "../editor.js";
12
+ import { createLogger } from "../logger.js";
13
+ import type { CapabilityType } from "../types.js";
14
+
15
+ export function makeCapabilityCommand(type: CapabilityType): Command {
16
+ const log = createLogger(`capability:${type}`);
17
+
18
+ const cmd = new Command(type);
19
+ cmd.description(`Manage ${type}s`);
20
+
21
+ cmd
22
+ .command("create <name>")
23
+ .description(`Create a new ${type}`)
24
+ .action(async (name: string) => {
25
+ const capabilitiesDir = getCapabilitiesDir();
26
+ const capability = createCapability(capabilitiesDir, type, name);
27
+ log.success(`Created ${type} '${name}' at ${capability.path}`);
28
+ await openInEditor(capability.path, getConfigValue("editor"));
29
+ });
30
+
31
+ cmd
32
+ .command("edit <name>")
33
+ .description(`Edit an existing ${type}`)
34
+ .action(async (name: string) => {
35
+ const capabilitiesDir = getCapabilitiesDir();
36
+ if (!capabilityExists(capabilitiesDir, type, name)) {
37
+ throw new Error(`${type} '${name}' not found`);
38
+ }
39
+ const path = getCapabilityPath(capabilitiesDir, type, name);
40
+ await openInEditor(path, getConfigValue("editor"));
41
+ log.success(`Done editing ${type} '${name}'`);
42
+ });
43
+
44
+ cmd
45
+ .command("list")
46
+ .description(`List all ${type}s`)
47
+ .action(() => {
48
+ const capabilitiesDir = getCapabilitiesDir();
49
+ const items = listCapabilities(capabilitiesDir, type);
50
+ if (items.length === 0) {
51
+ log.warn(`No ${type}s found.`);
52
+ } else {
53
+ log.info(`${type}s (${items.length}):`);
54
+ for (const item of items) {
55
+ log.info(` • ${item}`);
56
+ }
57
+ }
58
+ });
59
+
60
+ cmd
61
+ .command("delete <name>")
62
+ .description(`Delete a ${type}`)
63
+ .option("-f, --force", "Skip confirmation")
64
+ .action(async (name: string, opts: { force?: boolean }) => {
65
+ const capabilitiesDir = getCapabilitiesDir();
66
+ if (!opts.force) {
67
+ const { prompt } = await import("enquirer");
68
+ const response = await prompt<{ confirmed: boolean }>({
69
+ type: "confirm",
70
+ name: "confirmed",
71
+ message: `Delete ${type} '${name}'?`,
72
+ initial: false,
73
+ });
74
+ if (!response.confirmed) {
75
+ log.info("Aborted.");
76
+ return;
77
+ }
78
+ }
79
+ deleteCapability(capabilitiesDir, type, name);
80
+ log.success(`Deleted ${type} '${name}'`);
81
+ });
82
+
83
+ cmd
84
+ .command("show <name>")
85
+ .description(`Show the content of a ${type}`)
86
+ .action((name: string) => {
87
+ const capabilitiesDir = getCapabilitiesDir();
88
+ const path = getCapabilityPath(capabilitiesDir, type, name);
89
+ if (!capabilityExists(capabilitiesDir, type, name)) {
90
+ throw new Error(`${type} '${name}' not found`);
91
+ }
92
+ const content = readFileSync(path, "utf-8");
93
+ log.info(`--- ${type}: ${name} ---`);
94
+ log.info(content);
95
+ });
96
+
97
+ return cmd;
98
+ }
@@ -0,0 +1,108 @@
1
+ import { Command } from "commander";
2
+ import {
3
+ getConfigValue,
4
+ setConfigValue,
5
+ getConfigEntries,
6
+ getConfigFilePath,
7
+ ensureConfigFile,
8
+ resetConfig,
9
+ } from "../config.js";
10
+ import { openInEditor } from "../editor.js";
11
+ import { createLogger } from "../logger.js";
12
+ import type { Config } from "../types.js";
13
+
14
+ const log = createLogger("config-cmd");
15
+
16
+ // Using a satisfies check to ensure this record covers every key of Config.
17
+ // TypeScript will error if a key is added to Config but omitted here.
18
+ const CONFIG_KEYS_RECORD = {
19
+ capabilitiesDir: "capabilitiesDir",
20
+ editor: "editor",
21
+ gitRemote: "gitRemote",
22
+ } as const satisfies { [K in keyof Config]: K };
23
+
24
+ const VALID_KEYS = Object.keys(CONFIG_KEYS_RECORD) as (keyof Config)[];
25
+
26
+ function formatDefault(value: string | undefined): string {
27
+ return value !== undefined ? `(default: ${value})` : "(not set)";
28
+ }
29
+
30
+ export function makeConfigCommand(): Command {
31
+ const cmd = new Command("config");
32
+ cmd.description("Manage uplink configuration");
33
+
34
+ cmd
35
+ .command("get [key]")
36
+ .description("Get a configuration value (or all values if no key given)")
37
+ .action((key?: string) => {
38
+ if (!key) {
39
+ log.info("Current configuration:");
40
+ for (const { key: k, value, defaultValue } of getConfigEntries()) {
41
+ const display = value !== undefined ? value : formatDefault(defaultValue);
42
+ log.info(` ${k}: ${display}`);
43
+ }
44
+ return;
45
+ }
46
+
47
+ if (!VALID_KEYS.includes(key as keyof Config)) {
48
+ log.error(`Unknown config key '${key}'. Valid keys: ${VALID_KEYS.join(", ")}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const entry = getConfigEntries().find((e) => e.key === key);
53
+ if (entry?.value === undefined) {
54
+ log.info(`${key}: ${formatDefault(entry?.defaultValue)}`);
55
+ } else {
56
+ log.info(`${key}: ${entry.value}`);
57
+ }
58
+ });
59
+
60
+ cmd
61
+ .command("set <key> <value>")
62
+ .description("Set a configuration value")
63
+ .action((key: string, value: string) => {
64
+ if (!VALID_KEYS.includes(key as keyof Config)) {
65
+ log.error(`Unknown config key '${key}'. Valid keys: ${VALID_KEYS.join(", ")}`);
66
+ process.exit(1);
67
+ }
68
+ setConfigValue(key as keyof Config, value as Config[keyof Config]);
69
+ log.success(`Set ${key} = ${value}`);
70
+ });
71
+
72
+ cmd
73
+ .command("list")
74
+ .description("List all configuration values")
75
+ .action(() => {
76
+ log.info("Configuration:");
77
+ for (const { key, value, defaultValue } of getConfigEntries()) {
78
+ const display = value !== undefined ? value : formatDefault(defaultValue);
79
+ log.info(` ${key}: ${display}`);
80
+ }
81
+ });
82
+
83
+ cmd
84
+ .command("edit")
85
+ .description("Open the configuration file in your editor")
86
+ .action(async () => {
87
+ const configFile = getConfigFilePath();
88
+ // Ensure the config file exists before opening
89
+ ensureConfigFile();
90
+ try {
91
+ await openInEditor(configFile, getConfigValue("editor"));
92
+ log.success(`Done editing config`);
93
+ } catch (err) {
94
+ log.error(err);
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ cmd
100
+ .command("reset")
101
+ .description("Reset configuration to defaults")
102
+ .action(() => {
103
+ resetConfig();
104
+ log.success("Configuration reset to defaults");
105
+ });
106
+
107
+ return cmd;
108
+ }
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import {
6
+ INTEGRATIONS,
7
+ } from "./install.js";
8
+
9
+ const TMP = join(tmpdir(), `uplink-install-test-${String(process.pid)}`);
10
+
11
+ beforeEach(() => {
12
+ mkdirSync(TMP, { recursive: true });
13
+ process.env["HOME"] = TMP;
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(TMP, { recursive: true, force: true });
18
+ delete process.env["HOME"];
19
+ });
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Returns the VS Code user dir for the current fake HOME, by replicating the
27
+ * same logic as getVSCodeUserDir() but using the overridden HOME.
28
+ */
29
+ function vscodeUserDir(): string {
30
+ // On Linux the path is ~/.config/Code/User
31
+ // We override HOME so homedir() returns TMP, but getVSCodeUserDir() calls
32
+ // homedir() which is cached at module load. Use the direct path.
33
+ return join(TMP, ".config", "Code", "User");
34
+ }
35
+
36
+ function readJson(path: string): { [key: string]: unknown } {
37
+ return JSON.parse(readFileSync(path, "utf-8")) as { [key: string]: unknown };
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // vscode integration
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe("vscode install", () => {
45
+ it("creates settings.json and mcp.json in VS Code user dir", () => {
46
+ const capabilitiesDir = join(TMP, "capabilities");
47
+ const userDir = vscodeUserDir();
48
+ mkdirSync(userDir, { recursive: true });
49
+
50
+ const vscode = INTEGRATIONS["vscode"];
51
+ if (!vscode) throw new Error("vscode integration not defined");
52
+ vscode.install(capabilitiesDir);
53
+
54
+ const settings = readJson(join(userDir, "settings.json"));
55
+ expect(settings).toMatchObject({
56
+ "chat.agentSkillsLocations": { [join(capabilitiesDir, "skills")]: false },
57
+ "chat.instructionsFilesLocations": { [join(capabilitiesDir, "instructions")]: false },
58
+ "chat.agentFilesLocations": { [join(capabilitiesDir, "agents")]: false },
59
+ });
60
+
61
+ const mcp = readJson(join(userDir, "mcp.json"));
62
+ expect(mcp).toHaveProperty("servers.agent-uplink");
63
+ });
64
+
65
+ it("merges into an existing settings.json without overwriting other keys", () => {
66
+ const capabilitiesDir = join(TMP, "capabilities");
67
+ const userDir = vscodeUserDir();
68
+ mkdirSync(userDir, { recursive: true });
69
+
70
+ writeFileSync(
71
+ join(userDir, "settings.json"),
72
+ JSON.stringify({ "editor.fontSize": 14 }, null, 2),
73
+ "utf-8"
74
+ );
75
+
76
+ const vscode = INTEGRATIONS["vscode"];
77
+ if (!vscode) throw new Error("vscode integration not defined");
78
+ vscode.install(capabilitiesDir);
79
+
80
+ const settings = readJson(join(userDir, "settings.json"));
81
+ expect(settings).toMatchObject({
82
+ "editor.fontSize": 14,
83
+ "chat.agentSkillsLocations": { [join(capabilitiesDir, "skills")]: false },
84
+ "chat.instructionsFilesLocations": { [join(capabilitiesDir, "instructions")]: false },
85
+ "chat.agentFilesLocations": { [join(capabilitiesDir, "agents")]: false },
86
+ });
87
+ });
88
+
89
+ it("uninstall removes agent-uplink keys from settings.json and mcp.json", () => {
90
+ const capabilitiesDir = join(TMP, "capabilities");
91
+ const userDir = vscodeUserDir();
92
+ mkdirSync(userDir, { recursive: true });
93
+
94
+ const vscode = INTEGRATIONS["vscode"];
95
+ if (!vscode) throw new Error("vscode integration not defined");
96
+ vscode.install(capabilitiesDir);
97
+ vscode.uninstall();
98
+
99
+ const settings = readJson(join(userDir, "settings.json"));
100
+ expect(settings).not.toHaveProperty("chat.agentSkillsLocations");
101
+ expect(settings).not.toHaveProperty("chat.instructionsFilesLocations");
102
+ expect(settings).not.toHaveProperty("chat.agentFilesLocations");
103
+
104
+ const mcp = readJson(join(userDir, "mcp.json"));
105
+ expect(mcp).not.toHaveProperty("servers.agent-uplink");
106
+ });
107
+
108
+ it("uninstall only removes our entries, leaving others intact", () => {
109
+ const capabilitiesDir = join(TMP, "capabilities");
110
+ const userDir = vscodeUserDir();
111
+ mkdirSync(userDir, { recursive: true });
112
+
113
+ writeFileSync(
114
+ join(userDir, "settings.json"),
115
+ JSON.stringify({
116
+ "chat.agentSkillsLocations": { "/other/skills": false },
117
+ "chat.instructionsFilesLocations": { "/other/instructions": false },
118
+ "chat.agentFilesLocations": { "/other/agents": false },
119
+ }, null, 2),
120
+ "utf-8"
121
+ );
122
+
123
+ const vscode = INTEGRATIONS["vscode"];
124
+ if (!vscode) throw new Error("vscode integration not defined");
125
+ vscode.install(capabilitiesDir);
126
+
127
+ const settingsAfterInstall = readJson(join(userDir, "settings.json"));
128
+ expect(settingsAfterInstall).toMatchObject({
129
+ "chat.agentSkillsLocations": {
130
+ "/other/skills": false,
131
+ [join(capabilitiesDir, "skills")]: false,
132
+ },
133
+ "chat.instructionsFilesLocations": {
134
+ "/other/instructions": false,
135
+ [join(capabilitiesDir, "instructions")]: false,
136
+ },
137
+ "chat.agentFilesLocations": {
138
+ "/other/agents": false,
139
+ [join(capabilitiesDir, "agents")]: false,
140
+ },
141
+ });
142
+
143
+ vscode.uninstall();
144
+
145
+ const settingsAfterUninstall = readJson(join(userDir, "settings.json"));
146
+ expect(settingsAfterUninstall).toMatchObject({
147
+ "chat.agentSkillsLocations": { "/other/skills": false },
148
+ "chat.instructionsFilesLocations": { "/other/instructions": false },
149
+ "chat.agentFilesLocations": { "/other/agents": false },
150
+ });
151
+ expect(settingsAfterUninstall["chat.agentSkillsLocations"]).not.toHaveProperty(join(capabilitiesDir, "skills"));
152
+ expect(settingsAfterUninstall["chat.instructionsFilesLocations"]).not.toHaveProperty(join(capabilitiesDir, "instructions"));
153
+ expect(settingsAfterUninstall["chat.agentFilesLocations"]).not.toHaveProperty(join(capabilitiesDir, "agents"));
154
+ });
155
+ });
@@ -0,0 +1,323 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, platform } from "os";
5
+ import { z } from "zod";
6
+ import { getCapabilitiesDir } from "../config.js";
7
+ import { createLogger } from "../logger.js";
8
+
9
+ const log = createLogger("install");
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Zod schemas for parsing VS Code config files
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const LocationsSchema = z.record(z.string(), z.boolean());
16
+ const McpServerSchema = z.object({
17
+ env: z.object({ UPLINK_CAPABILITIES_DIR: z.string().optional() }).optional(),
18
+ }).loose();
19
+ const McpServersSchema = z.record(z.string(), McpServerSchema);
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function deepMerge(
26
+ target: { [key: string]: unknown },
27
+ source: { [key: string]: unknown }
28
+ ): { [key: string]: unknown } {
29
+ const result = { ...target };
30
+ for (const [key, value] of Object.entries(source)) {
31
+ if (
32
+ value !== null &&
33
+ typeof value === "object" &&
34
+ !Array.isArray(value) &&
35
+ typeof result[key] === "object" &&
36
+ result[key] !== null &&
37
+ !Array.isArray(result[key])
38
+ ) {
39
+ result[key] = deepMerge(
40
+ result[key] as { [key: string]: unknown },
41
+ value as { [key: string]: unknown }
42
+ );
43
+ } else {
44
+ result[key] = value;
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+
50
+ function readJsonFile(path: string): { [key: string]: unknown } {
51
+ if (!existsSync(path)) return {};
52
+ try {
53
+ return JSON.parse(readFileSync(path, "utf-8")) as { [key: string]: unknown };
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ function writeJsonFile(path: string, data: unknown): void {
60
+ const dir = join(path, "..");
61
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
62
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
63
+ }
64
+
65
+ /**
66
+ * Returns the path to the VS Code user-level configuration directory.
67
+ * Platform-specific: Linux uses ~/.config/Code/User, macOS uses
68
+ * ~/Library/Application Support/Code/User, Windows uses %APPDATA%/Code/User.
69
+ */
70
+ export function getVSCodeUserDir(): string {
71
+ const home = homedir();
72
+ const p = platform();
73
+ if (p === "darwin") {
74
+ return join(home, "Library", "Application Support", "Code", "User");
75
+ }
76
+ if (p === "win32") {
77
+ return join(
78
+ process.env["APPDATA"] ?? join(home, "AppData", "Roaming"),
79
+ "Code",
80
+ "User"
81
+ );
82
+ }
83
+ return join(home, ".config", "Code", "User");
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Integration definitions
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export interface Integration {
91
+ name: string;
92
+ description: string;
93
+ install(capabilitiesDir: string): void;
94
+ uninstall(): void;
95
+ }
96
+
97
+ export const INTEGRATIONS: { [key: string]: Integration | undefined } = {
98
+ vscode: {
99
+ name: "VS Code",
100
+ description:
101
+ "Configure user-level VS Code settings and MCP server for GitHub Copilot",
102
+
103
+ install(capabilitiesDir: string) {
104
+ const userDir = getVSCodeUserDir();
105
+
106
+ // User settings.json
107
+ const settingsPath = join(userDir, "settings.json");
108
+ const settings = readJsonFile(settingsPath);
109
+
110
+ // Skills → chat.agentSkillsLocations
111
+ const skillsDir = join(capabilitiesDir, "skills");
112
+ const existingSkillsLocations = LocationsSchema.catch({}).parse(
113
+ settings["chat.agentSkillsLocations"]
114
+ );
115
+ settings["chat.agentSkillsLocations"] = {
116
+ ...existingSkillsLocations,
117
+ [skillsDir]: false,
118
+ };
119
+
120
+ // Instructions → chat.instructionsFilesLocations
121
+ const instructionsDir = join(capabilitiesDir, "instructions");
122
+ const existingInstructionsLocations = LocationsSchema.catch({}).parse(
123
+ settings["chat.instructionsFilesLocations"]
124
+ );
125
+ settings["chat.instructionsFilesLocations"] = {
126
+ ...existingInstructionsLocations,
127
+ [instructionsDir]: false,
128
+ };
129
+
130
+ // Agents → chat.agentFilesLocations
131
+ const agentsDir = join(capabilitiesDir, "agents");
132
+ const existingAgentLocations = LocationsSchema.catch({}).parse(
133
+ settings["chat.agentFilesLocations"]
134
+ );
135
+ settings["chat.agentFilesLocations"] = {
136
+ ...existingAgentLocations,
137
+ [agentsDir]: false,
138
+ };
139
+
140
+ writeJsonFile(settingsPath, settings);
141
+ log.dim(`Updated ${settingsPath}`);
142
+
143
+ // User mcp.json — register the uplink MCP server
144
+ const mcpPath = join(userDir, "mcp.json");
145
+ const mergedMcp = deepMerge(readJsonFile(mcpPath), {
146
+ servers: {
147
+ "agent-uplink": {
148
+ type: "stdio",
149
+ command: "uplink",
150
+ args: ["mcp", "start", "--stdio"],
151
+ env: { UPLINK_CAPABILITIES_DIR: capabilitiesDir },
152
+ },
153
+ },
154
+ });
155
+ writeJsonFile(mcpPath, mergedMcp);
156
+ log.dim(`Updated ${mcpPath}`);
157
+ },
158
+
159
+ uninstall() {
160
+ const userDir = getVSCodeUserDir();
161
+
162
+ // Determine capabilitiesDir from stored MCP config (fallback to current config)
163
+ const mcpPath = join(userDir, "mcp.json");
164
+ let capabilitiesDir: string;
165
+ if (existsSync(mcpPath)) {
166
+ const mcp = readJsonFile(mcpPath);
167
+ const servers = McpServersSchema.nullable().catch(null).parse(mcp["servers"]);
168
+ capabilitiesDir =
169
+ servers?.["agent-uplink"]?.env?.UPLINK_CAPABILITIES_DIR ?? getCapabilitiesDir();
170
+ } else {
171
+ capabilitiesDir = getCapabilitiesDir();
172
+ }
173
+
174
+ // Remove our skill/instruction entries from the instructions array in settings.json
175
+ const settingsPath = join(userDir, "settings.json");
176
+ if (existsSync(settingsPath)) {
177
+ const settings = readJsonFile(settingsPath);
178
+
179
+ // Remove our skills entry from chat.agentSkillsLocations
180
+ const skillsDir = join(capabilitiesDir, "skills");
181
+ const skillsLocations = LocationsSchema.nullable().catch(null).parse(
182
+ settings["chat.agentSkillsLocations"]
183
+ );
184
+ if (skillsLocations) {
185
+ const { [skillsDir]: _removedSkills, ...restSkills } = skillsLocations;
186
+ if (Object.keys(restSkills).length === 0) {
187
+ delete settings["chat.agentSkillsLocations"];
188
+ } else {
189
+ settings["chat.agentSkillsLocations"] = restSkills;
190
+ }
191
+ }
192
+
193
+ // Remove our instruction entry from chat.instructionsFilesLocations
194
+ const instructionsDir = join(capabilitiesDir, "instructions");
195
+ const instructionsLocations = LocationsSchema.nullable().catch(null).parse(
196
+ settings["chat.instructionsFilesLocations"]
197
+ );
198
+ if (instructionsLocations) {
199
+ const { [instructionsDir]: _removedInstructions, ...restInstructions } = instructionsLocations;
200
+ if (Object.keys(restInstructions).length === 0) {
201
+ delete settings["chat.instructionsFilesLocations"];
202
+ } else {
203
+ settings["chat.instructionsFilesLocations"] = restInstructions;
204
+ }
205
+ }
206
+
207
+ // Remove agents directory from chat.agentFilesLocations
208
+ const agentsDir = join(capabilitiesDir, "agents");
209
+ const agentLocations = LocationsSchema.nullable().catch(null).parse(
210
+ settings["chat.agentFilesLocations"]
211
+ );
212
+ if (agentLocations) {
213
+ const { [agentsDir]: _removedAgents, ...restAgents } = agentLocations;
214
+ if (Object.keys(restAgents).length === 0) {
215
+ delete settings["chat.agentFilesLocations"];
216
+ } else {
217
+ settings["chat.agentFilesLocations"] = restAgents;
218
+ }
219
+ }
220
+
221
+ writeJsonFile(settingsPath, settings);
222
+ log.dim(`Cleaned ${settingsPath}`);
223
+ }
224
+
225
+ // Remove agent-uplink server from mcp.json
226
+ if (existsSync(mcpPath)) {
227
+ const mcp = readJsonFile(mcpPath);
228
+ const servers = McpServersSchema.nullable().catch(null).parse(mcp["servers"]);
229
+ if (servers) {
230
+ const { "agent-uplink": _removed, ...rest } = servers;
231
+ if (Object.keys(rest).length === 0) {
232
+ delete mcp["servers"];
233
+ } else {
234
+ mcp["servers"] = rest;
235
+ }
236
+ }
237
+ writeJsonFile(mcpPath, mcp);
238
+ log.dim(`Cleaned ${mcpPath}`);
239
+ }
240
+ },
241
+ },
242
+ };
243
+
244
+ const INTEGRATION_NAMES = Object.keys(INTEGRATIONS).join(", ");
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Commands
248
+ // ---------------------------------------------------------------------------
249
+
250
+ export function makeInstallCommand(): Command {
251
+ const cmd = new Command("install");
252
+ cmd
253
+ .description(
254
+ `Install a tool integration globally (${INTEGRATION_NAMES})`
255
+ )
256
+ .argument("<integration>", `Integration to install (${INTEGRATION_NAMES})`)
257
+ .action((integration: string) => {
258
+ const capabilitiesDir = getCapabilitiesDir();
259
+ const integrationDef = INTEGRATIONS[integration];
260
+
261
+ if (!integrationDef) {
262
+ log.error(
263
+ `Unknown integration '${integration}'. Available: ${INTEGRATION_NAMES}`
264
+ );
265
+ process.exit(1);
266
+ }
267
+
268
+ log.info(`Installing ${integrationDef.name} integration...`);
269
+ log.dim(`Capabilities directory: ${capabilitiesDir}`);
270
+
271
+ try {
272
+ integrationDef.install(capabilitiesDir);
273
+ log.success(
274
+ `${integrationDef.name} integration installed successfully.`
275
+ );
276
+ } catch (err) {
277
+ log.error(
278
+ `Installation failed: ${err instanceof Error ? err.message : String(err)}`
279
+ );
280
+ process.exit(1);
281
+ }
282
+ });
283
+
284
+ return cmd;
285
+ }
286
+
287
+ export function makeUninstallCommand(): Command {
288
+ const cmd = new Command("uninstall");
289
+ cmd
290
+ .description(
291
+ `Uninstall a tool integration (${INTEGRATION_NAMES})`
292
+ )
293
+ .argument(
294
+ "<integration>",
295
+ `Integration to remove (${INTEGRATION_NAMES})`
296
+ )
297
+ .action((integration: string) => {
298
+ const integrationDef = INTEGRATIONS[integration];
299
+
300
+ if (!integrationDef) {
301
+ log.error(
302
+ `Unknown integration '${integration}'. Available: ${INTEGRATION_NAMES}`
303
+ );
304
+ process.exit(1);
305
+ }
306
+
307
+ log.info(`Uninstalling ${integrationDef.name} integration...`);
308
+
309
+ try {
310
+ integrationDef.uninstall();
311
+ log.success(
312
+ `${integrationDef.name} integration uninstalled successfully.`
313
+ );
314
+ } catch (err) {
315
+ log.error(
316
+ `Uninstall failed: ${err instanceof Error ? err.message : String(err)}`
317
+ );
318
+ process.exit(1);
319
+ }
320
+ });
321
+
322
+ return cmd;
323
+ }