| export interface SshHost { | ||
| name: string; | ||
| hostname?: string; | ||
| user?: string; | ||
| port?: string; | ||
| identityFile?: string; | ||
| extras: Record<string, string>; | ||
| } | ||
| export declare function readSshConfig(): Promise<SshHost[]>; | ||
| export declare function parseSshConfig(content: string): SshHost[]; | ||
| /** | ||
| * Atomically append a host to the SSH config file using a temp file. | ||
| */ | ||
| export declare function appendHostToConfig(host: SshHost): Promise<void>; | ||
| /** | ||
| * Remove a host from the SSH config file atomically. | ||
| * Handles multiple hosts in a single Host directive and wildcard patterns. | ||
| */ | ||
| export declare function removeHostFromConfig(name: string): Promise<boolean>; | ||
| export declare function formatHostLabel(host: SshHost): string; |
+172
| import { readFile, writeFile, rename } from "fs/promises"; | ||
| import { homedir } from "os"; | ||
| import path from "path"; | ||
| import { randomBytes } from "crypto"; | ||
| const SSH_CONFIG_PATH = process.env.SSH_CONFIG || path.join(homedir(), ".ssh", "config"); | ||
| export async function readSshConfig() { | ||
| try { | ||
| const content = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| return parseSshConfig(content); | ||
| } | ||
| catch { | ||
| return []; | ||
| } | ||
| } | ||
| export function parseSshConfig(content) { | ||
| const hosts = []; | ||
| const lines = content.split("\n"); | ||
| let currentHost = null; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed.startsWith("#")) | ||
| continue; | ||
| const hostMatch = trimmed.match(/^Host\s+(.+)$/i); | ||
| if (hostMatch) { | ||
| // Support multiple hosts in single Host directive (space-separated) | ||
| const hostNames = hostMatch[1].trim().split(/\s+/); | ||
| for (const name of hostNames) { | ||
| // Skip wildcard hosts | ||
| if (name === "*" || name.includes("*") || name.includes("?")) | ||
| continue; | ||
| if (currentHost) | ||
| hosts.push(currentHost); | ||
| currentHost = { name, extras: {} }; | ||
| } | ||
| continue; | ||
| } | ||
| if (currentHost) { | ||
| const keyMatch = trimmed.match(/^(\w+)\s+(.+)$/); | ||
| if (keyMatch) { | ||
| const [, key, value] = keyMatch; | ||
| const lowerKey = key.toLowerCase(); | ||
| switch (lowerKey) { | ||
| case "hostname": | ||
| currentHost.hostname = value.trim(); | ||
| break; | ||
| case "user": | ||
| currentHost.user = value.trim(); | ||
| break; | ||
| case "port": | ||
| currentHost.port = value.trim(); | ||
| break; | ||
| case "identityfile": | ||
| currentHost.identityFile = value.trim(); | ||
| break; | ||
| default: | ||
| currentHost.extras[lowerKey] = value.trim(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (currentHost) | ||
| hosts.push(currentHost); | ||
| return hosts; | ||
| } | ||
| /** | ||
| * Atomically append a host to the SSH config file using a temp file. | ||
| */ | ||
| export async function appendHostToConfig(host) { | ||
| const lines = [ | ||
| "", | ||
| `Host ${host.name}`, | ||
| ]; | ||
| if (host.hostname) | ||
| lines.push(` HostName ${host.hostname}`); | ||
| if (host.user) | ||
| lines.push(` User ${host.user}`); | ||
| if (host.port) | ||
| lines.push(` Port ${host.port}`); | ||
| if (host.identityFile) | ||
| lines.push(` IdentityFile ${host.identityFile}`); | ||
| for (const [key, value] of Object.entries(host.extras)) { | ||
| lines.push(` ${key} ${value}`); | ||
| } | ||
| // Read existing content first, then write atomically | ||
| try { | ||
| const existingContent = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| const newContent = existingContent + lines.join("\n") + "\n"; | ||
| await writeAtomically(SSH_CONFIG_PATH, newContent); | ||
| } | ||
| catch { | ||
| // File doesn't exist, create new | ||
| await writeAtomically(SSH_CONFIG_PATH, lines.join("\n") + "\n"); | ||
| } | ||
| } | ||
| /** | ||
| * Write content to a file atomically using a temp file and rename. | ||
| */ | ||
| async function writeAtomically(filePath, content) { | ||
| const dir = path.dirname(filePath); | ||
| const tempFileName = `.ssh_config_tmp_${randomBytes(8).toString("hex")}`; | ||
| const tempPath = path.join(dir, tempFileName); | ||
| try { | ||
| await writeFile(tempPath, content, "utf-8"); | ||
| await rename(tempPath, filePath); | ||
| } | ||
| catch (error) { | ||
| // Clean up temp file on error | ||
| try { | ||
| await writeFile(tempPath, "", "utf-8"); | ||
| } | ||
| catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * Remove a host from the SSH config file atomically. | ||
| * Handles multiple hosts in a single Host directive and wildcard patterns. | ||
| */ | ||
| export async function removeHostFromConfig(name) { | ||
| const content = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| const lines = content.split("\n"); | ||
| const result = []; | ||
| let insideTarget = false; | ||
| let found = false; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| const hostMatch = trimmed.match(/^Host\s+(.+)$/i); | ||
| if (hostMatch) { | ||
| // Handle multiple hosts in Host directive | ||
| const hostNames = hostMatch[1].trim().split(/\s+/); | ||
| const matchingHosts = hostNames.filter(h => h === name); | ||
| if (matchingHosts.length > 0) { | ||
| found = true; | ||
| // If only one host matches, skip this Host line entirely | ||
| if (matchingHosts.length === hostNames.length) { | ||
| // All hosts in this directive match, skip the entire line | ||
| insideTarget = true; | ||
| continue; | ||
| } | ||
| else { | ||
| // Partial match - rewrite Host line with remaining hosts | ||
| const remainingHosts = hostNames.filter(h => h !== name).join(" "); | ||
| result.push(`Host ${remainingHosts}`); | ||
| insideTarget = true; | ||
| continue; | ||
| } | ||
| } | ||
| else { | ||
| insideTarget = false; | ||
| } | ||
| } | ||
| else if (insideTarget && trimmed === "") { | ||
| insideTarget = false; | ||
| continue; | ||
| } | ||
| if (!insideTarget) { | ||
| result.push(line); | ||
| } | ||
| } | ||
| await writeAtomically(SSH_CONFIG_PATH, result.join("\n") + "\n"); | ||
| return found; | ||
| } | ||
| export function formatHostLabel(host) { | ||
| const name = host.name.padEnd(20); | ||
| const userHost = host.user | ||
| ? `${host.user}@${host.hostname || host.name}` | ||
| : (host.hostname || host.name); | ||
| const port = host.port ? `:${host.port}` : ""; | ||
| return `${name} ${userHost}${port}`; | ||
| } |
| import type { SshHost } from "./config.js"; | ||
| export declare function hasFzf(): boolean; | ||
| export declare function selectHostWithFzf(hosts: SshHost[]): Promise<SshHost | null>; | ||
| export declare function connectSsh(host: SshHost): Promise<void>; |
+98
| import { execSync } from "child_process"; | ||
| import { platform } from "os"; | ||
| import { formatHostLabel } from "./config.js"; | ||
| export function hasFzf() { | ||
| try { | ||
| const isWindows = platform() === "win32"; | ||
| const command = isWindows ? "where.exe fzf" : "which fzf"; | ||
| execSync(command, { stdio: "ignore" }); | ||
| return true; | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| } | ||
| export async function selectHostWithFzf(hosts) { | ||
| const { spawn } = await import("child_process"); | ||
| return new Promise((resolve) => { | ||
| const options = [ | ||
| "--ansi", | ||
| "--prompt=SSH> ", | ||
| "--height=40%", | ||
| "--reverse", | ||
| ]; | ||
| const proc = spawn("fzf", options, { | ||
| stdio: ["pipe", "pipe", "inherit"], | ||
| }); | ||
| const input = hosts.map((h) => formatHostLabel(h)).join("\n"); | ||
| proc.stdin.write(input); | ||
| proc.stdin.end(); | ||
| let output = ""; | ||
| proc.stdout.on("data", (data) => { | ||
| output += data.toString(); | ||
| }); | ||
| proc.on("close", (code) => { | ||
| if (code !== 0 || !output.trim()) { | ||
| resolve(null); | ||
| return; | ||
| } | ||
| const selected = output.trim(); | ||
| const host = hosts.find((h) => formatHostLabel(h) === selected); | ||
| resolve(host || null); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * Validate and sanitize a string for use as SSH hostname/user. | ||
| * Only allows alphanumeric characters, dots, hyphens, and underscores. | ||
| */ | ||
| function validateSshParam(value, paramName) { | ||
| // SSH hostnames and usernames should only contain these characters | ||
| const validPattern = /^[a-zA-Z0-9.\-_@]+$/; | ||
| if (!validPattern.test(value)) { | ||
| throw new Error(`Invalid ${paramName}: contains disallowed characters`); | ||
| } | ||
| return value; | ||
| } | ||
| export async function connectSsh(host) { | ||
| const { spawn } = await import("child_process"); | ||
| const args = []; | ||
| // Build the target safely | ||
| let target; | ||
| try { | ||
| const hostname = validateSshParam(host.hostname || host.name, "hostname"); | ||
| if (host.user) { | ||
| const user = validateSshParam(host.user, "user"); | ||
| target = `${user}@${hostname}`; | ||
| } | ||
| else { | ||
| target = hostname; | ||
| } | ||
| } | ||
| catch (error) { | ||
| throw error; | ||
| } | ||
| args.push(target); | ||
| if (host.port) { | ||
| // Validate port number | ||
| const port = parseInt(host.port, 10); | ||
| if (isNaN(port) || port < 1 || port > 65535) { | ||
| throw new Error(`Invalid port number: ${host.port}`); | ||
| } | ||
| args.push("-p", host.port); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const proc = spawn("ssh", args, { | ||
| stdio: "inherit", | ||
| }); | ||
| proc.on("close", (code) => { | ||
| if (code === 0) | ||
| resolve(); | ||
| else if (code === null) | ||
| reject(new Error("ssh process was killed by signal")); | ||
| else | ||
| reject(new Error(`ssh exited with code ${code}`)); | ||
| }); | ||
| proc.on("error", reject); | ||
| }); | ||
| } |
| #!/usr/bin/env node | ||
| export {}; |
+112
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import { readSshConfig, appendHostToConfig, removeHostFromConfig, formatHostLabel, } from "./config.js"; | ||
| import { hasFzf, selectHostWithFzf, connectSsh } from "./fzf.js"; | ||
| const program = new Command(); | ||
| program | ||
| .name("eazyssh") | ||
| .description("A simple SSH host manager CLI") | ||
| .version("0.1.0"); | ||
| program | ||
| .command("connect") | ||
| .description("Select a host with fzf and SSH connect") | ||
| .action(async () => { | ||
| const hosts = await readSshConfig(); | ||
| if (hosts.length === 0) { | ||
| console.error("No hosts found in ~/.ssh/config"); | ||
| process.exit(1); | ||
| } | ||
| if (!hasFzf()) { | ||
| console.error("fzf not found. Install it: https://github.com/junegunn/fzf"); | ||
| process.exit(1); | ||
| } | ||
| const selected = await selectHostWithFzf(hosts); | ||
| if (!selected) { | ||
| process.exit(0); | ||
| } | ||
| await connectSsh(selected); | ||
| }); | ||
| program | ||
| .command("list") | ||
| .alias("ls") | ||
| .description("List all SSH hosts") | ||
| .action(async () => { | ||
| const hosts = await readSshConfig(); | ||
| for (const host of hosts) { | ||
| console.log(formatHostLabel(host)); | ||
| } | ||
| }); | ||
| /** | ||
| * Validate port number string. | ||
| * Returns true if valid port number (1-65535). | ||
| */ | ||
| function isValidPort(port) { | ||
| const num = parseInt(port, 10); | ||
| return !isNaN(num) && num >= 1 && num <= 65535 && String(num) === port; | ||
| } | ||
| program | ||
| .command("add <name>") | ||
| .description("Add a new SSH host") | ||
| .option("-H, --hostname <hostname>", "Hostname or IP address") | ||
| .option("-u, --user <user>", "SSH username") | ||
| .option("-p, --port <port>", "SSH port (1-65535)") | ||
| .option("-i, --identity-file <path>", "Path to identity file") | ||
| .action(async (name, options) => { | ||
| if (!options.hostname) { | ||
| console.error("Error: --hostname is required"); | ||
| process.exit(1); | ||
| } | ||
| // Validate port if provided | ||
| if (options.port && !isValidPort(options.port)) { | ||
| console.error("Error: --port must be a number between 1 and 65535"); | ||
| process.exit(1); | ||
| } | ||
| const host = { | ||
| name, | ||
| hostname: options.hostname, | ||
| user: options.user, | ||
| port: options.port, | ||
| identityFile: options.identityFile, | ||
| extras: {}, | ||
| }; | ||
| await appendHostToConfig(host); | ||
| console.log(`Host '${name}' added successfully.`); | ||
| }); | ||
| program | ||
| .command("remove <name>") | ||
| .alias("rm") | ||
| .description("Remove an SSH host") | ||
| .action(async (name) => { | ||
| const removed = await removeHostFromConfig(name); | ||
| if (removed) { | ||
| console.log(`Host '${name}' removed successfully.`); | ||
| } | ||
| else { | ||
| console.error(`Host '${name}' not found.`); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program | ||
| .command("show <name>") | ||
| .description("Show details of an SSH host") | ||
| .action(async (name) => { | ||
| const hosts = await readSshConfig(); | ||
| const host = hosts.find((h) => h.name === name); | ||
| if (!host) { | ||
| console.error(`Host '${name}' not found.`); | ||
| process.exit(1); | ||
| } | ||
| console.log(`Host: ${host.name}`); | ||
| if (host.hostname) | ||
| console.log(` HostName: ${host.hostname}`); | ||
| if (host.user) | ||
| console.log(` User: ${host.user}`); | ||
| if (host.port) | ||
| console.log(` Port: ${host.port}`); | ||
| if (host.identityFile) | ||
| console.log(` IdentityFile: ${host.identityFile}`); | ||
| for (const [key, value] of Object.entries(host.extras)) { | ||
| console.log(` ${key}: ${value}`); | ||
| } | ||
| }); | ||
| program.parse(); |
+16
-7
| { | ||
| "name": "eazyssh", | ||
| "version": "1.0.0", | ||
| "description": "", | ||
| "main": "index.js", | ||
| "version": "1.0.1", | ||
| "description": "A simple SSH host manager CLI", | ||
| "type": "module", | ||
| "bin": { | ||
| "eazyssh": "./dist/index.js" | ||
| }, | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1" | ||
| "build": "tsc", | ||
| "dev": "tsx src/index.ts", | ||
| "start": "node dist/index.js" | ||
| }, | ||
@@ -14,5 +19,9 @@ "keywords": [], | ||
| "dependencies": { | ||
| "commander": "^14.0.3", | ||
| "consola": "^3.4.2" | ||
| "commander": "^14.0.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "tsx": "^4.19.0", | ||
| "typescript": "^5.6.0" | ||
| } | ||
| } | ||
| } |
| version: 2 | ||
| updates: | ||
| - package-ecosystem: "npm" | ||
| directory: "/eazyssh" | ||
| schedule: | ||
| interval: "weekly" | ||
| commit-message: | ||
| prefix: "chore(deps)" | ||
| - package-ecosystem: "github-actions" | ||
| directory: "/" | ||
| schedule: | ||
| interval: "weekly" | ||
| commit-message: | ||
| prefix: "chore(ci)" |
| CLAUDE.md |
| { | ||
| "name": "eazyssh", | ||
| "version": "0.1.0", | ||
| "lockfileVersion": 3, | ||
| "requires": true, | ||
| "packages": { | ||
| "": { | ||
| "name": "eazyssh", | ||
| "version": "0.1.0", | ||
| "dependencies": { | ||
| "commander": "^14.0.3" | ||
| }, | ||
| "bin": { | ||
| "eazyssh": "dist/index.js" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "tsx": "^4.19.0", | ||
| "typescript": "^5.6.0" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/aix-ppc64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", | ||
| "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", | ||
| "cpu": [ | ||
| "ppc64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "aix" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/android-arm": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", | ||
| "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", | ||
| "cpu": [ | ||
| "arm" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "android" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/android-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "android" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/android-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", | ||
| "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "android" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/darwin-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "darwin" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/darwin-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", | ||
| "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "darwin" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/freebsd-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "freebsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/freebsd-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", | ||
| "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "freebsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-arm": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", | ||
| "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", | ||
| "cpu": [ | ||
| "arm" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-ia32": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", | ||
| "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", | ||
| "cpu": [ | ||
| "ia32" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-loong64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", | ||
| "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", | ||
| "cpu": [ | ||
| "loong64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-mips64el": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", | ||
| "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", | ||
| "cpu": [ | ||
| "mips64el" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-ppc64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", | ||
| "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", | ||
| "cpu": [ | ||
| "ppc64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-riscv64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", | ||
| "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", | ||
| "cpu": [ | ||
| "riscv64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-s390x": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", | ||
| "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", | ||
| "cpu": [ | ||
| "s390x" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/linux-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", | ||
| "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "linux" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/netbsd-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "netbsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/netbsd-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", | ||
| "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "netbsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/openbsd-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "openbsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/openbsd-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", | ||
| "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "openbsd" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/openharmony-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "openharmony" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/sunos-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", | ||
| "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "sunos" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/win32-arm64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", | ||
| "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", | ||
| "cpu": [ | ||
| "arm64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "win32" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/win32-ia32": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", | ||
| "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", | ||
| "cpu": [ | ||
| "ia32" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "win32" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@esbuild/win32-x64": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", | ||
| "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", | ||
| "cpu": [ | ||
| "x64" | ||
| ], | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "win32" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18" | ||
| } | ||
| }, | ||
| "node_modules/@types/node": { | ||
| "version": "25.5.0", | ||
| "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", | ||
| "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "undici-types": "~7.18.0" | ||
| } | ||
| }, | ||
| "node_modules/commander": { | ||
| "version": "14.0.3", | ||
| "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", | ||
| "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", | ||
| "license": "MIT", | ||
| "engines": { | ||
| "node": ">=20" | ||
| } | ||
| }, | ||
| "node_modules/esbuild": { | ||
| "version": "0.27.4", | ||
| "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", | ||
| "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", | ||
| "dev": true, | ||
| "hasInstallScript": true, | ||
| "license": "MIT", | ||
| "bin": { | ||
| "esbuild": "bin/esbuild" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18" | ||
| }, | ||
| "optionalDependencies": { | ||
| "@esbuild/aix-ppc64": "0.27.4", | ||
| "@esbuild/android-arm": "0.27.4", | ||
| "@esbuild/android-arm64": "0.27.4", | ||
| "@esbuild/android-x64": "0.27.4", | ||
| "@esbuild/darwin-arm64": "0.27.4", | ||
| "@esbuild/darwin-x64": "0.27.4", | ||
| "@esbuild/freebsd-arm64": "0.27.4", | ||
| "@esbuild/freebsd-x64": "0.27.4", | ||
| "@esbuild/linux-arm": "0.27.4", | ||
| "@esbuild/linux-arm64": "0.27.4", | ||
| "@esbuild/linux-ia32": "0.27.4", | ||
| "@esbuild/linux-loong64": "0.27.4", | ||
| "@esbuild/linux-mips64el": "0.27.4", | ||
| "@esbuild/linux-ppc64": "0.27.4", | ||
| "@esbuild/linux-riscv64": "0.27.4", | ||
| "@esbuild/linux-s390x": "0.27.4", | ||
| "@esbuild/linux-x64": "0.27.4", | ||
| "@esbuild/netbsd-arm64": "0.27.4", | ||
| "@esbuild/netbsd-x64": "0.27.4", | ||
| "@esbuild/openbsd-arm64": "0.27.4", | ||
| "@esbuild/openbsd-x64": "0.27.4", | ||
| "@esbuild/openharmony-arm64": "0.27.4", | ||
| "@esbuild/sunos-x64": "0.27.4", | ||
| "@esbuild/win32-arm64": "0.27.4", | ||
| "@esbuild/win32-ia32": "0.27.4", | ||
| "@esbuild/win32-x64": "0.27.4" | ||
| } | ||
| }, | ||
| "node_modules/fsevents": { | ||
| "version": "2.3.3", | ||
| "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | ||
| "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", | ||
| "dev": true, | ||
| "hasInstallScript": true, | ||
| "license": "MIT", | ||
| "optional": true, | ||
| "os": [ | ||
| "darwin" | ||
| ], | ||
| "engines": { | ||
| "node": "^8.16.0 || ^10.6.0 || >=11.0.0" | ||
| } | ||
| }, | ||
| "node_modules/get-tsconfig": { | ||
| "version": "4.13.6", | ||
| "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", | ||
| "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "resolve-pkg-maps": "^1.0.0" | ||
| }, | ||
| "funding": { | ||
| "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" | ||
| } | ||
| }, | ||
| "node_modules/resolve-pkg-maps": { | ||
| "version": "1.0.0", | ||
| "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", | ||
| "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "funding": { | ||
| "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" | ||
| } | ||
| }, | ||
| "node_modules/tsx": { | ||
| "version": "4.21.0", | ||
| "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", | ||
| "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "esbuild": "~0.27.0", | ||
| "get-tsconfig": "^4.7.5" | ||
| }, | ||
| "bin": { | ||
| "tsx": "dist/cli.mjs" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "optionalDependencies": { | ||
| "fsevents": "~2.3.3" | ||
| } | ||
| }, | ||
| "node_modules/typescript": { | ||
| "version": "5.9.3", | ||
| "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | ||
| "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | ||
| "dev": true, | ||
| "license": "Apache-2.0", | ||
| "bin": { | ||
| "tsc": "bin/tsc", | ||
| "tsserver": "bin/tsserver" | ||
| }, | ||
| "engines": { | ||
| "node": ">=14.17" | ||
| } | ||
| }, | ||
| "node_modules/undici-types": { | ||
| "version": "7.18.2", | ||
| "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", | ||
| "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", | ||
| "dev": true, | ||
| "license": "MIT" | ||
| } | ||
| } | ||
| } |
| { | ||
| "name": "eazyssh", | ||
| "version": "0.1.0", | ||
| "description": "A simple SSH host manager CLI", | ||
| "type": "module", | ||
| "bin": { | ||
| "eazyssh": "./dist/index.js" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "dev": "tsx src/index.ts", | ||
| "start": "node dist/index.js" | ||
| }, | ||
| "dependencies": { | ||
| "commander": "^14.0.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "tsx": "^4.19.0", | ||
| "typescript": "^5.6.0" | ||
| } | ||
| } |
| lockfileVersion: '9.0' | ||
| settings: | ||
| autoInstallPeers: true | ||
| excludeLinksFromLockfile: false | ||
| importers: | ||
| .: | ||
| dependencies: | ||
| commander: | ||
| specifier: ^14.0.3 | ||
| version: 14.0.3 | ||
| devDependencies: | ||
| '@types/node': | ||
| specifier: ^25.5.0 | ||
| version: 25.5.0 | ||
| tsx: | ||
| specifier: ^4.19.0 | ||
| version: 4.21.0 | ||
| typescript: | ||
| specifier: ^5.6.0 | ||
| version: 5.9.3 | ||
| packages: | ||
| '@esbuild/aix-ppc64@0.27.4': | ||
| resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} | ||
| engines: {node: '>=18'} | ||
| cpu: [ppc64] | ||
| os: [aix] | ||
| '@esbuild/android-arm64@0.27.4': | ||
| resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [android] | ||
| '@esbuild/android-arm@0.27.4': | ||
| resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm] | ||
| os: [android] | ||
| '@esbuild/android-x64@0.27.4': | ||
| resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [android] | ||
| '@esbuild/darwin-arm64@0.27.4': | ||
| resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [darwin] | ||
| '@esbuild/darwin-x64@0.27.4': | ||
| resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [darwin] | ||
| '@esbuild/freebsd-arm64@0.27.4': | ||
| resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [freebsd] | ||
| '@esbuild/freebsd-x64@0.27.4': | ||
| resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [freebsd] | ||
| '@esbuild/linux-arm64@0.27.4': | ||
| resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [linux] | ||
| '@esbuild/linux-arm@0.27.4': | ||
| resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm] | ||
| os: [linux] | ||
| '@esbuild/linux-ia32@0.27.4': | ||
| resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [ia32] | ||
| os: [linux] | ||
| '@esbuild/linux-loong64@0.27.4': | ||
| resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [loong64] | ||
| os: [linux] | ||
| '@esbuild/linux-mips64el@0.27.4': | ||
| resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [mips64el] | ||
| os: [linux] | ||
| '@esbuild/linux-ppc64@0.27.4': | ||
| resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [ppc64] | ||
| os: [linux] | ||
| '@esbuild/linux-riscv64@0.27.4': | ||
| resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [riscv64] | ||
| os: [linux] | ||
| '@esbuild/linux-s390x@0.27.4': | ||
| resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [s390x] | ||
| os: [linux] | ||
| '@esbuild/linux-x64@0.27.4': | ||
| resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [linux] | ||
| '@esbuild/netbsd-arm64@0.27.4': | ||
| resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [netbsd] | ||
| '@esbuild/netbsd-x64@0.27.4': | ||
| resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [netbsd] | ||
| '@esbuild/openbsd-arm64@0.27.4': | ||
| resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [openbsd] | ||
| '@esbuild/openbsd-x64@0.27.4': | ||
| resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [openbsd] | ||
| '@esbuild/openharmony-arm64@0.27.4': | ||
| resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [openharmony] | ||
| '@esbuild/sunos-x64@0.27.4': | ||
| resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [sunos] | ||
| '@esbuild/win32-arm64@0.27.4': | ||
| resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} | ||
| engines: {node: '>=18'} | ||
| cpu: [arm64] | ||
| os: [win32] | ||
| '@esbuild/win32-ia32@0.27.4': | ||
| resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} | ||
| engines: {node: '>=18'} | ||
| cpu: [ia32] | ||
| os: [win32] | ||
| '@esbuild/win32-x64@0.27.4': | ||
| resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} | ||
| engines: {node: '>=18'} | ||
| cpu: [x64] | ||
| os: [win32] | ||
| '@types/node@25.5.0': | ||
| resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} | ||
| commander@14.0.3: | ||
| resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} | ||
| engines: {node: '>=20'} | ||
| esbuild@0.27.4: | ||
| resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} | ||
| engines: {node: '>=18'} | ||
| hasBin: true | ||
| fsevents@2.3.3: | ||
| resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} | ||
| engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} | ||
| os: [darwin] | ||
| get-tsconfig@4.13.7: | ||
| resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} | ||
| resolve-pkg-maps@1.0.0: | ||
| resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} | ||
| tsx@4.21.0: | ||
| resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} | ||
| engines: {node: '>=18.0.0'} | ||
| hasBin: true | ||
| typescript@5.9.3: | ||
| resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} | ||
| engines: {node: '>=14.17'} | ||
| hasBin: true | ||
| undici-types@7.18.2: | ||
| resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} | ||
| snapshots: | ||
| '@esbuild/aix-ppc64@0.27.4': | ||
| optional: true | ||
| '@esbuild/android-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/android-arm@0.27.4': | ||
| optional: true | ||
| '@esbuild/android-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/darwin-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/darwin-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/freebsd-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/freebsd-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-arm@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-ia32@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-loong64@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-mips64el@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-ppc64@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-riscv64@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-s390x@0.27.4': | ||
| optional: true | ||
| '@esbuild/linux-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/netbsd-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/netbsd-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/openbsd-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/openbsd-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/openharmony-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/sunos-x64@0.27.4': | ||
| optional: true | ||
| '@esbuild/win32-arm64@0.27.4': | ||
| optional: true | ||
| '@esbuild/win32-ia32@0.27.4': | ||
| optional: true | ||
| '@esbuild/win32-x64@0.27.4': | ||
| optional: true | ||
| '@types/node@25.5.0': | ||
| dependencies: | ||
| undici-types: 7.18.2 | ||
| commander@14.0.3: {} | ||
| esbuild@0.27.4: | ||
| optionalDependencies: | ||
| '@esbuild/aix-ppc64': 0.27.4 | ||
| '@esbuild/android-arm': 0.27.4 | ||
| '@esbuild/android-arm64': 0.27.4 | ||
| '@esbuild/android-x64': 0.27.4 | ||
| '@esbuild/darwin-arm64': 0.27.4 | ||
| '@esbuild/darwin-x64': 0.27.4 | ||
| '@esbuild/freebsd-arm64': 0.27.4 | ||
| '@esbuild/freebsd-x64': 0.27.4 | ||
| '@esbuild/linux-arm': 0.27.4 | ||
| '@esbuild/linux-arm64': 0.27.4 | ||
| '@esbuild/linux-ia32': 0.27.4 | ||
| '@esbuild/linux-loong64': 0.27.4 | ||
| '@esbuild/linux-mips64el': 0.27.4 | ||
| '@esbuild/linux-ppc64': 0.27.4 | ||
| '@esbuild/linux-riscv64': 0.27.4 | ||
| '@esbuild/linux-s390x': 0.27.4 | ||
| '@esbuild/linux-x64': 0.27.4 | ||
| '@esbuild/netbsd-arm64': 0.27.4 | ||
| '@esbuild/netbsd-x64': 0.27.4 | ||
| '@esbuild/openbsd-arm64': 0.27.4 | ||
| '@esbuild/openbsd-x64': 0.27.4 | ||
| '@esbuild/openharmony-arm64': 0.27.4 | ||
| '@esbuild/sunos-x64': 0.27.4 | ||
| '@esbuild/win32-arm64': 0.27.4 | ||
| '@esbuild/win32-ia32': 0.27.4 | ||
| '@esbuild/win32-x64': 0.27.4 | ||
| fsevents@2.3.3: | ||
| optional: true | ||
| get-tsconfig@4.13.7: | ||
| dependencies: | ||
| resolve-pkg-maps: 1.0.0 | ||
| resolve-pkg-maps@1.0.0: {} | ||
| tsx@4.21.0: | ||
| dependencies: | ||
| esbuild: 0.27.4 | ||
| get-tsconfig: 4.13.7 | ||
| optionalDependencies: | ||
| fsevents: 2.3.3 | ||
| typescript@5.9.3: {} | ||
| undici-types@7.18.2: {} |
| import { readFile, writeFile, rename } from "fs/promises"; | ||
| import { homedir } from "os"; | ||
| import path from "path"; | ||
| import { randomBytes } from "crypto"; | ||
| export interface SshHost { | ||
| name: string; | ||
| hostname?: string; | ||
| user?: string; | ||
| port?: string; | ||
| identityFile?: string; | ||
| extras: Record<string, string>; | ||
| } | ||
| const SSH_CONFIG_PATH = process.env.SSH_CONFIG || path.join(homedir(), ".ssh", "config"); | ||
| export async function readSshConfig(): Promise<SshHost[]> { | ||
| try { | ||
| const content = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| return parseSshConfig(content); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| export function parseSshConfig(content: string): SshHost[] { | ||
| const hosts: SshHost[] = []; | ||
| const lines = content.split("\n"); | ||
| let currentHost: SshHost | null = null; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed.startsWith("#")) continue; | ||
| const hostMatch = trimmed.match(/^Host\s+(.+)$/i); | ||
| if (hostMatch) { | ||
| // Support multiple hosts in single Host directive (space-separated) | ||
| const hostNames = hostMatch[1].trim().split(/\s+/); | ||
| for (const name of hostNames) { | ||
| // Skip wildcard hosts | ||
| if (name === "*" || name.includes("*") || name.includes("?")) continue; | ||
| if (currentHost) hosts.push(currentHost); | ||
| currentHost = { name, extras: {} }; | ||
| } | ||
| continue; | ||
| } | ||
| if (currentHost) { | ||
| const keyMatch = trimmed.match(/^(\w+)\s+(.+)$/); | ||
| if (keyMatch) { | ||
| const [, key, value] = keyMatch; | ||
| const lowerKey = key.toLowerCase(); | ||
| switch (lowerKey) { | ||
| case "hostname": | ||
| currentHost.hostname = value.trim(); | ||
| break; | ||
| case "user": | ||
| currentHost.user = value.trim(); | ||
| break; | ||
| case "port": | ||
| currentHost.port = value.trim(); | ||
| break; | ||
| case "identityfile": | ||
| currentHost.identityFile = value.trim(); | ||
| break; | ||
| default: | ||
| currentHost.extras[lowerKey] = value.trim(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (currentHost) hosts.push(currentHost); | ||
| return hosts; | ||
| } | ||
| /** | ||
| * Atomically append a host to the SSH config file using a temp file. | ||
| */ | ||
| export async function appendHostToConfig(host: SshHost): Promise<void> { | ||
| const lines = [ | ||
| "", | ||
| `Host ${host.name}`, | ||
| ]; | ||
| if (host.hostname) lines.push(` HostName ${host.hostname}`); | ||
| if (host.user) lines.push(` User ${host.user}`); | ||
| if (host.port) lines.push(` Port ${host.port}`); | ||
| if (host.identityFile) lines.push(` IdentityFile ${host.identityFile}`); | ||
| for (const [key, value] of Object.entries(host.extras)) { | ||
| lines.push(` ${key} ${value}`); | ||
| } | ||
| // Read existing content first, then write atomically | ||
| try { | ||
| const existingContent = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| const newContent = existingContent + lines.join("\n") + "\n"; | ||
| await writeAtomically(SSH_CONFIG_PATH, newContent); | ||
| } catch { | ||
| // File doesn't exist, create new | ||
| await writeAtomically(SSH_CONFIG_PATH, lines.join("\n") + "\n"); | ||
| } | ||
| } | ||
| /** | ||
| * Write content to a file atomically using a temp file and rename. | ||
| */ | ||
| async function writeAtomically(filePath: string, content: string): Promise<void> { | ||
| const dir = path.dirname(filePath); | ||
| const tempFileName = `.ssh_config_tmp_${randomBytes(8).toString("hex")}`; | ||
| const tempPath = path.join(dir, tempFileName); | ||
| try { | ||
| await writeFile(tempPath, content, "utf-8"); | ||
| await rename(tempPath, filePath); | ||
| } catch (error) { | ||
| // Clean up temp file on error | ||
| try { | ||
| await writeFile(tempPath, "", "utf-8"); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * Remove a host from the SSH config file atomically. | ||
| * Handles multiple hosts in a single Host directive and wildcard patterns. | ||
| */ | ||
| export async function removeHostFromConfig(name: string): Promise<boolean> { | ||
| const content = await readFile(SSH_CONFIG_PATH, "utf-8"); | ||
| const lines = content.split("\n"); | ||
| const result: string[] = []; | ||
| let insideTarget = false; | ||
| let found = false; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| const hostMatch = trimmed.match(/^Host\s+(.+)$/i); | ||
| if (hostMatch) { | ||
| // Handle multiple hosts in Host directive | ||
| const hostNames = hostMatch[1].trim().split(/\s+/); | ||
| const matchingHosts = hostNames.filter(h => h === name); | ||
| if (matchingHosts.length > 0) { | ||
| found = true; | ||
| // If only one host matches, skip this Host line entirely | ||
| if (matchingHosts.length === hostNames.length) { | ||
| // All hosts in this directive match, skip the entire line | ||
| insideTarget = true; | ||
| continue; | ||
| } else { | ||
| // Partial match - rewrite Host line with remaining hosts | ||
| const remainingHosts = hostNames.filter(h => h !== name).join(" "); | ||
| result.push(`Host ${remainingHosts}`); | ||
| insideTarget = true; | ||
| continue; | ||
| } | ||
| } else { | ||
| insideTarget = false; | ||
| } | ||
| } else if (insideTarget && trimmed === "") { | ||
| // Empty line ends the current host block | ||
| insideTarget = false; | ||
| } | ||
| if (!insideTarget) { | ||
| result.push(line); | ||
| } | ||
| } | ||
| await writeAtomically(SSH_CONFIG_PATH, result.join("\n") + "\n"); | ||
| return found; | ||
| } | ||
| export function formatHostLabel(host: SshHost): string { | ||
| const name = host.name.padEnd(20); | ||
| const userHost = host.user | ||
| ? `${host.user}@${host.hostname || host.name}` | ||
| : (host.hostname || host.name); | ||
| const port = host.port ? `:${host.port}` : ""; | ||
| return `${name} ${userHost}${port}`; | ||
| } |
| import { execSync, spawn } from "child_process"; | ||
| import { platform } from "os"; | ||
| import type { SshHost } from "./config.js"; | ||
| import { formatHostLabel } from "./config.js"; | ||
| export function hasFzf(): boolean { | ||
| try { | ||
| const isWindows = platform() === "win32"; | ||
| const command = isWindows ? "where.exe fzf" : "which fzf"; | ||
| execSync(command, { stdio: "ignore" }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| export async function selectHostWithFzf( | ||
| hosts: SshHost[] | ||
| ): Promise<SshHost | null> { | ||
| const { spawn } = await import("child_process"); | ||
| return new Promise((resolve) => { | ||
| const options = [ | ||
| "--ansi", | ||
| "--prompt=SSH> ", | ||
| "--height=40%", | ||
| "--reverse", | ||
| ]; | ||
| const proc = spawn("fzf", options, { | ||
| stdio: ["pipe", "pipe", "inherit"], | ||
| }); | ||
| const input = hosts.map((h) => formatHostLabel(h)).join("\n"); | ||
| proc.stdin.write(input); | ||
| proc.stdin.end(); | ||
| let output = ""; | ||
| proc.stdout.on("data", (data: Buffer) => { | ||
| output += data.toString(); | ||
| }); | ||
| proc.on("close", (code: number) => { | ||
| if (code !== 0 || !output.trim()) { | ||
| resolve(null); | ||
| return; | ||
| } | ||
| const selected = output.trim(); | ||
| const host = hosts.find((h) => formatHostLabel(h) === selected); | ||
| resolve(host || null); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * Validate and sanitize a string for use as SSH hostname/user. | ||
| * Only allows alphanumeric characters, dots, hyphens, and underscores. | ||
| */ | ||
| function validateSshParam(value: string, paramName: string): string { | ||
| // SSH hostnames and usernames should only contain these characters | ||
| const validPattern = /^[a-zA-Z0-9.\-_@]+$/; | ||
| if (!validPattern.test(value)) { | ||
| throw new Error(`Invalid ${paramName}: contains disallowed characters`); | ||
| } | ||
| return value; | ||
| } | ||
| export async function connectSsh(host: SshHost): Promise<void> { | ||
| const { spawn } = await import("child_process"); | ||
| const args: string[] = []; | ||
| // Build the target safely | ||
| let target: string; | ||
| try { | ||
| const hostname = validateSshParam(host.hostname || host.name, "hostname"); | ||
| if (host.user) { | ||
| const user = validateSshParam(host.user, "user"); | ||
| target = `${user}@${hostname}`; | ||
| } else { | ||
| target = hostname; | ||
| } | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| args.push(target); | ||
| if (host.port) { | ||
| // Validate port number | ||
| const port = parseInt(host.port, 10); | ||
| if (isNaN(port) || port < 1 || port > 65535) { | ||
| throw new Error(`Invalid port number: ${host.port}`); | ||
| } | ||
| args.push("-p", host.port); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const proc = spawn("ssh", args, { | ||
| stdio: "inherit", | ||
| }); | ||
| proc.on("close", (code) => { | ||
| if (code === 0) resolve(); | ||
| else reject(new Error(`ssh exited with code ${code}`)); | ||
| }); | ||
| proc.on("error", reject); | ||
| }); | ||
| } |
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import { | ||
| readSshConfig, | ||
| appendHostToConfig, | ||
| removeHostFromConfig, | ||
| formatHostLabel, | ||
| type SshHost, | ||
| } from "./config.js"; | ||
| import { hasFzf, selectHostWithFzf, connectSsh } from "./fzf.js"; | ||
| const program = new Command(); | ||
| program | ||
| .name("eazyssh") | ||
| .description("A simple SSH host manager CLI") | ||
| .version("0.1.0"); | ||
| program | ||
| .command("connect") | ||
| .description("Select a host with fzf and SSH connect") | ||
| .action(async () => { | ||
| const hosts = await readSshConfig(); | ||
| if (hosts.length === 0) { | ||
| console.error("No hosts found in ~/.ssh/config"); | ||
| process.exit(1); | ||
| } | ||
| if (!hasFzf()) { | ||
| console.error("fzf not found. Install it: https://github.com/junegunn/fzf"); | ||
| process.exit(1); | ||
| } | ||
| const selected = await selectHostWithFzf(hosts); | ||
| if (!selected) { | ||
| process.exit(0); | ||
| } | ||
| await connectSsh(selected); | ||
| }); | ||
| program | ||
| .command("list") | ||
| .alias("ls") | ||
| .description("List all SSH hosts") | ||
| .action(async () => { | ||
| const hosts = await readSshConfig(); | ||
| for (const host of hosts) { | ||
| console.log(formatHostLabel(host)); | ||
| } | ||
| }); | ||
| /** | ||
| * Validate port number string. | ||
| * Returns true if valid port number (1-65535). | ||
| */ | ||
| function isValidPort(port: string): boolean { | ||
| const num = parseInt(port, 10); | ||
| return !isNaN(num) && num >= 1 && num <= 65535 && String(num) === port; | ||
| } | ||
| program | ||
| .command("add <name>") | ||
| .description("Add a new SSH host") | ||
| .option("-H, --hostname <hostname>", "Hostname or IP address") | ||
| .option("-u, --user <user>", "SSH username") | ||
| .option("-p, --port <port>", "SSH port (1-65535)") | ||
| .option("-i, --identity-file <path>", "Path to identity file") | ||
| .action(async (name, options) => { | ||
| if (!options.hostname) { | ||
| console.error("Error: --hostname is required"); | ||
| process.exit(1); | ||
| } | ||
| // Validate port if provided | ||
| if (options.port && !isValidPort(options.port)) { | ||
| console.error("Error: --port must be a number between 1 and 65535"); | ||
| process.exit(1); | ||
| } | ||
| const host: SshHost = { | ||
| name, | ||
| hostname: options.hostname, | ||
| user: options.user, | ||
| port: options.port, | ||
| identityFile: options.identityFile, | ||
| extras: {}, | ||
| }; | ||
| await appendHostToConfig(host); | ||
| console.log(`Host '${name}' added successfully.`); | ||
| }); | ||
| program | ||
| .command("remove <name>") | ||
| .alias("rm") | ||
| .description("Remove an SSH host") | ||
| .action(async (name) => { | ||
| const removed = await removeHostFromConfig(name); | ||
| if (removed) { | ||
| console.log(`Host '${name}' removed successfully.`); | ||
| } else { | ||
| console.error(`Host '${name}' not found.`); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program | ||
| .command("show <name>") | ||
| .description("Show details of an SSH host") | ||
| .action(async (name) => { | ||
| const hosts = await readSshConfig(); | ||
| const host = hosts.find((h) => h.name === name); | ||
| if (!host) { | ||
| console.error(`Host '${name}' not found.`); | ||
| process.exit(1); | ||
| } | ||
| console.log(`Host: ${host.name}`); | ||
| if (host.hostname) console.log(` HostName: ${host.hostname}`); | ||
| if (host.user) console.log(` User: ${host.user}`); | ||
| if (host.port) console.log(` Port: ${host.port}`); | ||
| if (host.identityFile) console.log(` IdentityFile: ${host.identityFile}`); | ||
| for (const [key, value] of Object.entries(host.extras)) { | ||
| console.log(` ${key}: ${value}`); | ||
| } | ||
| }); | ||
| program.parse(); |
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "outDir": "./dist", | ||
| "rootDir": "./src", | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "declaration": true | ||
| }, | ||
| "include": ["src/**/*"] | ||
| } |
-61
| { | ||
| "nodes": { | ||
| "flake-utils": { | ||
| "inputs": { | ||
| "systems": "systems" | ||
| }, | ||
| "locked": { | ||
| "lastModified": 1731533236, | ||
| "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", | ||
| "owner": "numtide", | ||
| "repo": "flake-utils", | ||
| "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", | ||
| "type": "github" | ||
| }, | ||
| "original": { | ||
| "owner": "numtide", | ||
| "repo": "flake-utils", | ||
| "type": "github" | ||
| } | ||
| }, | ||
| "nixpkgs": { | ||
| "locked": { | ||
| "lastModified": 1773821835, | ||
| "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", | ||
| "owner": "NixOS", | ||
| "repo": "nixpkgs", | ||
| "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", | ||
| "type": "github" | ||
| }, | ||
| "original": { | ||
| "owner": "NixOS", | ||
| "ref": "nixos-unstable", | ||
| "repo": "nixpkgs", | ||
| "type": "github" | ||
| } | ||
| }, | ||
| "root": { | ||
| "inputs": { | ||
| "flake-utils": "flake-utils", | ||
| "nixpkgs": "nixpkgs" | ||
| } | ||
| }, | ||
| "systems": { | ||
| "locked": { | ||
| "lastModified": 1681028828, | ||
| "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", | ||
| "owner": "nix-systems", | ||
| "repo": "default", | ||
| "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", | ||
| "type": "github" | ||
| }, | ||
| "original": { | ||
| "owner": "nix-systems", | ||
| "repo": "default", | ||
| "type": "github" | ||
| } | ||
| } | ||
| }, | ||
| "root": "root", | ||
| "version": 7 | ||
| } |
Sorry, the diff of this file is not supported yet
-48
| # eazyssh | ||
| `~/.ssh/config` を読み書きして、fzf でホストを選んで SSH 接続する CLI ツール。 | ||
| ## インストール | ||
| ### npm | ||
| ```bash | ||
| git clone https://github.com/nazozokc/simple-ssh | ||
| cd simple-ssh | ||
| npm install && npm run build | ||
| npm link | ||
| ``` | ||
| ### Nix | ||
| ```bash | ||
| nix run github:nazozokc/simple-ssh | ||
| ``` | ||
| ## 使い方 | ||
| ```bash | ||
| eazyssh # fzf でホストを選んで接続(デフォルト) | ||
| eazyssh ls # ホスト一覧 | ||
| eazyssh add myserver -H 192.168.1.10 -u nazozo -i ~/.ssh/id_ed25519 | ||
| eazyssh show myserver | ||
| eazyssh rm myserver | ||
| ``` | ||
| ### `add` オプション | ||
| | オプション | 説明 | | ||
| |---|---| | ||
| | `-H, --hostname` | 接続先ホスト名 or IP | | ||
| | `-u, --user` | ログインユーザー | | ||
| | `-p, --port` | ポート番号 | | ||
| | `-i, --identity` | 秘密鍵のパス | | ||
| ## 依存 | ||
| - [fzf](https://github.com/junegunn/cli) — ホスト選択UI | ||
| - Node.js 20+ | ||
| ## ライセンス | ||
| MIT |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
1
-50%Yes
NaN14113
-70%3
Infinity%7
-50%408
-58.83%1
Infinity%0
-100%2
100%- Removed
- Removed