@akms/mcp 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import fs, { existsSync, readFileSync } from 'fs';
3
+ import path2, { dirname, join } from 'path';
4
+ import { createHash, randomUUID } from 'crypto';
5
+ import os5 from 'os';
6
+ import { spawn, execFile, spawnSync } from 'child_process';
7
+ import { promisify, stripVTControlCharacters, isDeepStrictEqual } from 'util';
8
+ import { parse, TomlError } from 'smol-toml';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ // src/_defs/version.ts
12
+ var PACKAGE_VERSION = "0.4.0" ;
2
13
 
3
14
  // src/_defs/index.ts
4
15
  var MCP_PACKAGES = {
5
- db: { label: "Database", packageName: "@akms/mcp-db", command: "akms-mcp-db", setupCommand: "config" },
6
- ssh: { label: "SSH", packageName: "@akms/mcp-ssh", command: "akms-mcp-ssh", setupCommand: "config" },
7
- wsl: { label: "WSL", packageName: "@akms/mcp-wsl", command: "akms-mcp-wsl", setupCommand: "config" },
8
- s3: { label: "S3", packageName: "@akms/mcp-s3", command: "akms-mcp-s3", setupCommand: "config" }
16
+ db: { label: "Database", command: "akms-mcp-db", setupCommand: "config" },
17
+ ssh: { label: "SSH", command: "akms-mcp-ssh", setupCommand: "config" },
18
+ wsl: { label: "WSL", command: "akms-mcp-wsl", setupCommand: "config" },
19
+ s3: { label: "S3", command: "akms-mcp-s3", setupCommand: "config" }
9
20
  };
10
21
  var CLIENT_LABELS = {
11
22
  "claude-code": "Claude Code",
@@ -57,16 +68,6 @@ function parseArguments(argv) {
57
68
  }
58
69
  return parsed;
59
70
  }
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
71
  function readExisting(filePath) {
71
72
  try {
72
73
  return fs.readFileSync(filePath, "utf8");
@@ -96,8 +97,8 @@ function updateConfigFile(filePath, update, options = {}) {
96
97
  if (before != null) {
97
98
  fs.accessSync(destination, fs.constants.W_OK);
98
99
  }
99
- fs.mkdirSync(path.dirname(destination), { recursive: true });
100
- const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
100
+ fs.mkdirSync(path2.dirname(destination), { recursive: true });
101
+ const temporary = path2.join(path2.dirname(destination), `.${path2.basename(destination)}.${randomUUID()}.tmp`);
101
102
  const mode = options.mode ?? (before == null ? 384 : fs.statSync(destination).mode & 511);
102
103
  let descriptor;
103
104
  let ownsTemporary = false;
@@ -136,25 +137,25 @@ var CLIENT_LABELS2 = {
136
137
  cursor: "Cursor"
137
138
  };
138
139
  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}`;
140
+ var KIND_LABELS = { db: "Database", ssh: "SSH", wsl: "WSL", s3: "S3" };
141
+ var PACKAGE_NAME = "@akms/mcp";
142
+ var legacyPackageName = (kind) => `@akms/mcp-${kind}`;
147
143
  var binary = (command) => command.replace(/\\/g, "/").split("/").pop().toLowerCase();
144
+ var posixPath = (value) => value?.replace(/\\/g, "/") ?? "";
145
+ var isNode = (command) => /^(node|nodejs)(\.exe)?$/.test(binary(command));
146
+ var isCurrentScript = (script, kind) => posixPath(script).endsWith(`/node_modules/@akms/mcp/lib/${kind}.js`);
147
+ var isLegacyScript = (script, kind) => posixPath(script).endsWith(`/node_modules/@akms/mcp-${kind}/lib/cli.js`);
148
+ var isLegacyPackageArgument = (argument, kind) => new RegExp(`^@akms/mcp-${kind}(?:@[^\\s]+)?$`).test(argument);
148
149
  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
+ return path2.join(process.env.AKMS_MCP_HOME ? path2.resolve(process.env.AKMS_MCP_HOME) : path2.join(os5.homedir(), ".akms", "mcp"), "runtime");
150
151
  }
151
152
  function runtimeRoot(prefix = runtimePrefix(), platform = process.platform) {
152
153
  return path2.join(prefix, ...platform === "win32" ? [] : ["lib"], "node_modules");
153
154
  }
154
- var runtimeInstallCommand = (kind) => `npm install --global --prefix "${runtimePrefix()}" ${packageName(kind)}@latest`;
155
- async function latestRuntimeVersion(kind, timeoutMs = 6e3) {
155
+ var runtimeInstallCommand = () => `npm install --global --prefix "${runtimePrefix()}" ${PACKAGE_NAME}@latest`;
156
+ async function latestRuntimeVersion(timeoutMs = 6e3) {
156
157
  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
+ const { stdout } = await promisify(execFile)(process.execPath, [npmCli(), "view", `${PACKAGE_NAME}@latest`, "version", "--json", "--prefer-online", "--fetch-retries=0", "--fetch-timeout=4000"], {
158
159
  windowsHide: true,
159
160
  timeout: timeoutMs,
160
161
  maxBuffer: 65536
@@ -186,26 +187,33 @@ function runtimeIsOlder(installed, current) {
186
187
  }
187
188
  function launchKind(entry) {
188
189
  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;
190
+ if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(binary(entry.command)) || entry.args.some((arg) => isLegacyPackageArgument(arg, kind))) return kind;
191
+ if (isNode(entry.command) && (isCurrentScript(entry.args[0], kind) || isLegacyScript(entry.args[0], kind))) return kind;
191
192
  }
192
193
  return null;
193
194
  }
194
195
  function standardLaunch(entry, kind) {
195
196
  if (launchKind(entry) !== kind) return false;
196
197
  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 (isNode(entry.command)) return entry.args.length === 1 && (isCurrentScript(entry.args[0], kind) || isLegacyScript(entry.args[0], kind));
198
199
  if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(command)) return entry.args.length === 0;
199
200
  let args = entry.args;
200
201
  if (command === "cmd" || command === "cmd.exe") {
201
202
  if (args[0]?.toLowerCase() !== "/c" || !/^npx(?:\.cmd)?$/i.test(args[1] ?? "")) return false;
202
203
  args = args.slice(2);
203
204
  } 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
+ return args.length === 2 && ["-y", "--yes"].includes(args[0]) && isLegacyPackageArgument(args[1], kind);
205
206
  }
206
207
  function catalogLaunch(entry, kind = launchKind(entry)) {
207
208
  return !!kind && entry.args.length === 2 && entry.args[1] === "--catalog" && standardLaunch({ ...entry, args: entry.args.slice(0, 1) }, kind);
208
209
  }
210
+ function legacyLaunch(entry, kind = launchKind(entry)) {
211
+ if (!kind) return false;
212
+ const launch = catalogLaunch(entry, kind) ? { ...entry, args: entry.args.slice(0, 1) } : entry;
213
+ if (!standardLaunch(launch, kind)) return false;
214
+ if (isNode(launch.command)) return isLegacyScript(launch.args[0], kind);
215
+ return launch.args.some((arg) => isLegacyPackageArgument(arg, kind));
216
+ }
209
217
  function npmCli() {
210
218
  const candidates = [
211
219
  path2.join(path2.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
@@ -219,8 +227,8 @@ function npmCli() {
219
227
  }
220
228
  for (const file of candidates) {
221
229
  try {
222
- const resolved = fs2.realpathSync(file);
223
- if (resolved.endsWith(".js") && fs2.statSync(resolved).isFile()) return resolved;
230
+ const resolved = fs.realpathSync(file);
231
+ if (resolved.endsWith(".js") && fs.statSync(resolved).isFile()) return resolved;
224
232
  } catch {
225
233
  }
226
234
  }
@@ -228,34 +236,46 @@ function npmCli() {
228
236
  }
229
237
  function findGlobalRuntime(kind, root) {
230
238
  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() };
239
+ const directory = path2.join(root ?? runtimeRoot(), "@akms", "mcp");
240
+ const metadata = JSON.parse(fs.readFileSync(path2.join(directory, "package.json"), "utf8"));
241
+ const script = path2.join(directory, "lib", `${kind}.js`);
242
+ if (metadata.name !== PACKAGE_NAME || typeof metadata.version !== "string" || !fs.statSync(script).isFile()) return null;
243
+ return { command: process.execPath, args: [script], version: metadata.version, linked: fs.lstatSync(directory).isSymbolicLink() };
237
244
  } catch {
238
245
  return null;
239
246
  }
240
247
  }
241
- async function installGlobalRuntime(kind) {
248
+ function findLegacyRuntimes(root) {
249
+ const found = [];
250
+ for (const kind of KINDS) {
251
+ try {
252
+ const directory = path2.join(root ?? runtimeRoot(), "@akms", `mcp-${kind}`);
253
+ const metadata = JSON.parse(fs.readFileSync(path2.join(directory, "package.json"), "utf8"));
254
+ if (metadata.name !== legacyPackageName(kind)) continue;
255
+ found.push({ kind, packageName: legacyPackageName(kind), version: typeof metadata.version === "string" ? metadata.version : "unknown", linked: fs.lstatSync(directory).isSymbolicLink() });
256
+ } catch {
257
+ }
258
+ }
259
+ return found;
260
+ }
261
+ async function runNpm(argumentsList, failure, retry) {
242
262
  const cancellation = new AbortController();
243
263
  const cancel = () => cancellation.abort();
244
264
  process.on("SIGINT", cancel);
245
265
  process.on("SIGTERM", cancel);
246
266
  try {
247
- await promisify(execFile)(process.execPath, [npmCli(), "install", "--global", "--prefix", runtimePrefix(), "--prefer-online", "--no-fund", "--no-audit", `${packageName(kind)}@latest`], {
267
+ await promisify(execFile)(process.execPath, [npmCli(), ...argumentsList], {
248
268
  windowsHide: true,
249
269
  timeout: 18e4,
250
270
  maxBuffer: 1024 * 1024,
251
271
  signal: cancellation.signal,
252
- cwd: os.homedir()
272
+ cwd: os5.homedir()
253
273
  });
254
274
  } catch (error) {
255
275
  if (cancellation.signal.aborted) throw new Error("AKMS runtime installation cancelled.");
256
276
  const stderr = error.stderr;
257
277
  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 ? `
278
+ throw new Error(`${failure} Run ${retry} in your terminal and retry.${detail ? `
259
279
 
260
280
  ${detail}` : ""}`);
261
281
  } finally {
@@ -263,13 +283,24 @@ ${detail}` : ""}`);
263
283
  process.off("SIGTERM", cancel);
264
284
  }
265
285
  }
286
+ async function installGlobalRuntime() {
287
+ await runNpm(
288
+ ["install", "--global", "--prefix", runtimePrefix(), "--prefer-online", "--no-fund", "--no-audit", `${PACKAGE_NAME}@latest`],
289
+ "AKMS runtime installation failed.",
290
+ runtimeInstallCommand()
291
+ );
292
+ }
293
+ async function removeLegacyRuntimes(kinds) {
294
+ if (!kinds.length) return;
295
+ const names = kinds.map(legacyPackageName);
296
+ await runNpm(
297
+ ["uninstall", "--global", "--prefix", runtimePrefix(), "--no-fund", "--no-audit", ...names],
298
+ "Removing the old packages failed.",
299
+ `npm uninstall --global --prefix "${runtimePrefix()}" ${names.join(" ")}`
300
+ );
301
+ }
266
302
 
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
-
272
- // src/modules/launcher/prompter.ts
303
+ // src/modules/prompt-errors.ts
273
304
  var PromptClosedError = class extends Error {
274
305
  constructor() {
275
306
  super("input closed before the prompt was answered");
@@ -282,6 +313,8 @@ var PromptBackError = class extends Error {
282
313
  this.name = "PromptBackError";
283
314
  }
284
315
  };
316
+
317
+ // src/modules/launcher/prompter.ts
285
318
  var ESC = "\x1B";
286
319
  var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
287
320
  var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
@@ -383,6 +416,26 @@ var Prompter = class {
383
416
  constructor(input, output, initialInput = "") {
384
417
  this.input = input;
385
418
  this.output = output;
419
+ this.lineBuffer = "";
420
+ this.pendingLines = [];
421
+ this.lineWaiter = null;
422
+ this.inputEnded = false;
423
+ this.onInputData = (chunk) => {
424
+ this.onData(chunk);
425
+ };
426
+ this.onInputEnd = () => {
427
+ this.inputEnded = true;
428
+ if (this.lineWaiter != null) {
429
+ const waiter = this.lineWaiter;
430
+ this.lineWaiter = null;
431
+ waiter(null);
432
+ }
433
+ if (this.keyHandler != null) {
434
+ this.keyHandler(KEY_CTRL_D);
435
+ }
436
+ };
437
+ /** Set while a key-driven prompt owns the input. */
438
+ this.keyHandler = null;
386
439
  const ttyInput = input;
387
440
  const ttyOutput = output;
388
441
  this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
@@ -398,31 +451,6 @@ var Prompter = class {
398
451
  input.on("end", this.onInputEnd);
399
452
  if (initialInput) this.onData(initialInput);
400
453
  }
401
- input;
402
- output;
403
- lineBuffer = "";
404
- pendingLines = [];
405
- lineWaiter = null;
406
- inputEnded = false;
407
- onInputData = (chunk) => {
408
- this.onData(chunk);
409
- };
410
- onInputEnd = () => {
411
- this.inputEnded = true;
412
- if (this.lineWaiter != null) {
413
- const waiter = this.lineWaiter;
414
- this.lineWaiter = null;
415
- waiter(null);
416
- }
417
- if (this.keyHandler != null) {
418
- this.keyHandler(KEY_CTRL_D);
419
- }
420
- };
421
- /** Set while a key-driven prompt owns the input. */
422
- keyHandler = null;
423
- interactive;
424
- style;
425
- symbols;
426
454
  /** Stops reading so the process can exit once the command is done. */
427
455
  close() {
428
456
  if (this.interactive == true) {
@@ -597,6 +625,9 @@ var Prompter = class {
597
625
  if (item.hint != null && item.hint !== "") {
598
626
  line += ` ${item.hint}`;
599
627
  }
628
+ if (item.notice != null && item.notice !== "") {
629
+ line += ` ${item.notice}`;
630
+ }
600
631
  this.output.write(`${line.trimEnd()}
601
632
  `);
602
633
  }
@@ -721,6 +752,9 @@ var Prompter = class {
721
752
  if (item.hint != null && item.hint !== "") {
722
753
  parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
723
754
  }
755
+ if (item.notice != null && item.notice !== "") {
756
+ parts.push({ text: ` ${item.notice}`, paint: this.style[item.noticeTone ?? "warning"] });
757
+ }
724
758
  lines.push(this.fitLine(parts, width));
725
759
  }
726
760
  lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
@@ -798,7 +832,7 @@ function writeWrapped(prompter, text, indent = "", changed, style) {
798
832
  let segment = "";
799
833
  let highlighted = false;
800
834
  const flush = () => {
801
- line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : segment;
835
+ line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : style ? style(segment) : segment;
802
836
  segment = "";
803
837
  };
804
838
  for (let index = 0; index < characters.length; index++) {
@@ -806,7 +840,7 @@ function writeWrapped(prompter, text, indent = "", changed, style) {
806
840
  const size = displayWidth(character);
807
841
  if (width + size > prompter.width) {
808
842
  flush();
809
- prompter.write((style ? style(line) : line) + "\n");
843
+ prompter.write(line + "\n");
810
844
  line = indent;
811
845
  width = displayWidth(indent);
812
846
  }
@@ -819,7 +853,7 @@ function writeWrapped(prompter, text, indent = "", changed, style) {
819
853
  width += size;
820
854
  }
821
855
  flush();
822
- prompter.write((style ? style(line) : line) + "\n");
856
+ prompter.write(line + "\n");
823
857
  }
824
858
  function changedRange(values, index) {
825
859
  if (values.length !== 2 || values[0] === values[1]) return void 0;
@@ -836,7 +870,10 @@ function changedRange(values, index) {
836
870
  function compareEntries(prompter, versions) {
837
871
  const entries = versions.map((version) => version.entry);
838
872
  const fields = differingFields(entries);
839
- writeWrapped(prompter, descriptionOnly(entries) ? "Description differs. Connection settings match." : `${fields.length} setting(s) differ.`);
873
+ if (descriptionOnly(entries)) prompter.write(`${prompter.paint.warning("Description differs.")} ${prompter.paint.dim("Connection settings match.")}
874
+ `);
875
+ else prompter.write(`${prompter.paint.accent(String(fields.length))} ${prompter.paint.warning("setting(s) differ.")}
876
+ `);
840
877
  const values = fields.map((field) => {
841
878
  const key = field.replace(/^env\./, "");
842
879
  return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
@@ -849,7 +886,7 @@ function compareEntries(prompter, versions) {
849
886
  const size = displayWidth(character);
850
887
  if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
851
888
  const line = lines[lines.length - 1];
852
- line.text += changed && index >= changed[0] && index < changed[1] ? prompter.paint.accent(prompter.paint.bold(character)) : character;
889
+ line.text += prompter.paint.accent(changed && index >= changed[0] && index < changed[1] ? prompter.paint.bold(character) : character);
853
890
  line.width += size;
854
891
  }
855
892
  return lines;
@@ -874,13 +911,13 @@ ${prompter.paint.dim(fieldName(fields[field]))}
874
911
  }
875
912
  for (let index = 0; index < versions.length; index++) {
876
913
  prompter.write("\n");
877
- writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
914
+ writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`, "", void 0, prompter.paint.bold);
878
915
  for (let field = 0; field < fields.length; field++) {
879
- writeWrapped(prompter, fieldName(fields[field]), " ");
880
- writeWrapped(prompter, values[field][index], " ", changedRange(values[field], index));
916
+ writeWrapped(prompter, fieldName(fields[field]), " ", void 0, prompter.paint.dim);
917
+ writeWrapped(prompter, values[field][index], " ", changedRange(values[field], index), prompter.paint.accent);
881
918
  }
882
919
  }
883
- if (values.some((row) => row.includes("(hidden)"))) prompter.write("\nSecret values are hidden.\n");
920
+ if (values.some((row) => row.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
884
921
  prompter.write("\n");
885
922
  }
886
923
 
@@ -890,7 +927,7 @@ function visibleRuntimePath(entry) {
890
927
  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
928
  }
892
929
  function redactStartupText(text, entry) {
893
- const normalize2 = (value) => stripVTControlCharacters2(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
930
+ const normalize2 = (value) => stripVTControlCharacters(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
894
931
  const direct = visibleRuntimePath(entry);
895
932
  const privateValues = [...Object.entries(entry.env).filter(([key]) => !visibleField.test(key)).map(([, value]) => value), ...entry.args.slice(direct ? 1 : 0)];
896
933
  const secrets = /* @__PURE__ */ new Set();
@@ -959,7 +996,7 @@ async function checkStartup(entry, timeoutMs = 1e4) {
959
996
  child.stdin?.end();
960
997
  killTimer = setTimeout(() => {
961
998
  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 }, () => {
999
+ execFile(path2.join(process.env.SYSTEMROOT ?? "C:\\Windows", "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 4e3 }, () => {
963
1000
  });
964
1001
  } else if (child.pid) {
965
1002
  try {
@@ -1086,20 +1123,24 @@ async function checkConnections(prompter, entries, check = checkStartup) {
1086
1123
  }
1087
1124
  if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
1088
1125
  }
1089
-
1090
- // src/modules/registration/catalog.ts
1091
- import fs3 from "fs";
1092
- import os2 from "os";
1093
- import path4 from "path";
1094
1126
  var LONGEST_TOOL_NAME_LENGTH = { db: 13, ssh: 17, wsl: 17, s3: 11 };
1095
1127
  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");
1128
+ var defaultCatalogPath = () => path2.join(process.env.AKMS_MCP_HOME ? path2.resolve(process.env.AKMS_MCP_HOME) : path2.join(os5.homedir(), ".akms", "mcp"), "registry.json");
1097
1129
 
1098
1130
  // src/modules/registration/registry.ts
1099
1131
  var sessionName = (kind) => `akms_${kind}`;
1132
+ function legacyLaunches(state) {
1133
+ return KINDS.map((kind) => ({ kind, names: [
1134
+ ...state.sessions?.[kind] && legacyLaunch(state.sessions[kind].entry, kind) ? [sessionName(kind)] : [],
1135
+ ...Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry && legacyLaunch(item.entry, kind)).map(([name]) => name)
1136
+ ] })).filter((item) => item.names.length);
1137
+ }
1100
1138
  var MIGRATION_STEPS = [
1101
1139
  { 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 }
1140
+ { id: "catalog_default", label: "Make one entry per kind the default", needed: (state) => state.defaultMode === void 0 },
1141
+ // Every kind is grouped: one akms_<kind> entry per AI service. Per-connection entries are a 0.2 layout to convert, not a choice.
1142
+ { id: "group_all", label: "Group every kind's connections into one akms_<kind> entry", needed: (state) => state.version === 2 && (state.defaultMode === "individual" || Object.values(state.sessions ?? {}).some((session) => session?.mode === "individual")) },
1143
+ { id: "single_package", label: "Start servers from @akms/mcp instead of the separate per-kind packages", needed: (state) => legacyLaunches(state).length > 0 }
1103
1144
  ];
1104
1145
  var own = (value, key) => Object.hasOwn(value, key);
1105
1146
  var record = (value) => value != null && typeof value === "object" && !Array.isArray(value);
@@ -1134,18 +1175,45 @@ var Registry = class {
1134
1175
  constructor(targets, file = defaultRegistryPath(), runtimeOptions = {}) {
1135
1176
  this.targets = targets;
1136
1177
  this.runtimeOptions = runtimeOptions;
1178
+ this.backupCaptured = false;
1137
1179
  this.file = file;
1138
1180
  }
1139
- targets;
1140
- runtimeOptions;
1141
- file;
1142
- observedText;
1143
- backupCaptured = false;
1144
1181
  globalRuntime(kind) {
1145
1182
  return (this.runtimeOptions.find ?? findGlobalRuntime)(kind);
1146
1183
  }
1147
- latestRuntime(kind) {
1148
- return (this.runtimeOptions.latest ?? latestRuntimeVersion)(kind);
1184
+ /** The installed @akms/mcp, whichever kind is asked: one package carries them all. */
1185
+ installedRuntime() {
1186
+ return this.globalRuntime("db");
1187
+ }
1188
+ latestRuntime() {
1189
+ return (this.runtimeOptions.latest ?? latestRuntimeVersion)();
1190
+ }
1191
+ /**
1192
+ * Per-kind packages left in the runtime directory, with the services or catalog entries
1193
+ * that still start them. A service that cannot be read counts as using every one of them.
1194
+ */
1195
+ legacyRuntimes() {
1196
+ const state = this.read();
1197
+ return (this.runtimeOptions.legacy ?? findLegacyRuntimes)().map((runtime) => {
1198
+ const usedBy = legacyLaunches(state).filter((item) => item.kind === runtime.kind).flatMap((item) => item.names.map((name) => `AKMS catalog: ${name}`));
1199
+ for (const target of this.targets) {
1200
+ try {
1201
+ for (const [name, entry] of Object.entries(target.readEntries())) {
1202
+ const launch = catalogLaunch(entry, runtime.kind) ? { ...entry, args: entry.args.slice(0, 1) } : entry;
1203
+ if (legacyLaunch(launch, runtime.kind) && /^(node|nodejs)(\.exe)?$/i.test(path2.basename(launch.command))) usedBy.push(`${target.label}: ${name}`);
1204
+ }
1205
+ } catch {
1206
+ usedBy.push(`${target.label}: cannot be read`);
1207
+ }
1208
+ }
1209
+ return { runtime, usedBy };
1210
+ });
1211
+ }
1212
+ /** Removes only packages nothing uses at the moment of removal. */
1213
+ async removeLegacyRuntimes(kinds) {
1214
+ const inUse = this.legacyRuntimes().filter((item) => kinds.includes(item.runtime.kind) && item.usedBy.length);
1215
+ if (inUse.length) throw new RegistryError(`Still in use: ${inUse.map((item) => item.runtime.packageName).join(", ")}. Run Migrate registry and Sync first.`);
1216
+ await (this.runtimeOptions.removeLegacy ?? removeLegacyRuntimes)(kinds);
1149
1217
  }
1150
1218
  isSessionMode(kind, state = this.read()) {
1151
1219
  return state.version === 2 && (state.sessions?.[kind]?.mode ?? state.defaultMode) === "catalog";
@@ -1156,9 +1224,10 @@ var Registry = class {
1156
1224
  planMigration() {
1157
1225
  const state = this.read();
1158
1226
  const steps = MIGRATION_STEPS.filter((step) => step.needed(state)).map(({ id, label }) => ({ id, label }));
1159
- const kinds = steps.length ? KINDS.map((kind) => {
1227
+ const launches = legacyLaunches(state).map((item) => ({ ...item, ready: this.globalRuntime(item.kind) != null }));
1228
+ const layoutChanges = steps.some((step) => step.id === "v1_to_v2" || step.id === "catalog_default" || step.id === "group_all");
1229
+ const kinds = layoutChanges ? KINDS.map((kind) => {
1160
1230
  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
1231
  if (this.isSessionMode(kind, state)) return { kind, names, status: "catalog" };
1163
1232
  if (!names.length) return { kind, names, status: "empty" };
1164
1233
  try {
@@ -1185,7 +1254,7 @@ var Registry = class {
1185
1254
  return { kind, names, status: "blocked", detail: error instanceof RegistryError ? error.message : "Cannot preview this kind. Check its runtime and catalog settings." };
1186
1255
  }
1187
1256
  }) : [];
1188
- return { expected: JSON.stringify(state), steps, kinds };
1257
+ return { expected: JSON.stringify(state), steps, kinds, launches };
1189
1258
  }
1190
1259
  applyMigration(plan) {
1191
1260
  if (!plan.steps.length) return this.locked(() => {
@@ -1204,21 +1273,28 @@ var Registry = class {
1204
1273
  state.version = 2;
1205
1274
  state.sessions ??= {};
1206
1275
  } 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: {} };
1276
+ else if (step.id === "group_all") {
1277
+ state.defaultMode = "catalog";
1278
+ for (const kind of KINDS) if (state.sessions?.[kind]?.mode === "individual") delete state.sessions[kind];
1279
+ } else if (step.id === "single_package") {
1280
+ for (const item of plan.launches) {
1281
+ const runtime = this.globalRuntime(item.kind);
1282
+ if (runtime != null !== item.ready) throw new RegistryError("Runtime availability changed since review. Reopen Migrate registry.");
1283
+ if (!runtime) continue;
1284
+ const session = state.sessions?.[item.kind];
1285
+ if (session && legacyLaunch(session.entry, item.kind)) session.entry = this.sessionEntry(item.kind);
1286
+ for (const connection of Object.values(state.connections)) {
1287
+ if (connection.kind === item.kind && connection.entry && legacyLaunch(connection.entry, item.kind)) {
1288
+ connection.entry = { ...connection.entry, command: runtime.command, args: [...runtime.args] };
1289
+ }
1290
+ }
1291
+ }
1292
+ } else throw new RegistryError("Unknown registry migration step. Reopen Migrate registry.");
1219
1293
  }
1220
- state.defaultMode = mode;
1221
- }, []);
1294
+ }, [.../* @__PURE__ */ new Set([
1295
+ ...plan.kinds.filter((item) => item.status === "convert" || item.status === "pending-runtime").map((item) => item.kind),
1296
+ ...plan.launches.filter((item) => item.ready).map((item) => item.kind)
1297
+ ])]);
1222
1298
  }
1223
1299
  kindForName(name, state = this.read()) {
1224
1300
  return own(state.connections, name) ? state.connections[name].kind : KINDS.find((kind) => sessionName(kind) === name && (state.sessions?.[kind] || this.isSessionMode(kind, state)));
@@ -1228,10 +1304,10 @@ var Registry = class {
1228
1304
  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
1305
  }
1230
1306
  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.");
1307
+ if (path2.basename(this.file) !== "registry.json") throw new RegistryError("Session mode requires a catalog named registry.json in the AKMS catalog directory.");
1232
1308
  const runtime = this.requireRuntime(kind);
1233
1309
  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));
1310
+ if (process.env.AKMS_MCP_HOME || path2.resolve(this.file) !== path2.resolve(defaultCatalogPath())) env.AKMS_MCP_HOME = path2.dirname(path2.resolve(this.file));
1235
1311
  return { command: runtime.command, args: [...runtime.args, "--catalog"], env };
1236
1312
  }
1237
1313
  planSession(kind, mode) {
@@ -1299,49 +1375,21 @@ var Registry = class {
1299
1375
  }
1300
1376
  await checkConnections(prompter, entries, this.runtimeOptions.check);
1301
1377
  }
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);
1378
+ /** Installs or updates @akms/mcp, which carries every kind; returns the given kind's launch. */
1379
+ async installRuntime(kind = "db") {
1380
+ if (this.installedRuntime()?.linked) throw new RegistryError("This is a linked development installation. Build or update its source checkout instead.");
1381
+ await (this.runtimeOptions.install ?? installGlobalRuntime)();
1305
1382
  return this.requireRuntime(kind);
1306
1383
  }
1307
1384
  requireRuntime(kind) {
1308
1385
  const runtime = this.globalRuntime(kind);
1309
- if (!runtime) throw new RegistryError(`Install the AKMS runtime first: ${runtimeInstallCommand(kind)}. Then reopen Runtime.`);
1386
+ if (!runtime) throw new RegistryError(`Install the AKMS runtime first: ${runtimeInstallCommand()}. Then reopen Runtime.`);
1310
1387
  return runtime;
1311
1388
  }
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
1389
  read() {
1342
1390
  let text;
1343
1391
  try {
1344
- text = fs4.readFileSync(this.file, "utf8");
1392
+ text = fs.readFileSync(this.file, "utf8");
1345
1393
  this.observedText = text;
1346
1394
  } catch (error) {
1347
1395
  if (error.code === "ENOENT") {
@@ -1377,7 +1425,7 @@ var Registry = class {
1377
1425
  return after;
1378
1426
  }, { beforeCommit: (before) => {
1379
1427
  if (before !== null && !this.backupCaptured) {
1380
- updateConfigFile(path5.join(path5.dirname(this.file), "registry.previous.json"), () => before, { mode: fs4.statSync(this.file).mode & 511 });
1428
+ updateConfigFile(path2.join(path2.dirname(this.file), "registry.previous.json"), () => before, { mode: fs.statSync(this.file).mode & 511 });
1381
1429
  this.backupCaptured = true;
1382
1430
  }
1383
1431
  } });
@@ -1385,16 +1433,16 @@ var Registry = class {
1385
1433
  }
1386
1434
  /** Serializes standalone editors across packages, including their config-file writes. */
1387
1435
  locked(action) {
1388
- fs4.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
1436
+ fs.mkdirSync(path2.dirname(this.file), { recursive: true, mode: 448 });
1389
1437
  const lock = this.file + ".lock";
1390
1438
  let descriptor;
1391
1439
  try {
1392
- descriptor = fs4.openSync(lock, "wx", 384);
1440
+ descriptor = fs.openSync(lock, "wx", 384);
1393
1441
  } catch (error) {
1394
1442
  if (error.code !== "EEXIST") throw error;
1395
1443
  let abandoned = false;
1396
1444
  try {
1397
- const pid = Number(fs4.readFileSync(lock, "utf8"));
1445
+ const pid = Number(fs.readFileSync(lock, "utf8"));
1398
1446
  if (Number.isSafeInteger(pid) && pid > 0) {
1399
1447
  try {
1400
1448
  process.kill(pid, 0);
@@ -1405,17 +1453,17 @@ var Registry = class {
1405
1453
  } catch {
1406
1454
  }
1407
1455
  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);
1456
+ fs.unlinkSync(lock);
1457
+ descriptor = fs.openSync(lock, "wx", 384);
1410
1458
  }
1411
1459
  this.backupCaptured = false;
1412
1460
  try {
1413
- fs4.writeFileSync(descriptor, String(process.pid));
1461
+ fs.writeFileSync(descriptor, String(process.pid));
1414
1462
  return action();
1415
1463
  } finally {
1416
1464
  this.backupCaptured = false;
1417
- fs4.closeSync(descriptor);
1418
- fs4.unlinkSync(lock);
1465
+ fs.closeSync(descriptor);
1466
+ fs.unlinkSync(lock);
1419
1467
  }
1420
1468
  }
1421
1469
  change(action, kinds) {
@@ -1721,26 +1769,29 @@ var Registry = class {
1721
1769
  function formatResults(results, prefix = "", options = {}) {
1722
1770
  const plain = (text) => text;
1723
1771
  const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
1772
+ const accent = options.paint?.accent ?? paint.bold;
1724
1773
  const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
1725
1774
  const lines = prefix ? [paint.bold(prefix)] : [];
1726
1775
  const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
1727
- lines.push(` Connections: ${total}`);
1776
+ lines.push(`${paint.dim(" Connections: ")}${accent(String(total))}`);
1728
1777
  const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
1729
1778
  const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
1730
1779
  const removed = results.filter((item) => item.status === "removed").length;
1731
1780
  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`)}`);
1781
+ const count = (value, label, tone) => value ? `${accent(String(value))} ${paint[tone](label)}` : paint.dim(`0 ${label}`);
1782
+ lines.push(`${paint.dim(" Service entries: ")}${count(updated, "updated", "success")} ${paint.dim("\xB7")} ${count(unchanged, "already up to date", "dim")}${removed ? ` ${paint.dim("\xB7")} ${count(removed, "removed", "success")}` : ""} ${paint.dim("\xB7")} ${count(failed.length, "failed", "error")}`);
1733
1783
  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."));
1784
+ if (results.length && results.every((item) => item.status === "synced" || item.status === "removed")) lines.push(`${paint.success(" OK:")} ${paint.dim("all service entries are up to date.")}`);
1735
1785
  const waiting = results.filter((item) => item.status === "pending");
1736
1786
  for (const kind of KINDS) {
1737
1787
  const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
1738
1788
  const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
1739
1789
  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.");
1790
+ lines.push(runtime.length ? `
1791
+ ${paint.warning("Waiting for runtime")} - ${accent(kind.toUpperCase())}` : `
1792
+ ${paint.warning("Waiting for")} ${accent(sessionName(kind))}`);
1793
+ if (runtime.length) lines.push(paint.dim(" Install it from Runtime, then run Sync. Your existing entries keep working."));
1794
+ else lines.push(paint.dim(" Resolve this session entry in Sync. Existing connection entries stay until it is ready."));
1744
1795
  const retained = /* @__PURE__ */ new Map();
1745
1796
  for (const client of new Set(kept.map((item) => item.client))) {
1746
1797
  const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
@@ -1751,7 +1802,7 @@ ${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` :
1751
1802
  }
1752
1803
  for (const group of retained.values()) {
1753
1804
  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(", ")}`));
1805
+ lines.push(`${paint.dim(" Kept in ")}${paint.bold(clients)}${paint.dim(` until ${sessionName(kind)} is applied: `)}${accent(group.names.map(safe).join(", "))}`);
1755
1806
  }
1756
1807
  }
1757
1808
  for (const [heading, rows, color] of [
@@ -1763,258 +1814,376 @@ ${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` :
1763
1814
  lines.push(`
1764
1815
  ${color(`${heading}:`)}`);
1765
1816
  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)}`);
1817
+ lines.push(` ${accent(safe(name))}`);
1818
+ for (const row of rows.filter((item) => item.name === name)) lines.push(` ${row.status === "error" ? paint.error("FAILED ") : ""}${paint.dim(`${CLIENT_LABELS2[row.client]}: `)}${safe(row.detail)}`);
1768
1819
  }
1769
1820
  }
1770
- if (results.some((item) => item.status === "conflict")) lines.push(paint.warning(`
1821
+ if (results.some((item) => item.status === "conflict")) lines.push(paint.dim(`
1771
1822
  ${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."));
1823
+ if (failed.length) lines.push(`${paint.dim("\nNext: ")}${paint.error("fix the errors above")}${paint.dim(", then run ")}${accent("Sync")}${paint.dim(" again.")}`);
1773
1824
  return lines.join("\n");
1774
1825
  }
1775
1826
 
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.
1827
+ // src/modules/registration/migration-menu.ts
1828
+ function migrationStatus(registry) {
1829
+ if (!registry.needsMigration()) return { status: "none" };
1830
+ const plan = registry.planMigration();
1831
+ const blocked = plan.kinds.find((item) => item.status === "blocked");
1832
+ if (blocked) return { status: "blocked", detail: blocked.detail };
1833
+ if (plan.steps.some((step) => step.id !== "single_package") || plan.launches.some((item) => item.ready)) return { status: "ready" };
1834
+ return { status: "waiting", detail: `Install ${PACKAGE_NAME} first.` };
1835
+ }
1836
+ function migrationVersions(registry, plan = registry.planMigration()) {
1837
+ const pairs = [];
1838
+ if (plan.steps.some((step) => step.id === "v1_to_v2")) pairs.push({ label: "Registry", from: "v1", to: "v2" });
1839
+ const ready = plan.launches.filter((item) => item.ready);
1840
+ if (ready.length) {
1841
+ const installed = new Map(registry.legacyRuntimes().map((item) => [item.runtime.kind, `${item.runtime.packageName} ${item.runtime.version}`]));
1842
+ const from = [...new Set(ready.map((item) => installed.get(item.kind) ?? legacyPackageName(item.kind)))].join(", ");
1843
+ pairs.push({ label: "Servers", from, to: `${PACKAGE_NAME} ${registry.installedRuntime()?.version ?? ""}`.trim() });
1844
+ }
1845
+ return pairs;
1846
+ }
1847
+ function writePlan(prompter, registry, plan) {
1848
+ const paint = prompter.paint;
1849
+ prompter.clearScreen();
1850
+ prompter.write(`${paint.bold("Migrate registry")}
1815
1851
  `);
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)}
1852
+ for (const pair of migrationVersions(registry, plan)) {
1853
+ prompter.write(` ${paint.dim(pair.label.padEnd(10))}${paint.warning(plainText(pair.from))} ${paint.dim("\u2192")} ${paint.bold(paint.success(plainText(pair.to)))}
1818
1854
  `);
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)}
1855
+ }
1856
+ plan.steps.forEach((step, index) => prompter.write(` ${paint.accent(`${index + 1}.`)} ${step.label}
1857
+ `));
1858
+ if (plan.kinds.length) prompter.write(paint.dim(" Every kind is listed as one akms_<kind> entry in each AI service; connection settings stay in the catalog.\n"));
1859
+ prompter.write("\n");
1860
+ for (const item of plan.launches) {
1861
+ const names = item.names.length > 3 ? `${item.names.length} entries` : item.names.map(plainText).join(", ");
1862
+ prompter.write(` ${paint.dim(KIND_LABELS[item.kind].padEnd(10))}${names} ${paint.dim("\u2192")} ${paint.accent(`${PACKAGE_NAME}/lib/${item.kind}.js`)}${item.ready ? "" : paint.warning(" after installing it")}
1847
1863
  `);
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.
1864
+ }
1865
+ for (const item of plan.kinds) {
1866
+ const target = sessionName(item.kind);
1867
+ const action = item.status === "catalog" ? `already ${target}` : item.status === "empty" ? `no connections \xB7 new ones use ${target}` : item.status === "blocked" ? paint.error("blocked") : `${item.names.length} entries \u2192 ${target}${item.status === "pending-runtime" ? paint.warning(" after Runtime + Sync") : ""}`;
1868
+ prompter.write(` ${paint.dim(KIND_LABELS[item.kind].padEnd(10))}${action}
1858
1869
  `);
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):
1870
+ if (item.detail) prompter.write(` ${" ".repeat(10)}${paint.dim(plainText(item.detail))}
1867
1871
  `);
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.
1872
+ for (const change of item.session?.changes ?? item.changes ?? []) {
1873
+ const remove = change.remove.length > 3 ? `${change.remove.length} entries` : change.remove.join(", ") || "nothing";
1874
+ prompter.write(` ${" ".repeat(10)}${paint.dim(CLIENT_LABELS2[change.client].padEnd(15))}${paint.accent(change.add.join(", ") || "nothing")} ${paint.dim("\xB7 then remove")} ${plainText(remove)}
1885
1875
  `);
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."}
1876
+ if (change.review.length) prompter.write(` ${" ".repeat(25)}${paint.warning(`kept for Sync review: ${change.review.map(plainText).join(", ")}`)}
1892
1877
  `);
1893
- await prompter.acknowledge();
1894
1878
  }
1879
+ if (item.session?.custom.length) prompter.write(` ${" ".repeat(10)}${paint.dim(`custom commands are not started; their settings still load in the grouped entry: ${item.session.custom.map(plainText).join(", ")}`)}
1880
+ `);
1895
1881
  }
1882
+ prompter.write(paint.dim("\nServer entries hold no passwords; they stay in the catalog. The previous catalog is kept as registry.previous.json.\n"));
1896
1883
  }
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;
1884
+ async function migrationMenu(prompter, registry) {
1885
+ try {
1886
+ const paint = prompter.paint;
1887
+ let plan = registry.planMigration();
1888
+ if (!plan.steps.length) {
1942
1889
  prompter.clearScreen();
1943
- for (const row of selected) {
1944
- writeWrapped(prompter, `Updating ${packageName(row.kind)}...`);
1890
+ prompter.write(`${paint.bold("Migrate registry")}
1891
+ ${paint.success("Up to date")} ${paint.dim("\xB7 the registry is already in the current format")}
1892
+ `);
1893
+ await prompter.acknowledge();
1894
+ return;
1895
+ }
1896
+ writePlan(prompter, registry, plan);
1897
+ if (plan.launches.some((item) => !item.ready) && !registry.installedRuntime()?.linked) {
1898
+ prompter.write(paint.warning(`! ${PACKAGE_NAME} is not installed in the runtime directory yet.
1899
+ `));
1900
+ if (await prompter.askYesNo(`Install ${PACKAGE_NAME} now?`, true)) {
1901
+ prompter.write(`Installing ${PACKAGE_NAME}@latest...
1902
+ `);
1945
1903
  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);
1904
+ await registry.installRuntime();
1950
1905
  } 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");
1906
+ prompter.write(`${paint.error("Failed")} ${plainText(error instanceof Error ? error.message : "Installation failed.")}
1907
+ `);
1908
+ await prompter.acknowledge();
1955
1909
  }
1910
+ plan = registry.planMigration();
1911
+ writePlan(prompter, registry, plan);
1956
1912
  }
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
1913
  }
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");
1914
+ if (plan.kinds.some((item) => item.status === "blocked")) {
1915
+ prompter.write(paint.warning("! Resolve blocked kinds before migrating. No migration was applied.\n"));
1975
1916
  await prompter.acknowledge();
1976
1917
  return;
1977
1918
  }
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");
1919
+ if (plan.steps.every((step) => step.id === "single_package") && plan.launches.every((item) => !item.ready)) {
1920
+ prompter.write(`Nothing can be migrated until ${PACKAGE_NAME} is installed. Install it from Runtime, then reopen Migrate registry.
1921
+ `);
1998
1922
  await prompter.acknowledge();
1999
1923
  return;
2000
1924
  }
2001
- if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
1925
+ if (!await prompter.askYesNo("Migrate the registry and apply these changes?", false)) return;
2002
1926
  const results = registry.applyMigration(plan);
2003
1927
  prompter.clearScreen();
2004
1928
  prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
2005
1929
  `);
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");
1930
+ const state = registry.read();
1931
+ const launchKinds = plan.launches.filter((item) => item.ready).map((item) => item.kind);
1932
+ await registry.checkSaved(prompter, [.../* @__PURE__ */ new Set([
1933
+ ...plan.kinds.flatMap((item) => item.names),
1934
+ ...Object.entries(state.connections).filter(([, item]) => item.entry && launchKinds.includes(item.kind)).map(([name]) => name)
1935
+ ])]);
1936
+ if (plan.launches.some((item) => !item.ready)) prompter.write(paint.warning(`! Entries waiting for ${PACKAGE_NAME} keep their old launch. Install it from Runtime, then reopen Migrate registry.
1937
+ `));
1938
+ prompter.write("Reconnect affected clients to load the new launch commands. Fix startup failures first.\n");
2008
1939
  await prompter.acknowledge();
2009
1940
  } catch (error) {
2010
1941
  if (error instanceof PromptClosedError) throw error;
2011
1942
  if (error instanceof PromptBackError) return;
2012
- prompter.write(`${error instanceof Error ? error.message : "Could not migrate the registry."}
1943
+ prompter.write(`${error instanceof Error ? plainText(error.message) : "Could not migrate the registry."}
2013
1944
  `);
2014
1945
  await prompter.acknowledge();
2015
1946
  }
2016
1947
  }
2017
1948
 
1949
+ // src/modules/registration/runtime-menu.ts
1950
+ function updateTarget(installed, latest) {
1951
+ let target = null;
1952
+ for (const candidate of [PACKAGE_VERSION, latest]) {
1953
+ if (typeof candidate !== "string" || !runtimeIsOlder(installed, candidate)) continue;
1954
+ if (target == null || runtimeIsOlder(target, candidate)) target = candidate;
1955
+ }
1956
+ return target;
1957
+ }
1958
+ function runtimeNotice(registry) {
1959
+ const installed = registry.installedRuntime();
1960
+ if (!installed) return { hint: PACKAGE_NAME, notice: "Not installed" };
1961
+ const notices = [];
1962
+ const migration = migrationStatus(registry).status;
1963
+ if (migration === "ready") notices.push("Registry update available");
1964
+ else if (migration === "blocked") notices.push("Registry update blocked");
1965
+ const target = installed.linked ? null : updateTarget(installed.version, void 0);
1966
+ if (target) notices.push(`Update available ${target}`);
1967
+ const hint = `${PACKAGE_NAME} ${installed.version}${installed.linked ? " \xB7 development link" : ""}`;
1968
+ return notices.length ? { hint, notice: notices.join(" \xB7 ") } : { hint };
1969
+ }
1970
+ function connectionNames(registry, kind) {
1971
+ return Object.entries(registry.read().connections).filter(([, item]) => item.entry && (true)).map(([name]) => name);
1972
+ }
1973
+ function outdatedKinds(registry) {
1974
+ return KINDS.filter((kind) => {
1975
+ if (!registry.isSessionMode(kind) || !registry.globalRuntime(kind)) return false;
1976
+ const saved = registry.read().sessions?.[kind]?.entry;
1977
+ try {
1978
+ return saved != null && entryHash(saved) !== entryHash(registry.sessionEntry(kind));
1979
+ } catch {
1980
+ return false;
1981
+ }
1982
+ });
1983
+ }
1984
+ async function installOrUpdate(prompter, registry) {
1985
+ const paint = prompter.paint;
1986
+ prompter.clearScreen();
1987
+ prompter.write(`Installing ${paint.accent(`${PACKAGE_NAME}@latest`)} into ${plainText(runtimePrefix())}...
1988
+ `);
1989
+ try {
1990
+ const runtime = await registry.installRuntime();
1991
+ prompter.write(`${paint.success("Installed")} ${PACKAGE_NAME} ${paint.accent(runtime.version)}.
1992
+ `);
1993
+ const names = connectionNames(registry);
1994
+ if (names.length) await registry.checkSaved(prompter, names);
1995
+ prompter.write("Reconnect or restart your AI clients to load it.\n");
1996
+ if (migrationStatus(registry).status === "ready") prompter.write(paint.warning("! Registry update available. Choose Migrate registry below.\n"));
1997
+ } catch (error) {
1998
+ let detail = error instanceof Error ? error.message : "Installation failed.";
1999
+ for (const item of Object.values(registry.read().connections)) if (item.entry) detail = redactStartupText(detail, item.entry);
2000
+ prompter.write(`${paint.error("Failed")} ${plainText(detail)}
2001
+ `);
2002
+ if (/\b(?:EBUSY|EPERM|EACCES)\b/i.test(detail)) prompter.write("Close your AI clients, then retry. A running server may have locked its files.\n");
2003
+ }
2004
+ await prompter.acknowledge();
2005
+ }
2006
+ async function runtimeMenu(prompter, registry) {
2007
+ let latest;
2008
+ for (; ; ) {
2009
+ try {
2010
+ prompter.clearScreen();
2011
+ const paint = prompter.paint;
2012
+ const installed = registry.installedRuntime();
2013
+ const target = installed && !installed.linked ? updateTarget(installed.version, latest) : null;
2014
+ const legacy = registry.legacyRuntimes();
2015
+ const removable = legacy.filter((item) => !item.usedBy.length);
2016
+ const inUse = legacy.filter((item) => item.usedBy.length);
2017
+ const migration = migrationStatus(registry);
2018
+ const packages = (list) => [...new Set(list.map((item) => item.runtime.version))].map((version) => `${list.filter((item) => item.runtime.version === version).map((item) => item.runtime.packageName).join(", ")} ${version}`).join("; ");
2019
+ const row = (label, value) => prompter.write(` ${paint.dim(label.padEnd(10))}${value}
2020
+ `);
2021
+ prompter.write(`${paint.bold("Runtime")} ${PACKAGE_NAME} ${installed ? paint.accent(installed.version) : paint.warning("not installed")}${installed?.linked ? paint.dim(" development link") : ""}
2022
+ `);
2023
+ row("Location", plainText(runtimePrefix()));
2024
+ if (inUse.length) row("In use", `${paint.warning(packages(inUse))} ${paint.dim("\xB7 old packages")}`);
2025
+ if (removable.length) row("Unused", `${packages(removable)} ${paint.dim("\xB7 old packages")}`);
2026
+ if (latest === null) prompter.write(paint.warning("! Could not reach npm to check for updates.\n"));
2027
+ else if (latest !== void 0 && installed && !target) prompter.write(`${paint.success("Up to date")} ${paint.dim(`\xB7 latest ${latest}`)}
2028
+ `);
2029
+ const upgrading = legacy.length > 0 || migration.status !== "none";
2030
+ let next = null;
2031
+ if (upgrading) {
2032
+ const plan = migration.status === "none" ? null : registry.planMigration();
2033
+ const sessions = (plan?.launches ?? []).flatMap((item) => item.names.filter((name) => /^akms_/.test(name)));
2034
+ const pairs = migration.status === "ready" ? migrationVersions(registry, plan ?? void 0) : [];
2035
+ const moves = pairs.map((pair) => pair.label === "Registry" ? `registry ${pair.from} \u2192 ${pair.to}` : `${pair.from} \u2192 ${pair.to}`).join(" \xB7 ");
2036
+ const steps = [
2037
+ {
2038
+ id: "install",
2039
+ label: `Install ${PACKAGE_NAME}`,
2040
+ done: installed != null,
2041
+ blocked: false,
2042
+ detail: installed ? installed.version : "one package for every kind"
2043
+ },
2044
+ {
2045
+ id: "migrate",
2046
+ label: "Migrate registry",
2047
+ done: migration.status === "none",
2048
+ blocked: migration.status === "blocked",
2049
+ detail: migration.status === "none" ? "" : migration.status === "blocked" ? plainText(migration.detail ?? "") : moves || (sessions.length ? `moves ${sessions.join(", ")} to ${PACKAGE_NAME}` : "one akms_<kind> entry per kind")
2050
+ },
2051
+ {
2052
+ id: "remove",
2053
+ label: "Remove old packages",
2054
+ done: legacy.length === 0,
2055
+ blocked: false,
2056
+ detail: inUse.length && migration.status === "none" ? "run Sync first: an AI service still starts them" : "then restart your AI clients"
2057
+ }
2058
+ ];
2059
+ prompter.write(`
2060
+ ${paint.bold("Upgrade steps")}
2061
+ `);
2062
+ for (const [index, step] of steps.entries()) {
2063
+ let state = paint.dim("later ");
2064
+ if (step.done) state = paint.success("done ");
2065
+ else if (step.blocked) state = paint.error("blocked");
2066
+ else if (next == null) {
2067
+ state = paint.warning("next ");
2068
+ next = step.id;
2069
+ }
2070
+ const label = step.done ? paint.dim(step.label.padEnd(22)) : step.id === next ? paint.bold(step.label.padEnd(22)) : step.label.padEnd(22);
2071
+ prompter.write(` ${paint.dim(`${index + 1}`)} ${label}${state} ${step.id === next ? step.detail : paint.dim(step.detail)}
2072
+ `);
2073
+ }
2074
+ }
2075
+ prompter.write("\n");
2076
+ const items = [];
2077
+ if (!installed) items.push({ value: "install", key: "install", label: `Install ${PACKAGE_NAME}`, hint: upgrading ? "step 1" : "one package for every kind" });
2078
+ else if (target) items.push({ value: "update", key: "update", label: `Update to ${target}`, hint: `installed ${installed.version}` });
2079
+ else if (!installed.linked) items.push({ value: "check", key: "check", label: "Check for updates", hint: "asks npm for the latest release" });
2080
+ if (migration.status === "ready") {
2081
+ const pairs = migrationVersions(registry).map((pair) => pair.label === "Registry" ? { from: `registry ${pair.from}`, to: pair.to } : pair);
2082
+ const last = pairs.at(-1);
2083
+ items.push({
2084
+ value: "migrate",
2085
+ key: "migrate",
2086
+ label: "Migrate registry",
2087
+ hint: last ? ["step 2", ...pairs.slice(0, -1).map((pair) => `${pair.from} \u2192 ${pair.to}`), `${last.from} \u2192`].join(" \xB7 ") : "step 2 \xB7 one akms_<kind> entry per kind",
2088
+ ...last ? { notice: last.to, noticeTone: "success" } : {}
2089
+ });
2090
+ }
2091
+ const outdated = migration.status === "none" ? outdatedKinds(registry) : [];
2092
+ if (outdated.length) items.push({ value: "relaunch", key: "relaunch", label: "Update launch commands", hint: outdated.map(sessionName).join(", "), notice: "out of date" });
2093
+ if (removable.length) {
2094
+ items.push({
2095
+ value: "remove",
2096
+ key: "remove",
2097
+ label: inUse.length ? "Remove unused packages" : "Remove old packages",
2098
+ hint: `${inUse.length ? "" : "step 3 \xB7 "}${removable.map((item) => item.runtime.packageName).join(", ")}`
2099
+ });
2100
+ }
2101
+ items.push({ value: "back", key: "b", label: "Back" });
2102
+ const defaultValue = next === "migrate" && migration.status === "ready" ? "migrate" : next === "remove" && removable.length && !inUse.length ? "remove" : void 0;
2103
+ const picked = await prompter.select("Runtime", items, { cancelValue: "back", ...defaultValue ? { defaultValue } : {} });
2104
+ if (picked === "back") return;
2105
+ if (picked === "migrate") {
2106
+ await migrationMenu(prompter, registry);
2107
+ continue;
2108
+ }
2109
+ if (picked === "install" || picked === "update") {
2110
+ await installOrUpdate(prompter, registry);
2111
+ latest = void 0;
2112
+ continue;
2113
+ }
2114
+ if (picked === "check") {
2115
+ prompter.write(`Checking the latest ${PACKAGE_NAME} release...
2116
+ `);
2117
+ latest = await registry.latestRuntime();
2118
+ continue;
2119
+ }
2120
+ if (picked === "relaunch") {
2121
+ await updateLaunchCommands(prompter, registry, outdated);
2122
+ continue;
2123
+ }
2124
+ if (picked === "remove") {
2125
+ prompter.clearScreen();
2126
+ prompter.write(`${paint.bold("Remove old packages")}
2127
+ `);
2128
+ for (const item of removable) prompter.write(` ${item.runtime.packageName} ${paint.dim(`${item.runtime.version}${item.runtime.linked ? " \xB7 development link (the checkout stays)" : ""}`)}
2129
+ `);
2130
+ prompter.write(paint.dim("No AI service starts them any more; every kind now runs from @akms/mcp.\n"));
2131
+ if (!await prompter.askYesNo("Remove these packages?", false)) continue;
2132
+ await registry.removeLegacyRuntimes(removable.map((item) => item.runtime.kind));
2133
+ prompter.write(`${paint.success("Removed")} ${removable.map((item) => item.runtime.packageName).join(", ")}.
2134
+ `);
2135
+ await prompter.acknowledge();
2136
+ }
2137
+ } catch (error) {
2138
+ if (error instanceof PromptClosedError) throw error;
2139
+ if (error instanceof PromptBackError) continue;
2140
+ prompter.write(`${error instanceof Error ? plainText(error.message) : "Could not configure the AKMS runtime."}
2141
+ `);
2142
+ await prompter.acknowledge();
2143
+ }
2144
+ }
2145
+ }
2146
+ async function updateLaunchCommands(prompter, registry, kinds) {
2147
+ const paint = prompter.paint;
2148
+ prompter.clearScreen();
2149
+ prompter.write(`${paint.bold("Update launch commands")}
2150
+ `);
2151
+ for (const kind of kinds) prompter.write(` ${paint.dim(sessionName(kind).padEnd(10))}${paint.accent(plainText(registry.sessionEntry(kind).args[0]))} --catalog
2152
+ `);
2153
+ prompter.write(paint.dim("Only the command that starts each entry changes; connection settings stay in the catalog.\n"));
2154
+ if (!await prompter.askYesNo("Update these entries?", true)) return;
2155
+ const results = [];
2156
+ for (const kind of kinds) results.push(...registry.applySession(registry.planSession(kind, "catalog")));
2157
+ prompter.clearScreen();
2158
+ prompter.write(`${registry.formatResults(results, "Launch commands saved.", prompter.paint)}
2159
+ `);
2160
+ const connections = registry.read().connections;
2161
+ await registry.checkSaved(prompter, connectionNames(registry).filter((name) => kinds.includes(connections[name].kind)));
2162
+ prompter.write("Restart or reconnect your AI clients to use them.\n");
2163
+ await prompter.acknowledge();
2164
+ }
2165
+ var screenText = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, " ");
2166
+ var countLabel = (count, noun, plural = `${noun}s`) => `${count} ${count === 1 ? noun : plural}`;
2167
+ function writeHeading(screen, title) {
2168
+ screen.write(`${screen.paint.bold(screenText(title))}
2169
+ `);
2170
+ }
2171
+ function writeField(screen, label, value, note = "", tone = "accent") {
2172
+ const paint = screen.paint;
2173
+ const key = screenText(label);
2174
+ const padded = key.padEnd(Math.max(13, key.length + 2));
2175
+ screen.write(` ${paint.dim(padded)}${paint[tone](screenText(value))}${note ? ` ${paint.dim(screenText(note))}` : ""}
2176
+ `);
2177
+ }
2178
+ function writeHint(screen, text) {
2179
+ screen.write(`${screen.paint.dim(screenText(text))}
2180
+ `);
2181
+ }
2182
+ function writeWarning(screen, text) {
2183
+ screen.write(`${screen.paint.warning(`! ${screenText(text)}`)}
2184
+ `);
2185
+ }
2186
+
2018
2187
  // src/modules/registration/menu.ts
2019
2188
  function pendingImports(registry, kind) {
2020
2189
  const state = registry.read();
@@ -2024,11 +2193,10 @@ function pendingImports(registry, kind) {
2024
2193
  }
2025
2194
  function preview(prompter, entry) {
2026
2195
  if (!entry) {
2027
- prompter.write(" Remove this connection from the service.\n");
2196
+ writeField(prompter, "Action", "Remove from service", "", "warning");
2028
2197
  return;
2029
2198
  }
2030
- for (const [key, value] of Object.entries(entry.env)) prompter.write(` ${plainText(key)}: ${visibleField.test(key) ? plainText(value) : "(hidden)"}
2031
- `);
2199
+ for (const [key, value] of Object.entries(entry.env)) writeField(prompter, key, visibleField.test(key) ? value : "(hidden)", "", visibleField.test(key) ? "accent" : "dim");
2032
2200
  }
2033
2201
  async function registrationMenu(prompter, registry, initialClient) {
2034
2202
  const initial = registry.read();
@@ -2040,9 +2208,9 @@ async function registrationMenu(prompter, registry, initialClient) {
2040
2208
  for (; ; ) {
2041
2209
  try {
2042
2210
  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
- `);
2211
+ writeHeading(prompter, "Register \xB7 AI services");
2212
+ writeHint(prompter, "Toggle services, then Apply. Matching connections are imported; differences stay for review.");
2213
+ if (discovered.errors.length) writeWarning(prompter, discovered.errors.join(" "));
2046
2214
  const picked = await prompter.select("Register", [
2047
2215
  ...registry.targets.filter((target) => target.id !== "registered").map((target) => ({
2048
2216
  value: target.id,
@@ -2062,64 +2230,59 @@ async function registrationMenu(prompter, registry, initialClient) {
2062
2230
  continue;
2063
2231
  }
2064
2232
  const removed = initial.clients.filter((id) => !selected.has(id));
2065
- const count = Object.values(registry.read().connections).filter((item) => item.entry).length;
2233
+ const current = registry.read();
2066
2234
  const plan = registry.planRegistration([...selected]);
2067
2235
  const importNames = [...new Set(plan.imports.map((entry) => entry.name))];
2236
+ const connections = new Map(Object.entries(current.connections).filter(([, item]) => item.entry).map(([name, item]) => [name, item.kind]));
2237
+ for (const item of plan.imports) connections.set(item.name, item.kind);
2238
+ const kinds = KINDS.filter((kind) => [...connections.values()].includes(kind));
2239
+ const sessions = kinds.filter((kind) => registry.isSessionMode(kind, current)).map(sessionName);
2240
+ const individuals = [...connections].filter(([, kind]) => !registry.isSessionMode(kind, current)).map(([name]) => name);
2241
+ const imported = new Set(plan.imports.map((item) => `${item.client}:${item.name}:${entryHash(item.entry)}`));
2242
+ const retiring = discovered.entries.filter((item) => selected.has(item.client) && connections.has(item.name) && registry.isSessionMode(item.kind, current));
2243
+ const owned = (item) => imported.has(`${item.client}:${item.name}:${entryHash(item.entry)}`) || Object.hasOwn(current.connections, item.name) && current.connections[item.name].applied[item.client] === entryHash(item.entry);
2244
+ const replaces = [...new Set(retiring.filter(owned).map((item) => item.name))];
2245
+ const kept = [.../* @__PURE__ */ new Set([...plan.conflicts, ...retiring.filter((item) => !owned(item)).map((item) => item.name)])];
2068
2246
  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
- `);
2247
+ writeHeading(prompter, "Review registration");
2248
+ writeField(prompter, "Services", [...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none");
2249
+ writeField(prompter, "Connections", String(connections.size), kinds.length ? `(${kinds.map((kind) => `${KIND_LABELS[kind]} ${[...connections.values()].filter((value) => value === kind).length}`).join(" \xB7 ")})` : "");
2250
+ writeField(prompter, "Entries", [...sessions, ...individuals.length ? [`${individuals.length} individual entries`] : []].join(" \xB7 ") || "none", "in each service");
2251
+ if (replaces.length) writeField(prompter, "Replaces", `${replaces.length} individual entries`, `with ${sessions.join(" \xB7 ")}`);
2252
+ if (importNames.length) writeField(prompter, "Import", countLabel(importNames.length, "matching connection"));
2253
+ if (kept.length) writeField(prompter, "Kept", `${kept.length} need a version choice`, kept.join(", "), "warning");
2254
+ if (plan.errors.length) writeField(prompter, "Unavailable", countLabel(plan.errors.length, "service"), plan.errors.join(" "), "error");
2082
2255
  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");
2256
+ if (plan.imports.some((item) => !registry.globalRuntime(item.kind))) writeField(prompter, "Runtime", "Waiting", "install from Runtime, then Sync; existing entries stay", "warning");
2257
+ if (plan.imports.some((item) => !standardLaunch(item.entry, item.kind))) writeHint(prompter, "Custom commands are not started; the grouped entry loads their saved settings.");
2086
2258
  }
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
- `);
2259
+ if (removed.length) writeField(prompter, "Unregister", removed.map((id) => CLIENT_LABELS2[id]).join(" \xB7 "), "remove managed entries; keep the catalog");
2090
2260
  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");
2261
+ writeWarning(prompter, `Plaintext passwords: ${registry.file} + registry.previous.json. Do not share, sync or commit.`);
2094
2262
  privacyNoticeShown = true;
2095
2263
  }
2096
2264
  if (!await prompter.askYesNo("Apply this registration?", removed.length === 0)) continue;
2097
2265
  const results = registry.register([...selected], initial.clients, plan.imports);
2098
2266
  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.
2267
+ writeHeading(prompter, "Registration saved.");
2268
+ writeField(prompter, "Services", [...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none");
2269
+ prompter.write(`${registry.formatResults(results, "", prompter.paint)}
2104
2270
  `);
2271
+ if (importNames.length) writeField(prompter, "Imported", countLabel(importNames.length, "connection"), "saved in the shared catalog", "success");
2105
2272
  try {
2106
2273
  const conflicts = new Set(results.filter((result) => result.status === "conflict").map((result) => result.name));
2107
2274
  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));
2275
+ const current2 = registry.read();
2276
+ const remaining = registry.candidates().entries.filter((candidate) => !Object.hasOwn(current2.connections, candidate.name));
2110
2277
  const names = [...new Set(remaining.map((candidate) => candidate.name))];
2111
2278
  if (names.length) {
2112
- prompter.write(`
2113
- ${names.length} existing connection(s) still need a choice before joining the shared list:
2114
- `);
2279
+ writeField(prompter, "Not imported", countLabel(names.length, "connection"), "choose a version", "warning");
2115
2280
  for (const name of names) {
2116
2281
  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
- `);
2282
+ writeField(prompter, name, copies.map((copy) => CLIENT_LABELS2[copy.client]).join(" \xB7 "));
2283
+ if (new Set(copies.map((copy) => entryHash(copy.entry))).size > 1) writeHint(prompter, ` ${describeDifferences(copies.map((copy) => copy.entry))}`);
2121
2284
  }
2122
- prompter.write("Their existing settings are unchanged. Compare them now, or use Import existing connections later.\n");
2285
+ writeHint(prompter, "Existing settings stay. Compare now or open Import existing connections later.");
2123
2286
  if (await prompter.askYesNo("Compare existing settings and choose which to share?", true)) await importMenu(prompter, registry);
2124
2287
  }
2125
2288
  if (!conflicts.size && !names.length) await prompter.acknowledge();
@@ -2130,8 +2293,7 @@ ${names.length} existing connection(s) still need a choice before joining the sh
2130
2293
  } catch (error) {
2131
2294
  if (error instanceof PromptClosedError) throw error;
2132
2295
  if (error instanceof PromptBackError) continue;
2133
- prompter.write(` ${error instanceof Error ? error.message : "Registration could not be saved."}
2134
- `);
2296
+ writeWarning(prompter, error instanceof Error ? error.message : "Registration could not be saved.");
2135
2297
  await prompter.acknowledge();
2136
2298
  }
2137
2299
  }
@@ -2139,10 +2301,9 @@ ${names.length} existing connection(s) still need a choice before joining the sh
2139
2301
  async function importMenu(prompter, registry, kind) {
2140
2302
  for (; ; ) {
2141
2303
  prompter.clearScreen();
2142
- prompter.write("Import existing connections\n\n");
2304
+ writeHeading(prompter, "Import existing connections");
2143
2305
  const { entries, errors } = registry.candidates(kind);
2144
- for (const error of errors) prompter.write(` ${error}
2145
- `);
2306
+ for (const error of errors) writeWarning(prompter, error);
2146
2307
  const state = registry.read();
2147
2308
  const matching = registry.planRegistration(state.clients, kind);
2148
2309
  const matchingNames = [...new Set(matching.imports.map((entry) => entry.name))];
@@ -2151,10 +2312,11 @@ async function importMenu(prompter, registry, kind) {
2151
2312
  return !current?.entry || current.applied[candidate.client] !== entryHash(candidate.entry) || entryHash(current.entry) !== entryHash(candidate.entry);
2152
2313
  }).map((entry) => entry.name))];
2153
2314
  if (!names.length) {
2154
- prompter.write(" No existing connections need importing.\n");
2315
+ writeHint(prompter, "No existing connections need importing.");
2155
2316
  await prompter.acknowledge();
2156
2317
  return;
2157
2318
  }
2319
+ writeField(prompter, "Available", countLabel(names.length, "connection"), `${matchingNames.length} matching`);
2158
2320
  const picked = await prompter.select("Import existing connections", [
2159
2321
  ...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
2322
  ...matchingNames.length ? [{ value: "matching", key: "all", label: "Import all matching connections", hint: `${matchingNames.length} connections; differing versions stay unchanged` }] : [],
@@ -2164,10 +2326,9 @@ async function importMenu(prompter, registry, kind) {
2164
2326
  try {
2165
2327
  prompter.clearScreen();
2166
2328
  if (picked === "matching") {
2167
- prompter.write(`
2168
- Import existing connections:
2169
- ${matchingNames.join("\n ")}
2170
- `);
2329
+ writeHeading(prompter, "Review import");
2330
+ writeField(prompter, "Connections", String(matchingNames.length), matchingNames.join(", "));
2331
+ writeField(prompter, "Services", state.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ") || "none");
2171
2332
  if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
2172
2333
  const results2 = registry.register(state.clients, state.clients, matching.imports);
2173
2334
  prompter.clearScreen();
@@ -2181,9 +2342,8 @@ Import existing connections:
2181
2342
  const candidates = entries.filter((entry) => entry.name === name);
2182
2343
  let chosen = candidates[0];
2183
2344
  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
- `);
2345
+ writeHeading(prompter, `Compare ${name}`);
2346
+ writeField(prompter, "Services", state.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ") || "none");
2187
2347
  compareEntries(prompter, candidates.map((candidate) => ({ label: CLIENT_LABELS2[candidate.client], entry: candidate.entry })));
2188
2348
  const choice = await prompter.select("Use settings from", [
2189
2349
  ...candidates.map((candidate, index) => ({ value: String(index), label: `Use ${index + 1}: ${CLIENT_LABELS2[candidate.client]}` })),
@@ -2192,22 +2352,23 @@ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2192
2352
  if (choice === "back") continue;
2193
2353
  chosen = candidates[Number(choice)];
2194
2354
  }
2355
+ writeField(prompter, "Connection", name);
2356
+ writeField(prompter, "Version", CLIENT_LABELS2[chosen.client]);
2195
2357
  preview(prompter, chosen.entry);
2196
2358
  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");
2359
+ writeField(prompter, "Apply to", state.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ") || "none");
2198
2360
  if (!await prompter.askYesNo(`Import ${destination} and apply it?`, true)) continue;
2199
2361
  const results = registry.importEntry(destination, chosen, candidates);
2200
2362
  prompter.clearScreen();
2201
2363
  prompter.write(`
2202
2364
  ${registry.formatResults(results, "Import complete", prompter.paint)}
2203
2365
  `);
2204
- if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
2366
+ if (destination !== name) writeField(prompter, "Kept", name, "original entries retain this name", "warning");
2205
2367
  await prompter.acknowledge();
2206
2368
  } catch (error) {
2207
2369
  if (error instanceof PromptClosedError) throw error;
2208
2370
  if (error instanceof PromptBackError) continue;
2209
- prompter.write(` ${error instanceof Error ? error.message : "Import failed."}
2210
- `);
2371
+ writeWarning(prompter, error instanceof Error ? error.message : "Import failed.");
2211
2372
  await prompter.acknowledge();
2212
2373
  }
2213
2374
  }
@@ -2222,9 +2383,7 @@ async function syncMenu(prompter, registry) {
2222
2383
  ${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
2223
2384
  `);
2224
2385
  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
- `));
2386
+ if (imports.names.length) writeField(prompter, "Not imported", countLabel(imports.names.length, "connection"), `${imports.differing === imports.names.length ? "different settings \xB7 " : ""}choose Import existing connections`, "warning");
2228
2387
  const conflicts = results.filter((result) => result.status === "conflict");
2229
2388
  if (!conflicts.length && !imports.names.length) {
2230
2389
  await prompter.acknowledge();
@@ -2249,12 +2408,14 @@ Not yet imported: ${imports.names.length} connection(s)${imports.differing === i
2249
2408
  const session = state.sessions[sessionKind];
2250
2409
  const actual2 = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2251
2410
  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");
2411
+ writeWarning(prompter, "This name belongs to another server. Rename its external entry; existing settings stay.");
2253
2412
  await prompter.acknowledge();
2254
2413
  continue;
2255
2414
  }
2256
2415
  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]}`);
2416
+ writeHeading(prompter, "Review session launch");
2417
+ writeField(prompter, "Server", conflict.name);
2418
+ writeField(prompter, "Service", CLIENT_LABELS2[conflict.client]);
2258
2419
  compareEntries(prompter, [{ label: "AKMS session launch", entry: session.entry }, { label: "External settings", entry: actual2 }]);
2259
2420
  const choice2 = await prompter.select("Resolve session launch", [
2260
2421
  { value: "apply", label: removing2 ? "Remove the old session entry" : "Restore the AKMS session launch" },
@@ -2267,7 +2428,7 @@ Not yet imported: ${imports.names.length} connection(s)${imports.differing === i
2267
2428
  if (!connection) throw new Error("This connection changed. Reopen Sync to review its current settings.");
2268
2429
  const actual = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2269
2430
  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");
2431
+ writeWarning(prompter, "This name belongs to another server. Rename that entry or the shared connection.");
2271
2432
  await prompter.acknowledge();
2272
2433
  continue;
2273
2434
  }
@@ -2277,21 +2438,19 @@ Not yet imported: ${imports.names.length} connection(s)${imports.differing === i
2277
2438
  const removing = !connection.entry || !registered;
2278
2439
  const retiring = sessionMode && !removing;
2279
2440
  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"}`);
2441
+ writeHeading(prompter, `Sync / ${conflict.name}`);
2442
+ writeField(prompter, "Services", state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none");
2282
2443
  prompter.write("\n");
2283
2444
  if (!removing && connection.entry) {
2284
2445
  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.");
2446
+ writeField(prompter, "Server", sessionName(connection.kind));
2447
+ writeField(prompter, "Kept in", client, "old entry stays until this server is ready", "warning");
2287
2448
  }
2288
2449
  compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
2289
2450
  } 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
- `);
2451
+ writeField(prompter, "Action", !registered ? `${client} unregistered` : "Remove connection", "", "warning");
2452
+ writeHint(prompter, `Settings changed in ${client}; choose what to keep.`);
2453
+ writeHeading(prompter, `Currently in ${client}`);
2295
2454
  preview(prompter, actual);
2296
2455
  prompter.write("\n");
2297
2456
  }
@@ -2304,10 +2463,10 @@ Currently in ${client}
2304
2463
  if (choice === "back") continue;
2305
2464
  prompter.write("\n");
2306
2465
  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.`);
2466
+ writeField(prompter, "Save version", client, "in AKMS");
2467
+ writeField(prompter, "Apply to", state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 "));
2468
+ } else writeField(prompter, removing ? "Remove" : "Keep version", removing ? `${conflict.name} in ${client}` : "AKMS", removing ? "" : retiring ? `for ${sessionName(connection.kind)}` : `apply ${subject} to ${client}`);
2469
+ if (retiring) writeHint(prompter, `The old ${client} entry stays until ${sessionName(connection.kind)} is ready.`);
2311
2470
  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
2471
  if (await prompter.askYesNo(confirmation, false)) {
2313
2472
  pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
@@ -2315,29 +2474,15 @@ Currently in ${client}
2315
2474
  } catch (error) {
2316
2475
  if (error instanceof PromptClosedError) throw error;
2317
2476
  if (error instanceof PromptBackError) continue;
2318
- prompter.write(` ${error instanceof Error ? error.message : "Could not resolve the difference."}
2477
+ prompter.write(`${prompter.paint.error("Failed")} ${screenText(error instanceof Error ? error.message : "Could not resolve the difference.")}
2319
2478
  `);
2320
2479
  await prompter.acknowledge();
2321
2480
  }
2322
2481
  }
2323
2482
  }
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
2483
  function findExecutable(name) {
2339
2484
  const searchPath = process.env.PATH ?? "";
2340
- const directories = searchPath.split(path6.delimiter).filter((directory) => directory !== "");
2485
+ const directories = searchPath.split(path2.delimiter).filter((directory) => directory !== "");
2341
2486
  let suffixes = [""];
2342
2487
  if (process.platform === "win32") {
2343
2488
  const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
@@ -2345,25 +2490,17 @@ function findExecutable(name) {
2345
2490
  }
2346
2491
  for (const directory of directories) {
2347
2492
  for (const suffix of suffixes) {
2348
- const candidate = path6.join(directory, `${name}${suffix}`);
2349
- if (fs5.existsSync(candidate) == true && fs5.statSync(candidate).isFile() == true) {
2493
+ const candidate = path2.join(directory, `${name}${suffix}`);
2494
+ if (fs.existsSync(candidate) == true && fs.statSync(candidate).isFile() == true) {
2350
2495
  return candidate;
2351
2496
  }
2352
2497
  }
2353
2498
  }
2354
2499
  return null;
2355
2500
  }
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
2501
  function readExisting2(filePath) {
2365
2502
  try {
2366
- return fs6.readFileSync(filePath, "utf8");
2503
+ return fs.readFileSync(filePath, "utf8");
2367
2504
  } catch (error) {
2368
2505
  if (error.code === "ENOENT") {
2369
2506
  return null;
@@ -2375,45 +2512,45 @@ function updateConfigFile2(filePath, update) {
2375
2512
  let destination = filePath;
2376
2513
  let symbolicLink = false;
2377
2514
  try {
2378
- symbolicLink = fs6.lstatSync(filePath).isSymbolicLink();
2515
+ symbolicLink = fs.lstatSync(filePath).isSymbolicLink();
2379
2516
  } catch (error) {
2380
2517
  if (error.code !== "ENOENT") {
2381
2518
  throw error;
2382
2519
  }
2383
2520
  }
2384
2521
  if (symbolicLink) {
2385
- destination = fs6.realpathSync(filePath);
2522
+ destination = fs.realpathSync(filePath);
2386
2523
  }
2387
2524
  const before = readExisting2(destination);
2388
2525
  const updated = update(before ?? "");
2389
2526
  if (before != null) {
2390
- fs6.accessSync(destination, fs6.constants.W_OK);
2527
+ fs.accessSync(destination, fs.constants.W_OK);
2391
2528
  }
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;
2529
+ fs.mkdirSync(path2.dirname(destination), { recursive: true });
2530
+ const temporary = path2.join(path2.dirname(destination), `.${path2.basename(destination)}.${randomUUID()}.tmp`);
2531
+ const mode = before == null ? 384 : fs.statSync(destination).mode & 511;
2395
2532
  let descriptor;
2396
2533
  let ownsTemporary = false;
2397
2534
  try {
2398
- descriptor = fs6.openSync(temporary, "wx", mode);
2535
+ descriptor = fs.openSync(temporary, "wx", mode);
2399
2536
  ownsTemporary = true;
2400
- fs6.writeFileSync(descriptor, updated, "utf8");
2401
- fs6.fsyncSync(descriptor);
2402
- fs6.closeSync(descriptor);
2537
+ fs.writeFileSync(descriptor, updated, "utf8");
2538
+ fs.fsyncSync(descriptor);
2539
+ fs.closeSync(descriptor);
2403
2540
  descriptor = void 0;
2404
2541
  if (readExisting2(destination) !== before) {
2405
2542
  throw new Error("The config changed while saving; reload it and try again.");
2406
2543
  }
2407
- fs6.renameSync(temporary, destination);
2544
+ fs.renameSync(temporary, destination);
2408
2545
  ownsTemporary = false;
2409
2546
  } finally {
2410
2547
  try {
2411
2548
  if (descriptor != null) {
2412
- fs6.closeSync(descriptor);
2549
+ fs.closeSync(descriptor);
2413
2550
  }
2414
2551
  } finally {
2415
2552
  if (ownsTemporary) {
2416
- fs6.unlinkSync(temporary);
2553
+ fs.unlinkSync(temporary);
2417
2554
  }
2418
2555
  }
2419
2556
  }
@@ -2466,10 +2603,10 @@ function readMcpServersJson(document) {
2466
2603
  return entries;
2467
2604
  }
2468
2605
  function readDocument(filePath, contents) {
2469
- if (contents == null && fs7.existsSync(filePath) == false) {
2606
+ if (contents == null && fs.existsSync(filePath) == false) {
2470
2607
  return {};
2471
2608
  }
2472
- const text = contents ?? fs7.readFileSync(filePath, "utf8");
2609
+ const text = contents ?? fs.readFileSync(filePath, "utf8");
2473
2610
  if (text.trim() === "") {
2474
2611
  return {};
2475
2612
  }
@@ -2494,7 +2631,7 @@ function createJsonFileTarget(options) {
2494
2631
  id,
2495
2632
  label,
2496
2633
  location: filePath,
2497
- detected: fs7.existsSync(filePath),
2634
+ detected: fs.existsSync(filePath),
2498
2635
  readEntries() {
2499
2636
  const document = readDocument(filePath);
2500
2637
  return readMcpServersJson(document);
@@ -2525,7 +2662,7 @@ function createJsonFileTarget(options) {
2525
2662
  }
2526
2663
 
2527
2664
  // src/modules/registration/targets/claude-code.ts
2528
- var CLAUDE_CODE_CONFIG_PATH = path8.join(os3.homedir(), ".claude.json");
2665
+ var CLAUDE_CODE_CONFIG_PATH = path2.join(os5.homedir(), ".claude.json");
2529
2666
  var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
2530
2667
  function toClaudeAddJsonPayload(entry) {
2531
2668
  return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
@@ -2567,10 +2704,10 @@ function runClaude(executable, args) {
2567
2704
  return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
2568
2705
  }
2569
2706
  function readRawClaudeEntries() {
2570
- if (fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
2707
+ if (fs.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
2571
2708
  return {};
2572
2709
  }
2573
- const text = fs8.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
2710
+ const text = fs.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
2574
2711
  let document;
2575
2712
  try {
2576
2713
  document = JSON.parse(text);
@@ -2589,7 +2726,7 @@ function createClaudeCodeTarget() {
2589
2726
  id: "claude-code",
2590
2727
  label: "Claude Code (user scope)",
2591
2728
  location: CLAUDE_CODE_CONFIG_PATH,
2592
- detected: executable != null || fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
2729
+ detected: executable != null || fs.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
2593
2730
  readEntries() {
2594
2731
  return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
2595
2732
  },
@@ -2634,11 +2771,6 @@ function createClaudeCodeTarget() {
2634
2771
  };
2635
2772
  }
2636
2773
 
2637
- // src/modules/registration/targets/codex.ts
2638
- import fs9 from "fs";
2639
- import os4 from "os";
2640
- import path9 from "path";
2641
-
2642
2774
  // src/modules/registration/targets/toml-lines.ts
2643
2775
  function lineState() {
2644
2776
  return { quote: null, multiline: false, depth: 0 };
@@ -2678,10 +2810,6 @@ function scanTomlLine(line, state) {
2678
2810
  }
2679
2811
  }
2680
2812
  }
2681
-
2682
- // src/modules/registration/targets/toml-validation.ts
2683
- import { isDeepStrictEqual } from "util";
2684
- import { parse, TomlError } from "smol-toml";
2685
2813
  function isTable(value) {
2686
2814
  return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
2687
2815
  }
@@ -2932,18 +3060,18 @@ function editMcpServerToml(text, name, entry) {
2932
3060
 
2933
3061
  // src/modules/registration/targets/codex.ts
2934
3062
  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");
3063
+ const CODEX_CONFIG_PATH = path2.join(process.env.CODEX_HOME ? path2.resolve(process.env.CODEX_HOME) : path2.join(os5.homedir(), ".codex"), "config.toml");
2936
3064
  function readText() {
2937
- if (fs9.existsSync(CODEX_CONFIG_PATH) == false) {
3065
+ if (fs.existsSync(CODEX_CONFIG_PATH) == false) {
2938
3066
  return "";
2939
3067
  }
2940
- return fs9.readFileSync(CODEX_CONFIG_PATH, "utf8");
3068
+ return fs.readFileSync(CODEX_CONFIG_PATH, "utf8");
2941
3069
  }
2942
3070
  return {
2943
3071
  id: "codex",
2944
3072
  label: "Codex CLI",
2945
3073
  location: CODEX_CONFIG_PATH,
2946
- detected: findExecutable("codex") != null || fs9.existsSync(CODEX_CONFIG_PATH) == true,
3074
+ detected: findExecutable("codex") != null || fs.existsSync(CODEX_CONFIG_PATH) == true,
2947
3075
  readEntries() {
2948
3076
  const text = readText();
2949
3077
  return parseMcpServersToml(text);
@@ -2971,20 +3099,20 @@ function createCodexTarget() {
2971
3099
  // src/modules/registration/targets/index.ts
2972
3100
  function claudeDesktopConfigPath() {
2973
3101
  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");
3102
+ const appData = process.env.APPDATA ?? path2.join(os5.homedir(), "AppData", "Roaming");
3103
+ return path2.join(appData, "Claude", "claude_desktop_config.json");
2976
3104
  }
2977
3105
  if (process.platform === "darwin") {
2978
- return path10.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
3106
+ return path2.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2979
3107
  }
2980
- return path10.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
3108
+ return path2.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
2981
3109
  }
2982
3110
  function listConfigTargets() {
2983
3111
  const targets = [
2984
3112
  createClaudeCodeTarget(),
2985
3113
  createCodexTarget(),
2986
3114
  createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
2987
- createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path10.join(os5.homedir(), ".cursor", "mcp.json") })
3115
+ createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path2.join(os5.homedir(), ".cursor", "mcp.json") })
2988
3116
  ];
2989
3117
  const detected = targets.filter((target) => target.detected == true);
2990
3118
  const others = targets.filter((target) => target.detected == false);
@@ -3026,8 +3154,7 @@ ${failure}
3026
3154
  prompter.clearScreen();
3027
3155
  const state = registry.read();
3028
3156
  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"));
3157
+ const runtime = runtimeNotice(registry);
3031
3158
  const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
3032
3159
  { value: "db", label: "Database", hint: "PostgreSQL connections" },
3033
3160
  { value: "ssh", label: "SSH", hint: "Remote hosts" },
@@ -3036,20 +3163,16 @@ ${failure}
3036
3163
  { separator: true },
3037
3164
  { value: "register", label: "Register", hint: clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "Choose AI services once" },
3038
3165
  { 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" }] : [],
3166
+ { value: "runtime", label: "Runtime", hint: runtime.hint, notice: runtime.notice },
3042
3167
  { value: "quit", label: "Quit" }
3043
3168
  ], { defaultValue, cancelValue: "quit" });
3044
- if (picked !== "register" && picked !== "sync" && picked !== "runtime" && picked !== "updates" && picked !== "migrate") {
3169
+ if (picked !== "register" && picked !== "sync" && picked !== "runtime") {
3045
3170
  selected = picked;
3046
3171
  break;
3047
3172
  }
3048
3173
  try {
3049
3174
  if (picked === "register") await registrationMenu(prompter, registry, options.targetId);
3050
3175
  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
3176
  else await syncMenu(prompter, registry);
3054
3177
  } catch (error) {
3055
3178
  if (error instanceof PromptClosedError) throw error;
@@ -3078,66 +3201,18 @@ ${error instanceof Error ? error.message : "Could not update settings."}
3078
3201
  }
3079
3202
  }
3080
3203
  }
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"));
3204
+ function bundledEntry(kind) {
3205
+ let directory = dirname(fileURLToPath(import.meta.url));
3206
+ for (; ; ) {
3207
+ const manifest = join(directory, "package.json");
3208
+ if (existsSync(manifest) == true && JSON.parse(readFileSync(manifest, "utf8")).name === PACKAGE_NAME) {
3209
+ return { command: process.execPath, args: [join(directory, "lib", `${kind}.js`)] };
3106
3210
  }
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;
3211
+ const parent = dirname(directory);
3212
+ if (parent === directory) {
3213
+ throw new Error("The AKMS MCP package could not be located; setup was not started.");
3112
3214
  }
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 });
3215
+ directory = parent;
3141
3216
  }
3142
3217
  }
3143
3218
  async function launchMcp(options, dependencies = {}) {
@@ -3153,20 +3228,20 @@ async function launchMcp(options, dependencies = {}) {
3153
3228
  const prompter = new Prompter(dependencies.input ?? process.stdin, output, pendingInput);
3154
3229
  try {
3155
3230
  prompter.write(`
3156
- ${selected.packageName} is not installed in the AKMS runtime directory.
3231
+ ${PACKAGE_NAME} is not installed in the AKMS runtime directory.
3157
3232
  `);
3158
3233
  if (!await prompter.askYesNo("Install the AKMS runtime before opening setup?", true)) return 0;
3159
3234
  try {
3160
- prompter.write(`Installing ${selected.packageName}...
3235
+ prompter.write(`Installing ${PACKAGE_NAME}...
3161
3236
  `);
3162
- await (dependencies.installRuntime ?? installGlobalRuntime)(options.mcpId);
3237
+ await (dependencies.installRuntime ?? installGlobalRuntime)();
3163
3238
  runtime = findRuntime(options.mcpId);
3164
3239
  if (!runtime) throw new Error("Installation completed but the AKMS runtime could not be found.");
3165
3240
  } catch (error) {
3166
3241
  if (error instanceof Error && error.message === "AKMS runtime installation cancelled.") return 130;
3167
3242
  prompter.write(`${error instanceof Error ? error.message : "AKMS runtime installation failed."}
3168
3243
  `);
3169
- if (!await prompter.askYesNo("Open the latest setup with npm exec instead?", false)) return 1;
3244
+ if (!await prompter.askYesNo("Open setup from this launcher instead?", false)) return 1;
3170
3245
  }
3171
3246
  } catch (error) {
3172
3247
  if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
@@ -3176,61 +3251,16 @@ ${selected.packageName} is not installed in the AKMS runtime directory.
3176
3251
  pendingInput = prompter.takePendingInput();
3177
3252
  }
3178
3253
  }
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)" : ""}
3254
+ const launch = runtime ?? (dependencies.bundledEntry ?? bundledEntry)(options.mcpId);
3255
+ const argumentsList = [...launch.args, selected.setupCommand];
3256
+ if (options.targetId) argumentsList.push("--target", options.targetId);
3257
+ if (runtime) output.write(`Opening ${selected.label} setup \xB7 ${PACKAGE_NAME} ${runtime.version}${runtime.linked ? " (linked development checkout)" : ""}
3183
3258
 
3184
3259
  `);
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) {
3228
- argumentsList.push("--target", options.targetId);
3229
- }
3230
- output.write(`Opening ${selected.packageName} ${version} with npm exec
3260
+ else output.write(`Opening ${selected.label} setup \xB7 ${PACKAGE_NAME} ${PACKAGE_VERSION} from this launcher
3231
3261
 
3232
3262
  `);
3233
- return (dependencies.runChild ?? runChild)(process.execPath, argumentsList, pendingInput);
3263
+ return (dependencies.runChild ?? runChild)(launch.command, argumentsList, pendingInput);
3234
3264
  }
3235
3265
  async function runChild(command, argumentsList, pendingInput) {
3236
3266
  return new Promise((resolve, reject) => {
@@ -3239,9 +3269,10 @@ async function runChild(command, argumentsList, pendingInput) {
3239
3269
  if (interactiveInput == true) {
3240
3270
  inputMode = "inherit";
3241
3271
  }
3242
- const child = spawn2(command, argumentsList, {
3272
+ const child = spawn(command, argumentsList, {
3243
3273
  stdio: [inputMode, "inherit", "inherit"],
3244
- windowsHide: true
3274
+ windowsHide: true,
3275
+ env: { ...process.env, AKMS_MCP_MENU: "1" }
3245
3276
  });
3246
3277
  const interrupt = () => {
3247
3278
  child.kill("SIGINT");
@@ -3313,11 +3344,7 @@ async function main() {
3313
3344
  return;
3314
3345
  }
3315
3346
  if (argumentsList.showVersion == true) {
3316
- let version = "development";
3317
- if (true) {
3318
- version = "0.3.2";
3319
- }
3320
- process.stdout.write(`${version}
3347
+ process.stdout.write(`${PACKAGE_VERSION}
3321
3348
  `);
3322
3349
  return;
3323
3350
  }
@@ -3341,3 +3368,5 @@ main().catch((error) => {
3341
3368
  `);
3342
3369
  process.exitCode = 1;
3343
3370
  });
3371
+ //# sourceMappingURL=cli.js.map
3372
+ //# sourceMappingURL=cli.js.map