@akms/mcp 0.3.1 → 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`];
@@ -373,6 +406,8 @@ function createStyle(enabled) {
373
406
  return {
374
407
  accent: paint(36, 39),
375
408
  success: paint(32, 39),
409
+ warning: paint(33, 39),
410
+ error: paint(31, 39),
376
411
  dim: paint(2, 22),
377
412
  bold: paint(1, 22)
378
413
  };
@@ -381,6 +416,26 @@ var Prompter = class {
381
416
  constructor(input, output, initialInput = "") {
382
417
  this.input = input;
383
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;
384
439
  const ttyInput = input;
385
440
  const ttyOutput = output;
386
441
  this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
@@ -396,31 +451,6 @@ var Prompter = class {
396
451
  input.on("end", this.onInputEnd);
397
452
  if (initialInput) this.onData(initialInput);
398
453
  }
399
- input;
400
- output;
401
- lineBuffer = "";
402
- pendingLines = [];
403
- lineWaiter = null;
404
- inputEnded = false;
405
- onInputData = (chunk) => {
406
- this.onData(chunk);
407
- };
408
- onInputEnd = () => {
409
- this.inputEnded = true;
410
- if (this.lineWaiter != null) {
411
- const waiter = this.lineWaiter;
412
- this.lineWaiter = null;
413
- waiter(null);
414
- }
415
- if (this.keyHandler != null) {
416
- this.keyHandler(KEY_CTRL_D);
417
- }
418
- };
419
- /** Set while a key-driven prompt owns the input. */
420
- keyHandler = null;
421
- interactive;
422
- style;
423
- symbols;
424
454
  /** Stops reading so the process can exit once the command is done. */
425
455
  close() {
426
456
  if (this.interactive == true) {
@@ -595,6 +625,9 @@ var Prompter = class {
595
625
  if (item.hint != null && item.hint !== "") {
596
626
  line += ` ${item.hint}`;
597
627
  }
628
+ if (item.notice != null && item.notice !== "") {
629
+ line += ` ${item.notice}`;
630
+ }
598
631
  this.output.write(`${line.trimEnd()}
599
632
  `);
600
633
  }
@@ -719,6 +752,9 @@ var Prompter = class {
719
752
  if (item.hint != null && item.hint !== "") {
720
753
  parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
721
754
  }
755
+ if (item.notice != null && item.notice !== "") {
756
+ parts.push({ text: ` ${item.notice}`, paint: this.style[item.noticeTone ?? "warning"] });
757
+ }
722
758
  lines.push(this.fitLine(parts, width));
723
759
  }
724
760
  lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
@@ -789,14 +825,14 @@ var fieldName = (field) => {
789
825
  const names = { command: "Launch command", args: "Launch arguments", DB_HOST: "Host", SSH_HOST: "Host", DB_PORT: "Port", SSH_PORT: "Port", DB_NAME: "Database", DB_USER: "User", SSH_USER: "User", WSL_USER: "User", WSL_DISTRO: "Linux distribution", S3_BUCKET: "Bucket", S3_REGION: "Region", S3_PREFIX: "Key prefix" };
790
826
  return Object.hasOwn(names, key) ? names[key] : plainText(key);
791
827
  };
792
- function writeWrapped(prompter, text, indent = "", changed) {
828
+ function writeWrapped(prompter, text, indent = "", changed, style) {
793
829
  const characters = Array.from(plainText(text));
794
830
  let line = indent;
795
831
  let width = displayWidth(indent);
796
832
  let segment = "";
797
833
  let highlighted = false;
798
834
  const flush = () => {
799
- line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : segment;
835
+ line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : style ? style(segment) : segment;
800
836
  segment = "";
801
837
  };
802
838
  for (let index = 0; index < characters.length; index++) {
@@ -834,20 +870,54 @@ function changedRange(values, index) {
834
870
  function compareEntries(prompter, versions) {
835
871
  const entries = versions.map((version) => version.entry);
836
872
  const fields = differingFields(entries);
837
- 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
+ `);
838
877
  const values = fields.map((field) => {
839
878
  const key = field.replace(/^env\./, "");
840
879
  return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
841
880
  });
881
+ if (versions.length === 2 && prompter.width >= 90) {
882
+ const width = Math.floor((prompter.width - 3) / 2);
883
+ const wrap = (value, changed) => {
884
+ const lines = [{ text: "", width: 0 }];
885
+ for (const [index, character] of Array.from(value).entries()) {
886
+ const size = displayWidth(character);
887
+ if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
888
+ const line = lines[lines.length - 1];
889
+ line.text += prompter.paint.accent(changed && index >= changed[0] && index < changed[1] ? prompter.paint.bold(character) : character);
890
+ line.width += size;
891
+ }
892
+ return lines;
893
+ };
894
+ const row = (left, right) => {
895
+ for (let line = 0; line < Math.max(left.length, right.length); line++) {
896
+ prompter.write(`${left[line]?.text ?? ""}${" ".repeat(width - (left[line]?.width ?? 0))} | ${right[line]?.text ?? ""}
897
+ `);
898
+ }
899
+ };
900
+ prompter.write("\n");
901
+ row(wrap(`[1] ${plainText(versions[0].label)}`), wrap(`[2] ${plainText(versions[1].label)}`));
902
+ for (let field = 0; field < fields.length; field++) {
903
+ prompter.write(`
904
+ ${prompter.paint.dim(fieldName(fields[field]))}
905
+ `);
906
+ row(wrap(values[field][0], changedRange(values[field], 0)), wrap(values[field][1], changedRange(values[field], 1)));
907
+ }
908
+ if (values.some((row2) => row2.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
909
+ prompter.write("\n");
910
+ return;
911
+ }
842
912
  for (let index = 0; index < versions.length; index++) {
843
913
  prompter.write("\n");
844
- writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
914
+ writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`, "", void 0, prompter.paint.bold);
845
915
  for (let field = 0; field < fields.length; field++) {
846
- writeWrapped(prompter, fieldName(fields[field]), " ");
847
- 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);
848
918
  }
849
919
  }
850
- 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"));
851
921
  prompter.write("\n");
852
922
  }
853
923
 
@@ -857,7 +927,7 @@ function visibleRuntimePath(entry) {
857
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;
858
928
  }
859
929
  function redactStartupText(text, entry) {
860
- 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, "");
861
931
  const direct = visibleRuntimePath(entry);
862
932
  const privateValues = [...Object.entries(entry.env).filter(([key]) => !visibleField.test(key)).map(([, value]) => value), ...entry.args.slice(direct ? 1 : 0)];
863
933
  const secrets = /* @__PURE__ */ new Set();
@@ -926,7 +996,7 @@ async function checkStartup(entry, timeoutMs = 1e4) {
926
996
  child.stdin?.end();
927
997
  killTimer = setTimeout(() => {
928
998
  if (process.platform === "win32" && child.pid) {
929
- 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 }, () => {
930
1000
  });
931
1001
  } else if (child.pid) {
932
1002
  try {
@@ -1045,28 +1115,32 @@ async function checkConnections(prompter, entries, check = checkStartup) {
1045
1115
  const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
1046
1116
  if (result.ok && missing.length) result = { ...result, ok: false, detail: `Missing connection tools: ${missing.map((prefix) => prefix.slice(0, -2)).join(", ")}. Review catalog status before reconnecting.` };
1047
1117
  const safe = (value) => [entry, ...maskEntries ?? []].reduce((text, source) => redactStartupText(text, source), value);
1048
- writeWrapped(prompter, safe(result.ok ? `OK ${name}: ${result.server}@${result.version}, ${result.tools} tools` : `FAILED ${name}: exit ${result.exitCode ?? "unavailable"}. ${result.detail ?? "Server failed to start."}`));
1118
+ writeWrapped(prompter, safe(result.ok ? `OK ${name}: ${result.server}@${result.version}, ${result.tools} tools` : `FAILED ${name}: exit ${result.exitCode ?? "unavailable"}. ${result.detail ?? "Server failed to start."}`), "", void 0, result.ok ? prompter.paint.success : prompter.paint.error);
1049
1119
  if (!result.ok) {
1050
1120
  failed = true;
1051
- for (const line of result.stderr) writeWrapped(prompter, safe(line), " ");
1121
+ for (const line of result.stderr) writeWrapped(prompter, safe(line), " ", void 0, prompter.paint.dim);
1052
1122
  }
1053
1123
  }
1054
1124
  if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
1055
1125
  }
1056
-
1057
- // src/modules/registration/catalog.ts
1058
- import fs3 from "fs";
1059
- import os2 from "os";
1060
- import path4 from "path";
1061
1126
  var LONGEST_TOOL_NAME_LENGTH = { db: 13, ssh: 17, wsl: 17, s3: 11 };
1062
1127
  var maxConnectionNameLength = (kind) => 64 - `mcp__akms_${kind}__`.length - 2 - LONGEST_TOOL_NAME_LENGTH[kind];
1063
- 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");
1064
1129
 
1065
1130
  // src/modules/registration/registry.ts
1066
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
+ }
1067
1138
  var MIGRATION_STEPS = [
1068
1139
  { id: "v1_to_v2", label: "Upgrade registry version 1 to version 2", needed: (state) => state.version === 1 },
1069
- { 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 }
1070
1144
  ];
1071
1145
  var own = (value, key) => Object.hasOwn(value, key);
1072
1146
  var record = (value) => value != null && typeof value === "object" && !Array.isArray(value);
@@ -1101,18 +1175,45 @@ var Registry = class {
1101
1175
  constructor(targets, file = defaultRegistryPath(), runtimeOptions = {}) {
1102
1176
  this.targets = targets;
1103
1177
  this.runtimeOptions = runtimeOptions;
1178
+ this.backupCaptured = false;
1104
1179
  this.file = file;
1105
1180
  }
1106
- targets;
1107
- runtimeOptions;
1108
- file;
1109
- observedText;
1110
- backupCaptured = false;
1111
1181
  globalRuntime(kind) {
1112
1182
  return (this.runtimeOptions.find ?? findGlobalRuntime)(kind);
1113
1183
  }
1114
- latestRuntime(kind) {
1115
- 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);
1116
1217
  }
1117
1218
  isSessionMode(kind, state = this.read()) {
1118
1219
  return state.version === 2 && (state.sessions?.[kind]?.mode ?? state.defaultMode) === "catalog";
@@ -1123,9 +1224,10 @@ var Registry = class {
1123
1224
  planMigration() {
1124
1225
  const state = this.read();
1125
1226
  const steps = MIGRATION_STEPS.filter((step) => step.needed(state)).map(({ id, label }) => ({ id, label }));
1126
- 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) => {
1127
1230
  const names = Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => name);
1128
- if (state.sessions?.[kind]?.mode === "individual") return { kind, names, status: "individual" };
1129
1231
  if (this.isSessionMode(kind, state)) return { kind, names, status: "catalog" };
1130
1232
  if (!names.length) return { kind, names, status: "empty" };
1131
1233
  try {
@@ -1152,7 +1254,7 @@ var Registry = class {
1152
1254
  return { kind, names, status: "blocked", detail: error instanceof RegistryError ? error.message : "Cannot preview this kind. Check its runtime and catalog settings." };
1153
1255
  }
1154
1256
  }) : [];
1155
- return { expected: JSON.stringify(state), steps, kinds };
1257
+ return { expected: JSON.stringify(state), steps, kinds, launches };
1156
1258
  }
1157
1259
  applyMigration(plan) {
1158
1260
  if (!plan.steps.length) return this.locked(() => {
@@ -1171,21 +1273,28 @@ var Registry = class {
1171
1273
  state.version = 2;
1172
1274
  state.sessions ??= {};
1173
1275
  } else if (step.id === "catalog_default") state.defaultMode = "catalog";
1174
- else throw new RegistryError("Unknown registry migration step. Reopen Migrate registry.");
1175
- }
1176
- }, plan.kinds.filter((item) => item.status === "convert" || item.status === "pending-runtime").map((item) => item.kind));
1177
- }
1178
- setDefaultMode(mode) {
1179
- return this.change((state) => {
1180
- if (state.version !== 2 || state.defaultMode === void 0) throw new RegistryError("Migrate registry before changing the default for new kinds.");
1181
- if (state.defaultMode === mode) return;
1182
- for (const kind of KINDS) if (!state.sessions?.[kind] && Object.values(state.connections).some((item) => item.kind === kind && item.entry)) {
1183
- if (!this.globalRuntime(kind)) throw new RegistryError("Set up the pending runtimes and run Sync before changing the default for new kinds.");
1184
- state.sessions ??= {};
1185
- 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.");
1186
1293
  }
1187
- state.defaultMode = mode;
1188
- }, []);
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
+ ])]);
1189
1298
  }
1190
1299
  kindForName(name, state = this.read()) {
1191
1300
  return own(state.connections, name) ? state.connections[name].kind : KINDS.find((kind) => sessionName(kind) === name && (state.sessions?.[kind] || this.isSessionMode(kind, state)));
@@ -1195,10 +1304,10 @@ var Registry = class {
1195
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.");
1196
1305
  }
1197
1306
  sessionEntry(kind) {
1198
- 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.");
1199
1308
  const runtime = this.requireRuntime(kind);
1200
1309
  const env = {};
1201
- 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));
1202
1311
  return { command: runtime.command, args: [...runtime.args, "--catalog"], env };
1203
1312
  }
1204
1313
  planSession(kind, mode) {
@@ -1266,49 +1375,21 @@ var Registry = class {
1266
1375
  }
1267
1376
  await checkConnections(prompter, entries, this.runtimeOptions.check);
1268
1377
  }
1269
- async installRuntime(kind) {
1270
- if (this.globalRuntime(kind)?.linked) throw new RegistryError("This is a linked development installation. Build or update its source checkout instead.");
1271
- 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)();
1272
1382
  return this.requireRuntime(kind);
1273
1383
  }
1274
1384
  requireRuntime(kind) {
1275
1385
  const runtime = this.globalRuntime(kind);
1276
- 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.`);
1277
1387
  return runtime;
1278
1388
  }
1279
- runtimeSnapshot(state, kind) {
1280
- 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)))]);
1281
- }
1282
- planRuntime(kind) {
1283
- const state = this.read();
1284
- const runtime = this.requireRuntime(kind);
1285
- const names = [], skipped = [];
1286
- for (const [name, item] of Object.entries(state.connections)) {
1287
- if (item.kind !== kind || !item.entry) continue;
1288
- if (!standardLaunch(item.entry, kind)) {
1289
- skipped.push(name);
1290
- continue;
1291
- }
1292
- if (item.entry.command !== runtime.command || JSON.stringify(item.entry.args) !== JSON.stringify(runtime.args)) names.push(name);
1293
- }
1294
- return { kind, runtime, names, skipped, expected: this.runtimeSnapshot(state, kind) };
1295
- }
1296
- applyRuntime(plan) {
1297
- return this.change((state) => {
1298
- if (this.runtimeSnapshot(state, plan.kind) !== plan.expected) throw new RegistryError("Connections or registration changed since review. Reopen Runtime before applying.");
1299
- const runtime = this.requireRuntime(plan.kind);
1300
- if (JSON.stringify(runtime) !== JSON.stringify(plan.runtime)) throw new RegistryError("The global installation changed since review. Reopen Runtime before applying.");
1301
- for (const name of plan.names) {
1302
- const item = state.connections[name];
1303
- 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.");
1304
- item.entry = { ...item.entry, command: runtime.command, args: [...runtime.args] };
1305
- }
1306
- });
1307
- }
1308
1389
  read() {
1309
1390
  let text;
1310
1391
  try {
1311
- text = fs4.readFileSync(this.file, "utf8");
1392
+ text = fs.readFileSync(this.file, "utf8");
1312
1393
  this.observedText = text;
1313
1394
  } catch (error) {
1314
1395
  if (error.code === "ENOENT") {
@@ -1344,7 +1425,7 @@ var Registry = class {
1344
1425
  return after;
1345
1426
  }, { beforeCommit: (before) => {
1346
1427
  if (before !== null && !this.backupCaptured) {
1347
- 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 });
1348
1429
  this.backupCaptured = true;
1349
1430
  }
1350
1431
  } });
@@ -1352,16 +1433,16 @@ var Registry = class {
1352
1433
  }
1353
1434
  /** Serializes standalone editors across packages, including their config-file writes. */
1354
1435
  locked(action) {
1355
- fs4.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
1436
+ fs.mkdirSync(path2.dirname(this.file), { recursive: true, mode: 448 });
1356
1437
  const lock = this.file + ".lock";
1357
1438
  let descriptor;
1358
1439
  try {
1359
- descriptor = fs4.openSync(lock, "wx", 384);
1440
+ descriptor = fs.openSync(lock, "wx", 384);
1360
1441
  } catch (error) {
1361
1442
  if (error.code !== "EEXIST") throw error;
1362
1443
  let abandoned = false;
1363
1444
  try {
1364
- const pid = Number(fs4.readFileSync(lock, "utf8"));
1445
+ const pid = Number(fs.readFileSync(lock, "utf8"));
1365
1446
  if (Number.isSafeInteger(pid) && pid > 0) {
1366
1447
  try {
1367
1448
  process.kill(pid, 0);
@@ -1372,17 +1453,17 @@ var Registry = class {
1372
1453
  } catch {
1373
1454
  }
1374
1455
  if (!abandoned) throw new RegistryError("Another AKMS configuration command is running. Finish it and retry.");
1375
- fs4.unlinkSync(lock);
1376
- descriptor = fs4.openSync(lock, "wx", 384);
1456
+ fs.unlinkSync(lock);
1457
+ descriptor = fs.openSync(lock, "wx", 384);
1377
1458
  }
1378
1459
  this.backupCaptured = false;
1379
1460
  try {
1380
- fs4.writeFileSync(descriptor, String(process.pid));
1461
+ fs.writeFileSync(descriptor, String(process.pid));
1381
1462
  return action();
1382
1463
  } finally {
1383
1464
  this.backupCaptured = false;
1384
- fs4.closeSync(descriptor);
1385
- fs4.unlinkSync(lock);
1465
+ fs.closeSync(descriptor);
1466
+ fs.unlinkSync(lock);
1386
1467
  }
1387
1468
  }
1388
1469
  change(action, kinds) {
@@ -1478,11 +1559,26 @@ var Registry = class {
1478
1559
  inspect() {
1479
1560
  return this.reconcile(this.read(), false);
1480
1561
  }
1562
+ formatResults(results, prefix = "", paint, reviewHint) {
1563
+ return formatResults(results, prefix, {
1564
+ paint,
1565
+ reviewHint,
1566
+ connectionCount: Object.values(this.read().connections).filter((item) => item.entry).length
1567
+ });
1568
+ }
1481
1569
  reconcile(state, apply, kinds) {
1482
1570
  const results = [];
1483
1571
  const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
1484
1572
  for (const kind of kinds ?? KINDS) if (this.isSessionMode(kind, state) && active(kind).length && !state.sessions?.[kind]) {
1485
- for (const client of state.clients) results.push({ client, name: sessionName(kind), status: "pending", detail: "Install this kind's runtime from Runtime, then run Sync. Existing client entries are kept." });
1573
+ for (const client of state.clients) results.push({
1574
+ client,
1575
+ name: sessionName(kind),
1576
+ kind,
1577
+ connections: active(kind).map(([name]) => name),
1578
+ status: "pending",
1579
+ waitingFor: "runtime",
1580
+ detail: "Install it from Runtime, then run Sync. Your existing entries keep working."
1581
+ });
1486
1582
  }
1487
1583
  const work = [
1488
1584
  ...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
@@ -1498,7 +1594,15 @@ var Registry = class {
1498
1594
  const previous = connection.applied[client];
1499
1595
  if (!desired && !previous) continue;
1500
1596
  const target = this.targets.find((candidate) => candidate.id === client);
1501
- const report = (status, detail) => results.push({ client, name, status, detail });
1597
+ const report = (status, detail, waitingFor) => results.push({
1598
+ client,
1599
+ name,
1600
+ status,
1601
+ detail,
1602
+ kind: connection.kind,
1603
+ connections: bundle ? active(connection.kind).map(([name2]) => name2) : [name],
1604
+ ...waitingFor ? { waitingFor } : {}
1605
+ });
1502
1606
  if (!target) {
1503
1607
  report("error", "Service adapter unavailable; use a newer package to retry.");
1504
1608
  continue;
@@ -1516,20 +1620,18 @@ var Registry = class {
1516
1620
  report("removed", "Removed");
1517
1621
  } else if (exists && (!actual || !previous || hash !== previous)) {
1518
1622
  report("conflict", actual && desired ? describeDifferences([desired, actual]) : actual ? "Changed outside AKMS; review before removing this entry." : "This name is used by another server type; choose another name.");
1519
- } else if (!apply) {
1520
- report("pending", desired ? "Needs applying" : "Needs removal");
1521
1623
  } else {
1522
1624
  if (!desired && state.clients.includes(client)) {
1523
1625
  if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
1524
1626
  const session = state.sessions?.[connection.kind];
1525
1627
  const replacement2 = entries[sessionName(connection.kind)];
1526
1628
  if (!session || !replacement2 || entryHash(replacement2) !== entryHash(session.entry) || session.applied[client] !== entryHash(session.entry)) {
1527
- report("pending", "Connection entry kept until the session server is applied.");
1629
+ report("pending", "Connection entry kept until the session server is applied.", "session");
1528
1630
  continue;
1529
1631
  }
1530
1632
  }
1531
1633
  if (bundle && !this.isSessionMode(connection.kind, state) && active(connection.kind).some(([connectionName, value]) => !entries[connectionName] || entryHash(entries[connectionName]) !== entryHash(value.entry))) {
1532
- report("pending", "Session entry kept until all connection entries are applied.");
1634
+ report("pending", "Session entry kept until all connection entries are applied.", "connections");
1533
1635
  continue;
1534
1636
  }
1535
1637
  }
@@ -1540,7 +1642,11 @@ var Registry = class {
1540
1642
  replacement = state.connections[replacement.replacement];
1541
1643
  }
1542
1644
  if (!desired && state.clients.includes(client) && !this.isSessionMode(connection.kind, state) && replacement && replacement.entry && replacement.applied[client] !== entryHash(replacement.entry)) {
1543
- report("pending", "Old name kept until the new name is applied.");
1645
+ report("pending", "Old name kept until the new name is applied.", "rename");
1646
+ continue;
1647
+ }
1648
+ if (!apply) {
1649
+ report("pending", desired ? "Needs applying" : "Needs removal");
1544
1650
  continue;
1545
1651
  }
1546
1652
  const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
@@ -1631,7 +1737,7 @@ var Registry = class {
1631
1737
  const registry = this;
1632
1738
  let snapshot;
1633
1739
  const expected = (name) => snapshot ? own(snapshot, name) ? entryHash(snapshot[name]) : null : void 0;
1634
- const saved = (results) => ({ status: "written", detail: formatResults(results, "Saved in the shared AKMS catalog.") });
1740
+ const saved = (results) => ({ status: "written", detail: registry.formatResults(results, "Saved in the shared AKMS catalog.") });
1635
1741
  const prepare = (name, entry, oldName) => {
1636
1742
  if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
1637
1743
  const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
@@ -1660,283 +1766,437 @@ var Registry = class {
1660
1766
  };
1661
1767
  }
1662
1768
  };
1663
- function formatResults(results, prefix = "") {
1664
- const lines = prefix ? [prefix] : [];
1665
- if (!results.length) return [...lines, " No shared connections need applying."].join("\n");
1666
- const attention = results.filter((item) => !["synced", "removed"].includes(item.status));
1667
- const total = new Set(results.map((item) => item.name)).size;
1668
- const unresolved = new Set(attention.map((item) => item.name)).size;
1669
- lines.push(` Connections: ${total - unresolved} complete \xB7 ${unresolved} need attention`);
1769
+ function formatResults(results, prefix = "", options = {}) {
1770
+ const plain = (text) => text;
1771
+ const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
1772
+ const accent = options.paint?.accent ?? paint.bold;
1773
+ const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
1774
+ const lines = prefix ? [paint.bold(prefix)] : [];
1775
+ const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
1776
+ lines.push(`${paint.dim(" Connections: ")}${accent(String(total))}`);
1670
1777
  const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
1671
1778
  const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
1672
1779
  const removed = results.filter((item) => item.status === "removed").length;
1673
- lines.push(` Service entries: ${updated} updated \xB7 ${unchanged} already up to date${removed ? ` \xB7 ${removed} removed` : ""}`);
1674
- for (const [heading, rows] of [
1675
- ["Needs review (existing settings kept)", attention.filter((item) => item.status === "conflict")],
1676
- ["Could not finish", attention.filter((item) => item.status !== "conflict")]
1780
+ const failed = results.filter((item) => item.status === "error");
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")}`);
1783
+ if (!results.length) lines.push(paint.dim(" No shared connections need applying."));
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.")}`);
1785
+ const waiting = results.filter((item) => item.status === "pending");
1786
+ for (const kind of KINDS) {
1787
+ const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
1788
+ const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
1789
+ if (!runtime.length && !kept.length) continue;
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."));
1795
+ const retained = /* @__PURE__ */ new Map();
1796
+ for (const client of new Set(kept.map((item) => item.client))) {
1797
+ const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
1798
+ const key = JSON.stringify([...names].sort());
1799
+ const group = retained.get(key);
1800
+ if (group) group.clients.push(client);
1801
+ else retained.set(key, { clients: [client], names });
1802
+ }
1803
+ for (const group of retained.values()) {
1804
+ const clients = group.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ");
1805
+ lines.push(`${paint.dim(" Kept in ")}${paint.bold(clients)}${paint.dim(` until ${sessionName(kind)} is applied: `)}${accent(group.names.map(safe).join(", "))}`);
1806
+ }
1807
+ }
1808
+ for (const [heading, rows, color] of [
1809
+ ["Needs review (existing settings kept)", results.filter((item) => item.status === "conflict"), paint.warning],
1810
+ ["Waiting for Sync", waiting.filter((item) => !item.waitingFor || !["runtime", "session"].includes(item.waitingFor)), paint.warning],
1811
+ ["Could not finish", failed, paint.error]
1677
1812
  ]) {
1678
1813
  if (!rows.length) continue;
1679
1814
  lines.push(`
1680
- ${heading}:`);
1815
+ ${color(`${heading}:`)}`);
1681
1816
  for (const name of new Set(rows.map((item) => item.name))) {
1682
- lines.push(` ${name.replace(/[\x00-\x1f\x7f]/g, " ")}`);
1683
- for (const row of rows.filter((item) => item.name === name)) lines.push(` ${CLIENT_LABELS2[row.client]}: ${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)}`);
1684
1819
  }
1685
1820
  }
1686
- if (attention.length) lines.push("\nNext: open Sync to compare settings or retry unfinished changes.");
1821
+ if (results.some((item) => item.status === "conflict")) lines.push(paint.dim(`
1822
+ ${options.reviewHint ?? "Next: open Sync to compare settings and choose which version to use."}`));
1823
+ if (failed.length) lines.push(`${paint.dim("\nNext: ")}${paint.error("fix the errors above")}${paint.dim(", then run ")}${accent("Sync")}${paint.dim(" again.")}`);
1687
1824
  return lines.join("\n");
1688
1825
  }
1689
1826
 
1690
- // src/modules/registration/runtime-menu.ts
1691
- async function runtimeMenu(prompter, registry, requestedKind) {
1692
- let kind = requestedKind;
1693
- for (; ; ) {
1694
- prompter.clearScreen();
1695
- if (!kind) {
1696
- const selected = await prompter.select("Runtime / choose MCP", [
1697
- ...KINDS.map((value) => ({ value, label: packageName(value) })),
1698
- { value: "back", key: "b", label: "Back" }
1699
- ], { cancelValue: "back" });
1700
- if (selected === "back") return;
1701
- kind = selected;
1702
- }
1703
- try {
1704
- const installed = registry.globalRuntime(kind);
1705
- const sessionMode = registry.isSessionMode(kind);
1706
- writeWrapped(prompter, `Runtime / ${packageName(kind)}`);
1707
- writeWrapped(prompter, installed ? `Installed: ${installed.version}${installed.linked ? " (linked development checkout)" : ""}` : "Not installed in the AKMS runtime directory.");
1708
- writeWrapped(prompter, `Runtime directory: ${runtimePrefix()}`);
1709
- prompter.write("Connections start Node directly, without a resident npm/npx process.\n");
1710
- prompter.write(`Configuration: ${sessionMode ? "one session server for this kind" : "one entry per connection"}.
1711
- `);
1712
- if (installed) writeWrapped(prompter, installed.args[0]);
1713
- const picked = await prompter.select("AKMS runtime", [
1714
- ...installed && !sessionMode ? [{ value: "apply", label: "Use for existing connections", hint: "convert npx and old runtime paths together" }] : [],
1715
- ...installed ? [{ value: "catalog", key: "catalog", label: sessionMode ? "Reapply session launch command" : "Switch to session server", hint: `one ${sessionName(kind)} entry per registered service` }] : [],
1716
- ...sessionMode && (installed || registry.read().sessions?.[kind]) ? [{ value: "individual", key: "individual", label: "Switch back to per-connection entries", hint: "restore connection settings from the catalog" }] : [],
1717
- ...!installed?.linked ? [{ value: "install", label: installed ? "Update runtime installation" : "Install AKMS runtime", hint: runtimeInstallCommand(kind) }] : [],
1718
- ...!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" }] : [],
1719
- { value: "back", key: "b", label: "Back" }
1720
- ], { cancelValue: "back" });
1721
- if (picked === "back") {
1722
- if (requestedKind) return;
1723
- kind = void 0;
1724
- continue;
1725
- }
1726
- if (picked === "default") {
1727
- const mode = registry.read().defaultMode === "catalog" ? "individual" : "catalog";
1728
- 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")}
1729
1851
  `);
1730
- if (await prompter.askYesNo("Change the default for new kinds?", false)) {
1731
- prompter.write(`${formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.")}
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)))}
1732
1854
  `);
1733
- await prompter.acknowledge();
1734
- }
1735
- continue;
1736
- }
1737
- if (picked === "catalog" || picked === "individual") {
1738
- const plan2 = registry.planSession(kind, picked);
1739
- prompter.clearScreen();
1740
- writeWrapped(prompter, picked === "catalog" ? `Switch to session server: ${sessionName(kind)}` : "Switch back to per-connection entries");
1741
- 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");
1742
- for (const change of plan2.changes) {
1743
- writeWrapped(prompter, CLIENT_LABELS2[change.client]);
1744
- writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
1745
- writeWrapped(prompter, `Remove after replacements are applied: ${change.remove.join(", ") || "none"}`, " ");
1746
- if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.join(", ")}`, " ");
1747
- }
1748
- if (picked === "catalog") {
1749
- prompter.write("\nPasswords stay in the catalog and its previous-revision copy. Session entries contain no connection credentials.\n");
1750
- writeWrapped(prompter, `Command: ${plan2.entry.command}`);
1751
- writeWrapped(prompter, `Runtime: ${plan2.entry.args[0]} --catalog`);
1752
- if (plan2.custom.length) {
1753
- prompter.write("Custom commands will not run in catalog mode; they are retained for switching back:\n");
1754
- for (const name of plan2.custom) writeWrapped(prompter, plainText(name), " ");
1755
- }
1756
- } else prompter.write("\nConnection credentials will be copied back into registered client settings. The registry stays version 2.\n");
1757
- if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
1758
- const results2 = registry.applySession(plan2);
1759
- prompter.clearScreen();
1760
- prompter.write(`${formatResults(results2, "Configuration mode saved.")}
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")}
1761
1863
  `);
1762
- await registry.checkSaved(prompter, plan2.names);
1763
- prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
1764
- await prompter.acknowledge();
1765
- continue;
1766
- }
1767
- if (picked === "install") {
1768
- prompter.clearScreen();
1769
- writeWrapped(prompter, `Installing ${packageName(kind)}@latest in ${runtimePrefix()}...`);
1770
- const runtime = await registry.installRuntime(kind);
1771
- 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}
1772
1869
  `);
1773
- await prompter.acknowledge();
1774
- continue;
1775
- }
1776
- const plan = registry.planRuntime(kind);
1777
- prompter.clearScreen();
1778
- writeWrapped(prompter, `Use AKMS runtime ${packageName(kind)} ${plan.runtime.version}`);
1779
- prompter.write(`
1780
- Convert ${plan.names.length} connection(s):
1870
+ if (item.detail) prompter.write(` ${" ".repeat(10)}${paint.dim(plainText(item.detail))}
1781
1871
  `);
1782
- for (const name of plan.names) writeWrapped(prompter, plainText(name), " ");
1783
- if (plan.skipped.length) {
1784
- prompter.write("Custom commands kept for individual review:\n");
1785
- for (const name of plan.skipped) writeWrapped(prompter, plainText(name), " ");
1786
- }
1787
- prompter.write("\nConnection values and credentials stay the same.\nVersion-pinned npx entries will use the installed version shown above.\n");
1788
- if (!plan.names.length) {
1789
- 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");
1790
- await prompter.acknowledge();
1791
- continue;
1792
- }
1793
- if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
1794
- const results = registry.applyRuntime(plan);
1795
- prompter.clearScreen();
1796
- prompter.write(`${formatResults(results, "Runtime settings saved.")}
1797
-
1798
- 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)}
1799
1875
  `);
1800
- await registry.checkSaved(prompter, plan.names);
1801
- await prompter.acknowledge();
1802
- } catch (error) {
1803
- if (error instanceof PromptClosedError) throw error;
1804
- if (error instanceof PromptBackError) continue;
1805
- 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(", ")}`)}
1806
1877
  `);
1807
- await prompter.acknowledge();
1808
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
+ `);
1809
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"));
1810
1883
  }
1811
-
1812
- // src/modules/registration/updates-menu.ts
1813
- async function updatesMenu(prompter, registry) {
1814
- const kinds = KINDS.filter((kind) => kind !== "wsl" || process.platform === "win32");
1815
- const latest = /* @__PURE__ */ new Map();
1816
- const refresh = async () => {
1817
- prompter.clearScreen();
1818
- prompter.write("Updates / checking published runtime versions...\n");
1819
- await Promise.all(kinds.map(async (kind) => {
1820
- try {
1821
- latest.set(kind, await registry.latestRuntime(kind));
1822
- } catch {
1823
- latest.set(kind, null);
1824
- }
1825
- }));
1826
- };
1827
- await refresh();
1828
- for (; ; ) {
1829
- try {
1830
- prompter.clearScreen();
1831
- prompter.write("Updates / AKMS runtimes\n\n");
1832
- const rows = kinds.map((kind) => {
1833
- const installed = registry.globalRuntime(kind), published = latest.get(kind);
1834
- const update = !!installed && !installed.linked && !!published && runtimeIsOlder(installed.version, published);
1835
- const status = installed?.linked ? "Development link" : !published ? "Unavailable" : !installed ? "Not installed" : update ? "Update available" : "Up to date";
1836
- writeWrapped(prompter, `${packageName(kind)} | installed: ${installed?.version ?? "not installed"} | latest: ${published ?? "unavailable"} | ${status}`);
1837
- return { kind, installed, published, update };
1838
- });
1839
- const updates = rows.filter((row) => row.update);
1840
- const available = rows.filter((row) => row.published && !row.installed?.linked && (!row.installed || row.update));
1841
- prompter.write("\nDevelopment links are managed in their source checkouts.\n");
1842
- const picked = await prompter.select("Updates", [
1843
- ...available.map((row) => ({ value: row.kind, key: row.kind, label: `${row.installed ? "Update" : "Install"} ${packageName(row.kind)}` })),
1844
- ...updates.length ? [{ value: "all", key: "a", label: `Update all available (${updates.length})` }] : [],
1845
- { value: "refresh", key: "r", label: "Refresh versions" },
1846
- { value: "back", key: "b", label: "Back" }
1847
- ], { cancelValue: "back" });
1848
- if (picked === "back") return;
1849
- if (picked === "refresh") {
1850
- await refresh();
1851
- continue;
1852
- }
1853
- const selected = picked === "all" ? updates : available.filter((row) => row.kind === picked);
1854
- if (!selected.length) continue;
1855
- 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) {
1856
1889
  prompter.clearScreen();
1857
- for (const row of selected) {
1858
- 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
+ `);
1859
1903
  try {
1860
- const installed = await registry.installRuntime(row.kind);
1861
- writeWrapped(prompter, `Installed ${packageName(row.kind)} ${installed.version}.`);
1862
- const names = Object.entries(registry.read().connections).filter(([, item]) => item.kind === row.kind && item.entry).map(([name]) => name);
1863
- await registry.checkSaved(prompter, names);
1904
+ await registry.installRuntime();
1864
1905
  } catch (error) {
1865
- let detail = error instanceof Error ? error.message : "Installation failed.";
1866
- for (const item of Object.values(registry.read().connections)) if (item.entry) detail = redactStartupText(detail, item.entry);
1867
- writeWrapped(prompter, `FAILED ${packageName(row.kind)}: ${detail}`);
1868
- 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();
1869
1909
  }
1910
+ plan = registry.planMigration();
1911
+ writePlan(prompter, registry, plan);
1870
1912
  }
1871
- prompter.write("Reconnect or restart your AI clients to load updated runtimes. Fix any failed startup checks first.\n");
1872
- await prompter.acknowledge();
1873
- } catch (error) {
1874
- if (error instanceof PromptClosedError) throw error;
1875
- if (error instanceof PromptBackError) continue;
1876
- throw error;
1877
1913
  }
1878
- }
1879
- }
1880
-
1881
- // src/modules/registration/migration-menu.ts
1882
- async function migrationMenu(prompter, registry) {
1883
- try {
1884
- prompter.clearScreen();
1885
- const plan = registry.planMigration();
1886
- prompter.write("Migrate registry\n\n");
1887
- if (!plan.steps.length) {
1888
- 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"));
1889
1916
  await prompter.acknowledge();
1890
1917
  return;
1891
1918
  }
1892
- prompter.write("Steps:\n");
1893
- plan.steps.forEach((step, index) => writeWrapped(prompter, `${index + 1}. ${step.label}`, " "));
1894
- prompter.write('\nThe registry will record defaultMode: "catalog" for new kinds.\nA kind explicitly switched back to individual entries keeps that choice.\n');
1895
- for (const item of plan.kinds) {
1896
- prompter.write("\n");
1897
- const target = sessionName(item.kind);
1898
- 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" : ""}`;
1899
- writeWrapped(prompter, `${item.kind.toUpperCase()}: ${item.names.length} connection(s), ${action}`);
1900
- if (item.detail) writeWrapped(prompter, plainText(item.detail), " ");
1901
- for (const change of item.session?.changes ?? item.changes ?? []) {
1902
- writeWrapped(prompter, `${CLIENT_LABELS2[change.client]}${item.status === "pending-runtime" ? " (after Runtime + Sync)" : ""}`, " ");
1903
- writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
1904
- writeWrapped(prompter, `Remove after verification: ${change.remove.join(", ") || "none"}`, " ");
1905
- if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.map(plainText).join(", ")}`, " ");
1906
- }
1907
- if (item.session?.custom.length) writeWrapped(prompter, `Custom commands retained for rollback: ${item.session.custom.map(plainText).join(", ")}`, " ");
1908
- }
1909
- 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");
1910
- if (plan.kinds.some((item) => item.status === "blocked")) {
1911
- 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
+ `);
1912
1922
  await prompter.acknowledge();
1913
1923
  return;
1914
1924
  }
1915
- 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;
1916
1926
  const results = registry.applyMigration(plan);
1917
1927
  prompter.clearScreen();
1918
- prompter.write(`${formatResults(results, "Registry migration saved.")}
1928
+ prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
1919
1929
  `);
1920
- await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
1921
- 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");
1922
1939
  await prompter.acknowledge();
1923
1940
  } catch (error) {
1924
1941
  if (error instanceof PromptClosedError) throw error;
1925
1942
  if (error instanceof PromptBackError) return;
1926
- 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."}
1927
1944
  `);
1928
1945
  await prompter.acknowledge();
1929
1946
  }
1930
1947
  }
1931
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
+
1932
2187
  // src/modules/registration/menu.ts
2188
+ function pendingImports(registry, kind) {
2189
+ const state = registry.read();
2190
+ const entries = registry.candidates(kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name));
2191
+ const names = [...new Set(entries.map((candidate) => candidate.name))];
2192
+ return { names, differing: names.filter((name) => new Set(entries.filter((item) => item.name === name).map((item) => entryHash(item.entry))).size > 1).length };
2193
+ }
1933
2194
  function preview(prompter, entry) {
1934
2195
  if (!entry) {
1935
- prompter.write(" Remove this connection from the service.\n");
2196
+ writeField(prompter, "Action", "Remove from service", "", "warning");
1936
2197
  return;
1937
2198
  }
1938
- for (const [key, value] of Object.entries(entry.env)) prompter.write(` ${plainText(key)}: ${visibleField.test(key) ? plainText(value) : "(hidden)"}
1939
- `);
2199
+ for (const [key, value] of Object.entries(entry.env)) writeField(prompter, key, visibleField.test(key) ? value : "(hidden)", "", visibleField.test(key) ? "accent" : "dim");
1940
2200
  }
1941
2201
  async function registrationMenu(prompter, registry, initialClient) {
1942
2202
  const initial = registry.read();
@@ -1948,9 +2208,9 @@ async function registrationMenu(prompter, registry, initialClient) {
1948
2208
  for (; ; ) {
1949
2209
  try {
1950
2210
  prompter.clearScreen();
1951
- 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");
1952
- for (const error of discovered.errors) prompter.write(` ${error}
1953
- `);
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(" "));
1954
2214
  const picked = await prompter.select("Register", [
1955
2215
  ...registry.targets.filter((target) => target.id !== "registered").map((target) => ({
1956
2216
  value: target.id,
@@ -1970,64 +2230,59 @@ async function registrationMenu(prompter, registry, initialClient) {
1970
2230
  continue;
1971
2231
  }
1972
2232
  const removed = initial.clients.filter((id) => !selected.has(id));
1973
- const count = Object.values(registry.read().connections).filter((item) => item.entry).length;
2233
+ const current = registry.read();
1974
2234
  const plan = registry.planRegistration([...selected]);
1975
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)])];
1976
2246
  prompter.clearScreen();
1977
- prompter.write("Review registration\n");
1978
- prompter.write(`
1979
- Selected: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}
1980
- Apply ${count + importNames.length} shared connection(s) to these services.
1981
- `);
1982
- if (importNames.length) prompter.write(`Import ${importNames.length} existing connection(s); identical copies become one shared item:
1983
- ${importNames.join("\n ")}
1984
- `);
1985
- if (plan.conflicts.length) prompter.write(`Keep ${plan.conflicts.length} differing connection(s) unchanged until you choose a version:
1986
- ${plan.conflicts.join("\n ")}
1987
- `);
1988
- for (const error of plan.errors) prompter.write(` ${error}
1989
- `);
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");
1990
2255
  if (initial.defaultMode === "catalog") {
1991
- prompter.write("Services use one session entry per kind. Imported individual entries are removed after that session entry is verified.\n");
1992
- 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");
1993
- 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.");
1994
2258
  }
1995
- if (removed.length) prompter.write(`Unregister: ${removed.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}
1996
- Remove only AKMS-managed entries from these services; keep the shared catalog.
1997
- `);
2259
+ if (removed.length) writeField(prompter, "Unregister", removed.map((id) => CLIENT_LABELS2[id]).join(" \xB7 "), "remove managed entries; keep the catalog");
1998
2260
  if (!privacyNoticeShown) {
1999
- writeWrapped(prompter, `Connection settings, including passwords, are stored in ${plainText(registry.file)}.`);
2000
- prompter.write("Keep this file private: do not share, sync or commit it.\n");
2001
- 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.`);
2002
2262
  privacyNoticeShown = true;
2003
2263
  }
2004
2264
  if (!await prompter.askYesNo("Apply this registration?", removed.length === 0)) continue;
2005
2265
  const results = registry.register([...selected], initial.clients, plan.imports);
2006
2266
  prompter.clearScreen();
2007
- prompter.write(`
2008
- ${formatResults(results, `Registration saved.
2009
- AI services: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`)}
2010
- `);
2011
- 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)}
2012
2270
  `);
2271
+ if (importNames.length) writeField(prompter, "Imported", countLabel(importNames.length, "connection"), "saved in the shared catalog", "success");
2013
2272
  try {
2014
2273
  const conflicts = new Set(results.filter((result) => result.status === "conflict").map((result) => result.name));
2015
2274
  if (conflicts.size && await prompter.askYesNo(`Compare settings for ${conflicts.size} connection(s) now?`, true)) await syncMenu(prompter, registry);
2016
- const current = registry.read();
2017
- 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));
2018
2277
  const names = [...new Set(remaining.map((candidate) => candidate.name))];
2019
2278
  if (names.length) {
2020
- prompter.write(`
2021
- ${names.length} existing connection(s) still need a choice before joining the shared list:
2022
- `);
2279
+ writeField(prompter, "Not imported", countLabel(names.length, "connection"), "choose a version", "warning");
2023
2280
  for (const name of names) {
2024
2281
  const copies = remaining.filter((candidate) => candidate.name === name);
2025
- prompter.write(` ${plainText(name)} \xB7 ${copies.map((copy) => CLIENT_LABELS2[copy.client]).join(" \xB7 ")}
2026
- `);
2027
- if (new Set(copies.map((copy) => entryHash(copy.entry))).size > 1) prompter.write(` ${describeDifferences(copies.map((copy) => copy.entry))}
2028
- `);
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))}`);
2029
2284
  }
2030
- 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.");
2031
2286
  if (await prompter.askYesNo("Compare existing settings and choose which to share?", true)) await importMenu(prompter, registry);
2032
2287
  }
2033
2288
  if (!conflicts.size && !names.length) await prompter.acknowledge();
@@ -2038,8 +2293,7 @@ ${names.length} existing connection(s) still need a choice before joining the sh
2038
2293
  } catch (error) {
2039
2294
  if (error instanceof PromptClosedError) throw error;
2040
2295
  if (error instanceof PromptBackError) continue;
2041
- prompter.write(` ${error instanceof Error ? error.message : "Registration could not be saved."}
2042
- `);
2296
+ writeWarning(prompter, error instanceof Error ? error.message : "Registration could not be saved.");
2043
2297
  await prompter.acknowledge();
2044
2298
  }
2045
2299
  }
@@ -2047,10 +2301,9 @@ ${names.length} existing connection(s) still need a choice before joining the sh
2047
2301
  async function importMenu(prompter, registry, kind) {
2048
2302
  for (; ; ) {
2049
2303
  prompter.clearScreen();
2050
- prompter.write("Import existing connections\n\n");
2304
+ writeHeading(prompter, "Import existing connections");
2051
2305
  const { entries, errors } = registry.candidates(kind);
2052
- for (const error of errors) prompter.write(` ${error}
2053
- `);
2306
+ for (const error of errors) writeWarning(prompter, error);
2054
2307
  const state = registry.read();
2055
2308
  const matching = registry.planRegistration(state.clients, kind);
2056
2309
  const matchingNames = [...new Set(matching.imports.map((entry) => entry.name))];
@@ -2059,10 +2312,11 @@ async function importMenu(prompter, registry, kind) {
2059
2312
  return !current?.entry || current.applied[candidate.client] !== entryHash(candidate.entry) || entryHash(current.entry) !== entryHash(candidate.entry);
2060
2313
  }).map((entry) => entry.name))];
2061
2314
  if (!names.length) {
2062
- prompter.write(" No existing connections need importing.\n");
2315
+ writeHint(prompter, "No existing connections need importing.");
2063
2316
  await prompter.acknowledge();
2064
2317
  return;
2065
2318
  }
2319
+ writeField(prompter, "Available", countLabel(names.length, "connection"), `${matchingNames.length} matching`);
2066
2320
  const picked = await prompter.select("Import existing connections", [
2067
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 ") })),
2068
2322
  ...matchingNames.length ? [{ value: "matching", key: "all", label: "Import all matching connections", hint: `${matchingNames.length} connections; differing versions stay unchanged` }] : [],
@@ -2072,14 +2326,13 @@ async function importMenu(prompter, registry, kind) {
2072
2326
  try {
2073
2327
  prompter.clearScreen();
2074
2328
  if (picked === "matching") {
2075
- prompter.write(`
2076
- Import existing connections:
2077
- ${matchingNames.join("\n ")}
2078
- `);
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");
2079
2332
  if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
2080
2333
  const results2 = registry.register(state.clients, state.clients, matching.imports);
2081
2334
  prompter.clearScreen();
2082
- prompter.write(`${formatResults(results2, "Import complete")}
2335
+ prompter.write(`${registry.formatResults(results2, "Import complete", prompter.paint)}
2083
2336
  `);
2084
2337
  await prompter.acknowledge();
2085
2338
  }
@@ -2089,9 +2342,8 @@ Import existing connections:
2089
2342
  const candidates = entries.filter((entry) => entry.name === name);
2090
2343
  let chosen = candidates[0];
2091
2344
  if (new Set(candidates.map((candidate) => entryHash(candidate.entry))).size > 1) {
2092
- prompter.write(`
2093
- ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2094
- `);
2345
+ writeHeading(prompter, `Compare ${name}`);
2346
+ writeField(prompter, "Services", state.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ") || "none");
2095
2347
  compareEntries(prompter, candidates.map((candidate) => ({ label: CLIENT_LABELS2[candidate.client], entry: candidate.entry })));
2096
2348
  const choice = await prompter.select("Use settings from", [
2097
2349
  ...candidates.map((candidate, index) => ({ value: String(index), label: `Use ${index + 1}: ${CLIENT_LABELS2[candidate.client]}` })),
@@ -2100,22 +2352,23 @@ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2100
2352
  if (choice === "back") continue;
2101
2353
  chosen = candidates[Number(choice)];
2102
2354
  }
2355
+ writeField(prompter, "Connection", name);
2356
+ writeField(prompter, "Version", CLIENT_LABELS2[chosen.client]);
2103
2357
  preview(prompter, chosen.entry);
2104
2358
  const destination = await prompter.askRequired("Shared connection name (change it to keep both)", name);
2105
- 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");
2106
2360
  if (!await prompter.askYesNo(`Import ${destination} and apply it?`, true)) continue;
2107
2361
  const results = registry.importEntry(destination, chosen, candidates);
2108
2362
  prompter.clearScreen();
2109
2363
  prompter.write(`
2110
- ${formatResults(results, "Import complete")}
2364
+ ${registry.formatResults(results, "Import complete", prompter.paint)}
2111
2365
  `);
2112
- 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");
2113
2367
  await prompter.acknowledge();
2114
2368
  } catch (error) {
2115
2369
  if (error instanceof PromptClosedError) throw error;
2116
2370
  if (error instanceof PromptBackError) continue;
2117
- prompter.write(` ${error instanceof Error ? error.message : "Import failed."}
2118
- `);
2371
+ writeWarning(prompter, error instanceof Error ? error.message : "Import failed.");
2119
2372
  await prompter.acknowledge();
2120
2373
  }
2121
2374
  }
@@ -2127,19 +2380,26 @@ async function syncMenu(prompter, registry) {
2127
2380
  const results = pendingResults ?? registry.sync();
2128
2381
  pendingResults = void 0;
2129
2382
  prompter.write(`
2130
- ${formatResults(results, "Sync results")}
2383
+ ${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
2131
2384
  `);
2385
+ const imports = pendingImports(registry);
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");
2132
2387
  const conflicts = results.filter((result) => result.status === "conflict");
2133
- if (!conflicts.length) {
2388
+ if (!conflicts.length && !imports.names.length) {
2134
2389
  await prompter.acknowledge();
2135
2390
  return;
2136
2391
  }
2137
2392
  const picked = await prompter.select("Review differences", [
2138
2393
  ...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS2[item.client]} \xB7 ${item.name}` })),
2394
+ ...imports.names.length ? [{ value: "import", key: "import", label: "Import existing connections", hint: `${imports.names.length} connection(s) to review` }] : [],
2139
2395
  { value: "back", key: "b", label: "Later \u2014 keep existing settings" }
2140
2396
  ], { cancelValue: "back" });
2141
2397
  if (picked === "back") return;
2142
2398
  try {
2399
+ if (picked === "import") {
2400
+ await importMenu(prompter, registry);
2401
+ continue;
2402
+ }
2143
2403
  prompter.clearScreen();
2144
2404
  const conflict = conflicts[Number(picked)];
2145
2405
  const state = registry.read();
@@ -2148,12 +2408,14 @@ ${formatResults(results, "Sync results")}
2148
2408
  const session = state.sessions[sessionKind];
2149
2409
  const actual2 = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2150
2410
  if (!actual2 || entryKind(actual2) !== sessionKind) {
2151
- 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.");
2152
2412
  await prompter.acknowledge();
2153
2413
  continue;
2154
2414
  }
2155
2415
  const removing2 = !registry.isSessionMode(sessionKind, state) || !state.clients.includes(conflict.client) || !Object.values(state.connections).some((item) => item.kind === sessionKind && item.entry);
2156
- 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]);
2157
2419
  compareEntries(prompter, [{ label: "AKMS session launch", entry: session.entry }, { label: "External settings", entry: actual2 }]);
2158
2420
  const choice2 = await prompter.select("Resolve session launch", [
2159
2421
  { value: "apply", label: removing2 ? "Remove the old session entry" : "Restore the AKMS session launch" },
@@ -2166,71 +2428,61 @@ ${formatResults(results, "Sync results")}
2166
2428
  if (!connection) throw new Error("This connection changed. Reopen Sync to review its current settings.");
2167
2429
  const actual = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2168
2430
  if (!actual || entryKind(actual) !== connection.kind) {
2169
- 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.");
2170
2432
  await prompter.acknowledge();
2171
2433
  continue;
2172
2434
  }
2173
2435
  const client = CLIENT_LABELS2[conflict.client];
2174
2436
  const registered = state.clients.includes(conflict.client);
2175
2437
  const sessionMode = registry.isSessionMode(connection.kind, state);
2176
- const removing = !connection.entry || !registered || sessionMode;
2438
+ const removing = !connection.entry || !registered;
2439
+ const retiring = sessionMode && !removing;
2177
2440
  const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
2178
- writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
2179
- 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");
2180
2443
  prompter.write("\n");
2181
2444
  if (!removing && connection.entry) {
2445
+ if (retiring) {
2446
+ writeField(prompter, "Server", sessionName(connection.kind));
2447
+ writeField(prompter, "Kept in", client, "old entry stays until this server is ready", "warning");
2448
+ }
2182
2449
  compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
2183
2450
  } else {
2184
- 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.");
2185
- writeWrapped(prompter, `Its settings in ${client} changed before removal could finish.`);
2186
- prompter.write(`
2187
- Currently in ${client}
2188
- `);
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}`);
2189
2454
  preview(prompter, actual);
2190
2455
  prompter.write("\n");
2191
2456
  }
2192
2457
  const choices = [
2193
- { value: "shared", label: removing ? `Remove from ${client}` : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : `replace its ${subject} with version 1` },
2194
- ...registered ? [{ value: "client", label: sessionMode ? `Import ${client} settings into the catalog, then remove this entry` : removing ? `Restore from ${client}` : `Use 2: ${client} -> all ${state.clients.length} services`, hint: "save in AKMS and sync registered services" }] : [],
2458
+ { value: "shared", label: removing ? `Remove from ${client}` : retiring ? "Use 1: keep AKMS settings" : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : retiring ? `use version 1 in ${sessionName(connection.kind)}` : `replace its ${subject} with version 1` },
2459
+ ...registered ? [{ value: "client", label: removing ? `Restore from ${client}` : `Use 2: ${client} -> all ${state.clients.length} services`, hint: retiring ? `save version 2 in AKMS for ${sessionName(connection.kind)}` : "save in AKMS and sync registered services" }] : [],
2195
2460
  { value: "back", key: "b", label: "Back without changes" }
2196
2461
  ];
2197
2462
  const choice = await prompter.select(removing ? registered ? "Remove or restore this connection?" : `Remove this connection from ${client}?` : `Which ${subject} should be used?`, choices, { cancelValue: "back" });
2198
2463
  if (choice === "back") continue;
2199
2464
  prompter.write("\n");
2200
2465
  if (choice === "client") {
2201
- writeWrapped(prompter, `Save the ${client} version in AKMS.`);
2202
- writeWrapped(prompter, `Sync to: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}`);
2203
- } else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : `Copy the saved AKMS ${subject} to ${client}.`);
2204
- const confirmation = choice === "client" ? `Save in AKMS and sync ${state.clients.length} services?` : removing ? `Remove from ${client}?` : `Update ${client} with version 1?`;
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.`);
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?`;
2205
2471
  if (await prompter.askYesNo(confirmation, false)) {
2206
2472
  pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
2207
2473
  }
2208
2474
  } catch (error) {
2209
2475
  if (error instanceof PromptClosedError) throw error;
2210
2476
  if (error instanceof PromptBackError) continue;
2211
- 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.")}
2212
2478
  `);
2213
2479
  await prompter.acknowledge();
2214
2480
  }
2215
2481
  }
2216
2482
  }
2217
-
2218
- // src/modules/registration/targets/index.ts
2219
- import os5 from "os";
2220
- import path10 from "path";
2221
-
2222
- // src/modules/registration/targets/claude-code.ts
2223
- import { spawnSync } from "child_process";
2224
- import fs8 from "fs";
2225
- import os3 from "os";
2226
- import path8 from "path";
2227
-
2228
- // src/modules/registration/paths.ts
2229
- import fs5 from "fs";
2230
- import path6 from "path";
2231
2483
  function findExecutable(name) {
2232
2484
  const searchPath = process.env.PATH ?? "";
2233
- const directories = searchPath.split(path6.delimiter).filter((directory) => directory !== "");
2485
+ const directories = searchPath.split(path2.delimiter).filter((directory) => directory !== "");
2234
2486
  let suffixes = [""];
2235
2487
  if (process.platform === "win32") {
2236
2488
  const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
@@ -2238,25 +2490,17 @@ function findExecutable(name) {
2238
2490
  }
2239
2491
  for (const directory of directories) {
2240
2492
  for (const suffix of suffixes) {
2241
- const candidate = path6.join(directory, `${name}${suffix}`);
2242
- 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) {
2243
2495
  return candidate;
2244
2496
  }
2245
2497
  }
2246
2498
  }
2247
2499
  return null;
2248
2500
  }
2249
-
2250
- // src/modules/registration/targets/json-file.ts
2251
- import fs7 from "fs";
2252
-
2253
- // src/modules/registration/targets/config-file.ts
2254
- import fs6 from "fs";
2255
- import path7 from "path";
2256
- import { randomUUID as randomUUID2 } from "crypto";
2257
2501
  function readExisting2(filePath) {
2258
2502
  try {
2259
- return fs6.readFileSync(filePath, "utf8");
2503
+ return fs.readFileSync(filePath, "utf8");
2260
2504
  } catch (error) {
2261
2505
  if (error.code === "ENOENT") {
2262
2506
  return null;
@@ -2268,45 +2512,45 @@ function updateConfigFile2(filePath, update) {
2268
2512
  let destination = filePath;
2269
2513
  let symbolicLink = false;
2270
2514
  try {
2271
- symbolicLink = fs6.lstatSync(filePath).isSymbolicLink();
2515
+ symbolicLink = fs.lstatSync(filePath).isSymbolicLink();
2272
2516
  } catch (error) {
2273
2517
  if (error.code !== "ENOENT") {
2274
2518
  throw error;
2275
2519
  }
2276
2520
  }
2277
2521
  if (symbolicLink) {
2278
- destination = fs6.realpathSync(filePath);
2522
+ destination = fs.realpathSync(filePath);
2279
2523
  }
2280
2524
  const before = readExisting2(destination);
2281
2525
  const updated = update(before ?? "");
2282
2526
  if (before != null) {
2283
- fs6.accessSync(destination, fs6.constants.W_OK);
2527
+ fs.accessSync(destination, fs.constants.W_OK);
2284
2528
  }
2285
- fs6.mkdirSync(path7.dirname(destination), { recursive: true });
2286
- const temporary = path7.join(path7.dirname(destination), `.${path7.basename(destination)}.${randomUUID2()}.tmp`);
2287
- 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;
2288
2532
  let descriptor;
2289
2533
  let ownsTemporary = false;
2290
2534
  try {
2291
- descriptor = fs6.openSync(temporary, "wx", mode);
2535
+ descriptor = fs.openSync(temporary, "wx", mode);
2292
2536
  ownsTemporary = true;
2293
- fs6.writeFileSync(descriptor, updated, "utf8");
2294
- fs6.fsyncSync(descriptor);
2295
- fs6.closeSync(descriptor);
2537
+ fs.writeFileSync(descriptor, updated, "utf8");
2538
+ fs.fsyncSync(descriptor);
2539
+ fs.closeSync(descriptor);
2296
2540
  descriptor = void 0;
2297
2541
  if (readExisting2(destination) !== before) {
2298
2542
  throw new Error("The config changed while saving; reload it and try again.");
2299
2543
  }
2300
- fs6.renameSync(temporary, destination);
2544
+ fs.renameSync(temporary, destination);
2301
2545
  ownsTemporary = false;
2302
2546
  } finally {
2303
2547
  try {
2304
2548
  if (descriptor != null) {
2305
- fs6.closeSync(descriptor);
2549
+ fs.closeSync(descriptor);
2306
2550
  }
2307
2551
  } finally {
2308
2552
  if (ownsTemporary) {
2309
- fs6.unlinkSync(temporary);
2553
+ fs.unlinkSync(temporary);
2310
2554
  }
2311
2555
  }
2312
2556
  }
@@ -2359,10 +2603,10 @@ function readMcpServersJson(document) {
2359
2603
  return entries;
2360
2604
  }
2361
2605
  function readDocument(filePath, contents) {
2362
- if (contents == null && fs7.existsSync(filePath) == false) {
2606
+ if (contents == null && fs.existsSync(filePath) == false) {
2363
2607
  return {};
2364
2608
  }
2365
- const text = contents ?? fs7.readFileSync(filePath, "utf8");
2609
+ const text = contents ?? fs.readFileSync(filePath, "utf8");
2366
2610
  if (text.trim() === "") {
2367
2611
  return {};
2368
2612
  }
@@ -2387,7 +2631,7 @@ function createJsonFileTarget(options) {
2387
2631
  id,
2388
2632
  label,
2389
2633
  location: filePath,
2390
- detected: fs7.existsSync(filePath),
2634
+ detected: fs.existsSync(filePath),
2391
2635
  readEntries() {
2392
2636
  const document = readDocument(filePath);
2393
2637
  return readMcpServersJson(document);
@@ -2418,7 +2662,7 @@ function createJsonFileTarget(options) {
2418
2662
  }
2419
2663
 
2420
2664
  // src/modules/registration/targets/claude-code.ts
2421
- var CLAUDE_CODE_CONFIG_PATH = path8.join(os3.homedir(), ".claude.json");
2665
+ var CLAUDE_CODE_CONFIG_PATH = path2.join(os5.homedir(), ".claude.json");
2422
2666
  var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
2423
2667
  function toClaudeAddJsonPayload(entry) {
2424
2668
  return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
@@ -2460,10 +2704,10 @@ function runClaude(executable, args) {
2460
2704
  return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
2461
2705
  }
2462
2706
  function readRawClaudeEntries() {
2463
- if (fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
2707
+ if (fs.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
2464
2708
  return {};
2465
2709
  }
2466
- const text = fs8.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
2710
+ const text = fs.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
2467
2711
  let document;
2468
2712
  try {
2469
2713
  document = JSON.parse(text);
@@ -2482,7 +2726,7 @@ function createClaudeCodeTarget() {
2482
2726
  id: "claude-code",
2483
2727
  label: "Claude Code (user scope)",
2484
2728
  location: CLAUDE_CODE_CONFIG_PATH,
2485
- detected: executable != null || fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
2729
+ detected: executable != null || fs.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
2486
2730
  readEntries() {
2487
2731
  return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
2488
2732
  },
@@ -2527,11 +2771,6 @@ function createClaudeCodeTarget() {
2527
2771
  };
2528
2772
  }
2529
2773
 
2530
- // src/modules/registration/targets/codex.ts
2531
- import fs9 from "fs";
2532
- import os4 from "os";
2533
- import path9 from "path";
2534
-
2535
2774
  // src/modules/registration/targets/toml-lines.ts
2536
2775
  function lineState() {
2537
2776
  return { quote: null, multiline: false, depth: 0 };
@@ -2571,10 +2810,6 @@ function scanTomlLine(line, state) {
2571
2810
  }
2572
2811
  }
2573
2812
  }
2574
-
2575
- // src/modules/registration/targets/toml-validation.ts
2576
- import { isDeepStrictEqual } from "util";
2577
- import { parse, TomlError } from "smol-toml";
2578
2813
  function isTable(value) {
2579
2814
  return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
2580
2815
  }
@@ -2825,18 +3060,18 @@ function editMcpServerToml(text, name, entry) {
2825
3060
 
2826
3061
  // src/modules/registration/targets/codex.ts
2827
3062
  function createCodexTarget() {
2828
- 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");
2829
3064
  function readText() {
2830
- if (fs9.existsSync(CODEX_CONFIG_PATH) == false) {
3065
+ if (fs.existsSync(CODEX_CONFIG_PATH) == false) {
2831
3066
  return "";
2832
3067
  }
2833
- return fs9.readFileSync(CODEX_CONFIG_PATH, "utf8");
3068
+ return fs.readFileSync(CODEX_CONFIG_PATH, "utf8");
2834
3069
  }
2835
3070
  return {
2836
3071
  id: "codex",
2837
3072
  label: "Codex CLI",
2838
3073
  location: CODEX_CONFIG_PATH,
2839
- detected: findExecutable("codex") != null || fs9.existsSync(CODEX_CONFIG_PATH) == true,
3074
+ detected: findExecutable("codex") != null || fs.existsSync(CODEX_CONFIG_PATH) == true,
2840
3075
  readEntries() {
2841
3076
  const text = readText();
2842
3077
  return parseMcpServersToml(text);
@@ -2864,20 +3099,20 @@ function createCodexTarget() {
2864
3099
  // src/modules/registration/targets/index.ts
2865
3100
  function claudeDesktopConfigPath() {
2866
3101
  if (process.platform === "win32") {
2867
- const appData = process.env.APPDATA ?? path10.join(os5.homedir(), "AppData", "Roaming");
2868
- 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");
2869
3104
  }
2870
3105
  if (process.platform === "darwin") {
2871
- 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");
2872
3107
  }
2873
- return path10.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
3108
+ return path2.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
2874
3109
  }
2875
3110
  function listConfigTargets() {
2876
3111
  const targets = [
2877
3112
  createClaudeCodeTarget(),
2878
3113
  createCodexTarget(),
2879
3114
  createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
2880
- 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") })
2881
3116
  ];
2882
3117
  const detected = targets.filter((target) => target.detected == true);
2883
3118
  const others = targets.filter((target) => target.detected == false);
@@ -2919,8 +3154,7 @@ ${failure}
2919
3154
  prompter.clearScreen();
2920
3155
  const state = registry.read();
2921
3156
  const clients = state.clients;
2922
- const migration = registry.needsMigration(state);
2923
- if (migration) prompter.write("Registry update available. Choose Migrate registry to review the changes.\n");
3157
+ const runtime = runtimeNotice(registry);
2924
3158
  const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
2925
3159
  { value: "db", label: "Database", hint: "PostgreSQL connections" },
2926
3160
  { value: "ssh", label: "SSH", hint: "Remote hosts" },
@@ -2929,20 +3163,16 @@ ${failure}
2929
3163
  { separator: true },
2930
3164
  { value: "register", label: "Register", hint: clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "Choose AI services once" },
2931
3165
  { value: "sync", label: "Sync", hint: "Review and retry shared settings" },
2932
- { value: "runtime", label: "Runtime", hint: "AKMS installation / replace npx launches" },
2933
- { value: "updates", label: "Updates", hint: "Check installed MCP releases" },
2934
- ...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 },
2935
3167
  { value: "quit", label: "Quit" }
2936
3168
  ], { defaultValue, cancelValue: "quit" });
2937
- if (picked !== "register" && picked !== "sync" && picked !== "runtime" && picked !== "updates" && picked !== "migrate") {
3169
+ if (picked !== "register" && picked !== "sync" && picked !== "runtime") {
2938
3170
  selected = picked;
2939
3171
  break;
2940
3172
  }
2941
3173
  try {
2942
3174
  if (picked === "register") await registrationMenu(prompter, registry, options.targetId);
2943
3175
  else if (picked === "runtime") await runtimeMenu(prompter, registry);
2944
- else if (picked === "updates") await updatesMenu(prompter, registry);
2945
- else if (picked === "migrate") await migrationMenu(prompter, registry);
2946
3176
  else await syncMenu(prompter, registry);
2947
3177
  } catch (error) {
2948
3178
  if (error instanceof PromptClosedError) throw error;
@@ -2971,66 +3201,18 @@ ${error instanceof Error ? error.message : "Could not update settings."}
2971
3201
  }
2972
3202
  }
2973
3203
  }
2974
-
2975
- // src/modules/launcher/npm.ts
2976
- import { execFile as execFile3, spawn as spawn2 } from "child_process";
2977
- import { existsSync } from "fs";
2978
- import { mkdtemp, realpath, rm } from "fs/promises";
2979
- import { tmpdir } from "os";
2980
- import { basename, delimiter, dirname, join } from "path";
2981
- import { promisify as promisify2 } from "util";
2982
- var execFileAsync = promisify2(execFile3);
2983
- async function findNpmCli() {
2984
- const candidates = [];
2985
- const npmExecPath = process.env.npm_execpath;
2986
- if (npmExecPath != null && ["npm-cli.js", "npx-cli.js"].includes(basename(npmExecPath)) == true) {
2987
- candidates.push(join(dirname(npmExecPath), "npm-cli.js"));
2988
- }
2989
- candidates.push(join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"));
2990
- candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
2991
- for (const directory of (process.env.PATH ?? "").split(delimiter)) {
2992
- if (directory === "") {
2993
- continue;
2994
- }
2995
- if (process.platform === "win32") {
2996
- candidates.push(join(directory, "node_modules", "npm", "bin", "npm-cli.js"));
2997
- } else {
2998
- 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`)] };
2999
3210
  }
3000
- }
3001
- for (const candidate of candidates) {
3002
- if (existsSync(candidate) == true) {
3003
- const resolved = await realpath(candidate);
3004
- 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.");
3005
3214
  }
3006
- }
3007
- throw new Error("npm could not be found. Install Node.js with npm, then reopen your terminal.");
3008
- }
3009
- async function resolveLatestVersion(options) {
3010
- const metadataCache = await mkdtemp(join(tmpdir(), "akms-mcp-latest-"));
3011
- try {
3012
- const result = await execFileAsync(process.execPath, [
3013
- options.npmCli,
3014
- "view",
3015
- `${options.packageName}@latest`,
3016
- "version",
3017
- "--json",
3018
- "--prefer-online",
3019
- "--prefer-offline=false",
3020
- "--offline=false",
3021
- "--fetch-retries=0",
3022
- "--fetch-timeout=15000",
3023
- "--update-notifier=false",
3024
- "--cache",
3025
- metadataCache
3026
- ], { signal: options.signal, timeout: 2e4, windowsHide: true });
3027
- const version = JSON.parse(result.stdout);
3028
- if (typeof version !== "string" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) == false) {
3029
- throw new Error("npm returned an invalid latest version.");
3030
- }
3031
- return version;
3032
- } finally {
3033
- await rm(metadataCache, { recursive: true, force: true });
3215
+ directory = parent;
3034
3216
  }
3035
3217
  }
3036
3218
  async function launchMcp(options, dependencies = {}) {
@@ -3046,20 +3228,20 @@ async function launchMcp(options, dependencies = {}) {
3046
3228
  const prompter = new Prompter(dependencies.input ?? process.stdin, output, pendingInput);
3047
3229
  try {
3048
3230
  prompter.write(`
3049
- ${selected.packageName} is not installed in the AKMS runtime directory.
3231
+ ${PACKAGE_NAME} is not installed in the AKMS runtime directory.
3050
3232
  `);
3051
3233
  if (!await prompter.askYesNo("Install the AKMS runtime before opening setup?", true)) return 0;
3052
3234
  try {
3053
- prompter.write(`Installing ${selected.packageName}...
3235
+ prompter.write(`Installing ${PACKAGE_NAME}...
3054
3236
  `);
3055
- await (dependencies.installRuntime ?? installGlobalRuntime)(options.mcpId);
3237
+ await (dependencies.installRuntime ?? installGlobalRuntime)();
3056
3238
  runtime = findRuntime(options.mcpId);
3057
3239
  if (!runtime) throw new Error("Installation completed but the AKMS runtime could not be found.");
3058
3240
  } catch (error) {
3059
3241
  if (error instanceof Error && error.message === "AKMS runtime installation cancelled.") return 130;
3060
3242
  prompter.write(`${error instanceof Error ? error.message : "AKMS runtime installation failed."}
3061
3243
  `);
3062
- 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;
3063
3245
  }
3064
3246
  } catch (error) {
3065
3247
  if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
@@ -3069,61 +3251,16 @@ ${selected.packageName} is not installed in the AKMS runtime directory.
3069
3251
  pendingInput = prompter.takePendingInput();
3070
3252
  }
3071
3253
  }
3072
- if (runtime) {
3073
- const argumentsList = [...runtime.args, selected.setupCommand];
3074
- if (options.targetId) argumentsList.push("--target", options.targetId);
3075
- 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)" : ""}
3076
3258
 
3077
3259
  `);
3078
- return (dependencies.runChild ?? runChild)(runtime.command, argumentsList, pendingInput);
3079
- }
3080
- return launchLatestSetup(options, dependencies, pendingInput);
3081
- }
3082
- async function launchLatestSetup(options, dependencies, pendingInput) {
3083
- const selected = MCP_PACKAGES[options.mcpId];
3084
- const output = dependencies.output ?? process.stdout;
3085
- const npmCli2 = await (dependencies.findNpmCli ?? findNpmCli)();
3086
- const cancellation = new AbortController();
3087
- const cancelLookup = () => {
3088
- cancellation.abort();
3089
- };
3090
- process.on("SIGINT", cancelLookup);
3091
- process.on("SIGTERM", cancelLookup);
3092
- let version;
3093
- try {
3094
- output.write(`
3095
- Checking the latest ${selected.packageName} release...
3096
- `);
3097
- version = await (dependencies.resolveLatestVersion ?? resolveLatestVersion)({ npmCli: npmCli2, packageName: selected.packageName, signal: cancellation.signal });
3098
- } catch (error) {
3099
- if (cancellation.signal.aborted == true) {
3100
- return 130;
3101
- }
3102
- throw new Error(`Could not check the latest ${selected.packageName} release; setup was not started.
3103
- ${String(error)}`, { cause: error });
3104
- } finally {
3105
- process.off("SIGINT", cancelLookup);
3106
- process.off("SIGTERM", cancelLookup);
3107
- }
3108
- const argumentsList = [
3109
- npmCli2,
3110
- "exec",
3111
- "--yes",
3112
- "--prefer-online",
3113
- "--prefer-offline=false",
3114
- "--offline=false",
3115
- `--package=${selected.packageName}@${version}`,
3116
- "--",
3117
- selected.command,
3118
- selected.setupCommand
3119
- ];
3120
- if (options.targetId != null) {
3121
- argumentsList.push("--target", options.targetId);
3122
- }
3123
- 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
3124
3261
 
3125
3262
  `);
3126
- return (dependencies.runChild ?? runChild)(process.execPath, argumentsList, pendingInput);
3263
+ return (dependencies.runChild ?? runChild)(launch.command, argumentsList, pendingInput);
3127
3264
  }
3128
3265
  async function runChild(command, argumentsList, pendingInput) {
3129
3266
  return new Promise((resolve, reject) => {
@@ -3132,9 +3269,10 @@ async function runChild(command, argumentsList, pendingInput) {
3132
3269
  if (interactiveInput == true) {
3133
3270
  inputMode = "inherit";
3134
3271
  }
3135
- const child = spawn2(command, argumentsList, {
3272
+ const child = spawn(command, argumentsList, {
3136
3273
  stdio: [inputMode, "inherit", "inherit"],
3137
- windowsHide: true
3274
+ windowsHide: true,
3275
+ env: { ...process.env, AKMS_MCP_MENU: "1" }
3138
3276
  });
3139
3277
  const interrupt = () => {
3140
3278
  child.kill("SIGINT");
@@ -3206,11 +3344,7 @@ async function main() {
3206
3344
  return;
3207
3345
  }
3208
3346
  if (argumentsList.showVersion == true) {
3209
- let version = "development";
3210
- if (true) {
3211
- version = "0.3.1";
3212
- }
3213
- process.stdout.write(`${version}
3347
+ process.stdout.write(`${PACKAGE_VERSION}
3214
3348
  `);
3215
3349
  return;
3216
3350
  }
@@ -3234,3 +3368,5 @@ main().catch((error) => {
3234
3368
  `);
3235
3369
  process.exitCode = 1;
3236
3370
  });
3371
+ //# sourceMappingURL=cli.js.map
3372
+ //# sourceMappingURL=cli.js.map