diff --git a/script-meta-v2.json b/script-meta-v2.json index 2cf7b3e..027dce4 100644 --- a/script-meta-v2.json +++ b/script-meta-v2.json @@ -218,6 +218,15 @@ "publish_time": 1737272626138, "update_time": 1737650670432 }, + "ssh-bun.ts": { + "script_name": "ssh-bun.ts", + "script_length": 6813, + "script_sha256": "d65f2335b82fc5f5376f1df34057d9feae12fe5f74dbfb572ce18f9a089b3a3a", + "script_full_url": "https://git.hatter.ink/hatter/ts-scripts/raw/branch/main/single-scripts/ssh-bun.ts", + "single_script_file": true, + "publish_time": 1770397234563, + "update_time": 1770397234563 + }, "ssh-verify.ts": { "script_name": "ssh-verify.ts", "script_length": 2854, @@ -229,12 +238,12 @@ }, "ssh.ts": { "script_name": "ssh.ts", - "script_length": 6813, - "script_sha256": "d65f2335b82fc5f5376f1df34057d9feae12fe5f74dbfb572ce18f9a089b3a3a", + "script_length": 6600, + "script_sha256": "32133f8fd4d0804dfed9f0db65dc4d4f34dd851ab7b7886fd572aa2ea4795a1a", "script_full_url": "https://git.hatter.ink/hatter/ts-scripts/raw/branch/main/single-scripts/ssh.ts", "single_script_file": true, "publish_time": 1768111677531, - "update_time": 1769359601445 + "update_time": 1770397234557 }, "tree.ts": { "script_name": "tree.ts", diff --git a/single-scripts/ssh-bun.ts b/single-scripts/ssh-bun.ts new file mode 100755 index 0000000..1458318 --- /dev/null +++ b/single-scripts/ssh-bun.ts @@ -0,0 +1,216 @@ +#!/usr/bin/env runts -- --runtime-bun + +// reference: https://git.hatter.ink/rust-scripts/scriptbase/src/branch/main/ssh-rs/src/main.rs + +import { spawn } from "node:child_process"; +import { parseArgs } from "node:util"; + +import fs from "node:fs"; +import os from "node:os"; +import JSON5 from "json5"; + +// reference: https://bun.com/docs/runtime/color +const GREEN = Bun.color("green", "ansi"); +const BLUE = Bun.color("lightblue", "ansi"); +const YELLOW = Bun.color("yellow", "ansi"); +const RESET = "\x1B[0m"; + +class SshTsArgs { + forwardAgent?: boolean; + proxy?: boolean; + help?: boolean; + host?: string; +} + +interface SshConfig { + default_forward_agent?: boolean; + default_proxy?: boolean; + default_username?: string; + profiles: Record; +} + +interface SshProfile { + default_username?: string; + alias?: string[]; + host: string; + proxy?: boolean; + forward_agent?: boolean; + comment?: string; +} + +interface UsernameAndHost { + username?: string; + host: string; +} + +function printSshConfig(sshConfig: SshConfig) { + const allProfiles = []; + let maxProfileNameLength = 0; + let maxProfileHostLength = 0; + for (const k in sshConfig.profiles) { + const sshProfile = sshConfig.profiles[k]; + allProfiles.push(k); + if (k.length > maxProfileNameLength) { + maxProfileNameLength = k.length; + } + if (sshProfile.host.length > maxProfileHostLength) { + maxProfileHostLength = sshProfile.host.length; + } + } + console.log( + `${GREEN}[OK ]${RESET} Total ${allProfiles.length} server(s):`, + ); + allProfiles.sort((a, b) => a.localeCompare(b)); + for (let i = 0; i < allProfiles.length; i++) { + const k = allProfiles[i]; + const sshProfile = sshConfig.profiles[k]; + const features = []; + if (sshProfile.proxy) features.push("proxy"); + if (sshProfile.forward_agent) features.push("forward_agent"); + console.log( + `- ${k}${ + " ".repeat(maxProfileNameLength - k.length) + } : ${BLUE}${sshProfile.host}${ + " ".repeat(maxProfileHostLength - sshProfile.host.length) + }${RESET} ${YELLOW}${ + (sshProfile.alias && sshProfile.alias.length == 1) + ? "alias " + : "aliases" + }: [${ + (sshProfile.alias && sshProfile.alias.join(", ")) || "" + }]${RESET} # ${sshProfile.comment}${ + (features.length > 0) ? (" ;[" + features.join(" ") + "]") : "" + }`, + ); + } + + if (sshConfig.default_proxy || sshConfig.default_forward_agent) { + const features = []; + if (sshConfig.default_proxy) features.push("proxy"); + if (sshConfig.default_forward_agent) features.push("forward_agent"); + console.log( + `\n[INFO ] Global default features: [${features.join(" ")}]`, + ); + } +} + +function loadSshConfig(): SshConfig { + const configFile = os.homedir() + "/.config/ssh-rs-config.json"; + try { + const sshConfigText = fs.readFileSync(configFile, "utf8"); + return JSON5.parse(sshConfigText); + } catch (e) { + throw `Load config file: ${configFile} failed: ${e}`; + } +} + +function matchProfile(sshConfig: SshConfig, host: string): SshProfile | null { + let profiles = []; + for (const k in sshConfig.profiles) { + const sshProfile = sshConfig.profiles[k]; + if (k === host) { + profiles.push(sshProfile); + } else if (sshProfile.alias && sshProfile.alias.includes(host)) { + profiles.push(sshProfile); + } + } + if (profiles.length === 0) { + return null; + } else if (profiles.length > 1) { + throw new Error("Find multiple profiles"); + } + return profiles[0]; +} + +function parseUsernameAndHost(usernameAndHost: string): UsernameAndHost { + if (!usernameAndHost) { + throw new Error("Empty username@host"); + } + const usernameAndHostParts = usernameAndHost.split("@"); + if (usernameAndHostParts.length == 1) { + return { host: usernameAndHostParts[0] }; + } + if (usernameAndHostParts.length > 2) { + throw new Error(`Base username@host: ${usernameAndHost}`); + } + return { username: usernameAndHostParts[0], host: usernameAndHostParts[1] }; +} + +async function main() { + const sshConfig = loadSshConfig(); + + if (process.argv.length <= 2) { + printSshConfig(sshConfig); + return; + } + const args = process.argv.slice(2); + const options = { + "forward-agent": { type: "boolean", short: "f" }, + "proxy": { type: "boolean", short: "p" }, + "help": { type: "boolean", short: "h" }, + "host": { type: "string", short: "H" }, + }; + + const { values, positionals } = parseArgs({ + args, + options, + allowPositionals: true, + tokens: true, + }); + const sshTsArgs = values as SshTsArgs; + if (sshTsArgs.help) { + console.log("ssh.ts [-h|--help]"); + console.log( + "ssh.ts [-f|--forward-agent] [-p|--proxy] [-H|--host] [username@]host", + ); + console.log("ssh.ts [-f|--forward-agent] [-p|--proxy] [username@]host"); + return; + } + + sshTsArgs.forwardAgent = values["forward-agent"]; + if (!sshTsArgs.host && positionals && positionals.length > 0) { + sshTsArgs.host = positionals[0]; + } + if (!sshTsArgs.host) { + console.error("[ERROR] --host required"); + return; + } + + const { username, host } = parseUsernameAndHost(sshTsArgs.host); + const sshProfile = matchProfile(sshConfig, host); + if (sshProfile === null) { + console.error("[ERROR] No ssh profile found."); + return; + } + + const sshCommand = "ssh"; + const sshArgs = []; + + const sshForwardAgent = sshTsArgs.forwardAgent || + sshProfile.forward_agent || sshConfig.default_forward_agent || true; + if (sshForwardAgent) { + sshArgs.push("-o"); + sshArgs.push("ForwardAgent=yes"); + } + + const sshProxy = sshTsArgs.proxy || sshProfile.proxy || + sshConfig.default_proxy || false; + if (sshProxy) { + sshArgs.push("-o"); + sshArgs.push('"ProxyCommand=nc -X 5 -x 127.0.0.1:1080 %h %p"'); + } + + const sshUsername = username || sshProfile.default_username || + sshConfig.default_username || "root"; + sshArgs.push(`${sshUsername}@${sshProfile.host}`); + + console.log(`${GREEN}[OK ]${RESET} ${sshCommand} ${sshArgs.join(" ")}`); + spawn(sshCommand, sshArgs, { + shell: true, + stdio: ["inherit", "inherit", "inherit"], + }); +} +await main(); + +// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260126T004612+08:00.MEQCIFxHjeu7+pkPpSWVoCTb +// BbzbHhc+dsAG6vqx+i0H8MqLAiAYcK0dcCVECG1je0Q/ZCIYohHCNrvDIOr3kiNj7XWoXg== diff --git a/single-scripts/ssh.ts b/single-scripts/ssh.ts index 1458318..f805fe4 100755 --- a/single-scripts/ssh.ts +++ b/single-scripts/ssh.ts @@ -1,19 +1,11 @@ -#!/usr/bin/env runts -- --runtime-bun +#!/usr/bin/env runts -- --allow-env --allow-read --allow-run // reference: https://git.hatter.ink/rust-scripts/scriptbase/src/branch/main/ssh-rs/src/main.rs -import { spawn } from "node:child_process"; -import { parseArgs } from "node:util"; +import {parseArgs} from "node:util"; -import fs from "node:fs"; -import os from "node:os"; -import JSON5 from "json5"; - -// reference: https://bun.com/docs/runtime/color -const GREEN = Bun.color("green", "ansi"); -const BLUE = Bun.color("lightblue", "ansi"); -const YELLOW = Bun.color("yellow", "ansi"); -const RESET = "\x1B[0m"; +import {log, readFileToString, term, execCommandShell} from "../libraries/deno-commons-mod.ts"; +import JSON5 from "npm:json5"; class SshTsArgs { forwardAgent?: boolean; @@ -57,9 +49,7 @@ function printSshConfig(sshConfig: SshConfig) { maxProfileHostLength = sshProfile.host.length; } } - console.log( - `${GREEN}[OK ]${RESET} Total ${allProfiles.length} server(s):`, - ); + console.log(term.auto("[green][OK ][/green] Total 10 server(s):")); allProfiles.sort((a, b) => a.localeCompare(b)); for (let i = 0; i < allProfiles.length; i++) { const k = allProfiles[i]; @@ -67,37 +57,38 @@ function printSshConfig(sshConfig: SshConfig) { const features = []; if (sshProfile.proxy) features.push("proxy"); if (sshProfile.forward_agent) features.push("forward_agent"); - console.log( - `- ${k}${ - " ".repeat(maxProfileNameLength - k.length) - } : ${BLUE}${sshProfile.host}${ - " ".repeat(maxProfileHostLength - sshProfile.host.length) - }${RESET} ${YELLOW}${ - (sshProfile.alias && sshProfile.alias.length == 1) - ? "alias " - : "aliases" - }: [${ - (sshProfile.alias && sshProfile.alias.join(", ")) || "" - }]${RESET} # ${sshProfile.comment}${ + const nameWithPad = `${k}${ + " ".repeat(maxProfileNameLength - k.length) + }`; + const hostWithPad = `${sshProfile.host}${ + " ".repeat(maxProfileHostLength - sshProfile.host.length) + }`; + const alias = `${ + (sshProfile.alias && sshProfile.alias.length == 1) + ? "alias " + : "aliases" + }: [${(sshProfile.alias && sshProfile.alias.join(", ")) || ""}]`; + console.log(term.auto( + `- ${nameWithPad} : [blue]${hostWithPad}[/blue] [yellow]${alias}[/yellow] # ${sshProfile.comment}${ (features.length > 0) ? (" ;[" + features.join(" ") + "]") : "" }`, - ); + )); } if (sshConfig.default_proxy || sshConfig.default_forward_agent) { const features = []; if (sshConfig.default_proxy) features.push("proxy"); if (sshConfig.default_forward_agent) features.push("forward_agent"); - console.log( - `\n[INFO ] Global default features: [${features.join(" ")}]`, - ); + console.log(); + log.info(`Global default features: [${features.join(" ")}]`); } } -function loadSshConfig(): SshConfig { - const configFile = os.homedir() + "/.config/ssh-rs-config.json"; +async function loadSshConfig(): SshConfig { try { - const sshConfigText = fs.readFileSync(configFile, "utf8"); + const sshConfigText = await readFileToString( + "~/.config/ssh-rs-config.json", + ); return JSON5.parse(sshConfigText); } catch (e) { throw `Load config file: ${configFile} failed: ${e}`; @@ -137,7 +128,7 @@ function parseUsernameAndHost(usernameAndHost: string): UsernameAndHost { } async function main() { - const sshConfig = loadSshConfig(); + const sshConfig = await loadSshConfig(); if (process.argv.length <= 2) { printSshConfig(sshConfig); @@ -204,13 +195,10 @@ async function main() { sshConfig.default_username || "root"; sshArgs.push(`${sshUsername}@${sshProfile.host}`); - console.log(`${GREEN}[OK ]${RESET} ${sshCommand} ${sshArgs.join(" ")}`); - spawn(sshCommand, sshArgs, { - shell: true, - stdio: ["inherit", "inherit", "inherit"], - }); + console.log(term.auto(`[green][OK ][/green] ${sshCommand} ${sshArgs.join(" ")}`)); + await execCommandShell(sshCommand, sshArgs); } await main(); -// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260126T004612+08:00.MEQCIFxHjeu7+pkPpSWVoCTb -// BbzbHhc+dsAG6vqx+i0H8MqLAiAYcK0dcCVECG1je0Q/ZCIYohHCNrvDIOr3kiNj7XWoXg== +// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260207T010008+08:00.MEUCIEmqd5Xb7955nioxaPgT +// iHA794kvFjktf5GysW6x83aQAiEAkYRhu9ycPCRjMZa+iJRmNTxYz14O0WawQ3X32MQVIc0=