@akms/mcp-ssh 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -27,7 +27,7 @@ var SHUTDOWN_DEADLINE_MS = 5e3;
27
27
  var CWD_MARKER = "__AKMS_SSH_CWD__";
28
28
  var MAX_DIRECTORY_ENTRIES = 1e3;
29
29
  var SERVER_NAME = "akms-mcp-ssh";
30
- var SERVER_VERSION = "0.3.1";
30
+ var SERVER_VERSION = "0.3.2";
31
31
 
32
32
  // src/_defs/command-rules.ts
33
33
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -960,6 +960,8 @@ function createStyle(enabled) {
960
960
  return {
961
961
  accent: paint(36, 39),
962
962
  success: paint(32, 39),
963
+ warning: paint(33, 39),
964
+ error: paint(31, 39),
963
965
  dim: paint(2, 22),
964
966
  bold: paint(1, 22)
965
967
  };
@@ -1507,7 +1509,7 @@ var fieldName = (field) => {
1507
1509
  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" };
1508
1510
  return Object.hasOwn(names, key) ? names[key] : plainText(key);
1509
1511
  };
1510
- function writeWrapped(prompter, text, indent2 = "", changed) {
1512
+ function writeWrapped(prompter, text, indent2 = "", changed, style) {
1511
1513
  const characters = Array.from(plainText(text));
1512
1514
  let line = indent2;
1513
1515
  let width = displayWidth(indent2);
@@ -1522,7 +1524,7 @@ function writeWrapped(prompter, text, indent2 = "", changed) {
1522
1524
  const size = displayWidth(character);
1523
1525
  if (width + size > prompter.width) {
1524
1526
  flush();
1525
- prompter.write(line + "\n");
1527
+ prompter.write((style ? style(line) : line) + "\n");
1526
1528
  line = indent2;
1527
1529
  width = displayWidth(indent2);
1528
1530
  }
@@ -1535,7 +1537,7 @@ function writeWrapped(prompter, text, indent2 = "", changed) {
1535
1537
  width += size;
1536
1538
  }
1537
1539
  flush();
1538
- prompter.write(line + "\n");
1540
+ prompter.write((style ? style(line) : line) + "\n");
1539
1541
  }
1540
1542
  function changedRange(values, index) {
1541
1543
  if (values.length !== 2 || values[0] === values[1]) return void 0;
@@ -1557,6 +1559,37 @@ function compareEntries(prompter, versions) {
1557
1559
  const key = field.replace(/^env\./, "");
1558
1560
  return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
1559
1561
  });
1562
+ if (versions.length === 2 && prompter.width >= 90) {
1563
+ const width = Math.floor((prompter.width - 3) / 2);
1564
+ const wrap = (value, changed) => {
1565
+ const lines = [{ text: "", width: 0 }];
1566
+ for (const [index, character] of Array.from(value).entries()) {
1567
+ const size = displayWidth(character);
1568
+ if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
1569
+ const line = lines[lines.length - 1];
1570
+ line.text += changed && index >= changed[0] && index < changed[1] ? prompter.paint.accent(prompter.paint.bold(character)) : character;
1571
+ line.width += size;
1572
+ }
1573
+ return lines;
1574
+ };
1575
+ const row = (left, right) => {
1576
+ for (let line = 0; line < Math.max(left.length, right.length); line++) {
1577
+ prompter.write(`${left[line]?.text ?? ""}${" ".repeat(width - (left[line]?.width ?? 0))} | ${right[line]?.text ?? ""}
1578
+ `);
1579
+ }
1580
+ };
1581
+ prompter.write("\n");
1582
+ row(wrap(`[1] ${plainText(versions[0].label)}`), wrap(`[2] ${plainText(versions[1].label)}`));
1583
+ for (let field = 0; field < fields.length; field++) {
1584
+ prompter.write(`
1585
+ ${prompter.paint.dim(fieldName(fields[field]))}
1586
+ `);
1587
+ row(wrap(values[field][0], changedRange(values[field], 0)), wrap(values[field][1], changedRange(values[field], 1)));
1588
+ }
1589
+ if (values.some((row2) => row2.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
1590
+ prompter.write("\n");
1591
+ return;
1592
+ }
1560
1593
  for (let index = 0; index < versions.length; index++) {
1561
1594
  prompter.write("\n");
1562
1595
  writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
@@ -1763,10 +1796,10 @@ async function checkConnections(prompter, entries, check = checkStartup) {
1763
1796
  const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
1764
1797
  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.` };
1765
1798
  const safe = (value) => [entry, ...maskEntries ?? []].reduce((text, source) => redactStartupText(text, source), value);
1766
- 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."}`));
1799
+ 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);
1767
1800
  if (!result.ok) {
1768
1801
  failed = true;
1769
- for (const line of result.stderr) writeWrapped(prompter, safe(line), " ");
1802
+ for (const line of result.stderr) writeWrapped(prompter, safe(line), " ", void 0, prompter.paint.dim);
1770
1803
  }
1771
1804
  }
1772
1805
  if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
@@ -2229,11 +2262,26 @@ var Registry = class {
2229
2262
  inspect() {
2230
2263
  return this.reconcile(this.read(), false);
2231
2264
  }
2265
+ formatResults(results, prefix = "", paint, reviewHint) {
2266
+ return formatResults(results, prefix, {
2267
+ paint,
2268
+ reviewHint,
2269
+ connectionCount: Object.values(this.read().connections).filter((item) => item.entry).length
2270
+ });
2271
+ }
2232
2272
  reconcile(state, apply, kinds) {
2233
2273
  const results = [];
2234
2274
  const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
2235
2275
  for (const kind of kinds ?? KINDS) if (this.isSessionMode(kind, state) && active(kind).length && !state.sessions?.[kind]) {
2236
- 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." });
2276
+ for (const client of state.clients) results.push({
2277
+ client,
2278
+ name: sessionName(kind),
2279
+ kind,
2280
+ connections: active(kind).map(([name]) => name),
2281
+ status: "pending",
2282
+ waitingFor: "runtime",
2283
+ detail: "Install it from Runtime, then run Sync. Your existing entries keep working."
2284
+ });
2237
2285
  }
2238
2286
  const work = [
2239
2287
  ...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
@@ -2249,7 +2297,15 @@ var Registry = class {
2249
2297
  const previous = connection.applied[client];
2250
2298
  if (!desired && !previous) continue;
2251
2299
  const target = this.targets.find((candidate) => candidate.id === client);
2252
- const report = (status, detail) => results.push({ client, name, status, detail });
2300
+ const report = (status, detail, waitingFor) => results.push({
2301
+ client,
2302
+ name,
2303
+ status,
2304
+ detail,
2305
+ kind: connection.kind,
2306
+ connections: bundle ? active(connection.kind).map(([name2]) => name2) : [name],
2307
+ ...waitingFor ? { waitingFor } : {}
2308
+ });
2253
2309
  if (!target) {
2254
2310
  report("error", "Service adapter unavailable; use a newer package to retry.");
2255
2311
  continue;
@@ -2267,20 +2323,18 @@ var Registry = class {
2267
2323
  report("removed", "Removed");
2268
2324
  } else if (exists && (!actual || !previous || hash !== previous)) {
2269
2325
  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.");
2270
- } else if (!apply) {
2271
- report("pending", desired ? "Needs applying" : "Needs removal");
2272
2326
  } else {
2273
2327
  if (!desired && state.clients.includes(client)) {
2274
2328
  if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
2275
2329
  const session = state.sessions?.[connection.kind];
2276
2330
  const replacement2 = entries[sessionName(connection.kind)];
2277
2331
  if (!session || !replacement2 || entryHash(replacement2) !== entryHash(session.entry) || session.applied[client] !== entryHash(session.entry)) {
2278
- report("pending", "Connection entry kept until the session server is applied.");
2332
+ report("pending", "Connection entry kept until the session server is applied.", "session");
2279
2333
  continue;
2280
2334
  }
2281
2335
  }
2282
2336
  if (bundle && !this.isSessionMode(connection.kind, state) && active(connection.kind).some(([connectionName, value]) => !entries[connectionName] || entryHash(entries[connectionName]) !== entryHash(value.entry))) {
2283
- report("pending", "Session entry kept until all connection entries are applied.");
2337
+ report("pending", "Session entry kept until all connection entries are applied.", "connections");
2284
2338
  continue;
2285
2339
  }
2286
2340
  }
@@ -2291,7 +2345,11 @@ var Registry = class {
2291
2345
  replacement = state.connections[replacement.replacement];
2292
2346
  }
2293
2347
  if (!desired && state.clients.includes(client) && !this.isSessionMode(connection.kind, state) && replacement && replacement.entry && replacement.applied[client] !== entryHash(replacement.entry)) {
2294
- report("pending", "Old name kept until the new name is applied.");
2348
+ report("pending", "Old name kept until the new name is applied.", "rename");
2349
+ continue;
2350
+ }
2351
+ if (!apply) {
2352
+ report("pending", desired ? "Needs applying" : "Needs removal");
2295
2353
  continue;
2296
2354
  }
2297
2355
  const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
@@ -2382,7 +2440,7 @@ var Registry = class {
2382
2440
  const registry = this;
2383
2441
  let snapshot;
2384
2442
  const expected = (name) => snapshot ? own(snapshot, name) ? entryHash(snapshot[name]) : null : void 0;
2385
- const saved = (results) => ({ status: "written", detail: formatResults(results, "Saved in the shared AKMS catalog.") });
2443
+ const saved = (results) => ({ status: "written", detail: registry.formatResults(results, "Saved in the shared AKMS catalog.") });
2386
2444
  const prepare = (name, entry, oldName) => {
2387
2445
  if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
2388
2446
  const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
@@ -2411,30 +2469,58 @@ var Registry = class {
2411
2469
  };
2412
2470
  }
2413
2471
  };
2414
- function formatResults(results, prefix = "") {
2415
- const lines = prefix ? [prefix] : [];
2416
- if (!results.length) return [...lines, " No shared connections need applying."].join("\n");
2417
- const attention = results.filter((item) => !["synced", "removed"].includes(item.status));
2418
- const total = new Set(results.map((item) => item.name)).size;
2419
- const unresolved = new Set(attention.map((item) => item.name)).size;
2420
- lines.push(` Connections: ${total - unresolved} complete \xB7 ${unresolved} need attention`);
2472
+ function formatResults(results, prefix = "", options = {}) {
2473
+ const plain = (text) => text;
2474
+ const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
2475
+ const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
2476
+ const lines = prefix ? [paint.bold(prefix)] : [];
2477
+ const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
2478
+ lines.push(` Connections: ${total}`);
2421
2479
  const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
2422
2480
  const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
2423
2481
  const removed = results.filter((item) => item.status === "removed").length;
2424
- lines.push(` Service entries: ${updated} updated \xB7 ${unchanged} already up to date${removed ? ` \xB7 ${removed} removed` : ""}`);
2425
- for (const [heading, rows] of [
2426
- ["Needs review (existing settings kept)", attention.filter((item) => item.status === "conflict")],
2427
- ["Could not finish", attention.filter((item) => item.status !== "conflict")]
2482
+ const failed = results.filter((item) => item.status === "error");
2483
+ lines.push(` Service entries: ${paint.success(`${updated} updated \xB7 ${unchanged} already up to date`)}${removed ? ` \xB7 ${paint.success(`${removed} removed`)}` : ""} \xB7 ${(failed.length ? paint.error : paint.dim)(`${failed.length} failed`)}`);
2484
+ if (!results.length) lines.push(paint.dim(" No shared connections need applying."));
2485
+ if (results.length && results.every((item) => item.status === "synced" || item.status === "removed")) lines.push(paint.success(" OK: all service entries are up to date."));
2486
+ const waiting = results.filter((item) => item.status === "pending");
2487
+ for (const kind of KINDS) {
2488
+ const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
2489
+ const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
2490
+ if (!runtime.length && !kept.length) continue;
2491
+ lines.push(`
2492
+ ${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` : `Waiting for ${sessionName(kind)}`)}`);
2493
+ if (runtime.length) lines.push(" Install it from Runtime, then run Sync. Your existing entries keep working.");
2494
+ else lines.push(" Resolve this session entry in Sync. Existing connection entries stay until it is ready.");
2495
+ const retained = /* @__PURE__ */ new Map();
2496
+ for (const client of new Set(kept.map((item) => item.client))) {
2497
+ const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
2498
+ const key = JSON.stringify([...names].sort());
2499
+ const group = retained.get(key);
2500
+ if (group) group.clients.push(client);
2501
+ else retained.set(key, { clients: [client], names });
2502
+ }
2503
+ for (const group of retained.values()) {
2504
+ const clients = group.clients.map((client) => CLIENT_LABELS[client]).join(" \xB7 ");
2505
+ lines.push(paint.dim(` Kept in ${clients} until ${sessionName(kind)} is applied: ${group.names.map(safe).join(", ")}`));
2506
+ }
2507
+ }
2508
+ for (const [heading, rows, color] of [
2509
+ ["Needs review (existing settings kept)", results.filter((item) => item.status === "conflict"), paint.warning],
2510
+ ["Waiting for Sync", waiting.filter((item) => !item.waitingFor || !["runtime", "session"].includes(item.waitingFor)), paint.warning],
2511
+ ["Could not finish", failed, paint.error]
2428
2512
  ]) {
2429
2513
  if (!rows.length) continue;
2430
2514
  lines.push(`
2431
- ${heading}:`);
2515
+ ${color(`${heading}:`)}`);
2432
2516
  for (const name of new Set(rows.map((item) => item.name))) {
2433
- lines.push(` ${name.replace(/[\x00-\x1f\x7f]/g, " ")}`);
2434
- for (const row of rows.filter((item) => item.name === name)) lines.push(` ${CLIENT_LABELS[row.client]}: ${row.detail}`);
2517
+ lines.push(` ${safe(name)}`);
2518
+ for (const row of rows.filter((item) => item.name === name)) lines.push(` ${row.status === "error" ? paint.error("FAILED ") : ""}${CLIENT_LABELS[row.client]}: ${safe(row.detail)}`);
2435
2519
  }
2436
2520
  }
2437
- if (attention.length) lines.push("\nNext: open Sync to compare settings or retry unfinished changes.");
2521
+ if (results.some((item) => item.status === "conflict")) lines.push(paint.warning(`
2522
+ ${options.reviewHint ?? "Next: open Sync to compare settings and choose which version to use."}`));
2523
+ if (failed.length) lines.push(paint.error("\nNext: fix the errors above, then run Sync again."));
2438
2524
  return lines.join("\n");
2439
2525
  }
2440
2526
 
@@ -2503,7 +2589,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
2503
2589
  prompter.write(`New kinds will use ${mode === "catalog" ? "session servers" : "individual entries"}. Existing kinds keep their settings.
2504
2590
  `);
2505
2591
  if (await prompter.askYesNo("Change the default for new kinds?", false)) {
2506
- prompter.write(`${formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.")}
2592
+ prompter.write(`${registry.formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.", prompter.paint)}
2507
2593
  `);
2508
2594
  await prompter.acknowledge();
2509
2595
  }
@@ -2532,7 +2618,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
2532
2618
  if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
2533
2619
  const results2 = registry.applySession(plan2);
2534
2620
  prompter.clearScreen();
2535
- prompter.write(`${formatResults(results2, "Configuration mode saved.")}
2621
+ prompter.write(`${registry.formatResults(results2, "Configuration mode saved.", prompter.paint)}
2536
2622
  `);
2537
2623
  await registry.checkSaved(prompter, plan2.names);
2538
2624
  prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
@@ -2568,7 +2654,7 @@ Convert ${plan.names.length} connection(s):
2568
2654
  if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
2569
2655
  const results = registry.applyRuntime(plan);
2570
2656
  prompter.clearScreen();
2571
- prompter.write(`${formatResults(results, "Runtime settings saved.")}
2657
+ prompter.write(`${registry.formatResults(results, "Runtime settings saved.", prompter.paint)}
2572
2658
 
2573
2659
  Restart or reconnect AI clients to replace running npx processes.
2574
2660
  `);
@@ -2690,7 +2776,7 @@ async function migrationMenu(prompter, registry) {
2690
2776
  if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
2691
2777
  const results = registry.applyMigration(plan);
2692
2778
  prompter.clearScreen();
2693
- prompter.write(`${formatResults(results, "Registry migration saved.")}
2779
+ prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
2694
2780
  `);
2695
2781
  await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
2696
2782
  prompter.write("Reconnect affected clients after changes to their server layout. Fix startup failures first.\n");
@@ -2705,6 +2791,12 @@ async function migrationMenu(prompter, registry) {
2705
2791
  }
2706
2792
 
2707
2793
  // src/modules/registration/menu.ts
2794
+ function pendingImports(registry, kind) {
2795
+ const state = registry.read();
2796
+ const entries = registry.candidates(kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name));
2797
+ const names = [...new Set(entries.map((candidate) => candidate.name))];
2798
+ return { names, differing: names.filter((name) => new Set(entries.filter((item) => item.name === name).map((item) => entryHash(item.entry))).size > 1).length };
2799
+ }
2708
2800
  function preview(prompter, entry) {
2709
2801
  if (!entry) {
2710
2802
  prompter.write(" Remove this connection from the service.\n");
@@ -2735,22 +2827,21 @@ var ManagedSession = class {
2735
2827
  }
2736
2828
  writeStatus(prompter) {
2737
2829
  const state = this.registry.read();
2738
- if (this.registry.needsMigration(state)) prompter.write("Registry update available. Choose Migrate registry to review the changes.\n");
2830
+ if (this.registry.needsMigration(state)) prompter.write(prompter.paint.warning("Registry update available. Choose Migrate registry to review the changes.\n"));
2739
2831
  prompter.write(`Services: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}
2740
2832
  `);
2741
- if (!state.clients.length) prompter.write("No AI services registered. Choose Register before adding a connection.\n");
2742
- if (this.registry.isSessionMode(this.options.kind, state)) prompter.write(`Session server: ${sessionName(this.options.kind)}. Reconnect clients after changing connections.
2743
- `);
2744
- const attention = new Set(this.registry.inspect().filter((item) => this.registry.kindForName(item.name, state) === this.options.kind && !["synced", "removed"].includes(item.status)).map((item) => item.name)).size;
2745
- if (attention) prompter.write(`Needs attention: ${attention} connection(s). Open Sync to review.
2746
- `);
2833
+ if (!state.clients.length) prompter.write(prompter.paint.warning("No AI services registered. Choose Register before adding a connection.\n"));
2834
+ if (this.registry.isSessionMode(this.options.kind, state)) prompter.write(prompter.paint.dim(`Session server: ${sessionName(this.options.kind)}. Reconnect clients after changing connections.
2835
+ `));
2836
+ const attention = new Set(this.registry.inspect().filter((item) => this.registry.kindForName(item.name, state) === this.options.kind && !["synced", "removed"].includes(item.status)).flatMap((item) => item.connections ?? [item.name])).size;
2837
+ if (attention) prompter.write(prompter.paint.warning(`Needs attention: ${attention} connection(s). Open Sync to review.
2838
+ `));
2747
2839
  const pending = this.pendingImports();
2748
- if (pending) prompter.write(`Import existing connections: ${pending} connection(s) available. Existing settings are preserved.
2749
- `);
2840
+ if (pending) prompter.write(prompter.paint.warning(`Import available: ${pending} connection(s). Open Import existing connections.
2841
+ `));
2750
2842
  }
2751
2843
  pendingImports() {
2752
- const state = this.registry.read();
2753
- return new Set(this.registry.candidates(this.options.kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name)).map((candidate) => candidate.name)).size;
2844
+ return pendingImports(this.registry, this.options.kind).names.length;
2754
2845
  }
2755
2846
  items() {
2756
2847
  return [
@@ -2868,8 +2959,8 @@ Remove only AKMS-managed entries from these services; keep the shared catalog.
2868
2959
  const results = registry.register([...selected], initial.clients, plan.imports);
2869
2960
  prompter.clearScreen();
2870
2961
  prompter.write(`
2871
- ${formatResults(results, `Registration saved.
2872
- AI services: ${[...selected].map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}`)}
2962
+ ${registry.formatResults(results, `Registration saved.
2963
+ AI services: ${[...selected].map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}`, prompter.paint)}
2873
2964
  `);
2874
2965
  if (importNames.length) prompter.write(`Imported ${importNames.length} existing connection(s) into the shared list.
2875
2966
  `);
@@ -2942,7 +3033,7 @@ Import existing connections:
2942
3033
  if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
2943
3034
  const results2 = registry.register(state.clients, state.clients, matching.imports);
2944
3035
  prompter.clearScreen();
2945
- prompter.write(`${formatResults(results2, "Import complete")}
3036
+ prompter.write(`${registry.formatResults(results2, "Import complete", prompter.paint)}
2946
3037
  `);
2947
3038
  await prompter.acknowledge();
2948
3039
  }
@@ -2970,7 +3061,7 @@ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2970
3061
  const results = registry.importEntry(destination, chosen, candidates);
2971
3062
  prompter.clearScreen();
2972
3063
  prompter.write(`
2973
- ${formatResults(results, "Import complete")}
3064
+ ${registry.formatResults(results, "Import complete", prompter.paint)}
2974
3065
  `);
2975
3066
  if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
2976
3067
  await prompter.acknowledge();
@@ -2990,19 +3081,28 @@ async function syncMenu(prompter, registry) {
2990
3081
  const results = pendingResults ?? registry.sync();
2991
3082
  pendingResults = void 0;
2992
3083
  prompter.write(`
2993
- ${formatResults(results, "Sync results")}
3084
+ ${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
2994
3085
  `);
3086
+ const imports = pendingImports(registry);
3087
+ if (imports.names.length) prompter.write(prompter.paint.warning(`
3088
+ Not yet imported: ${imports.names.length} connection(s)${imports.differing === imports.names.length ? " with differing settings" : ""} - open Import existing connections.
3089
+ `));
2995
3090
  const conflicts = results.filter((result) => result.status === "conflict");
2996
- if (!conflicts.length) {
3091
+ if (!conflicts.length && !imports.names.length) {
2997
3092
  await prompter.acknowledge();
2998
3093
  return;
2999
3094
  }
3000
3095
  const picked = await prompter.select("Review differences", [
3001
3096
  ...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS[item.client]} \xB7 ${item.name}` })),
3097
+ ...imports.names.length ? [{ value: "import", key: "import", label: "Import existing connections", hint: `${imports.names.length} connection(s) to review` }] : [],
3002
3098
  { value: "back", key: "b", label: "Later \u2014 keep existing settings" }
3003
3099
  ], { cancelValue: "back" });
3004
3100
  if (picked === "back") return;
3005
3101
  try {
3102
+ if (picked === "import") {
3103
+ await importMenu(prompter, registry);
3104
+ continue;
3105
+ }
3006
3106
  prompter.clearScreen();
3007
3107
  const conflict = conflicts[Number(picked)];
3008
3108
  const state = registry.read();
@@ -3036,12 +3136,17 @@ ${formatResults(results, "Sync results")}
3036
3136
  const client = CLIENT_LABELS[conflict.client];
3037
3137
  const registered = state.clients.includes(conflict.client);
3038
3138
  const sessionMode = registry.isSessionMode(connection.kind, state);
3039
- const removing = !connection.entry || !registered || sessionMode;
3139
+ const removing = !connection.entry || !registered;
3140
+ const retiring = sessionMode && !removing;
3040
3141
  const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
3041
3142
  writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
3042
3143
  writeWrapped(prompter, `Registered services: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}`);
3043
3144
  prompter.write("\n");
3044
3145
  if (!removing && connection.entry) {
3146
+ if (retiring) {
3147
+ writeWrapped(prompter, `Choose settings for ${sessionName(connection.kind)} before removing the old ${client} entry.`);
3148
+ writeWrapped(prompter, "The old entry stays until the session server is ready.");
3149
+ }
3045
3150
  compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
3046
3151
  } else {
3047
3152
  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.");
@@ -3053,8 +3158,8 @@ Currently in ${client}
3053
3158
  prompter.write("\n");
3054
3159
  }
3055
3160
  const choices = [
3056
- { value: "shared", label: removing ? `Remove from ${client}` : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : `replace its ${subject} with version 1` },
3057
- ...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" }] : [],
3161
+ { 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` },
3162
+ ...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" }] : [],
3058
3163
  { value: "back", key: "b", label: "Back without changes" }
3059
3164
  ];
3060
3165
  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" });
@@ -3063,8 +3168,9 @@ Currently in ${client}
3063
3168
  if (choice === "client") {
3064
3169
  writeWrapped(prompter, `Save the ${client} version in AKMS.`);
3065
3170
  writeWrapped(prompter, `Sync to: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ")}`);
3066
- } else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : `Copy the saved AKMS ${subject} to ${client}.`);
3067
- const confirmation = choice === "client" ? `Save in AKMS and sync ${state.clients.length} services?` : removing ? `Remove from ${client}?` : `Update ${client} with version 1?`;
3171
+ } else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : retiring ? `Keep the saved AKMS ${subject}.` : `Copy the saved AKMS ${subject} to ${client}.`);
3172
+ if (retiring) writeWrapped(prompter, `Keep the old ${client} entry until ${sessionName(connection.kind)} is ready, then remove it.`);
3173
+ 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?`;
3068
3174
  if (await prompter.askYesNo(confirmation, false)) {
3069
3175
  pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
3070
3176
  }