@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.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 fs7 = require('fs');
6
- var os4 = require('os');
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 fs7__default = /*#__PURE__*/_interopDefault(fs7);
20
- var os4__default = /*#__PURE__*/_interopDefault(os4);
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.0";
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 (fs7__default.default.existsSync(candidate) == true && fs7__default.default.statSync(candidate).isFile() == true) {
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 = os4__default.default.homedir();
623
+ expanded = os5__default.default.homedir();
624
624
  } else if (inputPath.startsWith("~/") == true || inputPath.startsWith("~\\") == true) {
625
- expanded = path__default.default.join(os4__default.default.homedir(), inputPath.slice(2));
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 (fs7__default.default.existsSync(expandedKeyPath) == false) {
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 (fs7__default.default.existsSync(profile.jump.privateKeyPath) == false) {
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 fs7__default.default.realpathSync.native(inputPath);
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 = fs7__default.default.realpathSync.native(path__default.default.dirname(inputPath));
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 = fs7__default.default.readFileSync(keyPath);
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 = fs7__default.default.statSync(localPath);
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 = fs7__default.default.statSync(localPath);
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.waiter = null;
3404
+ this.lineWaiter = null;
2644
3405
  this.inputEnded = false;
2645
- this.secretMode = false;
2646
- this.secretBuffer = "";
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.waiter != null) {
2654
- const waiter = this.waiter;
2655
- this.waiter = null;
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 = ` [${defaultValue}]`;
3462
+ suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
2676
3463
  }
2677
- this.output.write(`${question}${suffix}: `);
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
- const trimmed = line.trim();
2683
- if (trimmed === "" && defaultValue != null) {
2684
- return defaultValue;
3470
+ let answer = line.trim();
3471
+ if (answer === "" && defaultValue != null) {
3472
+ answer = defaultValue;
2685
3473
  }
2686
- return trimmed;
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(" A value is required.\n");
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 = "[y/N]";
3489
+ let hint = "y/N";
2700
3490
  if (defaultValue == true) {
2701
- hint = "[Y/n]";
3491
+ hint = "Y/n";
2702
3492
  }
2703
3493
  for (; ; ) {
2704
- this.output.write(`${question} ${hint}: `);
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
- return defaultValue;
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 (normalized === "y" || normalized === "yes") {
2714
- return true;
2715
- }
2716
- if (normalized === "n" || normalized === "no") {
2717
- return false;
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(" Answer y or n.\n");
3517
+ this.output.write(` ${this.style.dim("Answer y or n.")}
3518
+ `);
2720
3519
  }
2721
3520
  }
2722
- /** Lists the choices one per line and accepts a key, case-insensitively. */
2723
- async askChoice(question, choices, defaultKey) {
2724
- this.output.write(`${question}
2725
- `);
2726
- for (const choice of choices) {
2727
- this.output.write(` ${choice.key}) ${choice.label}
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
- for (; ; ) {
2731
- const answer = await this.ask("Choice", defaultKey);
2732
- const matched = choices.find((choice) => choice.key.toLowerCase() === answer.toLowerCase());
2733
- if (matched != null) {
2734
- return matched.key;
2735
- }
2736
- this.output.write(` Pick one of ${choices.map((choice) => choice.key).join(", ")}.
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
- const rawModeStream = this.input;
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.output.write(`${question}: `);
2751
- this.secretBuffer = "";
2752
- this.secretMode = true;
2753
- rawModeStream.setRawMode(true);
2754
- try {
2755
- const line = await this.readLine();
2756
- if (line == null) {
2757
- throw new PromptClosedError();
2758
- }
2759
- return line;
2760
- } finally {
2761
- rawModeStream.setRawMode(false);
2762
- this.secretMode = false;
2763
- this.output.write("\n");
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.waiter = resolve;
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.secretMode == true) {
2789
- for (const character of chunk) {
2790
- if (character === "\r" || character === "\n") {
2791
- const secret = this.secretBuffer;
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.secretBuffer += character;
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.deliver(line);
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
- var DEFAULT_KEY_CANDIDATES = ["~/.ssh/id_ed25519", "~/.ssh/id_ecdsa", "~/.ssh/id_rsa"];
2819
- var CLEAR_ANSWER = "-";
2820
- async function askPort(prompter, question, current) {
2821
- for (; ; ) {
2822
- const answer = await prompter.ask(question, String(current));
2823
- const parsed = Number(answer);
2824
- if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
2825
- return parsed;
2826
- }
2827
- prompter.write(" Enter a port number between 1 and 65535.\n");
2828
- }
2829
- }
2830
- async function askOptional(prompter, question, current) {
2831
- const answer = await prompter.ask(question, current);
2832
- if (answer === "" || answer === CLEAR_ANSWER) {
2833
- return void 0;
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
- keyPath = await prompter.askRequired("Private key path", defaultKeyPath);
2888
- if (fs7__default.default.existsSync(expandHomePath(keyPath)) == true) {
2889
- break;
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
- prompter.write(` ${expandHomePath(keyPath)} does not exist.
3674
+ this.output.write(` Pick a number from 1 to ${choices.length}.
2892
3675
  `);
2893
- const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
2894
- if (useAnyway == true) {
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
- passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
3709
+ return line;
2899
3710
  }
2900
- if (auth === "password") {
2901
- prompter.write(" The password is written into the config file as plain text. A key file is safer.\n");
2902
- password = await askOptionalSecret(prompter, "Password", existing?.password);
2903
- if (password == null) {
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
- let jumpHost;
2908
- let jumpPort;
2909
- let jumpUser;
2910
- const viaJump = await prompter.askYesNo("Reach it through a jump host (ssh -J)?", existing?.jumpHost != null);
2911
- if (viaJump == true) {
2912
- jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
2913
- jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
2914
- jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
2915
- }
2916
- let proxyCommand;
2917
- const viaProxy = await prompter.askYesNo("Start a proxy command for the first hop (e.g. cloudflared)?", existing?.proxyCommand != null);
2918
- if (viaProxy == true) {
2919
- prompter.write(" %h / %p / %r expand to the first hop's host, port and user.\n");
2920
- proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
2921
- }
2922
- const description = await askOptional(prompter, "Description shown to the agent (what the host is for)", existing?.description);
2923
- const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
2924
- let currentAllowedPaths;
2925
- if (existing != null && existing.allowedPaths.length > 0) {
2926
- currentAllowedPaths = existing.allowedPaths.join(",");
2927
- }
2928
- let allowedPaths = [];
2929
- const allowedPathsAnswer = await askOptional(
2930
- prompter,
2931
- "Restrict file tools to these path prefixes (comma-separated, empty = unrestricted)",
2932
- currentAllowedPaths
2933
- );
2934
- if (allowedPathsAnswer != null) {
2935
- allowedPaths = allowedPathsAnswer.split(",").map((item) => item.trim()).filter((item) => item !== "");
2936
- }
2937
- let resolvedAuth = auth;
2938
- if (auth === "password" && password == null) {
2939
- resolvedAuth = "agent";
2940
- }
2941
- const answers = {
2942
- name,
2943
- host,
2944
- port,
2945
- username,
2946
- auth: resolvedAuth,
2947
- keyPath,
2948
- passphrase,
2949
- password,
2950
- description,
2951
- jumpHost,
2952
- jumpPort,
2953
- jumpUser,
2954
- proxyCommand,
2955
- readonly,
2956
- allowedPaths,
2957
- extraEnv: existing?.extraEnv ?? {}
2958
- };
2959
- if (existing != null && existing.host === host && existing.port === port) {
2960
- answers.hostKeyFingerprint = existing.hostKeyFingerprint;
2961
- }
2962
- if (existing != null && existing.jumpHost === jumpHost && existing.jumpPort === jumpPort) {
2963
- answers.jumpHostKeyFingerprint = existing.jumpHostKeyFingerprint;
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
- return answers;
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 (fs7__default.default.existsSync(filePath) == false) {
3888
+ if (fs6__default.default.existsSync(filePath) == false) {
3008
3889
  return {};
3009
3890
  }
3010
- const text = fs7__default.default.readFileSync(filePath, "utf8");
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
- fs7__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
3033
- fs7__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
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: fs7__default.default.existsSync(filePath),
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(os4__default.default.homedir(), ".claude.json");
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 (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
3997
+ if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
3117
3998
  return {};
3118
3999
  }
3119
- const text = fs7__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
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 || fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
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(os4__default.default.homedir(), ".codex", "config.toml");
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 (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4474
+ if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
3594
4475
  return "";
3595
4476
  }
3596
- return fs7__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4477
+ return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
3597
4478
  }
3598
4479
  function writeText(text) {
3599
- fs7__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
3600
- fs7__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
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 || fs7__default.default.existsSync(CODEX_CONFIG_PATH) == true,
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(os4__default.default.homedir(), "AppData", "Roaming");
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(os4__default.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
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(os4__default.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
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(os4__default.default.homedir(), ".cursor", "mcp.json") })
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(output, result, action) {
4564
+ function reportWrite(prompter, result, action) {
3683
4565
  if (result.status === "written") {
3684
- output.write(` ${action} \u2014 ${result.detail}
4566
+ prompter.write(` ${prompter.paint.success(action)} ${prompter.paint.dim(result.detail)}
3685
4567
  `);
3686
- output.write(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n");
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
- output.write(` Not ${action.toLowerCase()}: ${result.detail}
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 asked = await askHostQuestions(prompter, existing);
3785
- output.write("\n");
3786
- const answers = await verifyAnswers(prompter, output, asked);
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. The print view (p) gives the same entry ready to paste.\n\n");
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(output, written, "Written");
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 = profileFromEntry(output, entry);
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(`Pin the host key ${result.hostKeyFingerprint}?`, true);
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(`Pin the jump host key ${result.jumpHostKeyFingerprint}?`, true);
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(output, written, "Pinned");
4643
+ reportWrite(prompter, written, "Pinned");
3853
4644
  }
3854
4645
  }
3855
- function printHost(output, name, entry) {
3856
- output.write('\n JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):\n\n');
3857
- output.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
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
- output.write(" TOML \u2014 Codex (~/.codex/config.toml):\n\n");
3861
- output.write(`${indent(renderMcpServerTable(name, entry))}
4651
+ prompter.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
3862
4652
 
3863
4653
  `);
3864
- output.write(" Claude Code CLI (bash / zsh / Git Bash):\n\n");
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
- async function pickHost(prompter, output, names) {
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
- if (names.length === 0) {
3901
- output.write(" No SSH hosts registered here yet.\n");
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 (otherCount > 0) {
3916
- output.write(`
3917
- ${otherCount} other MCP server(s) in this file are not managed here.
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 readable = true;
4736
+ let readError = null;
3942
4737
  try {
3943
4738
  entries = target.readEntries();
3944
- names = renderHostList(output, target, entries);
4739
+ names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
3945
4740
  } catch (ex) {
3946
- readable = false;
3947
- output.write(`
3948
- ${target.label} \u2014 ${target.location}
3949
-
3950
- Cannot read it: ${errorMessage(ex)}
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
- let menu = " t) switch client q) quit";
3956
- if (readable == true && names.length > 0) {
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
- output.write(`${menu}
3962
- `);
3963
- const action = (await prompter.ask("Choice")).toLowerCase();
3964
- if (action === "q" || action === "quit") {
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 (action === "t") {
3968
- const picked = await prompter.askChoice(
4790
+ if (picked === "switch") {
4791
+ const switched = await prompter.select(
3969
4792
  "Which client?",
3970
4793
  targets.map((candidate) => {
3971
- let mark = "";
3972
- if (candidate.detected == true) {
3973
- mark = " (detected)";
4794
+ let hint = candidate.location;
4795
+ if (candidate.detected == false) {
4796
+ hint += " \xB7 not found on this machine";
3974
4797
  }
3975
- return { key: candidate.id, label: `${candidate.label} \u2014 ${candidate.location}${mark}` };
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 === picked) ?? target;
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 (action === "a") {
4806
+ if (picked === "add") {
3988
4807
  await addOrEditHost(prompter, output, target, entries);
3989
4808
  continue;
3990
4809
  }
3991
- if (action === "e" || action === "r" || action === "c" || action === "p") {
3992
- const name = await pickHost(prompter, output, names);
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 (fs7__default.default.existsSync(resolvedLocalPath) == false) {
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
- fs7__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
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
  });