@cg3/equip 0.2.23 → 0.4.1

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.
Files changed (57) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +26 -10
  3. package/bin/equip.js +159 -68
  4. package/demo/README.md +1 -1
  5. package/dist/index.d.ts +76 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +177 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/cli.d.ts +22 -0
  10. package/dist/lib/cli.d.ts.map +1 -0
  11. package/dist/lib/cli.js +148 -0
  12. package/dist/lib/cli.js.map +1 -0
  13. package/dist/lib/commands/doctor.d.ts +2 -0
  14. package/dist/lib/commands/doctor.d.ts.map +1 -0
  15. package/dist/lib/commands/doctor.js +162 -0
  16. package/dist/lib/commands/doctor.js.map +1 -0
  17. package/dist/lib/commands/status.d.ts +2 -0
  18. package/dist/lib/commands/status.d.ts.map +1 -0
  19. package/dist/lib/commands/status.js +134 -0
  20. package/dist/lib/commands/status.js.map +1 -0
  21. package/dist/lib/commands/update.d.ts +2 -0
  22. package/dist/lib/commands/update.d.ts.map +1 -0
  23. package/dist/lib/commands/update.js +93 -0
  24. package/dist/lib/commands/update.js.map +1 -0
  25. package/dist/lib/detect.d.ts +12 -0
  26. package/dist/lib/detect.d.ts.map +1 -0
  27. package/dist/lib/detect.js +109 -0
  28. package/dist/lib/detect.js.map +1 -0
  29. package/dist/lib/hooks.d.ts +40 -0
  30. package/dist/lib/hooks.d.ts.map +1 -0
  31. package/dist/lib/hooks.js +226 -0
  32. package/dist/lib/hooks.js.map +1 -0
  33. package/dist/lib/mcp.d.ts +73 -0
  34. package/dist/lib/mcp.d.ts.map +1 -0
  35. package/dist/lib/mcp.js +418 -0
  36. package/dist/lib/mcp.js.map +1 -0
  37. package/dist/lib/platforms.d.ts +67 -0
  38. package/dist/lib/platforms.d.ts.map +1 -0
  39. package/dist/lib/platforms.js +353 -0
  40. package/dist/lib/platforms.js.map +1 -0
  41. package/dist/lib/rules.d.ts +35 -0
  42. package/dist/lib/rules.d.ts.map +1 -0
  43. package/dist/lib/rules.js +161 -0
  44. package/dist/lib/rules.js.map +1 -0
  45. package/dist/lib/state.d.ts +33 -0
  46. package/dist/lib/state.d.ts.map +1 -0
  47. package/dist/lib/state.js +130 -0
  48. package/dist/lib/state.js.map +1 -0
  49. package/package.json +19 -13
  50. package/registry.json +9 -0
  51. package/index.js +0 -245
  52. package/lib/cli.js +0 -99
  53. package/lib/detect.js +0 -242
  54. package/lib/hooks.js +0 -238
  55. package/lib/mcp.js +0 -503
  56. package/lib/platforms.js +0 -210
  57. package/lib/rules.js +0 -170
package/lib/mcp.js DELETED
@@ -1,503 +0,0 @@
1
- // MCP config read/write/merge/uninstall.
2
- // Handles all platform-specific config format differences.
3
- // Zero dependencies.
4
-
5
- "use strict";
6
-
7
- const fs = require("fs");
8
- const path = require("path");
9
- const { execSync } = require("child_process");
10
-
11
- // ─── TOML Helpers (minimal, zero-dep) ───────────────────────
12
-
13
- /**
14
- * Parse a TOML table entry for [mcp_servers.<name>].
15
- * Returns key-value pairs as a plain object. Supports string, number, boolean, arrays.
16
- * This is NOT a full TOML parser — only handles flat tables needed for MCP config.
17
- */
18
- function parseTomlServerEntry(tomlContent, rootKey, serverName) {
19
- const tableHeader = `[${rootKey}.${serverName}]`;
20
- const idx = tomlContent.indexOf(tableHeader);
21
- if (idx === -1) return null;
22
-
23
- const afterHeader = tomlContent.slice(idx + tableHeader.length);
24
- const nextTable = afterHeader.search(/\n\[(?!\[)/); // next top-level table
25
- const block = nextTable === -1 ? afterHeader : afterHeader.slice(0, nextTable);
26
-
27
- const result = {};
28
- for (const line of block.split("\n")) {
29
- const trimmed = line.trim();
30
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[")) continue;
31
- const eq = trimmed.indexOf("=");
32
- if (eq === -1) continue;
33
- const key = trimmed.slice(0, eq).trim();
34
- let val = trimmed.slice(eq + 1).trim();
35
- // Parse value
36
- if (val.startsWith('"') && val.endsWith('"')) {
37
- result[key] = val.slice(1, -1);
38
- } else if (val === "true") {
39
- result[key] = true;
40
- } else if (val === "false") {
41
- result[key] = false;
42
- } else if (!isNaN(Number(val)) && val !== "") {
43
- result[key] = Number(val);
44
- } else {
45
- result[key] = val;
46
- }
47
- }
48
- return Object.keys(result).length > 0 ? result : null;
49
- }
50
-
51
- /**
52
- * Parse a nested TOML sub-table (e.g., [mcp_servers.prior.env] or [mcp_servers.prior.http_headers]).
53
- */
54
- function parseTomlSubTables(tomlContent, rootKey, serverName) {
55
- const prefix = `[${rootKey}.${serverName}.`;
56
- const result = {};
57
- let idx = 0;
58
- while ((idx = tomlContent.indexOf(prefix, idx)) !== -1) {
59
- const lineStart = tomlContent.lastIndexOf("\n", idx) + 1;
60
- const lineEnd = tomlContent.indexOf("\n", idx);
61
- const header = tomlContent.slice(idx, lineEnd === -1 ? undefined : lineEnd).trim();
62
- // Extract sub-table name from [mcp_servers.prior.env]
63
- const subName = header.slice(prefix.length, -1); // remove trailing ]
64
- if (!subName || subName.includes(".")) { idx++; continue; }
65
-
66
- const afterHeader = tomlContent.slice(lineEnd === -1 ? tomlContent.length : lineEnd);
67
- const nextTable = afterHeader.search(/\n\[(?!\[)/);
68
- const block = nextTable === -1 ? afterHeader : afterHeader.slice(0, nextTable);
69
-
70
- const sub = {};
71
- for (const line of block.split("\n")) {
72
- const t = line.trim();
73
- if (!t || t.startsWith("#") || t.startsWith("[")) continue;
74
- const eq = t.indexOf("=");
75
- if (eq === -1) continue;
76
- const k = t.slice(0, eq).trim();
77
- let v = t.slice(eq + 1).trim();
78
- if (v.startsWith('"') && v.endsWith('"')) sub[k] = v.slice(1, -1);
79
- else sub[k] = v;
80
- }
81
- if (Object.keys(sub).length > 0) result[subName] = sub;
82
- idx++;
83
- }
84
- return result;
85
- }
86
-
87
- /**
88
- * Build TOML text for a server entry.
89
- * @param {string} rootKey - e.g., "mcp_servers"
90
- * @param {string} serverName - e.g., "prior"
91
- * @param {object} config - { url, bearer_token_env_var, http_headers, ... }
92
- * @returns {string} TOML text block
93
- */
94
- function buildTomlEntry(rootKey, serverName, config) {
95
- const lines = [`[${rootKey}.${serverName}]`];
96
- const subTables = {};
97
-
98
- for (const [k, v] of Object.entries(config)) {
99
- if (typeof v === "object" && v !== null && !Array.isArray(v)) {
100
- subTables[k] = v;
101
- } else if (typeof v === "string") {
102
- lines.push(`${k} = "${v}"`);
103
- } else if (typeof v === "boolean" || typeof v === "number") {
104
- lines.push(`${k} = ${v}`);
105
- } else if (Array.isArray(v)) {
106
- lines.push(`${k} = [${v.map(x => typeof x === "string" ? `"${x}"` : x).join(", ")}]`);
107
- }
108
- }
109
-
110
- for (const [subName, subObj] of Object.entries(subTables)) {
111
- lines.push("", `[${rootKey}.${serverName}.${subName}]`);
112
- for (const [k, v] of Object.entries(subObj)) {
113
- if (typeof v === "string") lines.push(`${k} = "${v}"`);
114
- else lines.push(`${k} = ${v}`);
115
- }
116
- }
117
-
118
- return lines.join("\n");
119
- }
120
-
121
- /**
122
- * Remove a TOML server entry block from content.
123
- * Removes [rootKey.serverName] and any [rootKey.serverName.*] sub-tables.
124
- */
125
- function removeTomlEntry(tomlContent, rootKey, serverName) {
126
- const mainHeader = `[${rootKey}.${serverName}]`;
127
- const subPrefix = `[${rootKey}.${serverName}.`;
128
-
129
- // Find all lines belonging to this entry
130
- const lines = tomlContent.split("\n");
131
- const result = [];
132
- let inEntry = false;
133
-
134
- for (const line of lines) {
135
- const trimmed = line.trim();
136
- if (trimmed === mainHeader || trimmed.startsWith(subPrefix)) {
137
- inEntry = true;
138
- continue;
139
- }
140
- if (inEntry && trimmed.startsWith("[") && !trimmed.startsWith(subPrefix)) {
141
- inEntry = false;
142
- }
143
- if (!inEntry) {
144
- result.push(line);
145
- }
146
- }
147
-
148
- // Clean up extra blank lines
149
- return result.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
150
- }
151
-
152
- // ─── Read ────────────────────────────────────────────────────
153
-
154
- /**
155
- * Read an MCP server entry from a config file (JSON or TOML).
156
- * @param {string} configPath - Path to config file
157
- * @param {string} rootKey - Root key ("mcpServers", "servers", or "mcp_servers")
158
- * @param {string} serverName - Server name to read
159
- * @param {string} [configFormat="json"] - "json" or "toml"
160
- * @returns {object|null} Server config or null
161
- */
162
- function readMcpEntry(configPath, rootKey, serverName, configFormat = "json") {
163
- try {
164
- let raw = fs.readFileSync(configPath, "utf-8");
165
- if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // Strip BOM
166
-
167
- if (configFormat === "toml") {
168
- const entry = parseTomlServerEntry(raw, rootKey, serverName);
169
- if (!entry) return null;
170
- // Merge sub-tables
171
- const subs = parseTomlSubTables(raw, rootKey, serverName);
172
- return { ...entry, ...subs };
173
- }
174
-
175
- const data = JSON.parse(raw);
176
- return data?.[rootKey]?.[serverName] || null;
177
- } catch { return null; }
178
- }
179
-
180
- // ─── Config Builders ─────────────────────────────────────────
181
-
182
- /**
183
- * Build HTTP MCP config for a platform.
184
- * Handles platform-specific field names (url vs serverUrl, type field).
185
- * @param {string} serverUrl - MCP server URL
186
- * @param {string} platform - Platform id
187
- * @returns {object} MCP config object
188
- */
189
- function buildHttpConfig(serverUrl, platform) {
190
- if (platform === "windsurf") return { serverUrl };
191
- if (platform === "claude-code") return { type: "http", url: serverUrl };
192
- if (platform === "vscode") return { type: "http", url: serverUrl };
193
- if (platform === "cursor") return { type: "streamable-http", url: serverUrl };
194
- if (platform === "gemini-cli") return { httpUrl: serverUrl };
195
- // codex, cline, roo-code all use { url }
196
- return { url: serverUrl };
197
- }
198
-
199
- /**
200
- * Build HTTP MCP config with auth headers.
201
- * @param {string} serverUrl - MCP server URL
202
- * @param {string} apiKey - API key for auth
203
- * @param {string} platform - Platform id
204
- * @param {object} [extraHeaders] - Additional headers
205
- * @returns {object} MCP config with headers
206
- */
207
- function buildHttpConfigWithAuth(serverUrl, apiKey, platform, extraHeaders) {
208
- const base = buildHttpConfig(serverUrl, platform);
209
-
210
- if (platform === "codex") {
211
- // Codex TOML uses http_headers for static headers
212
- return {
213
- ...base,
214
- http_headers: {
215
- Authorization: `Bearer ${apiKey}`,
216
- ...extraHeaders,
217
- },
218
- };
219
- }
220
-
221
- if (platform === "gemini-cli") {
222
- // Gemini CLI uses headers object
223
- return {
224
- ...base,
225
- headers: {
226
- Authorization: `Bearer ${apiKey}`,
227
- ...extraHeaders,
228
- },
229
- };
230
- }
231
-
232
- return {
233
- ...base,
234
- headers: {
235
- Authorization: `Bearer ${apiKey}`,
236
- ...extraHeaders,
237
- },
238
- };
239
- }
240
-
241
- /**
242
- * Build stdio MCP config.
243
- * @param {string} command - Command to run
244
- * @param {string[]} args - Command arguments
245
- * @param {object} env - Environment variables
246
- * @returns {object} MCP stdio config
247
- */
248
- function buildStdioConfig(command, args, env) {
249
- if (process.platform === "win32") {
250
- return { command: "cmd", args: ["/c", command, ...args], env };
251
- }
252
- return { command, args, env };
253
- }
254
-
255
- // ─── Install ─────────────────────────────────────────────────
256
-
257
- function fileExists(p) {
258
- try { return fs.statSync(p).isFile(); } catch { return false; }
259
- }
260
-
261
- /**
262
- * Install MCP config for a platform.
263
- * Tries platform CLI first (if available), falls back to JSON write.
264
- * @param {object} platform - Platform object from detect
265
- * @param {string} serverName - Server name (e.g., "prior")
266
- * @param {object} mcpEntry - MCP config object
267
- * @param {object} [options] - { dryRun, serverUrl }
268
- * @returns {{ success: boolean, method: string }}
269
- */
270
- function installMcp(platform, serverName, mcpEntry, options = {}) {
271
- const { dryRun = false, serverUrl } = options;
272
-
273
- // Claude Code: try CLI first
274
- if (platform.platform === "claude-code" && platform.hasCli && mcpEntry.url) {
275
- try {
276
- if (!dryRun) {
277
- const headerArgs = mcpEntry.headers
278
- ? Object.entries(mcpEntry.headers).map(([k, v]) => `--header "${k}: ${v}"`).join(" ")
279
- : "";
280
- execSync(`claude mcp add --transport http -s user ${headerArgs} ${serverName} ${mcpEntry.url}`, {
281
- encoding: "utf-8", timeout: 15000, stdio: "pipe",
282
- });
283
- const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
284
- if (check) return { success: true, method: "cli" };
285
- } else {
286
- return { success: true, method: "cli" };
287
- }
288
- } catch { /* fall through */ }
289
- }
290
-
291
- // Cursor: try CLI first
292
- if (platform.platform === "cursor" && platform.hasCli) {
293
- try {
294
- const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
295
- if (!dryRun) {
296
- execSync(`cursor --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
297
- encoding: "utf-8", timeout: 15000, stdio: "pipe",
298
- });
299
- const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
300
- if (check) return { success: true, method: "cli" };
301
- } else {
302
- return { success: true, method: "cli" };
303
- }
304
- } catch { /* fall through */ }
305
- }
306
-
307
- // VS Code: try CLI first
308
- if (platform.platform === "vscode" && platform.hasCli) {
309
- try {
310
- const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
311
- if (!dryRun) {
312
- execSync(`code --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
313
- encoding: "utf-8", timeout: 15000, stdio: "pipe",
314
- });
315
- const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
316
- if (check) return { success: true, method: "cli" };
317
- } else {
318
- return { success: true, method: "cli" };
319
- }
320
- } catch { /* fall through */ }
321
- }
322
-
323
- // Codex: skip CLI for HTTP (codex mcp add treats URLs as stdio commands).
324
- // Use CLI only for stdio transport where the command syntax is unambiguous.
325
- if (platform.platform === "codex" && platform.hasCli && mcpEntry.command) {
326
- try {
327
- const cliArgs = [serverName, "--", mcpEntry.command, ...(mcpEntry.args || [])].join(" ");
328
- if (!dryRun) {
329
- execSync(`codex mcp add ${cliArgs}`, {
330
- encoding: "utf-8", timeout: 15000, stdio: "pipe",
331
- });
332
- const check = readMcpEntry(platform.configPath, platform.rootKey, serverName, "toml");
333
- if (check) return { success: true, method: "cli" };
334
- } else {
335
- return { success: true, method: "cli" };
336
- }
337
- } catch { /* fall through to TOML write */ }
338
- }
339
-
340
- // Gemini CLI: try CLI first (gemini mcp add <name> -- or manual JSON)
341
- // Gemini's `gemini mcp add` is for stdio primarily; HTTP goes through settings.json
342
- // Fall through to JSON write for HTTP
343
-
344
- // TOML write for Codex, JSON write for all others
345
- if (platform.configFormat === "toml") {
346
- return installMcpToml(platform, serverName, mcpEntry, dryRun);
347
- }
348
- return installMcpJson(platform, serverName, mcpEntry, dryRun);
349
- }
350
-
351
- /**
352
- * Write MCP config directly to JSON file.
353
- * Merges with existing config, creates backup.
354
- * @param {object} platform - Platform object
355
- * @param {string} serverName - Server name
356
- * @param {object} mcpEntry - MCP config
357
- * @param {boolean} dryRun
358
- * @returns {{ success: boolean, method: string }}
359
- */
360
- function installMcpJson(platform, serverName, mcpEntry, dryRun) {
361
- const { configPath, rootKey } = platform;
362
-
363
- let existing = {};
364
- try {
365
- let raw = fs.readFileSync(configPath, "utf-8");
366
- if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
367
- existing = JSON.parse(raw);
368
- if (typeof existing !== "object" || existing === null) existing = {};
369
- } catch { /* start fresh */ }
370
-
371
- if (!existing[rootKey]) existing[rootKey] = {};
372
- existing[rootKey][serverName] = mcpEntry;
373
-
374
- if (!dryRun) {
375
- const dir = path.dirname(configPath);
376
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
377
-
378
- if (fileExists(configPath)) {
379
- try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
380
- }
381
-
382
- fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
383
- }
384
-
385
- return { success: true, method: "json" };
386
- }
387
-
388
- /**
389
- * Write MCP config to TOML file (Codex).
390
- * Appends or replaces a [mcp_servers.<name>] table.
391
- * @param {object} platform - Platform object
392
- * @param {string} serverName - Server name
393
- * @param {object} mcpEntry - MCP config
394
- * @param {boolean} dryRun
395
- * @returns {{ success: boolean, method: string }}
396
- */
397
- function installMcpToml(platform, serverName, mcpEntry, dryRun) {
398
- const { configPath, rootKey } = platform;
399
-
400
- let existing = "";
401
- try { existing = fs.readFileSync(configPath, "utf-8"); } catch { /* start fresh */ }
402
-
403
- // Remove existing entry if present
404
- const tableHeader = `[${rootKey}.${serverName}]`;
405
- if (existing.includes(tableHeader)) {
406
- existing = removeTomlEntry(existing, rootKey, serverName);
407
- }
408
-
409
- const newBlock = buildTomlEntry(rootKey, serverName, mcpEntry);
410
-
411
- if (!dryRun) {
412
- const dir = path.dirname(configPath);
413
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
414
-
415
- if (fileExists(configPath)) {
416
- try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
417
- }
418
-
419
- const sep = existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
420
- fs.writeFileSync(configPath, existing + sep + newBlock + "\n");
421
- }
422
-
423
- return { success: true, method: "toml" };
424
- }
425
-
426
- /**
427
- * Remove an MCP server entry from a platform config.
428
- * @param {object} platform - Platform object
429
- * @param {string} serverName - Server name to remove
430
- * @param {boolean} dryRun
431
- * @returns {boolean} Whether anything was removed
432
- */
433
- function uninstallMcp(platform, serverName, dryRun) {
434
- const { configPath, rootKey } = platform;
435
- if (!fileExists(configPath)) return false;
436
-
437
- // TOML path (Codex)
438
- if (platform.configFormat === "toml") {
439
- try {
440
- const content = fs.readFileSync(configPath, "utf-8");
441
- const tableHeader = `[${rootKey}.${serverName}]`;
442
- if (!content.includes(tableHeader)) return false;
443
- if (!dryRun) {
444
- fs.copyFileSync(configPath, configPath + ".bak");
445
- const cleaned = removeTomlEntry(content, rootKey, serverName);
446
- if (cleaned.trim()) {
447
- fs.writeFileSync(configPath, cleaned);
448
- } else {
449
- fs.unlinkSync(configPath);
450
- }
451
- }
452
- return true;
453
- } catch { return false; }
454
- }
455
-
456
- // JSON path
457
- try {
458
- const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
459
- if (!data?.[rootKey]?.[serverName]) return false;
460
- delete data[rootKey][serverName];
461
- if (Object.keys(data[rootKey]).length === 0) delete data[rootKey];
462
- if (!dryRun) {
463
- fs.copyFileSync(configPath, configPath + ".bak");
464
- if (Object.keys(data).length === 0) {
465
- fs.unlinkSync(configPath);
466
- } else {
467
- fs.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n");
468
- }
469
- }
470
- return true;
471
- } catch { return false; }
472
- }
473
-
474
- /**
475
- * Update API key in existing MCP config.
476
- * @param {object} platform - Platform object
477
- * @param {string} serverName - Server name
478
- * @param {object} mcpEntry - New MCP config
479
- * @returns {{ success: boolean, method: string }}
480
- */
481
- function updateMcpKey(platform, serverName, mcpEntry) {
482
- if (platform.configFormat === "toml") {
483
- return installMcpToml(platform, serverName, mcpEntry, false);
484
- }
485
- return installMcpJson(platform, serverName, mcpEntry, false);
486
- }
487
-
488
- module.exports = {
489
- readMcpEntry,
490
- buildHttpConfig,
491
- buildHttpConfigWithAuth,
492
- buildStdioConfig,
493
- installMcp,
494
- installMcpJson,
495
- installMcpToml,
496
- uninstallMcp,
497
- updateMcpKey,
498
- // TOML helpers (exported for testing)
499
- parseTomlServerEntry,
500
- parseTomlSubTables,
501
- buildTomlEntry,
502
- removeTomlEntry,
503
- };