@akms/mcp-ssh 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/lib/cli.cjs +1268 -467
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +1266 -465
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/cli.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
-
var
|
|
6
|
-
var
|
|
5
|
+
var fs6 = require('fs');
|
|
6
|
+
var os5 = require('os');
|
|
7
7
|
var path = require('path');
|
|
8
8
|
var child_process = require('child_process');
|
|
9
9
|
var crypto = require('crypto');
|
|
@@ -16,8 +16,8 @@ var zod = require('zod');
|
|
|
16
16
|
|
|
17
17
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
18
|
|
|
19
|
-
var
|
|
20
|
-
var
|
|
19
|
+
var fs6__default = /*#__PURE__*/_interopDefault(fs6);
|
|
20
|
+
var os5__default = /*#__PURE__*/_interopDefault(os5);
|
|
21
21
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
22
22
|
var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
|
|
23
23
|
|
|
@@ -35,7 +35,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
35
35
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
36
36
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
37
37
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
38
|
-
var SERVER_VERSION = "0.1.
|
|
38
|
+
var SERVER_VERSION = "0.1.2";
|
|
39
39
|
|
|
40
40
|
// src/_defs/command-rules.ts
|
|
41
41
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -610,7 +610,7 @@ function findExecutable(name) {
|
|
|
610
610
|
for (const directory of directories) {
|
|
611
611
|
for (const suffix of suffixes) {
|
|
612
612
|
const candidate = path__default.default.join(directory, `${name}${suffix}`);
|
|
613
|
-
if (
|
|
613
|
+
if (fs6__default.default.existsSync(candidate) == true && fs6__default.default.statSync(candidate).isFile() == true) {
|
|
614
614
|
return candidate;
|
|
615
615
|
}
|
|
616
616
|
}
|
|
@@ -620,9 +620,9 @@ function findExecutable(name) {
|
|
|
620
620
|
function expandHomePath(inputPath) {
|
|
621
621
|
let expanded = inputPath;
|
|
622
622
|
if (inputPath === "~") {
|
|
623
|
-
expanded =
|
|
623
|
+
expanded = os5__default.default.homedir();
|
|
624
624
|
} else if (inputPath.startsWith("~/") == true || inputPath.startsWith("~\\") == true) {
|
|
625
|
-
expanded = path__default.default.join(
|
|
625
|
+
expanded = path__default.default.join(os5__default.default.homedir(), inputPath.slice(2));
|
|
626
626
|
}
|
|
627
627
|
return path__default.default.resolve(expanded);
|
|
628
628
|
}
|
|
@@ -751,7 +751,7 @@ function loadHostProfile(env = process.env) {
|
|
|
751
751
|
if (keyPath != null) {
|
|
752
752
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
753
753
|
profile.privateKeyPath = expandedKeyPath;
|
|
754
|
-
if (
|
|
754
|
+
if (fs6__default.default.existsSync(expandedKeyPath) == false) {
|
|
755
755
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
756
756
|
private_key_path: expandedKeyPath
|
|
757
757
|
});
|
|
@@ -783,7 +783,7 @@ function loadHostProfile(env = process.env) {
|
|
|
783
783
|
if (jumpKeyPath != null) {
|
|
784
784
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
785
785
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
786
|
-
if (
|
|
786
|
+
if (fs6__default.default.existsSync(profile.jump.privateKeyPath) == false) {
|
|
787
787
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
788
788
|
private_key_path: profile.jump.privateKeyPath
|
|
789
789
|
});
|
|
@@ -1269,14 +1269,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1269
1269
|
function toRealPath(inputPath) {
|
|
1270
1270
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1271
1271
|
try {
|
|
1272
|
-
return
|
|
1272
|
+
return fs6__default.default.realpathSync.native(inputPath);
|
|
1273
1273
|
} catch (ex) {
|
|
1274
1274
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1275
1275
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1276
1276
|
});
|
|
1277
1277
|
}
|
|
1278
1278
|
try {
|
|
1279
|
-
const parentRealPath =
|
|
1279
|
+
const parentRealPath = fs6__default.default.realpathSync.native(path__default.default.dirname(inputPath));
|
|
1280
1280
|
return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
|
|
1281
1281
|
} catch (ex) {
|
|
1282
1282
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1368,7 +1368,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1368
1368
|
return cached;
|
|
1369
1369
|
}
|
|
1370
1370
|
try {
|
|
1371
|
-
const contents =
|
|
1371
|
+
const contents = fs6__default.default.readFileSync(keyPath);
|
|
1372
1372
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1373
1373
|
return contents;
|
|
1374
1374
|
} catch (ex) {
|
|
@@ -2319,7 +2319,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2319
2319
|
}
|
|
2320
2320
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2321
2321
|
requirePermittedPath(connection, remotePath, "write");
|
|
2322
|
-
const localStats =
|
|
2322
|
+
const localStats = fs6__default.default.statSync(localPath);
|
|
2323
2323
|
if (localStats.isFile() == false) {
|
|
2324
2324
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2325
2325
|
}
|
|
@@ -2356,7 +2356,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2356
2356
|
});
|
|
2357
2357
|
});
|
|
2358
2358
|
});
|
|
2359
|
-
const localStats =
|
|
2359
|
+
const localStats = fs6__default.default.statSync(localPath);
|
|
2360
2360
|
const _logger = $logger.child({
|
|
2361
2361
|
context: "downloadFile",
|
|
2362
2362
|
profile: connection.profile.name,
|
|
@@ -2368,6 +2368,23 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2368
2368
|
}
|
|
2369
2369
|
|
|
2370
2370
|
// src/modules/manage/connection-check.ts
|
|
2371
|
+
function profileFromEnv(output, env) {
|
|
2372
|
+
try {
|
|
2373
|
+
const profile = loadHostProfile(env);
|
|
2374
|
+
if (profile == null) {
|
|
2375
|
+
output.write(" The entry has no host.\n");
|
|
2376
|
+
}
|
|
2377
|
+
return profile;
|
|
2378
|
+
} catch (ex) {
|
|
2379
|
+
let detail = String(ex);
|
|
2380
|
+
if (ex instanceof Error) {
|
|
2381
|
+
detail = ex.message;
|
|
2382
|
+
}
|
|
2383
|
+
output.write(` The server would refuse to start with this entry: ${detail}
|
|
2384
|
+
`);
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2371
2388
|
function describeAuth(profile) {
|
|
2372
2389
|
if (profile.privateKeyPath != null) {
|
|
2373
2390
|
return `key ${profile.privateKeyPath}`;
|
|
@@ -2626,6 +2643,654 @@ function summarizeEntry(entry) {
|
|
|
2626
2643
|
notes: notes.join(", ")
|
|
2627
2644
|
};
|
|
2628
2645
|
}
|
|
2646
|
+
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
2647
|
+
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
2648
|
+
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
2649
|
+
var CLEAR_ANSWER = "-";
|
|
2650
|
+
var OTHER_KEY_PATH = "\0other";
|
|
2651
|
+
var FIELD_NAME_WIDTH = 17;
|
|
2652
|
+
var SUB_FIELDS = ["key", "passphrase", "password", "jump-host", "jump-port", "jump-user", "proxy"];
|
|
2653
|
+
var FIELD_NAMES = {
|
|
2654
|
+
"host": "Host",
|
|
2655
|
+
"port": "Port",
|
|
2656
|
+
"user": "User",
|
|
2657
|
+
"name": "Server name",
|
|
2658
|
+
"auth": "Authentication",
|
|
2659
|
+
"key": "Private key",
|
|
2660
|
+
"passphrase": "Passphrase",
|
|
2661
|
+
"password": "Password",
|
|
2662
|
+
"route": "Route",
|
|
2663
|
+
"jump-host": "Jump host",
|
|
2664
|
+
"jump-port": "Jump port",
|
|
2665
|
+
"jump-user": "Jump user",
|
|
2666
|
+
"proxy": "Proxy command",
|
|
2667
|
+
"description": "Description",
|
|
2668
|
+
"readonly": "Read-only",
|
|
2669
|
+
"paths": "File paths",
|
|
2670
|
+
"host-key": "Host key",
|
|
2671
|
+
"jump-host-key": "Jump host key"
|
|
2672
|
+
};
|
|
2673
|
+
var AUTH_LABELS = {
|
|
2674
|
+
key: "Private key file",
|
|
2675
|
+
password: "Password",
|
|
2676
|
+
agent: "SSH agent"
|
|
2677
|
+
};
|
|
2678
|
+
var ROUTE_LABELS = {
|
|
2679
|
+
"direct": "Directly",
|
|
2680
|
+
"jump": "Through a jump host",
|
|
2681
|
+
"proxy": "Through a proxy command",
|
|
2682
|
+
"jump-proxy": "Proxy command, then a jump host"
|
|
2683
|
+
};
|
|
2684
|
+
var REQUIRED_MARK = "(required)";
|
|
2685
|
+
function findPrivateKeys() {
|
|
2686
|
+
const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
|
|
2687
|
+
let names = [];
|
|
2688
|
+
try {
|
|
2689
|
+
names = fs6__default.default.readdirSync(sshDirectory);
|
|
2690
|
+
} catch {
|
|
2691
|
+
return [];
|
|
2692
|
+
}
|
|
2693
|
+
const keys = [];
|
|
2694
|
+
for (const name of names) {
|
|
2695
|
+
if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
const fullPath = path__default.default.join(sshDirectory, name);
|
|
2699
|
+
let head = "";
|
|
2700
|
+
try {
|
|
2701
|
+
const stat = fs6__default.default.statSync(fullPath);
|
|
2702
|
+
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
const descriptor = fs6__default.default.openSync(fullPath, "r");
|
|
2706
|
+
const buffer = Buffer.alloc(64);
|
|
2707
|
+
const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
2708
|
+
fs6__default.default.closeSync(descriptor);
|
|
2709
|
+
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
2710
|
+
} catch {
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
|
|
2714
|
+
const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
|
|
2715
|
+
if (isPemKey == false && isPuttyKey == false) {
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
let hint = "";
|
|
2719
|
+
try {
|
|
2720
|
+
const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
2721
|
+
hint = publicKey[0].replace(/^ssh-/, "");
|
|
2722
|
+
if (publicKey.length > 2) {
|
|
2723
|
+
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
2724
|
+
}
|
|
2725
|
+
} catch {
|
|
2726
|
+
}
|
|
2727
|
+
keys.push({ path: `~/.ssh/${name}`, hint });
|
|
2728
|
+
}
|
|
2729
|
+
const rank = (key) => {
|
|
2730
|
+
const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
|
|
2731
|
+
if (index < 0) {
|
|
2732
|
+
return PREFERRED_KEY_NAMES.length;
|
|
2733
|
+
}
|
|
2734
|
+
return index;
|
|
2735
|
+
};
|
|
2736
|
+
return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
|
|
2737
|
+
}
|
|
2738
|
+
async function askPort(prompter, question, current) {
|
|
2739
|
+
for (; ; ) {
|
|
2740
|
+
const answer = await prompter.ask(question, String(current));
|
|
2741
|
+
const parsed = Number(answer);
|
|
2742
|
+
if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
|
|
2743
|
+
return parsed;
|
|
2744
|
+
}
|
|
2745
|
+
prompter.write(" Enter a port number between 1 and 65535.\n");
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
async function askOptional(prompter, question, current) {
|
|
2749
|
+
const answer = await prompter.ask(question, current);
|
|
2750
|
+
if (answer === "" || answer === CLEAR_ANSWER) {
|
|
2751
|
+
return void 0;
|
|
2752
|
+
}
|
|
2753
|
+
return answer;
|
|
2754
|
+
}
|
|
2755
|
+
async function askOptionalSecret(prompter, question, current) {
|
|
2756
|
+
let suffix = "(empty for none)";
|
|
2757
|
+
if (current != null) {
|
|
2758
|
+
suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
|
|
2759
|
+
}
|
|
2760
|
+
const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
|
|
2761
|
+
if (answer === CLEAR_ANSWER) {
|
|
2762
|
+
return void 0;
|
|
2763
|
+
}
|
|
2764
|
+
if (answer === "") {
|
|
2765
|
+
return current;
|
|
2766
|
+
}
|
|
2767
|
+
return answer;
|
|
2768
|
+
}
|
|
2769
|
+
async function askKeyPath(prompter, current) {
|
|
2770
|
+
const found = findPrivateKeys();
|
|
2771
|
+
let picked = OTHER_KEY_PATH;
|
|
2772
|
+
if (found.length > 0 || current != null) {
|
|
2773
|
+
const items = [];
|
|
2774
|
+
if (current != null && found.some((key) => key.path === current) == false) {
|
|
2775
|
+
items.push({ value: current, label: current, hint: "current" });
|
|
2776
|
+
}
|
|
2777
|
+
for (const key of found) {
|
|
2778
|
+
items.push({ value: key.path, label: key.path, hint: key.hint });
|
|
2779
|
+
}
|
|
2780
|
+
items.push({ separator: true });
|
|
2781
|
+
items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
|
|
2782
|
+
picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
|
|
2783
|
+
}
|
|
2784
|
+
if (picked !== OTHER_KEY_PATH) {
|
|
2785
|
+
return picked;
|
|
2786
|
+
}
|
|
2787
|
+
for (; ; ) {
|
|
2788
|
+
const typed = await prompter.askRequired("Private key path");
|
|
2789
|
+
if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
|
|
2790
|
+
return typed;
|
|
2791
|
+
}
|
|
2792
|
+
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
2793
|
+
`);
|
|
2794
|
+
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
2795
|
+
if (useAnyway == true) {
|
|
2796
|
+
return typed;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function routeUsesJump(route) {
|
|
2801
|
+
return route === "jump" || route === "jump-proxy";
|
|
2802
|
+
}
|
|
2803
|
+
function routeUsesProxy(route) {
|
|
2804
|
+
return route === "proxy" || route === "jump-proxy";
|
|
2805
|
+
}
|
|
2806
|
+
function deriveServerName(host) {
|
|
2807
|
+
return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
2808
|
+
}
|
|
2809
|
+
function finalizeAnswers(state) {
|
|
2810
|
+
const answers = { ...state.answers };
|
|
2811
|
+
if (routeUsesJump(state.route) == false) {
|
|
2812
|
+
answers.jumpHost = void 0;
|
|
2813
|
+
answers.jumpPort = void 0;
|
|
2814
|
+
answers.jumpUser = void 0;
|
|
2815
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
2816
|
+
}
|
|
2817
|
+
if (routeUsesProxy(state.route) == false) {
|
|
2818
|
+
answers.proxyCommand = void 0;
|
|
2819
|
+
}
|
|
2820
|
+
if (answers.auth !== "key") {
|
|
2821
|
+
answers.keyPath = void 0;
|
|
2822
|
+
answers.passphrase = void 0;
|
|
2823
|
+
}
|
|
2824
|
+
if (answers.auth !== "password") {
|
|
2825
|
+
answers.password = void 0;
|
|
2826
|
+
}
|
|
2827
|
+
return answers;
|
|
2828
|
+
}
|
|
2829
|
+
function snapshotOf(state) {
|
|
2830
|
+
return JSON.stringify(finalizeAnswers(state));
|
|
2831
|
+
}
|
|
2832
|
+
function missingFields(state) {
|
|
2833
|
+
const { answers } = state;
|
|
2834
|
+
const missing = [];
|
|
2835
|
+
if (answers.host === "") {
|
|
2836
|
+
missing.push("host");
|
|
2837
|
+
}
|
|
2838
|
+
if (answers.username === "") {
|
|
2839
|
+
missing.push("user");
|
|
2840
|
+
}
|
|
2841
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
2842
|
+
missing.push("key");
|
|
2843
|
+
}
|
|
2844
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
2845
|
+
missing.push("password");
|
|
2846
|
+
}
|
|
2847
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
2848
|
+
missing.push("jump-host");
|
|
2849
|
+
}
|
|
2850
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
2851
|
+
missing.push("proxy");
|
|
2852
|
+
}
|
|
2853
|
+
return missing;
|
|
2854
|
+
}
|
|
2855
|
+
function describePin(fingerprint) {
|
|
2856
|
+
if (fingerprint == null) {
|
|
2857
|
+
return "not pinned";
|
|
2858
|
+
}
|
|
2859
|
+
return `pinned \xB7 ${fingerprint.slice(0, 19)}\u2026`;
|
|
2860
|
+
}
|
|
2861
|
+
function valueOrRequired(value) {
|
|
2862
|
+
if (value == null || value === "") {
|
|
2863
|
+
return REQUIRED_MARK;
|
|
2864
|
+
}
|
|
2865
|
+
return value;
|
|
2866
|
+
}
|
|
2867
|
+
function fieldRow(id, value, hint) {
|
|
2868
|
+
let name = FIELD_NAMES[id];
|
|
2869
|
+
if (SUB_FIELDS.includes(id) == true) {
|
|
2870
|
+
name = ` ${name}`;
|
|
2871
|
+
}
|
|
2872
|
+
return {
|
|
2873
|
+
value: id,
|
|
2874
|
+
key: id,
|
|
2875
|
+
label: `${name.padEnd(FIELD_NAME_WIDTH)}${value}`,
|
|
2876
|
+
hint
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
function buildRows(state, destination) {
|
|
2880
|
+
const { answers } = state;
|
|
2881
|
+
const rows = [];
|
|
2882
|
+
rows.push(fieldRow("host", valueOrRequired(answers.host)));
|
|
2883
|
+
rows.push(fieldRow("port", String(answers.port)));
|
|
2884
|
+
rows.push(fieldRow("user", valueOrRequired(answers.username)));
|
|
2885
|
+
let nameValue = answers.name;
|
|
2886
|
+
let nameHint = "what the agent sees";
|
|
2887
|
+
if (state.nameTouched == false) {
|
|
2888
|
+
nameHint = "follows the host until you change it";
|
|
2889
|
+
}
|
|
2890
|
+
if (nameValue === "") {
|
|
2891
|
+
nameValue = "ssh-<host>";
|
|
2892
|
+
}
|
|
2893
|
+
rows.push(fieldRow("name", nameValue, nameHint));
|
|
2894
|
+
rows.push(fieldRow("auth", AUTH_LABELS[answers.auth]));
|
|
2895
|
+
if (answers.auth === "key") {
|
|
2896
|
+
rows.push(fieldRow("key", valueOrRequired(answers.keyPath)));
|
|
2897
|
+
let passphrase = "none";
|
|
2898
|
+
if (answers.passphrase != null) {
|
|
2899
|
+
passphrase = "set";
|
|
2900
|
+
}
|
|
2901
|
+
rows.push(fieldRow("passphrase", passphrase));
|
|
2902
|
+
}
|
|
2903
|
+
if (answers.auth === "password") {
|
|
2904
|
+
let password = REQUIRED_MARK;
|
|
2905
|
+
if (answers.password != null) {
|
|
2906
|
+
password = "set";
|
|
2907
|
+
}
|
|
2908
|
+
rows.push(fieldRow("password", password, "stored in the config file as plain text"));
|
|
2909
|
+
}
|
|
2910
|
+
rows.push(fieldRow("route", ROUTE_LABELS[state.route]));
|
|
2911
|
+
if (routeUsesJump(state.route) == true) {
|
|
2912
|
+
rows.push(fieldRow("jump-host", valueOrRequired(answers.jumpHost)));
|
|
2913
|
+
rows.push(fieldRow("jump-port", String(answers.jumpPort ?? DEFAULT_SSH_PORT)));
|
|
2914
|
+
let jumpUser = answers.jumpUser;
|
|
2915
|
+
if (jumpUser == null || jumpUser === answers.username) {
|
|
2916
|
+
let loginUser = answers.username;
|
|
2917
|
+
if (loginUser === "") {
|
|
2918
|
+
loginUser = "\u2026";
|
|
2919
|
+
}
|
|
2920
|
+
jumpUser = `same as login (${loginUser})`;
|
|
2921
|
+
}
|
|
2922
|
+
rows.push(fieldRow("jump-user", jumpUser));
|
|
2923
|
+
}
|
|
2924
|
+
if (routeUsesProxy(state.route) == true) {
|
|
2925
|
+
rows.push(fieldRow("proxy", valueOrRequired(answers.proxyCommand), "%h %p %r \u2192 first hop"));
|
|
2926
|
+
}
|
|
2927
|
+
rows.push(fieldRow("description", answers.description ?? "none", "shown to the agent"));
|
|
2928
|
+
let readonly = "no";
|
|
2929
|
+
if (answers.readonly == true) {
|
|
2930
|
+
readonly = "yes";
|
|
2931
|
+
}
|
|
2932
|
+
rows.push(fieldRow("readonly", readonly, "enter toggles"));
|
|
2933
|
+
let paths = "anywhere";
|
|
2934
|
+
if (answers.allowedPaths.length > 0) {
|
|
2935
|
+
paths = answers.allowedPaths.join(", ");
|
|
2936
|
+
}
|
|
2937
|
+
rows.push(fieldRow("paths", paths, "where the file tools may read and write"));
|
|
2938
|
+
rows.push(fieldRow("host-key", describePin(answers.hostKeyFingerprint)));
|
|
2939
|
+
if (routeUsesJump(state.route) == true) {
|
|
2940
|
+
rows.push(fieldRow("jump-host-key", describePin(answers.jumpHostKeyFingerprint)));
|
|
2941
|
+
}
|
|
2942
|
+
rows.push({ separator: true });
|
|
2943
|
+
let testHint = "not run yet";
|
|
2944
|
+
if (state.lastTest === "ok") {
|
|
2945
|
+
testHint = "last run: OK";
|
|
2946
|
+
} else if (state.lastTest === "failed") {
|
|
2947
|
+
testHint = "last run: failed";
|
|
2948
|
+
}
|
|
2949
|
+
rows.push({ value: "test", key: "test", label: "Test the connection", hint: testHint });
|
|
2950
|
+
rows.push({ value: "save", key: "save", label: "Save", hint: `to ${destination}` });
|
|
2951
|
+
rows.push({ value: "cancel", key: "cancel", label: "Cancel" });
|
|
2952
|
+
return rows;
|
|
2953
|
+
}
|
|
2954
|
+
function nextCursor(state, destination, edited) {
|
|
2955
|
+
if (edited === "readonly") {
|
|
2956
|
+
return edited;
|
|
2957
|
+
}
|
|
2958
|
+
const values = buildRows(state, destination).flatMap((row) => {
|
|
2959
|
+
if ("value" in row) {
|
|
2960
|
+
return [row.value];
|
|
2961
|
+
}
|
|
2962
|
+
return [];
|
|
2963
|
+
});
|
|
2964
|
+
let index = values.indexOf(edited) + 1;
|
|
2965
|
+
if (SUB_FIELDS.includes(edited) == false) {
|
|
2966
|
+
while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
|
|
2967
|
+
index += 1;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const next = values[index];
|
|
2971
|
+
if (next == null || next === "test") {
|
|
2972
|
+
return "save";
|
|
2973
|
+
}
|
|
2974
|
+
return next;
|
|
2975
|
+
}
|
|
2976
|
+
async function testConnection(prompter, output, state) {
|
|
2977
|
+
const answers = finalizeAnswers(state);
|
|
2978
|
+
output.write("\n");
|
|
2979
|
+
const profile = profileFromEnv(output, answersToEntry(answers).env);
|
|
2980
|
+
if (profile == null) {
|
|
2981
|
+
output.write("\n");
|
|
2982
|
+
state.lastTest = "failed";
|
|
2983
|
+
return false;
|
|
2984
|
+
}
|
|
2985
|
+
const result = await runConnectionCheck(profile, output);
|
|
2986
|
+
output.write("\n");
|
|
2987
|
+
if (result.connected == true) {
|
|
2988
|
+
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
2989
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
2990
|
+
if (pin == true) {
|
|
2991
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
if (result.jumpHostKeyFingerprint != null && routeUsesJump(state.route) == true && state.answers.jumpHostKeyFingerprint == null) {
|
|
2995
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
2996
|
+
if (pin == true) {
|
|
2997
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
state.lastTest = "ok";
|
|
3001
|
+
state.testedSnapshot = snapshotOf(state);
|
|
3002
|
+
return true;
|
|
3003
|
+
}
|
|
3004
|
+
const hostKeyChanged = result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== state.answers.hostKeyFingerprint;
|
|
3005
|
+
if (hostKeyChanged == true) {
|
|
3006
|
+
output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3007
|
+
const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
|
|
3008
|
+
if (repin == true) {
|
|
3009
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
|
|
3010
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && state.answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== state.answers.jumpHostKeyFingerprint;
|
|
3014
|
+
if (jumpKeyChanged == true) {
|
|
3015
|
+
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3016
|
+
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
3017
|
+
if (repin == true) {
|
|
3018
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint ?? void 0;
|
|
3019
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
state.lastTest = "failed";
|
|
3023
|
+
return false;
|
|
3024
|
+
}
|
|
3025
|
+
async function editField(prompter, output, state, id) {
|
|
3026
|
+
const { answers } = state;
|
|
3027
|
+
if (id === "host") {
|
|
3028
|
+
let current;
|
|
3029
|
+
if (answers.host !== "") {
|
|
3030
|
+
current = answers.host;
|
|
3031
|
+
}
|
|
3032
|
+
answers.host = await prompter.askRequired("Host (hostname or IP)", current);
|
|
3033
|
+
if (state.nameTouched == false) {
|
|
3034
|
+
answers.name = deriveServerName(answers.host);
|
|
3035
|
+
}
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (id === "port") {
|
|
3039
|
+
answers.port = await askPort(prompter, "Port", answers.port);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (id === "user") {
|
|
3043
|
+
let current;
|
|
3044
|
+
if (answers.username !== "") {
|
|
3045
|
+
current = answers.username;
|
|
3046
|
+
}
|
|
3047
|
+
answers.username = await prompter.askRequired("Login user", current);
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
if (id === "name") {
|
|
3051
|
+
let current;
|
|
3052
|
+
if (answers.name !== "") {
|
|
3053
|
+
current = answers.name;
|
|
3054
|
+
}
|
|
3055
|
+
for (; ; ) {
|
|
3056
|
+
const name = await prompter.askRequired("Server name the agent sees", current);
|
|
3057
|
+
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
3058
|
+
answers.name = name;
|
|
3059
|
+
state.nameTouched = true;
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
if (id === "auth") {
|
|
3066
|
+
answers.auth = await prompter.select(
|
|
3067
|
+
"Authentication",
|
|
3068
|
+
[
|
|
3069
|
+
{ value: "key", label: AUTH_LABELS.key, hint: "only the path is stored \u2014 recommended" },
|
|
3070
|
+
{ value: "password", label: AUTH_LABELS.password, hint: "stored in the config file as plain text" },
|
|
3071
|
+
{ value: "agent", label: AUTH_LABELS.agent, hint: "keys already loaded in ssh-agent" }
|
|
3072
|
+
],
|
|
3073
|
+
{ defaultValue: answers.auth }
|
|
3074
|
+
);
|
|
3075
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3076
|
+
answers.keyPath = await askKeyPath(prompter, void 0);
|
|
3077
|
+
}
|
|
3078
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3079
|
+
answers.password = await askOptionalSecret(prompter, "Password", void 0);
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
if (id === "key") {
|
|
3084
|
+
answers.keyPath = await askKeyPath(prompter, answers.keyPath);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (id === "passphrase") {
|
|
3088
|
+
answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (id === "password") {
|
|
3092
|
+
answers.password = await askOptionalSecret(prompter, "Password", answers.password);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
if (id === "route") {
|
|
3096
|
+
state.route = await prompter.select(
|
|
3097
|
+
"Route",
|
|
3098
|
+
[
|
|
3099
|
+
{ value: "direct", label: ROUTE_LABELS.direct },
|
|
3100
|
+
{ value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
|
|
3101
|
+
{ value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
|
|
3102
|
+
{ value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
|
|
3103
|
+
],
|
|
3104
|
+
{ defaultValue: state.route }
|
|
3105
|
+
);
|
|
3106
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3107
|
+
answers.jumpHost = await prompter.askRequired("Jump host");
|
|
3108
|
+
}
|
|
3109
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3110
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", "cloudflared access ssh --hostname %h");
|
|
3111
|
+
}
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (id === "jump-host") {
|
|
3115
|
+
answers.jumpHost = await prompter.askRequired("Jump host", answers.jumpHost);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
if (id === "jump-port") {
|
|
3119
|
+
answers.jumpPort = await askPort(prompter, "Jump host port", answers.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (id === "jump-user") {
|
|
3123
|
+
answers.jumpUser = await askOptional(prompter, "Jump host user (empty = same as the login user)", answers.jumpUser);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (id === "proxy") {
|
|
3127
|
+
output.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3128
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
if (id === "description") {
|
|
3132
|
+
answers.description = await askOptional(prompter, "Description shown to the agent", answers.description);
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
if (id === "readonly") {
|
|
3136
|
+
answers.readonly = answers.readonly == false;
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (id === "paths") {
|
|
3140
|
+
let current;
|
|
3141
|
+
if (answers.allowedPaths.length > 0) {
|
|
3142
|
+
current = answers.allowedPaths.join(",");
|
|
3143
|
+
}
|
|
3144
|
+
const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
|
|
3145
|
+
answers.allowedPaths = [];
|
|
3146
|
+
if (typed != null) {
|
|
3147
|
+
answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3148
|
+
}
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
const isJump = id === "jump-host-key";
|
|
3152
|
+
let pinned = answers.hostKeyFingerprint;
|
|
3153
|
+
if (isJump == true) {
|
|
3154
|
+
pinned = answers.jumpHostKeyFingerprint;
|
|
3155
|
+
}
|
|
3156
|
+
if (pinned != null) {
|
|
3157
|
+
const unpin = await prompter.askYesNo(`Unpin ${pinned}?`, false);
|
|
3158
|
+
if (unpin == true && isJump == true) {
|
|
3159
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3160
|
+
} else if (unpin == true) {
|
|
3161
|
+
answers.hostKeyFingerprint = void 0;
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const missing = missingFields(state);
|
|
3166
|
+
if (missing.length > 0) {
|
|
3167
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
|
|
3168
|
+
`);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
await testConnection(prompter, output, state);
|
|
3172
|
+
}
|
|
3173
|
+
async function runHostForm(prompter, output, options) {
|
|
3174
|
+
const { existing, takenNames, destination } = options;
|
|
3175
|
+
let state;
|
|
3176
|
+
if (existing != null) {
|
|
3177
|
+
let route = "direct";
|
|
3178
|
+
if (existing.jumpHost != null && existing.proxyCommand != null) {
|
|
3179
|
+
route = "jump-proxy";
|
|
3180
|
+
} else if (existing.jumpHost != null) {
|
|
3181
|
+
route = "jump";
|
|
3182
|
+
} else if (existing.proxyCommand != null) {
|
|
3183
|
+
route = "proxy";
|
|
3184
|
+
}
|
|
3185
|
+
state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
|
|
3186
|
+
} else {
|
|
3187
|
+
const foundKeys = findPrivateKeys();
|
|
3188
|
+
state = {
|
|
3189
|
+
answers: {
|
|
3190
|
+
name: "",
|
|
3191
|
+
host: "",
|
|
3192
|
+
port: DEFAULT_SSH_PORT,
|
|
3193
|
+
username: "",
|
|
3194
|
+
auth: "key",
|
|
3195
|
+
keyPath: foundKeys[0]?.path,
|
|
3196
|
+
readonly: false,
|
|
3197
|
+
allowedPaths: [],
|
|
3198
|
+
extraEnv: {}
|
|
3199
|
+
},
|
|
3200
|
+
route: "direct",
|
|
3201
|
+
nameTouched: false,
|
|
3202
|
+
lastTest: "never",
|
|
3203
|
+
testedSnapshot: null
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
const initialSnapshot = snapshotOf(state);
|
|
3207
|
+
const originalName = existing?.name;
|
|
3208
|
+
let title = "New host";
|
|
3209
|
+
if (existing != null) {
|
|
3210
|
+
title = `Editing ${existing.name}`;
|
|
3211
|
+
}
|
|
3212
|
+
output.write(prompter.paint.dim(`
|
|
3213
|
+
Pick a setting to change it. In a text field, Enter keeps the value shown and ${CLEAR_ANSWER} clears it.
|
|
3214
|
+
|
|
3215
|
+
`));
|
|
3216
|
+
const previousTransient = prompter.transient;
|
|
3217
|
+
prompter.transient = true;
|
|
3218
|
+
try {
|
|
3219
|
+
let cursor = "host";
|
|
3220
|
+
for (; ; ) {
|
|
3221
|
+
const picked = await prompter.select(title, buildRows(state, destination), { defaultValue: cursor, cancelValue: "cancel" });
|
|
3222
|
+
if (picked === "cancel") {
|
|
3223
|
+
if (snapshotOf(state) !== initialSnapshot) {
|
|
3224
|
+
const discard = await prompter.askYesNo("Discard the changes?", false);
|
|
3225
|
+
if (discard == false) {
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
const missing = missingFields(state);
|
|
3232
|
+
if (picked === "test") {
|
|
3233
|
+
if (missing.length > 0) {
|
|
3234
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3235
|
+
`);
|
|
3236
|
+
cursor = missing[0];
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
const connected = await testConnection(prompter, output, state);
|
|
3240
|
+
cursor = "test";
|
|
3241
|
+
if (connected == true) {
|
|
3242
|
+
cursor = "save";
|
|
3243
|
+
}
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (picked === "save") {
|
|
3247
|
+
if (missing.length > 0) {
|
|
3248
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3249
|
+
`);
|
|
3250
|
+
cursor = missing[0];
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
const name = state.answers.name;
|
|
3254
|
+
if (name !== originalName && takenNames.includes(name) == true) {
|
|
3255
|
+
const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
|
|
3256
|
+
if (replace == false) {
|
|
3257
|
+
cursor = "name";
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (snapshotOf(state) !== state.testedSnapshot) {
|
|
3262
|
+
const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
|
|
3263
|
+
if (testFirst == true) {
|
|
3264
|
+
const connected = await testConnection(prompter, output, state);
|
|
3265
|
+
if (connected == false) {
|
|
3266
|
+
const next = await prompter.select(
|
|
3267
|
+
"The check failed. What now?",
|
|
3268
|
+
[
|
|
3269
|
+
{ value: "back", key: "back", label: "Back to the settings" },
|
|
3270
|
+
{ value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
|
|
3271
|
+
{ value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
|
|
3272
|
+
],
|
|
3273
|
+
{ defaultValue: "back", cancelValue: "back" }
|
|
3274
|
+
);
|
|
3275
|
+
if (next === "discard") {
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
if (next === "back") {
|
|
3279
|
+
cursor = "test";
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
return finalizeAnswers(state);
|
|
3286
|
+
}
|
|
3287
|
+
await editField(prompter, output, state, picked);
|
|
3288
|
+
cursor = nextCursor(state, destination, picked);
|
|
3289
|
+
}
|
|
3290
|
+
} finally {
|
|
3291
|
+
prompter.transient = previousTransient;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
2629
3294
|
|
|
2630
3295
|
// src/modules/manage/prompter.ts
|
|
2631
3296
|
var PromptClosedError = class extends Error {
|
|
@@ -2634,36 +3299,158 @@ var PromptClosedError = class extends Error {
|
|
|
2634
3299
|
this.name = "PromptClosedError";
|
|
2635
3300
|
}
|
|
2636
3301
|
};
|
|
3302
|
+
var ESC = "\x1B";
|
|
3303
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`, "k"];
|
|
3304
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`, "j"];
|
|
3305
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`];
|
|
3306
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`];
|
|
3307
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
3308
|
+
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
3309
|
+
var KEY_CTRL_C = "";
|
|
3310
|
+
var KEY_CTRL_D = "";
|
|
3311
|
+
function splitKeys(chunk) {
|
|
3312
|
+
const keys = [];
|
|
3313
|
+
let index = 0;
|
|
3314
|
+
while (index < chunk.length) {
|
|
3315
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
3316
|
+
if (isSequence == true) {
|
|
3317
|
+
let end = index + 2;
|
|
3318
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
3319
|
+
end += 1;
|
|
3320
|
+
}
|
|
3321
|
+
keys.push(chunk.slice(index, end + 1));
|
|
3322
|
+
index = end + 1;
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
3326
|
+
const character = String.fromCodePoint(codePoint);
|
|
3327
|
+
keys.push(character);
|
|
3328
|
+
index += character.length;
|
|
3329
|
+
}
|
|
3330
|
+
return keys;
|
|
3331
|
+
}
|
|
3332
|
+
function charWidth(codePoint) {
|
|
3333
|
+
if (codePoint < 4352) {
|
|
3334
|
+
return 1;
|
|
3335
|
+
}
|
|
3336
|
+
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
3337
|
+
if (isWide == true) {
|
|
3338
|
+
return 2;
|
|
3339
|
+
}
|
|
3340
|
+
return 1;
|
|
3341
|
+
}
|
|
3342
|
+
function displayWidth(text) {
|
|
3343
|
+
let width = 0;
|
|
3344
|
+
for (const character of text) {
|
|
3345
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
3346
|
+
}
|
|
3347
|
+
return width;
|
|
3348
|
+
}
|
|
3349
|
+
function padToWidth(text, width) {
|
|
3350
|
+
const missing = width - displayWidth(text);
|
|
3351
|
+
if (missing <= 0) {
|
|
3352
|
+
return text;
|
|
3353
|
+
}
|
|
3354
|
+
return `${text}${" ".repeat(missing)}`;
|
|
3355
|
+
}
|
|
3356
|
+
function groupLabelWidths(items) {
|
|
3357
|
+
const widths = items.map(() => 0);
|
|
3358
|
+
let groupStart = 0;
|
|
3359
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
3360
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
3361
|
+
if (atBoundary == false) {
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
let groupWidth = 0;
|
|
3365
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
3366
|
+
const item = items[member];
|
|
3367
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
3368
|
+
}
|
|
3369
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
3370
|
+
widths[member] = groupWidth;
|
|
3371
|
+
}
|
|
3372
|
+
groupStart = index + 1;
|
|
3373
|
+
}
|
|
3374
|
+
return widths;
|
|
3375
|
+
}
|
|
3376
|
+
function supportsUnicode() {
|
|
3377
|
+
if (process.platform !== "win32") {
|
|
3378
|
+
return true;
|
|
3379
|
+
}
|
|
3380
|
+
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
3381
|
+
}
|
|
3382
|
+
function createStyle(enabled) {
|
|
3383
|
+
const paint = (open, close) => {
|
|
3384
|
+
return (text) => {
|
|
3385
|
+
if (enabled == false || text === "") {
|
|
3386
|
+
return text;
|
|
3387
|
+
}
|
|
3388
|
+
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
3389
|
+
};
|
|
3390
|
+
};
|
|
3391
|
+
return {
|
|
3392
|
+
accent: paint(36, 39),
|
|
3393
|
+
success: paint(32, 39),
|
|
3394
|
+
dim: paint(2, 22),
|
|
3395
|
+
bold: paint(1, 22)
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
2637
3398
|
var Prompter = class {
|
|
2638
3399
|
constructor(input, output) {
|
|
2639
3400
|
this.input = input;
|
|
2640
3401
|
this.output = output;
|
|
2641
3402
|
this.lineBuffer = "";
|
|
2642
3403
|
this.pendingLines = [];
|
|
2643
|
-
this.
|
|
3404
|
+
this.lineWaiter = null;
|
|
2644
3405
|
this.inputEnded = false;
|
|
2645
|
-
|
|
2646
|
-
this.
|
|
3406
|
+
/** Set while a key-driven prompt (select, secret) owns the input. */
|
|
3407
|
+
this.keyHandler = null;
|
|
3408
|
+
/**
|
|
3409
|
+
* When true, an answered prompt leaves nothing behind on a TTY instead of its
|
|
3410
|
+
* `✔ question · answer` line. The settings form sets it: the form itself shows every
|
|
3411
|
+
* value, so it can redraw in place rather than scroll a trail of edits.
|
|
3412
|
+
*/
|
|
3413
|
+
this.transient = false;
|
|
3414
|
+
const ttyInput = input;
|
|
3415
|
+
const ttyOutput = output;
|
|
3416
|
+
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
3417
|
+
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
3418
|
+
if (supportsUnicode() == true) {
|
|
3419
|
+
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
3420
|
+
} else {
|
|
3421
|
+
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
3422
|
+
}
|
|
2647
3423
|
input.setEncoding("utf8");
|
|
2648
3424
|
input.on("data", (chunk) => {
|
|
2649
3425
|
this.onData(chunk);
|
|
2650
3426
|
});
|
|
2651
3427
|
input.on("end", () => {
|
|
2652
3428
|
this.inputEnded = true;
|
|
2653
|
-
if (this.
|
|
2654
|
-
const waiter = this.
|
|
2655
|
-
this.
|
|
3429
|
+
if (this.lineWaiter != null) {
|
|
3430
|
+
const waiter = this.lineWaiter;
|
|
3431
|
+
this.lineWaiter = null;
|
|
2656
3432
|
waiter(null);
|
|
2657
3433
|
}
|
|
3434
|
+
if (this.keyHandler != null) {
|
|
3435
|
+
this.keyHandler(KEY_CTRL_D);
|
|
3436
|
+
}
|
|
2658
3437
|
});
|
|
2659
3438
|
}
|
|
2660
3439
|
/** Stops reading so the process can exit once the command is done. */
|
|
2661
3440
|
close() {
|
|
3441
|
+
if (this.interactive == true) {
|
|
3442
|
+
this.input.setRawMode?.(false);
|
|
3443
|
+
this.output.write(`${ESC}[?25h`);
|
|
3444
|
+
}
|
|
2662
3445
|
this.input.pause();
|
|
2663
3446
|
}
|
|
2664
3447
|
write(text) {
|
|
2665
3448
|
this.output.write(text);
|
|
2666
3449
|
}
|
|
3450
|
+
/** Styling for callers that print around the prompts; plain text off a TTY. */
|
|
3451
|
+
get paint() {
|
|
3452
|
+
return this.style;
|
|
3453
|
+
}
|
|
2667
3454
|
/**
|
|
2668
3455
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2669
3456
|
*
|
|
@@ -2672,18 +3459,20 @@ var Prompter = class {
|
|
|
2672
3459
|
async ask(question, defaultValue) {
|
|
2673
3460
|
let suffix = "";
|
|
2674
3461
|
if (defaultValue != null && defaultValue !== "") {
|
|
2675
|
-
suffix = `
|
|
3462
|
+
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2676
3463
|
}
|
|
2677
|
-
this.
|
|
3464
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
3465
|
+
this.output.write(prompt);
|
|
2678
3466
|
const line = await this.readLine();
|
|
2679
3467
|
if (line == null) {
|
|
2680
3468
|
throw new PromptClosedError();
|
|
2681
3469
|
}
|
|
2682
|
-
|
|
2683
|
-
if (
|
|
2684
|
-
|
|
3470
|
+
let answer = line.trim();
|
|
3471
|
+
if (answer === "" && defaultValue != null) {
|
|
3472
|
+
answer = defaultValue;
|
|
2685
3473
|
}
|
|
2686
|
-
|
|
3474
|
+
this.collapseAnswered(prompt, line, question, answer);
|
|
3475
|
+
return answer;
|
|
2687
3476
|
}
|
|
2688
3477
|
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
2689
3478
|
async askRequired(question, defaultValue) {
|
|
@@ -2692,77 +3481,115 @@ var Prompter = class {
|
|
|
2692
3481
|
if (answer !== "") {
|
|
2693
3482
|
return answer;
|
|
2694
3483
|
}
|
|
2695
|
-
this.output.write("
|
|
3484
|
+
this.output.write(` ${this.style.dim("A value is required.")}
|
|
3485
|
+
`);
|
|
2696
3486
|
}
|
|
2697
3487
|
}
|
|
2698
3488
|
async askYesNo(question, defaultValue) {
|
|
2699
|
-
let hint = "
|
|
3489
|
+
let hint = "y/N";
|
|
2700
3490
|
if (defaultValue == true) {
|
|
2701
|
-
hint = "
|
|
3491
|
+
hint = "Y/n";
|
|
2702
3492
|
}
|
|
2703
3493
|
for (; ; ) {
|
|
2704
|
-
this.
|
|
3494
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
3495
|
+
this.output.write(prompt);
|
|
2705
3496
|
const line = await this.readLine();
|
|
2706
3497
|
if (line == null) {
|
|
2707
3498
|
throw new PromptClosedError();
|
|
2708
3499
|
}
|
|
2709
3500
|
const normalized = line.trim().toLowerCase();
|
|
3501
|
+
let answer = null;
|
|
2710
3502
|
if (normalized === "") {
|
|
2711
|
-
|
|
3503
|
+
answer = defaultValue;
|
|
3504
|
+
} else if (normalized === "y" || normalized === "yes") {
|
|
3505
|
+
answer = true;
|
|
3506
|
+
} else if (normalized === "n" || normalized === "no") {
|
|
3507
|
+
answer = false;
|
|
2712
3508
|
}
|
|
2713
|
-
if (
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
3509
|
+
if (answer != null) {
|
|
3510
|
+
let shown = "no";
|
|
3511
|
+
if (answer == true) {
|
|
3512
|
+
shown = "yes";
|
|
3513
|
+
}
|
|
3514
|
+
this.collapseAnswered(prompt, line, question, shown);
|
|
3515
|
+
return answer;
|
|
2718
3516
|
}
|
|
2719
|
-
this.output.write("
|
|
3517
|
+
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
3518
|
+
`);
|
|
2720
3519
|
}
|
|
2721
3520
|
}
|
|
2722
|
-
/**
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3521
|
+
/**
|
|
3522
|
+
* Picks one of `items`. On a TTY: ↑/↓ (or k/j) move, 1–9 jump, Enter picks, Esc returns
|
|
3523
|
+
* `cancelValue`. Off a TTY: a numbered list, answered by number, key or label.
|
|
3524
|
+
*
|
|
3525
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
3526
|
+
*/
|
|
3527
|
+
async select(question, items, options) {
|
|
3528
|
+
const choices = items.filter((item) => "value" in item);
|
|
3529
|
+
if (choices.length === 0) {
|
|
3530
|
+
throw new Error("select needs at least one choice");
|
|
2729
3531
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
`);
|
|
3532
|
+
let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
|
|
3533
|
+
if (startIndex < 0) {
|
|
3534
|
+
startIndex = items.findIndex((item) => "value" in item);
|
|
3535
|
+
}
|
|
3536
|
+
if (this.interactive == true) {
|
|
3537
|
+
const picked2 = await this.selectWithKeys(question, items, startIndex, options?.cancelValue);
|
|
3538
|
+
return picked2;
|
|
2738
3539
|
}
|
|
3540
|
+
const picked = await this.selectWithLines(question, items, startIndex);
|
|
3541
|
+
return picked;
|
|
2739
3542
|
}
|
|
2740
3543
|
/**
|
|
2741
3544
|
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2742
3545
|
* caller says a value already exists.
|
|
3546
|
+
*
|
|
3547
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2743
3548
|
*/
|
|
2744
3549
|
async askSecret(question) {
|
|
2745
|
-
|
|
2746
|
-
if (rawModeStream.isTTY != true || rawModeStream.setRawMode == null) {
|
|
3550
|
+
if (this.interactive == false) {
|
|
2747
3551
|
const answer = await this.ask(question);
|
|
2748
3552
|
return answer;
|
|
2749
3553
|
}
|
|
2750
|
-
this.
|
|
2751
|
-
this.
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
3554
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
3555
|
+
this.output.write(prompt);
|
|
3556
|
+
const ttyInput = this.input;
|
|
3557
|
+
return new Promise((resolve, reject) => {
|
|
3558
|
+
let buffer = "";
|
|
3559
|
+
const finish = () => {
|
|
3560
|
+
this.keyHandler = null;
|
|
3561
|
+
ttyInput.setRawMode?.(false);
|
|
3562
|
+
this.output.write(`\r${ESC}[2K`);
|
|
3563
|
+
};
|
|
3564
|
+
this.keyHandler = (key) => {
|
|
3565
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3566
|
+
finish();
|
|
3567
|
+
reject(new PromptClosedError());
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3571
|
+
finish();
|
|
3572
|
+
let shown = this.style.dim("(empty)");
|
|
3573
|
+
if (buffer !== "") {
|
|
3574
|
+
shown = "\u2022".repeat(Math.min(buffer.length, 8));
|
|
3575
|
+
}
|
|
3576
|
+
this.writeAnswered(question, shown);
|
|
3577
|
+
resolve(buffer);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (KEY_BACKSPACE.includes(key) == true) {
|
|
3581
|
+
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
if (key.startsWith(ESC) == true) {
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
buffer += key;
|
|
3588
|
+
};
|
|
3589
|
+
ttyInput.setRawMode?.(true);
|
|
3590
|
+
});
|
|
2765
3591
|
}
|
|
3592
|
+
// ── line mode ──────────────────────────────────────────────────────────
|
|
2766
3593
|
readLine() {
|
|
2767
3594
|
const queued = this.pendingLines.shift();
|
|
2768
3595
|
if (queued != null) {
|
|
@@ -2772,36 +3599,16 @@ var Prompter = class {
|
|
|
2772
3599
|
return Promise.resolve(null);
|
|
2773
3600
|
}
|
|
2774
3601
|
return new Promise((resolve) => {
|
|
2775
|
-
this.
|
|
3602
|
+
this.lineWaiter = resolve;
|
|
2776
3603
|
});
|
|
2777
3604
|
}
|
|
2778
|
-
deliver(line) {
|
|
2779
|
-
if (this.waiter != null) {
|
|
2780
|
-
const waiter = this.waiter;
|
|
2781
|
-
this.waiter = null;
|
|
2782
|
-
waiter(line);
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
this.pendingLines.push(line);
|
|
2786
|
-
}
|
|
2787
3605
|
onData(chunk) {
|
|
2788
|
-
if (this.
|
|
2789
|
-
for (const
|
|
2790
|
-
if (
|
|
2791
|
-
|
|
2792
|
-
this.secretBuffer = "";
|
|
2793
|
-
this.deliver(secret);
|
|
2794
|
-
continue;
|
|
2795
|
-
}
|
|
2796
|
-
if (character === "") {
|
|
2797
|
-
this.output.write("\n");
|
|
2798
|
-
process.exit(130);
|
|
2799
|
-
}
|
|
2800
|
-
if (character === "\x7F" || character === "\b") {
|
|
2801
|
-
this.secretBuffer = this.secretBuffer.slice(0, -1);
|
|
2802
|
-
continue;
|
|
3606
|
+
if (this.keyHandler != null) {
|
|
3607
|
+
for (const key of splitKeys(chunk)) {
|
|
3608
|
+
if (this.keyHandler == null) {
|
|
3609
|
+
break;
|
|
2803
3610
|
}
|
|
2804
|
-
this.
|
|
3611
|
+
this.keyHandler(key);
|
|
2805
3612
|
}
|
|
2806
3613
|
return;
|
|
2807
3614
|
}
|
|
@@ -2810,160 +3617,234 @@ var Prompter = class {
|
|
|
2810
3617
|
while (newlineIndex >= 0) {
|
|
2811
3618
|
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
2812
3619
|
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
2813
|
-
this.
|
|
3620
|
+
if (this.lineWaiter != null) {
|
|
3621
|
+
const waiter = this.lineWaiter;
|
|
3622
|
+
this.lineWaiter = null;
|
|
3623
|
+
waiter(line);
|
|
3624
|
+
} else {
|
|
3625
|
+
this.pendingLines.push(line);
|
|
3626
|
+
}
|
|
2814
3627
|
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2815
3628
|
}
|
|
2816
3629
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
return answer;
|
|
2836
|
-
}
|
|
2837
|
-
async function askOptionalSecret(prompter, question, current) {
|
|
2838
|
-
let suffix = " (empty for none)";
|
|
2839
|
-
if (current != null) {
|
|
2840
|
-
suffix = ` [keep current, ${CLEAR_ANSWER} to clear]`;
|
|
2841
|
-
}
|
|
2842
|
-
const answer = await prompter.askSecret(`${question}${suffix}`);
|
|
2843
|
-
if (answer === CLEAR_ANSWER) {
|
|
2844
|
-
return void 0;
|
|
2845
|
-
}
|
|
2846
|
-
if (answer === "") {
|
|
2847
|
-
return current;
|
|
2848
|
-
}
|
|
2849
|
-
return answer;
|
|
2850
|
-
}
|
|
2851
|
-
async function askHostQuestions(prompter, existing) {
|
|
2852
|
-
if (existing != null) {
|
|
2853
|
-
prompter.write(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
|
|
2854
|
-
|
|
3630
|
+
async selectWithLines(question, items, startIndex) {
|
|
3631
|
+
const choices = items.filter((item) => "value" in item);
|
|
3632
|
+
const labelWidths = groupLabelWidths(items);
|
|
3633
|
+
this.output.write(`${this.symbols.question} ${question}
|
|
3634
|
+
`);
|
|
3635
|
+
let number = 0;
|
|
3636
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3637
|
+
const item = items[index];
|
|
3638
|
+
if ("value" in item == false) {
|
|
3639
|
+
this.output.write("\n");
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
number += 1;
|
|
3643
|
+
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
3644
|
+
if (item.hint != null && item.hint !== "") {
|
|
3645
|
+
line += ` ${item.hint}`;
|
|
3646
|
+
}
|
|
3647
|
+
this.output.write(`${line.trimEnd()}
|
|
2855
3648
|
`);
|
|
2856
|
-
}
|
|
2857
|
-
const host = await prompter.askRequired("Host (hostname or IP)", existing?.host);
|
|
2858
|
-
const port = await askPort(prompter, "Port", existing?.port ?? DEFAULT_SSH_PORT);
|
|
2859
|
-
const username = await prompter.askRequired("Login user", existing?.username);
|
|
2860
|
-
let name = "";
|
|
2861
|
-
const defaultName = existing?.name ?? `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
2862
|
-
for (; ; ) {
|
|
2863
|
-
name = await prompter.askRequired("Server name the agent sees", defaultName);
|
|
2864
|
-
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
2865
|
-
break;
|
|
2866
|
-
}
|
|
2867
|
-
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
2868
|
-
}
|
|
2869
|
-
const auth = await prompter.askChoice(
|
|
2870
|
-
"Authentication",
|
|
2871
|
-
[
|
|
2872
|
-
{ key: "key", label: "private key file (recommended \u2014 only the path is stored)" },
|
|
2873
|
-
{ key: "password", label: "password (stored in the config file in plain text)" },
|
|
2874
|
-
{ key: "agent", label: "ssh agent (SSH_AUTH_SOCK / OpenSSH agent pipe)" }
|
|
2875
|
-
],
|
|
2876
|
-
existing?.auth ?? "key"
|
|
2877
|
-
);
|
|
2878
|
-
let keyPath;
|
|
2879
|
-
let passphrase;
|
|
2880
|
-
let password;
|
|
2881
|
-
if (auth === "key") {
|
|
2882
|
-
let defaultKeyPath = existing?.keyPath;
|
|
2883
|
-
if (defaultKeyPath == null) {
|
|
2884
|
-
defaultKeyPath = DEFAULT_KEY_CANDIDATES.find((candidate) => fs7__default.default.existsSync(expandHomePath(candidate)) == true);
|
|
2885
3649
|
}
|
|
3650
|
+
const defaultChoice = items[startIndex];
|
|
3651
|
+
const defaultNumber = choices.indexOf(defaultChoice) + 1;
|
|
2886
3652
|
for (; ; ) {
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3653
|
+
this.output.write(`Choice (${defaultNumber}): `);
|
|
3654
|
+
const line = await this.readLine();
|
|
3655
|
+
if (line == null) {
|
|
3656
|
+
throw new PromptClosedError();
|
|
3657
|
+
}
|
|
3658
|
+
const answer = line.trim();
|
|
3659
|
+
if (answer === "") {
|
|
3660
|
+
return defaultChoice.value;
|
|
3661
|
+
}
|
|
3662
|
+
const index = Number(answer);
|
|
3663
|
+
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
3664
|
+
return choices[index - 1].value;
|
|
3665
|
+
}
|
|
3666
|
+
const lowered = answer.toLowerCase();
|
|
3667
|
+
const matched = choices.find((choice) => {
|
|
3668
|
+
const key = choice.key ?? choice.value;
|
|
3669
|
+
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
3670
|
+
});
|
|
3671
|
+
if (matched != null) {
|
|
3672
|
+
return matched.value;
|
|
2890
3673
|
}
|
|
2891
|
-
|
|
3674
|
+
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
2892
3675
|
`);
|
|
2893
|
-
|
|
2894
|
-
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
// ── TTY rendering ──────────────────────────────────────────────────────
|
|
3679
|
+
columns() {
|
|
3680
|
+
const columns = this.output.columns;
|
|
3681
|
+
if (columns == null || columns < 20) {
|
|
3682
|
+
return 80;
|
|
3683
|
+
}
|
|
3684
|
+
return columns;
|
|
3685
|
+
}
|
|
3686
|
+
/** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
|
|
3687
|
+
fitLine(parts, width) {
|
|
3688
|
+
let used = 0;
|
|
3689
|
+
let line = "";
|
|
3690
|
+
for (const part of parts) {
|
|
3691
|
+
let text = "";
|
|
3692
|
+
for (const character of part.text) {
|
|
3693
|
+
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
3694
|
+
if (used + characterWidth > width) {
|
|
3695
|
+
break;
|
|
3696
|
+
}
|
|
3697
|
+
used += characterWidth;
|
|
3698
|
+
text += character;
|
|
3699
|
+
}
|
|
3700
|
+
if (part.paint != null) {
|
|
3701
|
+
line += part.paint(text);
|
|
3702
|
+
} else {
|
|
3703
|
+
line += text;
|
|
3704
|
+
}
|
|
3705
|
+
if (used >= width) {
|
|
2895
3706
|
break;
|
|
2896
3707
|
}
|
|
2897
3708
|
}
|
|
2898
|
-
|
|
3709
|
+
return line;
|
|
2899
3710
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
|
|
3711
|
+
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3712
|
+
writeAnswered(question, answer) {
|
|
3713
|
+
if (this.transient == true) {
|
|
3714
|
+
return;
|
|
2905
3715
|
}
|
|
3716
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
3717
|
+
`);
|
|
2906
3718
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
};
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3719
|
+
/**
|
|
3720
|
+
* Replaces a cooked-mode prompt and its echoed answer with the one-line record. The
|
|
3721
|
+
* rows are worked out from the widths, since a long answer wraps.
|
|
3722
|
+
*/
|
|
3723
|
+
collapseAnswered(prompt, typed, question, answer) {
|
|
3724
|
+
if (this.interactive == false) {
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3728
|
+
const used = displayWidth(plainPrompt) + displayWidth(typed);
|
|
3729
|
+
const rows = Math.max(1, Math.ceil(used / this.columns()));
|
|
3730
|
+
this.output.write(`${ESC}[${rows}A\r${ESC}[J`);
|
|
3731
|
+
let shown = answer;
|
|
3732
|
+
if (shown === "") {
|
|
3733
|
+
shown = this.style.dim("(empty)");
|
|
3734
|
+
}
|
|
3735
|
+
this.writeAnswered(question, shown);
|
|
3736
|
+
}
|
|
3737
|
+
selectWithKeys(question, items, startIndex, cancelValue) {
|
|
3738
|
+
const ttyInput = this.input;
|
|
3739
|
+
const selectableIndexes = items.flatMap((item, index) => {
|
|
3740
|
+
if ("value" in item) {
|
|
3741
|
+
return [index];
|
|
3742
|
+
}
|
|
3743
|
+
return [];
|
|
3744
|
+
});
|
|
3745
|
+
const labelWidths = groupLabelWidths(items);
|
|
3746
|
+
const ruleWidth = Math.min(Math.max(...labelWidths), 24);
|
|
3747
|
+
let active = startIndex;
|
|
3748
|
+
let renderedLines = 0;
|
|
3749
|
+
const draw = (lines) => {
|
|
3750
|
+
let text = "";
|
|
3751
|
+
if (renderedLines > 1) {
|
|
3752
|
+
text += `${ESC}[${renderedLines - 1}A`;
|
|
3753
|
+
}
|
|
3754
|
+
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
3755
|
+
this.output.write(text);
|
|
3756
|
+
renderedLines = lines.length;
|
|
3757
|
+
};
|
|
3758
|
+
const buildLines = () => {
|
|
3759
|
+
const width = this.columns() - 1;
|
|
3760
|
+
const lines = [
|
|
3761
|
+
this.fitLine([
|
|
3762
|
+
{ text: this.symbols.question, paint: this.style.accent },
|
|
3763
|
+
{ text: " " },
|
|
3764
|
+
{ text: question, paint: this.style.bold }
|
|
3765
|
+
], width)
|
|
3766
|
+
];
|
|
3767
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3768
|
+
const item = items[index];
|
|
3769
|
+
if ("value" in item == false) {
|
|
3770
|
+
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
3771
|
+
continue;
|
|
3772
|
+
}
|
|
3773
|
+
const isActive = index === active;
|
|
3774
|
+
const parts = [];
|
|
3775
|
+
if (isActive == true) {
|
|
3776
|
+
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
3777
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
3778
|
+
} else {
|
|
3779
|
+
parts.push({ text: " " });
|
|
3780
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
3781
|
+
}
|
|
3782
|
+
if (item.hint != null && item.hint !== "") {
|
|
3783
|
+
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
3784
|
+
}
|
|
3785
|
+
lines.push(this.fitLine(parts, width));
|
|
3786
|
+
}
|
|
3787
|
+
let footer = "\u2191\u2193 move \xB7 enter select";
|
|
3788
|
+
if (cancelValue != null) {
|
|
3789
|
+
footer += " \xB7 esc back";
|
|
3790
|
+
}
|
|
3791
|
+
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3792
|
+
return lines;
|
|
3793
|
+
};
|
|
3794
|
+
const move = (step) => {
|
|
3795
|
+
const position = selectableIndexes.indexOf(active);
|
|
3796
|
+
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
3797
|
+
active = selectableIndexes[next];
|
|
3798
|
+
};
|
|
3799
|
+
return new Promise((resolve, reject) => {
|
|
3800
|
+
const finish = () => {
|
|
3801
|
+
this.keyHandler = null;
|
|
3802
|
+
ttyInput.setRawMode?.(false);
|
|
3803
|
+
draw([]);
|
|
3804
|
+
this.output.write(`${ESC}[?25h`);
|
|
3805
|
+
};
|
|
3806
|
+
this.keyHandler = (key) => {
|
|
3807
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3808
|
+
finish();
|
|
3809
|
+
reject(new PromptClosedError());
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3813
|
+
const picked = items[active];
|
|
3814
|
+
finish();
|
|
3815
|
+
this.writeAnswered(question, picked.label);
|
|
3816
|
+
resolve(picked.value);
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (key === ESC) {
|
|
3820
|
+
if (cancelValue == null) {
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
finish();
|
|
3824
|
+
resolve(cancelValue);
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
if (KEY_UP.includes(key) == true) {
|
|
3828
|
+
move(-1);
|
|
3829
|
+
} else if (KEY_DOWN.includes(key) == true) {
|
|
3830
|
+
move(1);
|
|
3831
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3832
|
+
active = selectableIndexes[0];
|
|
3833
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3834
|
+
active = selectableIndexes[selectableIndexes.length - 1];
|
|
3835
|
+
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
3836
|
+
active = selectableIndexes[Number(key) - 1];
|
|
3837
|
+
} else {
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
draw(buildLines());
|
|
3841
|
+
};
|
|
3842
|
+
ttyInput.setRawMode?.(true);
|
|
3843
|
+
this.output.write(`${ESC}[?25l`);
|
|
3844
|
+
draw(buildLines());
|
|
3845
|
+
});
|
|
2964
3846
|
}
|
|
2965
|
-
|
|
2966
|
-
}
|
|
3847
|
+
};
|
|
2967
3848
|
function toMcpServerEntry(value) {
|
|
2968
3849
|
if (typeof value !== "object" || value == null) {
|
|
2969
3850
|
return null;
|
|
@@ -3004,10 +3885,10 @@ function readMcpServersJson(document) {
|
|
|
3004
3885
|
return entries;
|
|
3005
3886
|
}
|
|
3006
3887
|
function readDocument(filePath) {
|
|
3007
|
-
if (
|
|
3888
|
+
if (fs6__default.default.existsSync(filePath) == false) {
|
|
3008
3889
|
return {};
|
|
3009
3890
|
}
|
|
3010
|
-
const text =
|
|
3891
|
+
const text = fs6__default.default.readFileSync(filePath, "utf8");
|
|
3011
3892
|
if (text.trim() === "") {
|
|
3012
3893
|
return {};
|
|
3013
3894
|
}
|
|
@@ -3029,15 +3910,15 @@ function readDocument(filePath) {
|
|
|
3029
3910
|
function createJsonFileTarget(options) {
|
|
3030
3911
|
const { id, label, filePath } = options;
|
|
3031
3912
|
function writeDocument(document) {
|
|
3032
|
-
|
|
3033
|
-
|
|
3913
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
|
|
3914
|
+
fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
3034
3915
|
`, "utf8");
|
|
3035
3916
|
}
|
|
3036
3917
|
return {
|
|
3037
3918
|
id,
|
|
3038
3919
|
label,
|
|
3039
3920
|
location: filePath,
|
|
3040
|
-
detected:
|
|
3921
|
+
detected: fs6__default.default.existsSync(filePath),
|
|
3041
3922
|
readEntries() {
|
|
3042
3923
|
const document = readDocument(filePath);
|
|
3043
3924
|
return readMcpServersJson(document);
|
|
@@ -3067,7 +3948,7 @@ function createJsonFileTarget(options) {
|
|
|
3067
3948
|
}
|
|
3068
3949
|
|
|
3069
3950
|
// src/modules/manage/targets/claude-code.ts
|
|
3070
|
-
var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(
|
|
3951
|
+
var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".claude.json");
|
|
3071
3952
|
var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
|
|
3072
3953
|
function toClaudeAddJsonPayload(entry) {
|
|
3073
3954
|
return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
|
|
@@ -3113,10 +3994,10 @@ function runClaude(executable, args) {
|
|
|
3113
3994
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
3114
3995
|
}
|
|
3115
3996
|
function readClaudeEntries() {
|
|
3116
|
-
if (
|
|
3997
|
+
if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
3117
3998
|
return {};
|
|
3118
3999
|
}
|
|
3119
|
-
const text =
|
|
4000
|
+
const text = fs6__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
3120
4001
|
let document;
|
|
3121
4002
|
try {
|
|
3122
4003
|
document = JSON.parse(text);
|
|
@@ -3135,7 +4016,7 @@ function createClaudeCodeTarget() {
|
|
|
3135
4016
|
id: "claude-code",
|
|
3136
4017
|
label: "Claude Code (user scope)",
|
|
3137
4018
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
3138
|
-
detected: executable != null ||
|
|
4019
|
+
detected: executable != null || fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
3139
4020
|
readEntries: readClaudeEntries,
|
|
3140
4021
|
writeEntry(name, entry) {
|
|
3141
4022
|
if (executable == null) {
|
|
@@ -3587,24 +4468,24 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
3587
4468
|
}
|
|
3588
4469
|
|
|
3589
4470
|
// src/modules/manage/targets/codex.ts
|
|
3590
|
-
var CODEX_CONFIG_PATH = path__default.default.join(
|
|
4471
|
+
var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
|
|
3591
4472
|
function createCodexTarget() {
|
|
3592
4473
|
function readText() {
|
|
3593
|
-
if (
|
|
4474
|
+
if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
3594
4475
|
return "";
|
|
3595
4476
|
}
|
|
3596
|
-
return
|
|
4477
|
+
return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
3597
4478
|
}
|
|
3598
4479
|
function writeText(text) {
|
|
3599
|
-
|
|
3600
|
-
|
|
4480
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4481
|
+
fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
3601
4482
|
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
3602
4483
|
}
|
|
3603
4484
|
return {
|
|
3604
4485
|
id: "codex",
|
|
3605
4486
|
label: "Codex CLI",
|
|
3606
4487
|
location: CODEX_CONFIG_PATH,
|
|
3607
|
-
detected: findExecutable("codex") != null ||
|
|
4488
|
+
detected: findExecutable("codex") != null || fs6__default.default.existsSync(CODEX_CONFIG_PATH) == true,
|
|
3608
4489
|
readEntries() {
|
|
3609
4490
|
const text = readText();
|
|
3610
4491
|
return parseMcpServersToml(text);
|
|
@@ -3625,20 +4506,20 @@ function createCodexTarget() {
|
|
|
3625
4506
|
// src/modules/manage/targets/index.ts
|
|
3626
4507
|
function claudeDesktopConfigPath() {
|
|
3627
4508
|
if (process.platform === "win32") {
|
|
3628
|
-
const appData = process.env.APPDATA ?? path__default.default.join(
|
|
4509
|
+
const appData = process.env.APPDATA ?? path__default.default.join(os5__default.default.homedir(), "AppData", "Roaming");
|
|
3629
4510
|
return path__default.default.join(appData, "Claude", "claude_desktop_config.json");
|
|
3630
4511
|
}
|
|
3631
4512
|
if (process.platform === "darwin") {
|
|
3632
|
-
return path__default.default.join(
|
|
4513
|
+
return path__default.default.join(os5__default.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
3633
4514
|
}
|
|
3634
|
-
return path__default.default.join(
|
|
4515
|
+
return path__default.default.join(os5__default.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
3635
4516
|
}
|
|
3636
4517
|
function listConfigTargets() {
|
|
3637
4518
|
const targets = [
|
|
3638
4519
|
createClaudeCodeTarget(),
|
|
3639
4520
|
createCodexTarget(),
|
|
3640
4521
|
createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
|
|
3641
|
-
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(
|
|
4522
|
+
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(os5__default.default.homedir(), ".cursor", "mcp.json") })
|
|
3642
4523
|
];
|
|
3643
4524
|
const detected = targets.filter((target) => target.detected == true);
|
|
3644
4525
|
const others = targets.filter((target) => target.detected == false);
|
|
@@ -3652,6 +4533,7 @@ var SECRET_ENV_NAMES = [
|
|
|
3652
4533
|
SINGLE_HOST_ENV.JUMP_PASSWORD,
|
|
3653
4534
|
SINGLE_HOST_ENV.JUMP_PASSPHRASE
|
|
3654
4535
|
];
|
|
4536
|
+
var HOST_VALUE_PREFIX = "host:";
|
|
3655
4537
|
function errorMessage(error) {
|
|
3656
4538
|
if (error instanceof Error) {
|
|
3657
4539
|
return error.message;
|
|
@@ -3679,125 +4561,34 @@ function formatForTarget(target, name, entry) {
|
|
|
3679
4561
|
function indent(text) {
|
|
3680
4562
|
return text.replace(/\n$/, "").split("\n").map((line) => ` ${line}`).join("\n");
|
|
3681
4563
|
}
|
|
3682
|
-
function reportWrite(
|
|
4564
|
+
function reportWrite(prompter, result, action) {
|
|
3683
4565
|
if (result.status === "written") {
|
|
3684
|
-
|
|
4566
|
+
prompter.write(` ${prompter.paint.success(action)} ${prompter.paint.dim(result.detail)}
|
|
3685
4567
|
`);
|
|
3686
|
-
|
|
4568
|
+
prompter.write(prompter.paint.dim(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3687
4569
|
return;
|
|
3688
4570
|
}
|
|
3689
|
-
|
|
4571
|
+
prompter.write(` Not ${action.toLowerCase()}: ${result.detail}
|
|
3690
4572
|
|
|
3691
4573
|
`);
|
|
3692
4574
|
}
|
|
3693
|
-
function profileFromEntry(output, entry) {
|
|
3694
|
-
try {
|
|
3695
|
-
const profile = loadHostProfile(entry.env);
|
|
3696
|
-
if (profile == null) {
|
|
3697
|
-
output.write(" The entry has no host.\n");
|
|
3698
|
-
}
|
|
3699
|
-
return profile;
|
|
3700
|
-
} catch (ex) {
|
|
3701
|
-
output.write(` The server would refuse to start with this entry: ${errorMessage(ex)}
|
|
3702
|
-
`);
|
|
3703
|
-
return null;
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
async function verifyAnswers(prompter, output, initial) {
|
|
3707
|
-
let answers = initial;
|
|
3708
|
-
for (; ; ) {
|
|
3709
|
-
const testNow = await prompter.askYesNo("Test the connection now?", true);
|
|
3710
|
-
if (testNow == false) {
|
|
3711
|
-
return answers;
|
|
3712
|
-
}
|
|
3713
|
-
output.write("\n");
|
|
3714
|
-
const profile = profileFromEntry(output, answersToEntry(answers));
|
|
3715
|
-
if (profile != null) {
|
|
3716
|
-
const result = await runConnectionCheck(profile, output);
|
|
3717
|
-
output.write("\n");
|
|
3718
|
-
if (result.connected == true) {
|
|
3719
|
-
if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
|
|
3720
|
-
const pin = await prompter.askYesNo(`Pin the host key ${result.hostKeyFingerprint}? (a changed key then aborts the connection)`, true);
|
|
3721
|
-
if (pin == true) {
|
|
3722
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
|
|
3726
|
-
const pin = await prompter.askYesNo(`Pin the jump host key ${result.jumpHostKeyFingerprint}?`, true);
|
|
3727
|
-
if (pin == true) {
|
|
3728
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
|
-
return answers;
|
|
3732
|
-
}
|
|
3733
|
-
const hostKeyChanged = result.hostKeyFingerprint != null && answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== answers.hostKeyFingerprint;
|
|
3734
|
-
if (hostKeyChanged == true) {
|
|
3735
|
-
output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3736
|
-
const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
|
|
3737
|
-
if (repin == true) {
|
|
3738
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint ?? void 0 };
|
|
3739
|
-
continue;
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== answers.jumpHostKeyFingerprint;
|
|
3743
|
-
if (jumpKeyChanged == true) {
|
|
3744
|
-
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3745
|
-
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
3746
|
-
if (repin == true) {
|
|
3747
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint ?? void 0 };
|
|
3748
|
-
continue;
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
}
|
|
3752
|
-
const next = await prompter.askChoice(
|
|
3753
|
-
"What now?",
|
|
3754
|
-
[
|
|
3755
|
-
{ key: "r", label: "retry the check" },
|
|
3756
|
-
{ key: "e", label: "edit the answers" },
|
|
3757
|
-
{ key: "w", label: "write it anyway" },
|
|
3758
|
-
{ key: "c", label: "cancel" }
|
|
3759
|
-
],
|
|
3760
|
-
"e"
|
|
3761
|
-
);
|
|
3762
|
-
if (next === "c") {
|
|
3763
|
-
return null;
|
|
3764
|
-
}
|
|
3765
|
-
if (next === "w") {
|
|
3766
|
-
return answers;
|
|
3767
|
-
}
|
|
3768
|
-
if (next === "e") {
|
|
3769
|
-
output.write("\n");
|
|
3770
|
-
answers = await askHostQuestions(prompter, answers);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
4575
|
async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
3775
4576
|
let existing;
|
|
3776
4577
|
if (existingName != null) {
|
|
3777
4578
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
3778
|
-
output.write(`
|
|
3779
|
-
Editing ${existingName}
|
|
3780
|
-
`);
|
|
3781
|
-
} else {
|
|
3782
|
-
output.write("\nAdding a host\n");
|
|
3783
4579
|
}
|
|
3784
|
-
const
|
|
3785
|
-
|
|
3786
|
-
|
|
4580
|
+
const answers = await runHostForm(prompter, output, {
|
|
4581
|
+
existing,
|
|
4582
|
+
takenNames: Object.keys(entries),
|
|
4583
|
+
destination: target.label
|
|
4584
|
+
});
|
|
3787
4585
|
if (answers == null) {
|
|
3788
4586
|
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
3789
4587
|
return;
|
|
3790
4588
|
}
|
|
3791
|
-
if (answers.name !== existingName && entries[answers.name] != null) {
|
|
3792
|
-
const replace = await prompter.askYesNo(`"${answers.name}" already exists in this file. Replace it?`, false);
|
|
3793
|
-
if (replace == false) {
|
|
3794
|
-
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
3795
|
-
return;
|
|
3796
|
-
}
|
|
3797
|
-
}
|
|
3798
4589
|
const entry = answersToEntry(answers);
|
|
3799
4590
|
output.write(`
|
|
3800
|
-
This goes into ${target.location}
|
|
4591
|
+
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
3801
4592
|
|
|
3802
4593
|
`);
|
|
3803
4594
|
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
@@ -3805,11 +4596,11 @@ Editing ${existingName}
|
|
|
3805
4596
|
`);
|
|
3806
4597
|
const confirmed = await prompter.askYesNo("Write it?", true);
|
|
3807
4598
|
if (confirmed == false) {
|
|
3808
|
-
output.write(" Not written.
|
|
4599
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
3809
4600
|
return;
|
|
3810
4601
|
}
|
|
3811
4602
|
const written = target.writeEntry(answers.name, entry);
|
|
3812
|
-
reportWrite(
|
|
4603
|
+
reportWrite(prompter, written, "Written");
|
|
3813
4604
|
if (written.status === "written" && existingName != null && existingName !== answers.name) {
|
|
3814
4605
|
const removed = target.removeEntry(existingName);
|
|
3815
4606
|
if (removed.status === "unavailable") {
|
|
@@ -3821,7 +4612,7 @@ Editing ${existingName}
|
|
|
3821
4612
|
}
|
|
3822
4613
|
async function checkHost(prompter, output, target, name, entry) {
|
|
3823
4614
|
output.write("\n");
|
|
3824
|
-
const profile =
|
|
4615
|
+
const profile = profileFromEnv(output, entry.env);
|
|
3825
4616
|
if (profile == null) {
|
|
3826
4617
|
output.write("\n");
|
|
3827
4618
|
return;
|
|
@@ -3834,14 +4625,14 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3834
4625
|
const env = { ...entry.env };
|
|
3835
4626
|
let changed = false;
|
|
3836
4627
|
if (result.hostKeyFingerprint != null && env[SINGLE_HOST_ENV.HOST_KEY] == null) {
|
|
3837
|
-
const pin = await prompter.askYesNo(
|
|
4628
|
+
const pin = await prompter.askYesNo("Pin this host key?", true);
|
|
3838
4629
|
if (pin == true) {
|
|
3839
4630
|
env[SINGLE_HOST_ENV.HOST_KEY] = result.hostKeyFingerprint;
|
|
3840
4631
|
changed = true;
|
|
3841
4632
|
}
|
|
3842
4633
|
}
|
|
3843
4634
|
if (result.jumpHostKeyFingerprint != null && env[SINGLE_HOST_ENV.JUMP_HOST_KEY] == null) {
|
|
3844
|
-
const pin = await prompter.askYesNo(
|
|
4635
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3845
4636
|
if (pin == true) {
|
|
3846
4637
|
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = result.jumpHostKeyFingerprint;
|
|
3847
4638
|
changed = true;
|
|
@@ -3849,76 +4640,79 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3849
4640
|
}
|
|
3850
4641
|
if (changed == true) {
|
|
3851
4642
|
const written = target.writeEntry(name, { ...entry, env });
|
|
3852
|
-
reportWrite(
|
|
4643
|
+
reportWrite(prompter, written, "Pinned");
|
|
3853
4644
|
}
|
|
3854
4645
|
}
|
|
3855
|
-
function printHost(
|
|
3856
|
-
|
|
3857
|
-
|
|
4646
|
+
function printHost(prompter, name, entry) {
|
|
4647
|
+
prompter.write(`
|
|
4648
|
+
${prompter.paint.dim('JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):')}
|
|
3858
4649
|
|
|
3859
4650
|
`);
|
|
3860
|
-
|
|
3861
|
-
output.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4651
|
+
prompter.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
|
|
3862
4652
|
|
|
3863
4653
|
`);
|
|
3864
|
-
|
|
3865
|
-
output.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4654
|
+
prompter.write(` ${prompter.paint.dim("TOML \u2014 Codex (~/.codex/config.toml):")}
|
|
3866
4655
|
|
|
3867
4656
|
`);
|
|
3868
|
-
}
|
|
3869
|
-
|
|
3870
|
-
if (names.length === 0) {
|
|
3871
|
-
output.write(" No hosts yet \u2014 add one with a.\n\n");
|
|
3872
|
-
return null;
|
|
3873
|
-
}
|
|
3874
|
-
if (names.length === 1) {
|
|
3875
|
-
return names[0];
|
|
3876
|
-
}
|
|
3877
|
-
for (; ; ) {
|
|
3878
|
-
const answer = await prompter.ask("Which host (number or name, empty to go back)");
|
|
3879
|
-
if (answer === "") {
|
|
3880
|
-
return null;
|
|
3881
|
-
}
|
|
3882
|
-
const index = Number(answer);
|
|
3883
|
-
if (Number.isInteger(index) == true && index >= 1 && index <= names.length) {
|
|
3884
|
-
return names[index - 1];
|
|
3885
|
-
}
|
|
3886
|
-
if (names.includes(answer) == true) {
|
|
3887
|
-
return answer;
|
|
3888
|
-
}
|
|
3889
|
-
output.write(` No host "${answer}".
|
|
4657
|
+
prompter.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4658
|
+
|
|
3890
4659
|
`);
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
function renderHostList(output, target, entries) {
|
|
3894
|
-
const names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3895
|
-
const otherCount = Object.keys(entries).length - names.length;
|
|
3896
|
-
output.write(`
|
|
3897
|
-
${target.label} \u2014 ${target.location}
|
|
4660
|
+
prompter.write(` ${prompter.paint.dim("Claude Code CLI (bash / zsh / Git Bash):")}
|
|
3898
4661
|
|
|
3899
4662
|
`);
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
} else {
|
|
3903
|
-
const rows = names.map((name, index) => {
|
|
3904
|
-
const summary = summarizeEntry(entries[name]);
|
|
3905
|
-
return [String(index + 1), name, summary.endpoint, summary.auth, summary.notes];
|
|
3906
|
-
});
|
|
3907
|
-
const header = ["#", "Server", "Endpoint", "Auth", "Notes"];
|
|
3908
|
-
const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => row[column].length)));
|
|
3909
|
-
for (const row of [header, ...rows]) {
|
|
3910
|
-
const line = row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
3911
|
-
output.write(` ${line}
|
|
4663
|
+
prompter.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4664
|
+
|
|
3912
4665
|
`);
|
|
4666
|
+
}
|
|
4667
|
+
function buildHostItems(names, entries) {
|
|
4668
|
+
const nameWidth = Math.max(...names.map((name) => displayWidth(name)));
|
|
4669
|
+
const summaries = names.map((name) => summarizeEntry(entries[name]));
|
|
4670
|
+
const endpointWidth = Math.max(...summaries.map((summary) => displayWidth(summary.endpoint)));
|
|
4671
|
+
return names.map((name, index) => {
|
|
4672
|
+
const summary = summaries[index];
|
|
4673
|
+
const hintParts = [summary.auth];
|
|
4674
|
+
if (summary.notes !== "") {
|
|
4675
|
+
hintParts.push(summary.notes);
|
|
3913
4676
|
}
|
|
4677
|
+
return {
|
|
4678
|
+
value: `${HOST_VALUE_PREFIX}${name}`,
|
|
4679
|
+
key: name,
|
|
4680
|
+
label: `${name.padEnd(nameWidth)} ${summary.endpoint.padEnd(endpointWidth)}`,
|
|
4681
|
+
hint: hintParts.join(" \xB7 ")
|
|
4682
|
+
};
|
|
4683
|
+
});
|
|
4684
|
+
}
|
|
4685
|
+
async function manageHost(prompter, output, target, entries, name) {
|
|
4686
|
+
const action = await prompter.select(
|
|
4687
|
+
name,
|
|
4688
|
+
[
|
|
4689
|
+
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4690
|
+
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4691
|
+
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4692
|
+
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4693
|
+
{ separator: true },
|
|
4694
|
+
{ value: "back", key: "b", label: "Back" }
|
|
4695
|
+
],
|
|
4696
|
+
{ defaultValue: "edit", cancelValue: "back" }
|
|
4697
|
+
);
|
|
4698
|
+
if (action === "edit") {
|
|
4699
|
+
await addOrEditHost(prompter, output, target, entries, name);
|
|
4700
|
+
return;
|
|
3914
4701
|
}
|
|
3915
|
-
if (
|
|
3916
|
-
output
|
|
3917
|
-
|
|
3918
|
-
|
|
4702
|
+
if (action === "check") {
|
|
4703
|
+
await checkHost(prompter, output, target, name, entries[name]);
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
if (action === "print") {
|
|
4707
|
+
printHost(prompter, name, entries[name]);
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
if (action === "remove") {
|
|
4711
|
+
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4712
|
+
if (remove == true) {
|
|
4713
|
+
reportWrite(prompter, target.removeEntry(name), "Removed");
|
|
4714
|
+
}
|
|
3919
4715
|
}
|
|
3920
|
-
output.write("\n");
|
|
3921
|
-
return names;
|
|
3922
4716
|
}
|
|
3923
4717
|
async function runConfigCommand(options) {
|
|
3924
4718
|
const { input, output } = options;
|
|
@@ -3934,81 +4728,88 @@ async function runConfigCommand(options) {
|
|
|
3934
4728
|
target = requested;
|
|
3935
4729
|
}
|
|
3936
4730
|
const prompter = new Prompter(input, output);
|
|
4731
|
+
const paint = prompter.paint;
|
|
3937
4732
|
try {
|
|
3938
4733
|
for (; ; ) {
|
|
3939
4734
|
let entries = {};
|
|
3940
4735
|
let names = [];
|
|
3941
|
-
let
|
|
4736
|
+
let readError = null;
|
|
3942
4737
|
try {
|
|
3943
4738
|
entries = target.readEntries();
|
|
3944
|
-
names =
|
|
4739
|
+
names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3945
4740
|
} catch (ex) {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4741
|
+
readError = errorMessage(ex);
|
|
4742
|
+
}
|
|
4743
|
+
output.write(`
|
|
4744
|
+
${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
4745
|
+
`);
|
|
4746
|
+
if (readError != null) {
|
|
4747
|
+
output.write(` Cannot read it: ${readError}
|
|
3951
4748
|
Fix the file by hand, or switch to another client.
|
|
3952
|
-
|
|
3953
4749
|
`);
|
|
4750
|
+
} else {
|
|
4751
|
+
const otherCount = Object.keys(entries).length - names.length;
|
|
4752
|
+
if (otherCount > 0) {
|
|
4753
|
+
output.write(paint.dim(` ${otherCount} other MCP server(s) in this file are not managed here.
|
|
4754
|
+
`));
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
output.write("\n");
|
|
4758
|
+
const items = [];
|
|
4759
|
+
if (names.length > 0) {
|
|
4760
|
+
items.push(...buildHostItems(names, entries));
|
|
4761
|
+
items.push({ separator: true });
|
|
3954
4762
|
}
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
menu = " a) add e) edit r) remove c) check p) print t) switch client q) quit";
|
|
3958
|
-
} else if (readable == true) {
|
|
3959
|
-
menu = " a) add t) switch client q) quit";
|
|
4763
|
+
if (readError == null) {
|
|
4764
|
+
items.push({ value: "add", key: "a", label: "Add a host" });
|
|
3960
4765
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4766
|
+
const otherTargets = targets.filter((candidate) => candidate.id !== target.id);
|
|
4767
|
+
if (otherTargets.length > 0) {
|
|
4768
|
+
const detectedOthers = otherTargets.filter((candidate) => candidate.detected == true).map((candidate) => candidate.label);
|
|
4769
|
+
let switchHint = "";
|
|
4770
|
+
if (detectedOthers.length > 0) {
|
|
4771
|
+
switchHint = `also found: ${detectedOthers.join(", ")}`;
|
|
4772
|
+
}
|
|
4773
|
+
items.push({ value: "switch", key: "t", label: "Switch client", hint: switchHint });
|
|
4774
|
+
}
|
|
4775
|
+
items.push({ value: "quit", key: "q", label: "Quit" });
|
|
4776
|
+
let question = "No SSH hosts here yet";
|
|
4777
|
+
if (names.length === 1) {
|
|
4778
|
+
question = "1 SSH host \u2014 pick it to manage it";
|
|
4779
|
+
} else if (names.length > 1) {
|
|
4780
|
+
question = `${names.length} SSH hosts \u2014 pick one to manage it`;
|
|
4781
|
+
}
|
|
4782
|
+
let defaultValue = "add";
|
|
4783
|
+
if (names.length > 0) {
|
|
4784
|
+
defaultValue = `${HOST_VALUE_PREFIX}${names[0]}`;
|
|
4785
|
+
}
|
|
4786
|
+
const picked = await prompter.select(question, items, { defaultValue, cancelValue: "quit" });
|
|
4787
|
+
if (picked === "quit") {
|
|
3965
4788
|
return 0;
|
|
3966
4789
|
}
|
|
3967
|
-
if (
|
|
3968
|
-
const
|
|
4790
|
+
if (picked === "switch") {
|
|
4791
|
+
const switched = await prompter.select(
|
|
3969
4792
|
"Which client?",
|
|
3970
4793
|
targets.map((candidate) => {
|
|
3971
|
-
let
|
|
3972
|
-
if (candidate.detected ==
|
|
3973
|
-
|
|
4794
|
+
let hint = candidate.location;
|
|
4795
|
+
if (candidate.detected == false) {
|
|
4796
|
+
hint += " \xB7 not found on this machine";
|
|
3974
4797
|
}
|
|
3975
|
-
return {
|
|
4798
|
+
return { value: candidate.id, label: candidate.label, hint };
|
|
3976
4799
|
}),
|
|
3977
|
-
target.id
|
|
4800
|
+
{ defaultValue: target.id, cancelValue: target.id }
|
|
3978
4801
|
);
|
|
3979
|
-
target = targets.find((candidate) => candidate.id ===
|
|
3980
|
-
continue;
|
|
3981
|
-
}
|
|
3982
|
-
if (readable == false) {
|
|
3983
|
-
output.write(" Not available while the file cannot be read.\n");
|
|
4802
|
+
target = targets.find((candidate) => candidate.id === switched) ?? target;
|
|
3984
4803
|
continue;
|
|
3985
4804
|
}
|
|
3986
4805
|
try {
|
|
3987
|
-
if (
|
|
4806
|
+
if (picked === "add") {
|
|
3988
4807
|
await addOrEditHost(prompter, output, target, entries);
|
|
3989
4808
|
continue;
|
|
3990
4809
|
}
|
|
3991
|
-
if (
|
|
3992
|
-
|
|
3993
|
-
if (name == null) {
|
|
3994
|
-
continue;
|
|
3995
|
-
}
|
|
3996
|
-
if (action === "e") {
|
|
3997
|
-
await addOrEditHost(prompter, output, target, entries, name);
|
|
3998
|
-
} else if (action === "r") {
|
|
3999
|
-
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4000
|
-
if (remove == true) {
|
|
4001
|
-
reportWrite(output, target.removeEntry(name), "Removed");
|
|
4002
|
-
}
|
|
4003
|
-
} else if (action === "c") {
|
|
4004
|
-
await checkHost(prompter, output, target, name, entries[name]);
|
|
4005
|
-
} else {
|
|
4006
|
-
printHost(output, name, entries[name]);
|
|
4007
|
-
}
|
|
4008
|
-
continue;
|
|
4810
|
+
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
4811
|
+
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
4009
4812
|
}
|
|
4010
|
-
output.write(` Unknown choice "${action}".
|
|
4011
|
-
`);
|
|
4012
4813
|
} catch (ex) {
|
|
4013
4814
|
if (ex instanceof PromptClosedError) {
|
|
4014
4815
|
throw ex;
|
|
@@ -4457,7 +5258,7 @@ ${file.content}`);
|
|
|
4457
5258
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4458
5259
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4459
5260
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
4460
|
-
if (
|
|
5261
|
+
if (fs6__default.default.existsSync(resolvedLocalPath) == false) {
|
|
4461
5262
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
4462
5263
|
}
|
|
4463
5264
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -4497,7 +5298,7 @@ ${file.content}`);
|
|
|
4497
5298
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4498
5299
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4499
5300
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
4500
|
-
|
|
5301
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
|
|
4501
5302
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
4502
5303
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
4503
5304
|
});
|