@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.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import
|
|
3
|
+
import fs7 from 'fs';
|
|
4
4
|
import os5 from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { spawnSync, spawn } from 'child_process';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
7
|
+
import { randomUUID, createHash } from 'crypto';
|
|
8
8
|
import { Duplex } from 'stream';
|
|
9
9
|
import { StringDecoder } from 'string_decoder';
|
|
10
10
|
import ssh2 from 'ssh2';
|
|
11
|
-
import { setInterval, clearInterval } from 'timers';
|
|
11
|
+
import { setInterval as setInterval$1, clearInterval as clearInterval$1 } from 'timers';
|
|
12
|
+
import { isDeepStrictEqual } from 'util';
|
|
13
|
+
import { parse, TomlError } from 'smol-toml';
|
|
12
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
15
|
import { z } from 'zod';
|
|
14
16
|
|
|
@@ -27,7 +29,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
27
29
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
28
30
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
29
31
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
30
|
-
var SERVER_VERSION = "0.1.
|
|
32
|
+
var SERVER_VERSION = "0.1.8";
|
|
31
33
|
|
|
32
34
|
// src/_defs/command-rules.ts
|
|
33
35
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -613,7 +615,7 @@ function findExecutable(name) {
|
|
|
613
615
|
for (const directory of directories) {
|
|
614
616
|
for (const suffix of suffixes) {
|
|
615
617
|
const candidate = path.join(directory, `${name}${suffix}`);
|
|
616
|
-
if (
|
|
618
|
+
if (fs7.existsSync(candidate) == true && fs7.statSync(candidate).isFile() == true) {
|
|
617
619
|
return candidate;
|
|
618
620
|
}
|
|
619
621
|
}
|
|
@@ -754,7 +756,7 @@ function loadHostProfile(env = process.env) {
|
|
|
754
756
|
if (keyPath != null) {
|
|
755
757
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
756
758
|
profile.privateKeyPath = expandedKeyPath;
|
|
757
|
-
if (
|
|
759
|
+
if (fs7.existsSync(expandedKeyPath) == false) {
|
|
758
760
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
759
761
|
private_key_path: expandedKeyPath
|
|
760
762
|
});
|
|
@@ -786,7 +788,7 @@ function loadHostProfile(env = process.env) {
|
|
|
786
788
|
if (jumpKeyPath != null) {
|
|
787
789
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
788
790
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
789
|
-
if (
|
|
791
|
+
if (fs7.existsSync(profile.jump.privateKeyPath) == false) {
|
|
790
792
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
791
793
|
private_key_path: profile.jump.privateKeyPath
|
|
792
794
|
});
|
|
@@ -1272,14 +1274,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1272
1274
|
function toRealPath(inputPath) {
|
|
1273
1275
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1274
1276
|
try {
|
|
1275
|
-
return
|
|
1277
|
+
return fs7.realpathSync.native(inputPath);
|
|
1276
1278
|
} catch (ex) {
|
|
1277
1279
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1278
1280
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1279
1281
|
});
|
|
1280
1282
|
}
|
|
1281
1283
|
try {
|
|
1282
|
-
const parentRealPath =
|
|
1284
|
+
const parentRealPath = fs7.realpathSync.native(path.dirname(inputPath));
|
|
1283
1285
|
return path.join(parentRealPath, path.basename(inputPath));
|
|
1284
1286
|
} catch (ex) {
|
|
1285
1287
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1371,7 +1373,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1371
1373
|
return cached;
|
|
1372
1374
|
}
|
|
1373
1375
|
try {
|
|
1374
|
-
const contents =
|
|
1376
|
+
const contents = fs7.readFileSync(keyPath);
|
|
1375
1377
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1376
1378
|
return contents;
|
|
1377
1379
|
} catch (ex) {
|
|
@@ -1640,7 +1642,7 @@ var SshConnection = class {
|
|
|
1640
1642
|
* @throws {Error} If the connection was closed, authentication fails, the host is
|
|
1641
1643
|
* unreachable, the handshake times out, or the carrier cannot be opened.
|
|
1642
1644
|
*/
|
|
1643
|
-
async connect() {
|
|
1645
|
+
async connect(onProgress) {
|
|
1644
1646
|
if (this.disposed == true) {
|
|
1645
1647
|
throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
|
|
1646
1648
|
}
|
|
@@ -1662,6 +1664,9 @@ var SshConnection = class {
|
|
|
1662
1664
|
this.observedFingerprint = fingerprint;
|
|
1663
1665
|
});
|
|
1664
1666
|
this.connecting = (async () => {
|
|
1667
|
+
if (this.profile.jump != null || this.profile.proxyCommand != null) {
|
|
1668
|
+
onProgress?.("transport");
|
|
1669
|
+
}
|
|
1665
1670
|
const transport = await openTransport(this.profile, (fingerprint) => {
|
|
1666
1671
|
this.observedJumpFingerprint = fingerprint;
|
|
1667
1672
|
});
|
|
@@ -1672,6 +1677,7 @@ var SshConnection = class {
|
|
|
1672
1677
|
if (transport != null) {
|
|
1673
1678
|
connectConfig.sock = transport.sock;
|
|
1674
1679
|
}
|
|
1680
|
+
onProgress?.("host");
|
|
1675
1681
|
await new Promise((resolve, reject) => {
|
|
1676
1682
|
const client = new Client();
|
|
1677
1683
|
this.pendingClient = client;
|
|
@@ -2127,7 +2133,7 @@ var SshSessionManager = class {
|
|
|
2127
2133
|
if (this.sweepTimer != null) {
|
|
2128
2134
|
return;
|
|
2129
2135
|
}
|
|
2130
|
-
this.sweepTimer = setInterval(() => {
|
|
2136
|
+
this.sweepTimer = setInterval$1(() => {
|
|
2131
2137
|
const expiredAt = Date.now() - SESSION_IDLE_TIMEOUT_MS;
|
|
2132
2138
|
for (const session of Array.from(this.sessions.values())) {
|
|
2133
2139
|
if (session.isBusy == true) {
|
|
@@ -2151,7 +2157,7 @@ var SshSessionManager = class {
|
|
|
2151
2157
|
if (this.sweepTimer == null) {
|
|
2152
2158
|
return;
|
|
2153
2159
|
}
|
|
2154
|
-
clearInterval(this.sweepTimer);
|
|
2160
|
+
clearInterval$1(this.sweepTimer);
|
|
2155
2161
|
this.sweepTimer = null;
|
|
2156
2162
|
}
|
|
2157
2163
|
};
|
|
@@ -2322,7 +2328,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2322
2328
|
}
|
|
2323
2329
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2324
2330
|
requirePermittedPath(connection, remotePath, "write");
|
|
2325
|
-
const localStats =
|
|
2331
|
+
const localStats = fs7.statSync(localPath);
|
|
2326
2332
|
if (localStats.isFile() == false) {
|
|
2327
2333
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2328
2334
|
}
|
|
@@ -2359,7 +2365,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2359
2365
|
});
|
|
2360
2366
|
});
|
|
2361
2367
|
});
|
|
2362
|
-
const localStats =
|
|
2368
|
+
const localStats = fs7.statSync(localPath);
|
|
2363
2369
|
const _logger = $logger.child({
|
|
2364
2370
|
context: "downloadFile",
|
|
2365
2371
|
profile: connection.profile.name,
|
|
@@ -2370,283 +2376,6 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2370
2376
|
return localStats.size;
|
|
2371
2377
|
}
|
|
2372
2378
|
|
|
2373
|
-
// src/modules/manage/connection-check.ts
|
|
2374
|
-
function profileFromEnv(output, env) {
|
|
2375
|
-
try {
|
|
2376
|
-
const profile = loadHostProfile(env);
|
|
2377
|
-
if (profile == null) {
|
|
2378
|
-
output.write(" The entry has no host.\n");
|
|
2379
|
-
}
|
|
2380
|
-
return profile;
|
|
2381
|
-
} catch (ex) {
|
|
2382
|
-
let detail = String(ex);
|
|
2383
|
-
if (ex instanceof Error) {
|
|
2384
|
-
detail = ex.message;
|
|
2385
|
-
}
|
|
2386
|
-
output.write(` The server would refuse to start with this entry: ${detail}
|
|
2387
|
-
`);
|
|
2388
|
-
return null;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
function describeAuth(profile) {
|
|
2392
|
-
if (profile.privateKeyPath != null) {
|
|
2393
|
-
return `key ${profile.privateKeyPath}`;
|
|
2394
|
-
}
|
|
2395
|
-
if (profile.password != null) {
|
|
2396
|
-
return "password";
|
|
2397
|
-
}
|
|
2398
|
-
return "ssh agent";
|
|
2399
|
-
}
|
|
2400
|
-
async function runConnectionCheck(profile, output) {
|
|
2401
|
-
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
2402
|
-
const auth = describeAuth(profile);
|
|
2403
|
-
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
2404
|
-
`);
|
|
2405
|
-
if (profile.proxyCommand != null) {
|
|
2406
|
-
output.write(` via proxy command: ${profile.proxyCommand}
|
|
2407
|
-
`);
|
|
2408
|
-
}
|
|
2409
|
-
if (profile.jump != null) {
|
|
2410
|
-
output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
|
|
2411
|
-
`);
|
|
2412
|
-
}
|
|
2413
|
-
output.write("\n");
|
|
2414
|
-
const connection = new SshConnection(profile);
|
|
2415
|
-
const startedAt = Date.now();
|
|
2416
|
-
let connected = false;
|
|
2417
|
-
let detail = "";
|
|
2418
|
-
try {
|
|
2419
|
-
await connection.connect();
|
|
2420
|
-
connected = true;
|
|
2421
|
-
} catch (ex) {
|
|
2422
|
-
detail = String(ex);
|
|
2423
|
-
if (ex instanceof Error) {
|
|
2424
|
-
detail = ex.message;
|
|
2425
|
-
}
|
|
2426
|
-
} finally {
|
|
2427
|
-
connection.disconnect();
|
|
2428
|
-
}
|
|
2429
|
-
const durationMs = Date.now() - startedAt;
|
|
2430
|
-
if (connected == true) {
|
|
2431
|
-
output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
|
|
2432
|
-
`);
|
|
2433
|
-
} else {
|
|
2434
|
-
output.write(` FAILED ${endpoint} (${auth})
|
|
2435
|
-
`);
|
|
2436
|
-
output.write(` ${detail}
|
|
2437
|
-
`);
|
|
2438
|
-
}
|
|
2439
|
-
if (connection.hostKeyFingerprint != null) {
|
|
2440
|
-
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
|
|
2441
|
-
if (profile.hostKeyFingerprint != null) {
|
|
2442
|
-
pinNote = "pinned";
|
|
2443
|
-
}
|
|
2444
|
-
output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
|
|
2445
|
-
`);
|
|
2446
|
-
}
|
|
2447
|
-
if (connection.jumpHostKeyFingerprint != null) {
|
|
2448
|
-
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
|
|
2449
|
-
if (profile.jump?.hostKeyFingerprint != null) {
|
|
2450
|
-
pinNote = "pinned";
|
|
2451
|
-
}
|
|
2452
|
-
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
2453
|
-
`);
|
|
2454
|
-
}
|
|
2455
|
-
return {
|
|
2456
|
-
connected,
|
|
2457
|
-
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
2458
|
-
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
2459
|
-
};
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
// src/modules/manage/entry.ts
|
|
2463
|
-
var MANAGED_ENV_NAMES = [
|
|
2464
|
-
SINGLE_HOST_ENV.HOST,
|
|
2465
|
-
SINGLE_HOST_ENV.PORT,
|
|
2466
|
-
SINGLE_HOST_ENV.USER,
|
|
2467
|
-
SINGLE_HOST_ENV.KEY,
|
|
2468
|
-
SINGLE_HOST_ENV.PASSPHRASE,
|
|
2469
|
-
SINGLE_HOST_ENV.PASSWORD,
|
|
2470
|
-
SINGLE_HOST_ENV.DESCRIPTION,
|
|
2471
|
-
SINGLE_HOST_ENV.JUMP_HOST,
|
|
2472
|
-
SINGLE_HOST_ENV.JUMP_PORT,
|
|
2473
|
-
SINGLE_HOST_ENV.JUMP_USER,
|
|
2474
|
-
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
2475
|
-
SINGLE_HOST_ENV.READONLY,
|
|
2476
|
-
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
2477
|
-
SINGLE_HOST_ENV.HOST_KEY,
|
|
2478
|
-
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
2479
|
-
];
|
|
2480
|
-
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
2481
|
-
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
2482
|
-
function isOurEntry(entry) {
|
|
2483
|
-
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
2484
|
-
return true;
|
|
2485
|
-
}
|
|
2486
|
-
const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
2487
|
-
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
2488
|
-
}
|
|
2489
|
-
function launchCommand() {
|
|
2490
|
-
if (process.platform === "win32") {
|
|
2491
|
-
return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
|
|
2492
|
-
}
|
|
2493
|
-
return { command: "npx", args: ["-y", PACKAGE_NAME] };
|
|
2494
|
-
}
|
|
2495
|
-
function answersToEntry(answers) {
|
|
2496
|
-
const env = {};
|
|
2497
|
-
env[SINGLE_HOST_ENV.HOST] = answers.host;
|
|
2498
|
-
if (answers.port !== DEFAULT_SSH_PORT) {
|
|
2499
|
-
env[SINGLE_HOST_ENV.PORT] = String(answers.port);
|
|
2500
|
-
}
|
|
2501
|
-
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
2502
|
-
if (answers.auth === "key" && answers.keyPath != null) {
|
|
2503
|
-
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
2504
|
-
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
2505
|
-
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
if (answers.auth === "password" && answers.password != null) {
|
|
2509
|
-
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
2510
|
-
}
|
|
2511
|
-
if (answers.hostKeyFingerprint != null) {
|
|
2512
|
-
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
2513
|
-
}
|
|
2514
|
-
if (answers.description != null && answers.description !== "") {
|
|
2515
|
-
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
2516
|
-
}
|
|
2517
|
-
if (answers.jumpHost != null) {
|
|
2518
|
-
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
2519
|
-
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
2520
|
-
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
2521
|
-
}
|
|
2522
|
-
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
2523
|
-
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
2524
|
-
}
|
|
2525
|
-
if (answers.jumpHostKeyFingerprint != null) {
|
|
2526
|
-
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
2530
|
-
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
2531
|
-
}
|
|
2532
|
-
if (answers.readonly == true) {
|
|
2533
|
-
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
2534
|
-
}
|
|
2535
|
-
if (answers.allowedPaths.length > 0) {
|
|
2536
|
-
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
2537
|
-
}
|
|
2538
|
-
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
2539
|
-
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
2540
|
-
continue;
|
|
2541
|
-
}
|
|
2542
|
-
if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
|
|
2543
|
-
continue;
|
|
2544
|
-
}
|
|
2545
|
-
env[name] = value;
|
|
2546
|
-
}
|
|
2547
|
-
const launch = launchCommand();
|
|
2548
|
-
return {
|
|
2549
|
-
command: launch.command,
|
|
2550
|
-
args: launch.args,
|
|
2551
|
-
env
|
|
2552
|
-
};
|
|
2553
|
-
}
|
|
2554
|
-
function readPort(value) {
|
|
2555
|
-
if (value == null) {
|
|
2556
|
-
return DEFAULT_SSH_PORT;
|
|
2557
|
-
}
|
|
2558
|
-
const parsed = Number(value);
|
|
2559
|
-
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
2560
|
-
return DEFAULT_SSH_PORT;
|
|
2561
|
-
}
|
|
2562
|
-
return parsed;
|
|
2563
|
-
}
|
|
2564
|
-
function isTruthy(value) {
|
|
2565
|
-
if (value == null) {
|
|
2566
|
-
return false;
|
|
2567
|
-
}
|
|
2568
|
-
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
2569
|
-
}
|
|
2570
|
-
function entryToAnswers(name, entry) {
|
|
2571
|
-
const env = entry.env;
|
|
2572
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
2573
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
2574
|
-
let auth = "agent";
|
|
2575
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2576
|
-
auth = "key";
|
|
2577
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2578
|
-
auth = "password";
|
|
2579
|
-
}
|
|
2580
|
-
let allowedPaths = [];
|
|
2581
|
-
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
2582
|
-
if (allowedPathsValue != null) {
|
|
2583
|
-
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
2584
|
-
}
|
|
2585
|
-
const extraEnv = {};
|
|
2586
|
-
for (const [variable, value] of Object.entries(env)) {
|
|
2587
|
-
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
2588
|
-
extraEnv[variable] = value;
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
const answers = {
|
|
2592
|
-
name,
|
|
2593
|
-
host,
|
|
2594
|
-
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
2595
|
-
username,
|
|
2596
|
-
auth,
|
|
2597
|
-
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
2598
|
-
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
2599
|
-
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
2600
|
-
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
2601
|
-
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
2602
|
-
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
2603
|
-
allowedPaths,
|
|
2604
|
-
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
2605
|
-
extraEnv
|
|
2606
|
-
};
|
|
2607
|
-
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
2608
|
-
if (jumpHost != null) {
|
|
2609
|
-
answers.jumpHost = jumpHost;
|
|
2610
|
-
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
2611
|
-
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
2612
|
-
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
2613
|
-
}
|
|
2614
|
-
return answers;
|
|
2615
|
-
}
|
|
2616
|
-
function summarizeEntry(entry) {
|
|
2617
|
-
const env = entry.env;
|
|
2618
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
2619
|
-
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
2620
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
2621
|
-
let auth = "agent";
|
|
2622
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2623
|
-
auth = "key";
|
|
2624
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2625
|
-
auth = "password";
|
|
2626
|
-
}
|
|
2627
|
-
const notes = [];
|
|
2628
|
-
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
2629
|
-
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
2630
|
-
}
|
|
2631
|
-
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
2632
|
-
notes.push("proxy");
|
|
2633
|
-
}
|
|
2634
|
-
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
2635
|
-
notes.push("readonly");
|
|
2636
|
-
}
|
|
2637
|
-
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
2638
|
-
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
2639
|
-
}
|
|
2640
|
-
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
2641
|
-
notes.push("pinned");
|
|
2642
|
-
}
|
|
2643
|
-
return {
|
|
2644
|
-
endpoint: `${username}@${host}:${port}`,
|
|
2645
|
-
auth,
|
|
2646
|
-
notes: notes.join(", ")
|
|
2647
|
-
};
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
2379
|
// src/modules/manage/prompter.ts
|
|
2651
2380
|
var PromptClosedError = class extends Error {
|
|
2652
2381
|
constructor() {
|
|
@@ -2823,6 +2552,17 @@ var Prompter = class {
|
|
|
2823
2552
|
get paint() {
|
|
2824
2553
|
return this.style;
|
|
2825
2554
|
}
|
|
2555
|
+
/** Keeps a report visible until Enter on a TTY; Esc goes back. Pipes need no extra answer. */
|
|
2556
|
+
async pause() {
|
|
2557
|
+
if (this.interactive == false) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (this.inputEnded == true) {
|
|
2561
|
+
throw new PromptClosedError();
|
|
2562
|
+
}
|
|
2563
|
+
const prompt = this.fitLine([{ text: "Press Enter to continue", paint: this.style.dim }], this.columns() - 1);
|
|
2564
|
+
await this.readText(prompt, false);
|
|
2565
|
+
}
|
|
2826
2566
|
/**
|
|
2827
2567
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2828
2568
|
*
|
|
@@ -3268,9 +3008,435 @@ var Prompter = class {
|
|
|
3268
3008
|
draw(buildLines());
|
|
3269
3009
|
});
|
|
3270
3010
|
}
|
|
3271
|
-
};
|
|
3272
|
-
|
|
3273
|
-
// src/modules/manage/
|
|
3011
|
+
};
|
|
3012
|
+
|
|
3013
|
+
// src/modules/manage/check-progress.ts
|
|
3014
|
+
var ESC2 = "\x1B";
|
|
3015
|
+
var CheckProgress = class {
|
|
3016
|
+
constructor(output) {
|
|
3017
|
+
this.output = output;
|
|
3018
|
+
this.unicode = supportsUnicode();
|
|
3019
|
+
this.frames = this.unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
|
|
3020
|
+
this.timer = null;
|
|
3021
|
+
this.label = null;
|
|
3022
|
+
this.startedAt = 0;
|
|
3023
|
+
this.frame = 0;
|
|
3024
|
+
this.enabled = output.isTTY === true && process.env.TERM !== "dumb";
|
|
3025
|
+
this.color = process.env.NO_COLOR == null;
|
|
3026
|
+
}
|
|
3027
|
+
/** Advancing means the preceding phase actually completed successfully. */
|
|
3028
|
+
start(label) {
|
|
3029
|
+
if (this.enabled == false) {
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
this.finish(true);
|
|
3033
|
+
this.label = label;
|
|
3034
|
+
this.startedAt = Date.now();
|
|
3035
|
+
this.frame = 0;
|
|
3036
|
+
this.draw(this.frames[0], 36);
|
|
3037
|
+
this.timer = setInterval(() => {
|
|
3038
|
+
this.frame = (this.frame + 1) % this.frames.length;
|
|
3039
|
+
this.draw(this.frames[this.frame], 36);
|
|
3040
|
+
}, 80);
|
|
3041
|
+
this.timer.unref();
|
|
3042
|
+
}
|
|
3043
|
+
/** Leaves the completed phase and its duration visible. */
|
|
3044
|
+
finish(ok) {
|
|
3045
|
+
if (this.label == null) {
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
this.clearTimer();
|
|
3049
|
+
const symbol = ok ? this.unicode ? "\u2714" : "OK" : this.unicode ? "\u2716" : "X";
|
|
3050
|
+
this.draw(symbol, ok ? 32 : 31);
|
|
3051
|
+
this.output.write("\n");
|
|
3052
|
+
this.label = null;
|
|
3053
|
+
}
|
|
3054
|
+
/** Clears an unfinished phase without recording a result. */
|
|
3055
|
+
stop() {
|
|
3056
|
+
this.clearTimer();
|
|
3057
|
+
if (this.label != null) {
|
|
3058
|
+
this.output.write(`\r${ESC2}[2K`);
|
|
3059
|
+
this.label = null;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
clearTimer() {
|
|
3063
|
+
if (this.timer != null) {
|
|
3064
|
+
clearInterval(this.timer);
|
|
3065
|
+
this.timer = null;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
draw(symbol, color) {
|
|
3069
|
+
const columns = this.output.columns;
|
|
3070
|
+
const width = Math.max(0, (columns != null && columns > 0 ? columns : 80) - 1);
|
|
3071
|
+
const elapsed = ` ${((Date.now() - this.startedAt) / 1e3).toFixed(1)}s`;
|
|
3072
|
+
const prefix = ` ${symbol} `;
|
|
3073
|
+
const labelWidth = Math.min(48, Math.max(0, width - displayWidth(prefix) - displayWidth(elapsed)));
|
|
3074
|
+
const label = this.fit(this.label ?? "", labelWidth);
|
|
3075
|
+
const line = this.fit(`${prefix}${label}${" ".repeat(labelWidth - displayWidth(label))}${elapsed}`, width);
|
|
3076
|
+
const painted = this.color ? `${ESC2}[${color}m${line}${ESC2}[39m` : line;
|
|
3077
|
+
this.output.write(`\r${ESC2}[2K${painted}`);
|
|
3078
|
+
}
|
|
3079
|
+
fit(text, width) {
|
|
3080
|
+
if (displayWidth(text) <= width) {
|
|
3081
|
+
return text;
|
|
3082
|
+
}
|
|
3083
|
+
if (width < 1) {
|
|
3084
|
+
return "";
|
|
3085
|
+
}
|
|
3086
|
+
let result = "";
|
|
3087
|
+
let used = 0;
|
|
3088
|
+
for (const character of text) {
|
|
3089
|
+
const size = displayWidth(character);
|
|
3090
|
+
if (used + size > width - 1) {
|
|
3091
|
+
break;
|
|
3092
|
+
}
|
|
3093
|
+
result += character;
|
|
3094
|
+
used += size;
|
|
3095
|
+
}
|
|
3096
|
+
return `${result}${this.unicode ? "\u2026" : "~"}`;
|
|
3097
|
+
}
|
|
3098
|
+
};
|
|
3099
|
+
|
|
3100
|
+
// src/modules/manage/connection-check.ts
|
|
3101
|
+
function profileFromEnv(output, env) {
|
|
3102
|
+
try {
|
|
3103
|
+
const profile = loadHostProfile(env);
|
|
3104
|
+
if (profile == null) {
|
|
3105
|
+
output.write(" The entry has no host.\n");
|
|
3106
|
+
}
|
|
3107
|
+
return profile;
|
|
3108
|
+
} catch (ex) {
|
|
3109
|
+
let detail = String(ex);
|
|
3110
|
+
if (ex instanceof Error) {
|
|
3111
|
+
detail = ex.message;
|
|
3112
|
+
}
|
|
3113
|
+
output.write(` The server would refuse to start with this entry: ${detail}
|
|
3114
|
+
`);
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
function describeAuth(profile) {
|
|
3119
|
+
if (profile.privateKeyPath != null) {
|
|
3120
|
+
return `key ${profile.privateKeyPath}`;
|
|
3121
|
+
}
|
|
3122
|
+
if (profile.password != null) {
|
|
3123
|
+
return "password";
|
|
3124
|
+
}
|
|
3125
|
+
return "ssh agent";
|
|
3126
|
+
}
|
|
3127
|
+
async function runConnectionCheck(profile, output) {
|
|
3128
|
+
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
3129
|
+
const auth = describeAuth(profile);
|
|
3130
|
+
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
3131
|
+
`);
|
|
3132
|
+
if (profile.proxyCommand != null) {
|
|
3133
|
+
output.write(` via proxy command: ${profile.proxyCommand}
|
|
3134
|
+
`);
|
|
3135
|
+
}
|
|
3136
|
+
if (profile.jump != null) {
|
|
3137
|
+
output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
|
|
3138
|
+
`);
|
|
3139
|
+
}
|
|
3140
|
+
output.write("\n");
|
|
3141
|
+
const connection = new SshConnection(profile);
|
|
3142
|
+
const progress = new CheckProgress(output);
|
|
3143
|
+
const startedAt = Date.now();
|
|
3144
|
+
let connected = false;
|
|
3145
|
+
let detail = "";
|
|
3146
|
+
try {
|
|
3147
|
+
progress.start("Preparing SSH connection");
|
|
3148
|
+
await connection.connect((stage) => {
|
|
3149
|
+
if (stage === "transport") {
|
|
3150
|
+
progress.start(profile.jump != null ? "Jump host connection and forwarding" : "Starting proxy connection");
|
|
3151
|
+
} else {
|
|
3152
|
+
progress.start("SSH handshake and authentication");
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
connected = true;
|
|
3156
|
+
} catch (ex) {
|
|
3157
|
+
detail = String(ex);
|
|
3158
|
+
if (ex instanceof Error) {
|
|
3159
|
+
detail = ex.message;
|
|
3160
|
+
}
|
|
3161
|
+
} finally {
|
|
3162
|
+
progress.finish(connected);
|
|
3163
|
+
connection.disconnect();
|
|
3164
|
+
}
|
|
3165
|
+
const durationMs = Date.now() - startedAt;
|
|
3166
|
+
if (connected == true) {
|
|
3167
|
+
output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
|
|
3168
|
+
`);
|
|
3169
|
+
} else {
|
|
3170
|
+
output.write(` FAILED ${endpoint} (${auth})
|
|
3171
|
+
`);
|
|
3172
|
+
output.write(` ${detail}
|
|
3173
|
+
`);
|
|
3174
|
+
}
|
|
3175
|
+
if (connection.hostKeyFingerprint != null) {
|
|
3176
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
|
|
3177
|
+
if (profile.hostKeyFingerprint != null) {
|
|
3178
|
+
pinNote = "pinned";
|
|
3179
|
+
}
|
|
3180
|
+
output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
|
|
3181
|
+
`);
|
|
3182
|
+
}
|
|
3183
|
+
if (connection.jumpHostKeyFingerprint != null) {
|
|
3184
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
|
|
3185
|
+
if (profile.jump?.hostKeyFingerprint != null) {
|
|
3186
|
+
pinNote = "pinned";
|
|
3187
|
+
}
|
|
3188
|
+
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
3189
|
+
`);
|
|
3190
|
+
}
|
|
3191
|
+
return {
|
|
3192
|
+
connected,
|
|
3193
|
+
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
3194
|
+
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// src/modules/manage/copy-entry.ts
|
|
3199
|
+
async function copyEntryToClient(prompter, source, targets, name, entry) {
|
|
3200
|
+
const destinations = targets.filter((target) => target.id !== source.id);
|
|
3201
|
+
if (destinations.length === 0) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
for (; ; ) {
|
|
3205
|
+
const items = destinations.map((target) => ({
|
|
3206
|
+
value: target.id,
|
|
3207
|
+
key: target.id,
|
|
3208
|
+
label: target.label,
|
|
3209
|
+
hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
|
|
3210
|
+
}));
|
|
3211
|
+
items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
|
|
3212
|
+
const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
|
|
3213
|
+
const destination = destinations.find((target) => target.id === picked);
|
|
3214
|
+
if (destination == null) {
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
|
|
3218
|
+
if (exists) {
|
|
3219
|
+
try {
|
|
3220
|
+
const overwrite = await prompter.askYesNo(
|
|
3221
|
+
`${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
|
|
3222
|
+
false
|
|
3223
|
+
);
|
|
3224
|
+
if (overwrite == false) {
|
|
3225
|
+
prompter.write(" Not copied; the destination entry was kept.\n\n");
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
} catch (ex) {
|
|
3229
|
+
if (ex instanceof PromptBackError == false) {
|
|
3230
|
+
throw ex;
|
|
3231
|
+
}
|
|
3232
|
+
continue;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
const result = destination.writeEntry(name, {
|
|
3236
|
+
command: entry.command,
|
|
3237
|
+
args: [...entry.args],
|
|
3238
|
+
env: { ...entry.env }
|
|
3239
|
+
}, { replace: true });
|
|
3240
|
+
if (result.status === "written") {
|
|
3241
|
+
prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
|
|
3242
|
+
`);
|
|
3243
|
+
prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3244
|
+
} else {
|
|
3245
|
+
prompter.write(` Not copied: ${result.detail}
|
|
3246
|
+
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// src/modules/manage/entry.ts
|
|
3254
|
+
var MANAGED_ENV_NAMES = [
|
|
3255
|
+
SINGLE_HOST_ENV.HOST,
|
|
3256
|
+
SINGLE_HOST_ENV.PORT,
|
|
3257
|
+
SINGLE_HOST_ENV.USER,
|
|
3258
|
+
SINGLE_HOST_ENV.KEY,
|
|
3259
|
+
SINGLE_HOST_ENV.PASSPHRASE,
|
|
3260
|
+
SINGLE_HOST_ENV.PASSWORD,
|
|
3261
|
+
SINGLE_HOST_ENV.DESCRIPTION,
|
|
3262
|
+
SINGLE_HOST_ENV.JUMP_HOST,
|
|
3263
|
+
SINGLE_HOST_ENV.JUMP_PORT,
|
|
3264
|
+
SINGLE_HOST_ENV.JUMP_USER,
|
|
3265
|
+
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
3266
|
+
SINGLE_HOST_ENV.READONLY,
|
|
3267
|
+
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
3268
|
+
SINGLE_HOST_ENV.HOST_KEY,
|
|
3269
|
+
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
3270
|
+
];
|
|
3271
|
+
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
3272
|
+
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
3273
|
+
function isOurEntry(entry) {
|
|
3274
|
+
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
3275
|
+
return true;
|
|
3276
|
+
}
|
|
3277
|
+
const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
3278
|
+
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
3279
|
+
}
|
|
3280
|
+
function launchCommand() {
|
|
3281
|
+
if (process.platform === "win32") {
|
|
3282
|
+
return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
|
|
3283
|
+
}
|
|
3284
|
+
return { command: "npx", args: ["-y", PACKAGE_NAME] };
|
|
3285
|
+
}
|
|
3286
|
+
function answersToEntry(answers) {
|
|
3287
|
+
const env = {};
|
|
3288
|
+
env[SINGLE_HOST_ENV.HOST] = answers.host;
|
|
3289
|
+
if (answers.port !== DEFAULT_SSH_PORT) {
|
|
3290
|
+
env[SINGLE_HOST_ENV.PORT] = String(answers.port);
|
|
3291
|
+
}
|
|
3292
|
+
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
3293
|
+
if (answers.auth === "key" && answers.keyPath != null) {
|
|
3294
|
+
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
3295
|
+
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
3296
|
+
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (answers.auth === "password" && answers.password != null) {
|
|
3300
|
+
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
3301
|
+
}
|
|
3302
|
+
if (answers.hostKeyFingerprint != null) {
|
|
3303
|
+
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
3304
|
+
}
|
|
3305
|
+
if (answers.description != null && answers.description !== "") {
|
|
3306
|
+
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
3307
|
+
}
|
|
3308
|
+
if (answers.jumpHost != null) {
|
|
3309
|
+
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
3310
|
+
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
3311
|
+
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
3312
|
+
}
|
|
3313
|
+
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
3314
|
+
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
3315
|
+
}
|
|
3316
|
+
if (answers.jumpHostKeyFingerprint != null) {
|
|
3317
|
+
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
3321
|
+
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
3322
|
+
}
|
|
3323
|
+
if (answers.readonly == true) {
|
|
3324
|
+
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
3325
|
+
}
|
|
3326
|
+
if (answers.allowedPaths.length > 0) {
|
|
3327
|
+
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
3328
|
+
}
|
|
3329
|
+
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
3330
|
+
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
env[name] = value;
|
|
3337
|
+
}
|
|
3338
|
+
const launch = launchCommand();
|
|
3339
|
+
return {
|
|
3340
|
+
command: launch.command,
|
|
3341
|
+
args: launch.args,
|
|
3342
|
+
env
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function readPort(value) {
|
|
3346
|
+
if (value == null) {
|
|
3347
|
+
return DEFAULT_SSH_PORT;
|
|
3348
|
+
}
|
|
3349
|
+
const parsed = Number(value);
|
|
3350
|
+
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
3351
|
+
return DEFAULT_SSH_PORT;
|
|
3352
|
+
}
|
|
3353
|
+
return parsed;
|
|
3354
|
+
}
|
|
3355
|
+
function isTruthy(value) {
|
|
3356
|
+
if (value == null) {
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3359
|
+
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
3360
|
+
}
|
|
3361
|
+
function entryToAnswers(name, entry) {
|
|
3362
|
+
const env = entry.env;
|
|
3363
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
3364
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
3365
|
+
let auth = "agent";
|
|
3366
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3367
|
+
auth = "key";
|
|
3368
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3369
|
+
auth = "password";
|
|
3370
|
+
}
|
|
3371
|
+
let allowedPaths = [];
|
|
3372
|
+
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
3373
|
+
if (allowedPathsValue != null) {
|
|
3374
|
+
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3375
|
+
}
|
|
3376
|
+
const extraEnv = {};
|
|
3377
|
+
for (const [variable, value] of Object.entries(env)) {
|
|
3378
|
+
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
3379
|
+
extraEnv[variable] = value;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
const answers = {
|
|
3383
|
+
name,
|
|
3384
|
+
host,
|
|
3385
|
+
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
3386
|
+
username,
|
|
3387
|
+
auth,
|
|
3388
|
+
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
3389
|
+
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
3390
|
+
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
3391
|
+
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
3392
|
+
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
3393
|
+
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
3394
|
+
allowedPaths,
|
|
3395
|
+
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
3396
|
+
extraEnv
|
|
3397
|
+
};
|
|
3398
|
+
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
3399
|
+
if (jumpHost != null) {
|
|
3400
|
+
answers.jumpHost = jumpHost;
|
|
3401
|
+
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
3402
|
+
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
3403
|
+
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
3404
|
+
}
|
|
3405
|
+
return answers;
|
|
3406
|
+
}
|
|
3407
|
+
function summarizeEntry(entry) {
|
|
3408
|
+
const env = entry.env;
|
|
3409
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
3410
|
+
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
3411
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
3412
|
+
let auth = "agent";
|
|
3413
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3414
|
+
auth = "key";
|
|
3415
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3416
|
+
auth = "password";
|
|
3417
|
+
}
|
|
3418
|
+
const notes = [];
|
|
3419
|
+
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
3420
|
+
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
3421
|
+
}
|
|
3422
|
+
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
3423
|
+
notes.push("proxy");
|
|
3424
|
+
}
|
|
3425
|
+
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
3426
|
+
notes.push("readonly");
|
|
3427
|
+
}
|
|
3428
|
+
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
3429
|
+
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
3430
|
+
}
|
|
3431
|
+
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
3432
|
+
notes.push("pinned");
|
|
3433
|
+
}
|
|
3434
|
+
return {
|
|
3435
|
+
endpoint: `${username}@${host}:${port}`,
|
|
3436
|
+
auth,
|
|
3437
|
+
notes: notes.join(", ")
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3274
3440
|
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3275
3441
|
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3276
3442
|
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
@@ -3314,7 +3480,7 @@ function findPrivateKeys() {
|
|
|
3314
3480
|
const sshDirectory = path.join(os5.homedir(), ".ssh");
|
|
3315
3481
|
let names = [];
|
|
3316
3482
|
try {
|
|
3317
|
-
names =
|
|
3483
|
+
names = fs7.readdirSync(sshDirectory);
|
|
3318
3484
|
} catch {
|
|
3319
3485
|
return [];
|
|
3320
3486
|
}
|
|
@@ -3326,14 +3492,14 @@ function findPrivateKeys() {
|
|
|
3326
3492
|
const fullPath = path.join(sshDirectory, name);
|
|
3327
3493
|
let head = "";
|
|
3328
3494
|
try {
|
|
3329
|
-
const stat =
|
|
3495
|
+
const stat = fs7.statSync(fullPath);
|
|
3330
3496
|
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3331
3497
|
continue;
|
|
3332
3498
|
}
|
|
3333
|
-
const descriptor =
|
|
3499
|
+
const descriptor = fs7.openSync(fullPath, "r");
|
|
3334
3500
|
const buffer = Buffer.alloc(64);
|
|
3335
|
-
const bytesRead =
|
|
3336
|
-
|
|
3501
|
+
const bytesRead = fs7.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3502
|
+
fs7.closeSync(descriptor);
|
|
3337
3503
|
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3338
3504
|
} catch {
|
|
3339
3505
|
continue;
|
|
@@ -3345,7 +3511,7 @@ function findPrivateKeys() {
|
|
|
3345
3511
|
}
|
|
3346
3512
|
let hint = "";
|
|
3347
3513
|
try {
|
|
3348
|
-
const publicKey =
|
|
3514
|
+
const publicKey = fs7.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3349
3515
|
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3350
3516
|
if (publicKey.length > 2) {
|
|
3351
3517
|
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
@@ -3414,7 +3580,7 @@ async function askKeyPath(prompter, current) {
|
|
|
3414
3580
|
}
|
|
3415
3581
|
for (; ; ) {
|
|
3416
3582
|
const typed = await prompter.askRequired("Private key path");
|
|
3417
|
-
if (
|
|
3583
|
+
if (fs7.existsSync(expandHomePath(typed)) == true) {
|
|
3418
3584
|
return typed;
|
|
3419
3585
|
}
|
|
3420
3586
|
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
@@ -3626,10 +3792,12 @@ async function testConnection(prompter, output, state) {
|
|
|
3626
3792
|
if (profile == null) {
|
|
3627
3793
|
output.write("\n");
|
|
3628
3794
|
state.lastTest = "failed";
|
|
3795
|
+
await prompter.pause();
|
|
3629
3796
|
return false;
|
|
3630
3797
|
}
|
|
3631
3798
|
const result = await runConnectionCheck(profile, output);
|
|
3632
3799
|
output.write("\n");
|
|
3800
|
+
await prompter.pause();
|
|
3633
3801
|
if (result.connected == true) {
|
|
3634
3802
|
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3635
3803
|
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
@@ -3946,6 +4114,65 @@ async function runHostForm(prompter, output, options) {
|
|
|
3946
4114
|
prompter.transient = previousTransient;
|
|
3947
4115
|
}
|
|
3948
4116
|
}
|
|
4117
|
+
function readExisting(filePath) {
|
|
4118
|
+
try {
|
|
4119
|
+
return fs7.readFileSync(filePath, "utf8");
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
if (error.code === "ENOENT") {
|
|
4122
|
+
return null;
|
|
4123
|
+
}
|
|
4124
|
+
throw error;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
function updateConfigFile(filePath, update) {
|
|
4128
|
+
let destination = filePath;
|
|
4129
|
+
let symbolicLink = false;
|
|
4130
|
+
try {
|
|
4131
|
+
symbolicLink = fs7.lstatSync(filePath).isSymbolicLink();
|
|
4132
|
+
} catch (error) {
|
|
4133
|
+
if (error.code !== "ENOENT") {
|
|
4134
|
+
throw error;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
if (symbolicLink) {
|
|
4138
|
+
destination = fs7.realpathSync(filePath);
|
|
4139
|
+
}
|
|
4140
|
+
const before = readExisting(destination);
|
|
4141
|
+
const updated = update(before ?? "");
|
|
4142
|
+
if (before != null) {
|
|
4143
|
+
fs7.accessSync(destination, fs7.constants.W_OK);
|
|
4144
|
+
}
|
|
4145
|
+
fs7.mkdirSync(path.dirname(destination), { recursive: true });
|
|
4146
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
4147
|
+
const mode = before == null ? 384 : fs7.statSync(destination).mode & 511;
|
|
4148
|
+
let descriptor;
|
|
4149
|
+
let ownsTemporary = false;
|
|
4150
|
+
try {
|
|
4151
|
+
descriptor = fs7.openSync(temporary, "wx", mode);
|
|
4152
|
+
ownsTemporary = true;
|
|
4153
|
+
fs7.writeFileSync(descriptor, updated, "utf8");
|
|
4154
|
+
fs7.fsyncSync(descriptor);
|
|
4155
|
+
fs7.closeSync(descriptor);
|
|
4156
|
+
descriptor = void 0;
|
|
4157
|
+
if (readExisting(destination) !== before) {
|
|
4158
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
4159
|
+
}
|
|
4160
|
+
fs7.renameSync(temporary, destination);
|
|
4161
|
+
ownsTemporary = false;
|
|
4162
|
+
} finally {
|
|
4163
|
+
try {
|
|
4164
|
+
if (descriptor != null) {
|
|
4165
|
+
fs7.closeSync(descriptor);
|
|
4166
|
+
}
|
|
4167
|
+
} finally {
|
|
4168
|
+
if (ownsTemporary) {
|
|
4169
|
+
fs7.unlinkSync(temporary);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// src/modules/manage/targets/json-file.ts
|
|
3949
4176
|
function toMcpServerEntry(value) {
|
|
3950
4177
|
if (typeof value !== "object" || value == null) {
|
|
3951
4178
|
return null;
|
|
@@ -3958,7 +4185,7 @@ function toMcpServerEntry(value) {
|
|
|
3958
4185
|
if (Array.isArray(record.args) == true) {
|
|
3959
4186
|
args = record.args.filter((item) => typeof item === "string");
|
|
3960
4187
|
}
|
|
3961
|
-
const env =
|
|
4188
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
3962
4189
|
if (typeof record.env === "object" && record.env != null) {
|
|
3963
4190
|
for (const [key, item] of Object.entries(record.env)) {
|
|
3964
4191
|
if (typeof item === "string") {
|
|
@@ -3968,16 +4195,22 @@ function toMcpServerEntry(value) {
|
|
|
3968
4195
|
}
|
|
3969
4196
|
return { command: record.command, args, env };
|
|
3970
4197
|
}
|
|
3971
|
-
function
|
|
3972
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)
|
|
4198
|
+
function readRawMcpServersJson(document) {
|
|
4199
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) {
|
|
3973
4200
|
throw new Error("the config file is not a JSON object");
|
|
3974
4201
|
}
|
|
3975
|
-
const entries = {};
|
|
3976
4202
|
const servers = document.mcpServers;
|
|
3977
|
-
if (
|
|
3978
|
-
return
|
|
4203
|
+
if (servers === void 0) {
|
|
4204
|
+
return {};
|
|
4205
|
+
}
|
|
4206
|
+
if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
|
|
4207
|
+
throw new Error("mcpServers is not a JSON object");
|
|
3979
4208
|
}
|
|
3980
|
-
|
|
4209
|
+
return servers;
|
|
4210
|
+
}
|
|
4211
|
+
function readMcpServersJson(document) {
|
|
4212
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
4213
|
+
for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
|
|
3981
4214
|
const entry = toMcpServerEntry(value);
|
|
3982
4215
|
if (entry != null) {
|
|
3983
4216
|
entries[name] = entry;
|
|
@@ -3985,11 +4218,11 @@ function readMcpServersJson(document) {
|
|
|
3985
4218
|
}
|
|
3986
4219
|
return entries;
|
|
3987
4220
|
}
|
|
3988
|
-
function readDocument(filePath) {
|
|
3989
|
-
if (
|
|
4221
|
+
function readDocument(filePath, contents) {
|
|
4222
|
+
if (contents == null && fs7.existsSync(filePath) == false) {
|
|
3990
4223
|
return {};
|
|
3991
4224
|
}
|
|
3992
|
-
const text =
|
|
4225
|
+
const text = contents ?? fs7.readFileSync(filePath, "utf8");
|
|
3993
4226
|
if (text.trim() === "") {
|
|
3994
4227
|
return {};
|
|
3995
4228
|
}
|
|
@@ -4010,39 +4243,35 @@ function readDocument(filePath) {
|
|
|
4010
4243
|
}
|
|
4011
4244
|
function createJsonFileTarget(options) {
|
|
4012
4245
|
const { id, label, filePath } = options;
|
|
4013
|
-
function writeDocument(document) {
|
|
4014
|
-
fs6.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
4015
|
-
fs6.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
4016
|
-
`, "utf8");
|
|
4017
|
-
}
|
|
4018
4246
|
return {
|
|
4019
4247
|
id,
|
|
4020
4248
|
label,
|
|
4021
4249
|
location: filePath,
|
|
4022
|
-
detected:
|
|
4250
|
+
detected: fs7.existsSync(filePath),
|
|
4023
4251
|
readEntries() {
|
|
4024
4252
|
const document = readDocument(filePath);
|
|
4025
4253
|
return readMcpServersJson(document);
|
|
4026
4254
|
},
|
|
4255
|
+
hasEntry(name) {
|
|
4256
|
+
return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
|
|
4257
|
+
},
|
|
4027
4258
|
writeEntry(name, entry) {
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
servers =
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
writeDocument(document);
|
|
4259
|
+
updateConfigFile(filePath, (contents) => {
|
|
4260
|
+
const document = readDocument(filePath, contents);
|
|
4261
|
+
const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
|
|
4262
|
+
servers[name] = entry;
|
|
4263
|
+
document.mcpServers = servers;
|
|
4264
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4265
|
+
});
|
|
4036
4266
|
return { status: "written", detail: filePath };
|
|
4037
4267
|
},
|
|
4038
4268
|
removeEntry(name) {
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
writeDocument(document);
|
|
4269
|
+
updateConfigFile(filePath, (contents) => {
|
|
4270
|
+
const document = readDocument(filePath, contents);
|
|
4271
|
+
const servers = readRawMcpServersJson(document);
|
|
4272
|
+
delete servers[name];
|
|
4273
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4274
|
+
});
|
|
4046
4275
|
return { status: "written", detail: filePath };
|
|
4047
4276
|
}
|
|
4048
4277
|
};
|
|
@@ -4094,11 +4323,11 @@ function runClaude(executable, args) {
|
|
|
4094
4323
|
}
|
|
4095
4324
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
4096
4325
|
}
|
|
4097
|
-
function
|
|
4098
|
-
if (
|
|
4326
|
+
function readRawClaudeEntries() {
|
|
4327
|
+
if (fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
4099
4328
|
return {};
|
|
4100
4329
|
}
|
|
4101
|
-
const text =
|
|
4330
|
+
const text = fs7.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
4102
4331
|
let document;
|
|
4103
4332
|
try {
|
|
4104
4333
|
document = JSON.parse(text);
|
|
@@ -4109,7 +4338,7 @@ function readClaudeEntries() {
|
|
|
4109
4338
|
}
|
|
4110
4339
|
throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
|
|
4111
4340
|
}
|
|
4112
|
-
return
|
|
4341
|
+
return readRawMcpServersJson(document);
|
|
4113
4342
|
}
|
|
4114
4343
|
function createClaudeCodeTarget() {
|
|
4115
4344
|
const executable = findExecutable("claude");
|
|
@@ -4117,28 +4346,35 @@ function createClaudeCodeTarget() {
|
|
|
4117
4346
|
id: "claude-code",
|
|
4118
4347
|
label: "Claude Code (user scope)",
|
|
4119
4348
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
4120
|
-
detected: executable != null ||
|
|
4121
|
-
readEntries
|
|
4349
|
+
detected: executable != null || fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
4350
|
+
readEntries() {
|
|
4351
|
+
return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
|
|
4352
|
+
},
|
|
4353
|
+
hasEntry(name) {
|
|
4354
|
+
return Object.hasOwn(readRawClaudeEntries(), name);
|
|
4355
|
+
},
|
|
4122
4356
|
writeEntry(name, entry) {
|
|
4123
4357
|
if (executable == null) {
|
|
4124
4358
|
return {
|
|
4125
4359
|
status: "unavailable",
|
|
4126
|
-
detail:
|
|
4127
|
-
${formatClaudeAddCommand(name, entry)}`
|
|
4360
|
+
detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
|
|
4128
4361
|
};
|
|
4129
4362
|
}
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4363
|
+
const previousEntries = readRawClaudeEntries();
|
|
4364
|
+
const hadPrevious = Object.hasOwn(previousEntries, name);
|
|
4365
|
+
const previous = previousEntries[name];
|
|
4366
|
+
if (hadPrevious) {
|
|
4132
4367
|
const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
4133
4368
|
if (removed.status === "unavailable") {
|
|
4134
4369
|
return removed;
|
|
4135
4370
|
}
|
|
4136
4371
|
}
|
|
4137
4372
|
const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
|
|
4138
|
-
if (added.status === "unavailable" &&
|
|
4139
|
-
runClaude(executable, ["mcp", "add-json", name,
|
|
4373
|
+
if (added.status === "unavailable" && hadPrevious) {
|
|
4374
|
+
const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
|
|
4375
|
+
const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
|
|
4140
4376
|
return { status: "unavailable", detail: `${added.detail}
|
|
4141
|
-
|
|
4377
|
+
${recovery}` };
|
|
4142
4378
|
}
|
|
4143
4379
|
return added;
|
|
4144
4380
|
},
|
|
@@ -4155,194 +4391,121 @@ function createClaudeCodeTarget() {
|
|
|
4155
4391
|
};
|
|
4156
4392
|
}
|
|
4157
4393
|
|
|
4158
|
-
// src/modules/manage/targets/toml-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
}
|
|
4178
|
-
parseValue() {
|
|
4179
|
-
this.skipWhitespace();
|
|
4180
|
-
const current = this.text[this.position];
|
|
4181
|
-
if (current === '"') {
|
|
4182
|
-
return this.parseBasicString();
|
|
4183
|
-
}
|
|
4184
|
-
if (current === "'") {
|
|
4185
|
-
return this.parseLiteralString();
|
|
4186
|
-
}
|
|
4187
|
-
if (current === "[") {
|
|
4188
|
-
return this.parseArray();
|
|
4189
|
-
}
|
|
4190
|
-
if (current === "{") {
|
|
4191
|
-
return this.parseInlineTable();
|
|
4192
|
-
}
|
|
4193
|
-
throw new Error(`unsupported toml value at offset ${this.position}`);
|
|
4194
|
-
}
|
|
4195
|
-
parseBasicString() {
|
|
4196
|
-
this.position += 1;
|
|
4197
|
-
let result = "";
|
|
4198
|
-
while (this.position < this.text.length) {
|
|
4199
|
-
const current = this.text[this.position];
|
|
4200
|
-
if (current === '"') {
|
|
4201
|
-
this.position += 1;
|
|
4202
|
-
return result;
|
|
4203
|
-
}
|
|
4204
|
-
if (current !== "\\") {
|
|
4205
|
-
result += current;
|
|
4206
|
-
this.position += 1;
|
|
4207
|
-
continue;
|
|
4208
|
-
}
|
|
4209
|
-
const escaped = this.text[this.position + 1];
|
|
4210
|
-
this.position += 2;
|
|
4211
|
-
if (escaped === "n") {
|
|
4212
|
-
result += "\n";
|
|
4213
|
-
} else if (escaped === "t") {
|
|
4214
|
-
result += " ";
|
|
4215
|
-
} else if (escaped === "r") {
|
|
4216
|
-
result += "\r";
|
|
4217
|
-
} else if (escaped === "u" || escaped === "U") {
|
|
4218
|
-
let length = 4;
|
|
4219
|
-
if (escaped === "U") {
|
|
4220
|
-
length = 8;
|
|
4394
|
+
// src/modules/manage/targets/toml-lines.ts
|
|
4395
|
+
function lineState() {
|
|
4396
|
+
return { quote: null, multiline: false, depth: 0 };
|
|
4397
|
+
}
|
|
4398
|
+
function scanTomlLine(line, state) {
|
|
4399
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
4400
|
+
const character = line[index];
|
|
4401
|
+
if (state.quote != null) {
|
|
4402
|
+
if (character === "\\" && state.quote === '"') {
|
|
4403
|
+
index += 1;
|
|
4404
|
+
} else if (character === state.quote) {
|
|
4405
|
+
if (!state.multiline) {
|
|
4406
|
+
state.quote = null;
|
|
4407
|
+
} else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
4408
|
+
while (line[index + 1] === character) {
|
|
4409
|
+
index += 1;
|
|
4410
|
+
}
|
|
4411
|
+
state.quote = null;
|
|
4412
|
+
state.multiline = false;
|
|
4221
4413
|
}
|
|
4222
|
-
const hex = this.text.slice(this.position, this.position + length);
|
|
4223
|
-
result += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
4224
|
-
this.position += length;
|
|
4225
|
-
} else {
|
|
4226
|
-
result += escaped;
|
|
4227
4414
|
}
|
|
4415
|
+
continue;
|
|
4228
4416
|
}
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
parseLiteralString() {
|
|
4232
|
-
const end = this.text.indexOf("'", this.position + 1);
|
|
4233
|
-
if (end < 0) {
|
|
4234
|
-
throw new Error("unterminated toml literal string");
|
|
4417
|
+
if (character === "#") {
|
|
4418
|
+
break;
|
|
4235
4419
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
this.position += 1;
|
|
4242
|
-
const items = [];
|
|
4243
|
-
for (; ; ) {
|
|
4244
|
-
this.skipWhitespace();
|
|
4245
|
-
if (this.position >= this.text.length) {
|
|
4246
|
-
throw new Error("unterminated toml array");
|
|
4247
|
-
}
|
|
4248
|
-
if (this.text[this.position] === "]") {
|
|
4249
|
-
this.position += 1;
|
|
4250
|
-
return items;
|
|
4251
|
-
}
|
|
4252
|
-
items.push(this.parseValue());
|
|
4253
|
-
this.skipWhitespace();
|
|
4254
|
-
if (this.text[this.position] === ",") {
|
|
4255
|
-
this.position += 1;
|
|
4420
|
+
if (character === '"' || character === "'") {
|
|
4421
|
+
state.quote = character;
|
|
4422
|
+
state.multiline = line.slice(index, index + 3) === character.repeat(3);
|
|
4423
|
+
if (state.multiline) {
|
|
4424
|
+
index += 2;
|
|
4256
4425
|
}
|
|
4426
|
+
} else if (character === "[" || character === "{") {
|
|
4427
|
+
state.depth += 1;
|
|
4428
|
+
} else if (character === "]" || character === "}") {
|
|
4429
|
+
state.depth -= 1;
|
|
4257
4430
|
}
|
|
4258
4431
|
}
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
}
|
|
4271
|
-
let key;
|
|
4272
|
-
if (this.text[this.position] === '"') {
|
|
4273
|
-
key = this.parseBasicString();
|
|
4274
|
-
} else {
|
|
4275
|
-
const match = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.position));
|
|
4276
|
-
if (match == null) {
|
|
4277
|
-
throw new Error(`invalid inline table key at offset ${this.position}`);
|
|
4278
|
-
}
|
|
4279
|
-
key = match[0];
|
|
4280
|
-
this.position += key.length;
|
|
4281
|
-
}
|
|
4282
|
-
this.skipWhitespace();
|
|
4283
|
-
if (this.text[this.position] !== "=") {
|
|
4284
|
-
throw new Error(`expected '=' at offset ${this.position}`);
|
|
4285
|
-
}
|
|
4286
|
-
this.position += 1;
|
|
4287
|
-
table[key] = this.parseValue();
|
|
4288
|
-
this.skipWhitespace();
|
|
4289
|
-
if (this.text[this.position] === ",") {
|
|
4290
|
-
this.position += 1;
|
|
4291
|
-
}
|
|
4292
|
-
}
|
|
4432
|
+
}
|
|
4433
|
+
function isTable(value) {
|
|
4434
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4435
|
+
}
|
|
4436
|
+
function parseConfigToml(text) {
|
|
4437
|
+
let document;
|
|
4438
|
+
try {
|
|
4439
|
+
document = parse(text, { integersAsBigInt: true });
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
const location = error instanceof TomlError ? ` at line ${error.line}, column ${error.column}` : "";
|
|
4442
|
+
throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
|
|
4293
4443
|
}
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
if (rawKey.startsWith('"') == false) {
|
|
4297
|
-
return rawKey;
|
|
4444
|
+
if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
|
|
4445
|
+
throw new Error("mcp_servers must be a TOML table.");
|
|
4298
4446
|
}
|
|
4299
|
-
|
|
4300
|
-
return parsed;
|
|
4447
|
+
return document;
|
|
4301
4448
|
}
|
|
4302
|
-
function
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
quote = current;
|
|
4317
|
-
} else if (current === "#") {
|
|
4318
|
-
break;
|
|
4319
|
-
} else if (current === "[" || current === "{") {
|
|
4320
|
-
balance += 1;
|
|
4321
|
-
} else if (current === "]" || current === "}") {
|
|
4322
|
-
balance -= 1;
|
|
4449
|
+
function readTomlEntries(text) {
|
|
4450
|
+
return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
|
|
4451
|
+
}
|
|
4452
|
+
function hasTomlEntry(text, name) {
|
|
4453
|
+
return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
|
|
4454
|
+
}
|
|
4455
|
+
function normalize(document) {
|
|
4456
|
+
const servers = document.mcp_servers;
|
|
4457
|
+
if (servers == null) {
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
for (const server of Object.values(servers)) {
|
|
4461
|
+
if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
|
|
4462
|
+
delete server.env;
|
|
4323
4463
|
}
|
|
4324
4464
|
}
|
|
4325
|
-
|
|
4465
|
+
if (Object.keys(servers).length === 0) {
|
|
4466
|
+
delete document.mcp_servers;
|
|
4467
|
+
}
|
|
4326
4468
|
}
|
|
4327
|
-
function
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4469
|
+
function verifyTomlEdit(before, after, name, entry) {
|
|
4470
|
+
const expected = parseConfigToml(before);
|
|
4471
|
+
const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
|
|
4472
|
+
if (entry == null) {
|
|
4473
|
+
delete servers[name];
|
|
4474
|
+
} else {
|
|
4475
|
+
const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
|
|
4476
|
+
Object.defineProperty(servers, name, {
|
|
4477
|
+
value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
|
|
4478
|
+
configurable: true,
|
|
4479
|
+
enumerable: true,
|
|
4480
|
+
writable: true
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
expected.mcp_servers = servers;
|
|
4484
|
+
let actual;
|
|
4485
|
+
try {
|
|
4486
|
+
actual = parseConfigToml(after);
|
|
4487
|
+
} catch {
|
|
4488
|
+
throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
|
|
4489
|
+
}
|
|
4490
|
+
normalize(expected);
|
|
4491
|
+
normalize(actual);
|
|
4492
|
+
if (!isDeepStrictEqual(expected, actual)) {
|
|
4493
|
+
throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
|
|
4494
|
+
}
|
|
4495
|
+
return after;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// src/modules/manage/targets/toml-subset.ts
|
|
4499
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4500
|
+
var ANY_HEADER_PATTERN = /^\s*\[/;
|
|
4501
|
+
var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
|
|
4502
|
+
var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
|
|
4503
|
+
var OWNED_TABLE_KEYS = ["command", "args", "env"];
|
|
4504
|
+
function unquoteKey(rawKey) {
|
|
4505
|
+
if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
|
|
4506
|
+
return parseConfigToml(`value = ${rawKey}`).value;
|
|
4344
4507
|
}
|
|
4345
|
-
return
|
|
4508
|
+
return rawKey;
|
|
4346
4509
|
}
|
|
4347
4510
|
function readStatements(body) {
|
|
4348
4511
|
const statements = [];
|
|
@@ -4354,28 +4517,25 @@ function readStatements(body) {
|
|
|
4354
4517
|
continue;
|
|
4355
4518
|
}
|
|
4356
4519
|
const start = index;
|
|
4357
|
-
|
|
4358
|
-
|
|
4520
|
+
const state = lineState();
|
|
4521
|
+
scanTomlLine(match[2], state);
|
|
4359
4522
|
index += 1;
|
|
4360
|
-
while (
|
|
4361
|
-
|
|
4362
|
-
rawValue += `
|
|
4363
|
-
${continuation}`;
|
|
4364
|
-
balance += bracketBalance(continuation);
|
|
4523
|
+
while ((state.quote != null || state.depth > 0) && index < body.length) {
|
|
4524
|
+
scanTomlLine(body[index], state);
|
|
4365
4525
|
index += 1;
|
|
4366
4526
|
}
|
|
4367
|
-
statements.push({ key: unquoteKey(match[1]),
|
|
4527
|
+
statements.push({ key: unquoteKey(match[1]), start, end: index });
|
|
4368
4528
|
}
|
|
4369
4529
|
return statements;
|
|
4370
4530
|
}
|
|
4371
4531
|
function splitSections(lines) {
|
|
4372
4532
|
const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
|
|
4373
|
-
|
|
4533
|
+
const state = lineState();
|
|
4374
4534
|
for (const line of lines) {
|
|
4375
4535
|
const current = sections[sections.length - 1];
|
|
4376
|
-
if (
|
|
4536
|
+
if (state.quote != null || state.depth > 0) {
|
|
4377
4537
|
current.body.push(line);
|
|
4378
|
-
|
|
4538
|
+
scanTomlLine(line, state);
|
|
4379
4539
|
continue;
|
|
4380
4540
|
}
|
|
4381
4541
|
if (ANY_HEADER_PATTERN.test(line) == true) {
|
|
@@ -4390,73 +4550,18 @@ function splitSections(lines) {
|
|
|
4390
4550
|
continue;
|
|
4391
4551
|
}
|
|
4392
4552
|
current.body.push(line);
|
|
4393
|
-
|
|
4394
|
-
if (keyValueMatch != null) {
|
|
4395
|
-
balance = bracketBalance(stripComment(keyValueMatch[2]));
|
|
4396
|
-
}
|
|
4553
|
+
scanTomlLine(line, state);
|
|
4397
4554
|
}
|
|
4398
4555
|
return sections;
|
|
4399
4556
|
}
|
|
4400
|
-
function
|
|
4401
|
-
|
|
4402
|
-
const value = new TomlValueParser(statement.rawValue).parse();
|
|
4403
|
-
return value;
|
|
4404
|
-
} catch {
|
|
4405
|
-
return null;
|
|
4406
|
-
}
|
|
4557
|
+
function hasMcpServerToml(text, name) {
|
|
4558
|
+
return hasTomlEntry(text, name);
|
|
4407
4559
|
}
|
|
4408
4560
|
function parseMcpServersToml(text) {
|
|
4409
|
-
|
|
4410
|
-
const entries = {};
|
|
4411
|
-
const envTables = {};
|
|
4412
|
-
for (const section of sections) {
|
|
4413
|
-
if (section.serverName == null) {
|
|
4414
|
-
continue;
|
|
4415
|
-
}
|
|
4416
|
-
if (section.subPath === "env") {
|
|
4417
|
-
const envTable = {};
|
|
4418
|
-
for (const statement of readStatements(section.body)) {
|
|
4419
|
-
const value = parseStatementValue(statement);
|
|
4420
|
-
if (typeof value === "string") {
|
|
4421
|
-
envTable[statement.key] = value;
|
|
4422
|
-
}
|
|
4423
|
-
}
|
|
4424
|
-
envTables[section.serverName] = envTable;
|
|
4425
|
-
continue;
|
|
4426
|
-
}
|
|
4427
|
-
if (section.subPath !== "") {
|
|
4428
|
-
continue;
|
|
4429
|
-
}
|
|
4430
|
-
let command = null;
|
|
4431
|
-
let args = [];
|
|
4432
|
-
const env = {};
|
|
4433
|
-
for (const statement of readStatements(section.body)) {
|
|
4434
|
-
const value = parseStatementValue(statement);
|
|
4435
|
-
if (statement.key === "command" && typeof value === "string") {
|
|
4436
|
-
command = value;
|
|
4437
|
-
} else if (statement.key === "args" && Array.isArray(value) == true) {
|
|
4438
|
-
args = value.filter((item) => typeof item === "string");
|
|
4439
|
-
} else if (statement.key === "env" && value != null && typeof value === "object" && Array.isArray(value) == false) {
|
|
4440
|
-
for (const [key, item] of Object.entries(value)) {
|
|
4441
|
-
if (typeof item === "string") {
|
|
4442
|
-
env[key] = item;
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4447
|
-
if (command != null) {
|
|
4448
|
-
entries[section.serverName] = { command, args, env };
|
|
4449
|
-
}
|
|
4450
|
-
}
|
|
4451
|
-
for (const [name, envTable] of Object.entries(envTables)) {
|
|
4452
|
-
if (entries[name] != null) {
|
|
4453
|
-
Object.assign(entries[name].env, envTable);
|
|
4454
|
-
}
|
|
4455
|
-
}
|
|
4456
|
-
return entries;
|
|
4561
|
+
return readTomlEntries(text);
|
|
4457
4562
|
}
|
|
4458
4563
|
function quoteTomlString(value) {
|
|
4459
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
4564
|
+
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")}`);
|
|
4460
4565
|
return `"${escaped}"`;
|
|
4461
4566
|
}
|
|
4462
4567
|
function formatTomlKey(key) {
|
|
@@ -4500,6 +4605,11 @@ function joinSections(sections, eol) {
|
|
|
4500
4605
|
return lines.join(eol);
|
|
4501
4606
|
}
|
|
4502
4607
|
function writeMcpServerToml(text, name, entry) {
|
|
4608
|
+
parseConfigToml(text);
|
|
4609
|
+
const updated = editMcpServerToml(text, name, entry);
|
|
4610
|
+
return verifyTomlEdit(text, updated, name, entry);
|
|
4611
|
+
}
|
|
4612
|
+
function editMcpServerToml(text, name, entry) {
|
|
4503
4613
|
let eol = "\n";
|
|
4504
4614
|
if (text.includes("\r\n") == true) {
|
|
4505
4615
|
eol = "\r\n";
|
|
@@ -4572,34 +4682,36 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
4572
4682
|
var CODEX_CONFIG_PATH = path.join(os5.homedir(), ".codex", "config.toml");
|
|
4573
4683
|
function createCodexTarget() {
|
|
4574
4684
|
function readText() {
|
|
4575
|
-
if (
|
|
4685
|
+
if (fs7.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
4576
4686
|
return "";
|
|
4577
4687
|
}
|
|
4578
|
-
return
|
|
4579
|
-
}
|
|
4580
|
-
function writeText(text) {
|
|
4581
|
-
fs6.mkdirSync(path.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4582
|
-
fs6.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
4583
|
-
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4688
|
+
return fs7.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
4584
4689
|
}
|
|
4585
4690
|
return {
|
|
4586
4691
|
id: "codex",
|
|
4587
4692
|
label: "Codex CLI",
|
|
4588
4693
|
location: CODEX_CONFIG_PATH,
|
|
4589
|
-
detected: findExecutable("codex") != null ||
|
|
4694
|
+
detected: findExecutable("codex") != null || fs7.existsSync(CODEX_CONFIG_PATH) == true,
|
|
4590
4695
|
readEntries() {
|
|
4591
4696
|
const text = readText();
|
|
4592
4697
|
return parseMcpServersToml(text);
|
|
4593
4698
|
},
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4699
|
+
hasEntry(name) {
|
|
4700
|
+
return hasMcpServerToml(readText(), name);
|
|
4701
|
+
},
|
|
4702
|
+
writeEntry(name, entry, options) {
|
|
4703
|
+
updateConfigFile(CODEX_CONFIG_PATH, (original) => {
|
|
4704
|
+
let text = original;
|
|
4705
|
+
if (options?.replace) {
|
|
4706
|
+
text = writeMcpServerToml(text, name, null);
|
|
4707
|
+
}
|
|
4708
|
+
return writeMcpServerToml(text, name, entry);
|
|
4709
|
+
});
|
|
4710
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4598
4711
|
},
|
|
4599
4712
|
removeEntry(name) {
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
return writeText(updated);
|
|
4713
|
+
updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
|
|
4714
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4603
4715
|
}
|
|
4604
4716
|
};
|
|
4605
4717
|
}
|
|
@@ -4738,10 +4850,12 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
4738
4850
|
const profile = profileFromEnv(output, entry.env);
|
|
4739
4851
|
if (profile == null) {
|
|
4740
4852
|
output.write("\n");
|
|
4853
|
+
await prompter.pause();
|
|
4741
4854
|
return;
|
|
4742
4855
|
}
|
|
4743
4856
|
const result = await runConnectionCheck(profile, output);
|
|
4744
4857
|
output.write("\n");
|
|
4858
|
+
await prompter.pause();
|
|
4745
4859
|
if (result.connected == false) {
|
|
4746
4860
|
return;
|
|
4747
4861
|
}
|
|
@@ -4805,13 +4919,14 @@ function buildHostItems(names, entries) {
|
|
|
4805
4919
|
};
|
|
4806
4920
|
});
|
|
4807
4921
|
}
|
|
4808
|
-
async function manageHost(prompter, output, target, entries, name) {
|
|
4922
|
+
async function manageHost(prompter, output, target, targets, entries, name) {
|
|
4809
4923
|
const action = await prompter.select(
|
|
4810
4924
|
name,
|
|
4811
4925
|
[
|
|
4812
4926
|
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4813
4927
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4814
4928
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4929
|
+
...targets.some((candidate) => candidate.id !== target.id) ? [{ value: "copy", key: "o", label: "Copy to another client", hint: "keep this entry and copy its settings" }] : [],
|
|
4815
4930
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4816
4931
|
{ separator: true },
|
|
4817
4932
|
{ value: "back", key: "b", label: "Back" }
|
|
@@ -4830,6 +4945,10 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4830
4945
|
printHost(prompter, name, entries[name]);
|
|
4831
4946
|
return;
|
|
4832
4947
|
}
|
|
4948
|
+
if (action === "copy") {
|
|
4949
|
+
await copyEntryToClient(prompter, target, targets, name, entries[name]);
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4833
4952
|
if (action === "remove") {
|
|
4834
4953
|
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4835
4954
|
if (remove == true) {
|
|
@@ -4931,7 +5050,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
4931
5050
|
continue;
|
|
4932
5051
|
}
|
|
4933
5052
|
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
4934
|
-
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5053
|
+
await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
4935
5054
|
}
|
|
4936
5055
|
} catch (ex) {
|
|
4937
5056
|
if (ex instanceof PromptClosedError) {
|
|
@@ -5384,7 +5503,7 @@ ${file.content}`);
|
|
|
5384
5503
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5385
5504
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5386
5505
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
5387
|
-
if (
|
|
5506
|
+
if (fs7.existsSync(resolvedLocalPath) == false) {
|
|
5388
5507
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
5389
5508
|
}
|
|
5390
5509
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -5424,7 +5543,7 @@ ${file.content}`);
|
|
|
5424
5543
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5425
5544
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5426
5545
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
5427
|
-
|
|
5546
|
+
fs7.mkdirSync(path.dirname(resolvedLocalPath), { recursive: true });
|
|
5428
5547
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
5429
5548
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
5430
5549
|
});
|