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