@akms/mcp-ssh 0.1.6 → 0.1.8
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 +242 -0
- package/README.md +299 -282
- package/lib/cli.cjs +726 -607
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +729 -610
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +6 -2
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +6 -2
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
package/lib/cli.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
-
var
|
|
5
|
+
var fs7 = require('fs');
|
|
6
6
|
var os5 = require('os');
|
|
7
7
|
var path = require('path');
|
|
8
8
|
var child_process = require('child_process');
|
|
@@ -11,12 +11,14 @@ var stream = require('stream');
|
|
|
11
11
|
var string_decoder = require('string_decoder');
|
|
12
12
|
var ssh2 = require('ssh2');
|
|
13
13
|
var timers = require('timers');
|
|
14
|
+
var util = require('util');
|
|
15
|
+
var smolToml = require('smol-toml');
|
|
14
16
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
15
17
|
var zod = require('zod');
|
|
16
18
|
|
|
17
19
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
20
|
|
|
19
|
-
var
|
|
21
|
+
var fs7__default = /*#__PURE__*/_interopDefault(fs7);
|
|
20
22
|
var os5__default = /*#__PURE__*/_interopDefault(os5);
|
|
21
23
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
22
24
|
var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
|
|
@@ -36,7 +38,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
36
38
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
37
39
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
38
40
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
39
|
-
var SERVER_VERSION = "0.1.
|
|
41
|
+
var SERVER_VERSION = "0.1.8";
|
|
40
42
|
|
|
41
43
|
// src/_defs/command-rules.ts
|
|
42
44
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -622,7 +624,7 @@ function findExecutable(name) {
|
|
|
622
624
|
for (const directory of directories) {
|
|
623
625
|
for (const suffix of suffixes) {
|
|
624
626
|
const candidate = path__default.default.join(directory, `${name}${suffix}`);
|
|
625
|
-
if (
|
|
627
|
+
if (fs7__default.default.existsSync(candidate) == true && fs7__default.default.statSync(candidate).isFile() == true) {
|
|
626
628
|
return candidate;
|
|
627
629
|
}
|
|
628
630
|
}
|
|
@@ -763,7 +765,7 @@ function loadHostProfile(env = process.env) {
|
|
|
763
765
|
if (keyPath != null) {
|
|
764
766
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
765
767
|
profile.privateKeyPath = expandedKeyPath;
|
|
766
|
-
if (
|
|
768
|
+
if (fs7__default.default.existsSync(expandedKeyPath) == false) {
|
|
767
769
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
768
770
|
private_key_path: expandedKeyPath
|
|
769
771
|
});
|
|
@@ -795,7 +797,7 @@ function loadHostProfile(env = process.env) {
|
|
|
795
797
|
if (jumpKeyPath != null) {
|
|
796
798
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
797
799
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
798
|
-
if (
|
|
800
|
+
if (fs7__default.default.existsSync(profile.jump.privateKeyPath) == false) {
|
|
799
801
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
800
802
|
private_key_path: profile.jump.privateKeyPath
|
|
801
803
|
});
|
|
@@ -1281,14 +1283,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1281
1283
|
function toRealPath(inputPath) {
|
|
1282
1284
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1283
1285
|
try {
|
|
1284
|
-
return
|
|
1286
|
+
return fs7__default.default.realpathSync.native(inputPath);
|
|
1285
1287
|
} catch (ex) {
|
|
1286
1288
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1287
1289
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1288
1290
|
});
|
|
1289
1291
|
}
|
|
1290
1292
|
try {
|
|
1291
|
-
const parentRealPath =
|
|
1293
|
+
const parentRealPath = fs7__default.default.realpathSync.native(path__default.default.dirname(inputPath));
|
|
1292
1294
|
return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
|
|
1293
1295
|
} catch (ex) {
|
|
1294
1296
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1380,7 +1382,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1380
1382
|
return cached;
|
|
1381
1383
|
}
|
|
1382
1384
|
try {
|
|
1383
|
-
const contents =
|
|
1385
|
+
const contents = fs7__default.default.readFileSync(keyPath);
|
|
1384
1386
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1385
1387
|
return contents;
|
|
1386
1388
|
} catch (ex) {
|
|
@@ -1649,7 +1651,7 @@ var SshConnection = class {
|
|
|
1649
1651
|
* @throws {Error} If the connection was closed, authentication fails, the host is
|
|
1650
1652
|
* unreachable, the handshake times out, or the carrier cannot be opened.
|
|
1651
1653
|
*/
|
|
1652
|
-
async connect() {
|
|
1654
|
+
async connect(onProgress) {
|
|
1653
1655
|
if (this.disposed == true) {
|
|
1654
1656
|
throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
|
|
1655
1657
|
}
|
|
@@ -1671,6 +1673,9 @@ var SshConnection = class {
|
|
|
1671
1673
|
this.observedFingerprint = fingerprint;
|
|
1672
1674
|
});
|
|
1673
1675
|
this.connecting = (async () => {
|
|
1676
|
+
if (this.profile.jump != null || this.profile.proxyCommand != null) {
|
|
1677
|
+
onProgress?.("transport");
|
|
1678
|
+
}
|
|
1674
1679
|
const transport = await openTransport(this.profile, (fingerprint) => {
|
|
1675
1680
|
this.observedJumpFingerprint = fingerprint;
|
|
1676
1681
|
});
|
|
@@ -1681,6 +1686,7 @@ var SshConnection = class {
|
|
|
1681
1686
|
if (transport != null) {
|
|
1682
1687
|
connectConfig.sock = transport.sock;
|
|
1683
1688
|
}
|
|
1689
|
+
onProgress?.("host");
|
|
1684
1690
|
await new Promise((resolve, reject) => {
|
|
1685
1691
|
const client = new Client();
|
|
1686
1692
|
this.pendingClient = client;
|
|
@@ -2331,7 +2337,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2331
2337
|
}
|
|
2332
2338
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2333
2339
|
requirePermittedPath(connection, remotePath, "write");
|
|
2334
|
-
const localStats =
|
|
2340
|
+
const localStats = fs7__default.default.statSync(localPath);
|
|
2335
2341
|
if (localStats.isFile() == false) {
|
|
2336
2342
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2337
2343
|
}
|
|
@@ -2368,7 +2374,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2368
2374
|
});
|
|
2369
2375
|
});
|
|
2370
2376
|
});
|
|
2371
|
-
const localStats =
|
|
2377
|
+
const localStats = fs7__default.default.statSync(localPath);
|
|
2372
2378
|
const _logger = $logger.child({
|
|
2373
2379
|
context: "downloadFile",
|
|
2374
2380
|
profile: connection.profile.name,
|
|
@@ -2379,283 +2385,6 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2379
2385
|
return localStats.size;
|
|
2380
2386
|
}
|
|
2381
2387
|
|
|
2382
|
-
// src/modules/manage/connection-check.ts
|
|
2383
|
-
function profileFromEnv(output, env) {
|
|
2384
|
-
try {
|
|
2385
|
-
const profile = loadHostProfile(env);
|
|
2386
|
-
if (profile == null) {
|
|
2387
|
-
output.write(" The entry has no host.\n");
|
|
2388
|
-
}
|
|
2389
|
-
return profile;
|
|
2390
|
-
} catch (ex) {
|
|
2391
|
-
let detail = String(ex);
|
|
2392
|
-
if (ex instanceof Error) {
|
|
2393
|
-
detail = ex.message;
|
|
2394
|
-
}
|
|
2395
|
-
output.write(` The server would refuse to start with this entry: ${detail}
|
|
2396
|
-
`);
|
|
2397
|
-
return null;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
function describeAuth(profile) {
|
|
2401
|
-
if (profile.privateKeyPath != null) {
|
|
2402
|
-
return `key ${profile.privateKeyPath}`;
|
|
2403
|
-
}
|
|
2404
|
-
if (profile.password != null) {
|
|
2405
|
-
return "password";
|
|
2406
|
-
}
|
|
2407
|
-
return "ssh agent";
|
|
2408
|
-
}
|
|
2409
|
-
async function runConnectionCheck(profile, output) {
|
|
2410
|
-
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
2411
|
-
const auth = describeAuth(profile);
|
|
2412
|
-
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
2413
|
-
`);
|
|
2414
|
-
if (profile.proxyCommand != null) {
|
|
2415
|
-
output.write(` via proxy command: ${profile.proxyCommand}
|
|
2416
|
-
`);
|
|
2417
|
-
}
|
|
2418
|
-
if (profile.jump != null) {
|
|
2419
|
-
output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
|
|
2420
|
-
`);
|
|
2421
|
-
}
|
|
2422
|
-
output.write("\n");
|
|
2423
|
-
const connection = new SshConnection(profile);
|
|
2424
|
-
const startedAt = Date.now();
|
|
2425
|
-
let connected = false;
|
|
2426
|
-
let detail = "";
|
|
2427
|
-
try {
|
|
2428
|
-
await connection.connect();
|
|
2429
|
-
connected = true;
|
|
2430
|
-
} catch (ex) {
|
|
2431
|
-
detail = String(ex);
|
|
2432
|
-
if (ex instanceof Error) {
|
|
2433
|
-
detail = ex.message;
|
|
2434
|
-
}
|
|
2435
|
-
} finally {
|
|
2436
|
-
connection.disconnect();
|
|
2437
|
-
}
|
|
2438
|
-
const durationMs = Date.now() - startedAt;
|
|
2439
|
-
if (connected == true) {
|
|
2440
|
-
output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
|
|
2441
|
-
`);
|
|
2442
|
-
} else {
|
|
2443
|
-
output.write(` FAILED ${endpoint} (${auth})
|
|
2444
|
-
`);
|
|
2445
|
-
output.write(` ${detail}
|
|
2446
|
-
`);
|
|
2447
|
-
}
|
|
2448
|
-
if (connection.hostKeyFingerprint != null) {
|
|
2449
|
-
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
|
|
2450
|
-
if (profile.hostKeyFingerprint != null) {
|
|
2451
|
-
pinNote = "pinned";
|
|
2452
|
-
}
|
|
2453
|
-
output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
|
|
2454
|
-
`);
|
|
2455
|
-
}
|
|
2456
|
-
if (connection.jumpHostKeyFingerprint != null) {
|
|
2457
|
-
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
|
|
2458
|
-
if (profile.jump?.hostKeyFingerprint != null) {
|
|
2459
|
-
pinNote = "pinned";
|
|
2460
|
-
}
|
|
2461
|
-
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
2462
|
-
`);
|
|
2463
|
-
}
|
|
2464
|
-
return {
|
|
2465
|
-
connected,
|
|
2466
|
-
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
2467
|
-
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// src/modules/manage/entry.ts
|
|
2472
|
-
var MANAGED_ENV_NAMES = [
|
|
2473
|
-
SINGLE_HOST_ENV.HOST,
|
|
2474
|
-
SINGLE_HOST_ENV.PORT,
|
|
2475
|
-
SINGLE_HOST_ENV.USER,
|
|
2476
|
-
SINGLE_HOST_ENV.KEY,
|
|
2477
|
-
SINGLE_HOST_ENV.PASSPHRASE,
|
|
2478
|
-
SINGLE_HOST_ENV.PASSWORD,
|
|
2479
|
-
SINGLE_HOST_ENV.DESCRIPTION,
|
|
2480
|
-
SINGLE_HOST_ENV.JUMP_HOST,
|
|
2481
|
-
SINGLE_HOST_ENV.JUMP_PORT,
|
|
2482
|
-
SINGLE_HOST_ENV.JUMP_USER,
|
|
2483
|
-
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
2484
|
-
SINGLE_HOST_ENV.READONLY,
|
|
2485
|
-
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
2486
|
-
SINGLE_HOST_ENV.HOST_KEY,
|
|
2487
|
-
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
2488
|
-
];
|
|
2489
|
-
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
2490
|
-
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
2491
|
-
function isOurEntry(entry) {
|
|
2492
|
-
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
2493
|
-
return true;
|
|
2494
|
-
}
|
|
2495
|
-
const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
2496
|
-
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
2497
|
-
}
|
|
2498
|
-
function launchCommand() {
|
|
2499
|
-
if (process.platform === "win32") {
|
|
2500
|
-
return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
|
|
2501
|
-
}
|
|
2502
|
-
return { command: "npx", args: ["-y", PACKAGE_NAME] };
|
|
2503
|
-
}
|
|
2504
|
-
function answersToEntry(answers) {
|
|
2505
|
-
const env = {};
|
|
2506
|
-
env[SINGLE_HOST_ENV.HOST] = answers.host;
|
|
2507
|
-
if (answers.port !== DEFAULT_SSH_PORT) {
|
|
2508
|
-
env[SINGLE_HOST_ENV.PORT] = String(answers.port);
|
|
2509
|
-
}
|
|
2510
|
-
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
2511
|
-
if (answers.auth === "key" && answers.keyPath != null) {
|
|
2512
|
-
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
2513
|
-
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
2514
|
-
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
if (answers.auth === "password" && answers.password != null) {
|
|
2518
|
-
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
2519
|
-
}
|
|
2520
|
-
if (answers.hostKeyFingerprint != null) {
|
|
2521
|
-
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
2522
|
-
}
|
|
2523
|
-
if (answers.description != null && answers.description !== "") {
|
|
2524
|
-
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
2525
|
-
}
|
|
2526
|
-
if (answers.jumpHost != null) {
|
|
2527
|
-
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
2528
|
-
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
2529
|
-
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
2530
|
-
}
|
|
2531
|
-
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
2532
|
-
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
2533
|
-
}
|
|
2534
|
-
if (answers.jumpHostKeyFingerprint != null) {
|
|
2535
|
-
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
2539
|
-
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
2540
|
-
}
|
|
2541
|
-
if (answers.readonly == true) {
|
|
2542
|
-
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
2543
|
-
}
|
|
2544
|
-
if (answers.allowedPaths.length > 0) {
|
|
2545
|
-
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
2546
|
-
}
|
|
2547
|
-
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
2548
|
-
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
2549
|
-
continue;
|
|
2550
|
-
}
|
|
2551
|
-
if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
|
|
2552
|
-
continue;
|
|
2553
|
-
}
|
|
2554
|
-
env[name] = value;
|
|
2555
|
-
}
|
|
2556
|
-
const launch = launchCommand();
|
|
2557
|
-
return {
|
|
2558
|
-
command: launch.command,
|
|
2559
|
-
args: launch.args,
|
|
2560
|
-
env
|
|
2561
|
-
};
|
|
2562
|
-
}
|
|
2563
|
-
function readPort(value) {
|
|
2564
|
-
if (value == null) {
|
|
2565
|
-
return DEFAULT_SSH_PORT;
|
|
2566
|
-
}
|
|
2567
|
-
const parsed = Number(value);
|
|
2568
|
-
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
2569
|
-
return DEFAULT_SSH_PORT;
|
|
2570
|
-
}
|
|
2571
|
-
return parsed;
|
|
2572
|
-
}
|
|
2573
|
-
function isTruthy(value) {
|
|
2574
|
-
if (value == null) {
|
|
2575
|
-
return false;
|
|
2576
|
-
}
|
|
2577
|
-
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
2578
|
-
}
|
|
2579
|
-
function entryToAnswers(name, entry) {
|
|
2580
|
-
const env = entry.env;
|
|
2581
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
2582
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
2583
|
-
let auth = "agent";
|
|
2584
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2585
|
-
auth = "key";
|
|
2586
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2587
|
-
auth = "password";
|
|
2588
|
-
}
|
|
2589
|
-
let allowedPaths = [];
|
|
2590
|
-
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
2591
|
-
if (allowedPathsValue != null) {
|
|
2592
|
-
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
2593
|
-
}
|
|
2594
|
-
const extraEnv = {};
|
|
2595
|
-
for (const [variable, value] of Object.entries(env)) {
|
|
2596
|
-
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
2597
|
-
extraEnv[variable] = value;
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
const answers = {
|
|
2601
|
-
name,
|
|
2602
|
-
host,
|
|
2603
|
-
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
2604
|
-
username,
|
|
2605
|
-
auth,
|
|
2606
|
-
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
2607
|
-
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
2608
|
-
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
2609
|
-
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
2610
|
-
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
2611
|
-
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
2612
|
-
allowedPaths,
|
|
2613
|
-
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
2614
|
-
extraEnv
|
|
2615
|
-
};
|
|
2616
|
-
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
2617
|
-
if (jumpHost != null) {
|
|
2618
|
-
answers.jumpHost = jumpHost;
|
|
2619
|
-
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
2620
|
-
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
2621
|
-
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
2622
|
-
}
|
|
2623
|
-
return answers;
|
|
2624
|
-
}
|
|
2625
|
-
function summarizeEntry(entry) {
|
|
2626
|
-
const env = entry.env;
|
|
2627
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
2628
|
-
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
2629
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
2630
|
-
let auth = "agent";
|
|
2631
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2632
|
-
auth = "key";
|
|
2633
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2634
|
-
auth = "password";
|
|
2635
|
-
}
|
|
2636
|
-
const notes = [];
|
|
2637
|
-
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
2638
|
-
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
2639
|
-
}
|
|
2640
|
-
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
2641
|
-
notes.push("proxy");
|
|
2642
|
-
}
|
|
2643
|
-
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
2644
|
-
notes.push("readonly");
|
|
2645
|
-
}
|
|
2646
|
-
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
2647
|
-
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
2648
|
-
}
|
|
2649
|
-
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
2650
|
-
notes.push("pinned");
|
|
2651
|
-
}
|
|
2652
|
-
return {
|
|
2653
|
-
endpoint: `${username}@${host}:${port}`,
|
|
2654
|
-
auth,
|
|
2655
|
-
notes: notes.join(", ")
|
|
2656
|
-
};
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
2388
|
// src/modules/manage/prompter.ts
|
|
2660
2389
|
var PromptClosedError = class extends Error {
|
|
2661
2390
|
constructor() {
|
|
@@ -2832,6 +2561,17 @@ var Prompter = class {
|
|
|
2832
2561
|
get paint() {
|
|
2833
2562
|
return this.style;
|
|
2834
2563
|
}
|
|
2564
|
+
/** Keeps a report visible until Enter on a TTY; Esc goes back. Pipes need no extra answer. */
|
|
2565
|
+
async pause() {
|
|
2566
|
+
if (this.interactive == false) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (this.inputEnded == true) {
|
|
2570
|
+
throw new PromptClosedError();
|
|
2571
|
+
}
|
|
2572
|
+
const prompt = this.fitLine([{ text: "Press Enter to continue", paint: this.style.dim }], this.columns() - 1);
|
|
2573
|
+
await this.readText(prompt, false);
|
|
2574
|
+
}
|
|
2835
2575
|
/**
|
|
2836
2576
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2837
2577
|
*
|
|
@@ -3277,9 +3017,435 @@ var Prompter = class {
|
|
|
3277
3017
|
draw(buildLines());
|
|
3278
3018
|
});
|
|
3279
3019
|
}
|
|
3280
|
-
};
|
|
3281
|
-
|
|
3282
|
-
// src/modules/manage/
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
// src/modules/manage/check-progress.ts
|
|
3023
|
+
var ESC2 = "\x1B";
|
|
3024
|
+
var CheckProgress = class {
|
|
3025
|
+
constructor(output) {
|
|
3026
|
+
this.output = output;
|
|
3027
|
+
this.unicode = supportsUnicode();
|
|
3028
|
+
this.frames = this.unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
|
|
3029
|
+
this.timer = null;
|
|
3030
|
+
this.label = null;
|
|
3031
|
+
this.startedAt = 0;
|
|
3032
|
+
this.frame = 0;
|
|
3033
|
+
this.enabled = output.isTTY === true && process.env.TERM !== "dumb";
|
|
3034
|
+
this.color = process.env.NO_COLOR == null;
|
|
3035
|
+
}
|
|
3036
|
+
/** Advancing means the preceding phase actually completed successfully. */
|
|
3037
|
+
start(label) {
|
|
3038
|
+
if (this.enabled == false) {
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
this.finish(true);
|
|
3042
|
+
this.label = label;
|
|
3043
|
+
this.startedAt = Date.now();
|
|
3044
|
+
this.frame = 0;
|
|
3045
|
+
this.draw(this.frames[0], 36);
|
|
3046
|
+
this.timer = setInterval(() => {
|
|
3047
|
+
this.frame = (this.frame + 1) % this.frames.length;
|
|
3048
|
+
this.draw(this.frames[this.frame], 36);
|
|
3049
|
+
}, 80);
|
|
3050
|
+
this.timer.unref();
|
|
3051
|
+
}
|
|
3052
|
+
/** Leaves the completed phase and its duration visible. */
|
|
3053
|
+
finish(ok) {
|
|
3054
|
+
if (this.label == null) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
this.clearTimer();
|
|
3058
|
+
const symbol = ok ? this.unicode ? "\u2714" : "OK" : this.unicode ? "\u2716" : "X";
|
|
3059
|
+
this.draw(symbol, ok ? 32 : 31);
|
|
3060
|
+
this.output.write("\n");
|
|
3061
|
+
this.label = null;
|
|
3062
|
+
}
|
|
3063
|
+
/** Clears an unfinished phase without recording a result. */
|
|
3064
|
+
stop() {
|
|
3065
|
+
this.clearTimer();
|
|
3066
|
+
if (this.label != null) {
|
|
3067
|
+
this.output.write(`\r${ESC2}[2K`);
|
|
3068
|
+
this.label = null;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
clearTimer() {
|
|
3072
|
+
if (this.timer != null) {
|
|
3073
|
+
clearInterval(this.timer);
|
|
3074
|
+
this.timer = null;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
draw(symbol, color) {
|
|
3078
|
+
const columns = this.output.columns;
|
|
3079
|
+
const width = Math.max(0, (columns != null && columns > 0 ? columns : 80) - 1);
|
|
3080
|
+
const elapsed = ` ${((Date.now() - this.startedAt) / 1e3).toFixed(1)}s`;
|
|
3081
|
+
const prefix = ` ${symbol} `;
|
|
3082
|
+
const labelWidth = Math.min(48, Math.max(0, width - displayWidth(prefix) - displayWidth(elapsed)));
|
|
3083
|
+
const label = this.fit(this.label ?? "", labelWidth);
|
|
3084
|
+
const line = this.fit(`${prefix}${label}${" ".repeat(labelWidth - displayWidth(label))}${elapsed}`, width);
|
|
3085
|
+
const painted = this.color ? `${ESC2}[${color}m${line}${ESC2}[39m` : line;
|
|
3086
|
+
this.output.write(`\r${ESC2}[2K${painted}`);
|
|
3087
|
+
}
|
|
3088
|
+
fit(text, width) {
|
|
3089
|
+
if (displayWidth(text) <= width) {
|
|
3090
|
+
return text;
|
|
3091
|
+
}
|
|
3092
|
+
if (width < 1) {
|
|
3093
|
+
return "";
|
|
3094
|
+
}
|
|
3095
|
+
let result = "";
|
|
3096
|
+
let used = 0;
|
|
3097
|
+
for (const character of text) {
|
|
3098
|
+
const size = displayWidth(character);
|
|
3099
|
+
if (used + size > width - 1) {
|
|
3100
|
+
break;
|
|
3101
|
+
}
|
|
3102
|
+
result += character;
|
|
3103
|
+
used += size;
|
|
3104
|
+
}
|
|
3105
|
+
return `${result}${this.unicode ? "\u2026" : "~"}`;
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
|
|
3109
|
+
// src/modules/manage/connection-check.ts
|
|
3110
|
+
function profileFromEnv(output, env) {
|
|
3111
|
+
try {
|
|
3112
|
+
const profile = loadHostProfile(env);
|
|
3113
|
+
if (profile == null) {
|
|
3114
|
+
output.write(" The entry has no host.\n");
|
|
3115
|
+
}
|
|
3116
|
+
return profile;
|
|
3117
|
+
} catch (ex) {
|
|
3118
|
+
let detail = String(ex);
|
|
3119
|
+
if (ex instanceof Error) {
|
|
3120
|
+
detail = ex.message;
|
|
3121
|
+
}
|
|
3122
|
+
output.write(` The server would refuse to start with this entry: ${detail}
|
|
3123
|
+
`);
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function describeAuth(profile) {
|
|
3128
|
+
if (profile.privateKeyPath != null) {
|
|
3129
|
+
return `key ${profile.privateKeyPath}`;
|
|
3130
|
+
}
|
|
3131
|
+
if (profile.password != null) {
|
|
3132
|
+
return "password";
|
|
3133
|
+
}
|
|
3134
|
+
return "ssh agent";
|
|
3135
|
+
}
|
|
3136
|
+
async function runConnectionCheck(profile, output) {
|
|
3137
|
+
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
3138
|
+
const auth = describeAuth(profile);
|
|
3139
|
+
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
3140
|
+
`);
|
|
3141
|
+
if (profile.proxyCommand != null) {
|
|
3142
|
+
output.write(` via proxy command: ${profile.proxyCommand}
|
|
3143
|
+
`);
|
|
3144
|
+
}
|
|
3145
|
+
if (profile.jump != null) {
|
|
3146
|
+
output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
|
|
3147
|
+
`);
|
|
3148
|
+
}
|
|
3149
|
+
output.write("\n");
|
|
3150
|
+
const connection = new SshConnection(profile);
|
|
3151
|
+
const progress = new CheckProgress(output);
|
|
3152
|
+
const startedAt = Date.now();
|
|
3153
|
+
let connected = false;
|
|
3154
|
+
let detail = "";
|
|
3155
|
+
try {
|
|
3156
|
+
progress.start("Preparing SSH connection");
|
|
3157
|
+
await connection.connect((stage) => {
|
|
3158
|
+
if (stage === "transport") {
|
|
3159
|
+
progress.start(profile.jump != null ? "Jump host connection and forwarding" : "Starting proxy connection");
|
|
3160
|
+
} else {
|
|
3161
|
+
progress.start("SSH handshake and authentication");
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
connected = true;
|
|
3165
|
+
} catch (ex) {
|
|
3166
|
+
detail = String(ex);
|
|
3167
|
+
if (ex instanceof Error) {
|
|
3168
|
+
detail = ex.message;
|
|
3169
|
+
}
|
|
3170
|
+
} finally {
|
|
3171
|
+
progress.finish(connected);
|
|
3172
|
+
connection.disconnect();
|
|
3173
|
+
}
|
|
3174
|
+
const durationMs = Date.now() - startedAt;
|
|
3175
|
+
if (connected == true) {
|
|
3176
|
+
output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
|
|
3177
|
+
`);
|
|
3178
|
+
} else {
|
|
3179
|
+
output.write(` FAILED ${endpoint} (${auth})
|
|
3180
|
+
`);
|
|
3181
|
+
output.write(` ${detail}
|
|
3182
|
+
`);
|
|
3183
|
+
}
|
|
3184
|
+
if (connection.hostKeyFingerprint != null) {
|
|
3185
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
|
|
3186
|
+
if (profile.hostKeyFingerprint != null) {
|
|
3187
|
+
pinNote = "pinned";
|
|
3188
|
+
}
|
|
3189
|
+
output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
|
|
3190
|
+
`);
|
|
3191
|
+
}
|
|
3192
|
+
if (connection.jumpHostKeyFingerprint != null) {
|
|
3193
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
|
|
3194
|
+
if (profile.jump?.hostKeyFingerprint != null) {
|
|
3195
|
+
pinNote = "pinned";
|
|
3196
|
+
}
|
|
3197
|
+
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
3198
|
+
`);
|
|
3199
|
+
}
|
|
3200
|
+
return {
|
|
3201
|
+
connected,
|
|
3202
|
+
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
3203
|
+
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// src/modules/manage/copy-entry.ts
|
|
3208
|
+
async function copyEntryToClient(prompter, source, targets, name, entry) {
|
|
3209
|
+
const destinations = targets.filter((target) => target.id !== source.id);
|
|
3210
|
+
if (destinations.length === 0) {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
for (; ; ) {
|
|
3214
|
+
const items = destinations.map((target) => ({
|
|
3215
|
+
value: target.id,
|
|
3216
|
+
key: target.id,
|
|
3217
|
+
label: target.label,
|
|
3218
|
+
hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
|
|
3219
|
+
}));
|
|
3220
|
+
items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
|
|
3221
|
+
const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
|
|
3222
|
+
const destination = destinations.find((target) => target.id === picked);
|
|
3223
|
+
if (destination == null) {
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
|
|
3227
|
+
if (exists) {
|
|
3228
|
+
try {
|
|
3229
|
+
const overwrite = await prompter.askYesNo(
|
|
3230
|
+
`${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
|
|
3231
|
+
false
|
|
3232
|
+
);
|
|
3233
|
+
if (overwrite == false) {
|
|
3234
|
+
prompter.write(" Not copied; the destination entry was kept.\n\n");
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
} catch (ex) {
|
|
3238
|
+
if (ex instanceof PromptBackError == false) {
|
|
3239
|
+
throw ex;
|
|
3240
|
+
}
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
const result = destination.writeEntry(name, {
|
|
3245
|
+
command: entry.command,
|
|
3246
|
+
args: [...entry.args],
|
|
3247
|
+
env: { ...entry.env }
|
|
3248
|
+
}, { replace: true });
|
|
3249
|
+
if (result.status === "written") {
|
|
3250
|
+
prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
|
|
3251
|
+
`);
|
|
3252
|
+
prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3253
|
+
} else {
|
|
3254
|
+
prompter.write(` Not copied: ${result.detail}
|
|
3255
|
+
|
|
3256
|
+
`);
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// src/modules/manage/entry.ts
|
|
3263
|
+
var MANAGED_ENV_NAMES = [
|
|
3264
|
+
SINGLE_HOST_ENV.HOST,
|
|
3265
|
+
SINGLE_HOST_ENV.PORT,
|
|
3266
|
+
SINGLE_HOST_ENV.USER,
|
|
3267
|
+
SINGLE_HOST_ENV.KEY,
|
|
3268
|
+
SINGLE_HOST_ENV.PASSPHRASE,
|
|
3269
|
+
SINGLE_HOST_ENV.PASSWORD,
|
|
3270
|
+
SINGLE_HOST_ENV.DESCRIPTION,
|
|
3271
|
+
SINGLE_HOST_ENV.JUMP_HOST,
|
|
3272
|
+
SINGLE_HOST_ENV.JUMP_PORT,
|
|
3273
|
+
SINGLE_HOST_ENV.JUMP_USER,
|
|
3274
|
+
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
3275
|
+
SINGLE_HOST_ENV.READONLY,
|
|
3276
|
+
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
3277
|
+
SINGLE_HOST_ENV.HOST_KEY,
|
|
3278
|
+
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
3279
|
+
];
|
|
3280
|
+
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
3281
|
+
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
3282
|
+
function isOurEntry(entry) {
|
|
3283
|
+
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
3284
|
+
return true;
|
|
3285
|
+
}
|
|
3286
|
+
const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
3287
|
+
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
3288
|
+
}
|
|
3289
|
+
function launchCommand() {
|
|
3290
|
+
if (process.platform === "win32") {
|
|
3291
|
+
return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
|
|
3292
|
+
}
|
|
3293
|
+
return { command: "npx", args: ["-y", PACKAGE_NAME] };
|
|
3294
|
+
}
|
|
3295
|
+
function answersToEntry(answers) {
|
|
3296
|
+
const env = {};
|
|
3297
|
+
env[SINGLE_HOST_ENV.HOST] = answers.host;
|
|
3298
|
+
if (answers.port !== DEFAULT_SSH_PORT) {
|
|
3299
|
+
env[SINGLE_HOST_ENV.PORT] = String(answers.port);
|
|
3300
|
+
}
|
|
3301
|
+
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
3302
|
+
if (answers.auth === "key" && answers.keyPath != null) {
|
|
3303
|
+
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
3304
|
+
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
3305
|
+
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (answers.auth === "password" && answers.password != null) {
|
|
3309
|
+
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
3310
|
+
}
|
|
3311
|
+
if (answers.hostKeyFingerprint != null) {
|
|
3312
|
+
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
3313
|
+
}
|
|
3314
|
+
if (answers.description != null && answers.description !== "") {
|
|
3315
|
+
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
3316
|
+
}
|
|
3317
|
+
if (answers.jumpHost != null) {
|
|
3318
|
+
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
3319
|
+
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
3320
|
+
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
3321
|
+
}
|
|
3322
|
+
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
3323
|
+
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
3324
|
+
}
|
|
3325
|
+
if (answers.jumpHostKeyFingerprint != null) {
|
|
3326
|
+
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
3330
|
+
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
3331
|
+
}
|
|
3332
|
+
if (answers.readonly == true) {
|
|
3333
|
+
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
3334
|
+
}
|
|
3335
|
+
if (answers.allowedPaths.length > 0) {
|
|
3336
|
+
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
3337
|
+
}
|
|
3338
|
+
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
3339
|
+
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
env[name] = value;
|
|
3346
|
+
}
|
|
3347
|
+
const launch = launchCommand();
|
|
3348
|
+
return {
|
|
3349
|
+
command: launch.command,
|
|
3350
|
+
args: launch.args,
|
|
3351
|
+
env
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function readPort(value) {
|
|
3355
|
+
if (value == null) {
|
|
3356
|
+
return DEFAULT_SSH_PORT;
|
|
3357
|
+
}
|
|
3358
|
+
const parsed = Number(value);
|
|
3359
|
+
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
3360
|
+
return DEFAULT_SSH_PORT;
|
|
3361
|
+
}
|
|
3362
|
+
return parsed;
|
|
3363
|
+
}
|
|
3364
|
+
function isTruthy(value) {
|
|
3365
|
+
if (value == null) {
|
|
3366
|
+
return false;
|
|
3367
|
+
}
|
|
3368
|
+
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
3369
|
+
}
|
|
3370
|
+
function entryToAnswers(name, entry) {
|
|
3371
|
+
const env = entry.env;
|
|
3372
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
3373
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
3374
|
+
let auth = "agent";
|
|
3375
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3376
|
+
auth = "key";
|
|
3377
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3378
|
+
auth = "password";
|
|
3379
|
+
}
|
|
3380
|
+
let allowedPaths = [];
|
|
3381
|
+
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
3382
|
+
if (allowedPathsValue != null) {
|
|
3383
|
+
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3384
|
+
}
|
|
3385
|
+
const extraEnv = {};
|
|
3386
|
+
for (const [variable, value] of Object.entries(env)) {
|
|
3387
|
+
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
3388
|
+
extraEnv[variable] = value;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
const answers = {
|
|
3392
|
+
name,
|
|
3393
|
+
host,
|
|
3394
|
+
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
3395
|
+
username,
|
|
3396
|
+
auth,
|
|
3397
|
+
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
3398
|
+
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
3399
|
+
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
3400
|
+
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
3401
|
+
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
3402
|
+
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
3403
|
+
allowedPaths,
|
|
3404
|
+
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
3405
|
+
extraEnv
|
|
3406
|
+
};
|
|
3407
|
+
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
3408
|
+
if (jumpHost != null) {
|
|
3409
|
+
answers.jumpHost = jumpHost;
|
|
3410
|
+
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
3411
|
+
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
3412
|
+
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
3413
|
+
}
|
|
3414
|
+
return answers;
|
|
3415
|
+
}
|
|
3416
|
+
function summarizeEntry(entry) {
|
|
3417
|
+
const env = entry.env;
|
|
3418
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
3419
|
+
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
3420
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
3421
|
+
let auth = "agent";
|
|
3422
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3423
|
+
auth = "key";
|
|
3424
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3425
|
+
auth = "password";
|
|
3426
|
+
}
|
|
3427
|
+
const notes = [];
|
|
3428
|
+
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
3429
|
+
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
3430
|
+
}
|
|
3431
|
+
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
3432
|
+
notes.push("proxy");
|
|
3433
|
+
}
|
|
3434
|
+
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
3435
|
+
notes.push("readonly");
|
|
3436
|
+
}
|
|
3437
|
+
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
3438
|
+
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
3439
|
+
}
|
|
3440
|
+
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
3441
|
+
notes.push("pinned");
|
|
3442
|
+
}
|
|
3443
|
+
return {
|
|
3444
|
+
endpoint: `${username}@${host}:${port}`,
|
|
3445
|
+
auth,
|
|
3446
|
+
notes: notes.join(", ")
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3283
3449
|
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3284
3450
|
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3285
3451
|
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
@@ -3323,7 +3489,7 @@ function findPrivateKeys() {
|
|
|
3323
3489
|
const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
|
|
3324
3490
|
let names = [];
|
|
3325
3491
|
try {
|
|
3326
|
-
names =
|
|
3492
|
+
names = fs7__default.default.readdirSync(sshDirectory);
|
|
3327
3493
|
} catch {
|
|
3328
3494
|
return [];
|
|
3329
3495
|
}
|
|
@@ -3335,14 +3501,14 @@ function findPrivateKeys() {
|
|
|
3335
3501
|
const fullPath = path__default.default.join(sshDirectory, name);
|
|
3336
3502
|
let head = "";
|
|
3337
3503
|
try {
|
|
3338
|
-
const stat =
|
|
3504
|
+
const stat = fs7__default.default.statSync(fullPath);
|
|
3339
3505
|
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3340
3506
|
continue;
|
|
3341
3507
|
}
|
|
3342
|
-
const descriptor =
|
|
3508
|
+
const descriptor = fs7__default.default.openSync(fullPath, "r");
|
|
3343
3509
|
const buffer = Buffer.alloc(64);
|
|
3344
|
-
const bytesRead =
|
|
3345
|
-
|
|
3510
|
+
const bytesRead = fs7__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3511
|
+
fs7__default.default.closeSync(descriptor);
|
|
3346
3512
|
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3347
3513
|
} catch {
|
|
3348
3514
|
continue;
|
|
@@ -3354,7 +3520,7 @@ function findPrivateKeys() {
|
|
|
3354
3520
|
}
|
|
3355
3521
|
let hint = "";
|
|
3356
3522
|
try {
|
|
3357
|
-
const publicKey =
|
|
3523
|
+
const publicKey = fs7__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3358
3524
|
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3359
3525
|
if (publicKey.length > 2) {
|
|
3360
3526
|
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
@@ -3423,7 +3589,7 @@ async function askKeyPath(prompter, current) {
|
|
|
3423
3589
|
}
|
|
3424
3590
|
for (; ; ) {
|
|
3425
3591
|
const typed = await prompter.askRequired("Private key path");
|
|
3426
|
-
if (
|
|
3592
|
+
if (fs7__default.default.existsSync(expandHomePath(typed)) == true) {
|
|
3427
3593
|
return typed;
|
|
3428
3594
|
}
|
|
3429
3595
|
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
@@ -3635,10 +3801,12 @@ async function testConnection(prompter, output, state) {
|
|
|
3635
3801
|
if (profile == null) {
|
|
3636
3802
|
output.write("\n");
|
|
3637
3803
|
state.lastTest = "failed";
|
|
3804
|
+
await prompter.pause();
|
|
3638
3805
|
return false;
|
|
3639
3806
|
}
|
|
3640
3807
|
const result = await runConnectionCheck(profile, output);
|
|
3641
3808
|
output.write("\n");
|
|
3809
|
+
await prompter.pause();
|
|
3642
3810
|
if (result.connected == true) {
|
|
3643
3811
|
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3644
3812
|
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
@@ -3955,6 +4123,65 @@ async function runHostForm(prompter, output, options) {
|
|
|
3955
4123
|
prompter.transient = previousTransient;
|
|
3956
4124
|
}
|
|
3957
4125
|
}
|
|
4126
|
+
function readExisting(filePath) {
|
|
4127
|
+
try {
|
|
4128
|
+
return fs7__default.default.readFileSync(filePath, "utf8");
|
|
4129
|
+
} catch (error) {
|
|
4130
|
+
if (error.code === "ENOENT") {
|
|
4131
|
+
return null;
|
|
4132
|
+
}
|
|
4133
|
+
throw error;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
function updateConfigFile(filePath, update) {
|
|
4137
|
+
let destination = filePath;
|
|
4138
|
+
let symbolicLink = false;
|
|
4139
|
+
try {
|
|
4140
|
+
symbolicLink = fs7__default.default.lstatSync(filePath).isSymbolicLink();
|
|
4141
|
+
} catch (error) {
|
|
4142
|
+
if (error.code !== "ENOENT") {
|
|
4143
|
+
throw error;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
if (symbolicLink) {
|
|
4147
|
+
destination = fs7__default.default.realpathSync(filePath);
|
|
4148
|
+
}
|
|
4149
|
+
const before = readExisting(destination);
|
|
4150
|
+
const updated = update(before ?? "");
|
|
4151
|
+
if (before != null) {
|
|
4152
|
+
fs7__default.default.accessSync(destination, fs7__default.default.constants.W_OK);
|
|
4153
|
+
}
|
|
4154
|
+
fs7__default.default.mkdirSync(path__default.default.dirname(destination), { recursive: true });
|
|
4155
|
+
const temporary = path__default.default.join(path__default.default.dirname(destination), `.${path__default.default.basename(destination)}.${crypto.randomUUID()}.tmp`);
|
|
4156
|
+
const mode = before == null ? 384 : fs7__default.default.statSync(destination).mode & 511;
|
|
4157
|
+
let descriptor;
|
|
4158
|
+
let ownsTemporary = false;
|
|
4159
|
+
try {
|
|
4160
|
+
descriptor = fs7__default.default.openSync(temporary, "wx", mode);
|
|
4161
|
+
ownsTemporary = true;
|
|
4162
|
+
fs7__default.default.writeFileSync(descriptor, updated, "utf8");
|
|
4163
|
+
fs7__default.default.fsyncSync(descriptor);
|
|
4164
|
+
fs7__default.default.closeSync(descriptor);
|
|
4165
|
+
descriptor = void 0;
|
|
4166
|
+
if (readExisting(destination) !== before) {
|
|
4167
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
4168
|
+
}
|
|
4169
|
+
fs7__default.default.renameSync(temporary, destination);
|
|
4170
|
+
ownsTemporary = false;
|
|
4171
|
+
} finally {
|
|
4172
|
+
try {
|
|
4173
|
+
if (descriptor != null) {
|
|
4174
|
+
fs7__default.default.closeSync(descriptor);
|
|
4175
|
+
}
|
|
4176
|
+
} finally {
|
|
4177
|
+
if (ownsTemporary) {
|
|
4178
|
+
fs7__default.default.unlinkSync(temporary);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// src/modules/manage/targets/json-file.ts
|
|
3958
4185
|
function toMcpServerEntry(value) {
|
|
3959
4186
|
if (typeof value !== "object" || value == null) {
|
|
3960
4187
|
return null;
|
|
@@ -3967,7 +4194,7 @@ function toMcpServerEntry(value) {
|
|
|
3967
4194
|
if (Array.isArray(record.args) == true) {
|
|
3968
4195
|
args = record.args.filter((item) => typeof item === "string");
|
|
3969
4196
|
}
|
|
3970
|
-
const env =
|
|
4197
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
3971
4198
|
if (typeof record.env === "object" && record.env != null) {
|
|
3972
4199
|
for (const [key, item] of Object.entries(record.env)) {
|
|
3973
4200
|
if (typeof item === "string") {
|
|
@@ -3977,16 +4204,22 @@ function toMcpServerEntry(value) {
|
|
|
3977
4204
|
}
|
|
3978
4205
|
return { command: record.command, args, env };
|
|
3979
4206
|
}
|
|
3980
|
-
function
|
|
3981
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)
|
|
4207
|
+
function readRawMcpServersJson(document) {
|
|
4208
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) {
|
|
3982
4209
|
throw new Error("the config file is not a JSON object");
|
|
3983
4210
|
}
|
|
3984
|
-
const entries = {};
|
|
3985
4211
|
const servers = document.mcpServers;
|
|
3986
|
-
if (
|
|
3987
|
-
return
|
|
4212
|
+
if (servers === void 0) {
|
|
4213
|
+
return {};
|
|
4214
|
+
}
|
|
4215
|
+
if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
|
|
4216
|
+
throw new Error("mcpServers is not a JSON object");
|
|
3988
4217
|
}
|
|
3989
|
-
|
|
4218
|
+
return servers;
|
|
4219
|
+
}
|
|
4220
|
+
function readMcpServersJson(document) {
|
|
4221
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
4222
|
+
for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
|
|
3990
4223
|
const entry = toMcpServerEntry(value);
|
|
3991
4224
|
if (entry != null) {
|
|
3992
4225
|
entries[name] = entry;
|
|
@@ -3994,11 +4227,11 @@ function readMcpServersJson(document) {
|
|
|
3994
4227
|
}
|
|
3995
4228
|
return entries;
|
|
3996
4229
|
}
|
|
3997
|
-
function readDocument(filePath) {
|
|
3998
|
-
if (
|
|
4230
|
+
function readDocument(filePath, contents) {
|
|
4231
|
+
if (contents == null && fs7__default.default.existsSync(filePath) == false) {
|
|
3999
4232
|
return {};
|
|
4000
4233
|
}
|
|
4001
|
-
const text =
|
|
4234
|
+
const text = contents ?? fs7__default.default.readFileSync(filePath, "utf8");
|
|
4002
4235
|
if (text.trim() === "") {
|
|
4003
4236
|
return {};
|
|
4004
4237
|
}
|
|
@@ -4019,39 +4252,35 @@ function readDocument(filePath) {
|
|
|
4019
4252
|
}
|
|
4020
4253
|
function createJsonFileTarget(options) {
|
|
4021
4254
|
const { id, label, filePath } = options;
|
|
4022
|
-
function writeDocument(document) {
|
|
4023
|
-
fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
|
|
4024
|
-
fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
4025
|
-
`, "utf8");
|
|
4026
|
-
}
|
|
4027
4255
|
return {
|
|
4028
4256
|
id,
|
|
4029
4257
|
label,
|
|
4030
4258
|
location: filePath,
|
|
4031
|
-
detected:
|
|
4259
|
+
detected: fs7__default.default.existsSync(filePath),
|
|
4032
4260
|
readEntries() {
|
|
4033
4261
|
const document = readDocument(filePath);
|
|
4034
4262
|
return readMcpServersJson(document);
|
|
4035
4263
|
},
|
|
4264
|
+
hasEntry(name) {
|
|
4265
|
+
return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
|
|
4266
|
+
},
|
|
4036
4267
|
writeEntry(name, entry) {
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
servers =
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
writeDocument(document);
|
|
4268
|
+
updateConfigFile(filePath, (contents) => {
|
|
4269
|
+
const document = readDocument(filePath, contents);
|
|
4270
|
+
const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
|
|
4271
|
+
servers[name] = entry;
|
|
4272
|
+
document.mcpServers = servers;
|
|
4273
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4274
|
+
});
|
|
4045
4275
|
return { status: "written", detail: filePath };
|
|
4046
4276
|
},
|
|
4047
4277
|
removeEntry(name) {
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
writeDocument(document);
|
|
4278
|
+
updateConfigFile(filePath, (contents) => {
|
|
4279
|
+
const document = readDocument(filePath, contents);
|
|
4280
|
+
const servers = readRawMcpServersJson(document);
|
|
4281
|
+
delete servers[name];
|
|
4282
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4283
|
+
});
|
|
4055
4284
|
return { status: "written", detail: filePath };
|
|
4056
4285
|
}
|
|
4057
4286
|
};
|
|
@@ -4103,11 +4332,11 @@ function runClaude(executable, args) {
|
|
|
4103
4332
|
}
|
|
4104
4333
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
4105
4334
|
}
|
|
4106
|
-
function
|
|
4107
|
-
if (
|
|
4335
|
+
function readRawClaudeEntries() {
|
|
4336
|
+
if (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
4108
4337
|
return {};
|
|
4109
4338
|
}
|
|
4110
|
-
const text =
|
|
4339
|
+
const text = fs7__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
4111
4340
|
let document;
|
|
4112
4341
|
try {
|
|
4113
4342
|
document = JSON.parse(text);
|
|
@@ -4118,7 +4347,7 @@ function readClaudeEntries() {
|
|
|
4118
4347
|
}
|
|
4119
4348
|
throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
|
|
4120
4349
|
}
|
|
4121
|
-
return
|
|
4350
|
+
return readRawMcpServersJson(document);
|
|
4122
4351
|
}
|
|
4123
4352
|
function createClaudeCodeTarget() {
|
|
4124
4353
|
const executable = findExecutable("claude");
|
|
@@ -4126,28 +4355,35 @@ function createClaudeCodeTarget() {
|
|
|
4126
4355
|
id: "claude-code",
|
|
4127
4356
|
label: "Claude Code (user scope)",
|
|
4128
4357
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
4129
|
-
detected: executable != null ||
|
|
4130
|
-
readEntries
|
|
4358
|
+
detected: executable != null || fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
4359
|
+
readEntries() {
|
|
4360
|
+
return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
|
|
4361
|
+
},
|
|
4362
|
+
hasEntry(name) {
|
|
4363
|
+
return Object.hasOwn(readRawClaudeEntries(), name);
|
|
4364
|
+
},
|
|
4131
4365
|
writeEntry(name, entry) {
|
|
4132
4366
|
if (executable == null) {
|
|
4133
4367
|
return {
|
|
4134
4368
|
status: "unavailable",
|
|
4135
|
-
detail:
|
|
4136
|
-
${formatClaudeAddCommand(name, entry)}`
|
|
4369
|
+
detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
|
|
4137
4370
|
};
|
|
4138
4371
|
}
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4372
|
+
const previousEntries = readRawClaudeEntries();
|
|
4373
|
+
const hadPrevious = Object.hasOwn(previousEntries, name);
|
|
4374
|
+
const previous = previousEntries[name];
|
|
4375
|
+
if (hadPrevious) {
|
|
4141
4376
|
const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
4142
4377
|
if (removed.status === "unavailable") {
|
|
4143
4378
|
return removed;
|
|
4144
4379
|
}
|
|
4145
4380
|
}
|
|
4146
4381
|
const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
|
|
4147
|
-
if (added.status === "unavailable" &&
|
|
4148
|
-
runClaude(executable, ["mcp", "add-json", name,
|
|
4382
|
+
if (added.status === "unavailable" && hadPrevious) {
|
|
4383
|
+
const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
|
|
4384
|
+
const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
|
|
4149
4385
|
return { status: "unavailable", detail: `${added.detail}
|
|
4150
|
-
|
|
4386
|
+
${recovery}` };
|
|
4151
4387
|
}
|
|
4152
4388
|
return added;
|
|
4153
4389
|
},
|
|
@@ -4164,194 +4400,121 @@ function createClaudeCodeTarget() {
|
|
|
4164
4400
|
};
|
|
4165
4401
|
}
|
|
4166
4402
|
|
|
4167
|
-
// src/modules/manage/targets/toml-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
}
|
|
4187
|
-
parseValue() {
|
|
4188
|
-
this.skipWhitespace();
|
|
4189
|
-
const current = this.text[this.position];
|
|
4190
|
-
if (current === '"') {
|
|
4191
|
-
return this.parseBasicString();
|
|
4192
|
-
}
|
|
4193
|
-
if (current === "'") {
|
|
4194
|
-
return this.parseLiteralString();
|
|
4195
|
-
}
|
|
4196
|
-
if (current === "[") {
|
|
4197
|
-
return this.parseArray();
|
|
4198
|
-
}
|
|
4199
|
-
if (current === "{") {
|
|
4200
|
-
return this.parseInlineTable();
|
|
4201
|
-
}
|
|
4202
|
-
throw new Error(`unsupported toml value at offset ${this.position}`);
|
|
4203
|
-
}
|
|
4204
|
-
parseBasicString() {
|
|
4205
|
-
this.position += 1;
|
|
4206
|
-
let result = "";
|
|
4207
|
-
while (this.position < this.text.length) {
|
|
4208
|
-
const current = this.text[this.position];
|
|
4209
|
-
if (current === '"') {
|
|
4210
|
-
this.position += 1;
|
|
4211
|
-
return result;
|
|
4212
|
-
}
|
|
4213
|
-
if (current !== "\\") {
|
|
4214
|
-
result += current;
|
|
4215
|
-
this.position += 1;
|
|
4216
|
-
continue;
|
|
4217
|
-
}
|
|
4218
|
-
const escaped = this.text[this.position + 1];
|
|
4219
|
-
this.position += 2;
|
|
4220
|
-
if (escaped === "n") {
|
|
4221
|
-
result += "\n";
|
|
4222
|
-
} else if (escaped === "t") {
|
|
4223
|
-
result += " ";
|
|
4224
|
-
} else if (escaped === "r") {
|
|
4225
|
-
result += "\r";
|
|
4226
|
-
} else if (escaped === "u" || escaped === "U") {
|
|
4227
|
-
let length = 4;
|
|
4228
|
-
if (escaped === "U") {
|
|
4229
|
-
length = 8;
|
|
4403
|
+
// src/modules/manage/targets/toml-lines.ts
|
|
4404
|
+
function lineState() {
|
|
4405
|
+
return { quote: null, multiline: false, depth: 0 };
|
|
4406
|
+
}
|
|
4407
|
+
function scanTomlLine(line, state) {
|
|
4408
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
4409
|
+
const character = line[index];
|
|
4410
|
+
if (state.quote != null) {
|
|
4411
|
+
if (character === "\\" && state.quote === '"') {
|
|
4412
|
+
index += 1;
|
|
4413
|
+
} else if (character === state.quote) {
|
|
4414
|
+
if (!state.multiline) {
|
|
4415
|
+
state.quote = null;
|
|
4416
|
+
} else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
4417
|
+
while (line[index + 1] === character) {
|
|
4418
|
+
index += 1;
|
|
4419
|
+
}
|
|
4420
|
+
state.quote = null;
|
|
4421
|
+
state.multiline = false;
|
|
4230
4422
|
}
|
|
4231
|
-
const hex = this.text.slice(this.position, this.position + length);
|
|
4232
|
-
result += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
4233
|
-
this.position += length;
|
|
4234
|
-
} else {
|
|
4235
|
-
result += escaped;
|
|
4236
4423
|
}
|
|
4424
|
+
continue;
|
|
4237
4425
|
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
parseLiteralString() {
|
|
4241
|
-
const end = this.text.indexOf("'", this.position + 1);
|
|
4242
|
-
if (end < 0) {
|
|
4243
|
-
throw new Error("unterminated toml literal string");
|
|
4426
|
+
if (character === "#") {
|
|
4427
|
+
break;
|
|
4244
4428
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
this.position += 1;
|
|
4251
|
-
const items = [];
|
|
4252
|
-
for (; ; ) {
|
|
4253
|
-
this.skipWhitespace();
|
|
4254
|
-
if (this.position >= this.text.length) {
|
|
4255
|
-
throw new Error("unterminated toml array");
|
|
4256
|
-
}
|
|
4257
|
-
if (this.text[this.position] === "]") {
|
|
4258
|
-
this.position += 1;
|
|
4259
|
-
return items;
|
|
4260
|
-
}
|
|
4261
|
-
items.push(this.parseValue());
|
|
4262
|
-
this.skipWhitespace();
|
|
4263
|
-
if (this.text[this.position] === ",") {
|
|
4264
|
-
this.position += 1;
|
|
4429
|
+
if (character === '"' || character === "'") {
|
|
4430
|
+
state.quote = character;
|
|
4431
|
+
state.multiline = line.slice(index, index + 3) === character.repeat(3);
|
|
4432
|
+
if (state.multiline) {
|
|
4433
|
+
index += 2;
|
|
4265
4434
|
}
|
|
4435
|
+
} else if (character === "[" || character === "{") {
|
|
4436
|
+
state.depth += 1;
|
|
4437
|
+
} else if (character === "]" || character === "}") {
|
|
4438
|
+
state.depth -= 1;
|
|
4266
4439
|
}
|
|
4267
4440
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
}
|
|
4280
|
-
let key;
|
|
4281
|
-
if (this.text[this.position] === '"') {
|
|
4282
|
-
key = this.parseBasicString();
|
|
4283
|
-
} else {
|
|
4284
|
-
const match = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.position));
|
|
4285
|
-
if (match == null) {
|
|
4286
|
-
throw new Error(`invalid inline table key at offset ${this.position}`);
|
|
4287
|
-
}
|
|
4288
|
-
key = match[0];
|
|
4289
|
-
this.position += key.length;
|
|
4290
|
-
}
|
|
4291
|
-
this.skipWhitespace();
|
|
4292
|
-
if (this.text[this.position] !== "=") {
|
|
4293
|
-
throw new Error(`expected '=' at offset ${this.position}`);
|
|
4294
|
-
}
|
|
4295
|
-
this.position += 1;
|
|
4296
|
-
table[key] = this.parseValue();
|
|
4297
|
-
this.skipWhitespace();
|
|
4298
|
-
if (this.text[this.position] === ",") {
|
|
4299
|
-
this.position += 1;
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4441
|
+
}
|
|
4442
|
+
function isTable(value) {
|
|
4443
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4444
|
+
}
|
|
4445
|
+
function parseConfigToml(text) {
|
|
4446
|
+
let document;
|
|
4447
|
+
try {
|
|
4448
|
+
document = smolToml.parse(text, { integersAsBigInt: true });
|
|
4449
|
+
} catch (error) {
|
|
4450
|
+
const location = error instanceof smolToml.TomlError ? ` at line ${error.line}, column ${error.column}` : "";
|
|
4451
|
+
throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
|
|
4302
4452
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
if (rawKey.startsWith('"') == false) {
|
|
4306
|
-
return rawKey;
|
|
4453
|
+
if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
|
|
4454
|
+
throw new Error("mcp_servers must be a TOML table.");
|
|
4307
4455
|
}
|
|
4308
|
-
|
|
4309
|
-
return parsed;
|
|
4456
|
+
return document;
|
|
4310
4457
|
}
|
|
4311
|
-
function
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
quote = current;
|
|
4326
|
-
} else if (current === "#") {
|
|
4327
|
-
break;
|
|
4328
|
-
} else if (current === "[" || current === "{") {
|
|
4329
|
-
balance += 1;
|
|
4330
|
-
} else if (current === "]" || current === "}") {
|
|
4331
|
-
balance -= 1;
|
|
4458
|
+
function readTomlEntries(text) {
|
|
4459
|
+
return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
|
|
4460
|
+
}
|
|
4461
|
+
function hasTomlEntry(text, name) {
|
|
4462
|
+
return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
|
|
4463
|
+
}
|
|
4464
|
+
function normalize(document) {
|
|
4465
|
+
const servers = document.mcp_servers;
|
|
4466
|
+
if (servers == null) {
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
for (const server of Object.values(servers)) {
|
|
4470
|
+
if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
|
|
4471
|
+
delete server.env;
|
|
4332
4472
|
}
|
|
4333
4473
|
}
|
|
4334
|
-
|
|
4474
|
+
if (Object.keys(servers).length === 0) {
|
|
4475
|
+
delete document.mcp_servers;
|
|
4476
|
+
}
|
|
4335
4477
|
}
|
|
4336
|
-
function
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4478
|
+
function verifyTomlEdit(before, after, name, entry) {
|
|
4479
|
+
const expected = parseConfigToml(before);
|
|
4480
|
+
const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
|
|
4481
|
+
if (entry == null) {
|
|
4482
|
+
delete servers[name];
|
|
4483
|
+
} else {
|
|
4484
|
+
const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
|
|
4485
|
+
Object.defineProperty(servers, name, {
|
|
4486
|
+
value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
|
|
4487
|
+
configurable: true,
|
|
4488
|
+
enumerable: true,
|
|
4489
|
+
writable: true
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
expected.mcp_servers = servers;
|
|
4493
|
+
let actual;
|
|
4494
|
+
try {
|
|
4495
|
+
actual = parseConfigToml(after);
|
|
4496
|
+
} catch {
|
|
4497
|
+
throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
|
|
4498
|
+
}
|
|
4499
|
+
normalize(expected);
|
|
4500
|
+
normalize(actual);
|
|
4501
|
+
if (!util.isDeepStrictEqual(expected, actual)) {
|
|
4502
|
+
throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
|
|
4503
|
+
}
|
|
4504
|
+
return after;
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
// src/modules/manage/targets/toml-subset.ts
|
|
4508
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4509
|
+
var ANY_HEADER_PATTERN = /^\s*\[/;
|
|
4510
|
+
var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
|
|
4511
|
+
var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
|
|
4512
|
+
var OWNED_TABLE_KEYS = ["command", "args", "env"];
|
|
4513
|
+
function unquoteKey(rawKey) {
|
|
4514
|
+
if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
|
|
4515
|
+
return parseConfigToml(`value = ${rawKey}`).value;
|
|
4353
4516
|
}
|
|
4354
|
-
return
|
|
4517
|
+
return rawKey;
|
|
4355
4518
|
}
|
|
4356
4519
|
function readStatements(body) {
|
|
4357
4520
|
const statements = [];
|
|
@@ -4363,28 +4526,25 @@ function readStatements(body) {
|
|
|
4363
4526
|
continue;
|
|
4364
4527
|
}
|
|
4365
4528
|
const start = index;
|
|
4366
|
-
|
|
4367
|
-
|
|
4529
|
+
const state = lineState();
|
|
4530
|
+
scanTomlLine(match[2], state);
|
|
4368
4531
|
index += 1;
|
|
4369
|
-
while (
|
|
4370
|
-
|
|
4371
|
-
rawValue += `
|
|
4372
|
-
${continuation}`;
|
|
4373
|
-
balance += bracketBalance(continuation);
|
|
4532
|
+
while ((state.quote != null || state.depth > 0) && index < body.length) {
|
|
4533
|
+
scanTomlLine(body[index], state);
|
|
4374
4534
|
index += 1;
|
|
4375
4535
|
}
|
|
4376
|
-
statements.push({ key: unquoteKey(match[1]),
|
|
4536
|
+
statements.push({ key: unquoteKey(match[1]), start, end: index });
|
|
4377
4537
|
}
|
|
4378
4538
|
return statements;
|
|
4379
4539
|
}
|
|
4380
4540
|
function splitSections(lines) {
|
|
4381
4541
|
const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
|
|
4382
|
-
|
|
4542
|
+
const state = lineState();
|
|
4383
4543
|
for (const line of lines) {
|
|
4384
4544
|
const current = sections[sections.length - 1];
|
|
4385
|
-
if (
|
|
4545
|
+
if (state.quote != null || state.depth > 0) {
|
|
4386
4546
|
current.body.push(line);
|
|
4387
|
-
|
|
4547
|
+
scanTomlLine(line, state);
|
|
4388
4548
|
continue;
|
|
4389
4549
|
}
|
|
4390
4550
|
if (ANY_HEADER_PATTERN.test(line) == true) {
|
|
@@ -4399,73 +4559,18 @@ function splitSections(lines) {
|
|
|
4399
4559
|
continue;
|
|
4400
4560
|
}
|
|
4401
4561
|
current.body.push(line);
|
|
4402
|
-
|
|
4403
|
-
if (keyValueMatch != null) {
|
|
4404
|
-
balance = bracketBalance(stripComment(keyValueMatch[2]));
|
|
4405
|
-
}
|
|
4562
|
+
scanTomlLine(line, state);
|
|
4406
4563
|
}
|
|
4407
4564
|
return sections;
|
|
4408
4565
|
}
|
|
4409
|
-
function
|
|
4410
|
-
|
|
4411
|
-
const value = new TomlValueParser(statement.rawValue).parse();
|
|
4412
|
-
return value;
|
|
4413
|
-
} catch {
|
|
4414
|
-
return null;
|
|
4415
|
-
}
|
|
4566
|
+
function hasMcpServerToml(text, name) {
|
|
4567
|
+
return hasTomlEntry(text, name);
|
|
4416
4568
|
}
|
|
4417
4569
|
function parseMcpServersToml(text) {
|
|
4418
|
-
|
|
4419
|
-
const entries = {};
|
|
4420
|
-
const envTables = {};
|
|
4421
|
-
for (const section of sections) {
|
|
4422
|
-
if (section.serverName == null) {
|
|
4423
|
-
continue;
|
|
4424
|
-
}
|
|
4425
|
-
if (section.subPath === "env") {
|
|
4426
|
-
const envTable = {};
|
|
4427
|
-
for (const statement of readStatements(section.body)) {
|
|
4428
|
-
const value = parseStatementValue(statement);
|
|
4429
|
-
if (typeof value === "string") {
|
|
4430
|
-
envTable[statement.key] = value;
|
|
4431
|
-
}
|
|
4432
|
-
}
|
|
4433
|
-
envTables[section.serverName] = envTable;
|
|
4434
|
-
continue;
|
|
4435
|
-
}
|
|
4436
|
-
if (section.subPath !== "") {
|
|
4437
|
-
continue;
|
|
4438
|
-
}
|
|
4439
|
-
let command = null;
|
|
4440
|
-
let args = [];
|
|
4441
|
-
const env = {};
|
|
4442
|
-
for (const statement of readStatements(section.body)) {
|
|
4443
|
-
const value = parseStatementValue(statement);
|
|
4444
|
-
if (statement.key === "command" && typeof value === "string") {
|
|
4445
|
-
command = value;
|
|
4446
|
-
} else if (statement.key === "args" && Array.isArray(value) == true) {
|
|
4447
|
-
args = value.filter((item) => typeof item === "string");
|
|
4448
|
-
} else if (statement.key === "env" && value != null && typeof value === "object" && Array.isArray(value) == false) {
|
|
4449
|
-
for (const [key, item] of Object.entries(value)) {
|
|
4450
|
-
if (typeof item === "string") {
|
|
4451
|
-
env[key] = item;
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
}
|
|
4456
|
-
if (command != null) {
|
|
4457
|
-
entries[section.serverName] = { command, args, env };
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
for (const [name, envTable] of Object.entries(envTables)) {
|
|
4461
|
-
if (entries[name] != null) {
|
|
4462
|
-
Object.assign(entries[name].env, envTable);
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
return entries;
|
|
4570
|
+
return readTomlEntries(text);
|
|
4466
4571
|
}
|
|
4467
4572
|
function quoteTomlString(value) {
|
|
4468
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
4573
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
4469
4574
|
return `"${escaped}"`;
|
|
4470
4575
|
}
|
|
4471
4576
|
function formatTomlKey(key) {
|
|
@@ -4509,6 +4614,11 @@ function joinSections(sections, eol) {
|
|
|
4509
4614
|
return lines.join(eol);
|
|
4510
4615
|
}
|
|
4511
4616
|
function writeMcpServerToml(text, name, entry) {
|
|
4617
|
+
parseConfigToml(text);
|
|
4618
|
+
const updated = editMcpServerToml(text, name, entry);
|
|
4619
|
+
return verifyTomlEdit(text, updated, name, entry);
|
|
4620
|
+
}
|
|
4621
|
+
function editMcpServerToml(text, name, entry) {
|
|
4512
4622
|
let eol = "\n";
|
|
4513
4623
|
if (text.includes("\r\n") == true) {
|
|
4514
4624
|
eol = "\r\n";
|
|
@@ -4581,34 +4691,36 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
4581
4691
|
var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
|
|
4582
4692
|
function createCodexTarget() {
|
|
4583
4693
|
function readText() {
|
|
4584
|
-
if (
|
|
4694
|
+
if (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
4585
4695
|
return "";
|
|
4586
4696
|
}
|
|
4587
|
-
return
|
|
4588
|
-
}
|
|
4589
|
-
function writeText(text) {
|
|
4590
|
-
fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4591
|
-
fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
4592
|
-
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4697
|
+
return fs7__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
4593
4698
|
}
|
|
4594
4699
|
return {
|
|
4595
4700
|
id: "codex",
|
|
4596
4701
|
label: "Codex CLI",
|
|
4597
4702
|
location: CODEX_CONFIG_PATH,
|
|
4598
|
-
detected: findExecutable("codex") != null ||
|
|
4703
|
+
detected: findExecutable("codex") != null || fs7__default.default.existsSync(CODEX_CONFIG_PATH) == true,
|
|
4599
4704
|
readEntries() {
|
|
4600
4705
|
const text = readText();
|
|
4601
4706
|
return parseMcpServersToml(text);
|
|
4602
4707
|
},
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4708
|
+
hasEntry(name) {
|
|
4709
|
+
return hasMcpServerToml(readText(), name);
|
|
4710
|
+
},
|
|
4711
|
+
writeEntry(name, entry, options) {
|
|
4712
|
+
updateConfigFile(CODEX_CONFIG_PATH, (original) => {
|
|
4713
|
+
let text = original;
|
|
4714
|
+
if (options?.replace) {
|
|
4715
|
+
text = writeMcpServerToml(text, name, null);
|
|
4716
|
+
}
|
|
4717
|
+
return writeMcpServerToml(text, name, entry);
|
|
4718
|
+
});
|
|
4719
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4607
4720
|
},
|
|
4608
4721
|
removeEntry(name) {
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
return writeText(updated);
|
|
4722
|
+
updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
|
|
4723
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4612
4724
|
}
|
|
4613
4725
|
};
|
|
4614
4726
|
}
|
|
@@ -4747,10 +4859,12 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
4747
4859
|
const profile = profileFromEnv(output, entry.env);
|
|
4748
4860
|
if (profile == null) {
|
|
4749
4861
|
output.write("\n");
|
|
4862
|
+
await prompter.pause();
|
|
4750
4863
|
return;
|
|
4751
4864
|
}
|
|
4752
4865
|
const result = await runConnectionCheck(profile, output);
|
|
4753
4866
|
output.write("\n");
|
|
4867
|
+
await prompter.pause();
|
|
4754
4868
|
if (result.connected == false) {
|
|
4755
4869
|
return;
|
|
4756
4870
|
}
|
|
@@ -4814,13 +4928,14 @@ function buildHostItems(names, entries) {
|
|
|
4814
4928
|
};
|
|
4815
4929
|
});
|
|
4816
4930
|
}
|
|
4817
|
-
async function manageHost(prompter, output, target, entries, name) {
|
|
4931
|
+
async function manageHost(prompter, output, target, targets, entries, name) {
|
|
4818
4932
|
const action = await prompter.select(
|
|
4819
4933
|
name,
|
|
4820
4934
|
[
|
|
4821
4935
|
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4822
4936
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4823
4937
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4938
|
+
...targets.some((candidate) => candidate.id !== target.id) ? [{ value: "copy", key: "o", label: "Copy to another client", hint: "keep this entry and copy its settings" }] : [],
|
|
4824
4939
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4825
4940
|
{ separator: true },
|
|
4826
4941
|
{ value: "back", key: "b", label: "Back" }
|
|
@@ -4839,6 +4954,10 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4839
4954
|
printHost(prompter, name, entries[name]);
|
|
4840
4955
|
return;
|
|
4841
4956
|
}
|
|
4957
|
+
if (action === "copy") {
|
|
4958
|
+
await copyEntryToClient(prompter, target, targets, name, entries[name]);
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4842
4961
|
if (action === "remove") {
|
|
4843
4962
|
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4844
4963
|
if (remove == true) {
|
|
@@ -4940,7 +5059,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
4940
5059
|
continue;
|
|
4941
5060
|
}
|
|
4942
5061
|
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
4943
|
-
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5062
|
+
await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
4944
5063
|
}
|
|
4945
5064
|
} catch (ex) {
|
|
4946
5065
|
if (ex instanceof PromptClosedError) {
|
|
@@ -5393,7 +5512,7 @@ ${file.content}`);
|
|
|
5393
5512
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5394
5513
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5395
5514
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
5396
|
-
if (
|
|
5515
|
+
if (fs7__default.default.existsSync(resolvedLocalPath) == false) {
|
|
5397
5516
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
5398
5517
|
}
|
|
5399
5518
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -5433,7 +5552,7 @@ ${file.content}`);
|
|
|
5433
5552
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5434
5553
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5435
5554
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
5436
|
-
|
|
5555
|
+
fs7__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
|
|
5437
5556
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
5438
5557
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
5439
5558
|
});
|