@vibecontrols/vibe-plugin-tool-ssh 2026.530.1 → 2026.530.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ ${suffix}`}};var MAX_32BIT_INT=4294967296,MAX_32BIT_BIGINT=(()=>{try{return Func
16
16
  `,publicB64,cipherName=Buffer.from(encrypted?encrypted.cipherName:"none"),kdfName=Buffer.from(encrypted?encrypted.kdfName:"none"),kdfOptions=encrypted?encrypted.kdfOptions:Buffer.alloc(0),blockLen=encrypted?encrypted.cipher.blockLen:8,parsed=parseDERs(keyType,pub,priv),checkInt=randomBytes(4),commentBin=Buffer.from(comment),privBlobLen=8+parsed.priv.length+4+commentBin.length,padding=[];for(let i=1;(privBlobLen+padding.length)%blockLen;++i)padding.push(i&255);padding=Buffer.from(padding);let privBlob=Buffer.allocUnsafe(privBlobLen+padding.length),extra;{let pos=0;privBlob.set(checkInt,pos+=0),privBlob.set(checkInt,pos+=4),privBlob.set(parsed.priv,pos+=4),privBlob.writeUInt32BE(commentBin.length,pos+=parsed.priv.length),privBlob.set(commentBin,pos+=4),privBlob.set(padding,pos+=commentBin.length)}if(encrypted){let options={authTagLength:encrypted.cipher.authLen},cipher=createCipheriv(encrypted.cipher.sslName,encrypted.key,encrypted.iv,options);if(cipher.setAutoPadding(!1),privBlob=Buffer.concat([cipher.update(privBlob),cipher.final()]),encrypted.cipher.authLen>0)extra=cipher.getAuthTag();else extra=Buffer.alloc(0);encrypted.key.fill(0),encrypted.iv.fill(0)}else extra=Buffer.alloc(0);let magicBytes=Buffer.from("openssh-key-v1\x00"),privBin=Buffer.allocUnsafe(magicBytes.length+4+cipherName.length+4+kdfName.length+4+kdfOptions.length+4+4+parsed.pub.length+4+privBlob.length+extra.length);{let pos=0;privBin.set(magicBytes,pos+=0),privBin.writeUInt32BE(cipherName.length,pos+=magicBytes.length),privBin.set(cipherName,pos+=4),privBin.writeUInt32BE(kdfName.length,pos+=cipherName.length),privBin.set(kdfName,pos+=4),privBin.writeUInt32BE(kdfOptions.length,pos+=kdfName.length),privBin.set(kdfOptions,pos+=4),privBin.writeUInt32BE(1,pos+=kdfOptions.length),privBin.writeUInt32BE(parsed.pub.length,pos+=4),privBin.set(parsed.pub,pos+=4),privBin.writeUInt32BE(privBlob.length,pos+=parsed.pub.length),privBin.set(privBlob,pos+=4),privBin.set(extra,pos+=privBlob.length)}{let b64=privBin.base64Slice(0,privBin.length),formatted=b64.replace(/.{64}/g,`$&
17
17
  `);if(b64.length&63)formatted+=`
18
18
  `;privateB64+=formatted}{let b64=parsed.pub.base64Slice(0,parsed.pub.length);publicB64=`${parsed.sshName} ${b64}${comment?` ${comment}`:""}`}return privateB64+=`-----END OPENSSH PRIVATE KEY-----
19
- `,{private:privateB64,public:publicB64}}default:throw Error("Invalid output key format")}}function noop(){}module.exports={generateKeyPair:(keyType,opts,cb)=>{if(typeof opts==="function")cb=opts,opts=void 0;if(typeof cb!=="function")cb=noop;let args=makeArgs(keyType,opts);generateKeyPair_(...args,(err,pub,priv)=>{if(err)return cb(err);let ret;try{ret=convertKeys(args[0],pub,priv,opts)}catch(ex){return cb(ex)}cb(null,ret)})},generateKeyPairSync:(keyType,opts)=>{let args=makeArgs(keyType,opts),{publicKey:pub,privateKey:priv}=generateKeyPairSync_(...args);return convertKeys(args[0],pub,priv,opts)}}});var require_lib3=__commonJS((exports,module)=>{var{AgentProtocol,BaseAgent,createAgent,CygwinAgent,OpenSSHAgent,PageantAgent}=require_agent(),{SSHTTPAgent:HTTPAgent,SSHTTPSAgent:HTTPSAgent}=require_http_agents(),{parseKey}=require_keyParser(),{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}=require_SFTP();module.exports={AgentProtocol,BaseAgent,createAgent,Client:require_client(),CygwinAgent,HTTPAgent,HTTPSAgent,OpenSSHAgent,PageantAgent,Server:require_server(),utils:{parseKey,...require_keygen(),sftp:{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}}}});import{homedir}from"os";import{resolve}from"path";function expandPath(p){if(!p)return p;if(p==="~")return homedir();if(p.startsWith("~/"))return resolve(homedir(),p.slice(2));return resolve(p)}var init_expand_path=()=>{};var exports_ssh={};__export(exports_ssh,{createSSHRoutes:()=>createSSHRoutes});import{Elysia as Elysia2}from"elysia";async function getAllConnections(storage){let raw=await storage.get("ssh","connections");if(!raw)return[];return JSON.parse(raw)}async function getConnectionById(storage,id){return(await getAllConnections(storage)).find((c)=>c.id===id)}async function saveConnections(storage,connections){await storage.set("ssh","connections",JSON.stringify(connections))}function sanitise(conn){let{password:_pw,privateKeyPath:_pk,...rest}=conn;return{...rest,privateKeyPath:conn.privateKeyPath?"***":void 0}}async function buildConnectConfig(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function createSSHRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia2({prefix:"/api/ssh"}).get("/connections",async()=>{return{connections:(await getAllConnections(storage)).map(sanitise)}}).post("/connections",async({body,set})=>{let{serverName,host,port=22,username,privateKeyPath,password}=body;try{let connections=await getAllConnections(storage),newConn={id:globalThis.crypto.randomUUID(),serverName,host,port,username,privateKeyPath,password,createdAt:new Date().toISOString()};return connections.push(newConn),await saveConnections(storage,connections),{connection:sanitise(newConn)}}catch(error){return set.status=500,{error:"Failed to create SSH connection",details:error instanceof Error?error.message:"Unknown error"}}}).post("/execute",async({body,set})=>{let{connectionId,command,workingDirectory}=body;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,output="",errorOutput="",connectConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{let fullCommand=workingDirectory?`cd ${workingDirectory} && ${command}`:command;conn.exec(fullCommand,(err,stream)=>{if(err){conn.end(),set.status=500,resolve2({error:"Failed to execute command",details:err.message});return}stream.on("close",(code)=>{conn.end(),resolve2({output,errorOutput,exitCode:code,success:code===0})}),stream.on("data",(data)=>{if(output+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stdout"});else console.log(`[ssh:stdout] ${connectionId}: ${data.toString().trimEnd()}`)}),stream.stderr.on("data",(data)=>{if(errorOutput+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stderr"});else console.error(`[ssh:stderr] ${connectionId}: ${data.toString().trimEnd()}`)})})}),conn.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),conn.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to execute SSH command",details:error instanceof Error?error.message:"Unknown error"}}}).post("/test/:connectionId",async({params,set})=>{let{connectionId}=params;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,testConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{conn.end(),resolve2({success:!0,message:"Connection successful"})}),conn.on("error",(err)=>{set.status=500,resolve2({success:!1,error:"Connection failed",details:err.message})}),conn.connect({...testConfig,readyTimeout:1e4})})}catch(error){return set.status=500,{success:!1,error:"Failed to test connection",details:error instanceof Error?error.message:"Unknown error"}}}).get("/connections/:id",async({params,set})=>{let conn=await getConnectionById(storage,params.id);if(!conn)return set.status=404,{error:"Connection not found"};return{connection:sanitise(conn)}}).put("/connections/:id",async({params,body,set})=>{let{id}=params,patch=body;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};let conn=connections[idx];if(patch.serverName!==void 0)conn.serverName=patch.serverName;if(patch.host!==void 0)conn.host=patch.host;if(patch.port!==void 0)conn.port=patch.port;if(patch.username!==void 0)conn.username=patch.username;if(patch.privateKeyPath!==void 0)conn.privateKeyPath=patch.privateKeyPath;if(patch.password!==void 0)conn.password=patch.password;return await saveConnections(storage,connections),{connection:sanitise(connections[idx])}}catch(error){return set.status=500,{error:"Failed to update connection",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/connections/:id",async({params,set})=>{let{id}=params;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};return connections.splice(idx,1),await saveConnections(storage,connections),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete connection",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh2;var init_ssh=__esm(()=>{init_expand_path();import_ssh2=__toESM(require_lib3(),1)});var exports_port_forward={};__export(exports_port_forward,{createPortForwardRoutes:()=>createPortForwardRoutes,cleanupAllTunnels:()=>cleanupAllTunnels});import{Elysia as Elysia3}from"elysia";import{createServer}from"net";async function getConnectionById2(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function getConnectionByName(storage,serverName){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.serverName===serverName)}async function getAllPortForwards(storage){let raw=await storage.get("ssh","port-forwards");if(!raw)return[];return JSON.parse(raw)}async function getPortForwardById(storage,id){return(await getAllPortForwards(storage)).find((pf)=>pf.id===id)}async function savePortForwards(storage,forwards){await storage.set("ssh","port-forwards",JSON.stringify(forwards))}async function updatePortForward(storage,id,patch){let all=await getAllPortForwards(storage),idx=all.findIndex((pf)=>pf.id===id);if(idx!==-1)all[idx]={...all[idx],...patch},await savePortForwards(storage,all)}async function deletePortForwardById(storage,id){let filtered=(await getAllPortForwards(storage)).filter((pf)=>pf.id!==id);await savePortForwards(storage,filtered)}async function buildConnectConfig2(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function cleanupAllTunnels(){for(let[,{client,server}]of activeConnections)server.close(),client.end();activeConnections.clear()}function createPortForwardRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia3({prefix:"/api/port-forward"}).get("/",async()=>{return{portForwards:await getAllPortForwards(storage)}}).post("/",async({body,set})=>{let{localPort,remoteHost,remotePort,connectionId}=body;try{if((await getAllPortForwards(storage)).find((pf)=>pf.localPort===localPort&&pf.status==="active"))return set.status=409,{error:"Local port is already in use"};let connectionConfig=await getConnectionById2(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let newPf={id:globalThis.crypto.randomUUID(),localPort,remoteHost,remotePort,serverName:connectionConfig.serverName,connectionId,status:"inactive",createdAt:new Date().toISOString()},all=await getAllPortForwards(storage);return all.push(newPf),await savePortForwards(storage,all),{portForward:newPf}}catch(error){return set.status=500,{error:"Failed to create port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/start",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active")return set.status=400,{error:"Port forward is already active"};let connectionConfig=portForward.connectionId?await getConnectionById2(storage,portForward.connectionId):await getConnectionByName(storage,portForward.serverName);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh22.Client,connectConfig=await buildConnectConfig2(connectionConfig),server=createServer((localSocket)=>{sshClient.forwardOut("localhost",portForward.localPort,portForward.remoteHost,portForward.remotePort,(err,stream)=>{if(err){localSocket.end(),console.error("Forward error:",err);return}localSocket.pipe(stream).pipe(localSocket),localSocket.on("close",()=>{stream.end()}),stream.on("close",()=>{localSocket.end()})})});return new Promise((resolve2)=>{sshClient.on("ready",()=>{server.listen(portForward.localPort,()=>{if(activeConnections.set(id,{client:sshClient,server}),updatePortForward(storage,id,{status:"active"}),eventBus)eventBus.emit("portforward:started",{id,localPort:portForward.localPort});else console.log(`[port-forward] Started tunnel ${id} on localhost:${portForward.localPort}`);resolve2({success:!0,message:`Port forwarding started on localhost:${portForward.localPort}`})}),server.on("error",(err)=>{sshClient.end(),set.status=500,resolve2({error:"Failed to start local server",details:err.message})})}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/stop",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status!=="active")return set.status=400,{error:"Port forward is not active"};let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id);if(await updatePortForward(storage,id,{status:"inactive"}),eventBus)eventBus.emit("portforward:stopped",{id,localPort:portForward.localPort});else console.log(`[port-forward] Stopped tunnel ${id} (localhost:${portForward.localPort})`);return{success:!0}}catch(error){return set.status=500,{error:"Failed to stop port forward",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/:id",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active"){let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id)}return await deletePortForwardById(storage,id),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete port forward",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh22,activeConnections;var init_port_forward=__esm(()=>{init_expand_path();import_ssh22=__toESM(require_lib3(),1),activeConnections=new Map});var exports_remote_terminal={};__export(exports_remote_terminal,{listTerminalSessions:()=>listTerminalSessions,getTerminalInfo:()=>getTerminalInfo,createRemoteTerminalRoutes:()=>createRemoteTerminalRoutes,cleanupAllTerminals:()=>cleanupAllTerminals});import{Elysia as Elysia4}from"elysia";import{createServer as createServer2}from"net";async function getConnectionById3(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function persistSessions(storage){let sessions=Array.from(activeTerminals.values()).map((t)=>t.session);await storage.set("ssh","terminal-sessions",JSON.stringify(sessions))}async function buildConnectConfig3(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function findFreeRemotePort(client,start=7881,end=8080){for(let port=start;port<=end;port++){let{code}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`),{stdout}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);if(stdout==="free")return port}throw Error(`No free port found on remote server in range ${start}-${end}`)}function findFreeLocalPort(start=7681,end=7880){return new Promise((resolve2,reject)=>{let current=start;function tryPort(){if(current>end)return reject(Error(`No free local port found in range ${start}-${end}`));for(let[,t]of activeTerminals)if(t.session.localPort===current)return current++,tryPort();let srv=createServer2();srv.once("error",()=>{current++,tryPort()}),srv.once("listening",()=>{srv.close(()=>resolve2(current))}),srv.listen(current,"127.0.0.1")}tryPort()})}async function ensureTtydInstalled(client){let{code}=await sshExec(client,"which ttyd");if(code===0)return!0;let{stdout:osRelease}=await sshExec(client,"cat /etc/os-release 2>/dev/null || echo unknown"),installCmd;if(osRelease.includes("debian")||osRelease.includes("ubuntu")||osRelease.includes("ID=debian")||osRelease.includes("ID=ubuntu"))installCmd="sudo apt-get update -qq && sudo apt-get install -y -qq ttyd 2>/dev/null";else if(osRelease.includes("centos")||osRelease.includes("fedora")||osRelease.includes("rhel")||osRelease.includes("ID=centos")||osRelease.includes("ID=fedora"))installCmd="sudo yum install -y -q ttyd 2>/dev/null";else if(osRelease.includes("alpine")||osRelease.includes("ID=alpine"))installCmd="sudo apk add --quiet ttyd 2>/dev/null";else{let{stdout:arch}=await sshExec(client,"uname -m");installCmd=`curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${{x86_64:"x86_64",aarch64:"aarch64",arm64:"aarch64"}[arch]||"x86_64"}" -o /tmp/ttyd && chmod +x /tmp/ttyd && sudo mv /tmp/ttyd /usr/local/bin/ttyd`}if((await sshExec(client,installCmd)).code!==0){let{code:retryCode}=await sshExec(client,installCmd.replace(/sudo /g,""));return retryCode===0}return!0}function cleanupAllTerminals(){for(let[id,terminal]of activeTerminals){try{if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill()}catch{}try{terminal.sshClient.end()}catch{}activeTerminals.delete(id)}}function getTerminalInfo(sessionId){let terminal=activeTerminals.get(sessionId);if(!terminal||terminal.session.status!=="active")return null;return{url:`http://127.0.0.1:${terminal.session.localPort}`,port:terminal.session.localPort,pid:process.pid}}function listTerminalSessions(){return Array.from(activeTerminals.values()).map((t)=>t.session)}function createRemoteTerminalRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia4({prefix:"/api/ssh/terminal"}).get("/sessions",()=>{return{sessions:listTerminalSessions()}}).get("/sessions/:id",({params,set})=>{let terminal=activeTerminals.get(params.id);if(!terminal)return set.status=404,{error:"Terminal session not found"};return{session:terminal.session}}).post("/start",async({body,set})=>{let{connectionId,shell="bash"}=body;try{let connConfig=await getConnectionById3(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sessionId=globalThis.crypto.randomUUID(),session={id:sessionId,connectionId,remotePort:0,localPort:0,remotePid:null,shell,status:"starting",startedAt:new Date().toISOString()},sshClient=new import_ssh23.Client,connectConfig=await buildConnectConfig3(connConfig);return new Promise((resolve2)=>{sshClient.on("error",(err)=>{session.status="error",session.error=err.message,set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.on("ready",async()=>{try{if(!await ensureTtydInstalled(sshClient))return sshClient.end(),session.status="error",session.error="Failed to install ttyd on remote server",set.status=500,resolve2({error:"ttyd is not installed on the remote server and auto-install failed"});let remotePort=await findFreeRemotePort(sshClient);session.remotePort=remotePort;let{stdout:pidOutput,code:startCode}=await sshExec(sshClient,`nohup ttyd --writable --port ${remotePort} ${shell} > /dev/null 2>&1 & echo $!`);if(startCode!==0||!pidOutput)return sshClient.end(),session.status="error",session.error="Failed to start ttyd on remote",set.status=500,resolve2({error:"Failed to start ttyd on remote"});let remotePid=parseInt(pidOutput,10);session.remotePid=remotePid,await new Promise((r)=>setTimeout(r,1500));let{stdout:listenCheck}=await sshExec(sshClient,`ss -tlnp 2>/dev/null | grep ':${remotePort} ' || echo NOT_LISTENING`);if(listenCheck.includes("NOT_LISTENING"))await new Promise((r)=>setTimeout(r,2000));let localPort=await findFreeLocalPort();session.localPort=localPort;try{let sshArgs=["-N","-L",`${localPort}:127.0.0.1:${remotePort}`,"-o","StrictHostKeyChecking=accept-new","-o","ServerAliveInterval=30","-o","ExitOnForwardFailure=yes","-p",String(connConfig.port)];if(connConfig.privateKeyPath)sshArgs.push("-i",expandPath(connConfig.privateKeyPath));sshArgs.push(`${connConfig.username}@${connConfig.host}`);let sshProc=Bun.spawn(["ssh",...sshArgs],{stdout:"ignore",stderr:"pipe"});await new Promise((r)=>setTimeout(r,2000));let checkSrv=createServer2();if(!await new Promise((res)=>{checkSrv.once("error",()=>res(!0)),checkSrv.once("listening",()=>{checkSrv.close(),res(!1)}),checkSrv.listen(localPort,"127.0.0.1")}))await new Promise((r)=>setTimeout(r,2000));if(session.status="active",activeTerminals.set(sessionId,{sshClient,sshTunnelProc:sshProc,session}),persistSessions(storage),broadcast)broadcast("ssh:terminal:started",{sessionId,connectionId,localPort,remotePort,host:connConfig.host});resolve2({sessionId,connectionId,localPort,remotePort,host:connConfig.host,status:"active"})}catch(bindErr){sshExec(sshClient,`kill ${remotePid} 2>/dev/null`),sshClient.end(),session.status="error",session.error=bindErr instanceof Error?bindErr.message:"Port bind failed",set.status=500,resolve2({error:"Failed to bind local port",details:session.error})}}catch(err){sshClient.end(),session.status="error",session.error=err instanceof Error?err.message:"Unknown error",set.status=500,resolve2({error:"Failed to start remote terminal",details:session.error})}}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start remote terminal",details:error instanceof Error?error.message:"Unknown error"}}}).post("/stop",async({body,set})=>{let{sessionId}=body,terminal=activeTerminals.get(sessionId);if(!terminal)return set.status=404,{error:"Terminal session not found"};try{if(terminal.session.status="stopping",terminal.session.remotePid)await sshExec(terminal.sshClient,`kill ${terminal.session.remotePid} 2>/dev/null`).catch(()=>{});if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill();if(terminal.sshClient.end(),terminal.session.status="stopped",activeTerminals.delete(sessionId),persistSessions(storage),broadcast)broadcast("ssh:terminal:stopped",{sessionId,connectionId:terminal.session.connectionId});return{success:!0,sessionId}}catch(error){return set.status=500,{error:"Failed to stop terminal session",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh23,activeTerminals;var init_remote_terminal=__esm(()=>{init_expand_path();import_ssh23=__toESM(require_lib3(),1),activeTerminals=new Map});var exports_remote_agent_install={};__export(exports_remote_agent_install,{createRemoteAgentInstallRoutes:()=>createRemoteAgentInstallRoutes});import{Elysia as Elysia5}from"elysia";import{homedir as homedir2,tmpdir}from"os";import{join as joinPath}from"path";async function getConnectionById4(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig4(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec2(client,command,timeoutMs=60000){return new Promise((resolve2,reject)=>{let timer=setTimeout(()=>{reject(Error(`Command timed out after ${timeoutMs}ms`))},timeoutMs);client.exec(command,(err,stream)=>{if(err)return clearTimeout(timer),reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{clearTimeout(timer),resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function runInstallation(job,connConfig,agentPort,hostServices,options={}){let{broadcast}=hostServices,sshClient=new import_ssh24.Client,registryUrl=hostServices.getPluginRegistry?.()??DEFAULT_REGISTRY;function emitProgress(){if(broadcast)broadcast("ssh:install:progress",{...job})}function updateStep(stepIdx,status,message){if(job.steps[stepIdx].status=status,message)job.steps[stepIdx].message=message;job.currentStep=stepIdx,emitProgress()}function fail(stepIdx,error){if(updateStep(stepIdx,"failed",error),job.status="failed",job.error=error,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}try{let connectConfig=await buildConnectConfig4(connConfig);updateStep(0,"running"),await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})}),updateStep(0,"completed","Connected successfully"),updateStep(1,"running");let{stdout:osInfo}=await sshExec2(sshClient,"uname -s && uname -m"),[osName,arch]=osInfo.split(`
19
+ `,{private:privateB64,public:publicB64}}default:throw Error("Invalid output key format")}}function noop(){}module.exports={generateKeyPair:(keyType,opts,cb)=>{if(typeof opts==="function")cb=opts,opts=void 0;if(typeof cb!=="function")cb=noop;let args=makeArgs(keyType,opts);generateKeyPair_(...args,(err,pub,priv)=>{if(err)return cb(err);let ret;try{ret=convertKeys(args[0],pub,priv,opts)}catch(ex){return cb(ex)}cb(null,ret)})},generateKeyPairSync:(keyType,opts)=>{let args=makeArgs(keyType,opts),{publicKey:pub,privateKey:priv}=generateKeyPairSync_(...args);return convertKeys(args[0],pub,priv,opts)}}});var require_lib3=__commonJS((exports,module)=>{var{AgentProtocol,BaseAgent,createAgent,CygwinAgent,OpenSSHAgent,PageantAgent}=require_agent(),{SSHTTPAgent:HTTPAgent,SSHTTPSAgent:HTTPSAgent}=require_http_agents(),{parseKey}=require_keyParser(),{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}=require_SFTP();module.exports={AgentProtocol,BaseAgent,createAgent,Client:require_client(),CygwinAgent,HTTPAgent,HTTPSAgent,OpenSSHAgent,PageantAgent,Server:require_server(),utils:{parseKey,...require_keygen(),sftp:{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}}}});import{homedir}from"os";import{resolve}from"path";function expandPath(p){if(!p)return p;if(p==="~")return homedir();if(p.startsWith("~/"))return resolve(homedir(),p.slice(2));return resolve(p)}var init_expand_path=()=>{};var exports_ssh={};__export(exports_ssh,{createSSHRoutes:()=>createSSHRoutes});import{Elysia as Elysia2}from"elysia";async function getAllConnections(storage){let raw=await storage.get("ssh","connections");if(!raw)return[];return JSON.parse(raw)}async function getConnectionById(storage,id){return(await getAllConnections(storage)).find((c)=>c.id===id)}async function saveConnections(storage,connections){await storage.set("ssh","connections",JSON.stringify(connections))}function sanitise(conn){let{password:_pw,privateKeyPath:_pk,...rest}=conn;return{...rest,privateKeyPath:conn.privateKeyPath?"***":void 0}}async function buildConnectConfig(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function createSSHRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia2({prefix:"/api/ssh"}).get("/connections",async()=>{return{connections:(await getAllConnections(storage)).map(sanitise)}}).post("/connections",async({body,set})=>{let{serverName,host,port=22,username,privateKeyPath,password}=body;try{let connections=await getAllConnections(storage),newConn={id:globalThis.crypto.randomUUID(),serverName,host,port,username,privateKeyPath,password,createdAt:new Date().toISOString()};return connections.push(newConn),await saveConnections(storage,connections),{connection:sanitise(newConn)}}catch(error){return set.status=500,{error:"Failed to create SSH connection",details:error instanceof Error?error.message:"Unknown error"}}}).post("/execute",async({body,set})=>{let{connectionId,command,workingDirectory}=body;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,output="",errorOutput="",connectConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{let fullCommand=workingDirectory?`cd ${workingDirectory} && ${command}`:command;conn.exec(fullCommand,(err,stream)=>{if(err){conn.end(),set.status=500,resolve2({error:"Failed to execute command",details:err.message});return}stream.on("close",(code)=>{conn.end(),resolve2({output,errorOutput,exitCode:code,success:code===0})}),stream.on("data",(data)=>{if(output+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stdout"});else console.log(`[ssh:stdout] ${connectionId}: ${data.toString().trimEnd()}`)}),stream.stderr.on("data",(data)=>{if(errorOutput+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stderr"});else console.error(`[ssh:stderr] ${connectionId}: ${data.toString().trimEnd()}`)})})}),conn.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),conn.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to execute SSH command",details:error instanceof Error?error.message:"Unknown error"}}}).post("/test/:connectionId",async({params,set})=>{let{connectionId}=params;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,testConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{conn.end(),resolve2({success:!0,message:"Connection successful"})}),conn.on("error",(err)=>{set.status=500,resolve2({success:!1,error:"Connection failed",details:err.message})}),conn.connect({...testConfig,readyTimeout:1e4})})}catch(error){return set.status=500,{success:!1,error:"Failed to test connection",details:error instanceof Error?error.message:"Unknown error"}}}).get("/connections/:id",async({params,set})=>{let conn=await getConnectionById(storage,params.id);if(!conn)return set.status=404,{error:"Connection not found"};return{connection:sanitise(conn)}}).put("/connections/:id",async({params,body,set})=>{let{id}=params,patch=body;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};let conn=connections[idx];if(patch.serverName!==void 0)conn.serverName=patch.serverName;if(patch.host!==void 0)conn.host=patch.host;if(patch.port!==void 0)conn.port=patch.port;if(patch.username!==void 0)conn.username=patch.username;if(patch.privateKeyPath!==void 0)conn.privateKeyPath=patch.privateKeyPath;if(patch.password!==void 0)conn.password=patch.password;return await saveConnections(storage,connections),{connection:sanitise(connections[idx])}}catch(error){return set.status=500,{error:"Failed to update connection",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/connections/:id",async({params,set})=>{let{id}=params;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};return connections.splice(idx,1),await saveConnections(storage,connections),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete connection",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh2;var init_ssh=__esm(()=>{init_expand_path();import_ssh2=__toESM(require_lib3(),1)});var exports_port_forward={};__export(exports_port_forward,{createPortForwardRoutes:()=>createPortForwardRoutes,cleanupAllTunnels:()=>cleanupAllTunnels});import{Elysia as Elysia3}from"elysia";import{createServer}from"net";async function getConnectionById2(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function getConnectionByName(storage,serverName){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.serverName===serverName)}async function getAllPortForwards(storage){let raw=await storage.get("ssh","port-forwards");if(!raw)return[];return JSON.parse(raw)}async function getPortForwardById(storage,id){return(await getAllPortForwards(storage)).find((pf)=>pf.id===id)}async function savePortForwards(storage,forwards){await storage.set("ssh","port-forwards",JSON.stringify(forwards))}async function updatePortForward(storage,id,patch){let all=await getAllPortForwards(storage),idx=all.findIndex((pf)=>pf.id===id);if(idx!==-1)all[idx]={...all[idx],...patch},await savePortForwards(storage,all)}async function deletePortForwardById(storage,id){let filtered=(await getAllPortForwards(storage)).filter((pf)=>pf.id!==id);await savePortForwards(storage,filtered)}async function buildConnectConfig2(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function cleanupAllTunnels(){for(let[,{client,server}]of activeConnections)server.close(),client.end();activeConnections.clear()}function createPortForwardRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia3({prefix:"/api/port-forward"}).get("/",async()=>{return{portForwards:await getAllPortForwards(storage)}}).post("/",async({body,set})=>{let{localPort,remoteHost,remotePort,connectionId}=body;try{if((await getAllPortForwards(storage)).find((pf)=>pf.localPort===localPort&&pf.status==="active"))return set.status=409,{error:"Local port is already in use"};let connectionConfig=await getConnectionById2(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let newPf={id:globalThis.crypto.randomUUID(),localPort,remoteHost,remotePort,serverName:connectionConfig.serverName,connectionId,status:"inactive",createdAt:new Date().toISOString()},all=await getAllPortForwards(storage);return all.push(newPf),await savePortForwards(storage,all),{portForward:newPf}}catch(error){return set.status=500,{error:"Failed to create port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/start",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active")return set.status=400,{error:"Port forward is already active"};let connectionConfig=portForward.connectionId?await getConnectionById2(storage,portForward.connectionId):await getConnectionByName(storage,portForward.serverName);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh22.Client,connectConfig=await buildConnectConfig2(connectionConfig),server=createServer((localSocket)=>{sshClient.forwardOut("localhost",portForward.localPort,portForward.remoteHost,portForward.remotePort,(err,stream)=>{if(err){localSocket.end(),console.error("Forward error:",err);return}localSocket.pipe(stream).pipe(localSocket),localSocket.on("close",()=>{stream.end()}),stream.on("close",()=>{localSocket.end()})})});return new Promise((resolve2)=>{sshClient.on("ready",()=>{server.listen(portForward.localPort,()=>{if(activeConnections.set(id,{client:sshClient,server}),updatePortForward(storage,id,{status:"active"}),eventBus)eventBus.emit("portforward:started",{id,localPort:portForward.localPort});else console.log(`[port-forward] Started tunnel ${id} on localhost:${portForward.localPort}`);resolve2({success:!0,message:`Port forwarding started on localhost:${portForward.localPort}`})}),server.on("error",(err)=>{sshClient.end(),set.status=500,resolve2({error:"Failed to start local server",details:err.message})})}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/stop",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status!=="active")return set.status=400,{error:"Port forward is not active"};let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id);if(await updatePortForward(storage,id,{status:"inactive"}),eventBus)eventBus.emit("portforward:stopped",{id,localPort:portForward.localPort});else console.log(`[port-forward] Stopped tunnel ${id} (localhost:${portForward.localPort})`);return{success:!0}}catch(error){return set.status=500,{error:"Failed to stop port forward",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/:id",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active"){let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id)}return await deletePortForwardById(storage,id),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete port forward",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh22,activeConnections;var init_port_forward=__esm(()=>{init_expand_path();import_ssh22=__toESM(require_lib3(),1),activeConnections=new Map});var exports_remote_terminal={};__export(exports_remote_terminal,{listTerminalSessions:()=>listTerminalSessions,getTerminalInfo:()=>getTerminalInfo,createRemoteTerminalRoutes:()=>createRemoteTerminalRoutes,cleanupAllTerminals:()=>cleanupAllTerminals});import{Elysia as Elysia4}from"elysia";import{createServer as createServer2}from"net";async function getConnectionById3(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function persistSessions(storage){let sessions=Array.from(activeTerminals.values()).map((t)=>t.session);await storage.set("ssh","terminal-sessions",JSON.stringify(sessions))}async function buildConnectConfig3(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function findFreeRemotePort(client,start=7881,end=8080){for(let port=start;port<=end;port++){let{code}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`),{stdout}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);if(stdout==="free")return port}throw Error(`No free port found on remote server in range ${start}-${end}`)}function findFreeLocalPort(start=7681,end=7880){return new Promise((resolve2,reject)=>{let current=start;function tryPort(){if(current>end)return reject(Error(`No free local port found in range ${start}-${end}`));for(let[,t]of activeTerminals)if(t.session.localPort===current)return current++,tryPort();let srv=createServer2();srv.once("error",()=>{current++,tryPort()}),srv.once("listening",()=>{srv.close(()=>resolve2(current))}),srv.listen(current,"127.0.0.1")}tryPort()})}async function ensureTtydInstalled(client){let{code}=await sshExec(client,"which ttyd");if(code===0)return!0;let{stdout:osRelease}=await sshExec(client,"cat /etc/os-release 2>/dev/null || echo unknown"),installCmd;if(osRelease.includes("debian")||osRelease.includes("ubuntu")||osRelease.includes("ID=debian")||osRelease.includes("ID=ubuntu"))installCmd="sudo apt-get update -qq && sudo apt-get install -y -qq ttyd 2>/dev/null";else if(osRelease.includes("centos")||osRelease.includes("fedora")||osRelease.includes("rhel")||osRelease.includes("ID=centos")||osRelease.includes("ID=fedora"))installCmd="sudo yum install -y -q ttyd 2>/dev/null";else if(osRelease.includes("alpine")||osRelease.includes("ID=alpine"))installCmd="sudo apk add --quiet ttyd 2>/dev/null";else{let{stdout:arch}=await sshExec(client,"uname -m");installCmd=`curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${{x86_64:"x86_64",aarch64:"aarch64",arm64:"aarch64"}[arch]||"x86_64"}" -o /tmp/ttyd && chmod +x /tmp/ttyd && sudo mv /tmp/ttyd /usr/local/bin/ttyd`}if((await sshExec(client,installCmd)).code!==0){let{code:retryCode}=await sshExec(client,installCmd.replace(/sudo /g,""));return retryCode===0}return!0}function cleanupAllTerminals(){for(let[id,terminal]of activeTerminals){try{if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill()}catch{}try{terminal.sshClient.end()}catch{}activeTerminals.delete(id)}}function getTerminalInfo(sessionId){let terminal=activeTerminals.get(sessionId);if(!terminal||terminal.session.status!=="active")return null;return{url:`http://127.0.0.1:${terminal.session.localPort}`,port:terminal.session.localPort,pid:process.pid,host:"127.0.0.1",wsPath:"/ws",subprotocols:["tty"]}}function listTerminalSessions(){return Array.from(activeTerminals.values()).map((t)=>t.session)}function createRemoteTerminalRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia4({prefix:"/api/ssh/terminal"}).get("/sessions",()=>{return{sessions:listTerminalSessions()}}).get("/sessions/:id",({params,set})=>{let terminal=activeTerminals.get(params.id);if(!terminal)return set.status=404,{error:"Terminal session not found"};return{session:terminal.session}}).post("/start",async({body,set})=>{let{connectionId,shell="bash"}=body;try{let connConfig=await getConnectionById3(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sessionId=globalThis.crypto.randomUUID(),session={id:sessionId,connectionId,remotePort:0,localPort:0,remotePid:null,shell,status:"starting",startedAt:new Date().toISOString()},sshClient=new import_ssh23.Client,connectConfig=await buildConnectConfig3(connConfig);return new Promise((resolve2)=>{sshClient.on("error",(err)=>{session.status="error",session.error=err.message,set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.on("ready",async()=>{try{if(!await ensureTtydInstalled(sshClient))return sshClient.end(),session.status="error",session.error="Failed to install ttyd on remote server",set.status=500,resolve2({error:"ttyd is not installed on the remote server and auto-install failed"});let remotePort=await findFreeRemotePort(sshClient);session.remotePort=remotePort;let{stdout:pidOutput,code:startCode}=await sshExec(sshClient,`nohup ttyd --writable --port ${remotePort} ${shell} > /dev/null 2>&1 & echo $!`);if(startCode!==0||!pidOutput)return sshClient.end(),session.status="error",session.error="Failed to start ttyd on remote",set.status=500,resolve2({error:"Failed to start ttyd on remote"});let remotePid=parseInt(pidOutput,10);session.remotePid=remotePid,await new Promise((r)=>setTimeout(r,1500));let{stdout:listenCheck}=await sshExec(sshClient,`ss -tlnp 2>/dev/null | grep ':${remotePort} ' || echo NOT_LISTENING`);if(listenCheck.includes("NOT_LISTENING"))await new Promise((r)=>setTimeout(r,2000));let localPort=await findFreeLocalPort();session.localPort=localPort;try{let sshArgs=["-N","-L",`${localPort}:127.0.0.1:${remotePort}`,"-o","StrictHostKeyChecking=accept-new","-o","ServerAliveInterval=30","-o","ExitOnForwardFailure=yes","-p",String(connConfig.port)];if(connConfig.privateKeyPath)sshArgs.push("-i",expandPath(connConfig.privateKeyPath));sshArgs.push(`${connConfig.username}@${connConfig.host}`);let sshProc=Bun.spawn(["ssh",...sshArgs],{stdout:"ignore",stderr:"pipe"});await new Promise((r)=>setTimeout(r,2000));let checkSrv=createServer2();if(!await new Promise((res)=>{checkSrv.once("error",()=>res(!0)),checkSrv.once("listening",()=>{checkSrv.close(),res(!1)}),checkSrv.listen(localPort,"127.0.0.1")}))await new Promise((r)=>setTimeout(r,2000));if(session.status="active",activeTerminals.set(sessionId,{sshClient,sshTunnelProc:sshProc,session}),persistSessions(storage),broadcast)broadcast("ssh:terminal:started",{sessionId,connectionId,localPort,remotePort,host:connConfig.host});resolve2({sessionId,connectionId,localPort,remotePort,host:connConfig.host,status:"active"})}catch(bindErr){sshExec(sshClient,`kill ${remotePid} 2>/dev/null`),sshClient.end(),session.status="error",session.error=bindErr instanceof Error?bindErr.message:"Port bind failed",set.status=500,resolve2({error:"Failed to bind local port",details:session.error})}}catch(err){sshClient.end(),session.status="error",session.error=err instanceof Error?err.message:"Unknown error",set.status=500,resolve2({error:"Failed to start remote terminal",details:session.error})}}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start remote terminal",details:error instanceof Error?error.message:"Unknown error"}}}).post("/stop",async({body,set})=>{let{sessionId}=body,terminal=activeTerminals.get(sessionId);if(!terminal)return set.status=404,{error:"Terminal session not found"};try{if(terminal.session.status="stopping",terminal.session.remotePid)await sshExec(terminal.sshClient,`kill ${terminal.session.remotePid} 2>/dev/null`).catch(()=>{});if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill();if(terminal.sshClient.end(),terminal.session.status="stopped",activeTerminals.delete(sessionId),persistSessions(storage),broadcast)broadcast("ssh:terminal:stopped",{sessionId,connectionId:terminal.session.connectionId});return{success:!0,sessionId}}catch(error){return set.status=500,{error:"Failed to stop terminal session",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh23,activeTerminals;var init_remote_terminal=__esm(()=>{init_expand_path();import_ssh23=__toESM(require_lib3(),1),activeTerminals=new Map});var exports_remote_agent_install={};__export(exports_remote_agent_install,{createRemoteAgentInstallRoutes:()=>createRemoteAgentInstallRoutes});import{Elysia as Elysia5}from"elysia";import{homedir as homedir2,tmpdir}from"os";import{join as joinPath}from"path";async function getConnectionById4(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig4(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec2(client,command,timeoutMs=60000){return new Promise((resolve2,reject)=>{let timer=setTimeout(()=>{reject(Error(`Command timed out after ${timeoutMs}ms`))},timeoutMs);client.exec(command,(err,stream)=>{if(err)return clearTimeout(timer),reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{clearTimeout(timer),resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function runInstallation(job,connConfig,agentPort,hostServices,options={}){let{broadcast}=hostServices,sshClient=new import_ssh24.Client,registryUrl=hostServices.getPluginRegistry?.()??DEFAULT_REGISTRY;function emitProgress(){if(broadcast)broadcast("ssh:install:progress",{...job})}function updateStep(stepIdx,status,message){if(job.steps[stepIdx].status=status,message)job.steps[stepIdx].message=message;job.currentStep=stepIdx,emitProgress()}function fail(stepIdx,error){if(updateStep(stepIdx,"failed",error),job.status="failed",job.error=error,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}try{let connectConfig=await buildConnectConfig4(connConfig);updateStep(0,"running"),await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})}),updateStep(0,"completed","Connected successfully"),updateStep(1,"running");let{stdout:osInfo}=await sshExec2(sshClient,"uname -s && uname -m"),[osName,arch]=osInfo.split(`
20
20
  `);if(updateStep(1,"completed",`${osName} ${arch}`),osName!=="Linux"&&osName!=="Darwin")return fail(1,`Unsupported OS: ${osName}. Only Linux and macOS.`);updateStep(2,"running");let{code:bunCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which bun 2>/dev/null`);if(bunCheck!==0){updateStep(2,"running","Installing prerequisites..."),await sshExec2(sshClient,"which unzip >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq unzip 2>/dev/null || sudo apt-get update -qq && sudo apt-get install -y -qq unzip 2>/dev/null || yum install -y -q unzip 2>/dev/null || sudo yum install -y -q unzip 2>/dev/null || apk add --quiet unzip 2>/dev/null || true)",60000),updateStep(2,"running","Installing Bun...");let{code:bunInstall,stderr:bunErr}=await sshExec2(sshClient,`curl -fsSL https://bun.sh/install | bash && ${REMOTE_PATH} && bun --version`,120000);if(bunInstall!==0)return fail(2,`Failed to install Bun: ${bunErr}`);updateStep(2,"completed","Bun installed")}else updateStep(2,"skipped","Bun already installed");updateStep(3,"running");let{code:agentCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which vibe 2>/dev/null`);if(agentCheck!==0){updateStep(3,"running",`Installing agent (registry: ${registryUrl})...`);let{code:installCode}=await sshExec2(sshClient,`${REMOTE_PATH} && bun install -g @vibecontrols/vibecontrols-agent --registry ${registryUrl}`,120000);if(installCode!==0){updateStep(3,"running","Registry unavailable, transferring directly...");try{let agentDir=hostServices.getConfig?.("agent:packageDir")||`${homedir2()}/products/vibecontrols/vibecontrols-agent`,localTmpDir=tmpdir(),tgzName=Bun.spawnSync(["npm","pack","--pack-destination",localTmpDir],{cwd:agentDir,stdout:"pipe",stderr:"pipe"}).stdout.toString().trim().split(`
21
21
  `).pop(),tgzPath=tgzName?joinPath(localTmpDir,tgzName):"";if(!tgzPath||!await Bun.file(tgzPath).exists())return fail(3,"Failed to pack agent for transfer");let scpArgs=["-o","StrictHostKeyChecking=accept-new","-P",String(connConfig.port)];if(connConfig.privateKeyPath)scpArgs.push("-i",expandPath(connConfig.privateKeyPath));if(scpArgs.push(tgzPath,`${connConfig.username}@${connConfig.host}:/tmp/vibecontrols-agent.tgz`),Bun.spawnSync(["scp",...scpArgs]).exitCode!==0)return fail(3,"Failed to transfer agent package via SCP");let{code:setupCode2,stderr:setupErr2}=await sshExec2(sshClient,`${REMOTE_PATH} && mkdir -p $HOME/.vibecontrols/agent && cd $HOME/.vibecontrols/agent && tar xzf /tmp/vibecontrols-agent.tgz --strip-components=1 && bun install --production --ignore-scripts 2>/dev/null; mkdir -p $HOME/.local/bin && ln -sf $HOME/.vibecontrols/agent/dist/cli.js $HOME/.local/bin/vibe && chmod +x $HOME/.vibecontrols/agent/dist/cli.js && rm -f /tmp/vibecontrols-agent.tgz`,120000);if(setupCode2!==0)return fail(3,`Failed to set up agent: ${setupErr2?.slice(0,200)}`);let{code:vibeCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which vibe`);if(vibeCheck!==0)return fail(3,"Agent installed but 'vibe' command not found in PATH")}catch(transferErr){return fail(3,`Failed to install agent: ${transferErr instanceof Error?transferErr.message:"transfer failed"}`)}}updateStep(3,"completed","Agent installed")}else updateStep(3,"skipped","Agent already installed");updateStep(4,"running");let{code:cfCheck}=await sshExec2(sshClient,"which cloudflared 2>/dev/null"),hasCloudflared=cfCheck===0,{code:setupCode,stderr:setupErr}=await sshExec2(sshClient,`${REMOTE_PATH} && vibe setup --non-interactive --port ${agentPort}`,30000);if(setupCode!==0)updateStep(4,"completed",setupErr?`Warning: ${setupErr.slice(0,120)}`:"Configured with defaults");else updateStep(4,"completed","Agent configured");updateStep(5,"running");let{code:portCheck}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/health`,5000);if(portCheck===0)updateStep(5,"skipped","Agent already running on this port");else{let tunnelEnv=hasCloudflared?"":"AGENT_TUNNEL=false ",{code:startCode,stderr:startErr}=await sshExec2(sshClient,`${REMOTE_PATH} && ${tunnelEnv}PORT=${agentPort} nohup vibe start > /dev/null 2>&1 & sleep 3 && echo started || (cd $HOME/.vibecontrols/agent && ${tunnelEnv}PORT=${agentPort} nohup bun run dist/index.js > /tmp/vibe-agent.log 2>&1 & sleep 3 && echo started)`,30000);if(startCode!==0&&!startErr.includes("already running"))return fail(5,`Failed to start agent: ${startErr}`);updateStep(5,"completed","Agent started")}updateStep(6,"running"),await new Promise((r)=>setTimeout(r,3000));let healthy=!1;for(let attempt=0;attempt<8;attempt++){let{code:healthCode}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/health`,5000);if(healthCode===0){healthy=!0;break}await new Promise((r)=>setTimeout(r,2000))}if(!healthy)return fail(6,"Agent health check failed after 8 attempts");updateStep(6,"completed","Agent is healthy"),updateStep(7,"running");let apiKey,tunnelUrl,hostname=connConfig.host,platform="linux",architecture="x86_64",{stdout:keyOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/api-key`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(keyOut)try{apiKey=JSON.parse(keyOut).apiKey}catch{}let{stdout:identityOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/identity`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(identityOut)try{let id=JSON.parse(identityOut);hostname=id.hostname||hostname,platform=id.platform||platform,architecture=id.arch||architecture}catch{}if(hasCloudflared)for(let i=0;i<6;i++){await new Promise((r)=>setTimeout(r,5000));let{stdout:statusOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/status`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(statusOut)try{let s=JSON.parse(statusOut);if(s.tunnelUrl){tunnelUrl=s.tunnelUrl;break}}catch{}}if(updateStep(7,"completed",tunnelUrl?`Tunnel: ${tunnelUrl}`:"Details retrieved (no tunnel)"),job.result={agentUrl:tunnelUrl||`http://${connConfig.host}:${agentPort}`,agentPort,apiKey,tunnelUrl,hostname,platform,architecture},options.autoRegister&&hostServices.isGatewayConfigured?.()&&hostServices.workspaceQuery){updateStep(8,"running");let workspaceId=hostServices.getWorkspaceId?.();if(!workspaceId)updateStep(8,"skipped","No workspace ID configured");else try{let agentName=options.agentName||hostname||connConfig.serverName,mutation=`
22
22
  mutation RegisterInstalledAgent($workspaceId: ID!, $input: RegisterInstalledAgentInput!) {
@@ -8,13 +8,9 @@
8
8
  * "terminal-sessions" → JSON array of SSHTerminalSession objects
9
9
  */
10
10
  import { Elysia } from "elysia";
11
- import type { HostServices, SSHTerminalSession } from "../types.js";
11
+ import type { HostServices, SSHTerminalSession, TerminalInfo } from "../types.js";
12
12
  export declare function cleanupAllTerminals(): void;
13
- export declare function getTerminalInfo(sessionId: string): {
14
- url: string;
15
- port: number;
16
- pid: number;
17
- } | null;
13
+ export declare function getTerminalInfo(sessionId: string): TerminalInfo | null;
18
14
  /** List all active terminal sessions (for the session provider shim). */
19
15
  export declare function listTerminalSessions(): SSHTerminalSession[];
20
16
  export declare function createRemoteTerminalRoutes(hostServices: HostServices): Elysia<"/api/ssh/terminal", {
package/dist/types.d.ts CHANGED
@@ -100,6 +100,35 @@ export interface SSHTerminalSession {
100
100
  startedAt: string;
101
101
  error?: string;
102
102
  }
103
+ /**
104
+ * Terminal transport descriptor returned to the agent's terminal proxy.
105
+ *
106
+ * The proxy connects the browser WebSocket to `ws://{host}:{port}{wsPath}`
107
+ * negotiating `subprotocols`, WITHOUT assuming any particular terminal
108
+ * backend. For this SSH provider the backend is a remote `ttyd` reached over
109
+ * an `ssh -L` port forward; ttyd serves the live PTY at `/ws` with the `tty`
110
+ * subprotocol, bound to loopback on the agent side of the tunnel.
111
+ */
112
+ export interface TerminalInfo {
113
+ url: string;
114
+ port: number;
115
+ pid: number;
116
+ /**
117
+ * Loopback host the (forwarded) terminal server listens on. The agent's
118
+ * terminal proxy connects here. Defaults to `127.0.0.1` when omitted.
119
+ */
120
+ host?: string;
121
+ /**
122
+ * WebSocket path the terminal server exposes for the live PTY stream, e.g.
123
+ * `/ws` for ttyd. Defaults to `/ws` when omitted.
124
+ */
125
+ wsPath?: string;
126
+ /**
127
+ * WebSocket subprotocols the terminal server negotiates (e.g. `["tty"]` for
128
+ * ttyd). Forwarded verbatim by the agent. Defaults to `["tty"]` when omitted.
129
+ */
130
+ subprotocols?: string[];
131
+ }
103
132
  export type InstallJobStatus = "pending" | "running" | "completed" | "failed";
104
133
  export type InstallStepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
105
134
  export interface RemoteAgentInstallStep {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-tool-ssh",
3
- "version": "2026.530.1",
3
+ "version": "2026.530.3",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "engines": {