@akms/mcp 0.2.1 → 0.3.2
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/CHANGELOG.md +50 -0
- package/README.md +99 -15
- package/lib/cli.js +2738 -164
- package/package.json +13 -3
package/lib/cli.js
CHANGED
|
@@ -44,7 +44,12 @@ function parseArguments(argv) {
|
|
|
44
44
|
commandSeen = true;
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
if (
|
|
47
|
+
if ((argument === "register" || argument === "sync") && !commandSeen && !parsed.mcpId) {
|
|
48
|
+
commandSeen = true;
|
|
49
|
+
parsed.managementMode = argument;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (Object.hasOwn(MCP_PACKAGES, argument) == true && parsed.mcpId == null && !parsed.managementMode) {
|
|
48
53
|
parsed.mcpId = argument;
|
|
49
54
|
continue;
|
|
50
55
|
}
|
|
@@ -53,6 +58,217 @@ function parseArguments(argv) {
|
|
|
53
58
|
return parsed;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
// src/modules/registration/registry.ts
|
|
62
|
+
import fs4 from "fs";
|
|
63
|
+
import path5 from "path";
|
|
64
|
+
import { createHash } from "crypto";
|
|
65
|
+
|
|
66
|
+
// src/modules/registration/config-file.ts
|
|
67
|
+
import fs from "fs";
|
|
68
|
+
import path from "path";
|
|
69
|
+
import { randomUUID } from "crypto";
|
|
70
|
+
function readExisting(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
return fs.readFileSync(filePath, "utf8");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === "ENOENT") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function updateConfigFile(filePath, update, options = {}) {
|
|
81
|
+
let destination = filePath;
|
|
82
|
+
let symbolicLink = false;
|
|
83
|
+
try {
|
|
84
|
+
symbolicLink = fs.lstatSync(filePath).isSymbolicLink();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (symbolicLink) {
|
|
91
|
+
destination = fs.realpathSync(filePath);
|
|
92
|
+
}
|
|
93
|
+
const before = readExisting(destination);
|
|
94
|
+
const updated = update(before ?? "");
|
|
95
|
+
if (before === updated) return;
|
|
96
|
+
if (before != null) {
|
|
97
|
+
fs.accessSync(destination, fs.constants.W_OK);
|
|
98
|
+
}
|
|
99
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
100
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
101
|
+
const mode = options.mode ?? (before == null ? 384 : fs.statSync(destination).mode & 511);
|
|
102
|
+
let descriptor;
|
|
103
|
+
let ownsTemporary = false;
|
|
104
|
+
try {
|
|
105
|
+
descriptor = fs.openSync(temporary, "wx", mode);
|
|
106
|
+
ownsTemporary = true;
|
|
107
|
+
fs.writeFileSync(descriptor, updated, "utf8");
|
|
108
|
+
fs.fsyncSync(descriptor);
|
|
109
|
+
fs.closeSync(descriptor);
|
|
110
|
+
descriptor = void 0;
|
|
111
|
+
if (readExisting(destination) !== before) {
|
|
112
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
113
|
+
}
|
|
114
|
+
options.beforeCommit?.(before);
|
|
115
|
+
if (readExisting(destination) !== before) throw new Error("The config changed while saving; reload it and try again.");
|
|
116
|
+
fs.renameSync(temporary, destination);
|
|
117
|
+
ownsTemporary = false;
|
|
118
|
+
} finally {
|
|
119
|
+
try {
|
|
120
|
+
if (descriptor != null) {
|
|
121
|
+
fs.closeSync(descriptor);
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
if (ownsTemporary) {
|
|
125
|
+
fs.unlinkSync(temporary);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/modules/registration/types.ts
|
|
132
|
+
var CLIENT_LABELS2 = {
|
|
133
|
+
"claude-code": "Claude Code",
|
|
134
|
+
codex: "Codex",
|
|
135
|
+
"claude-desktop": "Claude Desktop",
|
|
136
|
+
cursor: "Cursor"
|
|
137
|
+
};
|
|
138
|
+
var KINDS = ["db", "ssh", "wsl", "s3"];
|
|
139
|
+
|
|
140
|
+
// src/modules/registration/runtime.ts
|
|
141
|
+
import fs2 from "fs";
|
|
142
|
+
import os from "os";
|
|
143
|
+
import path2 from "path";
|
|
144
|
+
import { execFile } from "child_process";
|
|
145
|
+
import { promisify, stripVTControlCharacters } from "util";
|
|
146
|
+
var packageName = (kind) => `@akms/mcp-${kind}`;
|
|
147
|
+
var binary = (command) => command.replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
148
|
+
function runtimePrefix() {
|
|
149
|
+
return path2.join(process.env.AKMS_MCP_HOME ? path2.resolve(process.env.AKMS_MCP_HOME) : path2.join(os.homedir(), ".akms", "mcp"), "runtime");
|
|
150
|
+
}
|
|
151
|
+
function runtimeRoot(prefix = runtimePrefix(), platform = process.platform) {
|
|
152
|
+
return path2.join(prefix, ...platform === "win32" ? [] : ["lib"], "node_modules");
|
|
153
|
+
}
|
|
154
|
+
var runtimeInstallCommand = (kind) => `npm install --global --prefix "${runtimePrefix()}" ${packageName(kind)}@latest`;
|
|
155
|
+
async function latestRuntimeVersion(kind, timeoutMs = 6e3) {
|
|
156
|
+
try {
|
|
157
|
+
const { stdout } = await promisify(execFile)(process.execPath, [npmCli(), "view", `${packageName(kind)}@latest`, "version", "--json", "--prefer-online", "--fetch-retries=0", "--fetch-timeout=4000"], {
|
|
158
|
+
windowsHide: true,
|
|
159
|
+
timeout: timeoutMs,
|
|
160
|
+
maxBuffer: 65536
|
|
161
|
+
});
|
|
162
|
+
const version = JSON.parse(stdout);
|
|
163
|
+
return typeof version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) ? version : null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function runtimeIsOlder(installed, current) {
|
|
169
|
+
const parse2 = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
170
|
+
const left = parse2(installed), right = parse2(current);
|
|
171
|
+
if (!left || !right) return false;
|
|
172
|
+
for (let index = 1; index <= 3; index++) {
|
|
173
|
+
if (BigInt(left[index]) !== BigInt(right[index])) return BigInt(left[index]) < BigInt(right[index]);
|
|
174
|
+
}
|
|
175
|
+
if (!left[4] || !right[4]) return Boolean(left[4] && !right[4]);
|
|
176
|
+
const a = left[4].split("."), b = right[4].split(".");
|
|
177
|
+
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
178
|
+
if (a[index] === b[index]) continue;
|
|
179
|
+
if (a[index] === void 0 || b[index] === void 0) return a[index] === void 0;
|
|
180
|
+
const numericA = /^\d+$/.test(a[index]), numericB = /^\d+$/.test(b[index]);
|
|
181
|
+
if (numericA && numericB) return BigInt(a[index]) < BigInt(b[index]);
|
|
182
|
+
if (numericA !== numericB) return numericA;
|
|
183
|
+
return a[index] < b[index];
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function launchKind(entry) {
|
|
188
|
+
for (const kind of KINDS) {
|
|
189
|
+
if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(binary(entry.command)) || entry.args.some((arg) => new RegExp(`^@akms/mcp-${kind}(?:@[^\\s]+)?$`).test(arg))) return kind;
|
|
190
|
+
if (/^(node|nodejs)(\.exe)?$/.test(binary(entry.command)) && entry.args[0]?.replace(/\\/g, "/").endsWith(`/node_modules/@akms/mcp-${kind}/lib/cli.js`)) return kind;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function standardLaunch(entry, kind) {
|
|
195
|
+
if (launchKind(entry) !== kind) return false;
|
|
196
|
+
const command = binary(entry.command);
|
|
197
|
+
if (/^(node|nodejs)(\.exe)?$/.test(command)) return entry.args.length === 1 && entry.args[0].replace(/\\/g, "/").endsWith(`/node_modules/@akms/mcp-${kind}/lib/cli.js`);
|
|
198
|
+
if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(command)) return entry.args.length === 0;
|
|
199
|
+
let args = entry.args;
|
|
200
|
+
if (command === "cmd" || command === "cmd.exe") {
|
|
201
|
+
if (args[0]?.toLowerCase() !== "/c" || !/^npx(?:\.cmd)?$/i.test(args[1] ?? "")) return false;
|
|
202
|
+
args = args.slice(2);
|
|
203
|
+
} else if (!/^npx(?:\.cmd)?$/.test(command)) return false;
|
|
204
|
+
return args.length === 2 && ["-y", "--yes"].includes(args[0]) && new RegExp(`^@akms/mcp-${kind}(?:@[^\\s]+)?$`).test(args[1]);
|
|
205
|
+
}
|
|
206
|
+
function catalogLaunch(entry, kind = launchKind(entry)) {
|
|
207
|
+
return !!kind && entry.args.length === 2 && entry.args[1] === "--catalog" && standardLaunch({ ...entry, args: entry.args.slice(0, 1) }, kind);
|
|
208
|
+
}
|
|
209
|
+
function npmCli() {
|
|
210
|
+
const candidates = [
|
|
211
|
+
path2.join(path2.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
212
|
+
path2.join(path2.dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
|
|
213
|
+
];
|
|
214
|
+
if (process.env.npm_execpath && /^(npm|npx)-cli\.js$/.test(path2.basename(process.env.npm_execpath))) {
|
|
215
|
+
candidates.unshift(path2.join(path2.dirname(process.env.npm_execpath), "npm-cli.js"));
|
|
216
|
+
}
|
|
217
|
+
for (const folder of (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
|
|
218
|
+
candidates.push(process.platform === "win32" ? path2.join(folder, "node_modules", "npm", "bin", "npm-cli.js") : path2.join(folder, "npm"));
|
|
219
|
+
}
|
|
220
|
+
for (const file of candidates) {
|
|
221
|
+
try {
|
|
222
|
+
const resolved = fs2.realpathSync(file);
|
|
223
|
+
if (resolved.endsWith(".js") && fs2.statSync(resolved).isFile()) return resolved;
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new Error("npm was not found. Install Node.js with npm, then reopen configuration.");
|
|
228
|
+
}
|
|
229
|
+
function findGlobalRuntime(kind, root) {
|
|
230
|
+
try {
|
|
231
|
+
const directory = path2.join(root ?? runtimeRoot(), "@akms", `mcp-${kind}`);
|
|
232
|
+
const metadata = JSON.parse(fs2.readFileSync(path2.join(directory, "package.json"), "utf8"));
|
|
233
|
+
const bin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.[`akms-mcp-${kind}`];
|
|
234
|
+
const script = path2.join(directory, "lib", "cli.js");
|
|
235
|
+
if (metadata.name !== packageName(kind) || !["lib/cli.js", "./lib/cli.js"].includes(bin) || typeof metadata.version !== "string" || !fs2.statSync(script).isFile()) return null;
|
|
236
|
+
return { command: process.execPath, args: [script], version: metadata.version, linked: fs2.lstatSync(directory).isSymbolicLink() };
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function installGlobalRuntime(kind) {
|
|
242
|
+
const cancellation = new AbortController();
|
|
243
|
+
const cancel = () => cancellation.abort();
|
|
244
|
+
process.on("SIGINT", cancel);
|
|
245
|
+
process.on("SIGTERM", cancel);
|
|
246
|
+
try {
|
|
247
|
+
await promisify(execFile)(process.execPath, [npmCli(), "install", "--global", "--prefix", runtimePrefix(), "--prefer-online", "--no-fund", "--no-audit", `${packageName(kind)}@latest`], {
|
|
248
|
+
windowsHide: true,
|
|
249
|
+
timeout: 18e4,
|
|
250
|
+
maxBuffer: 1024 * 1024,
|
|
251
|
+
signal: cancellation.signal,
|
|
252
|
+
cwd: os.homedir()
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (cancellation.signal.aborted) throw new Error("AKMS runtime installation cancelled.");
|
|
256
|
+
const stderr = error.stderr;
|
|
257
|
+
const detail = stripVTControlCharacters(typeof stderr === "string" ? stderr : error instanceof Error ? error.message : "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").trim().split(/\r?\n/).slice(-6).map((line) => line.slice(0, 500)).join("\n");
|
|
258
|
+
throw new Error(`AKMS runtime installation failed. Run ${runtimeInstallCommand(kind)} in your terminal and retry.${detail ? `
|
|
259
|
+
|
|
260
|
+
${detail}` : ""}`);
|
|
261
|
+
} finally {
|
|
262
|
+
process.off("SIGINT", cancel);
|
|
263
|
+
process.off("SIGTERM", cancel);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/modules/registration/startup-check.ts
|
|
268
|
+
import { spawn, execFile as execFile2 } from "child_process";
|
|
269
|
+
import path3 from "path";
|
|
270
|
+
import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
|
|
271
|
+
|
|
56
272
|
// src/modules/launcher/prompter.ts
|
|
57
273
|
var PromptClosedError = class extends Error {
|
|
58
274
|
constructor() {
|
|
@@ -157,12 +373,14 @@ function createStyle(enabled) {
|
|
|
157
373
|
return {
|
|
158
374
|
accent: paint(36, 39),
|
|
159
375
|
success: paint(32, 39),
|
|
376
|
+
warning: paint(33, 39),
|
|
377
|
+
error: paint(31, 39),
|
|
160
378
|
dim: paint(2, 22),
|
|
161
379
|
bold: paint(1, 22)
|
|
162
380
|
};
|
|
163
381
|
}
|
|
164
382
|
var Prompter = class {
|
|
165
|
-
constructor(input, output) {
|
|
383
|
+
constructor(input, output, initialInput = "") {
|
|
166
384
|
this.input = input;
|
|
167
385
|
this.output = output;
|
|
168
386
|
const ttyInput = input;
|
|
@@ -178,6 +396,7 @@ var Prompter = class {
|
|
|
178
396
|
input.setEncoding("utf8");
|
|
179
397
|
input.on("data", this.onInputData);
|
|
180
398
|
input.on("end", this.onInputEnd);
|
|
399
|
+
if (initialInput) this.onData(initialInput);
|
|
181
400
|
}
|
|
182
401
|
input;
|
|
183
402
|
output;
|
|
@@ -220,6 +439,79 @@ var Prompter = class {
|
|
|
220
439
|
this.lineBuffer = "";
|
|
221
440
|
return text;
|
|
222
441
|
}
|
|
442
|
+
get paint() {
|
|
443
|
+
return this.style;
|
|
444
|
+
}
|
|
445
|
+
/** Available content columns, leaving room for the terminal cursor. */
|
|
446
|
+
get width() {
|
|
447
|
+
return this.columns() - 1;
|
|
448
|
+
}
|
|
449
|
+
/** Start a fresh terminal page without erasing scrollback or emitting ANSI to pipes. */
|
|
450
|
+
clearScreen() {
|
|
451
|
+
if (this.interactive) this.output.write(`${ESC}[2J${ESC}[H`);
|
|
452
|
+
}
|
|
453
|
+
/** Keep results visible before navigating; Esc acknowledges them like Enter. */
|
|
454
|
+
async acknowledge() {
|
|
455
|
+
try {
|
|
456
|
+
await this.pause();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (!(error instanceof PromptBackError)) throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async pause() {
|
|
462
|
+
if (this.interactive) await this.readText("Press Enter to continue ");
|
|
463
|
+
}
|
|
464
|
+
write(text) {
|
|
465
|
+
this.output.write(text);
|
|
466
|
+
}
|
|
467
|
+
async askRequired(question, defaultValue) {
|
|
468
|
+
for (; ; ) {
|
|
469
|
+
const answer = (await this.readText(`${question}${defaultValue ? ` (${defaultValue})` : ""}: `)).trim() || defaultValue;
|
|
470
|
+
if (answer) return answer;
|
|
471
|
+
this.write(" A value is required.\n");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async askYesNo(question, defaultValue) {
|
|
475
|
+
for (; ; ) {
|
|
476
|
+
const answer = (await this.readText(`${question} (${defaultValue ? "Y/n" : "y/N"}): `)).trim().toLowerCase();
|
|
477
|
+
if (!answer) return defaultValue;
|
|
478
|
+
if (answer === "y" || answer === "yes") return true;
|
|
479
|
+
if (answer === "n" || answer === "no") return false;
|
|
480
|
+
this.write(" Enter y or n.\n");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async readText(prompt) {
|
|
484
|
+
this.input.resume();
|
|
485
|
+
this.output.write(prompt);
|
|
486
|
+
if (!this.interactive) {
|
|
487
|
+
const line = await this.readLine();
|
|
488
|
+
if (line == null) throw new PromptClosedError();
|
|
489
|
+
if (line === "ESC" || line === ESC) throw new PromptBackError();
|
|
490
|
+
return line;
|
|
491
|
+
}
|
|
492
|
+
if (this.inputEnded) throw new PromptClosedError();
|
|
493
|
+
this.input.setRawMode?.(true);
|
|
494
|
+
this.output.write(`${ESC}[?25h`);
|
|
495
|
+
return new Promise((resolve, reject) => {
|
|
496
|
+
let text = "";
|
|
497
|
+
const finish = (error) => {
|
|
498
|
+
this.keyHandler = null;
|
|
499
|
+
this.input.setRawMode?.(false);
|
|
500
|
+
this.output.write("\n");
|
|
501
|
+
if (error) reject(error);
|
|
502
|
+
else resolve(text);
|
|
503
|
+
};
|
|
504
|
+
this.keyHandler = (key) => {
|
|
505
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) return finish(new PromptClosedError());
|
|
506
|
+
if (key === ESC) return finish(new PromptBackError());
|
|
507
|
+
if (key === "\r" || key === "\n") return finish();
|
|
508
|
+
if (key === "\b" || key === "\x7F") text = Array.from(text).slice(0, -1).join("");
|
|
509
|
+
else if (key === "") text = "";
|
|
510
|
+
else if (!/[\x00-\x1f\x7f]/.test(key)) text += key;
|
|
511
|
+
this.output.write(`\r${ESC}[K${prompt}${text}`);
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
223
515
|
/**
|
|
224
516
|
* Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
|
|
225
517
|
* TTY: a numbered list, answered by number, key or label.
|
|
@@ -486,193 +778,2468 @@ var Prompter = class {
|
|
|
486
778
|
}
|
|
487
779
|
};
|
|
488
780
|
|
|
489
|
-
// src/modules/
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
{ value: "cursor", label: CLIENT_LABELS.cursor },
|
|
521
|
-
{ separator: true },
|
|
522
|
-
{ value: "back", label: "Back" }
|
|
523
|
-
];
|
|
524
|
-
const client = await prompter.select("Client", clients, { defaultValue: targetId ?? "auto", cancelValue: "back" });
|
|
525
|
-
if (client === "auto") {
|
|
526
|
-
targetId = void 0;
|
|
527
|
-
} else if (client !== "back") {
|
|
528
|
-
targetId = client;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
} catch (error) {
|
|
532
|
-
if (error instanceof PromptClosedError) {
|
|
533
|
-
return 0;
|
|
534
|
-
}
|
|
535
|
-
throw error;
|
|
536
|
-
} finally {
|
|
537
|
-
prompter.close();
|
|
781
|
+
// src/modules/registration/comparison.ts
|
|
782
|
+
var visibleField = /^(DB_DRIVER|DB_HOST|DB_PORT|DB_NAME|DB_USER|DB_SSL|DB_DESCRIPTION|SSH_HOST|SSH_PORT|SSH_USER|SSH_DESCRIPTION|WSL_DISTRO|WSL_USER|WSL_DESCRIPTION|S3_BACKEND|S3_BUCKET|S3_REGION|S3_PREFIX|S3_AUTH|S3_DESCRIPTION)$/;
|
|
783
|
+
var plainText = (value) => value.replace(/[\x00-\x1f\x7f]/g, " ");
|
|
784
|
+
function descriptionOnly(entries) {
|
|
785
|
+
const fields = differingFields(entries);
|
|
786
|
+
return fields.length > 0 && fields.every((field) => /^env\.(DB|SSH|WSL|S3)_DESCRIPTION$/.test(field));
|
|
787
|
+
}
|
|
788
|
+
var fieldName = (field) => {
|
|
789
|
+
const key = field.replace(/^env\./, "");
|
|
790
|
+
if (/^(DB|SSH|WSL|S3)_DESCRIPTION$/.test(key)) return "Description";
|
|
791
|
+
const names = { command: "Launch command", args: "Launch arguments", DB_HOST: "Host", SSH_HOST: "Host", DB_PORT: "Port", SSH_PORT: "Port", DB_NAME: "Database", DB_USER: "User", SSH_USER: "User", WSL_USER: "User", WSL_DISTRO: "Linux distribution", S3_BUCKET: "Bucket", S3_REGION: "Region", S3_PREFIX: "Key prefix" };
|
|
792
|
+
return Object.hasOwn(names, key) ? names[key] : plainText(key);
|
|
793
|
+
};
|
|
794
|
+
function writeWrapped(prompter, text, indent = "", changed, style) {
|
|
795
|
+
const characters = Array.from(plainText(text));
|
|
796
|
+
let line = indent;
|
|
797
|
+
let width = displayWidth(indent);
|
|
798
|
+
let segment = "";
|
|
799
|
+
let highlighted = false;
|
|
800
|
+
const flush = () => {
|
|
801
|
+
line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : segment;
|
|
802
|
+
segment = "";
|
|
803
|
+
};
|
|
804
|
+
for (let index = 0; index < characters.length; index++) {
|
|
805
|
+
const character = characters[index];
|
|
806
|
+
const size = displayWidth(character);
|
|
807
|
+
if (width + size > prompter.width) {
|
|
808
|
+
flush();
|
|
809
|
+
prompter.write((style ? style(line) : line) + "\n");
|
|
810
|
+
line = indent;
|
|
811
|
+
width = displayWidth(indent);
|
|
538
812
|
}
|
|
539
|
-
|
|
540
|
-
|
|
813
|
+
const highlight = !!changed && index >= changed[0] && index < changed[1];
|
|
814
|
+
if (highlight !== highlighted) {
|
|
815
|
+
flush();
|
|
816
|
+
highlighted = highlight;
|
|
541
817
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
818
|
+
segment += character;
|
|
819
|
+
width += size;
|
|
820
|
+
}
|
|
821
|
+
flush();
|
|
822
|
+
prompter.write((style ? style(line) : line) + "\n");
|
|
823
|
+
}
|
|
824
|
+
function changedRange(values, index) {
|
|
825
|
+
if (values.length !== 2 || values[0] === values[1]) return void 0;
|
|
826
|
+
const current = Array.from(values[index]);
|
|
827
|
+
const other = Array.from(values[1 - index]);
|
|
828
|
+
let start = 0, end = current.length, otherEnd = other.length;
|
|
829
|
+
while (start < end && start < otherEnd && current[start] === other[start]) start++;
|
|
830
|
+
while (end > start && otherEnd > start && current[end - 1] === other[otherEnd - 1]) {
|
|
831
|
+
end--;
|
|
832
|
+
otherEnd--;
|
|
833
|
+
}
|
|
834
|
+
return [start, end];
|
|
835
|
+
}
|
|
836
|
+
function compareEntries(prompter, versions) {
|
|
837
|
+
const entries = versions.map((version) => version.entry);
|
|
838
|
+
const fields = differingFields(entries);
|
|
839
|
+
writeWrapped(prompter, descriptionOnly(entries) ? "Description differs. Connection settings match." : `${fields.length} setting(s) differ.`);
|
|
840
|
+
const values = fields.map((field) => {
|
|
841
|
+
const key = field.replace(/^env\./, "");
|
|
842
|
+
return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
|
|
843
|
+
});
|
|
844
|
+
if (versions.length === 2 && prompter.width >= 90) {
|
|
845
|
+
const width = Math.floor((prompter.width - 3) / 2);
|
|
846
|
+
const wrap = (value, changed) => {
|
|
847
|
+
const lines = [{ text: "", width: 0 }];
|
|
848
|
+
for (const [index, character] of Array.from(value).entries()) {
|
|
849
|
+
const size = displayWidth(character);
|
|
850
|
+
if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
|
|
851
|
+
const line = lines[lines.length - 1];
|
|
852
|
+
line.text += changed && index >= changed[0] && index < changed[1] ? prompter.paint.accent(prompter.paint.bold(character)) : character;
|
|
853
|
+
line.width += size;
|
|
547
854
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
855
|
+
return lines;
|
|
856
|
+
};
|
|
857
|
+
const row = (left, right) => {
|
|
858
|
+
for (let line = 0; line < Math.max(left.length, right.length); line++) {
|
|
859
|
+
prompter.write(`${left[line]?.text ?? ""}${" ".repeat(width - (left[line]?.width ?? 0))} | ${right[line]?.text ?? ""}
|
|
552
860
|
`);
|
|
553
861
|
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
${
|
|
557
|
-
|
|
862
|
+
};
|
|
863
|
+
prompter.write("\n");
|
|
864
|
+
row(wrap(`[1] ${plainText(versions[0].label)}`), wrap(`[2] ${plainText(versions[1].label)}`));
|
|
865
|
+
for (let field = 0; field < fields.length; field++) {
|
|
866
|
+
prompter.write(`
|
|
867
|
+
${prompter.paint.dim(fieldName(fields[field]))}
|
|
558
868
|
`);
|
|
869
|
+
row(wrap(values[field][0], changedRange(values[field], 0)), wrap(values[field][1], changedRange(values[field], 1)));
|
|
870
|
+
}
|
|
871
|
+
if (values.some((row2) => row2.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
|
|
872
|
+
prompter.write("\n");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
for (let index = 0; index < versions.length; index++) {
|
|
876
|
+
prompter.write("\n");
|
|
877
|
+
writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
|
|
878
|
+
for (let field = 0; field < fields.length; field++) {
|
|
879
|
+
writeWrapped(prompter, fieldName(fields[field]), " ");
|
|
880
|
+
writeWrapped(prompter, values[field][index], " ", changedRange(values[field], index));
|
|
559
881
|
}
|
|
560
882
|
}
|
|
883
|
+
if (values.some((row) => row.includes("(hidden)"))) prompter.write("\nSecret values are hidden.\n");
|
|
884
|
+
prompter.write("\n");
|
|
561
885
|
}
|
|
562
886
|
|
|
563
|
-
// src/modules/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
887
|
+
// src/modules/registration/startup-check.ts
|
|
888
|
+
function visibleRuntimePath(entry) {
|
|
889
|
+
const kind = launchKind(entry);
|
|
890
|
+
return kind && standardLaunch({ ...entry, args: entry.args.slice(0, 1) }, kind) && (entry.args.length === 1 || entry.args.length === 2 && entry.args[1] === "--catalog") ? entry.args[0] : void 0;
|
|
891
|
+
}
|
|
892
|
+
function redactStartupText(text, entry) {
|
|
893
|
+
const normalize2 = (value) => stripVTControlCharacters2(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
894
|
+
const direct = visibleRuntimePath(entry);
|
|
895
|
+
const privateValues = [...Object.entries(entry.env).filter(([key]) => !visibleField.test(key)).map(([, value]) => value), ...entry.args.slice(direct ? 1 : 0)];
|
|
896
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
897
|
+
for (const value of privateValues) {
|
|
898
|
+
if (!value) continue;
|
|
899
|
+
secrets.add(value);
|
|
900
|
+
const values = [value];
|
|
901
|
+
if (value.includes("=")) values.push(value.slice(value.indexOf("=") + 1));
|
|
902
|
+
for (const candidate of values) {
|
|
903
|
+
secrets.add(candidate);
|
|
904
|
+
try {
|
|
905
|
+
const url = new URL(candidate);
|
|
906
|
+
if (url.password) {
|
|
907
|
+
secrets.add(url.password);
|
|
908
|
+
secrets.add(decodeURIComponent(url.password));
|
|
909
|
+
}
|
|
910
|
+
} catch {
|
|
911
|
+
}
|
|
912
|
+
}
|
|
576
913
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
914
|
+
for (const value of [...secrets]) {
|
|
915
|
+
if (!value) continue;
|
|
916
|
+
secrets.add(JSON.stringify(value).slice(1, -1));
|
|
917
|
+
secrets.add(encodeURIComponent(value));
|
|
918
|
+
}
|
|
919
|
+
for (const value of [...secrets]) secrets.add(normalize2(value));
|
|
920
|
+
let result = normalize2(text);
|
|
921
|
+
for (const value of [...secrets].filter(Boolean).sort((a, b) => b.length - a.length)) result = result.split(value).join("(hidden)");
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
async function checkStartup(entry, timeoutMs = 1e4) {
|
|
925
|
+
return new Promise((resolve) => {
|
|
926
|
+
const inherited = process.platform === "win32" ? ["APPDATA", "LOCALAPPDATA", "HOMEDRIVE", "HOMEPATH", "PATH", "SYSTEMROOT", "SYSTEMDRIVE", "TEMP", "TMP", "USERPROFILE", "PROGRAMFILES"] : ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER", "TMPDIR"];
|
|
927
|
+
const env = Object.fromEntries(inherited.filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]]));
|
|
928
|
+
let stderr = "", stderrOmitted = false, output = "", bytes = 0, done = false, closing = false, detail;
|
|
929
|
+
let server, version, tools = 0, expected = 1, pages = 0;
|
|
930
|
+
const toolNames = [];
|
|
931
|
+
let timer, killTimer, endTimer;
|
|
932
|
+
let child;
|
|
933
|
+
const result = (code, signal) => {
|
|
934
|
+
if (done) return;
|
|
935
|
+
done = true;
|
|
936
|
+
clearTimeout(timer);
|
|
937
|
+
clearTimeout(killTimer);
|
|
938
|
+
clearTimeout(endTimer);
|
|
939
|
+
const clean = (value) => redactStartupText(value, entry);
|
|
940
|
+
const ok = !detail && closing && code === 0;
|
|
941
|
+
const runtime = visibleRuntimePath(entry);
|
|
942
|
+
const failure = detail ?? (code !== 0 ? `Exited ${code ?? signal ?? "without an exit code"}.` : "Startup check did not finish.");
|
|
943
|
+
resolve({
|
|
944
|
+
ok,
|
|
945
|
+
server: server && clean(server).slice(0, 200),
|
|
946
|
+
version: version && clean(version).slice(0, 100),
|
|
947
|
+
tools,
|
|
948
|
+
toolNames: toolNames.map(clean),
|
|
949
|
+
exitCode: code,
|
|
950
|
+
detail: ok ? void 0 : clean(`${failure}${runtime ? ` Runtime: ${runtime}` : ""}`).slice(0, 2e3),
|
|
951
|
+
stderr: clean(stderr).trim().split(/\r?\n/).filter(Boolean).slice(-6).map((line) => line.slice(0, 500))
|
|
952
|
+
});
|
|
953
|
+
};
|
|
954
|
+
const stop = (reason) => {
|
|
955
|
+
if (closing || done) return;
|
|
956
|
+
closing = true;
|
|
957
|
+
detail = reason;
|
|
958
|
+
clearTimeout(timer);
|
|
959
|
+
child.stdin?.end();
|
|
960
|
+
killTimer = setTimeout(() => {
|
|
961
|
+
if (process.platform === "win32" && child.pid) {
|
|
962
|
+
execFile2(path3.join(process.env.SYSTEMROOT ?? "C:\\Windows", "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 4e3 }, () => {
|
|
963
|
+
});
|
|
964
|
+
} else if (child.pid) {
|
|
965
|
+
try {
|
|
966
|
+
process.kill(-child.pid, "SIGKILL");
|
|
967
|
+
} catch {
|
|
968
|
+
child.kill("SIGKILL");
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
endTimer = setTimeout(() => {
|
|
972
|
+
child.kill("SIGKILL");
|
|
973
|
+
child.stdout?.destroy();
|
|
974
|
+
child.stderr?.destroy();
|
|
975
|
+
child.unref();
|
|
976
|
+
result(null);
|
|
977
|
+
}, 4500);
|
|
978
|
+
}, 500);
|
|
979
|
+
};
|
|
980
|
+
try {
|
|
981
|
+
child = spawn(entry.command, entry.args, { env: { ...env, ...entry.env }, shell: false, windowsHide: true, detached: process.platform !== "win32", stdio: "pipe" });
|
|
982
|
+
} catch (error) {
|
|
983
|
+
detail = error instanceof Error ? error.message : "Unable to start process.";
|
|
984
|
+
result(null);
|
|
985
|
+
return;
|
|
581
986
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
987
|
+
timer = setTimeout(() => stop(`Timed out after ${timeoutMs} ms.`), timeoutMs);
|
|
988
|
+
const send = (message) => child.stdin?.write(JSON.stringify(message) + "\n");
|
|
989
|
+
child.on("error", (error) => {
|
|
990
|
+
detail = `Unable to start ${entry.command} (${error.code ?? "error"}).`;
|
|
991
|
+
closing = true;
|
|
992
|
+
result(null);
|
|
993
|
+
});
|
|
994
|
+
child.on("close", (code, signal) => {
|
|
995
|
+
if (!closing) detail ??= "Process exited before initialize and tools/list completed.";
|
|
996
|
+
result(code, signal);
|
|
997
|
+
});
|
|
998
|
+
child.stdin?.on("error", () => {
|
|
999
|
+
if (!closing) stop("Server closed its input before the startup check completed.");
|
|
1000
|
+
});
|
|
1001
|
+
child.stderr?.setEncoding("utf8");
|
|
1002
|
+
child.stderr?.on("data", (chunk) => {
|
|
1003
|
+
if (stderrOmitted) return;
|
|
1004
|
+
stderr += chunk;
|
|
1005
|
+
if (stderr.length > 65536) {
|
|
1006
|
+
stderrOmitted = true;
|
|
1007
|
+
stderr = "[stderr exceeded the diagnostic limit; output omitted]";
|
|
1008
|
+
stop("Too much stderr output.");
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
child.stdout?.setEncoding("utf8");
|
|
1012
|
+
child.stdout?.on("data", (chunk) => {
|
|
1013
|
+
if (closing || done) return;
|
|
1014
|
+
bytes += Buffer.byteLength(chunk);
|
|
1015
|
+
if (bytes > 2 * 1024 * 1024) {
|
|
1016
|
+
stop("MCP response exceeded the startup check limit.");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
output += chunk;
|
|
1020
|
+
let newline;
|
|
1021
|
+
while (!closing && (newline = output.indexOf("\n")) >= 0) {
|
|
1022
|
+
const line = output.slice(0, newline);
|
|
1023
|
+
output = output.slice(newline + 1);
|
|
1024
|
+
try {
|
|
1025
|
+
const message = JSON.parse(line);
|
|
1026
|
+
if (message?.jsonrpc !== "2.0") throw new Error();
|
|
1027
|
+
if (message.method) {
|
|
1028
|
+
if (message.id !== void 0) send(message.method === "ping" ? { jsonrpc: "2.0", id: message.id, result: {} } : { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Not supported by startup check" } });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (message.id !== expected) continue;
|
|
1032
|
+
if (message.error) {
|
|
1033
|
+
stop(`MCP request failed: ${typeof message.error.message === "string" ? message.error.message : "server error"}`);
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
if (expected === 1) {
|
|
1037
|
+
if (typeof message.result?.protocolVersion !== "string" || typeof message.result?.serverInfo?.name !== "string" || typeof message.result?.serverInfo?.version !== "string") throw new Error();
|
|
1038
|
+
server = message.result.serverInfo.name;
|
|
1039
|
+
version = message.result.serverInfo.version;
|
|
1040
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
1041
|
+
send({ jsonrpc: "2.0", id: ++expected, method: "tools/list", params: {} });
|
|
1042
|
+
} else {
|
|
1043
|
+
if (!Array.isArray(message.result?.tools) || !message.result.tools.every((tool) => typeof tool?.name === "string")) throw new Error();
|
|
1044
|
+
tools += message.result.tools.length;
|
|
1045
|
+
toolNames.push(...message.result.tools.map((tool) => tool.name));
|
|
1046
|
+
if (message.result.nextCursor !== void 0) {
|
|
1047
|
+
if (typeof message.result.nextCursor !== "string" || ++pages > 100) throw new Error();
|
|
1048
|
+
send({ jsonrpc: "2.0", id: ++expected, method: "tools/list", params: { cursor: message.result.nextCursor } });
|
|
1049
|
+
} else stop();
|
|
1050
|
+
}
|
|
1051
|
+
} catch {
|
|
1052
|
+
stop("Invalid MCP response on stdout.");
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
child.once("spawn", () => send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "akms-startup-check", version: "1.0.0" } } }));
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
async function checkConnections(prompter, entries, check = checkStartup) {
|
|
1060
|
+
if (!entries.length) return;
|
|
1061
|
+
prompter.write("\nStartup check (initialize and tools/list only)\n");
|
|
1062
|
+
const checked = /* @__PURE__ */ new Map();
|
|
1063
|
+
let failed = false;
|
|
1064
|
+
for (const { name, entry, prefixes, maskEntries } of entries) {
|
|
1065
|
+
const key = JSON.stringify([entry.command, entry.args, Object.entries(entry.env).sort(([a], [b]) => a.localeCompare(b))]);
|
|
1066
|
+
let result = checked.get(key);
|
|
1067
|
+
if (!result) {
|
|
1068
|
+
try {
|
|
1069
|
+
result = await check(entry);
|
|
1070
|
+
} catch {
|
|
1071
|
+
result = { ok: false, exitCode: null, detail: "Unable to complete the startup check.", stderr: [] };
|
|
1072
|
+
}
|
|
1073
|
+
checked.set(key, result);
|
|
586
1074
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1075
|
+
const listedNames = result.toolNames ?? [];
|
|
1076
|
+
const longestFirst = [...prefixes ?? []].sort((a, b) => b.length - a.length);
|
|
1077
|
+
const covered = new Set(listedNames.map((tool) => longestFirst.find((prefix) => tool.startsWith(prefix))));
|
|
1078
|
+
const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
|
|
1079
|
+
if (result.ok && missing.length) result = { ...result, ok: false, detail: `Missing connection tools: ${missing.map((prefix) => prefix.slice(0, -2)).join(", ")}. Review catalog status before reconnecting.` };
|
|
1080
|
+
const safe = (value) => [entry, ...maskEntries ?? []].reduce((text, source) => redactStartupText(text, source), value);
|
|
1081
|
+
writeWrapped(prompter, safe(result.ok ? `OK ${name}: ${result.server}@${result.version}, ${result.tools} tools` : `FAILED ${name}: exit ${result.exitCode ?? "unavailable"}. ${result.detail ?? "Server failed to start."}`), "", void 0, result.ok ? prompter.paint.success : prompter.paint.error);
|
|
1082
|
+
if (!result.ok) {
|
|
1083
|
+
failed = true;
|
|
1084
|
+
for (const line of result.stderr) writeWrapped(prompter, safe(line), " ", void 0, prompter.paint.dim);
|
|
592
1085
|
}
|
|
593
1086
|
}
|
|
594
|
-
|
|
1087
|
+
if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
|
|
595
1088
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1089
|
+
|
|
1090
|
+
// src/modules/registration/catalog.ts
|
|
1091
|
+
import fs3 from "fs";
|
|
1092
|
+
import os2 from "os";
|
|
1093
|
+
import path4 from "path";
|
|
1094
|
+
var LONGEST_TOOL_NAME_LENGTH = { db: 13, ssh: 17, wsl: 17, s3: 11 };
|
|
1095
|
+
var maxConnectionNameLength = (kind) => 64 - `mcp__akms_${kind}__`.length - 2 - LONGEST_TOOL_NAME_LENGTH[kind];
|
|
1096
|
+
var defaultCatalogPath = () => path4.join(process.env.AKMS_MCP_HOME ? path4.resolve(process.env.AKMS_MCP_HOME) : path4.join(os2.homedir(), ".akms", "mcp"), "registry.json");
|
|
1097
|
+
|
|
1098
|
+
// src/modules/registration/registry.ts
|
|
1099
|
+
var sessionName = (kind) => `akms_${kind}`;
|
|
1100
|
+
var MIGRATION_STEPS = [
|
|
1101
|
+
{ id: "v1_to_v2", label: "Upgrade registry version 1 to version 2", needed: (state) => state.version === 1 },
|
|
1102
|
+
{ id: "catalog_default", label: "Record catalog mode as the default for new kinds", needed: (state) => state.defaultMode === void 0 }
|
|
1103
|
+
];
|
|
1104
|
+
var own = (value, key) => Object.hasOwn(value, key);
|
|
1105
|
+
var record = (value) => value != null && typeof value === "object" && !Array.isArray(value);
|
|
1106
|
+
var validEntry = (value) => record(value) && typeof value.command === "string" && Array.isArray(value.args) && value.args.every((arg) => typeof arg === "string") && record(value.env) && Object.values(value.env).every((item) => typeof item === "string");
|
|
1107
|
+
var RegistryError = class extends Error {
|
|
1108
|
+
};
|
|
1109
|
+
function entryHash(entry) {
|
|
1110
|
+
return createHash("sha256").update(JSON.stringify([entry.command, entry.args, Object.entries(entry.env).sort(([a], [b]) => a.localeCompare(b))])).digest("hex");
|
|
1111
|
+
}
|
|
1112
|
+
function differingFields(entries) {
|
|
1113
|
+
if (entries.length < 2) return [];
|
|
1114
|
+
const fields = [];
|
|
1115
|
+
if (new Set(entries.map((entry) => entry.command)).size > 1) fields.push("command");
|
|
1116
|
+
if (new Set(entries.map((entry) => JSON.stringify(entry.args))).size > 1) fields.push("args");
|
|
1117
|
+
for (const key of new Set(entries.flatMap((entry) => Object.keys(entry.env)))) {
|
|
1118
|
+
if (new Set(entries.map((entry) => own(entry.env, key) ? entry.env[key] : void 0)).size > 1) fields.push(`env.${key}`);
|
|
621
1119
|
}
|
|
1120
|
+
return fields;
|
|
622
1121
|
}
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
|
|
1122
|
+
function describeDifferences(entries) {
|
|
1123
|
+
const fields = differingFields(entries);
|
|
1124
|
+
if (fields.length && fields.every((field) => /^env\.(DB|SSH|WSL|S3)_DESCRIPTION$/.test(field))) return "Only the description differs; connection settings match.";
|
|
1125
|
+
return `Different fields: ${fields.map((field) => field.replace(/^env\./, "").replace(/[\x00-\x1f\x7f]/g, " ")).join(", ")}.`;
|
|
1126
|
+
}
|
|
1127
|
+
function entryKind(entry) {
|
|
1128
|
+
return launchKind(entry);
|
|
1129
|
+
}
|
|
1130
|
+
function defaultRegistryPath() {
|
|
1131
|
+
return defaultCatalogPath();
|
|
1132
|
+
}
|
|
1133
|
+
var Registry = class {
|
|
1134
|
+
constructor(targets, file = defaultRegistryPath(), runtimeOptions = {}) {
|
|
1135
|
+
this.targets = targets;
|
|
1136
|
+
this.runtimeOptions = runtimeOptions;
|
|
1137
|
+
this.file = file;
|
|
627
1138
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
let version;
|
|
636
|
-
try {
|
|
637
|
-
process.stdout.write(`
|
|
638
|
-
Checking the latest ${selected.packageName} release...
|
|
639
|
-
`);
|
|
640
|
-
version = await resolveLatestVersion({ npmCli, packageName: selected.packageName, signal: cancellation.signal });
|
|
641
|
-
} catch (error) {
|
|
642
|
-
if (cancellation.signal.aborted == true) {
|
|
643
|
-
return 130;
|
|
644
|
-
}
|
|
645
|
-
throw new Error(`Could not check the latest ${selected.packageName} release; setup was not started.
|
|
646
|
-
${String(error)}`, { cause: error });
|
|
647
|
-
} finally {
|
|
648
|
-
process.off("SIGINT", cancelLookup);
|
|
649
|
-
process.off("SIGTERM", cancelLookup);
|
|
1139
|
+
targets;
|
|
1140
|
+
runtimeOptions;
|
|
1141
|
+
file;
|
|
1142
|
+
observedText;
|
|
1143
|
+
backupCaptured = false;
|
|
1144
|
+
globalRuntime(kind) {
|
|
1145
|
+
return (this.runtimeOptions.find ?? findGlobalRuntime)(kind);
|
|
650
1146
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1147
|
+
latestRuntime(kind) {
|
|
1148
|
+
return (this.runtimeOptions.latest ?? latestRuntimeVersion)(kind);
|
|
1149
|
+
}
|
|
1150
|
+
isSessionMode(kind, state = this.read()) {
|
|
1151
|
+
return state.version === 2 && (state.sessions?.[kind]?.mode ?? state.defaultMode) === "catalog";
|
|
1152
|
+
}
|
|
1153
|
+
needsMigration(state = this.read()) {
|
|
1154
|
+
return MIGRATION_STEPS.some((step) => step.needed(state));
|
|
1155
|
+
}
|
|
1156
|
+
planMigration() {
|
|
1157
|
+
const state = this.read();
|
|
1158
|
+
const steps = MIGRATION_STEPS.filter((step) => step.needed(state)).map(({ id, label }) => ({ id, label }));
|
|
1159
|
+
const kinds = steps.length ? KINDS.map((kind) => {
|
|
1160
|
+
const names = Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => name);
|
|
1161
|
+
if (state.sessions?.[kind]?.mode === "individual") return { kind, names, status: "individual" };
|
|
1162
|
+
if (this.isSessionMode(kind, state)) return { kind, names, status: "catalog" };
|
|
1163
|
+
if (!names.length) return { kind, names, status: "empty" };
|
|
1164
|
+
try {
|
|
1165
|
+
for (const name of names) this.validateName(kind, name);
|
|
1166
|
+
if (!this.globalRuntime(kind)) {
|
|
1167
|
+
const changes = state.clients.map((client) => {
|
|
1168
|
+
const remove = names.filter((name) => state.connections[name].applied[client]);
|
|
1169
|
+
const review = [];
|
|
1170
|
+
try {
|
|
1171
|
+
const target = this.targets.find((item) => item.id === client);
|
|
1172
|
+
if (!target) throw new Error();
|
|
1173
|
+
const entries = target.readEntries();
|
|
1174
|
+
if (target.hasEntry?.(sessionName(kind)) ?? own(entries, sessionName(kind))) review.push(sessionName(kind));
|
|
1175
|
+
for (const name of remove) if ((target.hasEntry?.(name) ?? own(entries, name)) && (!entries[name] || entryHash(entries[name]) !== state.connections[name].applied[client])) review.push(name);
|
|
1176
|
+
} catch {
|
|
1177
|
+
review.push("Cannot inspect this service; Sync will retry.");
|
|
1178
|
+
}
|
|
1179
|
+
return { client, add: [sessionName(kind)], remove, review };
|
|
1180
|
+
});
|
|
1181
|
+
return { kind, names, status: "pending-runtime", changes, detail: "Install from Runtime, then run Sync. Existing entries stay until the session is verified." };
|
|
1182
|
+
}
|
|
1183
|
+
return { kind, names, status: "convert", session: this.sessionPlan(kind, "catalog", state) };
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
return { kind, names, status: "blocked", detail: error instanceof RegistryError ? error.message : "Cannot preview this kind. Check its runtime and catalog settings." };
|
|
1186
|
+
}
|
|
1187
|
+
}) : [];
|
|
1188
|
+
return { expected: JSON.stringify(state), steps, kinds };
|
|
1189
|
+
}
|
|
1190
|
+
applyMigration(plan) {
|
|
1191
|
+
if (!plan.steps.length) return this.locked(() => {
|
|
1192
|
+
if (JSON.stringify(this.read()) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Migrate registry.");
|
|
1193
|
+
return [];
|
|
1194
|
+
});
|
|
1195
|
+
return this.change((state) => {
|
|
1196
|
+
if (JSON.stringify(state) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Migrate registry.");
|
|
1197
|
+
if (plan.kinds.some((item) => item.status === "blocked")) throw new RegistryError("Resolve blocked kinds before migrating the registry.");
|
|
1198
|
+
for (const item of plan.kinds) {
|
|
1199
|
+
if (item.status === "pending-runtime" && this.globalRuntime(item.kind)) throw new RegistryError("Runtime availability changed since review. Reopen Migrate registry.");
|
|
1200
|
+
if (item.session) this.applySessionState(state, item.session, false);
|
|
1201
|
+
}
|
|
1202
|
+
for (const step of plan.steps) {
|
|
1203
|
+
if (step.id === "v1_to_v2") {
|
|
1204
|
+
state.version = 2;
|
|
1205
|
+
state.sessions ??= {};
|
|
1206
|
+
} else if (step.id === "catalog_default") state.defaultMode = "catalog";
|
|
1207
|
+
else throw new RegistryError("Unknown registry migration step. Reopen Migrate registry.");
|
|
1208
|
+
}
|
|
1209
|
+
}, plan.kinds.filter((item) => item.status === "convert" || item.status === "pending-runtime").map((item) => item.kind));
|
|
1210
|
+
}
|
|
1211
|
+
setDefaultMode(mode) {
|
|
1212
|
+
return this.change((state) => {
|
|
1213
|
+
if (state.version !== 2 || state.defaultMode === void 0) throw new RegistryError("Migrate registry before changing the default for new kinds.");
|
|
1214
|
+
if (state.defaultMode === mode) return;
|
|
1215
|
+
for (const kind of KINDS) if (!state.sessions?.[kind] && Object.values(state.connections).some((item) => item.kind === kind && item.entry)) {
|
|
1216
|
+
if (!this.globalRuntime(kind)) throw new RegistryError("Set up the pending runtimes and run Sync before changing the default for new kinds.");
|
|
1217
|
+
state.sessions ??= {};
|
|
1218
|
+
state.sessions[kind] = { mode: this.isSessionMode(kind, state) ? "catalog" : "individual", entry: this.sessionEntry(kind), applied: {} };
|
|
1219
|
+
}
|
|
1220
|
+
state.defaultMode = mode;
|
|
1221
|
+
}, []);
|
|
1222
|
+
}
|
|
1223
|
+
kindForName(name, state = this.read()) {
|
|
1224
|
+
return own(state.connections, name) ? state.connections[name].kind : KINDS.find((kind) => sessionName(kind) === name && (state.sessions?.[kind] || this.isSessionMode(kind, state)));
|
|
1225
|
+
}
|
|
1226
|
+
validateName(kind, name) {
|
|
1227
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name) || name.length > maxConnectionNameLength(kind)) throw new RegistryError(`Connection names must use 1-${maxConnectionNameLength(kind)} ASCII letters, digits, underscores or hyphens for ${kind.toUpperCase()}. Rename this connection before saving.`);
|
|
1228
|
+
if (KINDS.some((value) => sessionName(value) === name)) throw new RegistryError("Names akms_db, akms_ssh, akms_wsl and akms_s3 are reserved for session servers. Choose another name.");
|
|
1229
|
+
}
|
|
1230
|
+
sessionEntry(kind) {
|
|
1231
|
+
if (path5.basename(this.file) !== "registry.json") throw new RegistryError("Session mode requires a catalog named registry.json in the AKMS catalog directory.");
|
|
1232
|
+
const runtime = this.requireRuntime(kind);
|
|
1233
|
+
const env = {};
|
|
1234
|
+
if (process.env.AKMS_MCP_HOME || path5.resolve(this.file) !== path5.resolve(defaultCatalogPath())) env.AKMS_MCP_HOME = path5.dirname(path5.resolve(this.file));
|
|
1235
|
+
return { command: runtime.command, args: [...runtime.args, "--catalog"], env };
|
|
1236
|
+
}
|
|
1237
|
+
planSession(kind, mode) {
|
|
1238
|
+
return this.sessionPlan(kind, mode, this.read());
|
|
1239
|
+
}
|
|
1240
|
+
sessionPlan(kind, mode, state) {
|
|
1241
|
+
const names = Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => name);
|
|
1242
|
+
for (const name of names) this.validateName(kind, name);
|
|
1243
|
+
if (own(state.connections, sessionName(kind))) throw new RegistryError("Rename the connection using the reserved session server name before switching modes.");
|
|
1244
|
+
const entry = mode === "catalog" ? this.sessionEntry(kind) : state.sessions?.[kind]?.entry ?? (this.isSessionMode(kind, state) && this.globalRuntime(kind) ? this.sessionEntry(kind) : void 0);
|
|
1245
|
+
if (!entry) throw new RegistryError("This kind has no session server to switch back from.");
|
|
1246
|
+
const changes = state.clients.map((client) => {
|
|
1247
|
+
const target = this.targets.find((item) => item.id === client);
|
|
1248
|
+
const add = mode === "catalog" ? names.length ? [sessionName(kind)] : [] : names;
|
|
1249
|
+
const remove = mode === "catalog" ? Object.keys(state.connections).filter((name) => state.connections[name].kind === kind && state.connections[name].applied[client]) : [sessionName(kind)];
|
|
1250
|
+
const review = [];
|
|
1251
|
+
try {
|
|
1252
|
+
if (!target) throw new Error();
|
|
1253
|
+
const actual = target.readEntries();
|
|
1254
|
+
for (const name of [.../* @__PURE__ */ new Set([...add, ...remove])]) {
|
|
1255
|
+
const prior = name === sessionName(kind) ? state.sessions?.[kind]?.applied[client] : state.connections[name]?.applied[client];
|
|
1256
|
+
const wanted = mode === "catalog" && name === sessionName(kind) ? entry : mode === "individual" ? state.connections[name]?.entry : null;
|
|
1257
|
+
if ((target.hasEntry?.(name) ?? own(actual, name)) && (!actual[name] || entryHash(actual[name]) !== prior && (!wanted || entryHash(actual[name]) !== entryHash(wanted)))) review.push(name);
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
review.push("Cannot inspect this service; Sync will retry.");
|
|
1261
|
+
}
|
|
1262
|
+
return { client, add, remove, review };
|
|
1263
|
+
});
|
|
1264
|
+
return { kind, mode, entry, expected: JSON.stringify(state), names, changes, custom: names.filter((name) => !standardLaunch(state.connections[name].entry, kind)) };
|
|
1265
|
+
}
|
|
1266
|
+
applySession(plan) {
|
|
1267
|
+
return this.change((state) => this.applySessionState(state, plan));
|
|
1268
|
+
}
|
|
1269
|
+
applySessionState(state, plan, checkSnapshot = true) {
|
|
1270
|
+
if (checkSnapshot && JSON.stringify(state) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Runtime before switching modes.");
|
|
1271
|
+
if (plan.mode === "catalog" && entryHash(this.sessionEntry(plan.kind)) !== entryHash(plan.entry)) throw new RegistryError("The runtime changed since review. Reopen Runtime before switching modes.");
|
|
1272
|
+
state.version = 2;
|
|
1273
|
+
state.sessions ??= {};
|
|
1274
|
+
state.sessions[plan.kind] = { mode: plan.mode, entry: plan.entry, applied: state.sessions[plan.kind]?.applied ?? {} };
|
|
1275
|
+
}
|
|
1276
|
+
async checkSaved(prompter, names) {
|
|
1277
|
+
const state = this.read();
|
|
1278
|
+
const entries = [];
|
|
1279
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
1280
|
+
for (const name of new Set(names)) {
|
|
1281
|
+
const connection = state.connections[name];
|
|
1282
|
+
if (!connection?.entry) continue;
|
|
1283
|
+
if (this.isSessionMode(connection.kind, state)) sessions.add(connection.kind);
|
|
1284
|
+
else entries.push({ name, entry: connection.entry });
|
|
1285
|
+
}
|
|
1286
|
+
for (const kind of sessions) {
|
|
1287
|
+
const entry = state.sessions?.[kind]?.entry;
|
|
1288
|
+
if (!entry) {
|
|
1289
|
+
prompter.write(`Startup check pending for ${sessionName(kind)}: install its runtime from Runtime, then run Sync.
|
|
1290
|
+
`);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
entries.push({
|
|
1294
|
+
name: sessionName(kind),
|
|
1295
|
+
entry,
|
|
1296
|
+
prefixes: Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => `${name}__`),
|
|
1297
|
+
maskEntries: Object.values(state.connections).filter((item) => item.kind === kind && item.entry).map((item) => item.entry)
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
await checkConnections(prompter, entries, this.runtimeOptions.check);
|
|
1301
|
+
}
|
|
1302
|
+
async installRuntime(kind) {
|
|
1303
|
+
if (this.globalRuntime(kind)?.linked) throw new RegistryError("This is a linked development installation. Build or update its source checkout instead.");
|
|
1304
|
+
await (this.runtimeOptions.install ?? installGlobalRuntime)(kind);
|
|
1305
|
+
return this.requireRuntime(kind);
|
|
1306
|
+
}
|
|
1307
|
+
requireRuntime(kind) {
|
|
1308
|
+
const runtime = this.globalRuntime(kind);
|
|
1309
|
+
if (!runtime) throw new RegistryError(`Install the AKMS runtime first: ${runtimeInstallCommand(kind)}. Then reopen Runtime.`);
|
|
1310
|
+
return runtime;
|
|
1311
|
+
}
|
|
1312
|
+
runtimeSnapshot(state, kind) {
|
|
1313
|
+
return JSON.stringify([state.clients, Object.entries(state.connections).filter(([, item]) => item.kind === kind).map(([name, item]) => [name, item.entry ? entryHash(item.entry) : null]).sort(([a], [b]) => String(a).localeCompare(String(b)))]);
|
|
1314
|
+
}
|
|
1315
|
+
planRuntime(kind) {
|
|
1316
|
+
const state = this.read();
|
|
1317
|
+
const runtime = this.requireRuntime(kind);
|
|
1318
|
+
const names = [], skipped = [];
|
|
1319
|
+
for (const [name, item] of Object.entries(state.connections)) {
|
|
1320
|
+
if (item.kind !== kind || !item.entry) continue;
|
|
1321
|
+
if (!standardLaunch(item.entry, kind)) {
|
|
1322
|
+
skipped.push(name);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (item.entry.command !== runtime.command || JSON.stringify(item.entry.args) !== JSON.stringify(runtime.args)) names.push(name);
|
|
1326
|
+
}
|
|
1327
|
+
return { kind, runtime, names, skipped, expected: this.runtimeSnapshot(state, kind) };
|
|
1328
|
+
}
|
|
1329
|
+
applyRuntime(plan) {
|
|
1330
|
+
return this.change((state) => {
|
|
1331
|
+
if (this.runtimeSnapshot(state, plan.kind) !== plan.expected) throw new RegistryError("Connections or registration changed since review. Reopen Runtime before applying.");
|
|
1332
|
+
const runtime = this.requireRuntime(plan.kind);
|
|
1333
|
+
if (JSON.stringify(runtime) !== JSON.stringify(plan.runtime)) throw new RegistryError("The global installation changed since review. Reopen Runtime before applying.");
|
|
1334
|
+
for (const name of plan.names) {
|
|
1335
|
+
const item = state.connections[name];
|
|
1336
|
+
if (!item?.entry || item.kind !== plan.kind || !standardLaunch(item.entry, plan.kind)) throw new RegistryError("This connection has a custom launch command. Review it individually.");
|
|
1337
|
+
item.entry = { ...item.entry, command: runtime.command, args: [...runtime.args] };
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
read() {
|
|
1342
|
+
let text;
|
|
1343
|
+
try {
|
|
1344
|
+
text = fs4.readFileSync(this.file, "utf8");
|
|
1345
|
+
this.observedText = text;
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (error.code === "ENOENT") {
|
|
1348
|
+
this.observedText = null;
|
|
1349
|
+
return { version: 2, defaultMode: "catalog", clients: [], connections: {}, sessions: {} };
|
|
1350
|
+
}
|
|
1351
|
+
throw new RegistryError("Cannot read the AKMS registry. Check its permissions.");
|
|
1352
|
+
}
|
|
1353
|
+
let value;
|
|
1354
|
+
try {
|
|
1355
|
+
value = JSON.parse(text);
|
|
1356
|
+
} catch {
|
|
1357
|
+
throw new RegistryError("Invalid AKMS registry JSON; the file was not changed.");
|
|
1358
|
+
}
|
|
1359
|
+
if (!record(value) || ![1, 2].includes(value.version)) throw new RegistryError("Unsupported AKMS registry version; update this MCP package before changing it.");
|
|
1360
|
+
if (!Array.isArray(value.clients) || !value.clients.every((id) => typeof id === "string" && own(CLIENT_LABELS2, id)) || new Set(value.clients).size !== value.clients.length || !record(value.connections)) throw new RegistryError("Invalid AKMS registry; the file was not changed.");
|
|
1361
|
+
for (const [name, connection] of Object.entries(value.connections)) {
|
|
1362
|
+
if (!name.trim() || !record(connection) || !KINDS.includes(connection.kind) || !(connection.entry === null || validEntry(connection.entry)) || !record(connection.applied) || Object.entries(connection.applied).some(([id, hash]) => !own(CLIENT_LABELS2, id) || typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)) || connection.replacement != null && typeof connection.replacement !== "string") throw new RegistryError("Invalid AKMS registry entry; the file was not changed.");
|
|
1363
|
+
}
|
|
1364
|
+
if (value.version === 2) {
|
|
1365
|
+
if (!record(value.sessions) || value.defaultMode !== void 0 && !["catalog", "individual"].includes(value.defaultMode)) throw new RegistryError("Invalid session server state; the file was not changed.");
|
|
1366
|
+
for (const [kind, session] of Object.entries(value.sessions)) {
|
|
1367
|
+
if (!KINDS.includes(kind) || !record(session) || !["catalog", "individual"].includes(session.mode) || !validEntry(session.entry) || !catalogLaunch(session.entry, kind) || Object.keys(session.entry.env).some((key) => key !== "AKMS_MCP_HOME") || !record(session.applied) || Object.entries(session.applied).some(([id, hash]) => !own(CLIENT_LABELS2, id) || typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))) throw new RegistryError("Invalid session server state; the file was not changed.");
|
|
1368
|
+
if (own(value.connections, sessionName(kind))) throw new RegistryError("A connection uses a reserved session server name; the file was not changed.");
|
|
1369
|
+
}
|
|
1370
|
+
} else if (value.sessions !== void 0 || value.defaultMode !== void 0) throw new RegistryError("Session server state requires registry version 2; the file was not changed.");
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
save(state) {
|
|
1374
|
+
const after = JSON.stringify(state, null, 2) + "\n";
|
|
1375
|
+
updateConfigFile(this.file, (before) => {
|
|
1376
|
+
if (this.observedText !== void 0 && before !== (this.observedText ?? "")) throw new RegistryError("The AKMS catalog changed outside this command. Reload it and retry Sync.");
|
|
1377
|
+
return after;
|
|
1378
|
+
}, { beforeCommit: (before) => {
|
|
1379
|
+
if (before !== null && !this.backupCaptured) {
|
|
1380
|
+
updateConfigFile(path5.join(path5.dirname(this.file), "registry.previous.json"), () => before, { mode: fs4.statSync(this.file).mode & 511 });
|
|
1381
|
+
this.backupCaptured = true;
|
|
1382
|
+
}
|
|
1383
|
+
} });
|
|
1384
|
+
this.observedText = after;
|
|
1385
|
+
}
|
|
1386
|
+
/** Serializes standalone editors across packages, including their config-file writes. */
|
|
1387
|
+
locked(action) {
|
|
1388
|
+
fs4.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
|
|
1389
|
+
const lock = this.file + ".lock";
|
|
1390
|
+
let descriptor;
|
|
1391
|
+
try {
|
|
1392
|
+
descriptor = fs4.openSync(lock, "wx", 384);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (error.code !== "EEXIST") throw error;
|
|
1395
|
+
let abandoned = false;
|
|
1396
|
+
try {
|
|
1397
|
+
const pid = Number(fs4.readFileSync(lock, "utf8"));
|
|
1398
|
+
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
1399
|
+
try {
|
|
1400
|
+
process.kill(pid, 0);
|
|
1401
|
+
} catch (probe) {
|
|
1402
|
+
abandoned = probe.code === "ESRCH";
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
if (!abandoned) throw new RegistryError("Another AKMS configuration command is running. Finish it and retry.");
|
|
1408
|
+
fs4.unlinkSync(lock);
|
|
1409
|
+
descriptor = fs4.openSync(lock, "wx", 384);
|
|
1410
|
+
}
|
|
1411
|
+
this.backupCaptured = false;
|
|
1412
|
+
try {
|
|
1413
|
+
fs4.writeFileSync(descriptor, String(process.pid));
|
|
1414
|
+
return action();
|
|
1415
|
+
} finally {
|
|
1416
|
+
this.backupCaptured = false;
|
|
1417
|
+
fs4.closeSync(descriptor);
|
|
1418
|
+
fs4.unlinkSync(lock);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
change(action, kinds) {
|
|
1422
|
+
return this.locked(() => {
|
|
1423
|
+
const state = this.read();
|
|
1424
|
+
action(state);
|
|
1425
|
+
for (const kind of KINDS) {
|
|
1426
|
+
if (this.isSessionMode(kind, state) && !state.sessions?.[kind] && Object.values(state.connections).some((item) => item.kind === kind && item.entry) && this.globalRuntime(kind)) {
|
|
1427
|
+
state.sessions ??= {};
|
|
1428
|
+
state.sessions[kind] = { mode: "catalog", entry: this.sessionEntry(kind), applied: {} };
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
this.save(state);
|
|
1432
|
+
return this.reconcile(state, true, kinds);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
register(clients, expected, imports = []) {
|
|
1436
|
+
if (clients.some((id) => !own(CLIENT_LABELS2, id))) throw new RegistryError("Unknown AI service.");
|
|
1437
|
+
return this.change((state) => {
|
|
1438
|
+
if (expected && JSON.stringify(state.clients) !== JSON.stringify(expected)) throw new RegistryError("Registration changed in another command. Reopen Register before applying.");
|
|
1439
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1440
|
+
for (const candidate of imports) grouped.set(candidate.name, [...grouped.get(candidate.name) ?? [], candidate]);
|
|
1441
|
+
for (const [name, candidates] of grouped) {
|
|
1442
|
+
if (own(state.connections, name)) throw new RegistryError("The shared catalog changed since review. Reopen Register or Import before applying.");
|
|
1443
|
+
const chosen = candidates[0];
|
|
1444
|
+
this.validateName(chosen.kind, name);
|
|
1445
|
+
const applied = {};
|
|
1446
|
+
for (const candidate of candidates) {
|
|
1447
|
+
if (!clients.includes(candidate.client) || candidate.kind !== chosen.kind || entryKind(candidate.entry) !== chosen.kind || entryHash(candidate.entry) !== entryHash(chosen.entry)) throw new RegistryError("Different existing settings need an explicit choice before import.");
|
|
1448
|
+
const actual = this.targets.find((target) => target.id === candidate.client)?.readEntries()[name];
|
|
1449
|
+
if (!actual || entryHash(actual) !== entryHash(candidate.entry)) throw new RegistryError("Existing settings changed since review. Reopen Register or Import before applying.");
|
|
1450
|
+
applied[candidate.client] = entryHash(actual);
|
|
1451
|
+
}
|
|
1452
|
+
Object.defineProperty(state.connections, name, { value: { kind: chosen.kind, entry: chosen.entry, applied }, enumerable: true, configurable: true, writable: true });
|
|
1453
|
+
}
|
|
1454
|
+
state.clients = [...new Set(clients)];
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/** Discover without writing. Only unambiguous, previously unmanaged names are batched. */
|
|
1458
|
+
planRegistration(clients, kind) {
|
|
1459
|
+
const state = this.read();
|
|
1460
|
+
const { entries, errors } = this.candidates(kind, clients);
|
|
1461
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1462
|
+
for (const entry of entries) groups.set(entry.name, [...groups.get(entry.name) ?? [], entry]);
|
|
1463
|
+
const imports = [];
|
|
1464
|
+
const conflicts = [];
|
|
1465
|
+
for (const [name, candidates] of groups) {
|
|
1466
|
+
if (own(state.connections, name)) continue;
|
|
1467
|
+
try {
|
|
1468
|
+
this.validateName(candidates[0].kind, name);
|
|
1469
|
+
} catch {
|
|
1470
|
+
conflicts.push(name);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (new Set(candidates.map((candidate) => entryHash(candidate.entry))).size === 1) imports.push(...candidates);
|
|
1474
|
+
else conflicts.push(name);
|
|
1475
|
+
}
|
|
1476
|
+
return { imports, conflicts, errors };
|
|
1477
|
+
}
|
|
1478
|
+
write(kind, name, entry, oldName, expected) {
|
|
1479
|
+
this.validateName(kind, name);
|
|
1480
|
+
if (!validEntry(entry) || entryKind(entry) !== kind || catalogLaunch(entry)) throw new RegistryError("Invalid connection name or MCP entry.");
|
|
1481
|
+
return this.change((state) => {
|
|
1482
|
+
if (!state.clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
|
|
1483
|
+
const observedName = oldName ?? name;
|
|
1484
|
+
const observed = own(state.connections, observedName) ? state.connections[observedName].entry : null;
|
|
1485
|
+
if (expected !== void 0 && (observed ? entryHash(observed) : null) !== expected) throw new RegistryError("This connection changed while the form was open. Reload it before saving.");
|
|
1486
|
+
const previous = own(state.connections, name) ? state.connections[name] : void 0;
|
|
1487
|
+
if (previous && previous.kind !== kind) throw new RegistryError(`The name ${name} is already used by another MCP. Choose another name.`);
|
|
1488
|
+
if (oldName && oldName !== name && previous) throw new RegistryError(`The name ${name} already exists. Choose another name.`);
|
|
1489
|
+
if (oldName && oldName !== name) {
|
|
1490
|
+
const old = own(state.connections, oldName) ? state.connections[oldName] : void 0;
|
|
1491
|
+
if (!old || old.kind !== kind || !old.entry) throw new RegistryError("The connection changed; reload before renaming it.");
|
|
1492
|
+
old.entry = null;
|
|
1493
|
+
old.replacement = name;
|
|
1494
|
+
}
|
|
1495
|
+
Object.defineProperty(state.connections, name, { value: { kind, entry, applied: previous?.applied ?? {} }, enumerable: true, configurable: true, writable: true });
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
remove(kind, name, expected) {
|
|
1499
|
+
return this.change((state) => {
|
|
1500
|
+
const observed = own(state.connections, name) ? state.connections[name].entry : null;
|
|
1501
|
+
if (expected !== void 0 && (observed ? entryHash(observed) : null) !== expected) throw new RegistryError("This connection changed since it was displayed. Reload it before removing it.");
|
|
1502
|
+
if (!own(state.connections, name)) return;
|
|
1503
|
+
if (state.connections[name].kind !== kind) throw new RegistryError("This connection belongs to another MCP.");
|
|
1504
|
+
state.connections[name].entry = null;
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
sync() {
|
|
1508
|
+
return this.change(() => {
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
inspect() {
|
|
1512
|
+
return this.reconcile(this.read(), false);
|
|
1513
|
+
}
|
|
1514
|
+
formatResults(results, prefix = "", paint, reviewHint) {
|
|
1515
|
+
return formatResults(results, prefix, {
|
|
1516
|
+
paint,
|
|
1517
|
+
reviewHint,
|
|
1518
|
+
connectionCount: Object.values(this.read().connections).filter((item) => item.entry).length
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
reconcile(state, apply, kinds) {
|
|
1522
|
+
const results = [];
|
|
1523
|
+
const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
|
|
1524
|
+
for (const kind of kinds ?? KINDS) if (this.isSessionMode(kind, state) && active(kind).length && !state.sessions?.[kind]) {
|
|
1525
|
+
for (const client of state.clients) results.push({
|
|
1526
|
+
client,
|
|
1527
|
+
name: sessionName(kind),
|
|
1528
|
+
kind,
|
|
1529
|
+
connections: active(kind).map(([name]) => name),
|
|
1530
|
+
status: "pending",
|
|
1531
|
+
waitingFor: "runtime",
|
|
1532
|
+
detail: "Install it from Runtime, then run Sync. Your existing entries keep working."
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
const work = [
|
|
1536
|
+
...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
|
|
1537
|
+
...(kinds ?? KINDS).flatMap((kind) => state.sessions?.[kind] ? [{ name: sessionName(kind), bundle: true, connection: { kind, entry: state.sessions[kind].entry, applied: state.sessions[kind].applied } }] : [])
|
|
1538
|
+
];
|
|
1539
|
+
const wanted = (item) => item.bundle ? this.isSessionMode(item.connection.kind, state) && active(item.connection.kind).length ? item.connection.entry : null : this.isSessionMode(item.connection.kind, state) ? null : item.connection.entry;
|
|
1540
|
+
work.sort((a, b) => (wanted(a) ? 0 : a.bundle ? 2 : 1) - (wanted(b) ? 0 : b.bundle ? 2 : 1));
|
|
1541
|
+
for (const item of work) {
|
|
1542
|
+
const { name, connection, bundle } = item;
|
|
1543
|
+
const clients = /* @__PURE__ */ new Set([...state.clients, ...Object.keys(connection.applied)]);
|
|
1544
|
+
for (const client of clients) {
|
|
1545
|
+
const desired = state.clients.includes(client) ? wanted(item) : null;
|
|
1546
|
+
const previous = connection.applied[client];
|
|
1547
|
+
if (!desired && !previous) continue;
|
|
1548
|
+
const target = this.targets.find((candidate) => candidate.id === client);
|
|
1549
|
+
const report = (status, detail, waitingFor) => results.push({
|
|
1550
|
+
client,
|
|
1551
|
+
name,
|
|
1552
|
+
status,
|
|
1553
|
+
detail,
|
|
1554
|
+
kind: connection.kind,
|
|
1555
|
+
connections: bundle ? active(connection.kind).map(([name2]) => name2) : [name],
|
|
1556
|
+
...waitingFor ? { waitingFor } : {}
|
|
1557
|
+
});
|
|
1558
|
+
if (!target) {
|
|
1559
|
+
report("error", "Service adapter unavailable; use a newer package to retry.");
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
const entries = target.readEntries();
|
|
1564
|
+
const actual = own(entries, name) ? entries[name] : void 0;
|
|
1565
|
+
const exists = target.hasEntry?.(name) ?? actual != null;
|
|
1566
|
+
const hash = actual ? entryHash(actual) : void 0;
|
|
1567
|
+
if (desired && hash === entryHash(desired)) {
|
|
1568
|
+
if (apply) connection.applied[client] = hash;
|
|
1569
|
+
report("synced", "Up to date");
|
|
1570
|
+
} else if (!desired && !exists) {
|
|
1571
|
+
if (apply) delete connection.applied[client];
|
|
1572
|
+
report("removed", "Removed");
|
|
1573
|
+
} else if (exists && (!actual || !previous || hash !== previous)) {
|
|
1574
|
+
report("conflict", actual && desired ? describeDifferences([desired, actual]) : actual ? "Changed outside AKMS; review before removing this entry." : "This name is used by another server type; choose another name.");
|
|
1575
|
+
} else {
|
|
1576
|
+
if (!desired && state.clients.includes(client)) {
|
|
1577
|
+
if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
|
|
1578
|
+
const session = state.sessions?.[connection.kind];
|
|
1579
|
+
const replacement2 = entries[sessionName(connection.kind)];
|
|
1580
|
+
if (!session || !replacement2 || entryHash(replacement2) !== entryHash(session.entry) || session.applied[client] !== entryHash(session.entry)) {
|
|
1581
|
+
report("pending", "Connection entry kept until the session server is applied.", "session");
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (bundle && !this.isSessionMode(connection.kind, state) && active(connection.kind).some(([connectionName, value]) => !entries[connectionName] || entryHash(entries[connectionName]) !== entryHash(value.entry))) {
|
|
1586
|
+
report("pending", "Session entry kept until all connection entries are applied.", "connections");
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
let replacement = connection.replacement && state.connections[connection.replacement];
|
|
1591
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1592
|
+
while (replacement && !replacement.entry && replacement.replacement && !visited.has(replacement.replacement)) {
|
|
1593
|
+
visited.add(replacement.replacement);
|
|
1594
|
+
replacement = state.connections[replacement.replacement];
|
|
1595
|
+
}
|
|
1596
|
+
if (!desired && state.clients.includes(client) && !this.isSessionMode(connection.kind, state) && replacement && replacement.entry && replacement.applied[client] !== entryHash(replacement.entry)) {
|
|
1597
|
+
report("pending", "Old name kept until the new name is applied.", "rename");
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (!apply) {
|
|
1601
|
+
report("pending", desired ? "Needs applying" : "Needs removal");
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
|
|
1605
|
+
if (result.status !== "written") {
|
|
1606
|
+
report("error", "Could not write this service's settings. Check its CLI, syntax and permissions.");
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
const after = target.readEntries()[name];
|
|
1610
|
+
if (desired ? !after || entryHash(after) !== entryHash(desired) : target.hasEntry?.(name) ?? after != null) {
|
|
1611
|
+
report("error", "Settings could not be verified; retry Sync.");
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (desired) connection.applied[client] = entryHash(desired);
|
|
1615
|
+
else delete connection.applied[client];
|
|
1616
|
+
report(desired ? "synced" : "removed", desired ? "Applied" : "Removed");
|
|
1617
|
+
}
|
|
1618
|
+
} catch {
|
|
1619
|
+
report("error", "Cannot read or update settings. Check the configuration syntax, CLI and file permissions.");
|
|
1620
|
+
}
|
|
1621
|
+
if (apply) this.save(state);
|
|
1622
|
+
}
|
|
1623
|
+
if (apply && !bundle && !connection.entry && Object.keys(connection.applied).length === 0) delete state.connections[name];
|
|
1624
|
+
}
|
|
1625
|
+
if (apply) this.save(state);
|
|
1626
|
+
return results;
|
|
1627
|
+
}
|
|
1628
|
+
candidates(kind, clients = this.read().clients) {
|
|
1629
|
+
const entries = [];
|
|
1630
|
+
const errors = [];
|
|
1631
|
+
for (const client of clients) {
|
|
1632
|
+
const target = this.targets.find((item) => item.id === client);
|
|
1633
|
+
try {
|
|
1634
|
+
if (!target) throw new RegistryError();
|
|
1635
|
+
for (const [name, entry] of Object.entries(target.readEntries())) {
|
|
1636
|
+
const found = entryKind(entry);
|
|
1637
|
+
if (found && !catalogLaunch(entry) && (!kind || found === kind)) entries.push({ client, name, kind: found, entry });
|
|
1638
|
+
}
|
|
1639
|
+
} catch {
|
|
1640
|
+
errors.push(`${CLIENT_LABELS2[client]}: cannot inspect existing entries.`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return { entries, errors };
|
|
1644
|
+
}
|
|
1645
|
+
importEntry(name, chosen, reviewed) {
|
|
1646
|
+
this.validateName(chosen.kind, name);
|
|
1647
|
+
if (catalogLaunch(chosen.entry)) throw new RegistryError("Session server entries are not individual connections.");
|
|
1648
|
+
return this.change((state) => {
|
|
1649
|
+
if (!name.trim()) throw new RegistryError("A connection name is required.");
|
|
1650
|
+
const previous = own(state.connections, name) ? state.connections[name] : void 0;
|
|
1651
|
+
if (name !== chosen.name && previous) throw new RegistryError("That shared name already exists. Choose a new name to keep both connections.");
|
|
1652
|
+
if (previous && previous.kind !== chosen.kind) throw new RegistryError("This name belongs to another MCP. Choose another name.");
|
|
1653
|
+
const applied = { ...previous?.applied };
|
|
1654
|
+
for (const candidate of reviewed) {
|
|
1655
|
+
const target = this.targets.find((item) => item.id === candidate.client);
|
|
1656
|
+
const actual = target?.readEntries()[candidate.name];
|
|
1657
|
+
if (!actual || entryHash(actual) !== entryHash(candidate.entry)) throw new RegistryError("Settings changed since review; import again.");
|
|
1658
|
+
if (candidate.name === name && candidate.kind === chosen.kind) applied[candidate.client] = entryHash(actual);
|
|
1659
|
+
}
|
|
1660
|
+
Object.defineProperty(state.connections, name, { value: { kind: chosen.kind, entry: chosen.entry, applied }, enumerable: true, configurable: true, writable: true });
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
resolve(client, name, expected, useClient) {
|
|
1664
|
+
return this.change((state) => {
|
|
1665
|
+
const sessionKind = KINDS.find((kind) => sessionName(kind) === name && state.sessions?.[kind]);
|
|
1666
|
+
if (sessionKind) {
|
|
1667
|
+
const actual2 = this.targets.find((item) => item.id === client)?.readEntries()[name];
|
|
1668
|
+
if (!actual2 || entryHash(actual2) !== entryHash(expected)) throw new RegistryError("Settings changed since review; run Sync again.");
|
|
1669
|
+
if (useClient || entryKind(actual2) !== sessionKind) throw new RegistryError("Session launches can only be restored from AKMS. Keep external settings or rename the conflicting entry manually.");
|
|
1670
|
+
state.sessions[sessionKind].applied[client] = entryHash(actual2);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const connection = own(state.connections, name) ? state.connections[name] : void 0;
|
|
1674
|
+
const target = this.targets.find((item) => item.id === client);
|
|
1675
|
+
const actual = target?.readEntries()[name];
|
|
1676
|
+
if (!connection || !actual || entryHash(actual) !== entryHash(expected)) throw new RegistryError("Settings changed since review; run Sync again.");
|
|
1677
|
+
if (entryKind(actual) !== connection.kind) throw new RegistryError("This entry runs a different server. Rename it manually before retrying.");
|
|
1678
|
+
connection.applied[client] = entryHash(actual);
|
|
1679
|
+
if (useClient) {
|
|
1680
|
+
if (!state.clients.includes(client)) throw new RegistryError("Register this service before importing its changes.");
|
|
1681
|
+
this.validateName(connection.kind, name);
|
|
1682
|
+
if (catalogLaunch(actual)) throw new RegistryError("A session server cannot be imported as an individual connection.");
|
|
1683
|
+
connection.entry = actual;
|
|
1684
|
+
delete connection.replacement;
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
target(kind) {
|
|
1689
|
+
const registry = this;
|
|
1690
|
+
let snapshot;
|
|
1691
|
+
const expected = (name) => snapshot ? own(snapshot, name) ? entryHash(snapshot[name]) : null : void 0;
|
|
1692
|
+
const saved = (results) => ({ status: "written", detail: registry.formatResults(results, "Saved in the shared AKMS catalog.") });
|
|
1693
|
+
const prepare = (name, entry, oldName) => {
|
|
1694
|
+
if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
|
|
1695
|
+
const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
|
|
1696
|
+
if (prior && !standardLaunch(prior, kind)) return { ...entry, command: prior.command, args: [...prior.args] };
|
|
1697
|
+
const runtime = registry.requireRuntime(kind);
|
|
1698
|
+
return { ...entry, command: runtime.command, args: [...runtime.args] };
|
|
1699
|
+
};
|
|
1700
|
+
return {
|
|
1701
|
+
id: "registered",
|
|
1702
|
+
detected: true,
|
|
1703
|
+
get label() {
|
|
1704
|
+
const clients = registry.read().clients;
|
|
1705
|
+
return clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "No services registered";
|
|
1706
|
+
},
|
|
1707
|
+
location: this.file,
|
|
1708
|
+
readEntries: () => {
|
|
1709
|
+
const entries = Object.fromEntries(Object.entries(registry.read().connections).filter(([, value]) => value.kind === kind && value.entry).map(([name, value]) => [name, value.entry]));
|
|
1710
|
+
snapshot = structuredClone(entries);
|
|
1711
|
+
return entries;
|
|
1712
|
+
},
|
|
1713
|
+
hasEntry: (name) => own(registry.read().connections, name),
|
|
1714
|
+
prepareEntry: prepare,
|
|
1715
|
+
writeEntry: (name, entry) => saved(registry.write(kind, name, prepare(name, entry), void 0, expected(name))),
|
|
1716
|
+
renameEntry: (oldName, name, entry) => saved(registry.write(kind, name, prepare(name, entry, oldName), oldName, expected(oldName))),
|
|
1717
|
+
removeEntry: (name) => saved(registry.remove(kind, name, expected(name)))
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
function formatResults(results, prefix = "", options = {}) {
|
|
1722
|
+
const plain = (text) => text;
|
|
1723
|
+
const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
|
|
1724
|
+
const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
|
|
1725
|
+
const lines = prefix ? [paint.bold(prefix)] : [];
|
|
1726
|
+
const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
|
|
1727
|
+
lines.push(` Connections: ${total}`);
|
|
1728
|
+
const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
|
|
1729
|
+
const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
|
|
1730
|
+
const removed = results.filter((item) => item.status === "removed").length;
|
|
1731
|
+
const failed = results.filter((item) => item.status === "error");
|
|
1732
|
+
lines.push(` Service entries: ${paint.success(`${updated} updated \xB7 ${unchanged} already up to date`)}${removed ? ` \xB7 ${paint.success(`${removed} removed`)}` : ""} \xB7 ${(failed.length ? paint.error : paint.dim)(`${failed.length} failed`)}`);
|
|
1733
|
+
if (!results.length) lines.push(paint.dim(" No shared connections need applying."));
|
|
1734
|
+
if (results.length && results.every((item) => item.status === "synced" || item.status === "removed")) lines.push(paint.success(" OK: all service entries are up to date."));
|
|
1735
|
+
const waiting = results.filter((item) => item.status === "pending");
|
|
1736
|
+
for (const kind of KINDS) {
|
|
1737
|
+
const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
|
|
1738
|
+
const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
|
|
1739
|
+
if (!runtime.length && !kept.length) continue;
|
|
1740
|
+
lines.push(`
|
|
1741
|
+
${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` : `Waiting for ${sessionName(kind)}`)}`);
|
|
1742
|
+
if (runtime.length) lines.push(" Install it from Runtime, then run Sync. Your existing entries keep working.");
|
|
1743
|
+
else lines.push(" Resolve this session entry in Sync. Existing connection entries stay until it is ready.");
|
|
1744
|
+
const retained = /* @__PURE__ */ new Map();
|
|
1745
|
+
for (const client of new Set(kept.map((item) => item.client))) {
|
|
1746
|
+
const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
|
|
1747
|
+
const key = JSON.stringify([...names].sort());
|
|
1748
|
+
const group = retained.get(key);
|
|
1749
|
+
if (group) group.clients.push(client);
|
|
1750
|
+
else retained.set(key, { clients: [client], names });
|
|
1751
|
+
}
|
|
1752
|
+
for (const group of retained.values()) {
|
|
1753
|
+
const clients = group.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ");
|
|
1754
|
+
lines.push(paint.dim(` Kept in ${clients} until ${sessionName(kind)} is applied: ${group.names.map(safe).join(", ")}`));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
for (const [heading, rows, color] of [
|
|
1758
|
+
["Needs review (existing settings kept)", results.filter((item) => item.status === "conflict"), paint.warning],
|
|
1759
|
+
["Waiting for Sync", waiting.filter((item) => !item.waitingFor || !["runtime", "session"].includes(item.waitingFor)), paint.warning],
|
|
1760
|
+
["Could not finish", failed, paint.error]
|
|
1761
|
+
]) {
|
|
1762
|
+
if (!rows.length) continue;
|
|
1763
|
+
lines.push(`
|
|
1764
|
+
${color(`${heading}:`)}`);
|
|
1765
|
+
for (const name of new Set(rows.map((item) => item.name))) {
|
|
1766
|
+
lines.push(` ${safe(name)}`);
|
|
1767
|
+
for (const row of rows.filter((item) => item.name === name)) lines.push(` ${row.status === "error" ? paint.error("FAILED ") : ""}${CLIENT_LABELS2[row.client]}: ${safe(row.detail)}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (results.some((item) => item.status === "conflict")) lines.push(paint.warning(`
|
|
1771
|
+
${options.reviewHint ?? "Next: open Sync to compare settings and choose which version to use."}`));
|
|
1772
|
+
if (failed.length) lines.push(paint.error("\nNext: fix the errors above, then run Sync again."));
|
|
1773
|
+
return lines.join("\n");
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/modules/registration/runtime-menu.ts
|
|
1777
|
+
async function runtimeMenu(prompter, registry, requestedKind) {
|
|
1778
|
+
let kind = requestedKind;
|
|
1779
|
+
for (; ; ) {
|
|
1780
|
+
prompter.clearScreen();
|
|
1781
|
+
if (!kind) {
|
|
1782
|
+
const selected = await prompter.select("Runtime / choose MCP", [
|
|
1783
|
+
...KINDS.map((value) => ({ value, label: packageName(value) })),
|
|
1784
|
+
{ value: "back", key: "b", label: "Back" }
|
|
1785
|
+
], { cancelValue: "back" });
|
|
1786
|
+
if (selected === "back") return;
|
|
1787
|
+
kind = selected;
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
const installed = registry.globalRuntime(kind);
|
|
1791
|
+
const sessionMode = registry.isSessionMode(kind);
|
|
1792
|
+
writeWrapped(prompter, `Runtime / ${packageName(kind)}`);
|
|
1793
|
+
writeWrapped(prompter, installed ? `Installed: ${installed.version}${installed.linked ? " (linked development checkout)" : ""}` : "Not installed in the AKMS runtime directory.");
|
|
1794
|
+
writeWrapped(prompter, `Runtime directory: ${runtimePrefix()}`);
|
|
1795
|
+
prompter.write("Connections start Node directly, without a resident npm/npx process.\n");
|
|
1796
|
+
prompter.write(`Configuration: ${sessionMode ? "one session server for this kind" : "one entry per connection"}.
|
|
1797
|
+
`);
|
|
1798
|
+
if (installed) writeWrapped(prompter, installed.args[0]);
|
|
1799
|
+
const picked = await prompter.select("AKMS runtime", [
|
|
1800
|
+
...installed && !sessionMode ? [{ value: "apply", label: "Use for existing connections", hint: "convert npx and old runtime paths together" }] : [],
|
|
1801
|
+
...installed ? [{ value: "catalog", key: "catalog", label: sessionMode ? "Reapply session launch command" : "Switch to session server", hint: `one ${sessionName(kind)} entry per registered service` }] : [],
|
|
1802
|
+
...sessionMode && (installed || registry.read().sessions?.[kind]) ? [{ value: "individual", key: "individual", label: "Switch back to per-connection entries", hint: "restore connection settings from the catalog" }] : [],
|
|
1803
|
+
...!installed?.linked ? [{ value: "install", label: installed ? "Update runtime installation" : "Install AKMS runtime", hint: runtimeInstallCommand(kind) }] : [],
|
|
1804
|
+
...!registry.needsMigration() ? [{ value: "default", key: "default", label: registry.read().defaultMode === "catalog" ? "Use individual entries for new kinds" : "Start new kinds as session servers", hint: "keep existing kinds and rollback choices" }] : [],
|
|
1805
|
+
{ value: "back", key: "b", label: "Back" }
|
|
1806
|
+
], { cancelValue: "back" });
|
|
1807
|
+
if (picked === "back") {
|
|
1808
|
+
if (requestedKind) return;
|
|
1809
|
+
kind = void 0;
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (picked === "default") {
|
|
1813
|
+
const mode = registry.read().defaultMode === "catalog" ? "individual" : "catalog";
|
|
1814
|
+
prompter.write(`New kinds will use ${mode === "catalog" ? "session servers" : "individual entries"}. Existing kinds keep their settings.
|
|
1815
|
+
`);
|
|
1816
|
+
if (await prompter.askYesNo("Change the default for new kinds?", false)) {
|
|
1817
|
+
prompter.write(`${registry.formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.", prompter.paint)}
|
|
1818
|
+
`);
|
|
1819
|
+
await prompter.acknowledge();
|
|
1820
|
+
}
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
if (picked === "catalog" || picked === "individual") {
|
|
1824
|
+
const plan2 = registry.planSession(kind, picked);
|
|
1825
|
+
prompter.clearScreen();
|
|
1826
|
+
writeWrapped(prompter, picked === "catalog" ? `Switch to session server: ${sessionName(kind)}` : "Switch back to per-connection entries");
|
|
1827
|
+
prompter.write("Registry version 2 prevents older AKMS setup packages from changing these settings.\nThe previous catalog revision is kept as registry.previous.json.\n\n");
|
|
1828
|
+
for (const change of plan2.changes) {
|
|
1829
|
+
writeWrapped(prompter, CLIENT_LABELS2[change.client]);
|
|
1830
|
+
writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
|
|
1831
|
+
writeWrapped(prompter, `Remove after replacements are applied: ${change.remove.join(", ") || "none"}`, " ");
|
|
1832
|
+
if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.join(", ")}`, " ");
|
|
1833
|
+
}
|
|
1834
|
+
if (picked === "catalog") {
|
|
1835
|
+
prompter.write("\nPasswords stay in the catalog and its previous-revision copy. Session entries contain no connection credentials.\n");
|
|
1836
|
+
writeWrapped(prompter, `Command: ${plan2.entry.command}`);
|
|
1837
|
+
writeWrapped(prompter, `Runtime: ${plan2.entry.args[0]} --catalog`);
|
|
1838
|
+
if (plan2.custom.length) {
|
|
1839
|
+
prompter.write("Custom commands will not run in catalog mode; they are retained for switching back:\n");
|
|
1840
|
+
for (const name of plan2.custom) writeWrapped(prompter, plainText(name), " ");
|
|
1841
|
+
}
|
|
1842
|
+
} else prompter.write("\nConnection credentials will be copied back into registered client settings. The registry stays version 2.\n");
|
|
1843
|
+
if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
|
|
1844
|
+
const results2 = registry.applySession(plan2);
|
|
1845
|
+
prompter.clearScreen();
|
|
1846
|
+
prompter.write(`${registry.formatResults(results2, "Configuration mode saved.", prompter.paint)}
|
|
1847
|
+
`);
|
|
1848
|
+
await registry.checkSaved(prompter, plan2.names);
|
|
1849
|
+
prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
|
|
1850
|
+
await prompter.acknowledge();
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (picked === "install") {
|
|
1854
|
+
prompter.clearScreen();
|
|
1855
|
+
writeWrapped(prompter, `Installing ${packageName(kind)}@latest in ${runtimePrefix()}...`);
|
|
1856
|
+
const runtime = await registry.installRuntime(kind);
|
|
1857
|
+
prompter.write(`Installed ${runtime.version}. Reconnect your AI clients to load the update.
|
|
1858
|
+
`);
|
|
1859
|
+
await prompter.acknowledge();
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const plan = registry.planRuntime(kind);
|
|
1863
|
+
prompter.clearScreen();
|
|
1864
|
+
writeWrapped(prompter, `Use AKMS runtime ${packageName(kind)} ${plan.runtime.version}`);
|
|
1865
|
+
prompter.write(`
|
|
1866
|
+
Convert ${plan.names.length} connection(s):
|
|
1867
|
+
`);
|
|
1868
|
+
for (const name of plan.names) writeWrapped(prompter, plainText(name), " ");
|
|
1869
|
+
if (plan.skipped.length) {
|
|
1870
|
+
prompter.write("Custom commands kept for individual review:\n");
|
|
1871
|
+
for (const name of plan.skipped) writeWrapped(prompter, plainText(name), " ");
|
|
1872
|
+
}
|
|
1873
|
+
prompter.write("\nConnection values and credentials stay the same.\nVersion-pinned npx entries will use the installed version shown above.\n");
|
|
1874
|
+
if (!plan.names.length) {
|
|
1875
|
+
prompter.write("No saved launch commands need converting.\nIf a legacy client entry is missing here, import it from Register or Import existing connections first.\n");
|
|
1876
|
+
await prompter.acknowledge();
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
|
|
1880
|
+
const results = registry.applyRuntime(plan);
|
|
1881
|
+
prompter.clearScreen();
|
|
1882
|
+
prompter.write(`${registry.formatResults(results, "Runtime settings saved.", prompter.paint)}
|
|
1883
|
+
|
|
1884
|
+
Restart or reconnect AI clients to replace running npx processes.
|
|
1885
|
+
`);
|
|
1886
|
+
await registry.checkSaved(prompter, plan.names);
|
|
1887
|
+
await prompter.acknowledge();
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
if (error instanceof PromptClosedError) throw error;
|
|
1890
|
+
if (error instanceof PromptBackError) continue;
|
|
1891
|
+
prompter.write(`${error instanceof Error ? error.message : "Could not configure the AKMS runtime."}
|
|
1892
|
+
`);
|
|
1893
|
+
await prompter.acknowledge();
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/modules/registration/updates-menu.ts
|
|
1899
|
+
async function updatesMenu(prompter, registry) {
|
|
1900
|
+
const kinds = KINDS.filter((kind) => kind !== "wsl" || process.platform === "win32");
|
|
1901
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1902
|
+
const refresh = async () => {
|
|
1903
|
+
prompter.clearScreen();
|
|
1904
|
+
prompter.write("Updates / checking published runtime versions...\n");
|
|
1905
|
+
await Promise.all(kinds.map(async (kind) => {
|
|
1906
|
+
try {
|
|
1907
|
+
latest.set(kind, await registry.latestRuntime(kind));
|
|
1908
|
+
} catch {
|
|
1909
|
+
latest.set(kind, null);
|
|
1910
|
+
}
|
|
1911
|
+
}));
|
|
1912
|
+
};
|
|
1913
|
+
await refresh();
|
|
1914
|
+
for (; ; ) {
|
|
1915
|
+
try {
|
|
1916
|
+
prompter.clearScreen();
|
|
1917
|
+
prompter.write("Updates / AKMS runtimes\n\n");
|
|
1918
|
+
const rows = kinds.map((kind) => {
|
|
1919
|
+
const installed = registry.globalRuntime(kind), published = latest.get(kind);
|
|
1920
|
+
const update = !!installed && !installed.linked && !!published && runtimeIsOlder(installed.version, published);
|
|
1921
|
+
const status = installed?.linked ? "Development link" : !published ? "Unavailable" : !installed ? "Not installed" : update ? "Update available" : "Up to date";
|
|
1922
|
+
writeWrapped(prompter, `${packageName(kind)} | installed: ${installed?.version ?? "not installed"} | latest: ${published ?? "unavailable"} | ${status}`);
|
|
1923
|
+
return { kind, installed, published, update };
|
|
1924
|
+
});
|
|
1925
|
+
const updates = rows.filter((row) => row.update);
|
|
1926
|
+
const available = rows.filter((row) => row.published && !row.installed?.linked && (!row.installed || row.update));
|
|
1927
|
+
prompter.write("\nDevelopment links are managed in their source checkouts.\n");
|
|
1928
|
+
const picked = await prompter.select("Updates", [
|
|
1929
|
+
...available.map((row) => ({ value: row.kind, key: row.kind, label: `${row.installed ? "Update" : "Install"} ${packageName(row.kind)}` })),
|
|
1930
|
+
...updates.length ? [{ value: "all", key: "a", label: `Update all available (${updates.length})` }] : [],
|
|
1931
|
+
{ value: "refresh", key: "r", label: "Refresh versions" },
|
|
1932
|
+
{ value: "back", key: "b", label: "Back" }
|
|
1933
|
+
], { cancelValue: "back" });
|
|
1934
|
+
if (picked === "back") return;
|
|
1935
|
+
if (picked === "refresh") {
|
|
1936
|
+
await refresh();
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
const selected = picked === "all" ? updates : available.filter((row) => row.kind === picked);
|
|
1940
|
+
if (!selected.length) continue;
|
|
1941
|
+
if (!await prompter.askYesNo(`Install latest published releases for ${selected.map((row) => packageName(row.kind)).join(", ")}?`, true)) continue;
|
|
1942
|
+
prompter.clearScreen();
|
|
1943
|
+
for (const row of selected) {
|
|
1944
|
+
writeWrapped(prompter, `Updating ${packageName(row.kind)}...`);
|
|
1945
|
+
try {
|
|
1946
|
+
const installed = await registry.installRuntime(row.kind);
|
|
1947
|
+
writeWrapped(prompter, `Installed ${packageName(row.kind)} ${installed.version}.`);
|
|
1948
|
+
const names = Object.entries(registry.read().connections).filter(([, item]) => item.kind === row.kind && item.entry).map(([name]) => name);
|
|
1949
|
+
await registry.checkSaved(prompter, names);
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
let detail = error instanceof Error ? error.message : "Installation failed.";
|
|
1952
|
+
for (const item of Object.values(registry.read().connections)) if (item.entry) detail = redactStartupText(detail, item.entry);
|
|
1953
|
+
writeWrapped(prompter, `FAILED ${packageName(row.kind)}: ${detail}`);
|
|
1954
|
+
if (/\b(?:EBUSY|EPERM|EACCES)\b/i.test(detail)) prompter.write("Close your AI clients, then retry the update. A running runtime may have locked its files.\n");
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
prompter.write("Reconnect or restart your AI clients to load updated runtimes. Fix any failed startup checks first.\n");
|
|
1958
|
+
await prompter.acknowledge();
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
if (error instanceof PromptClosedError) throw error;
|
|
1961
|
+
if (error instanceof PromptBackError) continue;
|
|
1962
|
+
throw error;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/modules/registration/migration-menu.ts
|
|
1968
|
+
async function migrationMenu(prompter, registry) {
|
|
1969
|
+
try {
|
|
1970
|
+
prompter.clearScreen();
|
|
1971
|
+
const plan = registry.planMigration();
|
|
1972
|
+
prompter.write("Migrate registry\n\n");
|
|
1973
|
+
if (!plan.steps.length) {
|
|
1974
|
+
prompter.write("The registry is already in the current format.\n");
|
|
1975
|
+
await prompter.acknowledge();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
prompter.write("Steps:\n");
|
|
1979
|
+
plan.steps.forEach((step, index) => writeWrapped(prompter, `${index + 1}. ${step.label}`, " "));
|
|
1980
|
+
prompter.write('\nThe registry will record defaultMode: "catalog" for new kinds.\nA kind explicitly switched back to individual entries keeps that choice.\n');
|
|
1981
|
+
for (const item of plan.kinds) {
|
|
1982
|
+
prompter.write("\n");
|
|
1983
|
+
const target = sessionName(item.kind);
|
|
1984
|
+
const action = item.status === "catalog" ? `already ${target}; keep` : item.status === "individual" ? "keep explicit per-connection choice (rollback)" : item.status === "empty" ? `no connections; future connections use ${target}` : item.status === "blocked" ? "blocked" : `per-connection entries -> ${target}${item.status === "pending-runtime" ? " after runtime setup" : ""}`;
|
|
1985
|
+
writeWrapped(prompter, `${item.kind.toUpperCase()}: ${item.names.length} connection(s), ${action}`);
|
|
1986
|
+
if (item.detail) writeWrapped(prompter, plainText(item.detail), " ");
|
|
1987
|
+
for (const change of item.session?.changes ?? item.changes ?? []) {
|
|
1988
|
+
writeWrapped(prompter, `${CLIENT_LABELS2[change.client]}${item.status === "pending-runtime" ? " (after Runtime + Sync)" : ""}`, " ");
|
|
1989
|
+
writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
|
|
1990
|
+
writeWrapped(prompter, `Remove after verification: ${change.remove.join(", ") || "none"}`, " ");
|
|
1991
|
+
if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.map(plainText).join(", ")}`, " ");
|
|
1992
|
+
}
|
|
1993
|
+
if (item.session?.custom.length) writeWrapped(prompter, `Custom commands retained for rollback: ${item.session.custom.map(plainText).join(", ")}`, " ");
|
|
1994
|
+
}
|
|
1995
|
+
prompter.write("\nThe previous catalog revision is kept in registry.previous.json.\nSession entries contain no connection credentials; those stay in the catalog and its previous revision.\n");
|
|
1996
|
+
if (plan.kinds.some((item) => item.status === "blocked")) {
|
|
1997
|
+
prompter.write("Resolve blocked kinds before migrating. No migration was applied.\n");
|
|
1998
|
+
await prompter.acknowledge();
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
|
|
2002
|
+
const results = registry.applyMigration(plan);
|
|
2003
|
+
prompter.clearScreen();
|
|
2004
|
+
prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
|
|
2005
|
+
`);
|
|
2006
|
+
await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
|
|
2007
|
+
prompter.write("Reconnect affected clients after changes to their server layout. Fix startup failures first.\n");
|
|
2008
|
+
await prompter.acknowledge();
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
if (error instanceof PromptClosedError) throw error;
|
|
2011
|
+
if (error instanceof PromptBackError) return;
|
|
2012
|
+
prompter.write(`${error instanceof Error ? error.message : "Could not migrate the registry."}
|
|
2013
|
+
`);
|
|
2014
|
+
await prompter.acknowledge();
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/modules/registration/menu.ts
|
|
2019
|
+
function pendingImports(registry, kind) {
|
|
2020
|
+
const state = registry.read();
|
|
2021
|
+
const entries = registry.candidates(kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name));
|
|
2022
|
+
const names = [...new Set(entries.map((candidate) => candidate.name))];
|
|
2023
|
+
return { names, differing: names.filter((name) => new Set(entries.filter((item) => item.name === name).map((item) => entryHash(item.entry))).size > 1).length };
|
|
2024
|
+
}
|
|
2025
|
+
function preview(prompter, entry) {
|
|
2026
|
+
if (!entry) {
|
|
2027
|
+
prompter.write(" Remove this connection from the service.\n");
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
for (const [key, value] of Object.entries(entry.env)) prompter.write(` ${plainText(key)}: ${visibleField.test(key) ? plainText(value) : "(hidden)"}
|
|
2031
|
+
`);
|
|
2032
|
+
}
|
|
2033
|
+
async function registrationMenu(prompter, registry, initialClient) {
|
|
2034
|
+
const initial = registry.read();
|
|
2035
|
+
const selected = new Set(initial.clients);
|
|
2036
|
+
if (initialClient) selected.add(initialClient);
|
|
2037
|
+
const discovered = registry.candidates(void 0, registry.targets.filter((target) => target.id !== "registered").map((target) => target.id));
|
|
2038
|
+
let cursor = initialClient ?? registry.targets[0]?.id;
|
|
2039
|
+
let privacyNoticeShown = false;
|
|
2040
|
+
for (; ; ) {
|
|
2041
|
+
try {
|
|
2042
|
+
prompter.clearScreen();
|
|
2043
|
+
prompter.write("\nRegister \xB7 AI services\nShared by Database, SSH, WSL and S3.\nEnter toggles a service; Apply saves the selection.\nMatching existing connections are imported on Apply; differing settings stay unchanged.\n\n");
|
|
2044
|
+
for (const error of discovered.errors) prompter.write(` ${error}
|
|
2045
|
+
`);
|
|
2046
|
+
const picked = await prompter.select("Register", [
|
|
2047
|
+
...registry.targets.filter((target) => target.id !== "registered").map((target) => ({
|
|
2048
|
+
value: target.id,
|
|
2049
|
+
label: `[${selected.has(target.id) ? "x" : " "}] ${target.label}`,
|
|
2050
|
+
hint: `${initial.clients.includes(target.id) ? "Registered" : "Not registered"} \xB7 ${discovered.entries.filter((entry) => entry.client === target.id).length} existing AKMS \xB7 ${target.detected ? "Found" : "Not detected"} \xB7 ${target.location}`
|
|
2051
|
+
})),
|
|
2052
|
+
{ separator: true },
|
|
2053
|
+
{ value: "apply", key: "a", label: "Apply registration" },
|
|
2054
|
+
{ value: "back", key: "q", label: "Back" }
|
|
2055
|
+
], { defaultValue: cursor, cancelValue: "back" });
|
|
2056
|
+
if (picked === "back") return false;
|
|
2057
|
+
if (picked !== "apply") {
|
|
2058
|
+
const client = picked;
|
|
2059
|
+
if (selected.has(client)) selected.delete(client);
|
|
2060
|
+
else selected.add(client);
|
|
2061
|
+
cursor = client;
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
const removed = initial.clients.filter((id) => !selected.has(id));
|
|
2065
|
+
const count = Object.values(registry.read().connections).filter((item) => item.entry).length;
|
|
2066
|
+
const plan = registry.planRegistration([...selected]);
|
|
2067
|
+
const importNames = [...new Set(plan.imports.map((entry) => entry.name))];
|
|
2068
|
+
prompter.clearScreen();
|
|
2069
|
+
prompter.write("Review registration\n");
|
|
2070
|
+
prompter.write(`
|
|
2071
|
+
Selected: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}
|
|
2072
|
+
Apply ${count + importNames.length} shared connection(s) to these services.
|
|
2073
|
+
`);
|
|
2074
|
+
if (importNames.length) prompter.write(`Import ${importNames.length} existing connection(s); identical copies become one shared item:
|
|
2075
|
+
${importNames.join("\n ")}
|
|
2076
|
+
`);
|
|
2077
|
+
if (plan.conflicts.length) prompter.write(`Keep ${plan.conflicts.length} differing connection(s) unchanged until you choose a version:
|
|
2078
|
+
${plan.conflicts.join("\n ")}
|
|
2079
|
+
`);
|
|
2080
|
+
for (const error of plan.errors) prompter.write(` ${error}
|
|
2081
|
+
`);
|
|
2082
|
+
if (initial.defaultMode === "catalog") {
|
|
2083
|
+
prompter.write("Services use one session entry per kind. Imported individual entries are removed after that session entry is verified.\n");
|
|
2084
|
+
if (plan.imports.some((item) => !registry.globalRuntime(item.kind))) prompter.write("Install missing runtimes from Runtime, then run Sync. Existing entries stay until then.\n");
|
|
2085
|
+
if (plan.imports.some((item) => !standardLaunch(item.entry, item.kind))) prompter.write("Custom launch commands are retained for rollback; session mode uses each connection's saved environment.\n");
|
|
2086
|
+
}
|
|
2087
|
+
if (removed.length) prompter.write(`Unregister: ${removed.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}
|
|
2088
|
+
Remove only AKMS-managed entries from these services; keep the shared catalog.
|
|
2089
|
+
`);
|
|
2090
|
+
if (!privacyNoticeShown) {
|
|
2091
|
+
writeWrapped(prompter, `Connection settings, including passwords, are stored in ${plainText(registry.file)}.`);
|
|
2092
|
+
prompter.write("Keep this file private: do not share, sync or commit it.\n");
|
|
2093
|
+
prompter.write("Its previous revision, registry.previous.json, also contains saved credentials.\n");
|
|
2094
|
+
privacyNoticeShown = true;
|
|
2095
|
+
}
|
|
2096
|
+
if (!await prompter.askYesNo("Apply this registration?", removed.length === 0)) continue;
|
|
2097
|
+
const results = registry.register([...selected], initial.clients, plan.imports);
|
|
2098
|
+
prompter.clearScreen();
|
|
2099
|
+
prompter.write(`
|
|
2100
|
+
${registry.formatResults(results, `Registration saved.
|
|
2101
|
+
AI services: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`, prompter.paint)}
|
|
2102
|
+
`);
|
|
2103
|
+
if (importNames.length) prompter.write(`Imported ${importNames.length} existing connection(s) into the shared list.
|
|
2104
|
+
`);
|
|
2105
|
+
try {
|
|
2106
|
+
const conflicts = new Set(results.filter((result) => result.status === "conflict").map((result) => result.name));
|
|
2107
|
+
if (conflicts.size && await prompter.askYesNo(`Compare settings for ${conflicts.size} connection(s) now?`, true)) await syncMenu(prompter, registry);
|
|
2108
|
+
const current = registry.read();
|
|
2109
|
+
const remaining = registry.candidates().entries.filter((candidate) => !Object.hasOwn(current.connections, candidate.name));
|
|
2110
|
+
const names = [...new Set(remaining.map((candidate) => candidate.name))];
|
|
2111
|
+
if (names.length) {
|
|
2112
|
+
prompter.write(`
|
|
2113
|
+
${names.length} existing connection(s) still need a choice before joining the shared list:
|
|
2114
|
+
`);
|
|
2115
|
+
for (const name of names) {
|
|
2116
|
+
const copies = remaining.filter((candidate) => candidate.name === name);
|
|
2117
|
+
prompter.write(` ${plainText(name)} \xB7 ${copies.map((copy) => CLIENT_LABELS2[copy.client]).join(" \xB7 ")}
|
|
2118
|
+
`);
|
|
2119
|
+
if (new Set(copies.map((copy) => entryHash(copy.entry))).size > 1) prompter.write(` ${describeDifferences(copies.map((copy) => copy.entry))}
|
|
2120
|
+
`);
|
|
2121
|
+
}
|
|
2122
|
+
prompter.write("Their existing settings are unchanged. Compare them now, or use Import existing connections later.\n");
|
|
2123
|
+
if (await prompter.askYesNo("Compare existing settings and choose which to share?", true)) await importMenu(prompter, registry);
|
|
2124
|
+
}
|
|
2125
|
+
if (!conflicts.size && !names.length) await prompter.acknowledge();
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
if (!(error instanceof PromptBackError)) throw error;
|
|
2128
|
+
}
|
|
2129
|
+
return true;
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
if (error instanceof PromptClosedError) throw error;
|
|
2132
|
+
if (error instanceof PromptBackError) continue;
|
|
2133
|
+
prompter.write(` ${error instanceof Error ? error.message : "Registration could not be saved."}
|
|
2134
|
+
`);
|
|
2135
|
+
await prompter.acknowledge();
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
async function importMenu(prompter, registry, kind) {
|
|
2140
|
+
for (; ; ) {
|
|
2141
|
+
prompter.clearScreen();
|
|
2142
|
+
prompter.write("Import existing connections\n\n");
|
|
2143
|
+
const { entries, errors } = registry.candidates(kind);
|
|
2144
|
+
for (const error of errors) prompter.write(` ${error}
|
|
2145
|
+
`);
|
|
2146
|
+
const state = registry.read();
|
|
2147
|
+
const matching = registry.planRegistration(state.clients, kind);
|
|
2148
|
+
const matchingNames = [...new Set(matching.imports.map((entry) => entry.name))];
|
|
2149
|
+
const names = [...new Set(entries.filter((candidate) => {
|
|
2150
|
+
const current = Object.hasOwn(state.connections, candidate.name) ? state.connections[candidate.name] : void 0;
|
|
2151
|
+
return !current?.entry || current.applied[candidate.client] !== entryHash(candidate.entry) || entryHash(current.entry) !== entryHash(candidate.entry);
|
|
2152
|
+
}).map((entry) => entry.name))];
|
|
2153
|
+
if (!names.length) {
|
|
2154
|
+
prompter.write(" No existing connections need importing.\n");
|
|
2155
|
+
await prompter.acknowledge();
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
const picked = await prompter.select("Import existing connections", [
|
|
2159
|
+
...names.map((name) => ({ value: `entry:${name}`, key: name, label: name, hint: [...new Set(entries.filter((entry) => entry.name === name).map((entry) => CLIENT_LABELS2[entry.client]))].join(" \xB7 ") })),
|
|
2160
|
+
...matchingNames.length ? [{ value: "matching", key: "all", label: "Import all matching connections", hint: `${matchingNames.length} connections; differing versions stay unchanged` }] : [],
|
|
2161
|
+
{ value: "back", key: "b", label: "Back" }
|
|
2162
|
+
], { cancelValue: "back" });
|
|
2163
|
+
if (picked === "back") return;
|
|
2164
|
+
try {
|
|
2165
|
+
prompter.clearScreen();
|
|
2166
|
+
if (picked === "matching") {
|
|
2167
|
+
prompter.write(`
|
|
2168
|
+
Import existing connections:
|
|
2169
|
+
${matchingNames.join("\n ")}
|
|
2170
|
+
`);
|
|
2171
|
+
if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
|
|
2172
|
+
const results2 = registry.register(state.clients, state.clients, matching.imports);
|
|
2173
|
+
prompter.clearScreen();
|
|
2174
|
+
prompter.write(`${registry.formatResults(results2, "Import complete", prompter.paint)}
|
|
2175
|
+
`);
|
|
2176
|
+
await prompter.acknowledge();
|
|
2177
|
+
}
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
const name = picked.slice(6);
|
|
2181
|
+
const candidates = entries.filter((entry) => entry.name === name);
|
|
2182
|
+
let chosen = candidates[0];
|
|
2183
|
+
if (new Set(candidates.map((candidate) => entryHash(candidate.entry))).size > 1) {
|
|
2184
|
+
prompter.write(`
|
|
2185
|
+
${plainText(name)} \xB7 Choose the settings to use on all registered services.
|
|
2186
|
+
`);
|
|
2187
|
+
compareEntries(prompter, candidates.map((candidate) => ({ label: CLIENT_LABELS2[candidate.client], entry: candidate.entry })));
|
|
2188
|
+
const choice = await prompter.select("Use settings from", [
|
|
2189
|
+
...candidates.map((candidate, index) => ({ value: String(index), label: `Use ${index + 1}: ${CLIENT_LABELS2[candidate.client]}` })),
|
|
2190
|
+
{ value: "back", label: "Back" }
|
|
2191
|
+
], { cancelValue: "back" });
|
|
2192
|
+
if (choice === "back") continue;
|
|
2193
|
+
chosen = candidates[Number(choice)];
|
|
2194
|
+
}
|
|
2195
|
+
preview(prompter, chosen.entry);
|
|
2196
|
+
const destination = await prompter.askRequired("Shared connection name (change it to keep both)", name);
|
|
2197
|
+
prompter.write("The selected version will be used by every registered service.\n");
|
|
2198
|
+
if (!await prompter.askYesNo(`Import ${destination} and apply it?`, true)) continue;
|
|
2199
|
+
const results = registry.importEntry(destination, chosen, candidates);
|
|
2200
|
+
prompter.clearScreen();
|
|
2201
|
+
prompter.write(`
|
|
2202
|
+
${registry.formatResults(results, "Import complete", prompter.paint)}
|
|
2203
|
+
`);
|
|
2204
|
+
if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
|
|
2205
|
+
await prompter.acknowledge();
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
if (error instanceof PromptClosedError) throw error;
|
|
2208
|
+
if (error instanceof PromptBackError) continue;
|
|
2209
|
+
prompter.write(` ${error instanceof Error ? error.message : "Import failed."}
|
|
2210
|
+
`);
|
|
2211
|
+
await prompter.acknowledge();
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
async function syncMenu(prompter, registry) {
|
|
2216
|
+
let pendingResults;
|
|
2217
|
+
for (; ; ) {
|
|
2218
|
+
prompter.clearScreen();
|
|
2219
|
+
const results = pendingResults ?? registry.sync();
|
|
2220
|
+
pendingResults = void 0;
|
|
2221
|
+
prompter.write(`
|
|
2222
|
+
${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
|
|
2223
|
+
`);
|
|
2224
|
+
const imports = pendingImports(registry);
|
|
2225
|
+
if (imports.names.length) prompter.write(prompter.paint.warning(`
|
|
2226
|
+
Not yet imported: ${imports.names.length} connection(s)${imports.differing === imports.names.length ? " with differing settings" : ""} - open Import existing connections.
|
|
2227
|
+
`));
|
|
2228
|
+
const conflicts = results.filter((result) => result.status === "conflict");
|
|
2229
|
+
if (!conflicts.length && !imports.names.length) {
|
|
2230
|
+
await prompter.acknowledge();
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const picked = await prompter.select("Review differences", [
|
|
2234
|
+
...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS2[item.client]} \xB7 ${item.name}` })),
|
|
2235
|
+
...imports.names.length ? [{ value: "import", key: "import", label: "Import existing connections", hint: `${imports.names.length} connection(s) to review` }] : [],
|
|
2236
|
+
{ value: "back", key: "b", label: "Later \u2014 keep existing settings" }
|
|
2237
|
+
], { cancelValue: "back" });
|
|
2238
|
+
if (picked === "back") return;
|
|
2239
|
+
try {
|
|
2240
|
+
if (picked === "import") {
|
|
2241
|
+
await importMenu(prompter, registry);
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
prompter.clearScreen();
|
|
2245
|
+
const conflict = conflicts[Number(picked)];
|
|
2246
|
+
const state = registry.read();
|
|
2247
|
+
const sessionKind = Object.keys(state.sessions ?? {}).find((kind) => sessionName(kind) === conflict.name);
|
|
2248
|
+
if (sessionKind) {
|
|
2249
|
+
const session = state.sessions[sessionKind];
|
|
2250
|
+
const actual2 = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
|
|
2251
|
+
if (!actual2 || entryKind(actual2) !== sessionKind) {
|
|
2252
|
+
prompter.write("This session name belongs to an unsupported or different server. Rename the external entry manually; it was kept.\n");
|
|
2253
|
+
await prompter.acknowledge();
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
const removing2 = !registry.isSessionMode(sessionKind, state) || !state.clients.includes(conflict.client) || !Object.values(state.connections).some((item) => item.kind === sessionKind && item.entry);
|
|
2257
|
+
writeWrapped(prompter, `Session server / ${conflict.name} / ${CLIENT_LABELS2[conflict.client]}`);
|
|
2258
|
+
compareEntries(prompter, [{ label: "AKMS session launch", entry: session.entry }, { label: "External settings", entry: actual2 }]);
|
|
2259
|
+
const choice2 = await prompter.select("Resolve session launch", [
|
|
2260
|
+
{ value: "apply", label: removing2 ? "Remove the old session entry" : "Restore the AKMS session launch" },
|
|
2261
|
+
{ value: "back", key: "b", label: "Keep external settings for now" }
|
|
2262
|
+
], { cancelValue: "back" });
|
|
2263
|
+
if (choice2 === "apply" && await prompter.askYesNo(removing2 ? "Remove this session entry?" : "Replace these external launch settings?", false)) pendingResults = registry.resolve(conflict.client, conflict.name, actual2, false);
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
const connection = state.connections[conflict.name];
|
|
2267
|
+
if (!connection) throw new Error("This connection changed. Reopen Sync to review its current settings.");
|
|
2268
|
+
const actual = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
|
|
2269
|
+
if (!actual || entryKind(actual) !== connection.kind) {
|
|
2270
|
+
prompter.write(" This name belongs to a different or unsupported server. Rename that entry manually, or rename the shared connection.\n");
|
|
2271
|
+
await prompter.acknowledge();
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
const client = CLIENT_LABELS2[conflict.client];
|
|
2275
|
+
const registered = state.clients.includes(conflict.client);
|
|
2276
|
+
const sessionMode = registry.isSessionMode(connection.kind, state);
|
|
2277
|
+
const removing = !connection.entry || !registered;
|
|
2278
|
+
const retiring = sessionMode && !removing;
|
|
2279
|
+
const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
|
|
2280
|
+
writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
|
|
2281
|
+
writeWrapped(prompter, `Registered services: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`);
|
|
2282
|
+
prompter.write("\n");
|
|
2283
|
+
if (!removing && connection.entry) {
|
|
2284
|
+
if (retiring) {
|
|
2285
|
+
writeWrapped(prompter, `Choose settings for ${sessionName(connection.kind)} before removing the old ${client} entry.`);
|
|
2286
|
+
writeWrapped(prompter, "The old entry stays until the session server is ready.");
|
|
2287
|
+
}
|
|
2288
|
+
compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
|
|
2289
|
+
} else {
|
|
2290
|
+
writeWrapped(prompter, !registered ? `${client} is no longer registered.` : sessionMode ? `This kind now uses ${sessionName(connection.kind)}. The individual client entry is no longer needed.` : "This connection was removed from AKMS.");
|
|
2291
|
+
writeWrapped(prompter, `Its settings in ${client} changed before removal could finish.`);
|
|
2292
|
+
prompter.write(`
|
|
2293
|
+
Currently in ${client}
|
|
2294
|
+
`);
|
|
2295
|
+
preview(prompter, actual);
|
|
2296
|
+
prompter.write("\n");
|
|
2297
|
+
}
|
|
2298
|
+
const choices = [
|
|
2299
|
+
{ value: "shared", label: removing ? `Remove from ${client}` : retiring ? "Use 1: keep AKMS settings" : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : retiring ? `use version 1 in ${sessionName(connection.kind)}` : `replace its ${subject} with version 1` },
|
|
2300
|
+
...registered ? [{ value: "client", label: removing ? `Restore from ${client}` : `Use 2: ${client} -> all ${state.clients.length} services`, hint: retiring ? `save version 2 in AKMS for ${sessionName(connection.kind)}` : "save in AKMS and sync registered services" }] : [],
|
|
2301
|
+
{ value: "back", key: "b", label: "Back without changes" }
|
|
2302
|
+
];
|
|
2303
|
+
const choice = await prompter.select(removing ? registered ? "Remove or restore this connection?" : `Remove this connection from ${client}?` : `Which ${subject} should be used?`, choices, { cancelValue: "back" });
|
|
2304
|
+
if (choice === "back") continue;
|
|
2305
|
+
prompter.write("\n");
|
|
2306
|
+
if (choice === "client") {
|
|
2307
|
+
writeWrapped(prompter, `Save the ${client} version in AKMS.`);
|
|
2308
|
+
writeWrapped(prompter, `Sync to: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}`);
|
|
2309
|
+
} else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : retiring ? `Keep the saved AKMS ${subject}.` : `Copy the saved AKMS ${subject} to ${client}.`);
|
|
2310
|
+
if (retiring) writeWrapped(prompter, `Keep the old ${client} entry until ${sessionName(connection.kind)} is ready, then remove it.`);
|
|
2311
|
+
const confirmation = choice === "client" ? `Save in AKMS and sync ${state.clients.length} services?` : removing ? `Remove from ${client}?` : retiring ? "Keep version 1 and finish Sync?" : `Update ${client} with version 1?`;
|
|
2312
|
+
if (await prompter.askYesNo(confirmation, false)) {
|
|
2313
|
+
pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
|
|
2314
|
+
}
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
if (error instanceof PromptClosedError) throw error;
|
|
2317
|
+
if (error instanceof PromptBackError) continue;
|
|
2318
|
+
prompter.write(` ${error instanceof Error ? error.message : "Could not resolve the difference."}
|
|
2319
|
+
`);
|
|
2320
|
+
await prompter.acknowledge();
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/modules/registration/targets/index.ts
|
|
2326
|
+
import os5 from "os";
|
|
2327
|
+
import path10 from "path";
|
|
2328
|
+
|
|
2329
|
+
// src/modules/registration/targets/claude-code.ts
|
|
2330
|
+
import { spawnSync } from "child_process";
|
|
2331
|
+
import fs8 from "fs";
|
|
2332
|
+
import os3 from "os";
|
|
2333
|
+
import path8 from "path";
|
|
2334
|
+
|
|
2335
|
+
// src/modules/registration/paths.ts
|
|
2336
|
+
import fs5 from "fs";
|
|
2337
|
+
import path6 from "path";
|
|
2338
|
+
function findExecutable(name) {
|
|
2339
|
+
const searchPath = process.env.PATH ?? "";
|
|
2340
|
+
const directories = searchPath.split(path6.delimiter).filter((directory) => directory !== "");
|
|
2341
|
+
let suffixes = [""];
|
|
2342
|
+
if (process.platform === "win32") {
|
|
2343
|
+
const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
2344
|
+
suffixes = pathExt.split(";").filter((suffix) => suffix !== "");
|
|
2345
|
+
}
|
|
2346
|
+
for (const directory of directories) {
|
|
2347
|
+
for (const suffix of suffixes) {
|
|
2348
|
+
const candidate = path6.join(directory, `${name}${suffix}`);
|
|
2349
|
+
if (fs5.existsSync(candidate) == true && fs5.statSync(candidate).isFile() == true) {
|
|
2350
|
+
return candidate;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/modules/registration/targets/json-file.ts
|
|
2358
|
+
import fs7 from "fs";
|
|
2359
|
+
|
|
2360
|
+
// src/modules/registration/targets/config-file.ts
|
|
2361
|
+
import fs6 from "fs";
|
|
2362
|
+
import path7 from "path";
|
|
2363
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2364
|
+
function readExisting2(filePath) {
|
|
2365
|
+
try {
|
|
2366
|
+
return fs6.readFileSync(filePath, "utf8");
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
if (error.code === "ENOENT") {
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
throw error;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
function updateConfigFile2(filePath, update) {
|
|
2375
|
+
let destination = filePath;
|
|
2376
|
+
let symbolicLink = false;
|
|
2377
|
+
try {
|
|
2378
|
+
symbolicLink = fs6.lstatSync(filePath).isSymbolicLink();
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
if (error.code !== "ENOENT") {
|
|
2381
|
+
throw error;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (symbolicLink) {
|
|
2385
|
+
destination = fs6.realpathSync(filePath);
|
|
2386
|
+
}
|
|
2387
|
+
const before = readExisting2(destination);
|
|
2388
|
+
const updated = update(before ?? "");
|
|
2389
|
+
if (before != null) {
|
|
2390
|
+
fs6.accessSync(destination, fs6.constants.W_OK);
|
|
2391
|
+
}
|
|
2392
|
+
fs6.mkdirSync(path7.dirname(destination), { recursive: true });
|
|
2393
|
+
const temporary = path7.join(path7.dirname(destination), `.${path7.basename(destination)}.${randomUUID2()}.tmp`);
|
|
2394
|
+
const mode = before == null ? 384 : fs6.statSync(destination).mode & 511;
|
|
2395
|
+
let descriptor;
|
|
2396
|
+
let ownsTemporary = false;
|
|
2397
|
+
try {
|
|
2398
|
+
descriptor = fs6.openSync(temporary, "wx", mode);
|
|
2399
|
+
ownsTemporary = true;
|
|
2400
|
+
fs6.writeFileSync(descriptor, updated, "utf8");
|
|
2401
|
+
fs6.fsyncSync(descriptor);
|
|
2402
|
+
fs6.closeSync(descriptor);
|
|
2403
|
+
descriptor = void 0;
|
|
2404
|
+
if (readExisting2(destination) !== before) {
|
|
2405
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
2406
|
+
}
|
|
2407
|
+
fs6.renameSync(temporary, destination);
|
|
2408
|
+
ownsTemporary = false;
|
|
2409
|
+
} finally {
|
|
2410
|
+
try {
|
|
2411
|
+
if (descriptor != null) {
|
|
2412
|
+
fs6.closeSync(descriptor);
|
|
2413
|
+
}
|
|
2414
|
+
} finally {
|
|
2415
|
+
if (ownsTemporary) {
|
|
2416
|
+
fs6.unlinkSync(temporary);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/modules/registration/targets/json-file.ts
|
|
2423
|
+
function toMcpServerEntry(value) {
|
|
2424
|
+
if (typeof value !== "object" || value == null) {
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
const record2 = value;
|
|
2428
|
+
if (typeof record2.command !== "string") {
|
|
2429
|
+
return null;
|
|
2430
|
+
}
|
|
2431
|
+
let args = [];
|
|
2432
|
+
if (Array.isArray(record2.args) == true) {
|
|
2433
|
+
args = record2.args.filter((item) => typeof item === "string");
|
|
2434
|
+
}
|
|
2435
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
2436
|
+
if (typeof record2.env === "object" && record2.env != null) {
|
|
2437
|
+
for (const [key, item] of Object.entries(record2.env)) {
|
|
2438
|
+
if (typeof item === "string") {
|
|
2439
|
+
env[key] = item;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return { command: record2.command, args, env };
|
|
2444
|
+
}
|
|
2445
|
+
function readRawMcpServersJson(document) {
|
|
2446
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) {
|
|
2447
|
+
throw new Error("the config file is not a JSON object");
|
|
2448
|
+
}
|
|
2449
|
+
const servers = document.mcpServers;
|
|
2450
|
+
if (servers === void 0) {
|
|
2451
|
+
return {};
|
|
2452
|
+
}
|
|
2453
|
+
if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
|
|
2454
|
+
throw new Error("mcpServers is not a JSON object");
|
|
2455
|
+
}
|
|
2456
|
+
return servers;
|
|
2457
|
+
}
|
|
2458
|
+
function readMcpServersJson(document) {
|
|
2459
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
2460
|
+
for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
|
|
2461
|
+
const entry = toMcpServerEntry(value);
|
|
2462
|
+
if (entry != null) {
|
|
2463
|
+
entries[name] = entry;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return entries;
|
|
2467
|
+
}
|
|
2468
|
+
function readDocument(filePath, contents) {
|
|
2469
|
+
if (contents == null && fs7.existsSync(filePath) == false) {
|
|
2470
|
+
return {};
|
|
2471
|
+
}
|
|
2472
|
+
const text = contents ?? fs7.readFileSync(filePath, "utf8");
|
|
2473
|
+
if (text.trim() === "") {
|
|
2474
|
+
return {};
|
|
2475
|
+
}
|
|
2476
|
+
let document;
|
|
2477
|
+
try {
|
|
2478
|
+
document = JSON.parse(text);
|
|
2479
|
+
} catch (ex) {
|
|
2480
|
+
let detail = String(ex);
|
|
2481
|
+
if (ex instanceof Error) {
|
|
2482
|
+
detail = ex.message;
|
|
2483
|
+
}
|
|
2484
|
+
throw new Error(`${filePath} is not valid JSON: ${detail}`);
|
|
2485
|
+
}
|
|
2486
|
+
if (typeof document !== "object" || document == null || Array.isArray(document) == true) {
|
|
2487
|
+
throw new Error(`${filePath} is not a JSON object`);
|
|
2488
|
+
}
|
|
2489
|
+
return document;
|
|
2490
|
+
}
|
|
2491
|
+
function createJsonFileTarget(options) {
|
|
2492
|
+
const { id, label, filePath } = options;
|
|
2493
|
+
return {
|
|
2494
|
+
id,
|
|
2495
|
+
label,
|
|
2496
|
+
location: filePath,
|
|
2497
|
+
detected: fs7.existsSync(filePath),
|
|
2498
|
+
readEntries() {
|
|
2499
|
+
const document = readDocument(filePath);
|
|
2500
|
+
return readMcpServersJson(document);
|
|
2501
|
+
},
|
|
2502
|
+
hasEntry(name) {
|
|
2503
|
+
return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
|
|
2504
|
+
},
|
|
2505
|
+
writeEntry(name, entry) {
|
|
2506
|
+
updateConfigFile2(filePath, (contents) => {
|
|
2507
|
+
const document = readDocument(filePath, contents);
|
|
2508
|
+
const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
|
|
2509
|
+
servers[name] = entry;
|
|
2510
|
+
document.mcpServers = servers;
|
|
2511
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
2512
|
+
});
|
|
2513
|
+
return { status: "written", detail: filePath };
|
|
2514
|
+
},
|
|
2515
|
+
removeEntry(name) {
|
|
2516
|
+
updateConfigFile2(filePath, (contents) => {
|
|
2517
|
+
const document = readDocument(filePath, contents);
|
|
2518
|
+
const servers = readRawMcpServersJson(document);
|
|
2519
|
+
delete servers[name];
|
|
2520
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
2521
|
+
});
|
|
2522
|
+
return { status: "written", detail: filePath };
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/modules/registration/targets/claude-code.ts
|
|
2528
|
+
var CLAUDE_CODE_CONFIG_PATH = path8.join(os3.homedir(), ".claude.json");
|
|
2529
|
+
var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
|
|
2530
|
+
function toClaudeAddJsonPayload(entry) {
|
|
2531
|
+
return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
|
|
2532
|
+
}
|
|
2533
|
+
function escapeCmdArgument(argument) {
|
|
2534
|
+
let escaped = argument.replace(/(\\*)"/g, '$1$1\\"');
|
|
2535
|
+
escaped = escaped.replace(/(\\*)$/, "$1$1");
|
|
2536
|
+
escaped = `"${escaped}"`;
|
|
2537
|
+
escaped = escaped.replace(CMD_META_CHARACTER_PATTERN, "^$1");
|
|
2538
|
+
escaped = escaped.replace(CMD_META_CHARACTER_PATTERN, "^$1");
|
|
2539
|
+
return escaped;
|
|
2540
|
+
}
|
|
2541
|
+
function runCliCommand(executable, args) {
|
|
2542
|
+
const isBatchShim = /\.(cmd|bat)$/i.test(executable) == true;
|
|
2543
|
+
if (process.platform !== "win32" || isBatchShim == false) {
|
|
2544
|
+
const result2 = spawnSync(executable, args, { encoding: "utf8", windowsHide: true });
|
|
2545
|
+
return result2;
|
|
2546
|
+
}
|
|
2547
|
+
const commandLine = [executable.replace(CMD_META_CHARACTER_PATTERN, "^$1"), ...args.map(escapeCmdArgument)].join(" ");
|
|
2548
|
+
const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${commandLine}"`], {
|
|
2549
|
+
encoding: "utf8",
|
|
2550
|
+
windowsHide: true,
|
|
2551
|
+
windowsVerbatimArguments: true
|
|
2552
|
+
});
|
|
2553
|
+
return result;
|
|
2554
|
+
}
|
|
2555
|
+
function runClaude(executable, args) {
|
|
2556
|
+
const result = runCliCommand(executable, args);
|
|
2557
|
+
if (result.error != null) {
|
|
2558
|
+
return { status: "unavailable", detail: `could not run claude: ${result.error.message}` };
|
|
2559
|
+
}
|
|
2560
|
+
if (result.status !== 0) {
|
|
2561
|
+
let output = (result.stderr ?? "").trim();
|
|
2562
|
+
if (output === "") {
|
|
2563
|
+
output = (result.stdout ?? "").trim();
|
|
2564
|
+
}
|
|
2565
|
+
return { status: "unavailable", detail: `claude exited with ${result.status}: ${output}` };
|
|
2566
|
+
}
|
|
2567
|
+
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
2568
|
+
}
|
|
2569
|
+
function readRawClaudeEntries() {
|
|
2570
|
+
if (fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
2571
|
+
return {};
|
|
2572
|
+
}
|
|
2573
|
+
const text = fs8.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
2574
|
+
let document;
|
|
2575
|
+
try {
|
|
2576
|
+
document = JSON.parse(text);
|
|
2577
|
+
} catch (ex) {
|
|
2578
|
+
let detail = String(ex);
|
|
2579
|
+
if (ex instanceof Error) {
|
|
2580
|
+
detail = ex.message;
|
|
2581
|
+
}
|
|
2582
|
+
throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
|
|
2583
|
+
}
|
|
2584
|
+
return readRawMcpServersJson(document);
|
|
2585
|
+
}
|
|
2586
|
+
function createClaudeCodeTarget() {
|
|
2587
|
+
const executable = findExecutable("claude");
|
|
2588
|
+
return {
|
|
2589
|
+
id: "claude-code",
|
|
2590
|
+
label: "Claude Code (user scope)",
|
|
2591
|
+
location: CLAUDE_CODE_CONFIG_PATH,
|
|
2592
|
+
detected: executable != null || fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
2593
|
+
readEntries() {
|
|
2594
|
+
return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
|
|
2595
|
+
},
|
|
2596
|
+
hasEntry(name) {
|
|
2597
|
+
return Object.hasOwn(readRawClaudeEntries(), name);
|
|
2598
|
+
},
|
|
2599
|
+
writeEntry(name, entry) {
|
|
2600
|
+
if (executable == null) {
|
|
2601
|
+
return {
|
|
2602
|
+
status: "unavailable",
|
|
2603
|
+
detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
const previousEntries = readRawClaudeEntries();
|
|
2607
|
+
const hadPrevious = Object.hasOwn(previousEntries, name);
|
|
2608
|
+
const previous = previousEntries[name];
|
|
2609
|
+
if (hadPrevious) {
|
|
2610
|
+
const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
2611
|
+
if (removed.status === "unavailable") {
|
|
2612
|
+
return removed;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
|
|
2616
|
+
if (added.status === "unavailable" && hadPrevious) {
|
|
2617
|
+
const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
|
|
2618
|
+
const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
|
|
2619
|
+
return { status: "unavailable", detail: `${added.detail}
|
|
2620
|
+
${recovery}` };
|
|
2621
|
+
}
|
|
2622
|
+
return added;
|
|
2623
|
+
},
|
|
2624
|
+
removeEntry(name) {
|
|
2625
|
+
if (executable == null) {
|
|
2626
|
+
return {
|
|
2627
|
+
status: "unavailable",
|
|
2628
|
+
detail: `the claude CLI is not on PATH \u2014 run this yourself:
|
|
2629
|
+
claude mcp remove ${name} -s user`
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
return runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/modules/registration/targets/codex.ts
|
|
2638
|
+
import fs9 from "fs";
|
|
2639
|
+
import os4 from "os";
|
|
2640
|
+
import path9 from "path";
|
|
2641
|
+
|
|
2642
|
+
// src/modules/registration/targets/toml-lines.ts
|
|
2643
|
+
function lineState() {
|
|
2644
|
+
return { quote: null, multiline: false, depth: 0 };
|
|
2645
|
+
}
|
|
2646
|
+
function scanTomlLine(line, state) {
|
|
2647
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
2648
|
+
const character = line[index];
|
|
2649
|
+
if (state.quote != null) {
|
|
2650
|
+
if (character === "\\" && state.quote === '"') {
|
|
2651
|
+
index += 1;
|
|
2652
|
+
} else if (character === state.quote) {
|
|
2653
|
+
if (!state.multiline) {
|
|
2654
|
+
state.quote = null;
|
|
2655
|
+
} else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
2656
|
+
while (line[index + 1] === character) {
|
|
2657
|
+
index += 1;
|
|
2658
|
+
}
|
|
2659
|
+
state.quote = null;
|
|
2660
|
+
state.multiline = false;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
if (character === "#") {
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
if (character === '"' || character === "'") {
|
|
2669
|
+
state.quote = character;
|
|
2670
|
+
state.multiline = line.slice(index, index + 3) === character.repeat(3);
|
|
2671
|
+
if (state.multiline) {
|
|
2672
|
+
index += 2;
|
|
2673
|
+
}
|
|
2674
|
+
} else if (character === "[" || character === "{") {
|
|
2675
|
+
state.depth += 1;
|
|
2676
|
+
} else if (character === "]" || character === "}") {
|
|
2677
|
+
state.depth -= 1;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/modules/registration/targets/toml-validation.ts
|
|
2683
|
+
import { isDeepStrictEqual } from "util";
|
|
2684
|
+
import { parse, TomlError } from "smol-toml";
|
|
2685
|
+
function isTable(value) {
|
|
2686
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
2687
|
+
}
|
|
2688
|
+
function parseConfigToml(text) {
|
|
2689
|
+
let document;
|
|
2690
|
+
try {
|
|
2691
|
+
document = parse(text, { integersAsBigInt: true });
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
const location = error instanceof TomlError ? ` at line ${error.line}, column ${error.column}` : "";
|
|
2694
|
+
throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
|
|
2695
|
+
}
|
|
2696
|
+
if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
|
|
2697
|
+
throw new Error("mcp_servers must be a TOML table.");
|
|
2698
|
+
}
|
|
2699
|
+
return document;
|
|
2700
|
+
}
|
|
2701
|
+
function readTomlEntries(text) {
|
|
2702
|
+
return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
|
|
2703
|
+
}
|
|
2704
|
+
function hasTomlEntry(text, name) {
|
|
2705
|
+
return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
|
|
2706
|
+
}
|
|
2707
|
+
function normalize(document) {
|
|
2708
|
+
const servers = document.mcp_servers;
|
|
2709
|
+
if (servers == null) {
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
for (const server of Object.values(servers)) {
|
|
2713
|
+
if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
|
|
2714
|
+
delete server.env;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (Object.keys(servers).length === 0) {
|
|
2718
|
+
delete document.mcp_servers;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function verifyTomlEdit(before, after, name, entry) {
|
|
2722
|
+
const expected = parseConfigToml(before);
|
|
2723
|
+
const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
|
|
2724
|
+
if (entry == null) {
|
|
2725
|
+
delete servers[name];
|
|
2726
|
+
} else {
|
|
2727
|
+
const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
|
|
2728
|
+
Object.defineProperty(servers, name, {
|
|
2729
|
+
value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
|
|
2730
|
+
configurable: true,
|
|
2731
|
+
enumerable: true,
|
|
2732
|
+
writable: true
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
expected.mcp_servers = servers;
|
|
2736
|
+
let actual;
|
|
2737
|
+
try {
|
|
2738
|
+
actual = parseConfigToml(after);
|
|
2739
|
+
} catch {
|
|
2740
|
+
throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
|
|
2741
|
+
}
|
|
2742
|
+
normalize(expected);
|
|
2743
|
+
normalize(actual);
|
|
2744
|
+
if (!isDeepStrictEqual(expected, actual)) {
|
|
2745
|
+
throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
|
|
2746
|
+
}
|
|
2747
|
+
return after;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// src/modules/registration/targets/toml-subset.ts
|
|
2751
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2752
|
+
var ANY_HEADER_PATTERN = /^\s*\[/;
|
|
2753
|
+
var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
|
|
2754
|
+
var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
|
|
2755
|
+
var OWNED_TABLE_KEYS = ["command", "args", "env"];
|
|
2756
|
+
function unquoteKey(rawKey) {
|
|
2757
|
+
if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
|
|
2758
|
+
return parseConfigToml(`value = ${rawKey}`).value;
|
|
2759
|
+
}
|
|
2760
|
+
return rawKey;
|
|
2761
|
+
}
|
|
2762
|
+
function readStatements(body) {
|
|
2763
|
+
const statements = [];
|
|
2764
|
+
let index = 0;
|
|
2765
|
+
while (index < body.length) {
|
|
2766
|
+
const match = KEY_VALUE_PATTERN.exec(body[index]);
|
|
2767
|
+
if (match == null) {
|
|
2768
|
+
index += 1;
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
const start = index;
|
|
2772
|
+
const state = lineState();
|
|
2773
|
+
scanTomlLine(match[2], state);
|
|
2774
|
+
index += 1;
|
|
2775
|
+
while ((state.quote != null || state.depth > 0) && index < body.length) {
|
|
2776
|
+
scanTomlLine(body[index], state);
|
|
2777
|
+
index += 1;
|
|
2778
|
+
}
|
|
2779
|
+
statements.push({ key: unquoteKey(match[1]), start, end: index });
|
|
2780
|
+
}
|
|
2781
|
+
return statements;
|
|
2782
|
+
}
|
|
2783
|
+
function splitSections(lines) {
|
|
2784
|
+
const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
|
|
2785
|
+
const state = lineState();
|
|
2786
|
+
for (const line of lines) {
|
|
2787
|
+
const current = sections[sections.length - 1];
|
|
2788
|
+
if (state.quote != null || state.depth > 0) {
|
|
2789
|
+
current.body.push(line);
|
|
2790
|
+
scanTomlLine(line, state);
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
if (ANY_HEADER_PATTERN.test(line) == true) {
|
|
2794
|
+
const serverMatch = SERVER_HEADER_PATTERN.exec(line);
|
|
2795
|
+
let serverName = null;
|
|
2796
|
+
let subPath = "";
|
|
2797
|
+
if (serverMatch != null) {
|
|
2798
|
+
serverName = unquoteKey(serverMatch[1]);
|
|
2799
|
+
subPath = serverMatch[2].replace(/^\./, "").replace(/\s+/g, "");
|
|
2800
|
+
}
|
|
2801
|
+
sections.push({ header: line, serverName, subPath, body: [] });
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
current.body.push(line);
|
|
2805
|
+
scanTomlLine(line, state);
|
|
2806
|
+
}
|
|
2807
|
+
return sections;
|
|
2808
|
+
}
|
|
2809
|
+
function hasMcpServerToml(text, name) {
|
|
2810
|
+
return hasTomlEntry(text, name);
|
|
2811
|
+
}
|
|
2812
|
+
function parseMcpServersToml(text) {
|
|
2813
|
+
return readTomlEntries(text);
|
|
2814
|
+
}
|
|
2815
|
+
function quoteTomlString(value) {
|
|
2816
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
2817
|
+
return `"${escaped}"`;
|
|
2818
|
+
}
|
|
2819
|
+
function formatTomlKey(key) {
|
|
2820
|
+
if (BARE_KEY_PATTERN.test(key) == true) {
|
|
2821
|
+
return key;
|
|
2822
|
+
}
|
|
2823
|
+
return quoteTomlString(key);
|
|
2824
|
+
}
|
|
2825
|
+
function renderOwnedLines(entry) {
|
|
2826
|
+
return [
|
|
2827
|
+
`command = ${quoteTomlString(entry.command)}`,
|
|
2828
|
+
`args = [${entry.args.map((argument) => quoteTomlString(argument)).join(", ")}]`
|
|
2829
|
+
];
|
|
2830
|
+
}
|
|
2831
|
+
function renderEnvLines(entry) {
|
|
2832
|
+
return Object.entries(entry.env).map(([key, value]) => `${formatTomlKey(key)} = ${quoteTomlString(value)}`);
|
|
2833
|
+
}
|
|
2834
|
+
function renderMcpServerTable(name, entry) {
|
|
2835
|
+
const tableKey = formatTomlKey(name);
|
|
2836
|
+
const lines = [`[mcp_servers.${tableKey}]`, ...renderOwnedLines(entry)];
|
|
2837
|
+
const envLines = renderEnvLines(entry);
|
|
2838
|
+
if (envLines.length > 0) {
|
|
2839
|
+
lines.push("");
|
|
2840
|
+
lines.push(`[mcp_servers.${tableKey}.env]`);
|
|
2841
|
+
lines.push(...envLines);
|
|
2842
|
+
}
|
|
2843
|
+
return `${lines.join("\n")}
|
|
2844
|
+
`;
|
|
2845
|
+
}
|
|
2846
|
+
function isBlank(line) {
|
|
2847
|
+
return line.trim() === "";
|
|
2848
|
+
}
|
|
2849
|
+
function joinSections(sections, eol) {
|
|
2850
|
+
const lines = [];
|
|
2851
|
+
for (const section of sections) {
|
|
2852
|
+
if (section.header != null) {
|
|
2853
|
+
lines.push(section.header);
|
|
2854
|
+
}
|
|
2855
|
+
lines.push(...section.body);
|
|
2856
|
+
}
|
|
2857
|
+
return lines.join(eol);
|
|
2858
|
+
}
|
|
2859
|
+
function writeMcpServerToml(text, name, entry) {
|
|
2860
|
+
parseConfigToml(text);
|
|
2861
|
+
const updated = editMcpServerToml(text, name, entry);
|
|
2862
|
+
return verifyTomlEdit(text, updated, name, entry);
|
|
2863
|
+
}
|
|
2864
|
+
function editMcpServerToml(text, name, entry) {
|
|
2865
|
+
let eol = "\n";
|
|
2866
|
+
if (text.includes("\r\n") == true) {
|
|
2867
|
+
eol = "\r\n";
|
|
2868
|
+
}
|
|
2869
|
+
const sections = splitSections(text.split(/\r?\n/));
|
|
2870
|
+
const isOwn = (section) => section.serverName === name;
|
|
2871
|
+
if (entry == null) {
|
|
2872
|
+
const kept = sections.filter((section) => isOwn(section) == false);
|
|
2873
|
+
const removedText = joinSections(kept, eol);
|
|
2874
|
+
return removedText;
|
|
2875
|
+
}
|
|
2876
|
+
const tableKey = formatTomlKey(name);
|
|
2877
|
+
const envLines = renderEnvLines(entry);
|
|
2878
|
+
const tableIndex = sections.findIndex((section) => isOwn(section) == true && section.subPath === "");
|
|
2879
|
+
if (tableIndex < 0) {
|
|
2880
|
+
const withoutStrayEnv = sections.filter((section) => (isOwn(section) == true && section.subPath === "env") == false);
|
|
2881
|
+
let prefix = joinSections(withoutStrayEnv, eol);
|
|
2882
|
+
if (prefix.trim() === "") {
|
|
2883
|
+
prefix = "";
|
|
2884
|
+
} else {
|
|
2885
|
+
prefix = prefix.replace(/(\r?\n)*$/, "");
|
|
2886
|
+
prefix = `${prefix}${eol}${eol}`;
|
|
2887
|
+
}
|
|
2888
|
+
const block = renderMcpServerTable(name, entry).replace(/\n/g, eol);
|
|
2889
|
+
return `${prefix}${block}`;
|
|
2890
|
+
}
|
|
2891
|
+
const table = sections[tableIndex];
|
|
2892
|
+
const ownedLineIndexes = /* @__PURE__ */ new Set();
|
|
2893
|
+
for (const statement of readStatements(table.body)) {
|
|
2894
|
+
if (OWNED_TABLE_KEYS.includes(statement.key) == true) {
|
|
2895
|
+
for (let index = statement.start; index < statement.end; index += 1) {
|
|
2896
|
+
ownedLineIndexes.add(index);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
const userLines = table.body.filter((_line, index) => ownedLineIndexes.has(index) == false);
|
|
2901
|
+
let trailingBlankCount = 0;
|
|
2902
|
+
while (trailingBlankCount < userLines.length && isBlank(userLines[userLines.length - 1 - trailingBlankCount]) == true) {
|
|
2903
|
+
trailingBlankCount += 1;
|
|
2904
|
+
}
|
|
2905
|
+
const trailingBlanks = userLines.slice(userLines.length - trailingBlankCount);
|
|
2906
|
+
const userContent = userLines.slice(0, userLines.length - trailingBlankCount);
|
|
2907
|
+
const rebuilt = [];
|
|
2908
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
2909
|
+
const section = sections[index];
|
|
2910
|
+
if (isOwn(section) == true && section.subPath === "env") {
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
if (index !== tableIndex) {
|
|
2914
|
+
rebuilt.push(section);
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
if (envLines.length === 0) {
|
|
2918
|
+
rebuilt.push({ ...section, body: [...renderOwnedLines(entry), ...userContent, ...trailingBlanks] });
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
rebuilt.push({ ...section, body: [...renderOwnedLines(entry), ...userContent, ""] });
|
|
2922
|
+
rebuilt.push({
|
|
2923
|
+
header: `[mcp_servers.${tableKey}.env]`,
|
|
2924
|
+
serverName: name,
|
|
2925
|
+
subPath: "env",
|
|
2926
|
+
body: [...envLines, ...trailingBlanks]
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
const rebuiltText = joinSections(rebuilt, eol);
|
|
2930
|
+
return rebuiltText;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/modules/registration/targets/codex.ts
|
|
2934
|
+
function createCodexTarget() {
|
|
2935
|
+
const CODEX_CONFIG_PATH = path9.join(process.env.CODEX_HOME ? path9.resolve(process.env.CODEX_HOME) : path9.join(os4.homedir(), ".codex"), "config.toml");
|
|
2936
|
+
function readText() {
|
|
2937
|
+
if (fs9.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
2938
|
+
return "";
|
|
2939
|
+
}
|
|
2940
|
+
return fs9.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
2941
|
+
}
|
|
2942
|
+
return {
|
|
2943
|
+
id: "codex",
|
|
2944
|
+
label: "Codex CLI",
|
|
2945
|
+
location: CODEX_CONFIG_PATH,
|
|
2946
|
+
detected: findExecutable("codex") != null || fs9.existsSync(CODEX_CONFIG_PATH) == true,
|
|
2947
|
+
readEntries() {
|
|
2948
|
+
const text = readText();
|
|
2949
|
+
return parseMcpServersToml(text);
|
|
2950
|
+
},
|
|
2951
|
+
hasEntry(name) {
|
|
2952
|
+
return hasMcpServerToml(readText(), name);
|
|
2953
|
+
},
|
|
2954
|
+
writeEntry(name, entry, options) {
|
|
2955
|
+
updateConfigFile2(CODEX_CONFIG_PATH, (original) => {
|
|
2956
|
+
let text = original;
|
|
2957
|
+
if (options?.replace) {
|
|
2958
|
+
text = writeMcpServerToml(text, name, null);
|
|
2959
|
+
}
|
|
2960
|
+
return writeMcpServerToml(text, name, entry);
|
|
2961
|
+
});
|
|
2962
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
2963
|
+
},
|
|
2964
|
+
removeEntry(name) {
|
|
2965
|
+
updateConfigFile2(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
|
|
2966
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/modules/registration/targets/index.ts
|
|
2972
|
+
function claudeDesktopConfigPath() {
|
|
2973
|
+
if (process.platform === "win32") {
|
|
2974
|
+
const appData = process.env.APPDATA ?? path10.join(os5.homedir(), "AppData", "Roaming");
|
|
2975
|
+
return path10.join(appData, "Claude", "claude_desktop_config.json");
|
|
2976
|
+
}
|
|
2977
|
+
if (process.platform === "darwin") {
|
|
2978
|
+
return path10.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2979
|
+
}
|
|
2980
|
+
return path10.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
2981
|
+
}
|
|
2982
|
+
function listConfigTargets() {
|
|
2983
|
+
const targets = [
|
|
2984
|
+
createClaudeCodeTarget(),
|
|
2985
|
+
createCodexTarget(),
|
|
2986
|
+
createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
|
|
2987
|
+
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path10.join(os5.homedir(), ".cursor", "mcp.json") })
|
|
2988
|
+
];
|
|
2989
|
+
const detected = targets.filter((target) => target.detected == true);
|
|
2990
|
+
const others = targets.filter((target) => target.detected == false);
|
|
2991
|
+
return [...detected, ...others];
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// src/modules/launcher/menu.ts
|
|
2995
|
+
async function runRegistrationCommand(options) {
|
|
2996
|
+
const prompter = new Prompter(options.input, options.output);
|
|
2997
|
+
const registry = new Registry(options.targets ?? listConfigTargets(), options.registryPath, options.runtimeOptions);
|
|
2998
|
+
try {
|
|
2999
|
+
if (options.mode === "register") await registrationMenu(prompter, registry, options.targetId);
|
|
3000
|
+
else await syncMenu(prompter, registry);
|
|
3001
|
+
return 0;
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
|
|
3004
|
+
throw error;
|
|
3005
|
+
} finally {
|
|
3006
|
+
prompter.close();
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
async function runMenu(options) {
|
|
3010
|
+
const registry = new Registry(options.targets ?? listConfigTargets(), options.registryPath, options.runtimeOptions);
|
|
3011
|
+
let defaultValue = "db";
|
|
3012
|
+
let failure;
|
|
3013
|
+
for (; ; ) {
|
|
3014
|
+
const prompter = new Prompter(options.input, options.output);
|
|
3015
|
+
let selected = "quit";
|
|
3016
|
+
try {
|
|
3017
|
+
if (failure) {
|
|
3018
|
+
prompter.write(`
|
|
3019
|
+
${failure}
|
|
3020
|
+
|
|
3021
|
+
`);
|
|
3022
|
+
await prompter.acknowledge();
|
|
3023
|
+
failure = void 0;
|
|
3024
|
+
}
|
|
3025
|
+
for (; ; ) {
|
|
3026
|
+
prompter.clearScreen();
|
|
3027
|
+
const state = registry.read();
|
|
3028
|
+
const clients = state.clients;
|
|
3029
|
+
const migration = registry.needsMigration(state);
|
|
3030
|
+
if (migration) prompter.write(prompter.paint.warning("Registry update available. Choose Migrate registry to review the changes.\n"));
|
|
3031
|
+
const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
|
|
3032
|
+
{ value: "db", label: "Database", hint: "PostgreSQL connections" },
|
|
3033
|
+
{ value: "ssh", label: "SSH", hint: "Remote hosts" },
|
|
3034
|
+
{ value: "wsl", label: "WSL", hint: "Local Linux distros \xB7 Windows" },
|
|
3035
|
+
{ value: "s3", label: "S3", hint: "AWS S3 and S3-compatible buckets" },
|
|
3036
|
+
{ separator: true },
|
|
3037
|
+
{ value: "register", label: "Register", hint: clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "Choose AI services once" },
|
|
3038
|
+
{ value: "sync", label: "Sync", hint: "Review and retry shared settings" },
|
|
3039
|
+
{ value: "runtime", label: "Runtime", hint: "AKMS installation / replace npx launches" },
|
|
3040
|
+
{ value: "updates", label: "Updates", hint: "Check installed MCP releases" },
|
|
3041
|
+
...migration ? [{ value: "migrate", key: "migrate", label: "Migrate registry", hint: "Review older registry format and session layout" }] : [],
|
|
3042
|
+
{ value: "quit", label: "Quit" }
|
|
3043
|
+
], { defaultValue, cancelValue: "quit" });
|
|
3044
|
+
if (picked !== "register" && picked !== "sync" && picked !== "runtime" && picked !== "updates" && picked !== "migrate") {
|
|
3045
|
+
selected = picked;
|
|
3046
|
+
break;
|
|
3047
|
+
}
|
|
3048
|
+
try {
|
|
3049
|
+
if (picked === "register") await registrationMenu(prompter, registry, options.targetId);
|
|
3050
|
+
else if (picked === "runtime") await runtimeMenu(prompter, registry);
|
|
3051
|
+
else if (picked === "updates") await updatesMenu(prompter, registry);
|
|
3052
|
+
else if (picked === "migrate") await migrationMenu(prompter, registry);
|
|
3053
|
+
else await syncMenu(prompter, registry);
|
|
3054
|
+
} catch (error) {
|
|
3055
|
+
if (error instanceof PromptClosedError) throw error;
|
|
3056
|
+
if (!(error instanceof PromptBackError)) {
|
|
3057
|
+
options.output.write(`
|
|
3058
|
+
${error instanceof Error ? error.message : "Could not update settings."}
|
|
3059
|
+
`);
|
|
3060
|
+
await prompter.acknowledge();
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
if (error instanceof PromptClosedError) return 0;
|
|
3066
|
+
throw error;
|
|
3067
|
+
} finally {
|
|
3068
|
+
prompter.close();
|
|
3069
|
+
}
|
|
3070
|
+
if (selected === "quit") return 0;
|
|
3071
|
+
defaultValue = selected;
|
|
3072
|
+
try {
|
|
3073
|
+
const code = await options.launch({ mcpId: selected, targetId: options.targetId, pendingInput: prompter.takePendingInput() });
|
|
3074
|
+
if (code === 130) return 0;
|
|
3075
|
+
if (code !== 0) failure = `Setup exited with code ${code}.`;
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
failure = String(error);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// src/modules/launcher/npm.ts
|
|
3083
|
+
import { execFile as execFile3, spawn as spawn2 } from "child_process";
|
|
3084
|
+
import { existsSync } from "fs";
|
|
3085
|
+
import { mkdtemp, realpath, rm } from "fs/promises";
|
|
3086
|
+
import { tmpdir } from "os";
|
|
3087
|
+
import { basename, delimiter, dirname, join } from "path";
|
|
3088
|
+
import { promisify as promisify2 } from "util";
|
|
3089
|
+
var execFileAsync = promisify2(execFile3);
|
|
3090
|
+
async function findNpmCli() {
|
|
3091
|
+
const candidates = [];
|
|
3092
|
+
const npmExecPath = process.env.npm_execpath;
|
|
3093
|
+
if (npmExecPath != null && ["npm-cli.js", "npx-cli.js"].includes(basename(npmExecPath)) == true) {
|
|
3094
|
+
candidates.push(join(dirname(npmExecPath), "npm-cli.js"));
|
|
3095
|
+
}
|
|
3096
|
+
candidates.push(join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"));
|
|
3097
|
+
candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
|
|
3098
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
3099
|
+
if (directory === "") {
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (process.platform === "win32") {
|
|
3103
|
+
candidates.push(join(directory, "node_modules", "npm", "bin", "npm-cli.js"));
|
|
3104
|
+
} else {
|
|
3105
|
+
candidates.push(join(directory, "npm"));
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
for (const candidate of candidates) {
|
|
3109
|
+
if (existsSync(candidate) == true) {
|
|
3110
|
+
const resolved = await realpath(candidate);
|
|
3111
|
+
if (resolved.endsWith(".js")) return resolved;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
throw new Error("npm could not be found. Install Node.js with npm, then reopen your terminal.");
|
|
3115
|
+
}
|
|
3116
|
+
async function resolveLatestVersion(options) {
|
|
3117
|
+
const metadataCache = await mkdtemp(join(tmpdir(), "akms-mcp-latest-"));
|
|
3118
|
+
try {
|
|
3119
|
+
const result = await execFileAsync(process.execPath, [
|
|
3120
|
+
options.npmCli,
|
|
3121
|
+
"view",
|
|
3122
|
+
`${options.packageName}@latest`,
|
|
3123
|
+
"version",
|
|
3124
|
+
"--json",
|
|
3125
|
+
"--prefer-online",
|
|
3126
|
+
"--prefer-offline=false",
|
|
3127
|
+
"--offline=false",
|
|
3128
|
+
"--fetch-retries=0",
|
|
3129
|
+
"--fetch-timeout=15000",
|
|
3130
|
+
"--update-notifier=false",
|
|
3131
|
+
"--cache",
|
|
3132
|
+
metadataCache
|
|
3133
|
+
], { signal: options.signal, timeout: 2e4, windowsHide: true });
|
|
3134
|
+
const version = JSON.parse(result.stdout);
|
|
3135
|
+
if (typeof version !== "string" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) == false) {
|
|
3136
|
+
throw new Error("npm returned an invalid latest version.");
|
|
3137
|
+
}
|
|
3138
|
+
return version;
|
|
3139
|
+
} finally {
|
|
3140
|
+
await rm(metadataCache, { recursive: true, force: true });
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
async function launchMcp(options, dependencies = {}) {
|
|
3144
|
+
const selected = MCP_PACKAGES[options.mcpId];
|
|
3145
|
+
if (options.mcpId === "wsl" && process.platform !== "win32") {
|
|
3146
|
+
throw new Error("WSL setup must run from Windows.");
|
|
3147
|
+
}
|
|
3148
|
+
const findRuntime = dependencies.findRuntime ?? findGlobalRuntime;
|
|
3149
|
+
const output = dependencies.output ?? process.stdout;
|
|
3150
|
+
let runtime = findRuntime(options.mcpId);
|
|
3151
|
+
let pendingInput = options.pendingInput;
|
|
3152
|
+
if (!runtime) {
|
|
3153
|
+
const prompter = new Prompter(dependencies.input ?? process.stdin, output, pendingInput);
|
|
3154
|
+
try {
|
|
3155
|
+
prompter.write(`
|
|
3156
|
+
${selected.packageName} is not installed in the AKMS runtime directory.
|
|
3157
|
+
`);
|
|
3158
|
+
if (!await prompter.askYesNo("Install the AKMS runtime before opening setup?", true)) return 0;
|
|
3159
|
+
try {
|
|
3160
|
+
prompter.write(`Installing ${selected.packageName}...
|
|
3161
|
+
`);
|
|
3162
|
+
await (dependencies.installRuntime ?? installGlobalRuntime)(options.mcpId);
|
|
3163
|
+
runtime = findRuntime(options.mcpId);
|
|
3164
|
+
if (!runtime) throw new Error("Installation completed but the AKMS runtime could not be found.");
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
if (error instanceof Error && error.message === "AKMS runtime installation cancelled.") return 130;
|
|
3167
|
+
prompter.write(`${error instanceof Error ? error.message : "AKMS runtime installation failed."}
|
|
3168
|
+
`);
|
|
3169
|
+
if (!await prompter.askYesNo("Open the latest setup with npm exec instead?", false)) return 1;
|
|
3170
|
+
}
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
|
|
3173
|
+
throw error;
|
|
3174
|
+
} finally {
|
|
3175
|
+
prompter.close();
|
|
3176
|
+
pendingInput = prompter.takePendingInput();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
if (runtime) {
|
|
3180
|
+
const argumentsList = [...runtime.args, selected.setupCommand];
|
|
3181
|
+
if (options.targetId) argumentsList.push("--target", options.targetId);
|
|
3182
|
+
output.write(`Opening ${selected.packageName} ${runtime.version}${runtime.linked ? " (linked development checkout)" : ""}
|
|
3183
|
+
|
|
3184
|
+
`);
|
|
3185
|
+
return (dependencies.runChild ?? runChild)(runtime.command, argumentsList, pendingInput);
|
|
3186
|
+
}
|
|
3187
|
+
return launchLatestSetup(options, dependencies, pendingInput);
|
|
3188
|
+
}
|
|
3189
|
+
async function launchLatestSetup(options, dependencies, pendingInput) {
|
|
3190
|
+
const selected = MCP_PACKAGES[options.mcpId];
|
|
3191
|
+
const output = dependencies.output ?? process.stdout;
|
|
3192
|
+
const npmCli2 = await (dependencies.findNpmCli ?? findNpmCli)();
|
|
3193
|
+
const cancellation = new AbortController();
|
|
3194
|
+
const cancelLookup = () => {
|
|
3195
|
+
cancellation.abort();
|
|
3196
|
+
};
|
|
3197
|
+
process.on("SIGINT", cancelLookup);
|
|
3198
|
+
process.on("SIGTERM", cancelLookup);
|
|
3199
|
+
let version;
|
|
3200
|
+
try {
|
|
3201
|
+
output.write(`
|
|
3202
|
+
Checking the latest ${selected.packageName} release...
|
|
3203
|
+
`);
|
|
3204
|
+
version = await (dependencies.resolveLatestVersion ?? resolveLatestVersion)({ npmCli: npmCli2, packageName: selected.packageName, signal: cancellation.signal });
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
if (cancellation.signal.aborted == true) {
|
|
3207
|
+
return 130;
|
|
3208
|
+
}
|
|
3209
|
+
throw new Error(`Could not check the latest ${selected.packageName} release; setup was not started.
|
|
3210
|
+
${String(error)}`, { cause: error });
|
|
3211
|
+
} finally {
|
|
3212
|
+
process.off("SIGINT", cancelLookup);
|
|
3213
|
+
process.off("SIGTERM", cancelLookup);
|
|
3214
|
+
}
|
|
3215
|
+
const argumentsList = [
|
|
3216
|
+
npmCli2,
|
|
3217
|
+
"exec",
|
|
3218
|
+
"--yes",
|
|
3219
|
+
"--prefer-online",
|
|
3220
|
+
"--prefer-offline=false",
|
|
3221
|
+
"--offline=false",
|
|
3222
|
+
`--package=${selected.packageName}@${version}`,
|
|
3223
|
+
"--",
|
|
3224
|
+
selected.command,
|
|
3225
|
+
selected.setupCommand
|
|
3226
|
+
];
|
|
3227
|
+
if (options.targetId != null) {
|
|
664
3228
|
argumentsList.push("--target", options.targetId);
|
|
665
3229
|
}
|
|
666
|
-
|
|
3230
|
+
output.write(`Opening ${selected.packageName} ${version} with npm exec
|
|
667
3231
|
|
|
668
3232
|
`);
|
|
3233
|
+
return (dependencies.runChild ?? runChild)(process.execPath, argumentsList, pendingInput);
|
|
3234
|
+
}
|
|
3235
|
+
async function runChild(command, argumentsList, pendingInput) {
|
|
669
3236
|
return new Promise((resolve, reject) => {
|
|
670
3237
|
const interactiveInput = process.stdin.isTTY === true;
|
|
671
3238
|
let inputMode = "pipe";
|
|
672
3239
|
if (interactiveInput == true) {
|
|
673
3240
|
inputMode = "inherit";
|
|
674
3241
|
}
|
|
675
|
-
const child =
|
|
3242
|
+
const child = spawn2(command, argumentsList, {
|
|
676
3243
|
stdio: [inputMode, "inherit", "inherit"],
|
|
677
3244
|
windowsHide: true
|
|
678
3245
|
});
|
|
@@ -690,8 +3257,8 @@ ${String(error)}`, { cause: error });
|
|
|
690
3257
|
reject(error);
|
|
691
3258
|
}
|
|
692
3259
|
});
|
|
693
|
-
if (
|
|
694
|
-
child.stdin.write(
|
|
3260
|
+
if (pendingInput != null) {
|
|
3261
|
+
child.stdin.write(pendingInput);
|
|
695
3262
|
}
|
|
696
3263
|
process.stdin.pipe(child.stdin);
|
|
697
3264
|
}
|
|
@@ -726,14 +3293,17 @@ var HELP_TEXT = [
|
|
|
726
3293
|
" akms-mcp Open the setup menu",
|
|
727
3294
|
" akms-mcp [setup|config] Open the same menu",
|
|
728
3295
|
" akms-mcp [setup|config] <mcp> Open db, ssh, wsl or s3 setup directly",
|
|
3296
|
+
" akms-mcp register Register AI services for all AKMS connections",
|
|
3297
|
+
" akms-mcp sync Review differences and retry pending changes",
|
|
729
3298
|
"",
|
|
730
3299
|
"Options:",
|
|
731
3300
|
" --target <client> claude-code | codex | claude-desktop | cursor",
|
|
732
3301
|
" -h, --help Print this help",
|
|
733
3302
|
" -v, --version Print the launcher version",
|
|
734
3303
|
"",
|
|
735
|
-
"
|
|
736
|
-
"Node.js 22+ and npm are required. WSL setup runs on Windows."
|
|
3304
|
+
"Opens the installed runtime's setup. Missing runtimes can be installed with confirmation.",
|
|
3305
|
+
"Node.js 22+ and npm are required. WSL setup runs on Windows.",
|
|
3306
|
+
"Catalog: ~/.akms/mcp/registry.json (or $AKMS_MCP_HOME/registry.json) stores settings and passwords in plaintext."
|
|
737
3307
|
].join("\n");
|
|
738
3308
|
async function main() {
|
|
739
3309
|
const argumentsList = parseArguments(process.argv.slice(2));
|
|
@@ -745,7 +3315,7 @@ async function main() {
|
|
|
745
3315
|
if (argumentsList.showVersion == true) {
|
|
746
3316
|
let version = "development";
|
|
747
3317
|
if (true) {
|
|
748
|
-
version = "0.2
|
|
3318
|
+
version = "0.3.2";
|
|
749
3319
|
}
|
|
750
3320
|
process.stdout.write(`${version}
|
|
751
3321
|
`);
|
|
@@ -755,6 +3325,10 @@ async function main() {
|
|
|
755
3325
|
process.exitCode = await launchMcp({ mcpId: argumentsList.mcpId, targetId: argumentsList.targetId });
|
|
756
3326
|
return;
|
|
757
3327
|
}
|
|
3328
|
+
if (argumentsList.managementMode) {
|
|
3329
|
+
process.exitCode = await runRegistrationCommand({ input: process.stdin, output: process.stdout, mode: argumentsList.managementMode, targetId: argumentsList.targetId });
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
758
3332
|
process.exitCode = await runMenu({
|
|
759
3333
|
input: process.stdin,
|
|
760
3334
|
output: process.stdout,
|