@akms/mcp-ssh 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/lib/cli.cjs +738 -282
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +736 -280
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/cli.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
-
var
|
|
6
|
-
var
|
|
5
|
+
var fs6 = require('fs');
|
|
6
|
+
var os5 = require('os');
|
|
7
7
|
var path = require('path');
|
|
8
8
|
var child_process = require('child_process');
|
|
9
9
|
var crypto = require('crypto');
|
|
@@ -16,8 +16,8 @@ var zod = require('zod');
|
|
|
16
16
|
|
|
17
17
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
18
|
|
|
19
|
-
var
|
|
20
|
-
var
|
|
19
|
+
var fs6__default = /*#__PURE__*/_interopDefault(fs6);
|
|
20
|
+
var os5__default = /*#__PURE__*/_interopDefault(os5);
|
|
21
21
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
22
22
|
var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
|
|
23
23
|
|
|
@@ -35,7 +35,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
35
35
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
36
36
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
37
37
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
38
|
-
var SERVER_VERSION = "0.1.
|
|
38
|
+
var SERVER_VERSION = "0.1.1";
|
|
39
39
|
|
|
40
40
|
// src/_defs/command-rules.ts
|
|
41
41
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -610,7 +610,7 @@ function findExecutable(name) {
|
|
|
610
610
|
for (const directory of directories) {
|
|
611
611
|
for (const suffix of suffixes) {
|
|
612
612
|
const candidate = path__default.default.join(directory, `${name}${suffix}`);
|
|
613
|
-
if (
|
|
613
|
+
if (fs6__default.default.existsSync(candidate) == true && fs6__default.default.statSync(candidate).isFile() == true) {
|
|
614
614
|
return candidate;
|
|
615
615
|
}
|
|
616
616
|
}
|
|
@@ -620,9 +620,9 @@ function findExecutable(name) {
|
|
|
620
620
|
function expandHomePath(inputPath) {
|
|
621
621
|
let expanded = inputPath;
|
|
622
622
|
if (inputPath === "~") {
|
|
623
|
-
expanded =
|
|
623
|
+
expanded = os5__default.default.homedir();
|
|
624
624
|
} else if (inputPath.startsWith("~/") == true || inputPath.startsWith("~\\") == true) {
|
|
625
|
-
expanded = path__default.default.join(
|
|
625
|
+
expanded = path__default.default.join(os5__default.default.homedir(), inputPath.slice(2));
|
|
626
626
|
}
|
|
627
627
|
return path__default.default.resolve(expanded);
|
|
628
628
|
}
|
|
@@ -751,7 +751,7 @@ function loadHostProfile(env = process.env) {
|
|
|
751
751
|
if (keyPath != null) {
|
|
752
752
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
753
753
|
profile.privateKeyPath = expandedKeyPath;
|
|
754
|
-
if (
|
|
754
|
+
if (fs6__default.default.existsSync(expandedKeyPath) == false) {
|
|
755
755
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
756
756
|
private_key_path: expandedKeyPath
|
|
757
757
|
});
|
|
@@ -783,7 +783,7 @@ function loadHostProfile(env = process.env) {
|
|
|
783
783
|
if (jumpKeyPath != null) {
|
|
784
784
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
785
785
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
786
|
-
if (
|
|
786
|
+
if (fs6__default.default.existsSync(profile.jump.privateKeyPath) == false) {
|
|
787
787
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
788
788
|
private_key_path: profile.jump.privateKeyPath
|
|
789
789
|
});
|
|
@@ -1269,14 +1269,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1269
1269
|
function toRealPath(inputPath) {
|
|
1270
1270
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1271
1271
|
try {
|
|
1272
|
-
return
|
|
1272
|
+
return fs6__default.default.realpathSync.native(inputPath);
|
|
1273
1273
|
} catch (ex) {
|
|
1274
1274
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1275
1275
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1276
1276
|
});
|
|
1277
1277
|
}
|
|
1278
1278
|
try {
|
|
1279
|
-
const parentRealPath =
|
|
1279
|
+
const parentRealPath = fs6__default.default.realpathSync.native(path__default.default.dirname(inputPath));
|
|
1280
1280
|
return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
|
|
1281
1281
|
} catch (ex) {
|
|
1282
1282
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1368,7 +1368,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1368
1368
|
return cached;
|
|
1369
1369
|
}
|
|
1370
1370
|
try {
|
|
1371
|
-
const contents =
|
|
1371
|
+
const contents = fs6__default.default.readFileSync(keyPath);
|
|
1372
1372
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1373
1373
|
return contents;
|
|
1374
1374
|
} catch (ex) {
|
|
@@ -2319,7 +2319,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2319
2319
|
}
|
|
2320
2320
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2321
2321
|
requirePermittedPath(connection, remotePath, "write");
|
|
2322
|
-
const localStats =
|
|
2322
|
+
const localStats = fs6__default.default.statSync(localPath);
|
|
2323
2323
|
if (localStats.isFile() == false) {
|
|
2324
2324
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2325
2325
|
}
|
|
@@ -2356,7 +2356,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2356
2356
|
});
|
|
2357
2357
|
});
|
|
2358
2358
|
});
|
|
2359
|
-
const localStats =
|
|
2359
|
+
const localStats = fs6__default.default.statSync(localPath);
|
|
2360
2360
|
const _logger = $logger.child({
|
|
2361
2361
|
context: "downloadFile",
|
|
2362
2362
|
profile: connection.profile.name,
|
|
@@ -2634,36 +2634,152 @@ var PromptClosedError = class extends Error {
|
|
|
2634
2634
|
this.name = "PromptClosedError";
|
|
2635
2635
|
}
|
|
2636
2636
|
};
|
|
2637
|
+
var ESC = "\x1B";
|
|
2638
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`, "k"];
|
|
2639
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`, "j"];
|
|
2640
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`];
|
|
2641
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`];
|
|
2642
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
2643
|
+
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2644
|
+
var KEY_CTRL_C = "";
|
|
2645
|
+
var KEY_CTRL_D = "";
|
|
2646
|
+
function splitKeys(chunk) {
|
|
2647
|
+
const keys = [];
|
|
2648
|
+
let index = 0;
|
|
2649
|
+
while (index < chunk.length) {
|
|
2650
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
2651
|
+
if (isSequence == true) {
|
|
2652
|
+
let end = index + 2;
|
|
2653
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
2654
|
+
end += 1;
|
|
2655
|
+
}
|
|
2656
|
+
keys.push(chunk.slice(index, end + 1));
|
|
2657
|
+
index = end + 1;
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
2661
|
+
const character = String.fromCodePoint(codePoint);
|
|
2662
|
+
keys.push(character);
|
|
2663
|
+
index += character.length;
|
|
2664
|
+
}
|
|
2665
|
+
return keys;
|
|
2666
|
+
}
|
|
2667
|
+
function charWidth(codePoint) {
|
|
2668
|
+
if (codePoint < 4352) {
|
|
2669
|
+
return 1;
|
|
2670
|
+
}
|
|
2671
|
+
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
2672
|
+
if (isWide == true) {
|
|
2673
|
+
return 2;
|
|
2674
|
+
}
|
|
2675
|
+
return 1;
|
|
2676
|
+
}
|
|
2677
|
+
function displayWidth(text) {
|
|
2678
|
+
let width = 0;
|
|
2679
|
+
for (const character of text) {
|
|
2680
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
2681
|
+
}
|
|
2682
|
+
return width;
|
|
2683
|
+
}
|
|
2684
|
+
function padToWidth(text, width) {
|
|
2685
|
+
const missing = width - displayWidth(text);
|
|
2686
|
+
if (missing <= 0) {
|
|
2687
|
+
return text;
|
|
2688
|
+
}
|
|
2689
|
+
return `${text}${" ".repeat(missing)}`;
|
|
2690
|
+
}
|
|
2691
|
+
function groupLabelWidths(items) {
|
|
2692
|
+
const widths = items.map(() => 0);
|
|
2693
|
+
let groupStart = 0;
|
|
2694
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
2695
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
2696
|
+
if (atBoundary == false) {
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
let groupWidth = 0;
|
|
2700
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2701
|
+
const item = items[member];
|
|
2702
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
2703
|
+
}
|
|
2704
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2705
|
+
widths[member] = groupWidth;
|
|
2706
|
+
}
|
|
2707
|
+
groupStart = index + 1;
|
|
2708
|
+
}
|
|
2709
|
+
return widths;
|
|
2710
|
+
}
|
|
2711
|
+
function supportsUnicode() {
|
|
2712
|
+
if (process.platform !== "win32") {
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
2716
|
+
}
|
|
2717
|
+
function createStyle(enabled) {
|
|
2718
|
+
const paint = (open, close) => {
|
|
2719
|
+
return (text) => {
|
|
2720
|
+
if (enabled == false || text === "") {
|
|
2721
|
+
return text;
|
|
2722
|
+
}
|
|
2723
|
+
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
2724
|
+
};
|
|
2725
|
+
};
|
|
2726
|
+
return {
|
|
2727
|
+
accent: paint(36, 39),
|
|
2728
|
+
success: paint(32, 39),
|
|
2729
|
+
dim: paint(2, 22),
|
|
2730
|
+
bold: paint(1, 22)
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2637
2733
|
var Prompter = class {
|
|
2638
2734
|
constructor(input, output) {
|
|
2639
2735
|
this.input = input;
|
|
2640
2736
|
this.output = output;
|
|
2641
2737
|
this.lineBuffer = "";
|
|
2642
2738
|
this.pendingLines = [];
|
|
2643
|
-
this.
|
|
2739
|
+
this.lineWaiter = null;
|
|
2644
2740
|
this.inputEnded = false;
|
|
2645
|
-
|
|
2646
|
-
this.
|
|
2741
|
+
/** Set while a key-driven prompt (select, secret) owns the input. */
|
|
2742
|
+
this.keyHandler = null;
|
|
2743
|
+
const ttyInput = input;
|
|
2744
|
+
const ttyOutput = output;
|
|
2745
|
+
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
2746
|
+
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
2747
|
+
if (supportsUnicode() == true) {
|
|
2748
|
+
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
2749
|
+
} else {
|
|
2750
|
+
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
2751
|
+
}
|
|
2647
2752
|
input.setEncoding("utf8");
|
|
2648
2753
|
input.on("data", (chunk) => {
|
|
2649
2754
|
this.onData(chunk);
|
|
2650
2755
|
});
|
|
2651
2756
|
input.on("end", () => {
|
|
2652
2757
|
this.inputEnded = true;
|
|
2653
|
-
if (this.
|
|
2654
|
-
const waiter = this.
|
|
2655
|
-
this.
|
|
2758
|
+
if (this.lineWaiter != null) {
|
|
2759
|
+
const waiter = this.lineWaiter;
|
|
2760
|
+
this.lineWaiter = null;
|
|
2656
2761
|
waiter(null);
|
|
2657
2762
|
}
|
|
2763
|
+
if (this.keyHandler != null) {
|
|
2764
|
+
this.keyHandler(KEY_CTRL_D);
|
|
2765
|
+
}
|
|
2658
2766
|
});
|
|
2659
2767
|
}
|
|
2660
2768
|
/** Stops reading so the process can exit once the command is done. */
|
|
2661
2769
|
close() {
|
|
2770
|
+
if (this.interactive == true) {
|
|
2771
|
+
this.input.setRawMode?.(false);
|
|
2772
|
+
this.output.write(`${ESC}[?25h`);
|
|
2773
|
+
}
|
|
2662
2774
|
this.input.pause();
|
|
2663
2775
|
}
|
|
2664
2776
|
write(text) {
|
|
2665
2777
|
this.output.write(text);
|
|
2666
2778
|
}
|
|
2779
|
+
/** Styling for callers that print around the prompts; plain text off a TTY. */
|
|
2780
|
+
get paint() {
|
|
2781
|
+
return this.style;
|
|
2782
|
+
}
|
|
2667
2783
|
/**
|
|
2668
2784
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2669
2785
|
*
|
|
@@ -2672,18 +2788,20 @@ var Prompter = class {
|
|
|
2672
2788
|
async ask(question, defaultValue) {
|
|
2673
2789
|
let suffix = "";
|
|
2674
2790
|
if (defaultValue != null && defaultValue !== "") {
|
|
2675
|
-
suffix = `
|
|
2791
|
+
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2676
2792
|
}
|
|
2677
|
-
this.
|
|
2793
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
2794
|
+
this.output.write(prompt);
|
|
2678
2795
|
const line = await this.readLine();
|
|
2679
2796
|
if (line == null) {
|
|
2680
2797
|
throw new PromptClosedError();
|
|
2681
2798
|
}
|
|
2682
|
-
|
|
2683
|
-
if (
|
|
2684
|
-
|
|
2799
|
+
let answer = line.trim();
|
|
2800
|
+
if (answer === "" && defaultValue != null) {
|
|
2801
|
+
answer = defaultValue;
|
|
2685
2802
|
}
|
|
2686
|
-
|
|
2803
|
+
this.collapseAnswered(prompt, line, question, answer);
|
|
2804
|
+
return answer;
|
|
2687
2805
|
}
|
|
2688
2806
|
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
2689
2807
|
async askRequired(question, defaultValue) {
|
|
@@ -2692,77 +2810,115 @@ var Prompter = class {
|
|
|
2692
2810
|
if (answer !== "") {
|
|
2693
2811
|
return answer;
|
|
2694
2812
|
}
|
|
2695
|
-
this.output.write("
|
|
2813
|
+
this.output.write(` ${this.style.dim("A value is required.")}
|
|
2814
|
+
`);
|
|
2696
2815
|
}
|
|
2697
2816
|
}
|
|
2698
2817
|
async askYesNo(question, defaultValue) {
|
|
2699
|
-
let hint = "
|
|
2818
|
+
let hint = "y/N";
|
|
2700
2819
|
if (defaultValue == true) {
|
|
2701
|
-
hint = "
|
|
2820
|
+
hint = "Y/n";
|
|
2702
2821
|
}
|
|
2703
2822
|
for (; ; ) {
|
|
2704
|
-
this.
|
|
2823
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
2824
|
+
this.output.write(prompt);
|
|
2705
2825
|
const line = await this.readLine();
|
|
2706
2826
|
if (line == null) {
|
|
2707
2827
|
throw new PromptClosedError();
|
|
2708
2828
|
}
|
|
2709
2829
|
const normalized = line.trim().toLowerCase();
|
|
2830
|
+
let answer = null;
|
|
2710
2831
|
if (normalized === "") {
|
|
2711
|
-
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2832
|
+
answer = defaultValue;
|
|
2833
|
+
} else if (normalized === "y" || normalized === "yes") {
|
|
2834
|
+
answer = true;
|
|
2835
|
+
} else if (normalized === "n" || normalized === "no") {
|
|
2836
|
+
answer = false;
|
|
2715
2837
|
}
|
|
2716
|
-
if (
|
|
2717
|
-
|
|
2838
|
+
if (answer != null) {
|
|
2839
|
+
let shown = "no";
|
|
2840
|
+
if (answer == true) {
|
|
2841
|
+
shown = "yes";
|
|
2842
|
+
}
|
|
2843
|
+
this.collapseAnswered(prompt, line, question, shown);
|
|
2844
|
+
return answer;
|
|
2718
2845
|
}
|
|
2719
|
-
this.output.write("
|
|
2846
|
+
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
2847
|
+
`);
|
|
2720
2848
|
}
|
|
2721
2849
|
}
|
|
2722
|
-
/**
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2850
|
+
/**
|
|
2851
|
+
* Picks one of `items`. On a TTY: ↑/↓ (or k/j) move, 1–9 jump, Enter picks, Esc returns
|
|
2852
|
+
* `cancelValue`. Off a TTY: a numbered list, answered by number, key or label.
|
|
2853
|
+
*
|
|
2854
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2855
|
+
*/
|
|
2856
|
+
async select(question, items, options) {
|
|
2857
|
+
const choices = items.filter((item) => "value" in item);
|
|
2858
|
+
if (choices.length === 0) {
|
|
2859
|
+
throw new Error("select needs at least one choice");
|
|
2729
2860
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
`);
|
|
2861
|
+
let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
|
|
2862
|
+
if (startIndex < 0) {
|
|
2863
|
+
startIndex = items.findIndex((item) => "value" in item);
|
|
2864
|
+
}
|
|
2865
|
+
if (this.interactive == true) {
|
|
2866
|
+
const picked2 = await this.selectWithKeys(question, items, startIndex, options?.cancelValue);
|
|
2867
|
+
return picked2;
|
|
2738
2868
|
}
|
|
2869
|
+
const picked = await this.selectWithLines(question, items, startIndex);
|
|
2870
|
+
return picked;
|
|
2739
2871
|
}
|
|
2740
2872
|
/**
|
|
2741
2873
|
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2742
2874
|
* caller says a value already exists.
|
|
2875
|
+
*
|
|
2876
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2743
2877
|
*/
|
|
2744
2878
|
async askSecret(question) {
|
|
2745
|
-
|
|
2746
|
-
if (rawModeStream.isTTY != true || rawModeStream.setRawMode == null) {
|
|
2879
|
+
if (this.interactive == false) {
|
|
2747
2880
|
const answer = await this.ask(question);
|
|
2748
2881
|
return answer;
|
|
2749
2882
|
}
|
|
2750
|
-
this.
|
|
2751
|
-
this.
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2883
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
2884
|
+
this.output.write(prompt);
|
|
2885
|
+
const ttyInput = this.input;
|
|
2886
|
+
return new Promise((resolve, reject) => {
|
|
2887
|
+
let buffer = "";
|
|
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
|
+
});
|
|
2765
2920
|
}
|
|
2921
|
+
// ── line mode ──────────────────────────────────────────────────────────
|
|
2766
2922
|
readLine() {
|
|
2767
2923
|
const queued = this.pendingLines.shift();
|
|
2768
2924
|
if (queued != null) {
|
|
@@ -2772,36 +2928,16 @@ var Prompter = class {
|
|
|
2772
2928
|
return Promise.resolve(null);
|
|
2773
2929
|
}
|
|
2774
2930
|
return new Promise((resolve) => {
|
|
2775
|
-
this.
|
|
2931
|
+
this.lineWaiter = resolve;
|
|
2776
2932
|
});
|
|
2777
2933
|
}
|
|
2778
|
-
deliver(line) {
|
|
2779
|
-
if (this.waiter != null) {
|
|
2780
|
-
const waiter = this.waiter;
|
|
2781
|
-
this.waiter = null;
|
|
2782
|
-
waiter(line);
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
this.pendingLines.push(line);
|
|
2786
|
-
}
|
|
2787
2934
|
onData(chunk) {
|
|
2788
|
-
if (this.
|
|
2789
|
-
for (const
|
|
2790
|
-
if (
|
|
2791
|
-
|
|
2792
|
-
this.secretBuffer = "";
|
|
2793
|
-
this.deliver(secret);
|
|
2794
|
-
continue;
|
|
2795
|
-
}
|
|
2796
|
-
if (character === "") {
|
|
2797
|
-
this.output.write("\n");
|
|
2798
|
-
process.exit(130);
|
|
2799
|
-
}
|
|
2800
|
-
if (character === "\x7F" || character === "\b") {
|
|
2801
|
-
this.secretBuffer = this.secretBuffer.slice(0, -1);
|
|
2802
|
-
continue;
|
|
2935
|
+
if (this.keyHandler != null) {
|
|
2936
|
+
for (const key of splitKeys(chunk)) {
|
|
2937
|
+
if (this.keyHandler == null) {
|
|
2938
|
+
break;
|
|
2803
2939
|
}
|
|
2804
|
-
this.
|
|
2940
|
+
this.keyHandler(key);
|
|
2805
2941
|
}
|
|
2806
2942
|
return;
|
|
2807
2943
|
}
|
|
@@ -2810,13 +2946,289 @@ var Prompter = class {
|
|
|
2810
2946
|
while (newlineIndex >= 0) {
|
|
2811
2947
|
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
2812
2948
|
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
2813
|
-
this.
|
|
2949
|
+
if (this.lineWaiter != null) {
|
|
2950
|
+
const waiter = this.lineWaiter;
|
|
2951
|
+
this.lineWaiter = null;
|
|
2952
|
+
waiter(line);
|
|
2953
|
+
} else {
|
|
2954
|
+
this.pendingLines.push(line);
|
|
2955
|
+
}
|
|
2814
2956
|
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2815
2957
|
}
|
|
2816
2958
|
}
|
|
2959
|
+
async selectWithLines(question, items, startIndex) {
|
|
2960
|
+
const choices = items.filter((item) => "value" in item);
|
|
2961
|
+
const labelWidths = groupLabelWidths(items);
|
|
2962
|
+
this.output.write(`${this.symbols.question} ${question}
|
|
2963
|
+
`);
|
|
2964
|
+
let number = 0;
|
|
2965
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2966
|
+
const item = items[index];
|
|
2967
|
+
if ("value" in item == false) {
|
|
2968
|
+
this.output.write("\n");
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
number += 1;
|
|
2972
|
+
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
2973
|
+
if (item.hint != null && item.hint !== "") {
|
|
2974
|
+
line += ` ${item.hint}`;
|
|
2975
|
+
}
|
|
2976
|
+
this.output.write(`${line.trimEnd()}
|
|
2977
|
+
`);
|
|
2978
|
+
}
|
|
2979
|
+
const defaultChoice = items[startIndex];
|
|
2980
|
+
const defaultNumber = choices.indexOf(defaultChoice) + 1;
|
|
2981
|
+
for (; ; ) {
|
|
2982
|
+
this.output.write(`Choice (${defaultNumber}): `);
|
|
2983
|
+
const line = await this.readLine();
|
|
2984
|
+
if (line == null) {
|
|
2985
|
+
throw new PromptClosedError();
|
|
2986
|
+
}
|
|
2987
|
+
const answer = line.trim();
|
|
2988
|
+
if (answer === "") {
|
|
2989
|
+
return defaultChoice.value;
|
|
2990
|
+
}
|
|
2991
|
+
const index = Number(answer);
|
|
2992
|
+
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
2993
|
+
return choices[index - 1].value;
|
|
2994
|
+
}
|
|
2995
|
+
const lowered = answer.toLowerCase();
|
|
2996
|
+
const matched = choices.find((choice) => {
|
|
2997
|
+
const key = choice.key ?? choice.value;
|
|
2998
|
+
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
2999
|
+
});
|
|
3000
|
+
if (matched != null) {
|
|
3001
|
+
return matched.value;
|
|
3002
|
+
}
|
|
3003
|
+
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
3004
|
+
`);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
// ── TTY rendering ──────────────────────────────────────────────────────
|
|
3008
|
+
columns() {
|
|
3009
|
+
const columns = this.output.columns;
|
|
3010
|
+
if (columns == null || columns < 20) {
|
|
3011
|
+
return 80;
|
|
3012
|
+
}
|
|
3013
|
+
return columns;
|
|
3014
|
+
}
|
|
3015
|
+
/** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
|
|
3016
|
+
fitLine(parts, width) {
|
|
3017
|
+
let used = 0;
|
|
3018
|
+
let line = "";
|
|
3019
|
+
for (const part of parts) {
|
|
3020
|
+
let text = "";
|
|
3021
|
+
for (const character of part.text) {
|
|
3022
|
+
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
3023
|
+
if (used + characterWidth > width) {
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
used += characterWidth;
|
|
3027
|
+
text += character;
|
|
3028
|
+
}
|
|
3029
|
+
if (part.paint != null) {
|
|
3030
|
+
line += part.paint(text);
|
|
3031
|
+
} else {
|
|
3032
|
+
line += text;
|
|
3033
|
+
}
|
|
3034
|
+
if (used >= width) {
|
|
3035
|
+
break;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
return line;
|
|
3039
|
+
}
|
|
3040
|
+
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3041
|
+
writeAnswered(question, answer) {
|
|
3042
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
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) {
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3054
|
+
const used = displayWidth(plainPrompt) + displayWidth(typed);
|
|
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);
|
|
3062
|
+
}
|
|
3063
|
+
selectWithKeys(question, items, startIndex, cancelValue) {
|
|
3064
|
+
const ttyInput = this.input;
|
|
3065
|
+
const selectableIndexes = items.flatMap((item, index) => {
|
|
3066
|
+
if ("value" in item) {
|
|
3067
|
+
return [index];
|
|
3068
|
+
}
|
|
3069
|
+
return [];
|
|
3070
|
+
});
|
|
3071
|
+
const labelWidths = groupLabelWidths(items);
|
|
3072
|
+
const ruleWidth = Math.min(Math.max(...labelWidths), 24);
|
|
3073
|
+
let active = startIndex;
|
|
3074
|
+
let renderedLines = 0;
|
|
3075
|
+
const draw = (lines) => {
|
|
3076
|
+
let text = "";
|
|
3077
|
+
if (renderedLines > 1) {
|
|
3078
|
+
text += `${ESC}[${renderedLines - 1}A`;
|
|
3079
|
+
}
|
|
3080
|
+
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
3081
|
+
this.output.write(text);
|
|
3082
|
+
renderedLines = lines.length;
|
|
3083
|
+
};
|
|
3084
|
+
const buildLines = () => {
|
|
3085
|
+
const width = this.columns() - 1;
|
|
3086
|
+
const lines = [
|
|
3087
|
+
this.fitLine([
|
|
3088
|
+
{ text: this.symbols.question, paint: this.style.accent },
|
|
3089
|
+
{ text: " " },
|
|
3090
|
+
{ text: question, paint: this.style.bold }
|
|
3091
|
+
], width)
|
|
3092
|
+
];
|
|
3093
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3094
|
+
const item = items[index];
|
|
3095
|
+
if ("value" in item == false) {
|
|
3096
|
+
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
const isActive = index === active;
|
|
3100
|
+
const parts = [];
|
|
3101
|
+
if (isActive == true) {
|
|
3102
|
+
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
3103
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
3104
|
+
} else {
|
|
3105
|
+
parts.push({ text: " " });
|
|
3106
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
3107
|
+
}
|
|
3108
|
+
if (item.hint != null && item.hint !== "") {
|
|
3109
|
+
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
3110
|
+
}
|
|
3111
|
+
lines.push(this.fitLine(parts, width));
|
|
3112
|
+
}
|
|
3113
|
+
let footer = "\u2191\u2193 move \xB7 enter select";
|
|
3114
|
+
if (cancelValue != null) {
|
|
3115
|
+
footer += " \xB7 esc back";
|
|
3116
|
+
}
|
|
3117
|
+
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3118
|
+
return lines;
|
|
3119
|
+
};
|
|
3120
|
+
const move = (step) => {
|
|
3121
|
+
const position = selectableIndexes.indexOf(active);
|
|
3122
|
+
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
3123
|
+
active = selectableIndexes[next];
|
|
3124
|
+
};
|
|
3125
|
+
return new Promise((resolve, reject) => {
|
|
3126
|
+
const finish = () => {
|
|
3127
|
+
this.keyHandler = null;
|
|
3128
|
+
ttyInput.setRawMode?.(false);
|
|
3129
|
+
draw([]);
|
|
3130
|
+
this.output.write(`${ESC}[?25h`);
|
|
3131
|
+
};
|
|
3132
|
+
this.keyHandler = (key) => {
|
|
3133
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3134
|
+
finish();
|
|
3135
|
+
reject(new PromptClosedError());
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3139
|
+
const picked = items[active];
|
|
3140
|
+
finish();
|
|
3141
|
+
this.writeAnswered(question, picked.label);
|
|
3142
|
+
resolve(picked.value);
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (key === ESC) {
|
|
3146
|
+
if (cancelValue == null) {
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
finish();
|
|
3150
|
+
resolve(cancelValue);
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (KEY_UP.includes(key) == true) {
|
|
3154
|
+
move(-1);
|
|
3155
|
+
} else if (KEY_DOWN.includes(key) == true) {
|
|
3156
|
+
move(1);
|
|
3157
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3158
|
+
active = selectableIndexes[0];
|
|
3159
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3160
|
+
active = selectableIndexes[selectableIndexes.length - 1];
|
|
3161
|
+
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
3162
|
+
active = selectableIndexes[Number(key) - 1];
|
|
3163
|
+
} else {
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
draw(buildLines());
|
|
3167
|
+
};
|
|
3168
|
+
ttyInput.setRawMode?.(true);
|
|
3169
|
+
this.output.write(`${ESC}[?25l`);
|
|
3170
|
+
draw(buildLines());
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
2817
3173
|
};
|
|
2818
|
-
var
|
|
3174
|
+
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3175
|
+
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3176
|
+
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
2819
3177
|
var CLEAR_ANSWER = "-";
|
|
3178
|
+
var OTHER_KEY_PATH = "\0other";
|
|
3179
|
+
function findPrivateKeys() {
|
|
3180
|
+
const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
|
|
3181
|
+
let names = [];
|
|
3182
|
+
try {
|
|
3183
|
+
names = fs6__default.default.readdirSync(sshDirectory);
|
|
3184
|
+
} catch {
|
|
3185
|
+
return [];
|
|
3186
|
+
}
|
|
3187
|
+
const keys = [];
|
|
3188
|
+
for (const name of names) {
|
|
3189
|
+
if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
const fullPath = path__default.default.join(sshDirectory, name);
|
|
3193
|
+
let head = "";
|
|
3194
|
+
try {
|
|
3195
|
+
const stat = fs6__default.default.statSync(fullPath);
|
|
3196
|
+
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
const descriptor = fs6__default.default.openSync(fullPath, "r");
|
|
3200
|
+
const buffer = Buffer.alloc(64);
|
|
3201
|
+
const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3202
|
+
fs6__default.default.closeSync(descriptor);
|
|
3203
|
+
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3204
|
+
} catch {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
|
|
3208
|
+
const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
|
|
3209
|
+
if (isPemKey == false && isPuttyKey == false) {
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
let hint = "";
|
|
3213
|
+
try {
|
|
3214
|
+
const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3215
|
+
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3216
|
+
if (publicKey.length > 2) {
|
|
3217
|
+
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
3218
|
+
}
|
|
3219
|
+
} catch {
|
|
3220
|
+
}
|
|
3221
|
+
keys.push({ path: `~/.ssh/${name}`, hint });
|
|
3222
|
+
}
|
|
3223
|
+
const rank = (key) => {
|
|
3224
|
+
const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
|
|
3225
|
+
if (index < 0) {
|
|
3226
|
+
return PREFERRED_KEY_NAMES.length;
|
|
3227
|
+
}
|
|
3228
|
+
return index;
|
|
3229
|
+
};
|
|
3230
|
+
return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
|
|
3231
|
+
}
|
|
2820
3232
|
async function askPort(prompter, question, current) {
|
|
2821
3233
|
for (; ; ) {
|
|
2822
3234
|
const answer = await prompter.ask(question, String(current));
|
|
@@ -2835,11 +3247,11 @@ async function askOptional(prompter, question, current) {
|
|
|
2835
3247
|
return answer;
|
|
2836
3248
|
}
|
|
2837
3249
|
async function askOptionalSecret(prompter, question, current) {
|
|
2838
|
-
let suffix = "
|
|
3250
|
+
let suffix = "(empty for none)";
|
|
2839
3251
|
if (current != null) {
|
|
2840
|
-
suffix = `
|
|
3252
|
+
suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
|
|
2841
3253
|
}
|
|
2842
|
-
const answer = await prompter.askSecret(`${question}${suffix}`);
|
|
3254
|
+
const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
|
|
2843
3255
|
if (answer === CLEAR_ANSWER) {
|
|
2844
3256
|
return void 0;
|
|
2845
3257
|
}
|
|
@@ -2848,11 +3260,42 @@ async function askOptionalSecret(prompter, question, current) {
|
|
|
2848
3260
|
}
|
|
2849
3261
|
return answer;
|
|
2850
3262
|
}
|
|
3263
|
+
async function askKeyPath(prompter, current) {
|
|
3264
|
+
const found = findPrivateKeys();
|
|
3265
|
+
let picked = OTHER_KEY_PATH;
|
|
3266
|
+
if (found.length > 0 || current != null) {
|
|
3267
|
+
const items = [];
|
|
3268
|
+
if (current != null && found.some((key) => key.path === current) == false) {
|
|
3269
|
+
items.push({ value: current, label: current, hint: "current" });
|
|
3270
|
+
}
|
|
3271
|
+
for (const key of found) {
|
|
3272
|
+
items.push({ value: key.path, label: key.path, hint: key.hint });
|
|
3273
|
+
}
|
|
3274
|
+
items.push({ separator: true });
|
|
3275
|
+
items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
|
|
3276
|
+
picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
|
|
3277
|
+
}
|
|
3278
|
+
if (picked !== OTHER_KEY_PATH) {
|
|
3279
|
+
return picked;
|
|
3280
|
+
}
|
|
3281
|
+
for (; ; ) {
|
|
3282
|
+
const typed = await prompter.askRequired("Private key path");
|
|
3283
|
+
if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
|
|
3284
|
+
return typed;
|
|
3285
|
+
}
|
|
3286
|
+
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
3287
|
+
`);
|
|
3288
|
+
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
3289
|
+
if (useAnyway == true) {
|
|
3290
|
+
return typed;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
2851
3294
|
async function askHostQuestions(prompter, existing) {
|
|
2852
3295
|
if (existing != null) {
|
|
2853
|
-
prompter.write(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
|
|
3296
|
+
prompter.write(prompter.paint.dim(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
|
|
2854
3297
|
|
|
2855
|
-
`);
|
|
3298
|
+
`));
|
|
2856
3299
|
}
|
|
2857
3300
|
const host = await prompter.askRequired("Host (hostname or IP)", existing?.host);
|
|
2858
3301
|
const port = await askPort(prompter, "Port", existing?.port ?? DEFAULT_SSH_PORT);
|
|
@@ -2866,60 +3309,60 @@ async function askHostQuestions(prompter, existing) {
|
|
|
2866
3309
|
}
|
|
2867
3310
|
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
2868
3311
|
}
|
|
2869
|
-
const auth = await prompter.
|
|
3312
|
+
const auth = await prompter.select(
|
|
2870
3313
|
"Authentication",
|
|
2871
3314
|
[
|
|
2872
|
-
{
|
|
2873
|
-
{
|
|
2874
|
-
{
|
|
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" }
|
|
2875
3318
|
],
|
|
2876
|
-
existing?.auth ?? "key"
|
|
3319
|
+
{ defaultValue: existing?.auth ?? "key" }
|
|
2877
3320
|
);
|
|
2878
3321
|
let keyPath;
|
|
2879
3322
|
let passphrase;
|
|
2880
3323
|
let password;
|
|
2881
3324
|
if (auth === "key") {
|
|
2882
|
-
|
|
2883
|
-
if (defaultKeyPath == null) {
|
|
2884
|
-
defaultKeyPath = DEFAULT_KEY_CANDIDATES.find((candidate) => fs7__default.default.existsSync(expandHomePath(candidate)) == true);
|
|
2885
|
-
}
|
|
2886
|
-
for (; ; ) {
|
|
2887
|
-
keyPath = await prompter.askRequired("Private key path", defaultKeyPath);
|
|
2888
|
-
if (fs7__default.default.existsSync(expandHomePath(keyPath)) == true) {
|
|
2889
|
-
break;
|
|
2890
|
-
}
|
|
2891
|
-
prompter.write(` ${expandHomePath(keyPath)} does not exist.
|
|
2892
|
-
`);
|
|
2893
|
-
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
2894
|
-
if (useAnyway == true) {
|
|
2895
|
-
break;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
3325
|
+
keyPath = await askKeyPath(prompter, existing?.keyPath);
|
|
2898
3326
|
passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
|
|
2899
3327
|
}
|
|
2900
3328
|
if (auth === "password") {
|
|
2901
|
-
prompter.write(" The password is written into the config file as plain text. A key file is safer.\n");
|
|
2902
3329
|
password = await askOptionalSecret(prompter, "Password", existing?.password);
|
|
2903
3330
|
if (password == null) {
|
|
2904
3331
|
prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
|
|
2905
3332
|
}
|
|
2906
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
|
+
);
|
|
2907
3352
|
let jumpHost;
|
|
2908
3353
|
let jumpPort;
|
|
2909
3354
|
let jumpUser;
|
|
2910
|
-
|
|
2911
|
-
if (
|
|
3355
|
+
let proxyCommand;
|
|
3356
|
+
if (route === "jump" || route === "jump-proxy") {
|
|
2912
3357
|
jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
|
|
2913
3358
|
jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
|
|
2914
3359
|
jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
|
|
2915
3360
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
if (viaProxy == true) {
|
|
2919
|
-
prompter.write(" %h / %p / %r expand to the first hop's host, port and user.\n");
|
|
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"));
|
|
2920
3363
|
proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
2921
3364
|
}
|
|
2922
|
-
const description = await askOptional(prompter, "Description shown to the agent
|
|
3365
|
+
const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
|
|
2923
3366
|
const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
|
|
2924
3367
|
let currentAllowedPaths;
|
|
2925
3368
|
if (existing != null && existing.allowedPaths.length > 0) {
|
|
@@ -2928,7 +3371,7 @@ async function askHostQuestions(prompter, existing) {
|
|
|
2928
3371
|
let allowedPaths = [];
|
|
2929
3372
|
const allowedPathsAnswer = await askOptional(
|
|
2930
3373
|
prompter,
|
|
2931
|
-
"
|
|
3374
|
+
"Limit file tools to path prefixes (comma-separated, empty = anywhere)",
|
|
2932
3375
|
currentAllowedPaths
|
|
2933
3376
|
);
|
|
2934
3377
|
if (allowedPathsAnswer != null) {
|
|
@@ -3004,10 +3447,10 @@ function readMcpServersJson(document) {
|
|
|
3004
3447
|
return entries;
|
|
3005
3448
|
}
|
|
3006
3449
|
function readDocument(filePath) {
|
|
3007
|
-
if (
|
|
3450
|
+
if (fs6__default.default.existsSync(filePath) == false) {
|
|
3008
3451
|
return {};
|
|
3009
3452
|
}
|
|
3010
|
-
const text =
|
|
3453
|
+
const text = fs6__default.default.readFileSync(filePath, "utf8");
|
|
3011
3454
|
if (text.trim() === "") {
|
|
3012
3455
|
return {};
|
|
3013
3456
|
}
|
|
@@ -3029,15 +3472,15 @@ function readDocument(filePath) {
|
|
|
3029
3472
|
function createJsonFileTarget(options) {
|
|
3030
3473
|
const { id, label, filePath } = options;
|
|
3031
3474
|
function writeDocument(document) {
|
|
3032
|
-
|
|
3033
|
-
|
|
3475
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
|
|
3476
|
+
fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
3034
3477
|
`, "utf8");
|
|
3035
3478
|
}
|
|
3036
3479
|
return {
|
|
3037
3480
|
id,
|
|
3038
3481
|
label,
|
|
3039
3482
|
location: filePath,
|
|
3040
|
-
detected:
|
|
3483
|
+
detected: fs6__default.default.existsSync(filePath),
|
|
3041
3484
|
readEntries() {
|
|
3042
3485
|
const document = readDocument(filePath);
|
|
3043
3486
|
return readMcpServersJson(document);
|
|
@@ -3067,7 +3510,7 @@ function createJsonFileTarget(options) {
|
|
|
3067
3510
|
}
|
|
3068
3511
|
|
|
3069
3512
|
// src/modules/manage/targets/claude-code.ts
|
|
3070
|
-
var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(
|
|
3513
|
+
var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".claude.json");
|
|
3071
3514
|
var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
|
|
3072
3515
|
function toClaudeAddJsonPayload(entry) {
|
|
3073
3516
|
return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
|
|
@@ -3113,10 +3556,10 @@ function runClaude(executable, args) {
|
|
|
3113
3556
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
3114
3557
|
}
|
|
3115
3558
|
function readClaudeEntries() {
|
|
3116
|
-
if (
|
|
3559
|
+
if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
3117
3560
|
return {};
|
|
3118
3561
|
}
|
|
3119
|
-
const text =
|
|
3562
|
+
const text = fs6__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
3120
3563
|
let document;
|
|
3121
3564
|
try {
|
|
3122
3565
|
document = JSON.parse(text);
|
|
@@ -3135,7 +3578,7 @@ function createClaudeCodeTarget() {
|
|
|
3135
3578
|
id: "claude-code",
|
|
3136
3579
|
label: "Claude Code (user scope)",
|
|
3137
3580
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
3138
|
-
detected: executable != null ||
|
|
3581
|
+
detected: executable != null || fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
3139
3582
|
readEntries: readClaudeEntries,
|
|
3140
3583
|
writeEntry(name, entry) {
|
|
3141
3584
|
if (executable == null) {
|
|
@@ -3587,24 +4030,24 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
3587
4030
|
}
|
|
3588
4031
|
|
|
3589
4032
|
// src/modules/manage/targets/codex.ts
|
|
3590
|
-
var CODEX_CONFIG_PATH = path__default.default.join(
|
|
4033
|
+
var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
|
|
3591
4034
|
function createCodexTarget() {
|
|
3592
4035
|
function readText() {
|
|
3593
|
-
if (
|
|
4036
|
+
if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
3594
4037
|
return "";
|
|
3595
4038
|
}
|
|
3596
|
-
return
|
|
4039
|
+
return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
3597
4040
|
}
|
|
3598
4041
|
function writeText(text) {
|
|
3599
|
-
|
|
3600
|
-
|
|
4042
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4043
|
+
fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
3601
4044
|
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
3602
4045
|
}
|
|
3603
4046
|
return {
|
|
3604
4047
|
id: "codex",
|
|
3605
4048
|
label: "Codex CLI",
|
|
3606
4049
|
location: CODEX_CONFIG_PATH,
|
|
3607
|
-
detected: findExecutable("codex") != null ||
|
|
4050
|
+
detected: findExecutable("codex") != null || fs6__default.default.existsSync(CODEX_CONFIG_PATH) == true,
|
|
3608
4051
|
readEntries() {
|
|
3609
4052
|
const text = readText();
|
|
3610
4053
|
return parseMcpServersToml(text);
|
|
@@ -3625,20 +4068,20 @@ function createCodexTarget() {
|
|
|
3625
4068
|
// src/modules/manage/targets/index.ts
|
|
3626
4069
|
function claudeDesktopConfigPath() {
|
|
3627
4070
|
if (process.platform === "win32") {
|
|
3628
|
-
const appData = process.env.APPDATA ?? path__default.default.join(
|
|
4071
|
+
const appData = process.env.APPDATA ?? path__default.default.join(os5__default.default.homedir(), "AppData", "Roaming");
|
|
3629
4072
|
return path__default.default.join(appData, "Claude", "claude_desktop_config.json");
|
|
3630
4073
|
}
|
|
3631
4074
|
if (process.platform === "darwin") {
|
|
3632
|
-
return path__default.default.join(
|
|
4075
|
+
return path__default.default.join(os5__default.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
3633
4076
|
}
|
|
3634
|
-
return path__default.default.join(
|
|
4077
|
+
return path__default.default.join(os5__default.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
3635
4078
|
}
|
|
3636
4079
|
function listConfigTargets() {
|
|
3637
4080
|
const targets = [
|
|
3638
4081
|
createClaudeCodeTarget(),
|
|
3639
4082
|
createCodexTarget(),
|
|
3640
4083
|
createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
|
|
3641
|
-
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(
|
|
4084
|
+
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(os5__default.default.homedir(), ".cursor", "mcp.json") })
|
|
3642
4085
|
];
|
|
3643
4086
|
const detected = targets.filter((target) => target.detected == true);
|
|
3644
4087
|
const others = targets.filter((target) => target.detected == false);
|
|
@@ -3652,6 +4095,7 @@ var SECRET_ENV_NAMES = [
|
|
|
3652
4095
|
SINGLE_HOST_ENV.JUMP_PASSWORD,
|
|
3653
4096
|
SINGLE_HOST_ENV.JUMP_PASSPHRASE
|
|
3654
4097
|
];
|
|
4098
|
+
var HOST_VALUE_PREFIX = "host:";
|
|
3655
4099
|
function errorMessage(error) {
|
|
3656
4100
|
if (error instanceof Error) {
|
|
3657
4101
|
return error.message;
|
|
@@ -3679,14 +4123,14 @@ function formatForTarget(target, name, entry) {
|
|
|
3679
4123
|
function indent(text) {
|
|
3680
4124
|
return text.replace(/\n$/, "").split("\n").map((line) => ` ${line}`).join("\n");
|
|
3681
4125
|
}
|
|
3682
|
-
function reportWrite(
|
|
4126
|
+
function reportWrite(prompter, result, action) {
|
|
3683
4127
|
if (result.status === "written") {
|
|
3684
|
-
|
|
4128
|
+
prompter.write(` ${prompter.paint.success(action)} ${prompter.paint.dim(result.detail)}
|
|
3685
4129
|
`);
|
|
3686
|
-
|
|
4130
|
+
prompter.write(prompter.paint.dim(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3687
4131
|
return;
|
|
3688
4132
|
}
|
|
3689
|
-
|
|
4133
|
+
prompter.write(` Not ${action.toLowerCase()}: ${result.detail}
|
|
3690
4134
|
|
|
3691
4135
|
`);
|
|
3692
4136
|
}
|
|
@@ -3717,13 +4161,13 @@ async function verifyAnswers(prompter, output, initial) {
|
|
|
3717
4161
|
output.write("\n");
|
|
3718
4162
|
if (result.connected == true) {
|
|
3719
4163
|
if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
|
|
3720
|
-
const pin = await prompter.askYesNo(
|
|
4164
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
3721
4165
|
if (pin == true) {
|
|
3722
4166
|
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
|
|
3723
4167
|
}
|
|
3724
4168
|
}
|
|
3725
4169
|
if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
|
|
3726
|
-
const pin = await prompter.askYesNo(
|
|
4170
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3727
4171
|
if (pin == true) {
|
|
3728
4172
|
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
|
|
3729
4173
|
}
|
|
@@ -3749,23 +4193,23 @@ async function verifyAnswers(prompter, output, initial) {
|
|
|
3749
4193
|
}
|
|
3750
4194
|
}
|
|
3751
4195
|
}
|
|
3752
|
-
const next = await prompter.
|
|
3753
|
-
"What now?",
|
|
4196
|
+
const next = await prompter.select(
|
|
4197
|
+
"The check failed. What now?",
|
|
3754
4198
|
[
|
|
3755
|
-
{ key: "
|
|
3756
|
-
{ key: "
|
|
3757
|
-
{ key: "w", label: "
|
|
3758
|
-
{ key: "c", label: "
|
|
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" }
|
|
3759
4203
|
],
|
|
3760
|
-
"
|
|
4204
|
+
{ defaultValue: "edit", cancelValue: "cancel" }
|
|
3761
4205
|
);
|
|
3762
|
-
if (next === "
|
|
4206
|
+
if (next === "cancel") {
|
|
3763
4207
|
return null;
|
|
3764
4208
|
}
|
|
3765
|
-
if (next === "
|
|
4209
|
+
if (next === "write") {
|
|
3766
4210
|
return answers;
|
|
3767
4211
|
}
|
|
3768
|
-
if (next === "
|
|
4212
|
+
if (next === "edit") {
|
|
3769
4213
|
output.write("\n");
|
|
3770
4214
|
answers = await askHostQuestions(prompter, answers);
|
|
3771
4215
|
}
|
|
@@ -3776,10 +4220,12 @@ async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
|
3776
4220
|
if (existingName != null) {
|
|
3777
4221
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
3778
4222
|
output.write(`
|
|
3779
|
-
Editing ${existingName}
|
|
4223
|
+
${prompter.paint.bold(`Editing ${existingName}`)}
|
|
3780
4224
|
`);
|
|
3781
4225
|
} else {
|
|
3782
|
-
output.write(
|
|
4226
|
+
output.write(`
|
|
4227
|
+
${prompter.paint.bold("Adding a host")}
|
|
4228
|
+
`);
|
|
3783
4229
|
}
|
|
3784
4230
|
const asked = await askHostQuestions(prompter, existing);
|
|
3785
4231
|
output.write("\n");
|
|
@@ -3797,7 +4243,7 @@ Editing ${existingName}
|
|
|
3797
4243
|
}
|
|
3798
4244
|
const entry = answersToEntry(answers);
|
|
3799
4245
|
output.write(`
|
|
3800
|
-
This goes into ${target.location}
|
|
4246
|
+
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
3801
4247
|
|
|
3802
4248
|
`);
|
|
3803
4249
|
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
@@ -3805,11 +4251,11 @@ Editing ${existingName}
|
|
|
3805
4251
|
`);
|
|
3806
4252
|
const confirmed = await prompter.askYesNo("Write it?", true);
|
|
3807
4253
|
if (confirmed == false) {
|
|
3808
|
-
output.write(" Not written.
|
|
4254
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
3809
4255
|
return;
|
|
3810
4256
|
}
|
|
3811
4257
|
const written = target.writeEntry(answers.name, entry);
|
|
3812
|
-
reportWrite(
|
|
4258
|
+
reportWrite(prompter, written, "Written");
|
|
3813
4259
|
if (written.status === "written" && existingName != null && existingName !== answers.name) {
|
|
3814
4260
|
const removed = target.removeEntry(existingName);
|
|
3815
4261
|
if (removed.status === "unavailable") {
|
|
@@ -3834,14 +4280,14 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3834
4280
|
const env = { ...entry.env };
|
|
3835
4281
|
let changed = false;
|
|
3836
4282
|
if (result.hostKeyFingerprint != null && env[SINGLE_HOST_ENV.HOST_KEY] == null) {
|
|
3837
|
-
const pin = await prompter.askYesNo(
|
|
4283
|
+
const pin = await prompter.askYesNo("Pin this host key?", true);
|
|
3838
4284
|
if (pin == true) {
|
|
3839
4285
|
env[SINGLE_HOST_ENV.HOST_KEY] = result.hostKeyFingerprint;
|
|
3840
4286
|
changed = true;
|
|
3841
4287
|
}
|
|
3842
4288
|
}
|
|
3843
4289
|
if (result.jumpHostKeyFingerprint != null && env[SINGLE_HOST_ENV.JUMP_HOST_KEY] == null) {
|
|
3844
|
-
const pin = await prompter.askYesNo(
|
|
4290
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3845
4291
|
if (pin == true) {
|
|
3846
4292
|
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = result.jumpHostKeyFingerprint;
|
|
3847
4293
|
changed = true;
|
|
@@ -3849,76 +4295,79 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3849
4295
|
}
|
|
3850
4296
|
if (changed == true) {
|
|
3851
4297
|
const written = target.writeEntry(name, { ...entry, env });
|
|
3852
|
-
reportWrite(
|
|
4298
|
+
reportWrite(prompter, written, "Pinned");
|
|
3853
4299
|
}
|
|
3854
4300
|
}
|
|
3855
|
-
function printHost(
|
|
3856
|
-
|
|
3857
|
-
|
|
4301
|
+
function printHost(prompter, name, entry) {
|
|
4302
|
+
prompter.write(`
|
|
4303
|
+
${prompter.paint.dim('JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):')}
|
|
3858
4304
|
|
|
3859
4305
|
`);
|
|
3860
|
-
|
|
3861
|
-
output.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4306
|
+
prompter.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
|
|
3862
4307
|
|
|
3863
4308
|
`);
|
|
3864
|
-
|
|
3865
|
-
output.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4309
|
+
prompter.write(` ${prompter.paint.dim("TOML \u2014 Codex (~/.codex/config.toml):")}
|
|
3866
4310
|
|
|
3867
4311
|
`);
|
|
3868
|
-
}
|
|
3869
|
-
|
|
3870
|
-
if (names.length === 0) {
|
|
3871
|
-
output.write(" No hosts yet \u2014 add one with a.\n\n");
|
|
3872
|
-
return null;
|
|
3873
|
-
}
|
|
3874
|
-
if (names.length === 1) {
|
|
3875
|
-
return names[0];
|
|
3876
|
-
}
|
|
3877
|
-
for (; ; ) {
|
|
3878
|
-
const answer = await prompter.ask("Which host (number or name, empty to go back)");
|
|
3879
|
-
if (answer === "") {
|
|
3880
|
-
return null;
|
|
3881
|
-
}
|
|
3882
|
-
const index = Number(answer);
|
|
3883
|
-
if (Number.isInteger(index) == true && index >= 1 && index <= names.length) {
|
|
3884
|
-
return names[index - 1];
|
|
3885
|
-
}
|
|
3886
|
-
if (names.includes(answer) == true) {
|
|
3887
|
-
return answer;
|
|
3888
|
-
}
|
|
3889
|
-
output.write(` No host "${answer}".
|
|
4312
|
+
prompter.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4313
|
+
|
|
3890
4314
|
`);
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
function renderHostList(output, target, entries) {
|
|
3894
|
-
const names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3895
|
-
const otherCount = Object.keys(entries).length - names.length;
|
|
3896
|
-
output.write(`
|
|
3897
|
-
${target.label} \u2014 ${target.location}
|
|
4315
|
+
prompter.write(` ${prompter.paint.dim("Claude Code CLI (bash / zsh / Git Bash):")}
|
|
3898
4316
|
|
|
3899
4317
|
`);
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
} else {
|
|
3903
|
-
const rows = names.map((name, index) => {
|
|
3904
|
-
const summary = summarizeEntry(entries[name]);
|
|
3905
|
-
return [String(index + 1), name, summary.endpoint, summary.auth, summary.notes];
|
|
3906
|
-
});
|
|
3907
|
-
const header = ["#", "Server", "Endpoint", "Auth", "Notes"];
|
|
3908
|
-
const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => row[column].length)));
|
|
3909
|
-
for (const row of [header, ...rows]) {
|
|
3910
|
-
const line = row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
3911
|
-
output.write(` ${line}
|
|
4318
|
+
prompter.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4319
|
+
|
|
3912
4320
|
`);
|
|
4321
|
+
}
|
|
4322
|
+
function buildHostItems(names, entries) {
|
|
4323
|
+
const nameWidth = Math.max(...names.map((name) => displayWidth(name)));
|
|
4324
|
+
const summaries = names.map((name) => summarizeEntry(entries[name]));
|
|
4325
|
+
const endpointWidth = Math.max(...summaries.map((summary) => displayWidth(summary.endpoint)));
|
|
4326
|
+
return names.map((name, index) => {
|
|
4327
|
+
const summary = summaries[index];
|
|
4328
|
+
const hintParts = [summary.auth];
|
|
4329
|
+
if (summary.notes !== "") {
|
|
4330
|
+
hintParts.push(summary.notes);
|
|
3913
4331
|
}
|
|
4332
|
+
return {
|
|
4333
|
+
value: `${HOST_VALUE_PREFIX}${name}`,
|
|
4334
|
+
key: name,
|
|
4335
|
+
label: `${name.padEnd(nameWidth)} ${summary.endpoint.padEnd(endpointWidth)}`,
|
|
4336
|
+
hint: hintParts.join(" \xB7 ")
|
|
4337
|
+
};
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
async function manageHost(prompter, output, target, entries, name) {
|
|
4341
|
+
const action = await prompter.select(
|
|
4342
|
+
name,
|
|
4343
|
+
[
|
|
4344
|
+
{ value: "edit", key: "e", label: "Edit", hint: "change the answers, re-check, re-write" },
|
|
4345
|
+
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4346
|
+
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4347
|
+
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4348
|
+
{ separator: true },
|
|
4349
|
+
{ value: "back", key: "b", label: "Back" }
|
|
4350
|
+
],
|
|
4351
|
+
{ defaultValue: "edit", cancelValue: "back" }
|
|
4352
|
+
);
|
|
4353
|
+
if (action === "edit") {
|
|
4354
|
+
await addOrEditHost(prompter, output, target, entries, name);
|
|
4355
|
+
return;
|
|
3914
4356
|
}
|
|
3915
|
-
if (
|
|
3916
|
-
output
|
|
3917
|
-
|
|
3918
|
-
|
|
4357
|
+
if (action === "check") {
|
|
4358
|
+
await checkHost(prompter, output, target, name, entries[name]);
|
|
4359
|
+
return;
|
|
4360
|
+
}
|
|
4361
|
+
if (action === "print") {
|
|
4362
|
+
printHost(prompter, name, entries[name]);
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
if (action === "remove") {
|
|
4366
|
+
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4367
|
+
if (remove == true) {
|
|
4368
|
+
reportWrite(prompter, target.removeEntry(name), "Removed");
|
|
4369
|
+
}
|
|
3919
4370
|
}
|
|
3920
|
-
output.write("\n");
|
|
3921
|
-
return names;
|
|
3922
4371
|
}
|
|
3923
4372
|
async function runConfigCommand(options) {
|
|
3924
4373
|
const { input, output } = options;
|
|
@@ -3934,81 +4383,88 @@ async function runConfigCommand(options) {
|
|
|
3934
4383
|
target = requested;
|
|
3935
4384
|
}
|
|
3936
4385
|
const prompter = new Prompter(input, output);
|
|
4386
|
+
const paint = prompter.paint;
|
|
3937
4387
|
try {
|
|
3938
4388
|
for (; ; ) {
|
|
3939
4389
|
let entries = {};
|
|
3940
4390
|
let names = [];
|
|
3941
|
-
let
|
|
4391
|
+
let readError = null;
|
|
3942
4392
|
try {
|
|
3943
4393
|
entries = target.readEntries();
|
|
3944
|
-
names =
|
|
4394
|
+
names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3945
4395
|
} catch (ex) {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4396
|
+
readError = errorMessage(ex);
|
|
4397
|
+
}
|
|
4398
|
+
output.write(`
|
|
4399
|
+
${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
4400
|
+
`);
|
|
4401
|
+
if (readError != null) {
|
|
4402
|
+
output.write(` Cannot read it: ${readError}
|
|
3951
4403
|
Fix the file by hand, or switch to another client.
|
|
3952
|
-
|
|
3953
4404
|
`);
|
|
4405
|
+
} else {
|
|
4406
|
+
const otherCount = Object.keys(entries).length - names.length;
|
|
4407
|
+
if (otherCount > 0) {
|
|
4408
|
+
output.write(paint.dim(` ${otherCount} other MCP server(s) in this file are not managed here.
|
|
4409
|
+
`));
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
output.write("\n");
|
|
4413
|
+
const items = [];
|
|
4414
|
+
if (names.length > 0) {
|
|
4415
|
+
items.push(...buildHostItems(names, entries));
|
|
4416
|
+
items.push({ separator: true });
|
|
3954
4417
|
}
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
menu = " a) add e) edit r) remove c) check p) print t) switch client q) quit";
|
|
3958
|
-
} else if (readable == true) {
|
|
3959
|
-
menu = " a) add t) switch client q) quit";
|
|
4418
|
+
if (readError == null) {
|
|
4419
|
+
items.push({ value: "add", key: "a", label: "Add a host" });
|
|
3960
4420
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4421
|
+
const otherTargets = targets.filter((candidate) => candidate.id !== target.id);
|
|
4422
|
+
if (otherTargets.length > 0) {
|
|
4423
|
+
const detectedOthers = otherTargets.filter((candidate) => candidate.detected == true).map((candidate) => candidate.label);
|
|
4424
|
+
let switchHint = "";
|
|
4425
|
+
if (detectedOthers.length > 0) {
|
|
4426
|
+
switchHint = `also found: ${detectedOthers.join(", ")}`;
|
|
4427
|
+
}
|
|
4428
|
+
items.push({ value: "switch", key: "t", label: "Switch client", hint: switchHint });
|
|
4429
|
+
}
|
|
4430
|
+
items.push({ value: "quit", key: "q", label: "Quit" });
|
|
4431
|
+
let question = "No SSH hosts here yet";
|
|
4432
|
+
if (names.length === 1) {
|
|
4433
|
+
question = "1 SSH host \u2014 pick it to manage it";
|
|
4434
|
+
} else if (names.length > 1) {
|
|
4435
|
+
question = `${names.length} SSH hosts \u2014 pick one to manage it`;
|
|
4436
|
+
}
|
|
4437
|
+
let defaultValue = "add";
|
|
4438
|
+
if (names.length > 0) {
|
|
4439
|
+
defaultValue = `${HOST_VALUE_PREFIX}${names[0]}`;
|
|
4440
|
+
}
|
|
4441
|
+
const picked = await prompter.select(question, items, { defaultValue, cancelValue: "quit" });
|
|
4442
|
+
if (picked === "quit") {
|
|
3965
4443
|
return 0;
|
|
3966
4444
|
}
|
|
3967
|
-
if (
|
|
3968
|
-
const
|
|
4445
|
+
if (picked === "switch") {
|
|
4446
|
+
const switched = await prompter.select(
|
|
3969
4447
|
"Which client?",
|
|
3970
4448
|
targets.map((candidate) => {
|
|
3971
|
-
let
|
|
3972
|
-
if (candidate.detected ==
|
|
3973
|
-
|
|
4449
|
+
let hint = candidate.location;
|
|
4450
|
+
if (candidate.detected == false) {
|
|
4451
|
+
hint += " \xB7 not found on this machine";
|
|
3974
4452
|
}
|
|
3975
|
-
return {
|
|
4453
|
+
return { value: candidate.id, label: candidate.label, hint };
|
|
3976
4454
|
}),
|
|
3977
|
-
target.id
|
|
4455
|
+
{ defaultValue: target.id, cancelValue: target.id }
|
|
3978
4456
|
);
|
|
3979
|
-
target = targets.find((candidate) => candidate.id ===
|
|
3980
|
-
continue;
|
|
3981
|
-
}
|
|
3982
|
-
if (readable == false) {
|
|
3983
|
-
output.write(" Not available while the file cannot be read.\n");
|
|
4457
|
+
target = targets.find((candidate) => candidate.id === switched) ?? target;
|
|
3984
4458
|
continue;
|
|
3985
4459
|
}
|
|
3986
4460
|
try {
|
|
3987
|
-
if (
|
|
4461
|
+
if (picked === "add") {
|
|
3988
4462
|
await addOrEditHost(prompter, output, target, entries);
|
|
3989
4463
|
continue;
|
|
3990
4464
|
}
|
|
3991
|
-
if (
|
|
3992
|
-
|
|
3993
|
-
if (name == null) {
|
|
3994
|
-
continue;
|
|
3995
|
-
}
|
|
3996
|
-
if (action === "e") {
|
|
3997
|
-
await addOrEditHost(prompter, output, target, entries, name);
|
|
3998
|
-
} else if (action === "r") {
|
|
3999
|
-
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4000
|
-
if (remove == true) {
|
|
4001
|
-
reportWrite(output, target.removeEntry(name), "Removed");
|
|
4002
|
-
}
|
|
4003
|
-
} else if (action === "c") {
|
|
4004
|
-
await checkHost(prompter, output, target, name, entries[name]);
|
|
4005
|
-
} else {
|
|
4006
|
-
printHost(output, name, entries[name]);
|
|
4007
|
-
}
|
|
4008
|
-
continue;
|
|
4465
|
+
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
4466
|
+
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
4009
4467
|
}
|
|
4010
|
-
output.write(` Unknown choice "${action}".
|
|
4011
|
-
`);
|
|
4012
4468
|
} catch (ex) {
|
|
4013
4469
|
if (ex instanceof PromptClosedError) {
|
|
4014
4470
|
throw ex;
|
|
@@ -4457,7 +4913,7 @@ ${file.content}`);
|
|
|
4457
4913
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4458
4914
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4459
4915
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
4460
|
-
if (
|
|
4916
|
+
if (fs6__default.default.existsSync(resolvedLocalPath) == false) {
|
|
4461
4917
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
4462
4918
|
}
|
|
4463
4919
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -4497,7 +4953,7 @@ ${file.content}`);
|
|
|
4497
4953
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4498
4954
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4499
4955
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
4500
|
-
|
|
4956
|
+
fs6__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
|
|
4501
4957
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
4502
4958
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
4503
4959
|
});
|