@hadesgod/sshx
Version:
A modern CLI tool for managing SSH connections with interactive host selection and port forwarding capabilities.
6 lines • 8.45 kB
JavaScript
import{Array as e,Console as t,Data as n,Effect as r,Match as i,Option as a,pipe as o}from"effect";import{join as s}from"node:path";import{homedir as c}from"node:os";import{access as l,readFile as u}from"node:fs/promises";import*as d from"effect/Schema";import*as ee from"effect/Schema";import*as f from"effect/Schema";import p from"chalk";import{confirm as te,input as m,search as ne,select as re}from"@inquirer/prompts";import h from"fuse.js";import g from"clipboardy";import{dual as _}from"effect/Function";import v from"boxen";const y=d.String.pipe(d.brand(`FilePath`)),b=d.String.pipe(d.optionalWith({as:`Option`})),x=d.Struct({host:d.String,hostname:d.String,user:b,port:d.NumberFromString.pipe(d.optionalWith({as:`Option`})),password:b,availablePorts:d.Array(d.Struct({port:d.NumberFromString,name:d.String}))});var S=class extends n.TaggedError(`Parser/File/Access/Error`){static new=e=>new this({error:e})},C=class extends n.TaggedError(`File/NotFound/Error`){static new=e=>new this({error:e})},w=class extends n.TaggedError(`File/PermissionDenied/Error`){static new=e=>new this({error:e})},T=class extends n.TaggedError(`File/Read/Error`){static new=e=>new this({error:e})};function E(){let e=s(c(),`.ssh`),t=s(e,`config`);return r.tryPromise({try:async()=>(await l(t),y.make(t)),catch:e=>e.code===`ENOENT`?C.new(e):e.code===`EACCES`?w.new(e):S.new(e)})}function D(e){return e!==void 0&&e.startsWith(`#`)}function O(e){return e!==void 0&&e.startsWith(`##`)}function k(e){let t=e.split(/\s+/),n=t[0],r=t.slice(1).join(` `).trim();return n===void 0?a.none():a.some({directive:n.toLowerCase().replace(`##`,``).trim(),value:r})}function A(e){return Object.entries(e).flatMap(([e,t])=>e.startsWith(`port_`)?{port:t,name:e.replace(`port_`,``)}:[])}function j(e){return ee.decodeUnknown(x)({...e,availablePorts:A(e)})}function M(t){let n=/\r?\n/;return o(t.split(n),e.map(e=>e.trim()),e.filter(e=>O(e)===!0?!0:D(e)!==!0),e.map(k),e.filter(a.isSome),e.map(e=>e.value),e.reduce({hosts:[],includes:[]},(e,t)=>{if(t.directive===`include`)return e.includes.push(t.value),e;t.directive===`host`&&e.hosts.push({host:t.value});let n=e.hosts[e.hosts.length-1];return n&&(n[t.directive]=t.value),e}),r.succeed,r.bind(`newHosts`,t=>r.forEach(t.hosts,e=>j(e).pipe(r.catchTag(`ParseError`,()=>r.succeed(null)))).pipe(r.map(e.map(a.fromNullable)),r.map(e.filter(a.isSome)),r.map(e.map(e=>e.value)))),r.map(({newHosts:e,includes:t})=>({hosts:e,includes:t})))}function N(n,i=new Set){if(i.has(n))return r.succeed([]);let a=new Set(i).add(n),s=F(n),c=o(P(s),r.andThen(M),r.bind(`includedHosts`,t=>r.forEach(t.includes,e=>o(r.succeed(F(e)),r.flatMap(e=>N(e,a)))).pipe(r.map(e.flatten))),r.map(({hosts:e,includedHosts:t})=>[...e,...t]),r.tapError(e=>t.log(e)),r.catchAll(()=>r.succeed([])));return c}function P(e){return r.tryPromise({try:async()=>u(e),catch:T.new}).pipe(r.map(e=>e.toString()))}function F(e){return e.replace(/^~/,c())}function I(e){return e.sort((e,t)=>{let n=e.host,r=t.host;return n.localeCompare(r)})}var L=class extends n.TaggedError(`UI/SearchHost/Error`){static new=e=>new this({error:e})},R=class extends n.TaggedError(`UI/SearchCancellByUser`){static new=e=>new this({error:e})};const z={keys:[{name:`host`,weight:.7},{name:`hostname`,weight:.3}],includeScore:!0,ignoreLocation:!0,threshold:.4,shouldSort:!0,minMatchCharLength:1};function B(e,t=``){if(t===``)return e;let n=new h(e,z),r=n.search(t);return r.map(e=>e.item)}function V(e){let t=i.value({user:e.user,port:e.port}).pipe(i.when({user:{_tag:`Some`},port:{_tag:`Some`}},({port:t,user:n})=>`${n.value}@${e.hostname}:${t.value}`),i.when({user:{_tag:`Some`},port:{_tag:`None`}},({user:t})=>`${t.value}@${e.hostname}`),i.when({user:{_tag:`None`},port:{_tag:`Some`}},({port:t})=>`${e.hostname}:${t.value}`),i.orElse(()=>e.hostname));return`${p.cyan(e.host)} ${p.dim(`→`)} ${p.green(t)}`}function H(e,t){return r.tryPromise({try:async()=>ne({...t,pageSize:10,source:t=>o(e,I,e=>B(e,t),e=>e.length===0&&t?[{name:p.red(`No matches found.`),value:null,disabled:!0}]:e.length===0&&!t?[{name:p.red(`No hosts available.`),value:null,disabled:!0}]:e.map(e=>({name:V(e),value:e})))}),catch:L.new}).pipe(r.flatMap(r.fromNullable))}function U(e){e.length===0&&(console.log(p.yellow(`No SSH hosts available. Please check your SSH configuration.`)),process.exit(0));let t=H(e,{message:`Select a host to connect to`}).pipe(r.map(e=>e),r.catchTag(`NoSuchElementException`,e=>r.fail(R.new(e))));return t}var W=class extends n.TaggedError(`UI/Prompt/Confirm/Error`){static new=e=>new this({error:e})},G=class extends n.TaggedError(`UI/Select/Port/Error`){static new=e=>new this({error:e})},K=class extends n.TaggedError(`UI/Prompt/RemotePort/Error`){static new=e=>new this({error:e})},q=class extends n.TaggedError(`UI/Prompt/LocalPort/Error`){static new=e=>new this({error:e})},J=class extends n.TaggedError(`UI/Prompt/User/NoNeed/ForwardPort`){static new=e=>new this({error:e})};function Y(e){return r.tryPromise({try:()=>te(e),catch:W.new})}function X(){return console.log(p.cyan(`
--- SSH Port Forwarding Configuration ---`)),console.log(p.white(`Port forwarding allows you to access remote services on your local machine.
Example: Remote PostgreSQL (port 5432) accessible at localhost:8432
Command: ${p.green(`ssh -L 8432:localhost:5432 user@server`)}
Benefit: Access remote services on localhost.`)),Y({message:`Do you want to set up port forwarding?`,default:!1})}function ie(e){return{name:`${e.name} => ${e.port}`,value:e.port,description:`${e.name} => ${e.port}`}}function ae(t){let n=e.map(t,ie);return r.tryPromise({try:()=>re({message:`available ports on remote host`,choices:[...n,{name:`custom`,value:0}]}),catch:G.new})}function Z(e){return r.tryPromise({try:()=>m({default:e,message:`Enter the remote port (e.g. 5432) for forwarding`,validate:e=>{let t=f.decodeUnknownOption(f.NumberFromString)(e);return a.isNone(t)?p.red(`invalid port number`):!0},transformer:e=>{let t=f.decodeUnknownOption(f.NumberFromString)(e);if(a.isNone(t))return p.red(e);let n=t.value;return n<1||n>65535?p.red(e):p.green(n)}}),catch:K.new}).pipe(r.map(e=>Number.parseInt(e)))}function oe(e){return r.tryPromise({try:()=>m({default:e,message:`Enter the local port (e.g. 5432) for forwarding`,validate:e=>{let t=f.decodeUnknownOption(f.NumberFromString)(e);return a.isNone(t)?p.red(`invalid port number`):!0},transformer:e=>{let t=f.decodeUnknownOption(f.NumberFromString)(e);if(a.isNone(t))return p.red(e);let n=t.value;return n<1||n>65535?p.red(e):p.green(n)}}),catch:q.new}).pipe(r.map(e=>Number.parseInt(e)))}function se(e){return X().pipe(r.flatMap(r.if({onFalse:()=>r.fail(J.new(`user don't want to forward port`)),onTrue:()=>{let t=e.availablePorts.length>0;return r.Do.pipe(r.bind(`remotePort`,()=>t===!1?Z():ae([...e.availablePorts]).pipe(r.flatMap(e=>e===0?Z():r.succeed(e)))),r.bind(`localPort`,({remotePort:e})=>oe(e.toString())))}})))}var Q=class extends n.TaggedError(`Clippy/Write/Error`){static new=e=>new this({error:e})};const $=_(2,v);function ce(e,t){let n=i.value(t).pipe(i.when({_tag:`Some`},e=>{let{remotePort:t,localPort:n}=e.value;return`-L ${n}:localhost:${t}`}),i.when({_tag:`None`},()=>``),i.exhaustive),r=i.value(e.port).pipe(i.when({_tag:`Some`},e=>`-p ${e}`),i.orElse(()=>``));return`${n} ${r}`}function le(e){return[`ssh`,...e].join(` `)}function ue(e,t){let n=ce(e,t),i=le([e.host,n]),s=o(`Command:`,p.bold,p.green);return o(`${s} ${i}`,$({borderStyle:`round`,borderColor:`greenBright`}),console.log),r.tryPromise({try:()=>g.write(i),catch:Q.new}).pipe(r.tap(()=>{if(a.isSome(e.password)){let t=o(`Password:`,p.bold,p.yellow),n=`${t} ${e.password.value}`;o(n,$({borderStyle:`round`,borderColor:`yellow`}),console.log)}return r.void}))}const de=o(E(),r.flatMap(N),r.bind(`selectedHost`,U),r.bind(`forwardPort`,({selectedHost:e})=>se(e).pipe(r.map(a.some),r.catchTag(`UI/Prompt/User/NoNeed/ForwardPort`,()=>r.succeed(a.none())))),r.map(({selectedHost:e,forwardPort:t})=>({selectedHost:e,forwardPort:t})),r.flatMap(({selectedHost:e,forwardPort:t})=>ue(e,t)),r.catchTag(`UI/SearchHost/Error`,()=>r.void),r.catchTag(`UI/SearchCancellByUser`,()=>r.void),r.catchTag(`UI/Prompt/Confirm/Error`,()=>r.void),r.catchTag(`UI/Prompt/RemotePort/Error`,()=>r.void),r.catchTag(`UI/Select/Port/Error`,()=>r.void),r.catchTag(`UI/Prompt/LocalPort/Error`,()=>r.void),r.catchTag(`Parser/File/Access/Error`,()=>r.void),r.catchTag(`Clippy/Write/Error`,()=>r.void),r.runPromise);