@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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@akms/mcp-ssh`. Dates are `YYYY-MM-DD`.
|
|
4
4
|
|
|
5
|
+
## [0.3.2] - 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Point Sync review instructions to its current menu and name the services retaining old connection entries, grouping only identical retained lists.
|
|
10
|
+
- Separate runtime waits from failed changes in Register and Sync. Group retained connection names, count saved connections separately from service entries, and show semantic colors only on interactive terminals without NO_COLOR.
|
|
11
|
+
- Offer unimported connections directly from Sync and compare both saved and external settings before resolving an individual entry during session migration. Wide terminals show versions side by side; narrow terminals keep wrapped values.
|
|
12
|
+
- Preserve existing terminal symbols and keep status words visible when colors are disabled.
|
|
13
|
+
- Document the upgrade path from 0.2.x, including pending runtimes, differing imported settings, reconnecting clients, rollback and registry migration.
|
|
14
|
+
|
|
5
15
|
## [0.3.1] - 2026-10-26
|
|
6
16
|
|
|
7
17
|
### Verification
|
package/README.md
CHANGED
|
@@ -18,6 +18,14 @@ akms-mcp-ssh --help
|
|
|
18
18
|
akms-mcp-ssh --version # show the installed version
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Upgrading from 0.2.x
|
|
22
|
+
|
|
23
|
+
Open the current setup with `npx -y --prefer-online @akms/mcp@latest`, install this runtime from **Runtime**, then use **Register > Apply registration** to import existing entries. Compare differing descriptions when prompted, or use **Import existing connections** later.
|
|
24
|
+
|
|
25
|
+
If you register before installing the runtime, existing entries are kept in a normal pending state. Install the runtime, run **Sync**, then restart or reconnect your client and check `ssh_catalog_status`.
|
|
26
|
+
|
|
27
|
+
See the launcher's [full upgrade guide](https://github.com/juncha9/npm.akms.mcp/blob/main/README.md#upgrading-from-02x) for the comparison steps, Codex thread cleanup, rollback and **Migrate registry** for older development catalogs.
|
|
28
|
+
|
|
21
29
|
## Minimum working configuration
|
|
22
30
|
|
|
23
31
|
Install the server from **Runtime** before using these manual examples. On Windows its script is `<AKMS_MCP_HOME or ~/.akms/mcp>/runtime/node_modules/@akms/mcp-ssh/lib/cli.js`; on macOS/Linux insert `lib/` before `node_modules`. Replace the placeholder below with that absolute path. Use the Node executable from `node -p "process.execPath"` as `command`. The `config` command writes both paths for you.
|
|
@@ -62,7 +70,7 @@ Managed connections start Node directly using a dedicated installation at `<AKMS
|
|
|
62
70
|
|
|
63
71
|
Before Add or a standard-launch Edit, setup installs a missing runtime with `npm install --global --prefix "<AKMS runtime directory>" --prefer-online --no-fund --no-audit @akms/mcp-ssh@latest`. If the installed version is older than this setup package, it offers an update (default Yes); declining continues with the installed version. Linked development packages skip this prompt. Host-key saves for standard launches also check the runtime before writing. Custom launch commands keep their existing command and require no managed-runtime installation. If an update fails but an installed runtime is still usable, setup reports the error and continues with that version. Servers do not invoke npm or query updates when AI clients connect.
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
For an existing catalog that still uses individual entries, open `akms-mcp-ssh config`, choose **Runtime**, install the runtime if needed, then choose **Use for existing connections**. Both legacy npx entries and direct paths in ordinary global installations can be converted. Review and apply the list, then **restart or reconnect the AI clients** to replace running processes. Connection values and credentials stay intact; custom commands and external client edits are preserved for individual review. Register/import legacy entries first if they are missing from the shared list. Version-pinned npx entries switch to the installed version shown in the preview.
|
|
66
74
|
|
|
67
75
|
**Runtime > Update runtime installation** explicitly installs the latest release into this same directory. The Node executable keeps its existing alias path, allowing nvm-windows to switch Node versions while the package path stays fixed. If the Node command path or `AKMS_MCP_HOME` moves, reapply current paths from Runtime. Removing the runtime directory breaks entries that use it.
|
|
68
76
|
|
package/lib/cli.cjs
CHANGED
|
@@ -35,7 +35,7 @@ var SHUTDOWN_DEADLINE_MS = 5e3;
|
|
|
35
35
|
var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
36
36
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
37
37
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
38
|
-
var SERVER_VERSION = "0.3.
|
|
38
|
+
var SERVER_VERSION = "0.3.2";
|
|
39
39
|
|
|
40
40
|
// src/_defs/command-rules.ts
|
|
41
41
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -968,6 +968,8 @@ function createStyle(enabled) {
|
|
|
968
968
|
return {
|
|
969
969
|
accent: paint(36, 39),
|
|
970
970
|
success: paint(32, 39),
|
|
971
|
+
warning: paint(33, 39),
|
|
972
|
+
error: paint(31, 39),
|
|
971
973
|
dim: paint(2, 22),
|
|
972
974
|
bold: paint(1, 22)
|
|
973
975
|
};
|
|
@@ -1515,7 +1517,7 @@ var fieldName = (field) => {
|
|
|
1515
1517
|
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" };
|
|
1516
1518
|
return Object.hasOwn(names, key) ? names[key] : plainText(key);
|
|
1517
1519
|
};
|
|
1518
|
-
function writeWrapped(prompter, text, indent2 = "", changed) {
|
|
1520
|
+
function writeWrapped(prompter, text, indent2 = "", changed, style) {
|
|
1519
1521
|
const characters = Array.from(plainText(text));
|
|
1520
1522
|
let line = indent2;
|
|
1521
1523
|
let width = displayWidth(indent2);
|
|
@@ -1530,7 +1532,7 @@ function writeWrapped(prompter, text, indent2 = "", changed) {
|
|
|
1530
1532
|
const size = displayWidth(character);
|
|
1531
1533
|
if (width + size > prompter.width) {
|
|
1532
1534
|
flush();
|
|
1533
|
-
prompter.write(line + "\n");
|
|
1535
|
+
prompter.write((style ? style(line) : line) + "\n");
|
|
1534
1536
|
line = indent2;
|
|
1535
1537
|
width = displayWidth(indent2);
|
|
1536
1538
|
}
|
|
@@ -1543,7 +1545,7 @@ function writeWrapped(prompter, text, indent2 = "", changed) {
|
|
|
1543
1545
|
width += size;
|
|
1544
1546
|
}
|
|
1545
1547
|
flush();
|
|
1546
|
-
prompter.write(line + "\n");
|
|
1548
|
+
prompter.write((style ? style(line) : line) + "\n");
|
|
1547
1549
|
}
|
|
1548
1550
|
function changedRange(values, index) {
|
|
1549
1551
|
if (values.length !== 2 || values[0] === values[1]) return void 0;
|
|
@@ -1565,6 +1567,37 @@ function compareEntries(prompter, versions) {
|
|
|
1565
1567
|
const key = field.replace(/^env\./, "");
|
|
1566
1568
|
return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
|
|
1567
1569
|
});
|
|
1570
|
+
if (versions.length === 2 && prompter.width >= 90) {
|
|
1571
|
+
const width = Math.floor((prompter.width - 3) / 2);
|
|
1572
|
+
const wrap = (value, changed) => {
|
|
1573
|
+
const lines = [{ text: "", width: 0 }];
|
|
1574
|
+
for (const [index, character] of Array.from(value).entries()) {
|
|
1575
|
+
const size = displayWidth(character);
|
|
1576
|
+
if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
|
|
1577
|
+
const line = lines[lines.length - 1];
|
|
1578
|
+
line.text += changed && index >= changed[0] && index < changed[1] ? prompter.paint.accent(prompter.paint.bold(character)) : character;
|
|
1579
|
+
line.width += size;
|
|
1580
|
+
}
|
|
1581
|
+
return lines;
|
|
1582
|
+
};
|
|
1583
|
+
const row = (left, right) => {
|
|
1584
|
+
for (let line = 0; line < Math.max(left.length, right.length); line++) {
|
|
1585
|
+
prompter.write(`${left[line]?.text ?? ""}${" ".repeat(width - (left[line]?.width ?? 0))} | ${right[line]?.text ?? ""}
|
|
1586
|
+
`);
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
prompter.write("\n");
|
|
1590
|
+
row(wrap(`[1] ${plainText(versions[0].label)}`), wrap(`[2] ${plainText(versions[1].label)}`));
|
|
1591
|
+
for (let field = 0; field < fields.length; field++) {
|
|
1592
|
+
prompter.write(`
|
|
1593
|
+
${prompter.paint.dim(fieldName(fields[field]))}
|
|
1594
|
+
`);
|
|
1595
|
+
row(wrap(values[field][0], changedRange(values[field], 0)), wrap(values[field][1], changedRange(values[field], 1)));
|
|
1596
|
+
}
|
|
1597
|
+
if (values.some((row2) => row2.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
|
|
1598
|
+
prompter.write("\n");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1568
1601
|
for (let index = 0; index < versions.length; index++) {
|
|
1569
1602
|
prompter.write("\n");
|
|
1570
1603
|
writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
|
|
@@ -1771,10 +1804,10 @@ async function checkConnections(prompter, entries, check = checkStartup) {
|
|
|
1771
1804
|
const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
|
|
1772
1805
|
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.` };
|
|
1773
1806
|
const safe = (value) => [entry, ...maskEntries ?? []].reduce((text, source) => redactStartupText(text, source), value);
|
|
1774
|
-
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."}`));
|
|
1807
|
+
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);
|
|
1775
1808
|
if (!result.ok) {
|
|
1776
1809
|
failed = true;
|
|
1777
|
-
for (const line of result.stderr) writeWrapped(prompter, safe(line), " ");
|
|
1810
|
+
for (const line of result.stderr) writeWrapped(prompter, safe(line), " ", void 0, prompter.paint.dim);
|
|
1778
1811
|
}
|
|
1779
1812
|
}
|
|
1780
1813
|
if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
|
|
@@ -2237,11 +2270,26 @@ var Registry = class {
|
|
|
2237
2270
|
inspect() {
|
|
2238
2271
|
return this.reconcile(this.read(), false);
|
|
2239
2272
|
}
|
|
2273
|
+
formatResults(results, prefix = "", paint, reviewHint) {
|
|
2274
|
+
return formatResults(results, prefix, {
|
|
2275
|
+
paint,
|
|
2276
|
+
reviewHint,
|
|
2277
|
+
connectionCount: Object.values(this.read().connections).filter((item) => item.entry).length
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2240
2280
|
reconcile(state, apply, kinds) {
|
|
2241
2281
|
const results = [];
|
|
2242
2282
|
const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
|
|
2243
2283
|
for (const kind of kinds ?? KINDS) if (this.isSessionMode(kind, state) && active(kind).length && !state.sessions?.[kind]) {
|
|
2244
|
-
for (const client of state.clients) results.push({
|
|
2284
|
+
for (const client of state.clients) results.push({
|
|
2285
|
+
client,
|
|
2286
|
+
name: sessionName(kind),
|
|
2287
|
+
kind,
|
|
2288
|
+
connections: active(kind).map(([name]) => name),
|
|
2289
|
+
status: "pending",
|
|
2290
|
+
waitingFor: "runtime",
|
|
2291
|
+
detail: "Install it from Runtime, then run Sync. Your existing entries keep working."
|
|
2292
|
+
});
|
|
2245
2293
|
}
|
|
2246
2294
|
const work = [
|
|
2247
2295
|
...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
|
|
@@ -2257,7 +2305,15 @@ var Registry = class {
|
|
|
2257
2305
|
const previous = connection.applied[client];
|
|
2258
2306
|
if (!desired && !previous) continue;
|
|
2259
2307
|
const target = this.targets.find((candidate) => candidate.id === client);
|
|
2260
|
-
const report = (status, detail) => results.push({
|
|
2308
|
+
const report = (status, detail, waitingFor) => results.push({
|
|
2309
|
+
client,
|
|
2310
|
+
name,
|
|
2311
|
+
status,
|
|
2312
|
+
detail,
|
|
2313
|
+
kind: connection.kind,
|
|
2314
|
+
connections: bundle ? active(connection.kind).map(([name2]) => name2) : [name],
|
|
2315
|
+
...waitingFor ? { waitingFor } : {}
|
|
2316
|
+
});
|
|
2261
2317
|
if (!target) {
|
|
2262
2318
|
report("error", "Service adapter unavailable; use a newer package to retry.");
|
|
2263
2319
|
continue;
|
|
@@ -2275,20 +2331,18 @@ var Registry = class {
|
|
|
2275
2331
|
report("removed", "Removed");
|
|
2276
2332
|
} else if (exists && (!actual || !previous || hash !== previous)) {
|
|
2277
2333
|
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.");
|
|
2278
|
-
} else if (!apply) {
|
|
2279
|
-
report("pending", desired ? "Needs applying" : "Needs removal");
|
|
2280
2334
|
} else {
|
|
2281
2335
|
if (!desired && state.clients.includes(client)) {
|
|
2282
2336
|
if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
|
|
2283
2337
|
const session = state.sessions?.[connection.kind];
|
|
2284
2338
|
const replacement2 = entries[sessionName(connection.kind)];
|
|
2285
2339
|
if (!session || !replacement2 || entryHash(replacement2) !== entryHash(session.entry) || session.applied[client] !== entryHash(session.entry)) {
|
|
2286
|
-
report("pending", "Connection entry kept until the session server is applied.");
|
|
2340
|
+
report("pending", "Connection entry kept until the session server is applied.", "session");
|
|
2287
2341
|
continue;
|
|
2288
2342
|
}
|
|
2289
2343
|
}
|
|
2290
2344
|
if (bundle && !this.isSessionMode(connection.kind, state) && active(connection.kind).some(([connectionName, value]) => !entries[connectionName] || entryHash(entries[connectionName]) !== entryHash(value.entry))) {
|
|
2291
|
-
report("pending", "Session entry kept until all connection entries are applied.");
|
|
2345
|
+
report("pending", "Session entry kept until all connection entries are applied.", "connections");
|
|
2292
2346
|
continue;
|
|
2293
2347
|
}
|
|
2294
2348
|
}
|
|
@@ -2299,7 +2353,11 @@ var Registry = class {
|
|
|
2299
2353
|
replacement = state.connections[replacement.replacement];
|
|
2300
2354
|
}
|
|
2301
2355
|
if (!desired && state.clients.includes(client) && !this.isSessionMode(connection.kind, state) && replacement && replacement.entry && replacement.applied[client] !== entryHash(replacement.entry)) {
|
|
2302
|
-
report("pending", "Old name kept until the new name is applied.");
|
|
2356
|
+
report("pending", "Old name kept until the new name is applied.", "rename");
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
if (!apply) {
|
|
2360
|
+
report("pending", desired ? "Needs applying" : "Needs removal");
|
|
2303
2361
|
continue;
|
|
2304
2362
|
}
|
|
2305
2363
|
const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
|
|
@@ -2390,7 +2448,7 @@ var Registry = class {
|
|
|
2390
2448
|
const registry = this;
|
|
2391
2449
|
let snapshot;
|
|
2392
2450
|
const expected = (name) => snapshot ? own(snapshot, name) ? entryHash(snapshot[name]) : null : void 0;
|
|
2393
|
-
const saved = (results) => ({ status: "written", detail: formatResults(results, "Saved in the shared AKMS catalog.") });
|
|
2451
|
+
const saved = (results) => ({ status: "written", detail: registry.formatResults(results, "Saved in the shared AKMS catalog.") });
|
|
2394
2452
|
const prepare = (name, entry, oldName) => {
|
|
2395
2453
|
if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
|
|
2396
2454
|
const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
|
|
@@ -2419,30 +2477,58 @@ var Registry = class {
|
|
|
2419
2477
|
};
|
|
2420
2478
|
}
|
|
2421
2479
|
};
|
|
2422
|
-
function formatResults(results, prefix = "") {
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
const
|
|
2426
|
-
const
|
|
2427
|
-
const
|
|
2428
|
-
lines.push(` Connections: ${total
|
|
2480
|
+
function formatResults(results, prefix = "", options = {}) {
|
|
2481
|
+
const plain = (text) => text;
|
|
2482
|
+
const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
|
|
2483
|
+
const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
|
|
2484
|
+
const lines = prefix ? [paint.bold(prefix)] : [];
|
|
2485
|
+
const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
|
|
2486
|
+
lines.push(` Connections: ${total}`);
|
|
2429
2487
|
const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
|
|
2430
2488
|
const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
|
|
2431
2489
|
const removed = results.filter((item) => item.status === "removed").length;
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2490
|
+
const failed = results.filter((item) => item.status === "error");
|
|
2491
|
+
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`)}`);
|
|
2492
|
+
if (!results.length) lines.push(paint.dim(" No shared connections need applying."));
|
|
2493
|
+
if (results.length && results.every((item) => item.status === "synced" || item.status === "removed")) lines.push(paint.success(" OK: all service entries are up to date."));
|
|
2494
|
+
const waiting = results.filter((item) => item.status === "pending");
|
|
2495
|
+
for (const kind of KINDS) {
|
|
2496
|
+
const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
|
|
2497
|
+
const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
|
|
2498
|
+
if (!runtime.length && !kept.length) continue;
|
|
2499
|
+
lines.push(`
|
|
2500
|
+
${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` : `Waiting for ${sessionName(kind)}`)}`);
|
|
2501
|
+
if (runtime.length) lines.push(" Install it from Runtime, then run Sync. Your existing entries keep working.");
|
|
2502
|
+
else lines.push(" Resolve this session entry in Sync. Existing connection entries stay until it is ready.");
|
|
2503
|
+
const retained = /* @__PURE__ */ new Map();
|
|
2504
|
+
for (const client of new Set(kept.map((item) => item.client))) {
|
|
2505
|
+
const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
|
|
2506
|
+
const key = JSON.stringify([...names].sort());
|
|
2507
|
+
const group = retained.get(key);
|
|
2508
|
+
if (group) group.clients.push(client);
|
|
2509
|
+
else retained.set(key, { clients: [client], names });
|
|
2510
|
+
}
|
|
2511
|
+
for (const group of retained.values()) {
|
|
2512
|
+
const clients = group.clients.map((client) => CLIENT_LABELS[client]).join(" \xB7 ");
|
|
2513
|
+
lines.push(paint.dim(` Kept in ${clients} until ${sessionName(kind)} is applied: ${group.names.map(safe).join(", ")}`));
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
for (const [heading, rows, color] of [
|
|
2517
|
+
["Needs review (existing settings kept)", results.filter((item) => item.status === "conflict"), paint.warning],
|
|
2518
|
+
["Waiting for Sync", waiting.filter((item) => !item.waitingFor || !["runtime", "session"].includes(item.waitingFor)), paint.warning],
|
|
2519
|
+
["Could not finish", failed, paint.error]
|
|
2436
2520
|
]) {
|
|
2437
2521
|
if (!rows.length) continue;
|
|
2438
2522
|
lines.push(`
|
|
2439
|
-
${heading}:`);
|
|
2523
|
+
${color(`${heading}:`)}`);
|
|
2440
2524
|
for (const name of new Set(rows.map((item) => item.name))) {
|
|
2441
|
-
lines.push(` ${name
|
|
2442
|
-
for (const row of rows.filter((item) => item.name === name)) lines.push(` ${CLIENT_LABELS[row.client]}: ${row.detail}`);
|
|
2525
|
+
lines.push(` ${safe(name)}`);
|
|
2526
|
+
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)}`);
|
|
2443
2527
|
}
|
|
2444
2528
|
}
|
|
2445
|
-
if (
|
|
2529
|
+
if (results.some((item) => item.status === "conflict")) lines.push(paint.warning(`
|
|
2530
|
+
${options.reviewHint ?? "Next: open Sync to compare settings and choose which version to use."}`));
|
|
2531
|
+
if (failed.length) lines.push(paint.error("\nNext: fix the errors above, then run Sync again."));
|
|
2446
2532
|
return lines.join("\n");
|
|
2447
2533
|
}
|
|
2448
2534
|
|
|
@@ -2511,7 +2597,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
|
|
|
2511
2597
|
prompter.write(`New kinds will use ${mode === "catalog" ? "session servers" : "individual entries"}. Existing kinds keep their settings.
|
|
2512
2598
|
`);
|
|
2513
2599
|
if (await prompter.askYesNo("Change the default for new kinds?", false)) {
|
|
2514
|
-
prompter.write(`${formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.")}
|
|
2600
|
+
prompter.write(`${registry.formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.", prompter.paint)}
|
|
2515
2601
|
`);
|
|
2516
2602
|
await prompter.acknowledge();
|
|
2517
2603
|
}
|
|
@@ -2540,7 +2626,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
|
|
|
2540
2626
|
if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
|
|
2541
2627
|
const results2 = registry.applySession(plan2);
|
|
2542
2628
|
prompter.clearScreen();
|
|
2543
|
-
prompter.write(`${formatResults(results2, "Configuration mode saved.")}
|
|
2629
|
+
prompter.write(`${registry.formatResults(results2, "Configuration mode saved.", prompter.paint)}
|
|
2544
2630
|
`);
|
|
2545
2631
|
await registry.checkSaved(prompter, plan2.names);
|
|
2546
2632
|
prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
|
|
@@ -2576,7 +2662,7 @@ Convert ${plan.names.length} connection(s):
|
|
|
2576
2662
|
if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
|
|
2577
2663
|
const results = registry.applyRuntime(plan);
|
|
2578
2664
|
prompter.clearScreen();
|
|
2579
|
-
prompter.write(`${formatResults(results, "Runtime settings saved.")}
|
|
2665
|
+
prompter.write(`${registry.formatResults(results, "Runtime settings saved.", prompter.paint)}
|
|
2580
2666
|
|
|
2581
2667
|
Restart or reconnect AI clients to replace running npx processes.
|
|
2582
2668
|
`);
|
|
@@ -2698,7 +2784,7 @@ async function migrationMenu(prompter, registry) {
|
|
|
2698
2784
|
if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
|
|
2699
2785
|
const results = registry.applyMigration(plan);
|
|
2700
2786
|
prompter.clearScreen();
|
|
2701
|
-
prompter.write(`${formatResults(results, "Registry migration saved.")}
|
|
2787
|
+
prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
|
|
2702
2788
|
`);
|
|
2703
2789
|
await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
|
|
2704
2790
|
prompter.write("Reconnect affected clients after changes to their server layout. Fix startup failures first.\n");
|
|
@@ -2713,6 +2799,12 @@ async function migrationMenu(prompter, registry) {
|
|
|
2713
2799
|
}
|
|
2714
2800
|
|
|
2715
2801
|
// src/modules/registration/menu.ts
|
|
2802
|
+
function pendingImports(registry, kind) {
|
|
2803
|
+
const state = registry.read();
|
|
2804
|
+
const entries = registry.candidates(kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name));
|
|
2805
|
+
const names = [...new Set(entries.map((candidate) => candidate.name))];
|
|
2806
|
+
return { names, differing: names.filter((name) => new Set(entries.filter((item) => item.name === name).map((item) => entryHash(item.entry))).size > 1).length };
|
|
2807
|
+
}
|
|
2716
2808
|
function preview(prompter, entry) {
|
|
2717
2809
|
if (!entry) {
|
|
2718
2810
|
prompter.write(" Remove this connection from the service.\n");
|
|
@@ -2743,22 +2835,21 @@ var ManagedSession = class {
|
|
|
2743
2835
|
}
|
|
2744
2836
|
writeStatus(prompter) {
|
|
2745
2837
|
const state = this.registry.read();
|
|
2746
|
-
if (this.registry.needsMigration(state)) prompter.write("Registry update available. Choose Migrate registry to review the changes.\n");
|
|
2838
|
+
if (this.registry.needsMigration(state)) prompter.write(prompter.paint.warning("Registry update available. Choose Migrate registry to review the changes.\n"));
|
|
2747
2839
|
prompter.write(`Services: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}
|
|
2748
2840
|
`);
|
|
2749
|
-
if (!state.clients.length) prompter.write("No AI services registered. Choose Register before adding a connection.\n");
|
|
2750
|
-
if (this.registry.isSessionMode(this.options.kind, state)) prompter.write(`Session server: ${sessionName(this.options.kind)}. Reconnect clients after changing connections.
|
|
2751
|
-
`);
|
|
2752
|
-
const attention = new Set(this.registry.inspect().filter((item) => this.registry.kindForName(item.name, state) === this.options.kind && !["synced", "removed"].includes(item.status)).
|
|
2753
|
-
if (attention) prompter.write(`Needs attention: ${attention} connection(s). Open Sync to review.
|
|
2754
|
-
`);
|
|
2841
|
+
if (!state.clients.length) prompter.write(prompter.paint.warning("No AI services registered. Choose Register before adding a connection.\n"));
|
|
2842
|
+
if (this.registry.isSessionMode(this.options.kind, state)) prompter.write(prompter.paint.dim(`Session server: ${sessionName(this.options.kind)}. Reconnect clients after changing connections.
|
|
2843
|
+
`));
|
|
2844
|
+
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;
|
|
2845
|
+
if (attention) prompter.write(prompter.paint.warning(`Needs attention: ${attention} connection(s). Open Sync to review.
|
|
2846
|
+
`));
|
|
2755
2847
|
const pending = this.pendingImports();
|
|
2756
|
-
if (pending) prompter.write(`Import
|
|
2757
|
-
`);
|
|
2848
|
+
if (pending) prompter.write(prompter.paint.warning(`Import available: ${pending} connection(s). Open Import existing connections.
|
|
2849
|
+
`));
|
|
2758
2850
|
}
|
|
2759
2851
|
pendingImports() {
|
|
2760
|
-
|
|
2761
|
-
return new Set(this.registry.candidates(this.options.kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name)).map((candidate) => candidate.name)).size;
|
|
2852
|
+
return pendingImports(this.registry, this.options.kind).names.length;
|
|
2762
2853
|
}
|
|
2763
2854
|
items() {
|
|
2764
2855
|
return [
|
|
@@ -2876,8 +2967,8 @@ Remove only AKMS-managed entries from these services; keep the shared catalog.
|
|
|
2876
2967
|
const results = registry.register([...selected], initial.clients, plan.imports);
|
|
2877
2968
|
prompter.clearScreen();
|
|
2878
2969
|
prompter.write(`
|
|
2879
|
-
${formatResults(results, `Registration saved.
|
|
2880
|
-
AI services: ${[...selected].map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}
|
|
2970
|
+
${registry.formatResults(results, `Registration saved.
|
|
2971
|
+
AI services: ${[...selected].map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}`, prompter.paint)}
|
|
2881
2972
|
`);
|
|
2882
2973
|
if (importNames.length) prompter.write(`Imported ${importNames.length} existing connection(s) into the shared list.
|
|
2883
2974
|
`);
|
|
@@ -2950,7 +3041,7 @@ Import existing connections:
|
|
|
2950
3041
|
if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
|
|
2951
3042
|
const results2 = registry.register(state.clients, state.clients, matching.imports);
|
|
2952
3043
|
prompter.clearScreen();
|
|
2953
|
-
prompter.write(`${formatResults(results2, "Import complete")}
|
|
3044
|
+
prompter.write(`${registry.formatResults(results2, "Import complete", prompter.paint)}
|
|
2954
3045
|
`);
|
|
2955
3046
|
await prompter.acknowledge();
|
|
2956
3047
|
}
|
|
@@ -2978,7 +3069,7 @@ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
|
|
|
2978
3069
|
const results = registry.importEntry(destination, chosen, candidates);
|
|
2979
3070
|
prompter.clearScreen();
|
|
2980
3071
|
prompter.write(`
|
|
2981
|
-
${formatResults(results, "Import complete")}
|
|
3072
|
+
${registry.formatResults(results, "Import complete", prompter.paint)}
|
|
2982
3073
|
`);
|
|
2983
3074
|
if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
|
|
2984
3075
|
await prompter.acknowledge();
|
|
@@ -2998,19 +3089,28 @@ async function syncMenu(prompter, registry) {
|
|
|
2998
3089
|
const results = pendingResults ?? registry.sync();
|
|
2999
3090
|
pendingResults = void 0;
|
|
3000
3091
|
prompter.write(`
|
|
3001
|
-
${formatResults(results, "Sync results")}
|
|
3092
|
+
${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
|
|
3002
3093
|
`);
|
|
3094
|
+
const imports = pendingImports(registry);
|
|
3095
|
+
if (imports.names.length) prompter.write(prompter.paint.warning(`
|
|
3096
|
+
Not yet imported: ${imports.names.length} connection(s)${imports.differing === imports.names.length ? " with differing settings" : ""} - open Import existing connections.
|
|
3097
|
+
`));
|
|
3003
3098
|
const conflicts = results.filter((result) => result.status === "conflict");
|
|
3004
|
-
if (!conflicts.length) {
|
|
3099
|
+
if (!conflicts.length && !imports.names.length) {
|
|
3005
3100
|
await prompter.acknowledge();
|
|
3006
3101
|
return;
|
|
3007
3102
|
}
|
|
3008
3103
|
const picked = await prompter.select("Review differences", [
|
|
3009
3104
|
...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS[item.client]} \xB7 ${item.name}` })),
|
|
3105
|
+
...imports.names.length ? [{ value: "import", key: "import", label: "Import existing connections", hint: `${imports.names.length} connection(s) to review` }] : [],
|
|
3010
3106
|
{ value: "back", key: "b", label: "Later \u2014 keep existing settings" }
|
|
3011
3107
|
], { cancelValue: "back" });
|
|
3012
3108
|
if (picked === "back") return;
|
|
3013
3109
|
try {
|
|
3110
|
+
if (picked === "import") {
|
|
3111
|
+
await importMenu(prompter, registry);
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3014
3114
|
prompter.clearScreen();
|
|
3015
3115
|
const conflict = conflicts[Number(picked)];
|
|
3016
3116
|
const state = registry.read();
|
|
@@ -3044,12 +3144,17 @@ ${formatResults(results, "Sync results")}
|
|
|
3044
3144
|
const client = CLIENT_LABELS[conflict.client];
|
|
3045
3145
|
const registered = state.clients.includes(conflict.client);
|
|
3046
3146
|
const sessionMode = registry.isSessionMode(connection.kind, state);
|
|
3047
|
-
const removing = !connection.entry || !registered
|
|
3147
|
+
const removing = !connection.entry || !registered;
|
|
3148
|
+
const retiring = sessionMode && !removing;
|
|
3048
3149
|
const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
|
|
3049
3150
|
writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
|
|
3050
3151
|
writeWrapped(prompter, `Registered services: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ") || "none"}`);
|
|
3051
3152
|
prompter.write("\n");
|
|
3052
3153
|
if (!removing && connection.entry) {
|
|
3154
|
+
if (retiring) {
|
|
3155
|
+
writeWrapped(prompter, `Choose settings for ${sessionName(connection.kind)} before removing the old ${client} entry.`);
|
|
3156
|
+
writeWrapped(prompter, "The old entry stays until the session server is ready.");
|
|
3157
|
+
}
|
|
3053
3158
|
compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
|
|
3054
3159
|
} else {
|
|
3055
3160
|
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.");
|
|
@@ -3061,8 +3166,8 @@ Currently in ${client}
|
|
|
3061
3166
|
prompter.write("\n");
|
|
3062
3167
|
}
|
|
3063
3168
|
const choices = [
|
|
3064
|
-
{ value: "shared", label: removing ? `Remove from ${client}` : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : `replace its ${subject} with version 1` },
|
|
3065
|
-
...registered ? [{ value: "client", label:
|
|
3169
|
+
{ 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` },
|
|
3170
|
+
...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" }] : [],
|
|
3066
3171
|
{ value: "back", key: "b", label: "Back without changes" }
|
|
3067
3172
|
];
|
|
3068
3173
|
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" });
|
|
@@ -3071,8 +3176,9 @@ Currently in ${client}
|
|
|
3071
3176
|
if (choice === "client") {
|
|
3072
3177
|
writeWrapped(prompter, `Save the ${client} version in AKMS.`);
|
|
3073
3178
|
writeWrapped(prompter, `Sync to: ${state.clients.map((id) => CLIENT_LABELS[id]).join(" \xB7 ")}`);
|
|
3074
|
-
} else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : `Copy the saved AKMS ${subject} to ${client}.`);
|
|
3075
|
-
|
|
3179
|
+
} else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : retiring ? `Keep the saved AKMS ${subject}.` : `Copy the saved AKMS ${subject} to ${client}.`);
|
|
3180
|
+
if (retiring) writeWrapped(prompter, `Keep the old ${client} entry until ${sessionName(connection.kind)} is ready, then remove it.`);
|
|
3181
|
+
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?`;
|
|
3076
3182
|
if (await prompter.askYesNo(confirmation, false)) {
|
|
3077
3183
|
pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
|
|
3078
3184
|
}
|