@akms/mcp-ssh 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +262 -262
- package/lib/cli.cjs +786 -317
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +786 -317
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -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.
|
|
29
|
+
var SERVER_VERSION = "0.1.3";
|
|
30
30
|
|
|
31
31
|
// src/_defs/command-rules.ts
|
|
32
32
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -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}`;
|
|
@@ -2625,15 +2642,25 @@ var PromptClosedError = class extends Error {
|
|
|
2625
2642
|
this.name = "PromptClosedError";
|
|
2626
2643
|
}
|
|
2627
2644
|
};
|
|
2645
|
+
var PromptBackError = class extends Error {
|
|
2646
|
+
constructor() {
|
|
2647
|
+
super("prompt left with esc");
|
|
2648
|
+
this.name = "PromptBackError";
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2628
2651
|
var ESC = "\x1B";
|
|
2629
|
-
var KEY_UP = [`${ESC}[A`, `${ESC}OA
|
|
2630
|
-
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB
|
|
2631
|
-
var
|
|
2632
|
-
var
|
|
2652
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
2653
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
2654
|
+
var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
|
|
2655
|
+
var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
|
|
2656
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
2657
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
2658
|
+
var KEY_DELETE = [`${ESC}[3~`];
|
|
2633
2659
|
var KEY_ENTER = ["\r", "\n"];
|
|
2634
2660
|
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2635
2661
|
var KEY_CTRL_C = "";
|
|
2636
2662
|
var KEY_CTRL_D = "";
|
|
2663
|
+
var KEY_CLEAR_LINE = "";
|
|
2637
2664
|
function splitKeys(chunk) {
|
|
2638
2665
|
const keys = [];
|
|
2639
2666
|
let index = 0;
|
|
@@ -2655,6 +2682,13 @@ function splitKeys(chunk) {
|
|
|
2655
2682
|
}
|
|
2656
2683
|
return keys;
|
|
2657
2684
|
}
|
|
2685
|
+
function isPrintableKey(key) {
|
|
2686
|
+
if (key.startsWith(ESC) == true) {
|
|
2687
|
+
return false;
|
|
2688
|
+
}
|
|
2689
|
+
const codePoint = key.codePointAt(0) ?? 0;
|
|
2690
|
+
return codePoint >= 32 && codePoint !== 127;
|
|
2691
|
+
}
|
|
2658
2692
|
function charWidth(codePoint) {
|
|
2659
2693
|
if (codePoint < 4352) {
|
|
2660
2694
|
return 1;
|
|
@@ -2729,8 +2763,14 @@ var Prompter = class {
|
|
|
2729
2763
|
this.pendingLines = [];
|
|
2730
2764
|
this.lineWaiter = null;
|
|
2731
2765
|
this.inputEnded = false;
|
|
2732
|
-
/** Set while a key-driven prompt
|
|
2766
|
+
/** Set while a key-driven prompt owns the input. */
|
|
2733
2767
|
this.keyHandler = null;
|
|
2768
|
+
/**
|
|
2769
|
+
* When true, an answered prompt leaves nothing behind on a TTY instead of its
|
|
2770
|
+
* `✔ question · answer` line. The settings form sets it: the form itself shows every
|
|
2771
|
+
* value, so it can redraw in place rather than scroll a trail of edits.
|
|
2772
|
+
*/
|
|
2773
|
+
this.transient = false;
|
|
2734
2774
|
const ttyInput = input;
|
|
2735
2775
|
const ttyOutput = output;
|
|
2736
2776
|
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
@@ -2774,6 +2814,7 @@ var Prompter = class {
|
|
|
2774
2814
|
/**
|
|
2775
2815
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2776
2816
|
*
|
|
2817
|
+
* @throws {PromptBackError} On Esc.
|
|
2777
2818
|
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2778
2819
|
*/
|
|
2779
2820
|
async ask(question, defaultValue) {
|
|
@@ -2782,16 +2823,16 @@ var Prompter = class {
|
|
|
2782
2823
|
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2783
2824
|
}
|
|
2784
2825
|
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
2785
|
-
this.
|
|
2786
|
-
|
|
2787
|
-
if (line == null) {
|
|
2788
|
-
throw new PromptClosedError();
|
|
2789
|
-
}
|
|
2790
|
-
let answer = line.trim();
|
|
2826
|
+
const typed = await this.readText(prompt, false);
|
|
2827
|
+
let answer = typed.trim();
|
|
2791
2828
|
if (answer === "" && defaultValue != null) {
|
|
2792
2829
|
answer = defaultValue;
|
|
2793
2830
|
}
|
|
2794
|
-
|
|
2831
|
+
let shown = answer;
|
|
2832
|
+
if (shown === "") {
|
|
2833
|
+
shown = this.style.dim("(empty)");
|
|
2834
|
+
}
|
|
2835
|
+
this.writeAnswered(question, shown);
|
|
2795
2836
|
return answer;
|
|
2796
2837
|
}
|
|
2797
2838
|
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
@@ -2805,6 +2846,10 @@ var Prompter = class {
|
|
|
2805
2846
|
`);
|
|
2806
2847
|
}
|
|
2807
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* @throws {PromptBackError} On Esc — the caller decides what "neither" means.
|
|
2851
|
+
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2852
|
+
*/
|
|
2808
2853
|
async askYesNo(question, defaultValue) {
|
|
2809
2854
|
let hint = "y/N";
|
|
2810
2855
|
if (defaultValue == true) {
|
|
@@ -2812,12 +2857,8 @@ var Prompter = class {
|
|
|
2812
2857
|
}
|
|
2813
2858
|
for (; ; ) {
|
|
2814
2859
|
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
2815
|
-
this.
|
|
2816
|
-
const
|
|
2817
|
-
if (line == null) {
|
|
2818
|
-
throw new PromptClosedError();
|
|
2819
|
-
}
|
|
2820
|
-
const normalized = line.trim().toLowerCase();
|
|
2860
|
+
const typed = await this.readText(prompt, false);
|
|
2861
|
+
const normalized = typed.trim().toLowerCase();
|
|
2821
2862
|
let answer = null;
|
|
2822
2863
|
if (normalized === "") {
|
|
2823
2864
|
answer = defaultValue;
|
|
@@ -2831,7 +2872,7 @@ var Prompter = class {
|
|
|
2831
2872
|
if (answer == true) {
|
|
2832
2873
|
shown = "yes";
|
|
2833
2874
|
}
|
|
2834
|
-
this.
|
|
2875
|
+
this.writeAnswered(question, shown);
|
|
2835
2876
|
return answer;
|
|
2836
2877
|
}
|
|
2837
2878
|
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
@@ -2839,9 +2880,10 @@ var Prompter = class {
|
|
|
2839
2880
|
}
|
|
2840
2881
|
}
|
|
2841
2882
|
/**
|
|
2842
|
-
* Picks one of `items`. On a TTY: ↑/↓
|
|
2843
|
-
*
|
|
2883
|
+
* Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
|
|
2884
|
+
* TTY: a numbered list, answered by number, key or label.
|
|
2844
2885
|
*
|
|
2886
|
+
* @throws {PromptBackError} On Esc without a `cancelValue`.
|
|
2845
2887
|
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2846
2888
|
*/
|
|
2847
2889
|
async select(question, items, options) {
|
|
@@ -2853,63 +2895,62 @@ var Prompter = class {
|
|
|
2853
2895
|
if (startIndex < 0) {
|
|
2854
2896
|
startIndex = items.findIndex((item) => "value" in item);
|
|
2855
2897
|
}
|
|
2898
|
+
let picked;
|
|
2856
2899
|
if (this.interactive == true) {
|
|
2857
|
-
|
|
2858
|
-
|
|
2900
|
+
picked = await this.selectWithKeys(question, items, startIndex);
|
|
2901
|
+
} else {
|
|
2902
|
+
picked = await this.selectWithLines(question, items, startIndex);
|
|
2859
2903
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2904
|
+
if (picked != null) {
|
|
2905
|
+
return picked;
|
|
2906
|
+
}
|
|
2907
|
+
if (options?.cancelValue != null) {
|
|
2908
|
+
return options.cancelValue;
|
|
2909
|
+
}
|
|
2910
|
+
throw new PromptBackError();
|
|
2862
2911
|
}
|
|
2863
2912
|
/**
|
|
2864
2913
|
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2865
2914
|
* caller says a value already exists.
|
|
2866
2915
|
*
|
|
2916
|
+
* @throws {PromptBackError} On Esc.
|
|
2867
2917
|
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2868
2918
|
*/
|
|
2869
2919
|
async askSecret(question) {
|
|
2920
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
2921
|
+
const secret = await this.readText(prompt, true);
|
|
2922
|
+
let shown = this.style.dim("(empty)");
|
|
2923
|
+
if (secret !== "") {
|
|
2924
|
+
shown = "\u2022".repeat(Math.min(secret.length, 8));
|
|
2925
|
+
}
|
|
2926
|
+
this.writeAnswered(question, shown);
|
|
2927
|
+
return secret;
|
|
2928
|
+
}
|
|
2929
|
+
// ── input ──────────────────────────────────────────────────────────────
|
|
2930
|
+
/**
|
|
2931
|
+
* One text answer, through the line editor on a TTY and a line read off one. The prompt
|
|
2932
|
+
* is erased once answered; the caller writes the summary line.
|
|
2933
|
+
*
|
|
2934
|
+
* @throws {PromptBackError} On Esc, or a line of just `ESC` off a TTY.
|
|
2935
|
+
*/
|
|
2936
|
+
async readText(prompt, secret) {
|
|
2870
2937
|
if (this.interactive == false) {
|
|
2871
|
-
|
|
2872
|
-
|
|
2938
|
+
this.output.write(prompt);
|
|
2939
|
+
const line = await this.readLine();
|
|
2940
|
+
if (line == null) {
|
|
2941
|
+
throw new PromptClosedError();
|
|
2942
|
+
}
|
|
2943
|
+
if (line === ESC) {
|
|
2944
|
+
throw new PromptBackError();
|
|
2945
|
+
}
|
|
2946
|
+
return line;
|
|
2873
2947
|
}
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
const finish = () => {
|
|
2880
|
-
this.keyHandler = null;
|
|
2881
|
-
ttyInput.setRawMode?.(false);
|
|
2882
|
-
this.output.write(`\r${ESC}[2K`);
|
|
2883
|
-
};
|
|
2884
|
-
this.keyHandler = (key) => {
|
|
2885
|
-
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
2886
|
-
finish();
|
|
2887
|
-
reject(new PromptClosedError());
|
|
2888
|
-
return;
|
|
2889
|
-
}
|
|
2890
|
-
if (KEY_ENTER.includes(key) == true) {
|
|
2891
|
-
finish();
|
|
2892
|
-
let shown = this.style.dim("(empty)");
|
|
2893
|
-
if (buffer !== "") {
|
|
2894
|
-
shown = "\u2022".repeat(Math.min(buffer.length, 8));
|
|
2895
|
-
}
|
|
2896
|
-
this.writeAnswered(question, shown);
|
|
2897
|
-
resolve(buffer);
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
if (KEY_BACKSPACE.includes(key) == true) {
|
|
2901
|
-
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
2902
|
-
return;
|
|
2903
|
-
}
|
|
2904
|
-
if (key.startsWith(ESC) == true) {
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
buffer += key;
|
|
2908
|
-
};
|
|
2909
|
-
ttyInput.setRawMode?.(true);
|
|
2910
|
-
});
|
|
2948
|
+
const result = await this.editLine(prompt, secret);
|
|
2949
|
+
if (result.kind === "back") {
|
|
2950
|
+
throw new PromptBackError();
|
|
2951
|
+
}
|
|
2952
|
+
return result.text;
|
|
2911
2953
|
}
|
|
2912
|
-
// ── line mode ──────────────────────────────────────────────────────────
|
|
2913
2954
|
readLine() {
|
|
2914
2955
|
const queued = this.pendingLines.shift();
|
|
2915
2956
|
if (queued != null) {
|
|
@@ -2947,6 +2988,78 @@ var Prompter = class {
|
|
|
2947
2988
|
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2948
2989
|
}
|
|
2949
2990
|
}
|
|
2991
|
+
/**
|
|
2992
|
+
* A one-line editor in raw mode: insert at the cursor, Backspace / Delete, ←/→, Home /
|
|
2993
|
+
* End, Ctrl+U clears, Enter answers, Esc goes back. Secrets draw nothing after the
|
|
2994
|
+
* prompt. Cooked mode is not used for text on a TTY because it never reports Esc: the
|
|
2995
|
+
* terminal swallows it or stuffs the `ESC` byte into the value.
|
|
2996
|
+
*/
|
|
2997
|
+
editLine(prompt, secret) {
|
|
2998
|
+
const ttyInput = this.input;
|
|
2999
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3000
|
+
let characters = [];
|
|
3001
|
+
let cursor = 0;
|
|
3002
|
+
const draw = () => {
|
|
3003
|
+
let text = `\r${ESC}[2K${prompt}`;
|
|
3004
|
+
if (secret == false) {
|
|
3005
|
+
text += characters.join("");
|
|
3006
|
+
const column = displayWidth(plainPrompt) + displayWidth(characters.slice(0, cursor).join(""));
|
|
3007
|
+
text += `\r${ESC}[${column}C`;
|
|
3008
|
+
}
|
|
3009
|
+
this.output.write(text);
|
|
3010
|
+
};
|
|
3011
|
+
return new Promise((resolve, reject) => {
|
|
3012
|
+
const finish = () => {
|
|
3013
|
+
this.keyHandler = null;
|
|
3014
|
+
ttyInput.setRawMode?.(false);
|
|
3015
|
+
this.output.write(`\r${ESC}[2K`);
|
|
3016
|
+
};
|
|
3017
|
+
this.keyHandler = (key) => {
|
|
3018
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3019
|
+
finish();
|
|
3020
|
+
reject(new PromptClosedError());
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3024
|
+
finish();
|
|
3025
|
+
resolve({ kind: "entered", text: characters.join("") });
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (key === ESC) {
|
|
3029
|
+
finish();
|
|
3030
|
+
resolve({ kind: "back" });
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (KEY_BACKSPACE.includes(key) == true) {
|
|
3034
|
+
if (cursor > 0) {
|
|
3035
|
+
characters.splice(cursor - 1, 1);
|
|
3036
|
+
cursor -= 1;
|
|
3037
|
+
}
|
|
3038
|
+
} else if (KEY_DELETE.includes(key) == true) {
|
|
3039
|
+
characters.splice(cursor, 1);
|
|
3040
|
+
} else if (KEY_LEFT.includes(key) == true) {
|
|
3041
|
+
cursor = Math.max(0, cursor - 1);
|
|
3042
|
+
} else if (KEY_RIGHT.includes(key) == true) {
|
|
3043
|
+
cursor = Math.min(characters.length, cursor + 1);
|
|
3044
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3045
|
+
cursor = 0;
|
|
3046
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3047
|
+
cursor = characters.length;
|
|
3048
|
+
} else if (key === KEY_CLEAR_LINE) {
|
|
3049
|
+
characters = [];
|
|
3050
|
+
cursor = 0;
|
|
3051
|
+
} else if (isPrintableKey(key) == true) {
|
|
3052
|
+
characters.splice(cursor, 0, key);
|
|
3053
|
+
cursor += 1;
|
|
3054
|
+
} else {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
draw();
|
|
3058
|
+
};
|
|
3059
|
+
ttyInput.setRawMode?.(true);
|
|
3060
|
+
draw();
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
2950
3063
|
async selectWithLines(question, items, startIndex) {
|
|
2951
3064
|
const choices = items.filter((item) => "value" in item);
|
|
2952
3065
|
const labelWidths = groupLabelWidths(items);
|
|
@@ -2975,6 +3088,9 @@ var Prompter = class {
|
|
|
2975
3088
|
if (line == null) {
|
|
2976
3089
|
throw new PromptClosedError();
|
|
2977
3090
|
}
|
|
3091
|
+
if (line === ESC) {
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
2978
3094
|
const answer = line.trim();
|
|
2979
3095
|
if (answer === "") {
|
|
2980
3096
|
return defaultChoice.value;
|
|
@@ -3030,28 +3146,14 @@ var Prompter = class {
|
|
|
3030
3146
|
}
|
|
3031
3147
|
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3032
3148
|
writeAnswered(question, answer) {
|
|
3033
|
-
|
|
3034
|
-
`);
|
|
3035
|
-
}
|
|
3036
|
-
/**
|
|
3037
|
-
* Replaces a cooked-mode prompt and its echoed answer with the one-line record. The
|
|
3038
|
-
* rows are worked out from the widths, since a long answer wraps.
|
|
3039
|
-
*/
|
|
3040
|
-
collapseAnswered(prompt, typed, question, answer) {
|
|
3041
|
-
if (this.interactive == false) {
|
|
3149
|
+
if (this.interactive == false || this.transient == true) {
|
|
3042
3150
|
return;
|
|
3043
3151
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
const rows = Math.max(1, Math.ceil(used / this.columns()));
|
|
3047
|
-
this.output.write(`${ESC}[${rows}A\r${ESC}[J`);
|
|
3048
|
-
let shown = answer;
|
|
3049
|
-
if (shown === "") {
|
|
3050
|
-
shown = this.style.dim("(empty)");
|
|
3051
|
-
}
|
|
3052
|
-
this.writeAnswered(question, shown);
|
|
3152
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
3153
|
+
`);
|
|
3053
3154
|
}
|
|
3054
|
-
|
|
3155
|
+
/** @returns The picked value, or null on Esc. */
|
|
3156
|
+
selectWithKeys(question, items, startIndex) {
|
|
3055
3157
|
const ttyInput = this.input;
|
|
3056
3158
|
const selectableIndexes = items.flatMap((item, index) => {
|
|
3057
3159
|
if ("value" in item) {
|
|
@@ -3101,11 +3203,7 @@ var Prompter = class {
|
|
|
3101
3203
|
}
|
|
3102
3204
|
lines.push(this.fitLine(parts, width));
|
|
3103
3205
|
}
|
|
3104
|
-
|
|
3105
|
-
if (cancelValue != null) {
|
|
3106
|
-
footer += " \xB7 esc back";
|
|
3107
|
-
}
|
|
3108
|
-
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3206
|
+
lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
|
|
3109
3207
|
return lines;
|
|
3110
3208
|
};
|
|
3111
3209
|
const move = (step) => {
|
|
@@ -3134,11 +3232,8 @@ var Prompter = class {
|
|
|
3134
3232
|
return;
|
|
3135
3233
|
}
|
|
3136
3234
|
if (key === ESC) {
|
|
3137
|
-
if (cancelValue == null) {
|
|
3138
|
-
return;
|
|
3139
|
-
}
|
|
3140
3235
|
finish();
|
|
3141
|
-
resolve(
|
|
3236
|
+
resolve(null);
|
|
3142
3237
|
return;
|
|
3143
3238
|
}
|
|
3144
3239
|
if (KEY_UP.includes(key) == true) {
|
|
@@ -3162,11 +3257,47 @@ var Prompter = class {
|
|
|
3162
3257
|
});
|
|
3163
3258
|
}
|
|
3164
3259
|
};
|
|
3260
|
+
|
|
3261
|
+
// src/modules/manage/host-form.ts
|
|
3165
3262
|
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3166
3263
|
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3167
3264
|
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
3168
3265
|
var CLEAR_ANSWER = "-";
|
|
3169
3266
|
var OTHER_KEY_PATH = "\0other";
|
|
3267
|
+
var FIELD_NAME_WIDTH = 17;
|
|
3268
|
+
var SUB_FIELDS = ["key", "passphrase", "password", "jump-host", "jump-port", "jump-user", "proxy"];
|
|
3269
|
+
var FIELD_NAMES = {
|
|
3270
|
+
"host": "Host",
|
|
3271
|
+
"port": "Port",
|
|
3272
|
+
"user": "User",
|
|
3273
|
+
"name": "Server name",
|
|
3274
|
+
"auth": "Authentication",
|
|
3275
|
+
"key": "Private key",
|
|
3276
|
+
"passphrase": "Passphrase",
|
|
3277
|
+
"password": "Password",
|
|
3278
|
+
"route": "Route",
|
|
3279
|
+
"jump-host": "Jump host",
|
|
3280
|
+
"jump-port": "Jump port",
|
|
3281
|
+
"jump-user": "Jump user",
|
|
3282
|
+
"proxy": "Proxy command",
|
|
3283
|
+
"description": "Description",
|
|
3284
|
+
"readonly": "Read-only",
|
|
3285
|
+
"paths": "File paths",
|
|
3286
|
+
"host-key": "Host key",
|
|
3287
|
+
"jump-host-key": "Jump host key"
|
|
3288
|
+
};
|
|
3289
|
+
var AUTH_LABELS = {
|
|
3290
|
+
key: "Private key file",
|
|
3291
|
+
password: "Password",
|
|
3292
|
+
agent: "SSH agent"
|
|
3293
|
+
};
|
|
3294
|
+
var ROUTE_LABELS = {
|
|
3295
|
+
"direct": "Directly",
|
|
3296
|
+
"jump": "Through a jump host",
|
|
3297
|
+
"proxy": "Through a proxy command",
|
|
3298
|
+
"jump-proxy": "Proxy command, then a jump host"
|
|
3299
|
+
};
|
|
3300
|
+
var REQUIRED_MARK = "(required)";
|
|
3170
3301
|
function findPrivateKeys() {
|
|
3171
3302
|
const sshDirectory = path.join(os5.homedir(), ".ssh");
|
|
3172
3303
|
let names = [];
|
|
@@ -3282,121 +3413,526 @@ async function askKeyPath(prompter, current) {
|
|
|
3282
3413
|
}
|
|
3283
3414
|
}
|
|
3284
3415
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3416
|
+
function routeUsesJump(route) {
|
|
3417
|
+
return route === "jump" || route === "jump-proxy";
|
|
3418
|
+
}
|
|
3419
|
+
function routeUsesProxy(route) {
|
|
3420
|
+
return route === "proxy" || route === "jump-proxy";
|
|
3421
|
+
}
|
|
3422
|
+
function deriveServerName(host) {
|
|
3423
|
+
return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
3424
|
+
}
|
|
3425
|
+
function finalizeAnswers(state) {
|
|
3426
|
+
const answers = { ...state.answers };
|
|
3427
|
+
if (routeUsesJump(state.route) == false) {
|
|
3428
|
+
answers.jumpHost = void 0;
|
|
3429
|
+
answers.jumpPort = void 0;
|
|
3430
|
+
answers.jumpUser = void 0;
|
|
3431
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3290
3432
|
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3433
|
+
if (routeUsesProxy(state.route) == false) {
|
|
3434
|
+
answers.proxyCommand = void 0;
|
|
3435
|
+
}
|
|
3436
|
+
if (answers.auth !== "key") {
|
|
3437
|
+
answers.keyPath = void 0;
|
|
3438
|
+
answers.passphrase = void 0;
|
|
3439
|
+
}
|
|
3440
|
+
if (answers.auth !== "password") {
|
|
3441
|
+
answers.password = void 0;
|
|
3442
|
+
}
|
|
3443
|
+
return answers;
|
|
3444
|
+
}
|
|
3445
|
+
function snapshotOf(state) {
|
|
3446
|
+
return JSON.stringify(finalizeAnswers(state));
|
|
3447
|
+
}
|
|
3448
|
+
function cloneState(state) {
|
|
3449
|
+
return {
|
|
3450
|
+
...state,
|
|
3451
|
+
answers: {
|
|
3452
|
+
...state.answers,
|
|
3453
|
+
allowedPaths: [...state.answers.allowedPaths],
|
|
3454
|
+
extraEnv: { ...state.answers.extraEnv }
|
|
3300
3455
|
}
|
|
3301
|
-
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
function restoreState(state, snapshot) {
|
|
3459
|
+
const restored = cloneState(snapshot);
|
|
3460
|
+
state.answers = restored.answers;
|
|
3461
|
+
state.route = restored.route;
|
|
3462
|
+
state.nameTouched = restored.nameTouched;
|
|
3463
|
+
state.lastTest = restored.lastTest;
|
|
3464
|
+
state.testedSnapshot = restored.testedSnapshot;
|
|
3465
|
+
}
|
|
3466
|
+
function missingFields(state) {
|
|
3467
|
+
const { answers } = state;
|
|
3468
|
+
const missing = [];
|
|
3469
|
+
if (answers.host === "") {
|
|
3470
|
+
missing.push("host");
|
|
3302
3471
|
}
|
|
3303
|
-
|
|
3304
|
-
"
|
|
3305
|
-
[
|
|
3306
|
-
{ value: "key", label: "Private key file", hint: "only the path is stored \u2014 recommended" },
|
|
3307
|
-
{ value: "password", label: "Password", hint: "stored in the config file as plain text" },
|
|
3308
|
-
{ value: "agent", label: "SSH agent", hint: "keys already loaded in ssh-agent" }
|
|
3309
|
-
],
|
|
3310
|
-
{ defaultValue: existing?.auth ?? "key" }
|
|
3311
|
-
);
|
|
3312
|
-
let keyPath;
|
|
3313
|
-
let passphrase;
|
|
3314
|
-
let password;
|
|
3315
|
-
if (auth === "key") {
|
|
3316
|
-
keyPath = await askKeyPath(prompter, existing?.keyPath);
|
|
3317
|
-
passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
|
|
3318
|
-
}
|
|
3319
|
-
if (auth === "password") {
|
|
3320
|
-
password = await askOptionalSecret(prompter, "Password", existing?.password);
|
|
3321
|
-
if (password == null) {
|
|
3322
|
-
prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
let currentRoute = "direct";
|
|
3326
|
-
if (existing?.jumpHost != null && existing.proxyCommand != null) {
|
|
3327
|
-
currentRoute = "jump-proxy";
|
|
3328
|
-
} else if (existing?.jumpHost != null) {
|
|
3329
|
-
currentRoute = "jump";
|
|
3330
|
-
} else if (existing?.proxyCommand != null) {
|
|
3331
|
-
currentRoute = "proxy";
|
|
3332
|
-
}
|
|
3333
|
-
const route = await prompter.select(
|
|
3334
|
-
"How is it reached?",
|
|
3335
|
-
[
|
|
3336
|
-
{ value: "direct", label: "Directly" },
|
|
3337
|
-
{ value: "jump", label: "Through a jump host", hint: "ssh -J" },
|
|
3338
|
-
{ value: "proxy", label: "Through a proxy command", hint: "e.g. cloudflared access ssh" },
|
|
3339
|
-
{ value: "jump-proxy", label: "Proxy command, then a jump host", hint: "cloudflared to the bastion, then on to the host" }
|
|
3340
|
-
],
|
|
3341
|
-
{ defaultValue: currentRoute }
|
|
3342
|
-
);
|
|
3343
|
-
let jumpHost;
|
|
3344
|
-
let jumpPort;
|
|
3345
|
-
let jumpUser;
|
|
3346
|
-
let proxyCommand;
|
|
3347
|
-
if (route === "jump" || route === "jump-proxy") {
|
|
3348
|
-
jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
|
|
3349
|
-
jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3350
|
-
jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
|
|
3351
|
-
}
|
|
3352
|
-
if (route === "proxy" || route === "jump-proxy") {
|
|
3353
|
-
prompter.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3354
|
-
proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3355
|
-
}
|
|
3356
|
-
const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
|
|
3357
|
-
const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
|
|
3358
|
-
let currentAllowedPaths;
|
|
3359
|
-
if (existing != null && existing.allowedPaths.length > 0) {
|
|
3360
|
-
currentAllowedPaths = existing.allowedPaths.join(",");
|
|
3472
|
+
if (answers.username === "") {
|
|
3473
|
+
missing.push("user");
|
|
3361
3474
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
prompter,
|
|
3365
|
-
"Limit file tools to path prefixes (comma-separated, empty = anywhere)",
|
|
3366
|
-
currentAllowedPaths
|
|
3367
|
-
);
|
|
3368
|
-
if (allowedPathsAnswer != null) {
|
|
3369
|
-
allowedPaths = allowedPathsAnswer.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3475
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3476
|
+
missing.push("key");
|
|
3370
3477
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
resolvedAuth = "agent";
|
|
3478
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3479
|
+
missing.push("password");
|
|
3374
3480
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3481
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3482
|
+
missing.push("jump-host");
|
|
3483
|
+
}
|
|
3484
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3485
|
+
missing.push("proxy");
|
|
3486
|
+
}
|
|
3487
|
+
return missing;
|
|
3488
|
+
}
|
|
3489
|
+
function describePin(fingerprint) {
|
|
3490
|
+
if (fingerprint == null) {
|
|
3491
|
+
return "not pinned";
|
|
3492
|
+
}
|
|
3493
|
+
return `pinned \xB7 ${fingerprint.slice(0, 19)}\u2026`;
|
|
3494
|
+
}
|
|
3495
|
+
function valueOrRequired(value) {
|
|
3496
|
+
if (value == null || value === "") {
|
|
3497
|
+
return REQUIRED_MARK;
|
|
3498
|
+
}
|
|
3499
|
+
return value;
|
|
3500
|
+
}
|
|
3501
|
+
function fieldRow(id, value, hint) {
|
|
3502
|
+
let name = FIELD_NAMES[id];
|
|
3503
|
+
if (SUB_FIELDS.includes(id) == true) {
|
|
3504
|
+
name = ` ${name}`;
|
|
3505
|
+
}
|
|
3506
|
+
return {
|
|
3507
|
+
value: id,
|
|
3508
|
+
key: id,
|
|
3509
|
+
label: `${name.padEnd(FIELD_NAME_WIDTH)}${value}`,
|
|
3510
|
+
hint
|
|
3392
3511
|
};
|
|
3393
|
-
|
|
3394
|
-
|
|
3512
|
+
}
|
|
3513
|
+
function buildRows(state, destination) {
|
|
3514
|
+
const { answers } = state;
|
|
3515
|
+
const rows = [];
|
|
3516
|
+
rows.push(fieldRow("host", valueOrRequired(answers.host)));
|
|
3517
|
+
rows.push(fieldRow("port", String(answers.port)));
|
|
3518
|
+
rows.push(fieldRow("user", valueOrRequired(answers.username)));
|
|
3519
|
+
let nameValue = answers.name;
|
|
3520
|
+
let nameHint = "what the agent sees";
|
|
3521
|
+
if (state.nameTouched == false) {
|
|
3522
|
+
nameHint = "follows the host until you change it";
|
|
3523
|
+
}
|
|
3524
|
+
if (nameValue === "") {
|
|
3525
|
+
nameValue = "ssh-<host>";
|
|
3526
|
+
}
|
|
3527
|
+
rows.push(fieldRow("name", nameValue, nameHint));
|
|
3528
|
+
rows.push(fieldRow("auth", AUTH_LABELS[answers.auth]));
|
|
3529
|
+
if (answers.auth === "key") {
|
|
3530
|
+
rows.push(fieldRow("key", valueOrRequired(answers.keyPath)));
|
|
3531
|
+
let passphrase = "none";
|
|
3532
|
+
if (answers.passphrase != null) {
|
|
3533
|
+
passphrase = "set";
|
|
3534
|
+
}
|
|
3535
|
+
rows.push(fieldRow("passphrase", passphrase));
|
|
3536
|
+
}
|
|
3537
|
+
if (answers.auth === "password") {
|
|
3538
|
+
let password = REQUIRED_MARK;
|
|
3539
|
+
if (answers.password != null) {
|
|
3540
|
+
password = "set";
|
|
3541
|
+
}
|
|
3542
|
+
rows.push(fieldRow("password", password, "stored in the config file as plain text"));
|
|
3543
|
+
}
|
|
3544
|
+
rows.push(fieldRow("route", ROUTE_LABELS[state.route]));
|
|
3545
|
+
if (routeUsesJump(state.route) == true) {
|
|
3546
|
+
rows.push(fieldRow("jump-host", valueOrRequired(answers.jumpHost)));
|
|
3547
|
+
rows.push(fieldRow("jump-port", String(answers.jumpPort ?? DEFAULT_SSH_PORT)));
|
|
3548
|
+
let jumpUser = answers.jumpUser;
|
|
3549
|
+
if (jumpUser == null || jumpUser === answers.username) {
|
|
3550
|
+
let loginUser = answers.username;
|
|
3551
|
+
if (loginUser === "") {
|
|
3552
|
+
loginUser = "\u2026";
|
|
3553
|
+
}
|
|
3554
|
+
jumpUser = `same as login (${loginUser})`;
|
|
3555
|
+
}
|
|
3556
|
+
rows.push(fieldRow("jump-user", jumpUser));
|
|
3395
3557
|
}
|
|
3396
|
-
if (
|
|
3397
|
-
answers.
|
|
3558
|
+
if (routeUsesProxy(state.route) == true) {
|
|
3559
|
+
rows.push(fieldRow("proxy", valueOrRequired(answers.proxyCommand), "%h %p %r \u2192 first hop"));
|
|
3560
|
+
}
|
|
3561
|
+
rows.push(fieldRow("description", answers.description ?? "none", "shown to the agent"));
|
|
3562
|
+
let readonly = "no";
|
|
3563
|
+
if (answers.readonly == true) {
|
|
3564
|
+
readonly = "yes";
|
|
3565
|
+
}
|
|
3566
|
+
rows.push(fieldRow("readonly", readonly, "enter toggles"));
|
|
3567
|
+
let paths = "anywhere";
|
|
3568
|
+
if (answers.allowedPaths.length > 0) {
|
|
3569
|
+
paths = answers.allowedPaths.join(", ");
|
|
3570
|
+
}
|
|
3571
|
+
rows.push(fieldRow("paths", paths, "where the file tools may read and write"));
|
|
3572
|
+
rows.push(fieldRow("host-key", describePin(answers.hostKeyFingerprint)));
|
|
3573
|
+
if (routeUsesJump(state.route) == true) {
|
|
3574
|
+
rows.push(fieldRow("jump-host-key", describePin(answers.jumpHostKeyFingerprint)));
|
|
3575
|
+
}
|
|
3576
|
+
rows.push({ separator: true });
|
|
3577
|
+
let testHint = "not run yet";
|
|
3578
|
+
if (state.lastTest === "ok") {
|
|
3579
|
+
testHint = "last run: OK";
|
|
3580
|
+
} else if (state.lastTest === "failed") {
|
|
3581
|
+
testHint = "last run: failed";
|
|
3582
|
+
}
|
|
3583
|
+
rows.push({ value: "test", key: "test", label: "Test the connection", hint: testHint });
|
|
3584
|
+
rows.push({ value: "save", key: "save", label: "Save", hint: `to ${destination}` });
|
|
3585
|
+
rows.push({ value: "cancel", key: "cancel", label: "Cancel" });
|
|
3586
|
+
return rows;
|
|
3587
|
+
}
|
|
3588
|
+
function nextCursor(state, destination, edited) {
|
|
3589
|
+
if (edited === "readonly") {
|
|
3590
|
+
return edited;
|
|
3591
|
+
}
|
|
3592
|
+
const values = buildRows(state, destination).flatMap((row) => {
|
|
3593
|
+
if ("value" in row) {
|
|
3594
|
+
return [row.value];
|
|
3595
|
+
}
|
|
3596
|
+
return [];
|
|
3597
|
+
});
|
|
3598
|
+
let index = values.indexOf(edited) + 1;
|
|
3599
|
+
if (SUB_FIELDS.includes(edited) == false) {
|
|
3600
|
+
while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
|
|
3601
|
+
index += 1;
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
const next = values[index];
|
|
3605
|
+
if (next == null || next === "test") {
|
|
3606
|
+
return "save";
|
|
3607
|
+
}
|
|
3608
|
+
return next;
|
|
3609
|
+
}
|
|
3610
|
+
async function testConnection(prompter, output, state) {
|
|
3611
|
+
const answers = finalizeAnswers(state);
|
|
3612
|
+
output.write("\n");
|
|
3613
|
+
const profile = profileFromEnv(output, answersToEntry(answers).env);
|
|
3614
|
+
if (profile == null) {
|
|
3615
|
+
output.write("\n");
|
|
3616
|
+
state.lastTest = "failed";
|
|
3617
|
+
return false;
|
|
3618
|
+
}
|
|
3619
|
+
const result = await runConnectionCheck(profile, output);
|
|
3620
|
+
output.write("\n");
|
|
3621
|
+
if (result.connected == true) {
|
|
3622
|
+
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3623
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
3624
|
+
if (pin == true) {
|
|
3625
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
if (result.jumpHostKeyFingerprint != null && routeUsesJump(state.route) == true && state.answers.jumpHostKeyFingerprint == null) {
|
|
3629
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3630
|
+
if (pin == true) {
|
|
3631
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
state.lastTest = "ok";
|
|
3635
|
+
state.testedSnapshot = snapshotOf(state);
|
|
3636
|
+
return true;
|
|
3637
|
+
}
|
|
3638
|
+
const hostKeyChanged = result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== state.answers.hostKeyFingerprint;
|
|
3639
|
+
if (hostKeyChanged == true) {
|
|
3640
|
+
output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3641
|
+
const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
|
|
3642
|
+
if (repin == true) {
|
|
3643
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
|
|
3644
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && state.answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== state.answers.jumpHostKeyFingerprint;
|
|
3648
|
+
if (jumpKeyChanged == true) {
|
|
3649
|
+
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3650
|
+
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
3651
|
+
if (repin == true) {
|
|
3652
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint ?? void 0;
|
|
3653
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
state.lastTest = "failed";
|
|
3657
|
+
return false;
|
|
3658
|
+
}
|
|
3659
|
+
async function editField(prompter, output, state, id) {
|
|
3660
|
+
const { answers } = state;
|
|
3661
|
+
if (id === "host") {
|
|
3662
|
+
let current;
|
|
3663
|
+
if (answers.host !== "") {
|
|
3664
|
+
current = answers.host;
|
|
3665
|
+
}
|
|
3666
|
+
answers.host = await prompter.askRequired("Host (hostname or IP)", current);
|
|
3667
|
+
if (state.nameTouched == false) {
|
|
3668
|
+
answers.name = deriveServerName(answers.host);
|
|
3669
|
+
}
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
if (id === "port") {
|
|
3673
|
+
answers.port = await askPort(prompter, "Port", answers.port);
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
if (id === "user") {
|
|
3677
|
+
let current;
|
|
3678
|
+
if (answers.username !== "") {
|
|
3679
|
+
current = answers.username;
|
|
3680
|
+
}
|
|
3681
|
+
answers.username = await prompter.askRequired("Login user", current);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
if (id === "name") {
|
|
3685
|
+
let current;
|
|
3686
|
+
if (answers.name !== "") {
|
|
3687
|
+
current = answers.name;
|
|
3688
|
+
}
|
|
3689
|
+
for (; ; ) {
|
|
3690
|
+
const name = await prompter.askRequired("Server name the agent sees", current);
|
|
3691
|
+
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
3692
|
+
answers.name = name;
|
|
3693
|
+
state.nameTouched = true;
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
if (id === "auth") {
|
|
3700
|
+
answers.auth = await prompter.select(
|
|
3701
|
+
"Authentication",
|
|
3702
|
+
[
|
|
3703
|
+
{ value: "key", label: AUTH_LABELS.key, hint: "only the path is stored \u2014 recommended" },
|
|
3704
|
+
{ value: "password", label: AUTH_LABELS.password, hint: "stored in the config file as plain text" },
|
|
3705
|
+
{ value: "agent", label: AUTH_LABELS.agent, hint: "keys already loaded in ssh-agent" }
|
|
3706
|
+
],
|
|
3707
|
+
{ defaultValue: answers.auth }
|
|
3708
|
+
);
|
|
3709
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3710
|
+
answers.keyPath = await askKeyPath(prompter, void 0);
|
|
3711
|
+
}
|
|
3712
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3713
|
+
answers.password = await askOptionalSecret(prompter, "Password", void 0);
|
|
3714
|
+
}
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
if (id === "key") {
|
|
3718
|
+
answers.keyPath = await askKeyPath(prompter, answers.keyPath);
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
if (id === "passphrase") {
|
|
3722
|
+
answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (id === "password") {
|
|
3726
|
+
answers.password = await askOptionalSecret(prompter, "Password", answers.password);
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (id === "route") {
|
|
3730
|
+
state.route = await prompter.select(
|
|
3731
|
+
"Route",
|
|
3732
|
+
[
|
|
3733
|
+
{ value: "direct", label: ROUTE_LABELS.direct },
|
|
3734
|
+
{ value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
|
|
3735
|
+
{ value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
|
|
3736
|
+
{ value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
|
|
3737
|
+
],
|
|
3738
|
+
{ defaultValue: state.route }
|
|
3739
|
+
);
|
|
3740
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3741
|
+
answers.jumpHost = await prompter.askRequired("Jump host");
|
|
3742
|
+
}
|
|
3743
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3744
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", "cloudflared access ssh --hostname %h");
|
|
3745
|
+
}
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
if (id === "jump-host") {
|
|
3749
|
+
answers.jumpHost = await prompter.askRequired("Jump host", answers.jumpHost);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (id === "jump-port") {
|
|
3753
|
+
answers.jumpPort = await askPort(prompter, "Jump host port", answers.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
if (id === "jump-user") {
|
|
3757
|
+
answers.jumpUser = await askOptional(prompter, "Jump host user (empty = same as the login user)", answers.jumpUser);
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (id === "proxy") {
|
|
3761
|
+
output.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3762
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (id === "description") {
|
|
3766
|
+
answers.description = await askOptional(prompter, "Description shown to the agent", answers.description);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (id === "readonly") {
|
|
3770
|
+
answers.readonly = answers.readonly == false;
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
if (id === "paths") {
|
|
3774
|
+
let current;
|
|
3775
|
+
if (answers.allowedPaths.length > 0) {
|
|
3776
|
+
current = answers.allowedPaths.join(",");
|
|
3777
|
+
}
|
|
3778
|
+
const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
|
|
3779
|
+
answers.allowedPaths = [];
|
|
3780
|
+
if (typed != null) {
|
|
3781
|
+
answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3782
|
+
}
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
const isJump = id === "jump-host-key";
|
|
3786
|
+
let pinned = answers.hostKeyFingerprint;
|
|
3787
|
+
if (isJump == true) {
|
|
3788
|
+
pinned = answers.jumpHostKeyFingerprint;
|
|
3789
|
+
}
|
|
3790
|
+
if (pinned != null) {
|
|
3791
|
+
const unpin = await prompter.askYesNo(`Unpin ${pinned}?`, false);
|
|
3792
|
+
if (unpin == true && isJump == true) {
|
|
3793
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3794
|
+
} else if (unpin == true) {
|
|
3795
|
+
answers.hostKeyFingerprint = void 0;
|
|
3796
|
+
}
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
const missing = missingFields(state);
|
|
3800
|
+
if (missing.length > 0) {
|
|
3801
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
|
|
3802
|
+
`);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
await testConnection(prompter, output, state);
|
|
3806
|
+
}
|
|
3807
|
+
async function runHostForm(prompter, output, options) {
|
|
3808
|
+
const { existing, takenNames, destination } = options;
|
|
3809
|
+
let state;
|
|
3810
|
+
if (existing != null) {
|
|
3811
|
+
let route = "direct";
|
|
3812
|
+
if (existing.jumpHost != null && existing.proxyCommand != null) {
|
|
3813
|
+
route = "jump-proxy";
|
|
3814
|
+
} else if (existing.jumpHost != null) {
|
|
3815
|
+
route = "jump";
|
|
3816
|
+
} else if (existing.proxyCommand != null) {
|
|
3817
|
+
route = "proxy";
|
|
3818
|
+
}
|
|
3819
|
+
state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
|
|
3820
|
+
} else {
|
|
3821
|
+
const foundKeys = findPrivateKeys();
|
|
3822
|
+
state = {
|
|
3823
|
+
answers: {
|
|
3824
|
+
name: "",
|
|
3825
|
+
host: "",
|
|
3826
|
+
port: DEFAULT_SSH_PORT,
|
|
3827
|
+
username: "",
|
|
3828
|
+
auth: "key",
|
|
3829
|
+
keyPath: foundKeys[0]?.path,
|
|
3830
|
+
readonly: false,
|
|
3831
|
+
allowedPaths: [],
|
|
3832
|
+
extraEnv: {}
|
|
3833
|
+
},
|
|
3834
|
+
route: "direct",
|
|
3835
|
+
nameTouched: false,
|
|
3836
|
+
lastTest: "never",
|
|
3837
|
+
testedSnapshot: null
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
const initialSnapshot = snapshotOf(state);
|
|
3841
|
+
const originalName = existing?.name;
|
|
3842
|
+
let title = "New host";
|
|
3843
|
+
if (existing != null) {
|
|
3844
|
+
title = `Editing ${existing.name}`;
|
|
3845
|
+
}
|
|
3846
|
+
output.write(prompter.paint.dim(`
|
|
3847
|
+
Pick a setting to change it. In a text field, Enter keeps the value shown and ${CLEAR_ANSWER} clears it.
|
|
3848
|
+
|
|
3849
|
+
`));
|
|
3850
|
+
const previousTransient = prompter.transient;
|
|
3851
|
+
prompter.transient = true;
|
|
3852
|
+
try {
|
|
3853
|
+
let cursor = "host";
|
|
3854
|
+
for (; ; ) {
|
|
3855
|
+
const picked = await prompter.select(title, buildRows(state, destination), { defaultValue: cursor, cancelValue: "cancel" });
|
|
3856
|
+
const before = cloneState(state);
|
|
3857
|
+
try {
|
|
3858
|
+
if (picked === "cancel") {
|
|
3859
|
+
if (snapshotOf(state) !== initialSnapshot) {
|
|
3860
|
+
const discard = await prompter.askYesNo("Discard the changes?", false);
|
|
3861
|
+
if (discard == false) {
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
const missing = missingFields(state);
|
|
3868
|
+
if (picked === "test") {
|
|
3869
|
+
if (missing.length > 0) {
|
|
3870
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3871
|
+
`);
|
|
3872
|
+
cursor = missing[0];
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
const connected = await testConnection(prompter, output, state);
|
|
3876
|
+
cursor = "test";
|
|
3877
|
+
if (connected == true) {
|
|
3878
|
+
cursor = "save";
|
|
3879
|
+
}
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
if (picked === "save") {
|
|
3883
|
+
if (missing.length > 0) {
|
|
3884
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3885
|
+
`);
|
|
3886
|
+
cursor = missing[0];
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
const name = state.answers.name;
|
|
3890
|
+
if (name !== originalName && takenNames.includes(name) == true) {
|
|
3891
|
+
const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
|
|
3892
|
+
if (replace == false) {
|
|
3893
|
+
cursor = "name";
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
if (snapshotOf(state) !== state.testedSnapshot) {
|
|
3898
|
+
const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
|
|
3899
|
+
if (testFirst == true) {
|
|
3900
|
+
const connected = await testConnection(prompter, output, state);
|
|
3901
|
+
if (connected == false) {
|
|
3902
|
+
const next = await prompter.select(
|
|
3903
|
+
"The check failed. What now?",
|
|
3904
|
+
[
|
|
3905
|
+
{ value: "back", key: "back", label: "Back to the settings" },
|
|
3906
|
+
{ value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
|
|
3907
|
+
{ value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
|
|
3908
|
+
],
|
|
3909
|
+
{ defaultValue: "back", cancelValue: "back" }
|
|
3910
|
+
);
|
|
3911
|
+
if (next === "discard") {
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
if (next === "back") {
|
|
3915
|
+
cursor = "test";
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
return finalizeAnswers(state);
|
|
3922
|
+
}
|
|
3923
|
+
await editField(prompter, output, state, picked);
|
|
3924
|
+
cursor = nextCursor(state, destination, picked);
|
|
3925
|
+
} catch (ex) {
|
|
3926
|
+
if (ex instanceof PromptBackError == false) {
|
|
3927
|
+
throw ex;
|
|
3928
|
+
}
|
|
3929
|
+
restoreState(state, before);
|
|
3930
|
+
cursor = picked;
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
} finally {
|
|
3934
|
+
prompter.transient = previousTransient;
|
|
3398
3935
|
}
|
|
3399
|
-
return answers;
|
|
3400
3936
|
}
|
|
3401
3937
|
function toMcpServerEntry(value) {
|
|
3402
3938
|
if (typeof value !== "object" || value == null) {
|
|
@@ -4125,125 +4661,54 @@ function reportWrite(prompter, result, action) {
|
|
|
4125
4661
|
|
|
4126
4662
|
`);
|
|
4127
4663
|
}
|
|
4128
|
-
function profileFromEntry(output, entry) {
|
|
4129
|
-
try {
|
|
4130
|
-
const profile = loadHostProfile(entry.env);
|
|
4131
|
-
if (profile == null) {
|
|
4132
|
-
output.write(" The entry has no host.\n");
|
|
4133
|
-
}
|
|
4134
|
-
return profile;
|
|
4135
|
-
} catch (ex) {
|
|
4136
|
-
output.write(` The server would refuse to start with this entry: ${errorMessage(ex)}
|
|
4137
|
-
`);
|
|
4138
|
-
return null;
|
|
4139
|
-
}
|
|
4140
|
-
}
|
|
4141
|
-
async function verifyAnswers(prompter, output, initial) {
|
|
4142
|
-
let answers = initial;
|
|
4143
|
-
for (; ; ) {
|
|
4144
|
-
const testNow = await prompter.askYesNo("Test the connection now?", true);
|
|
4145
|
-
if (testNow == false) {
|
|
4146
|
-
return answers;
|
|
4147
|
-
}
|
|
4148
|
-
output.write("\n");
|
|
4149
|
-
const profile = profileFromEntry(output, answersToEntry(answers));
|
|
4150
|
-
if (profile != null) {
|
|
4151
|
-
const result = await runConnectionCheck(profile, output);
|
|
4152
|
-
output.write("\n");
|
|
4153
|
-
if (result.connected == true) {
|
|
4154
|
-
if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
|
|
4155
|
-
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
4156
|
-
if (pin == true) {
|
|
4157
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
|
|
4161
|
-
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
4162
|
-
if (pin == true) {
|
|
4163
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
return answers;
|
|
4167
|
-
}
|
|
4168
|
-
const hostKeyChanged = result.hostKeyFingerprint != null && answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== answers.hostKeyFingerprint;
|
|
4169
|
-
if (hostKeyChanged == true) {
|
|
4170
|
-
output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
4171
|
-
const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
|
|
4172
|
-
if (repin == true) {
|
|
4173
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint ?? void 0 };
|
|
4174
|
-
continue;
|
|
4175
|
-
}
|
|
4176
|
-
}
|
|
4177
|
-
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== answers.jumpHostKeyFingerprint;
|
|
4178
|
-
if (jumpKeyChanged == true) {
|
|
4179
|
-
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
4180
|
-
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
4181
|
-
if (repin == true) {
|
|
4182
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint ?? void 0 };
|
|
4183
|
-
continue;
|
|
4184
|
-
}
|
|
4185
|
-
}
|
|
4186
|
-
}
|
|
4187
|
-
const next = await prompter.select(
|
|
4188
|
-
"The check failed. What now?",
|
|
4189
|
-
[
|
|
4190
|
-
{ value: "edit", key: "e", label: "Change the answers" },
|
|
4191
|
-
{ value: "retry", key: "r", label: "Try again", hint: "e.g. after starting a tunnel" },
|
|
4192
|
-
{ value: "write", key: "w", label: "Write it anyway", hint: "the host may be unreachable from here" },
|
|
4193
|
-
{ value: "cancel", key: "c", label: "Cancel", hint: "nothing is written" }
|
|
4194
|
-
],
|
|
4195
|
-
{ defaultValue: "edit", cancelValue: "cancel" }
|
|
4196
|
-
);
|
|
4197
|
-
if (next === "cancel") {
|
|
4198
|
-
return null;
|
|
4199
|
-
}
|
|
4200
|
-
if (next === "write") {
|
|
4201
|
-
return answers;
|
|
4202
|
-
}
|
|
4203
|
-
if (next === "edit") {
|
|
4204
|
-
output.write("\n");
|
|
4205
|
-
answers = await askHostQuestions(prompter, answers);
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
}
|
|
4209
4664
|
async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
4210
4665
|
let existing;
|
|
4211
4666
|
if (existingName != null) {
|
|
4212
4667
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
4213
|
-
output.write(`
|
|
4214
|
-
${prompter.paint.bold(`Editing ${existingName}`)}
|
|
4215
|
-
`);
|
|
4216
|
-
} else {
|
|
4217
|
-
output.write(`
|
|
4218
|
-
${prompter.paint.bold("Adding a host")}
|
|
4219
|
-
`);
|
|
4220
4668
|
}
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4669
|
+
let answers = await runHostForm(prompter, output, {
|
|
4670
|
+
existing,
|
|
4671
|
+
takenNames: Object.keys(entries),
|
|
4672
|
+
destination: target.label
|
|
4673
|
+
});
|
|
4224
4674
|
if (answers == null) {
|
|
4225
4675
|
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4226
4676
|
return;
|
|
4227
4677
|
}
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4232
|
-
return;
|
|
4233
|
-
}
|
|
4234
|
-
}
|
|
4235
|
-
const entry = answersToEntry(answers);
|
|
4236
|
-
output.write(`
|
|
4678
|
+
let entry = answersToEntry(answers);
|
|
4679
|
+
for (; ; ) {
|
|
4680
|
+
output.write(`
|
|
4237
4681
|
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
4238
4682
|
|
|
4239
4683
|
`);
|
|
4240
|
-
|
|
4684
|
+
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
4241
4685
|
|
|
4242
4686
|
`);
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4687
|
+
let confirmed;
|
|
4688
|
+
try {
|
|
4689
|
+
confirmed = await prompter.askYesNo("Write it?", true);
|
|
4690
|
+
} catch (ex) {
|
|
4691
|
+
if (ex instanceof PromptBackError == false) {
|
|
4692
|
+
throw ex;
|
|
4693
|
+
}
|
|
4694
|
+
const revised = await runHostForm(prompter, output, {
|
|
4695
|
+
existing: answers,
|
|
4696
|
+
takenNames: Object.keys(entries),
|
|
4697
|
+
destination: target.label
|
|
4698
|
+
});
|
|
4699
|
+
if (revised == null) {
|
|
4700
|
+
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
answers = revised;
|
|
4704
|
+
entry = answersToEntry(answers);
|
|
4705
|
+
continue;
|
|
4706
|
+
}
|
|
4707
|
+
if (confirmed == false) {
|
|
4708
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
4711
|
+
break;
|
|
4247
4712
|
}
|
|
4248
4713
|
const written = target.writeEntry(answers.name, entry);
|
|
4249
4714
|
reportWrite(prompter, written, "Written");
|
|
@@ -4258,7 +4723,7 @@ ${prompter.paint.bold("Adding a host")}
|
|
|
4258
4723
|
}
|
|
4259
4724
|
async function checkHost(prompter, output, target, name, entry) {
|
|
4260
4725
|
output.write("\n");
|
|
4261
|
-
const profile =
|
|
4726
|
+
const profile = profileFromEnv(output, entry.env);
|
|
4262
4727
|
if (profile == null) {
|
|
4263
4728
|
output.write("\n");
|
|
4264
4729
|
return;
|
|
@@ -4332,7 +4797,7 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4332
4797
|
const action = await prompter.select(
|
|
4333
4798
|
name,
|
|
4334
4799
|
[
|
|
4335
|
-
{ value: "edit", key: "e", label: "Edit", hint: "
|
|
4800
|
+
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4336
4801
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4337
4802
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4338
4803
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
@@ -4360,7 +4825,7 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4360
4825
|
}
|
|
4361
4826
|
}
|
|
4362
4827
|
}
|
|
4363
|
-
async function
|
|
4828
|
+
async function runSetupCommand(options) {
|
|
4364
4829
|
const { input, output } = options;
|
|
4365
4830
|
const targets = options.targets ?? listConfigTargets();
|
|
4366
4831
|
let target = targets[0];
|
|
@@ -4460,6 +4925,9 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
4460
4925
|
if (ex instanceof PromptClosedError) {
|
|
4461
4926
|
throw ex;
|
|
4462
4927
|
}
|
|
4928
|
+
if (ex instanceof PromptBackError) {
|
|
4929
|
+
continue;
|
|
4930
|
+
}
|
|
4463
4931
|
output.write(` Failed: ${errorMessage(ex)}
|
|
4464
4932
|
|
|
4465
4933
|
`);
|
|
@@ -5167,11 +5635,12 @@ function createSshMcpServer() {
|
|
|
5167
5635
|
}
|
|
5168
5636
|
|
|
5169
5637
|
// src/cli.ts
|
|
5638
|
+
var SETUP_COMMAND_NAMES = ["setup", "config"];
|
|
5170
5639
|
function parseArguments(argv) {
|
|
5171
|
-
const parsed = { showHelp: false, showVersion: false, checkOnly: false,
|
|
5640
|
+
const parsed = { showHelp: false, showVersion: false, checkOnly: false, setupMode: false };
|
|
5172
5641
|
let rest = argv;
|
|
5173
|
-
if (argv[0]
|
|
5174
|
-
parsed.
|
|
5642
|
+
if (argv[0] != null && SETUP_COMMAND_NAMES.includes(argv[0]) == true) {
|
|
5643
|
+
parsed.setupMode = true;
|
|
5175
5644
|
rest = argv.slice(1);
|
|
5176
5645
|
}
|
|
5177
5646
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -5205,17 +5674,17 @@ var HELP_TEXT = [
|
|
|
5205
5674
|
"",
|
|
5206
5675
|
"Usage:",
|
|
5207
5676
|
" akms-mcp-ssh [--check]",
|
|
5208
|
-
" akms-mcp-ssh
|
|
5677
|
+
" akms-mcp-ssh setup [--target <client>]",
|
|
5209
5678
|
"",
|
|
5210
5679
|
"Commands:",
|
|
5211
|
-
"
|
|
5680
|
+
" setup Manage the SSH hosts registered in an MCP client's config \u2014",
|
|
5212
5681
|
" list, add, edit, remove, check, print. Asks plain questions,",
|
|
5213
5682
|
" so the SSH_* names below never have to be typed.",
|
|
5214
5683
|
"",
|
|
5215
5684
|
"Options:",
|
|
5216
5685
|
" --check Connect to the configured host, report the result, and exit.",
|
|
5217
5686
|
" Prints the host key fingerprint so it can be pinned.",
|
|
5218
|
-
" --target <id>
|
|
5687
|
+
" --target <id> setup: claude-code | codex | claude-desktop | cursor",
|
|
5219
5688
|
" (default: the first one installed)",
|
|
5220
5689
|
" -v, --version Print the version and exit",
|
|
5221
5690
|
" -h, --help Print this help and exit",
|
|
@@ -5249,7 +5718,7 @@ var HELP_TEXT = [
|
|
|
5249
5718
|
].join("\n");
|
|
5250
5719
|
async function runCheckCommand(profile) {
|
|
5251
5720
|
if (profile == null) {
|
|
5252
|
-
process.stdout.write(`No SSH host is registered \u2014 set ${SINGLE_HOST_ENV.HOST} and ${SINGLE_HOST_ENV.USER}, or run: akms-mcp-ssh
|
|
5721
|
+
process.stdout.write(`No SSH host is registered \u2014 set ${SINGLE_HOST_ENV.HOST} and ${SINGLE_HOST_ENV.USER}, or run: akms-mcp-ssh setup
|
|
5253
5722
|
`);
|
|
5254
5723
|
return 1;
|
|
5255
5724
|
}
|
|
@@ -5272,11 +5741,11 @@ async function main() {
|
|
|
5272
5741
|
`);
|
|
5273
5742
|
return;
|
|
5274
5743
|
}
|
|
5275
|
-
if (cliArguments.
|
|
5744
|
+
if (cliArguments.setupMode == true) {
|
|
5276
5745
|
if (process.env[LOG_LEVEL_ENV] == null) {
|
|
5277
5746
|
process.env[LOG_LEVEL_ENV] = "warn";
|
|
5278
5747
|
}
|
|
5279
|
-
const exitCode = await
|
|
5748
|
+
const exitCode = await runSetupCommand({
|
|
5280
5749
|
input: process.stdin,
|
|
5281
5750
|
output: process.stdout,
|
|
5282
5751
|
targetId: cliArguments.targetId
|