@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.cjs
CHANGED
|
@@ -35,7 +35,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
35
35
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
36
36
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
37
37
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
38
|
-
var SERVER_VERSION = "0.1.
|
|
38
|
+
var SERVER_VERSION = "0.1.3";
|
|
39
39
|
|
|
40
40
|
// src/_defs/command-rules.ts
|
|
41
41
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -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}`;
|
|
@@ -2634,15 +2651,25 @@ var PromptClosedError = class extends Error {
|
|
|
2634
2651
|
this.name = "PromptClosedError";
|
|
2635
2652
|
}
|
|
2636
2653
|
};
|
|
2654
|
+
var PromptBackError = class extends Error {
|
|
2655
|
+
constructor() {
|
|
2656
|
+
super("prompt left with esc");
|
|
2657
|
+
this.name = "PromptBackError";
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2637
2660
|
var ESC = "\x1B";
|
|
2638
|
-
var KEY_UP = [`${ESC}[A`, `${ESC}OA
|
|
2639
|
-
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB
|
|
2640
|
-
var
|
|
2641
|
-
var
|
|
2661
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
2662
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
2663
|
+
var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
|
|
2664
|
+
var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
|
|
2665
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
2666
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
2667
|
+
var KEY_DELETE = [`${ESC}[3~`];
|
|
2642
2668
|
var KEY_ENTER = ["\r", "\n"];
|
|
2643
2669
|
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2644
2670
|
var KEY_CTRL_C = "";
|
|
2645
2671
|
var KEY_CTRL_D = "";
|
|
2672
|
+
var KEY_CLEAR_LINE = "";
|
|
2646
2673
|
function splitKeys(chunk) {
|
|
2647
2674
|
const keys = [];
|
|
2648
2675
|
let index = 0;
|
|
@@ -2664,6 +2691,13 @@ function splitKeys(chunk) {
|
|
|
2664
2691
|
}
|
|
2665
2692
|
return keys;
|
|
2666
2693
|
}
|
|
2694
|
+
function isPrintableKey(key) {
|
|
2695
|
+
if (key.startsWith(ESC) == true) {
|
|
2696
|
+
return false;
|
|
2697
|
+
}
|
|
2698
|
+
const codePoint = key.codePointAt(0) ?? 0;
|
|
2699
|
+
return codePoint >= 32 && codePoint !== 127;
|
|
2700
|
+
}
|
|
2667
2701
|
function charWidth(codePoint) {
|
|
2668
2702
|
if (codePoint < 4352) {
|
|
2669
2703
|
return 1;
|
|
@@ -2738,8 +2772,14 @@ var Prompter = class {
|
|
|
2738
2772
|
this.pendingLines = [];
|
|
2739
2773
|
this.lineWaiter = null;
|
|
2740
2774
|
this.inputEnded = false;
|
|
2741
|
-
/** Set while a key-driven prompt
|
|
2775
|
+
/** Set while a key-driven prompt owns the input. */
|
|
2742
2776
|
this.keyHandler = null;
|
|
2777
|
+
/**
|
|
2778
|
+
* When true, an answered prompt leaves nothing behind on a TTY instead of its
|
|
2779
|
+
* `✔ question · answer` line. The settings form sets it: the form itself shows every
|
|
2780
|
+
* value, so it can redraw in place rather than scroll a trail of edits.
|
|
2781
|
+
*/
|
|
2782
|
+
this.transient = false;
|
|
2743
2783
|
const ttyInput = input;
|
|
2744
2784
|
const ttyOutput = output;
|
|
2745
2785
|
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
@@ -2783,6 +2823,7 @@ var Prompter = class {
|
|
|
2783
2823
|
/**
|
|
2784
2824
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2785
2825
|
*
|
|
2826
|
+
* @throws {PromptBackError} On Esc.
|
|
2786
2827
|
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2787
2828
|
*/
|
|
2788
2829
|
async ask(question, defaultValue) {
|
|
@@ -2791,16 +2832,16 @@ var Prompter = class {
|
|
|
2791
2832
|
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2792
2833
|
}
|
|
2793
2834
|
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
2794
|
-
this.
|
|
2795
|
-
|
|
2796
|
-
if (line == null) {
|
|
2797
|
-
throw new PromptClosedError();
|
|
2798
|
-
}
|
|
2799
|
-
let answer = line.trim();
|
|
2835
|
+
const typed = await this.readText(prompt, false);
|
|
2836
|
+
let answer = typed.trim();
|
|
2800
2837
|
if (answer === "" && defaultValue != null) {
|
|
2801
2838
|
answer = defaultValue;
|
|
2802
2839
|
}
|
|
2803
|
-
|
|
2840
|
+
let shown = answer;
|
|
2841
|
+
if (shown === "") {
|
|
2842
|
+
shown = this.style.dim("(empty)");
|
|
2843
|
+
}
|
|
2844
|
+
this.writeAnswered(question, shown);
|
|
2804
2845
|
return answer;
|
|
2805
2846
|
}
|
|
2806
2847
|
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
@@ -2814,6 +2855,10 @@ var Prompter = class {
|
|
|
2814
2855
|
`);
|
|
2815
2856
|
}
|
|
2816
2857
|
}
|
|
2858
|
+
/**
|
|
2859
|
+
* @throws {PromptBackError} On Esc — the caller decides what "neither" means.
|
|
2860
|
+
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2861
|
+
*/
|
|
2817
2862
|
async askYesNo(question, defaultValue) {
|
|
2818
2863
|
let hint = "y/N";
|
|
2819
2864
|
if (defaultValue == true) {
|
|
@@ -2821,12 +2866,8 @@ var Prompter = class {
|
|
|
2821
2866
|
}
|
|
2822
2867
|
for (; ; ) {
|
|
2823
2868
|
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
2824
|
-
this.
|
|
2825
|
-
const
|
|
2826
|
-
if (line == null) {
|
|
2827
|
-
throw new PromptClosedError();
|
|
2828
|
-
}
|
|
2829
|
-
const normalized = line.trim().toLowerCase();
|
|
2869
|
+
const typed = await this.readText(prompt, false);
|
|
2870
|
+
const normalized = typed.trim().toLowerCase();
|
|
2830
2871
|
let answer = null;
|
|
2831
2872
|
if (normalized === "") {
|
|
2832
2873
|
answer = defaultValue;
|
|
@@ -2840,7 +2881,7 @@ var Prompter = class {
|
|
|
2840
2881
|
if (answer == true) {
|
|
2841
2882
|
shown = "yes";
|
|
2842
2883
|
}
|
|
2843
|
-
this.
|
|
2884
|
+
this.writeAnswered(question, shown);
|
|
2844
2885
|
return answer;
|
|
2845
2886
|
}
|
|
2846
2887
|
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
@@ -2848,9 +2889,10 @@ var Prompter = class {
|
|
|
2848
2889
|
}
|
|
2849
2890
|
}
|
|
2850
2891
|
/**
|
|
2851
|
-
* Picks one of `items`. On a TTY: ↑/↓
|
|
2852
|
-
*
|
|
2892
|
+
* Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
|
|
2893
|
+
* TTY: a numbered list, answered by number, key or label.
|
|
2853
2894
|
*
|
|
2895
|
+
* @throws {PromptBackError} On Esc without a `cancelValue`.
|
|
2854
2896
|
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2855
2897
|
*/
|
|
2856
2898
|
async select(question, items, options) {
|
|
@@ -2862,63 +2904,62 @@ var Prompter = class {
|
|
|
2862
2904
|
if (startIndex < 0) {
|
|
2863
2905
|
startIndex = items.findIndex((item) => "value" in item);
|
|
2864
2906
|
}
|
|
2907
|
+
let picked;
|
|
2865
2908
|
if (this.interactive == true) {
|
|
2866
|
-
|
|
2867
|
-
|
|
2909
|
+
picked = await this.selectWithKeys(question, items, startIndex);
|
|
2910
|
+
} else {
|
|
2911
|
+
picked = await this.selectWithLines(question, items, startIndex);
|
|
2868
2912
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2913
|
+
if (picked != null) {
|
|
2914
|
+
return picked;
|
|
2915
|
+
}
|
|
2916
|
+
if (options?.cancelValue != null) {
|
|
2917
|
+
return options.cancelValue;
|
|
2918
|
+
}
|
|
2919
|
+
throw new PromptBackError();
|
|
2871
2920
|
}
|
|
2872
2921
|
/**
|
|
2873
2922
|
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2874
2923
|
* caller says a value already exists.
|
|
2875
2924
|
*
|
|
2925
|
+
* @throws {PromptBackError} On Esc.
|
|
2876
2926
|
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2877
2927
|
*/
|
|
2878
2928
|
async askSecret(question) {
|
|
2929
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
2930
|
+
const secret = await this.readText(prompt, true);
|
|
2931
|
+
let shown = this.style.dim("(empty)");
|
|
2932
|
+
if (secret !== "") {
|
|
2933
|
+
shown = "\u2022".repeat(Math.min(secret.length, 8));
|
|
2934
|
+
}
|
|
2935
|
+
this.writeAnswered(question, shown);
|
|
2936
|
+
return secret;
|
|
2937
|
+
}
|
|
2938
|
+
// ── input ──────────────────────────────────────────────────────────────
|
|
2939
|
+
/**
|
|
2940
|
+
* One text answer, through the line editor on a TTY and a line read off one. The prompt
|
|
2941
|
+
* is erased once answered; the caller writes the summary line.
|
|
2942
|
+
*
|
|
2943
|
+
* @throws {PromptBackError} On Esc, or a line of just `ESC` off a TTY.
|
|
2944
|
+
*/
|
|
2945
|
+
async readText(prompt, secret) {
|
|
2879
2946
|
if (this.interactive == false) {
|
|
2880
|
-
|
|
2881
|
-
|
|
2947
|
+
this.output.write(prompt);
|
|
2948
|
+
const line = await this.readLine();
|
|
2949
|
+
if (line == null) {
|
|
2950
|
+
throw new PromptClosedError();
|
|
2951
|
+
}
|
|
2952
|
+
if (line === ESC) {
|
|
2953
|
+
throw new PromptBackError();
|
|
2954
|
+
}
|
|
2955
|
+
return line;
|
|
2882
2956
|
}
|
|
2883
|
-
const
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
const finish = () => {
|
|
2889
|
-
this.keyHandler = null;
|
|
2890
|
-
ttyInput.setRawMode?.(false);
|
|
2891
|
-
this.output.write(`\r${ESC}[2K`);
|
|
2892
|
-
};
|
|
2893
|
-
this.keyHandler = (key) => {
|
|
2894
|
-
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
2895
|
-
finish();
|
|
2896
|
-
reject(new PromptClosedError());
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
if (KEY_ENTER.includes(key) == true) {
|
|
2900
|
-
finish();
|
|
2901
|
-
let shown = this.style.dim("(empty)");
|
|
2902
|
-
if (buffer !== "") {
|
|
2903
|
-
shown = "\u2022".repeat(Math.min(buffer.length, 8));
|
|
2904
|
-
}
|
|
2905
|
-
this.writeAnswered(question, shown);
|
|
2906
|
-
resolve(buffer);
|
|
2907
|
-
return;
|
|
2908
|
-
}
|
|
2909
|
-
if (KEY_BACKSPACE.includes(key) == true) {
|
|
2910
|
-
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
if (key.startsWith(ESC) == true) {
|
|
2914
|
-
return;
|
|
2915
|
-
}
|
|
2916
|
-
buffer += key;
|
|
2917
|
-
};
|
|
2918
|
-
ttyInput.setRawMode?.(true);
|
|
2919
|
-
});
|
|
2957
|
+
const result = await this.editLine(prompt, secret);
|
|
2958
|
+
if (result.kind === "back") {
|
|
2959
|
+
throw new PromptBackError();
|
|
2960
|
+
}
|
|
2961
|
+
return result.text;
|
|
2920
2962
|
}
|
|
2921
|
-
// ── line mode ──────────────────────────────────────────────────────────
|
|
2922
2963
|
readLine() {
|
|
2923
2964
|
const queued = this.pendingLines.shift();
|
|
2924
2965
|
if (queued != null) {
|
|
@@ -2956,6 +2997,78 @@ var Prompter = class {
|
|
|
2956
2997
|
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2957
2998
|
}
|
|
2958
2999
|
}
|
|
3000
|
+
/**
|
|
3001
|
+
* A one-line editor in raw mode: insert at the cursor, Backspace / Delete, ←/→, Home /
|
|
3002
|
+
* End, Ctrl+U clears, Enter answers, Esc goes back. Secrets draw nothing after the
|
|
3003
|
+
* prompt. Cooked mode is not used for text on a TTY because it never reports Esc: the
|
|
3004
|
+
* terminal swallows it or stuffs the `ESC` byte into the value.
|
|
3005
|
+
*/
|
|
3006
|
+
editLine(prompt, secret) {
|
|
3007
|
+
const ttyInput = this.input;
|
|
3008
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3009
|
+
let characters = [];
|
|
3010
|
+
let cursor = 0;
|
|
3011
|
+
const draw = () => {
|
|
3012
|
+
let text = `\r${ESC}[2K${prompt}`;
|
|
3013
|
+
if (secret == false) {
|
|
3014
|
+
text += characters.join("");
|
|
3015
|
+
const column = displayWidth(plainPrompt) + displayWidth(characters.slice(0, cursor).join(""));
|
|
3016
|
+
text += `\r${ESC}[${column}C`;
|
|
3017
|
+
}
|
|
3018
|
+
this.output.write(text);
|
|
3019
|
+
};
|
|
3020
|
+
return new Promise((resolve, reject) => {
|
|
3021
|
+
const finish = () => {
|
|
3022
|
+
this.keyHandler = null;
|
|
3023
|
+
ttyInput.setRawMode?.(false);
|
|
3024
|
+
this.output.write(`\r${ESC}[2K`);
|
|
3025
|
+
};
|
|
3026
|
+
this.keyHandler = (key) => {
|
|
3027
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3028
|
+
finish();
|
|
3029
|
+
reject(new PromptClosedError());
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3033
|
+
finish();
|
|
3034
|
+
resolve({ kind: "entered", text: characters.join("") });
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
if (key === ESC) {
|
|
3038
|
+
finish();
|
|
3039
|
+
resolve({ kind: "back" });
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (KEY_BACKSPACE.includes(key) == true) {
|
|
3043
|
+
if (cursor > 0) {
|
|
3044
|
+
characters.splice(cursor - 1, 1);
|
|
3045
|
+
cursor -= 1;
|
|
3046
|
+
}
|
|
3047
|
+
} else if (KEY_DELETE.includes(key) == true) {
|
|
3048
|
+
characters.splice(cursor, 1);
|
|
3049
|
+
} else if (KEY_LEFT.includes(key) == true) {
|
|
3050
|
+
cursor = Math.max(0, cursor - 1);
|
|
3051
|
+
} else if (KEY_RIGHT.includes(key) == true) {
|
|
3052
|
+
cursor = Math.min(characters.length, cursor + 1);
|
|
3053
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3054
|
+
cursor = 0;
|
|
3055
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3056
|
+
cursor = characters.length;
|
|
3057
|
+
} else if (key === KEY_CLEAR_LINE) {
|
|
3058
|
+
characters = [];
|
|
3059
|
+
cursor = 0;
|
|
3060
|
+
} else if (isPrintableKey(key) == true) {
|
|
3061
|
+
characters.splice(cursor, 0, key);
|
|
3062
|
+
cursor += 1;
|
|
3063
|
+
} else {
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
draw();
|
|
3067
|
+
};
|
|
3068
|
+
ttyInput.setRawMode?.(true);
|
|
3069
|
+
draw();
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
2959
3072
|
async selectWithLines(question, items, startIndex) {
|
|
2960
3073
|
const choices = items.filter((item) => "value" in item);
|
|
2961
3074
|
const labelWidths = groupLabelWidths(items);
|
|
@@ -2984,6 +3097,9 @@ var Prompter = class {
|
|
|
2984
3097
|
if (line == null) {
|
|
2985
3098
|
throw new PromptClosedError();
|
|
2986
3099
|
}
|
|
3100
|
+
if (line === ESC) {
|
|
3101
|
+
return null;
|
|
3102
|
+
}
|
|
2987
3103
|
const answer = line.trim();
|
|
2988
3104
|
if (answer === "") {
|
|
2989
3105
|
return defaultChoice.value;
|
|
@@ -3039,28 +3155,14 @@ var Prompter = class {
|
|
|
3039
3155
|
}
|
|
3040
3156
|
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3041
3157
|
writeAnswered(question, answer) {
|
|
3042
|
-
|
|
3043
|
-
`);
|
|
3044
|
-
}
|
|
3045
|
-
/**
|
|
3046
|
-
* Replaces a cooked-mode prompt and its echoed answer with the one-line record. The
|
|
3047
|
-
* rows are worked out from the widths, since a long answer wraps.
|
|
3048
|
-
*/
|
|
3049
|
-
collapseAnswered(prompt, typed, question, answer) {
|
|
3050
|
-
if (this.interactive == false) {
|
|
3158
|
+
if (this.interactive == false || this.transient == true) {
|
|
3051
3159
|
return;
|
|
3052
3160
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
const rows = Math.max(1, Math.ceil(used / this.columns()));
|
|
3056
|
-
this.output.write(`${ESC}[${rows}A\r${ESC}[J`);
|
|
3057
|
-
let shown = answer;
|
|
3058
|
-
if (shown === "") {
|
|
3059
|
-
shown = this.style.dim("(empty)");
|
|
3060
|
-
}
|
|
3061
|
-
this.writeAnswered(question, shown);
|
|
3161
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
3162
|
+
`);
|
|
3062
3163
|
}
|
|
3063
|
-
|
|
3164
|
+
/** @returns The picked value, or null on Esc. */
|
|
3165
|
+
selectWithKeys(question, items, startIndex) {
|
|
3064
3166
|
const ttyInput = this.input;
|
|
3065
3167
|
const selectableIndexes = items.flatMap((item, index) => {
|
|
3066
3168
|
if ("value" in item) {
|
|
@@ -3110,11 +3212,7 @@ var Prompter = class {
|
|
|
3110
3212
|
}
|
|
3111
3213
|
lines.push(this.fitLine(parts, width));
|
|
3112
3214
|
}
|
|
3113
|
-
|
|
3114
|
-
if (cancelValue != null) {
|
|
3115
|
-
footer += " \xB7 esc back";
|
|
3116
|
-
}
|
|
3117
|
-
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3215
|
+
lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
|
|
3118
3216
|
return lines;
|
|
3119
3217
|
};
|
|
3120
3218
|
const move = (step) => {
|
|
@@ -3143,11 +3241,8 @@ var Prompter = class {
|
|
|
3143
3241
|
return;
|
|
3144
3242
|
}
|
|
3145
3243
|
if (key === ESC) {
|
|
3146
|
-
if (cancelValue == null) {
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
3244
|
finish();
|
|
3150
|
-
resolve(
|
|
3245
|
+
resolve(null);
|
|
3151
3246
|
return;
|
|
3152
3247
|
}
|
|
3153
3248
|
if (KEY_UP.includes(key) == true) {
|
|
@@ -3171,11 +3266,47 @@ var Prompter = class {
|
|
|
3171
3266
|
});
|
|
3172
3267
|
}
|
|
3173
3268
|
};
|
|
3269
|
+
|
|
3270
|
+
// src/modules/manage/host-form.ts
|
|
3174
3271
|
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3175
3272
|
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3176
3273
|
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
3177
3274
|
var CLEAR_ANSWER = "-";
|
|
3178
3275
|
var OTHER_KEY_PATH = "\0other";
|
|
3276
|
+
var FIELD_NAME_WIDTH = 17;
|
|
3277
|
+
var SUB_FIELDS = ["key", "passphrase", "password", "jump-host", "jump-port", "jump-user", "proxy"];
|
|
3278
|
+
var FIELD_NAMES = {
|
|
3279
|
+
"host": "Host",
|
|
3280
|
+
"port": "Port",
|
|
3281
|
+
"user": "User",
|
|
3282
|
+
"name": "Server name",
|
|
3283
|
+
"auth": "Authentication",
|
|
3284
|
+
"key": "Private key",
|
|
3285
|
+
"passphrase": "Passphrase",
|
|
3286
|
+
"password": "Password",
|
|
3287
|
+
"route": "Route",
|
|
3288
|
+
"jump-host": "Jump host",
|
|
3289
|
+
"jump-port": "Jump port",
|
|
3290
|
+
"jump-user": "Jump user",
|
|
3291
|
+
"proxy": "Proxy command",
|
|
3292
|
+
"description": "Description",
|
|
3293
|
+
"readonly": "Read-only",
|
|
3294
|
+
"paths": "File paths",
|
|
3295
|
+
"host-key": "Host key",
|
|
3296
|
+
"jump-host-key": "Jump host key"
|
|
3297
|
+
};
|
|
3298
|
+
var AUTH_LABELS = {
|
|
3299
|
+
key: "Private key file",
|
|
3300
|
+
password: "Password",
|
|
3301
|
+
agent: "SSH agent"
|
|
3302
|
+
};
|
|
3303
|
+
var ROUTE_LABELS = {
|
|
3304
|
+
"direct": "Directly",
|
|
3305
|
+
"jump": "Through a jump host",
|
|
3306
|
+
"proxy": "Through a proxy command",
|
|
3307
|
+
"jump-proxy": "Proxy command, then a jump host"
|
|
3308
|
+
};
|
|
3309
|
+
var REQUIRED_MARK = "(required)";
|
|
3179
3310
|
function findPrivateKeys() {
|
|
3180
3311
|
const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
|
|
3181
3312
|
let names = [];
|
|
@@ -3291,121 +3422,526 @@ async function askKeyPath(prompter, current) {
|
|
|
3291
3422
|
}
|
|
3292
3423
|
}
|
|
3293
3424
|
}
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3425
|
+
function routeUsesJump(route) {
|
|
3426
|
+
return route === "jump" || route === "jump-proxy";
|
|
3427
|
+
}
|
|
3428
|
+
function routeUsesProxy(route) {
|
|
3429
|
+
return route === "proxy" || route === "jump-proxy";
|
|
3430
|
+
}
|
|
3431
|
+
function deriveServerName(host) {
|
|
3432
|
+
return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
3433
|
+
}
|
|
3434
|
+
function finalizeAnswers(state) {
|
|
3435
|
+
const answers = { ...state.answers };
|
|
3436
|
+
if (routeUsesJump(state.route) == false) {
|
|
3437
|
+
answers.jumpHost = void 0;
|
|
3438
|
+
answers.jumpPort = void 0;
|
|
3439
|
+
answers.jumpUser = void 0;
|
|
3440
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3299
3441
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3442
|
+
if (routeUsesProxy(state.route) == false) {
|
|
3443
|
+
answers.proxyCommand = void 0;
|
|
3444
|
+
}
|
|
3445
|
+
if (answers.auth !== "key") {
|
|
3446
|
+
answers.keyPath = void 0;
|
|
3447
|
+
answers.passphrase = void 0;
|
|
3448
|
+
}
|
|
3449
|
+
if (answers.auth !== "password") {
|
|
3450
|
+
answers.password = void 0;
|
|
3451
|
+
}
|
|
3452
|
+
return answers;
|
|
3453
|
+
}
|
|
3454
|
+
function snapshotOf(state) {
|
|
3455
|
+
return JSON.stringify(finalizeAnswers(state));
|
|
3456
|
+
}
|
|
3457
|
+
function cloneState(state) {
|
|
3458
|
+
return {
|
|
3459
|
+
...state,
|
|
3460
|
+
answers: {
|
|
3461
|
+
...state.answers,
|
|
3462
|
+
allowedPaths: [...state.answers.allowedPaths],
|
|
3463
|
+
extraEnv: { ...state.answers.extraEnv }
|
|
3309
3464
|
}
|
|
3310
|
-
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
function restoreState(state, snapshot) {
|
|
3468
|
+
const restored = cloneState(snapshot);
|
|
3469
|
+
state.answers = restored.answers;
|
|
3470
|
+
state.route = restored.route;
|
|
3471
|
+
state.nameTouched = restored.nameTouched;
|
|
3472
|
+
state.lastTest = restored.lastTest;
|
|
3473
|
+
state.testedSnapshot = restored.testedSnapshot;
|
|
3474
|
+
}
|
|
3475
|
+
function missingFields(state) {
|
|
3476
|
+
const { answers } = state;
|
|
3477
|
+
const missing = [];
|
|
3478
|
+
if (answers.host === "") {
|
|
3479
|
+
missing.push("host");
|
|
3311
3480
|
}
|
|
3312
|
-
|
|
3313
|
-
"
|
|
3314
|
-
[
|
|
3315
|
-
{ value: "key", label: "Private key file", hint: "only the path is stored \u2014 recommended" },
|
|
3316
|
-
{ value: "password", label: "Password", hint: "stored in the config file as plain text" },
|
|
3317
|
-
{ value: "agent", label: "SSH agent", hint: "keys already loaded in ssh-agent" }
|
|
3318
|
-
],
|
|
3319
|
-
{ defaultValue: existing?.auth ?? "key" }
|
|
3320
|
-
);
|
|
3321
|
-
let keyPath;
|
|
3322
|
-
let passphrase;
|
|
3323
|
-
let password;
|
|
3324
|
-
if (auth === "key") {
|
|
3325
|
-
keyPath = await askKeyPath(prompter, existing?.keyPath);
|
|
3326
|
-
passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
|
|
3327
|
-
}
|
|
3328
|
-
if (auth === "password") {
|
|
3329
|
-
password = await askOptionalSecret(prompter, "Password", existing?.password);
|
|
3330
|
-
if (password == null) {
|
|
3331
|
-
prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
let currentRoute = "direct";
|
|
3335
|
-
if (existing?.jumpHost != null && existing.proxyCommand != null) {
|
|
3336
|
-
currentRoute = "jump-proxy";
|
|
3337
|
-
} else if (existing?.jumpHost != null) {
|
|
3338
|
-
currentRoute = "jump";
|
|
3339
|
-
} else if (existing?.proxyCommand != null) {
|
|
3340
|
-
currentRoute = "proxy";
|
|
3341
|
-
}
|
|
3342
|
-
const route = await prompter.select(
|
|
3343
|
-
"How is it reached?",
|
|
3344
|
-
[
|
|
3345
|
-
{ value: "direct", label: "Directly" },
|
|
3346
|
-
{ value: "jump", label: "Through a jump host", hint: "ssh -J" },
|
|
3347
|
-
{ value: "proxy", label: "Through a proxy command", hint: "e.g. cloudflared access ssh" },
|
|
3348
|
-
{ value: "jump-proxy", label: "Proxy command, then a jump host", hint: "cloudflared to the bastion, then on to the host" }
|
|
3349
|
-
],
|
|
3350
|
-
{ defaultValue: currentRoute }
|
|
3351
|
-
);
|
|
3352
|
-
let jumpHost;
|
|
3353
|
-
let jumpPort;
|
|
3354
|
-
let jumpUser;
|
|
3355
|
-
let proxyCommand;
|
|
3356
|
-
if (route === "jump" || route === "jump-proxy") {
|
|
3357
|
-
jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
|
|
3358
|
-
jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3359
|
-
jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
|
|
3360
|
-
}
|
|
3361
|
-
if (route === "proxy" || route === "jump-proxy") {
|
|
3362
|
-
prompter.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3363
|
-
proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3364
|
-
}
|
|
3365
|
-
const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
|
|
3366
|
-
const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
|
|
3367
|
-
let currentAllowedPaths;
|
|
3368
|
-
if (existing != null && existing.allowedPaths.length > 0) {
|
|
3369
|
-
currentAllowedPaths = existing.allowedPaths.join(",");
|
|
3481
|
+
if (answers.username === "") {
|
|
3482
|
+
missing.push("user");
|
|
3370
3483
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
prompter,
|
|
3374
|
-
"Limit file tools to path prefixes (comma-separated, empty = anywhere)",
|
|
3375
|
-
currentAllowedPaths
|
|
3376
|
-
);
|
|
3377
|
-
if (allowedPathsAnswer != null) {
|
|
3378
|
-
allowedPaths = allowedPathsAnswer.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3484
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3485
|
+
missing.push("key");
|
|
3379
3486
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
resolvedAuth = "agent";
|
|
3487
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3488
|
+
missing.push("password");
|
|
3383
3489
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3490
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3491
|
+
missing.push("jump-host");
|
|
3492
|
+
}
|
|
3493
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3494
|
+
missing.push("proxy");
|
|
3495
|
+
}
|
|
3496
|
+
return missing;
|
|
3497
|
+
}
|
|
3498
|
+
function describePin(fingerprint) {
|
|
3499
|
+
if (fingerprint == null) {
|
|
3500
|
+
return "not pinned";
|
|
3501
|
+
}
|
|
3502
|
+
return `pinned \xB7 ${fingerprint.slice(0, 19)}\u2026`;
|
|
3503
|
+
}
|
|
3504
|
+
function valueOrRequired(value) {
|
|
3505
|
+
if (value == null || value === "") {
|
|
3506
|
+
return REQUIRED_MARK;
|
|
3507
|
+
}
|
|
3508
|
+
return value;
|
|
3509
|
+
}
|
|
3510
|
+
function fieldRow(id, value, hint) {
|
|
3511
|
+
let name = FIELD_NAMES[id];
|
|
3512
|
+
if (SUB_FIELDS.includes(id) == true) {
|
|
3513
|
+
name = ` ${name}`;
|
|
3514
|
+
}
|
|
3515
|
+
return {
|
|
3516
|
+
value: id,
|
|
3517
|
+
key: id,
|
|
3518
|
+
label: `${name.padEnd(FIELD_NAME_WIDTH)}${value}`,
|
|
3519
|
+
hint
|
|
3401
3520
|
};
|
|
3402
|
-
|
|
3403
|
-
|
|
3521
|
+
}
|
|
3522
|
+
function buildRows(state, destination) {
|
|
3523
|
+
const { answers } = state;
|
|
3524
|
+
const rows = [];
|
|
3525
|
+
rows.push(fieldRow("host", valueOrRequired(answers.host)));
|
|
3526
|
+
rows.push(fieldRow("port", String(answers.port)));
|
|
3527
|
+
rows.push(fieldRow("user", valueOrRequired(answers.username)));
|
|
3528
|
+
let nameValue = answers.name;
|
|
3529
|
+
let nameHint = "what the agent sees";
|
|
3530
|
+
if (state.nameTouched == false) {
|
|
3531
|
+
nameHint = "follows the host until you change it";
|
|
3532
|
+
}
|
|
3533
|
+
if (nameValue === "") {
|
|
3534
|
+
nameValue = "ssh-<host>";
|
|
3535
|
+
}
|
|
3536
|
+
rows.push(fieldRow("name", nameValue, nameHint));
|
|
3537
|
+
rows.push(fieldRow("auth", AUTH_LABELS[answers.auth]));
|
|
3538
|
+
if (answers.auth === "key") {
|
|
3539
|
+
rows.push(fieldRow("key", valueOrRequired(answers.keyPath)));
|
|
3540
|
+
let passphrase = "none";
|
|
3541
|
+
if (answers.passphrase != null) {
|
|
3542
|
+
passphrase = "set";
|
|
3543
|
+
}
|
|
3544
|
+
rows.push(fieldRow("passphrase", passphrase));
|
|
3545
|
+
}
|
|
3546
|
+
if (answers.auth === "password") {
|
|
3547
|
+
let password = REQUIRED_MARK;
|
|
3548
|
+
if (answers.password != null) {
|
|
3549
|
+
password = "set";
|
|
3550
|
+
}
|
|
3551
|
+
rows.push(fieldRow("password", password, "stored in the config file as plain text"));
|
|
3552
|
+
}
|
|
3553
|
+
rows.push(fieldRow("route", ROUTE_LABELS[state.route]));
|
|
3554
|
+
if (routeUsesJump(state.route) == true) {
|
|
3555
|
+
rows.push(fieldRow("jump-host", valueOrRequired(answers.jumpHost)));
|
|
3556
|
+
rows.push(fieldRow("jump-port", String(answers.jumpPort ?? DEFAULT_SSH_PORT)));
|
|
3557
|
+
let jumpUser = answers.jumpUser;
|
|
3558
|
+
if (jumpUser == null || jumpUser === answers.username) {
|
|
3559
|
+
let loginUser = answers.username;
|
|
3560
|
+
if (loginUser === "") {
|
|
3561
|
+
loginUser = "\u2026";
|
|
3562
|
+
}
|
|
3563
|
+
jumpUser = `same as login (${loginUser})`;
|
|
3564
|
+
}
|
|
3565
|
+
rows.push(fieldRow("jump-user", jumpUser));
|
|
3404
3566
|
}
|
|
3405
|
-
if (
|
|
3406
|
-
answers.
|
|
3567
|
+
if (routeUsesProxy(state.route) == true) {
|
|
3568
|
+
rows.push(fieldRow("proxy", valueOrRequired(answers.proxyCommand), "%h %p %r \u2192 first hop"));
|
|
3569
|
+
}
|
|
3570
|
+
rows.push(fieldRow("description", answers.description ?? "none", "shown to the agent"));
|
|
3571
|
+
let readonly = "no";
|
|
3572
|
+
if (answers.readonly == true) {
|
|
3573
|
+
readonly = "yes";
|
|
3574
|
+
}
|
|
3575
|
+
rows.push(fieldRow("readonly", readonly, "enter toggles"));
|
|
3576
|
+
let paths = "anywhere";
|
|
3577
|
+
if (answers.allowedPaths.length > 0) {
|
|
3578
|
+
paths = answers.allowedPaths.join(", ");
|
|
3579
|
+
}
|
|
3580
|
+
rows.push(fieldRow("paths", paths, "where the file tools may read and write"));
|
|
3581
|
+
rows.push(fieldRow("host-key", describePin(answers.hostKeyFingerprint)));
|
|
3582
|
+
if (routeUsesJump(state.route) == true) {
|
|
3583
|
+
rows.push(fieldRow("jump-host-key", describePin(answers.jumpHostKeyFingerprint)));
|
|
3584
|
+
}
|
|
3585
|
+
rows.push({ separator: true });
|
|
3586
|
+
let testHint = "not run yet";
|
|
3587
|
+
if (state.lastTest === "ok") {
|
|
3588
|
+
testHint = "last run: OK";
|
|
3589
|
+
} else if (state.lastTest === "failed") {
|
|
3590
|
+
testHint = "last run: failed";
|
|
3591
|
+
}
|
|
3592
|
+
rows.push({ value: "test", key: "test", label: "Test the connection", hint: testHint });
|
|
3593
|
+
rows.push({ value: "save", key: "save", label: "Save", hint: `to ${destination}` });
|
|
3594
|
+
rows.push({ value: "cancel", key: "cancel", label: "Cancel" });
|
|
3595
|
+
return rows;
|
|
3596
|
+
}
|
|
3597
|
+
function nextCursor(state, destination, edited) {
|
|
3598
|
+
if (edited === "readonly") {
|
|
3599
|
+
return edited;
|
|
3600
|
+
}
|
|
3601
|
+
const values = buildRows(state, destination).flatMap((row) => {
|
|
3602
|
+
if ("value" in row) {
|
|
3603
|
+
return [row.value];
|
|
3604
|
+
}
|
|
3605
|
+
return [];
|
|
3606
|
+
});
|
|
3607
|
+
let index = values.indexOf(edited) + 1;
|
|
3608
|
+
if (SUB_FIELDS.includes(edited) == false) {
|
|
3609
|
+
while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
|
|
3610
|
+
index += 1;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
const next = values[index];
|
|
3614
|
+
if (next == null || next === "test") {
|
|
3615
|
+
return "save";
|
|
3616
|
+
}
|
|
3617
|
+
return next;
|
|
3618
|
+
}
|
|
3619
|
+
async function testConnection(prompter, output, state) {
|
|
3620
|
+
const answers = finalizeAnswers(state);
|
|
3621
|
+
output.write("\n");
|
|
3622
|
+
const profile = profileFromEnv(output, answersToEntry(answers).env);
|
|
3623
|
+
if (profile == null) {
|
|
3624
|
+
output.write("\n");
|
|
3625
|
+
state.lastTest = "failed";
|
|
3626
|
+
return false;
|
|
3627
|
+
}
|
|
3628
|
+
const result = await runConnectionCheck(profile, output);
|
|
3629
|
+
output.write("\n");
|
|
3630
|
+
if (result.connected == true) {
|
|
3631
|
+
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3632
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
3633
|
+
if (pin == true) {
|
|
3634
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
if (result.jumpHostKeyFingerprint != null && routeUsesJump(state.route) == true && state.answers.jumpHostKeyFingerprint == null) {
|
|
3638
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3639
|
+
if (pin == true) {
|
|
3640
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
state.lastTest = "ok";
|
|
3644
|
+
state.testedSnapshot = snapshotOf(state);
|
|
3645
|
+
return true;
|
|
3646
|
+
}
|
|
3647
|
+
const hostKeyChanged = result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== state.answers.hostKeyFingerprint;
|
|
3648
|
+
if (hostKeyChanged == true) {
|
|
3649
|
+
output.write(" The 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.hostKeyFingerprint} instead?`, false);
|
|
3651
|
+
if (repin == true) {
|
|
3652
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
|
|
3653
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && state.answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== state.answers.jumpHostKeyFingerprint;
|
|
3657
|
+
if (jumpKeyChanged == true) {
|
|
3658
|
+
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3659
|
+
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
3660
|
+
if (repin == true) {
|
|
3661
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint ?? void 0;
|
|
3662
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
state.lastTest = "failed";
|
|
3666
|
+
return false;
|
|
3667
|
+
}
|
|
3668
|
+
async function editField(prompter, output, state, id) {
|
|
3669
|
+
const { answers } = state;
|
|
3670
|
+
if (id === "host") {
|
|
3671
|
+
let current;
|
|
3672
|
+
if (answers.host !== "") {
|
|
3673
|
+
current = answers.host;
|
|
3674
|
+
}
|
|
3675
|
+
answers.host = await prompter.askRequired("Host (hostname or IP)", current);
|
|
3676
|
+
if (state.nameTouched == false) {
|
|
3677
|
+
answers.name = deriveServerName(answers.host);
|
|
3678
|
+
}
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
if (id === "port") {
|
|
3682
|
+
answers.port = await askPort(prompter, "Port", answers.port);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
if (id === "user") {
|
|
3686
|
+
let current;
|
|
3687
|
+
if (answers.username !== "") {
|
|
3688
|
+
current = answers.username;
|
|
3689
|
+
}
|
|
3690
|
+
answers.username = await prompter.askRequired("Login user", current);
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (id === "name") {
|
|
3694
|
+
let current;
|
|
3695
|
+
if (answers.name !== "") {
|
|
3696
|
+
current = answers.name;
|
|
3697
|
+
}
|
|
3698
|
+
for (; ; ) {
|
|
3699
|
+
const name = await prompter.askRequired("Server name the agent sees", current);
|
|
3700
|
+
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
3701
|
+
answers.name = name;
|
|
3702
|
+
state.nameTouched = true;
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
if (id === "auth") {
|
|
3709
|
+
answers.auth = await prompter.select(
|
|
3710
|
+
"Authentication",
|
|
3711
|
+
[
|
|
3712
|
+
{ value: "key", label: AUTH_LABELS.key, hint: "only the path is stored \u2014 recommended" },
|
|
3713
|
+
{ value: "password", label: AUTH_LABELS.password, hint: "stored in the config file as plain text" },
|
|
3714
|
+
{ value: "agent", label: AUTH_LABELS.agent, hint: "keys already loaded in ssh-agent" }
|
|
3715
|
+
],
|
|
3716
|
+
{ defaultValue: answers.auth }
|
|
3717
|
+
);
|
|
3718
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3719
|
+
answers.keyPath = await askKeyPath(prompter, void 0);
|
|
3720
|
+
}
|
|
3721
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3722
|
+
answers.password = await askOptionalSecret(prompter, "Password", void 0);
|
|
3723
|
+
}
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
if (id === "key") {
|
|
3727
|
+
answers.keyPath = await askKeyPath(prompter, answers.keyPath);
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
if (id === "passphrase") {
|
|
3731
|
+
answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (id === "password") {
|
|
3735
|
+
answers.password = await askOptionalSecret(prompter, "Password", answers.password);
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
if (id === "route") {
|
|
3739
|
+
state.route = await prompter.select(
|
|
3740
|
+
"Route",
|
|
3741
|
+
[
|
|
3742
|
+
{ value: "direct", label: ROUTE_LABELS.direct },
|
|
3743
|
+
{ value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
|
|
3744
|
+
{ value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
|
|
3745
|
+
{ value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
|
|
3746
|
+
],
|
|
3747
|
+
{ defaultValue: state.route }
|
|
3748
|
+
);
|
|
3749
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3750
|
+
answers.jumpHost = await prompter.askRequired("Jump host");
|
|
3751
|
+
}
|
|
3752
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3753
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", "cloudflared access ssh --hostname %h");
|
|
3754
|
+
}
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
if (id === "jump-host") {
|
|
3758
|
+
answers.jumpHost = await prompter.askRequired("Jump host", answers.jumpHost);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
if (id === "jump-port") {
|
|
3762
|
+
answers.jumpPort = await askPort(prompter, "Jump host port", answers.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (id === "jump-user") {
|
|
3766
|
+
answers.jumpUser = await askOptional(prompter, "Jump host user (empty = same as the login user)", answers.jumpUser);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (id === "proxy") {
|
|
3770
|
+
output.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3771
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
if (id === "description") {
|
|
3775
|
+
answers.description = await askOptional(prompter, "Description shown to the agent", answers.description);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
if (id === "readonly") {
|
|
3779
|
+
answers.readonly = answers.readonly == false;
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
if (id === "paths") {
|
|
3783
|
+
let current;
|
|
3784
|
+
if (answers.allowedPaths.length > 0) {
|
|
3785
|
+
current = answers.allowedPaths.join(",");
|
|
3786
|
+
}
|
|
3787
|
+
const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
|
|
3788
|
+
answers.allowedPaths = [];
|
|
3789
|
+
if (typed != null) {
|
|
3790
|
+
answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3791
|
+
}
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
const isJump = id === "jump-host-key";
|
|
3795
|
+
let pinned = answers.hostKeyFingerprint;
|
|
3796
|
+
if (isJump == true) {
|
|
3797
|
+
pinned = answers.jumpHostKeyFingerprint;
|
|
3798
|
+
}
|
|
3799
|
+
if (pinned != null) {
|
|
3800
|
+
const unpin = await prompter.askYesNo(`Unpin ${pinned}?`, false);
|
|
3801
|
+
if (unpin == true && isJump == true) {
|
|
3802
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3803
|
+
} else if (unpin == true) {
|
|
3804
|
+
answers.hostKeyFingerprint = void 0;
|
|
3805
|
+
}
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
const missing = missingFields(state);
|
|
3809
|
+
if (missing.length > 0) {
|
|
3810
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
|
|
3811
|
+
`);
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
await testConnection(prompter, output, state);
|
|
3815
|
+
}
|
|
3816
|
+
async function runHostForm(prompter, output, options) {
|
|
3817
|
+
const { existing, takenNames, destination } = options;
|
|
3818
|
+
let state;
|
|
3819
|
+
if (existing != null) {
|
|
3820
|
+
let route = "direct";
|
|
3821
|
+
if (existing.jumpHost != null && existing.proxyCommand != null) {
|
|
3822
|
+
route = "jump-proxy";
|
|
3823
|
+
} else if (existing.jumpHost != null) {
|
|
3824
|
+
route = "jump";
|
|
3825
|
+
} else if (existing.proxyCommand != null) {
|
|
3826
|
+
route = "proxy";
|
|
3827
|
+
}
|
|
3828
|
+
state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
|
|
3829
|
+
} else {
|
|
3830
|
+
const foundKeys = findPrivateKeys();
|
|
3831
|
+
state = {
|
|
3832
|
+
answers: {
|
|
3833
|
+
name: "",
|
|
3834
|
+
host: "",
|
|
3835
|
+
port: DEFAULT_SSH_PORT,
|
|
3836
|
+
username: "",
|
|
3837
|
+
auth: "key",
|
|
3838
|
+
keyPath: foundKeys[0]?.path,
|
|
3839
|
+
readonly: false,
|
|
3840
|
+
allowedPaths: [],
|
|
3841
|
+
extraEnv: {}
|
|
3842
|
+
},
|
|
3843
|
+
route: "direct",
|
|
3844
|
+
nameTouched: false,
|
|
3845
|
+
lastTest: "never",
|
|
3846
|
+
testedSnapshot: null
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
const initialSnapshot = snapshotOf(state);
|
|
3850
|
+
const originalName = existing?.name;
|
|
3851
|
+
let title = "New host";
|
|
3852
|
+
if (existing != null) {
|
|
3853
|
+
title = `Editing ${existing.name}`;
|
|
3854
|
+
}
|
|
3855
|
+
output.write(prompter.paint.dim(`
|
|
3856
|
+
Pick a setting to change it. In a text field, Enter keeps the value shown and ${CLEAR_ANSWER} clears it.
|
|
3857
|
+
|
|
3858
|
+
`));
|
|
3859
|
+
const previousTransient = prompter.transient;
|
|
3860
|
+
prompter.transient = true;
|
|
3861
|
+
try {
|
|
3862
|
+
let cursor = "host";
|
|
3863
|
+
for (; ; ) {
|
|
3864
|
+
const picked = await prompter.select(title, buildRows(state, destination), { defaultValue: cursor, cancelValue: "cancel" });
|
|
3865
|
+
const before = cloneState(state);
|
|
3866
|
+
try {
|
|
3867
|
+
if (picked === "cancel") {
|
|
3868
|
+
if (snapshotOf(state) !== initialSnapshot) {
|
|
3869
|
+
const discard = await prompter.askYesNo("Discard the changes?", false);
|
|
3870
|
+
if (discard == false) {
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return null;
|
|
3875
|
+
}
|
|
3876
|
+
const missing = missingFields(state);
|
|
3877
|
+
if (picked === "test") {
|
|
3878
|
+
if (missing.length > 0) {
|
|
3879
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3880
|
+
`);
|
|
3881
|
+
cursor = missing[0];
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
const connected = await testConnection(prompter, output, state);
|
|
3885
|
+
cursor = "test";
|
|
3886
|
+
if (connected == true) {
|
|
3887
|
+
cursor = "save";
|
|
3888
|
+
}
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
if (picked === "save") {
|
|
3892
|
+
if (missing.length > 0) {
|
|
3893
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3894
|
+
`);
|
|
3895
|
+
cursor = missing[0];
|
|
3896
|
+
continue;
|
|
3897
|
+
}
|
|
3898
|
+
const name = state.answers.name;
|
|
3899
|
+
if (name !== originalName && takenNames.includes(name) == true) {
|
|
3900
|
+
const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
|
|
3901
|
+
if (replace == false) {
|
|
3902
|
+
cursor = "name";
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
if (snapshotOf(state) !== state.testedSnapshot) {
|
|
3907
|
+
const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
|
|
3908
|
+
if (testFirst == true) {
|
|
3909
|
+
const connected = await testConnection(prompter, output, state);
|
|
3910
|
+
if (connected == false) {
|
|
3911
|
+
const next = await prompter.select(
|
|
3912
|
+
"The check failed. What now?",
|
|
3913
|
+
[
|
|
3914
|
+
{ value: "back", key: "back", label: "Back to the settings" },
|
|
3915
|
+
{ value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
|
|
3916
|
+
{ value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
|
|
3917
|
+
],
|
|
3918
|
+
{ defaultValue: "back", cancelValue: "back" }
|
|
3919
|
+
);
|
|
3920
|
+
if (next === "discard") {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
if (next === "back") {
|
|
3924
|
+
cursor = "test";
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
return finalizeAnswers(state);
|
|
3931
|
+
}
|
|
3932
|
+
await editField(prompter, output, state, picked);
|
|
3933
|
+
cursor = nextCursor(state, destination, picked);
|
|
3934
|
+
} catch (ex) {
|
|
3935
|
+
if (ex instanceof PromptBackError == false) {
|
|
3936
|
+
throw ex;
|
|
3937
|
+
}
|
|
3938
|
+
restoreState(state, before);
|
|
3939
|
+
cursor = picked;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
} finally {
|
|
3943
|
+
prompter.transient = previousTransient;
|
|
3407
3944
|
}
|
|
3408
|
-
return answers;
|
|
3409
3945
|
}
|
|
3410
3946
|
function toMcpServerEntry(value) {
|
|
3411
3947
|
if (typeof value !== "object" || value == null) {
|
|
@@ -4134,125 +4670,54 @@ function reportWrite(prompter, result, action) {
|
|
|
4134
4670
|
|
|
4135
4671
|
`);
|
|
4136
4672
|
}
|
|
4137
|
-
function profileFromEntry(output, entry) {
|
|
4138
|
-
try {
|
|
4139
|
-
const profile = loadHostProfile(entry.env);
|
|
4140
|
-
if (profile == null) {
|
|
4141
|
-
output.write(" The entry has no host.\n");
|
|
4142
|
-
}
|
|
4143
|
-
return profile;
|
|
4144
|
-
} catch (ex) {
|
|
4145
|
-
output.write(` The server would refuse to start with this entry: ${errorMessage(ex)}
|
|
4146
|
-
`);
|
|
4147
|
-
return null;
|
|
4148
|
-
}
|
|
4149
|
-
}
|
|
4150
|
-
async function verifyAnswers(prompter, output, initial) {
|
|
4151
|
-
let answers = initial;
|
|
4152
|
-
for (; ; ) {
|
|
4153
|
-
const testNow = await prompter.askYesNo("Test the connection now?", true);
|
|
4154
|
-
if (testNow == false) {
|
|
4155
|
-
return answers;
|
|
4156
|
-
}
|
|
4157
|
-
output.write("\n");
|
|
4158
|
-
const profile = profileFromEntry(output, answersToEntry(answers));
|
|
4159
|
-
if (profile != null) {
|
|
4160
|
-
const result = await runConnectionCheck(profile, output);
|
|
4161
|
-
output.write("\n");
|
|
4162
|
-
if (result.connected == true) {
|
|
4163
|
-
if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
|
|
4164
|
-
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
4165
|
-
if (pin == true) {
|
|
4166
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
|
|
4167
|
-
}
|
|
4168
|
-
}
|
|
4169
|
-
if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
|
|
4170
|
-
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
4171
|
-
if (pin == true) {
|
|
4172
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4175
|
-
return answers;
|
|
4176
|
-
}
|
|
4177
|
-
const hostKeyChanged = result.hostKeyFingerprint != null && answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== answers.hostKeyFingerprint;
|
|
4178
|
-
if (hostKeyChanged == true) {
|
|
4179
|
-
output.write(" The 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.hostKeyFingerprint} instead?`, false);
|
|
4181
|
-
if (repin == true) {
|
|
4182
|
-
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint ?? void 0 };
|
|
4183
|
-
continue;
|
|
4184
|
-
}
|
|
4185
|
-
}
|
|
4186
|
-
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== answers.jumpHostKeyFingerprint;
|
|
4187
|
-
if (jumpKeyChanged == true) {
|
|
4188
|
-
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
4189
|
-
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
4190
|
-
if (repin == true) {
|
|
4191
|
-
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint ?? void 0 };
|
|
4192
|
-
continue;
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
4196
|
-
const next = await prompter.select(
|
|
4197
|
-
"The check failed. What now?",
|
|
4198
|
-
[
|
|
4199
|
-
{ value: "edit", key: "e", label: "Change the answers" },
|
|
4200
|
-
{ value: "retry", key: "r", label: "Try again", hint: "e.g. after starting a tunnel" },
|
|
4201
|
-
{ value: "write", key: "w", label: "Write it anyway", hint: "the host may be unreachable from here" },
|
|
4202
|
-
{ value: "cancel", key: "c", label: "Cancel", hint: "nothing is written" }
|
|
4203
|
-
],
|
|
4204
|
-
{ defaultValue: "edit", cancelValue: "cancel" }
|
|
4205
|
-
);
|
|
4206
|
-
if (next === "cancel") {
|
|
4207
|
-
return null;
|
|
4208
|
-
}
|
|
4209
|
-
if (next === "write") {
|
|
4210
|
-
return answers;
|
|
4211
|
-
}
|
|
4212
|
-
if (next === "edit") {
|
|
4213
|
-
output.write("\n");
|
|
4214
|
-
answers = await askHostQuestions(prompter, answers);
|
|
4215
|
-
}
|
|
4216
|
-
}
|
|
4217
|
-
}
|
|
4218
4673
|
async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
4219
4674
|
let existing;
|
|
4220
4675
|
if (existingName != null) {
|
|
4221
4676
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
4222
|
-
output.write(`
|
|
4223
|
-
${prompter.paint.bold(`Editing ${existingName}`)}
|
|
4224
|
-
`);
|
|
4225
|
-
} else {
|
|
4226
|
-
output.write(`
|
|
4227
|
-
${prompter.paint.bold("Adding a host")}
|
|
4228
|
-
`);
|
|
4229
4677
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4678
|
+
let answers = await runHostForm(prompter, output, {
|
|
4679
|
+
existing,
|
|
4680
|
+
takenNames: Object.keys(entries),
|
|
4681
|
+
destination: target.label
|
|
4682
|
+
});
|
|
4233
4683
|
if (answers == null) {
|
|
4234
4684
|
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4235
4685
|
return;
|
|
4236
4686
|
}
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4241
|
-
return;
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
const entry = answersToEntry(answers);
|
|
4245
|
-
output.write(`
|
|
4687
|
+
let entry = answersToEntry(answers);
|
|
4688
|
+
for (; ; ) {
|
|
4689
|
+
output.write(`
|
|
4246
4690
|
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
4247
4691
|
|
|
4248
4692
|
`);
|
|
4249
|
-
|
|
4693
|
+
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
4250
4694
|
|
|
4251
4695
|
`);
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4696
|
+
let confirmed;
|
|
4697
|
+
try {
|
|
4698
|
+
confirmed = await prompter.askYesNo("Write it?", true);
|
|
4699
|
+
} catch (ex) {
|
|
4700
|
+
if (ex instanceof PromptBackError == false) {
|
|
4701
|
+
throw ex;
|
|
4702
|
+
}
|
|
4703
|
+
const revised = await runHostForm(prompter, output, {
|
|
4704
|
+
existing: answers,
|
|
4705
|
+
takenNames: Object.keys(entries),
|
|
4706
|
+
destination: target.label
|
|
4707
|
+
});
|
|
4708
|
+
if (revised == null) {
|
|
4709
|
+
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
answers = revised;
|
|
4713
|
+
entry = answersToEntry(answers);
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
if (confirmed == false) {
|
|
4717
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
break;
|
|
4256
4721
|
}
|
|
4257
4722
|
const written = target.writeEntry(answers.name, entry);
|
|
4258
4723
|
reportWrite(prompter, written, "Written");
|
|
@@ -4267,7 +4732,7 @@ ${prompter.paint.bold("Adding a host")}
|
|
|
4267
4732
|
}
|
|
4268
4733
|
async function checkHost(prompter, output, target, name, entry) {
|
|
4269
4734
|
output.write("\n");
|
|
4270
|
-
const profile =
|
|
4735
|
+
const profile = profileFromEnv(output, entry.env);
|
|
4271
4736
|
if (profile == null) {
|
|
4272
4737
|
output.write("\n");
|
|
4273
4738
|
return;
|
|
@@ -4341,7 +4806,7 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4341
4806
|
const action = await prompter.select(
|
|
4342
4807
|
name,
|
|
4343
4808
|
[
|
|
4344
|
-
{ value: "edit", key: "e", label: "Edit", hint: "
|
|
4809
|
+
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4345
4810
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4346
4811
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4347
4812
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
@@ -4369,7 +4834,7 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4369
4834
|
}
|
|
4370
4835
|
}
|
|
4371
4836
|
}
|
|
4372
|
-
async function
|
|
4837
|
+
async function runSetupCommand(options) {
|
|
4373
4838
|
const { input, output } = options;
|
|
4374
4839
|
const targets = options.targets ?? listConfigTargets();
|
|
4375
4840
|
let target = targets[0];
|
|
@@ -4469,6 +4934,9 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
4469
4934
|
if (ex instanceof PromptClosedError) {
|
|
4470
4935
|
throw ex;
|
|
4471
4936
|
}
|
|
4937
|
+
if (ex instanceof PromptBackError) {
|
|
4938
|
+
continue;
|
|
4939
|
+
}
|
|
4472
4940
|
output.write(` Failed: ${errorMessage(ex)}
|
|
4473
4941
|
|
|
4474
4942
|
`);
|
|
@@ -5176,11 +5644,12 @@ function createSshMcpServer() {
|
|
|
5176
5644
|
}
|
|
5177
5645
|
|
|
5178
5646
|
// src/cli.ts
|
|
5647
|
+
var SETUP_COMMAND_NAMES = ["setup", "config"];
|
|
5179
5648
|
function parseArguments(argv) {
|
|
5180
|
-
const parsed = { showHelp: false, showVersion: false, checkOnly: false,
|
|
5649
|
+
const parsed = { showHelp: false, showVersion: false, checkOnly: false, setupMode: false };
|
|
5181
5650
|
let rest = argv;
|
|
5182
|
-
if (argv[0]
|
|
5183
|
-
parsed.
|
|
5651
|
+
if (argv[0] != null && SETUP_COMMAND_NAMES.includes(argv[0]) == true) {
|
|
5652
|
+
parsed.setupMode = true;
|
|
5184
5653
|
rest = argv.slice(1);
|
|
5185
5654
|
}
|
|
5186
5655
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -5214,17 +5683,17 @@ var HELP_TEXT = [
|
|
|
5214
5683
|
"",
|
|
5215
5684
|
"Usage:",
|
|
5216
5685
|
" akms-mcp-ssh [--check]",
|
|
5217
|
-
" akms-mcp-ssh
|
|
5686
|
+
" akms-mcp-ssh setup [--target <client>]",
|
|
5218
5687
|
"",
|
|
5219
5688
|
"Commands:",
|
|
5220
|
-
"
|
|
5689
|
+
" setup Manage the SSH hosts registered in an MCP client's config \u2014",
|
|
5221
5690
|
" list, add, edit, remove, check, print. Asks plain questions,",
|
|
5222
5691
|
" so the SSH_* names below never have to be typed.",
|
|
5223
5692
|
"",
|
|
5224
5693
|
"Options:",
|
|
5225
5694
|
" --check Connect to the configured host, report the result, and exit.",
|
|
5226
5695
|
" Prints the host key fingerprint so it can be pinned.",
|
|
5227
|
-
" --target <id>
|
|
5696
|
+
" --target <id> setup: claude-code | codex | claude-desktop | cursor",
|
|
5228
5697
|
" (default: the first one installed)",
|
|
5229
5698
|
" -v, --version Print the version and exit",
|
|
5230
5699
|
" -h, --help Print this help and exit",
|
|
@@ -5258,7 +5727,7 @@ var HELP_TEXT = [
|
|
|
5258
5727
|
].join("\n");
|
|
5259
5728
|
async function runCheckCommand(profile) {
|
|
5260
5729
|
if (profile == null) {
|
|
5261
|
-
process.stdout.write(`No SSH host is registered \u2014 set ${SINGLE_HOST_ENV.HOST} and ${SINGLE_HOST_ENV.USER}, or run: akms-mcp-ssh
|
|
5730
|
+
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
|
|
5262
5731
|
`);
|
|
5263
5732
|
return 1;
|
|
5264
5733
|
}
|
|
@@ -5281,11 +5750,11 @@ async function main() {
|
|
|
5281
5750
|
`);
|
|
5282
5751
|
return;
|
|
5283
5752
|
}
|
|
5284
|
-
if (cliArguments.
|
|
5753
|
+
if (cliArguments.setupMode == true) {
|
|
5285
5754
|
if (process.env[LOG_LEVEL_ENV] == null) {
|
|
5286
5755
|
process.env[LOG_LEVEL_ENV] = "warn";
|
|
5287
5756
|
}
|
|
5288
|
-
const exitCode = await
|
|
5757
|
+
const exitCode = await runSetupCommand({
|
|
5289
5758
|
input: process.stdin,
|
|
5290
5759
|
output: process.stdout,
|
|
5291
5760
|
targetId: cliArguments.targetId
|