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