@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/CHANGELOG.md +10 -0
- package/README.md +9 -1
- package/lib/cli.cjs +161 -55
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +161 -55
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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({
|
|
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({
|
|
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
|
|
2416
|
-
|
|
2417
|
-
const
|
|
2418
|
-
const
|
|
2419
|
-
const
|
|
2420
|
-
lines.push(` Connections: ${total
|
|
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
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
|
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 (
|
|
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)).
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
}
|