@vibecontrols/vibe-plugin-tool-ssh 2026.525.1 → 2026.528.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,9 +16,9 @@ ${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}}}});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(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((resolve)=>{conn.on("ready",()=>{let fullCommand=workingDirectory?`cd ${workingDirectory} && ${command}`:command;conn.exec(fullCommand,(err,stream)=>{if(err){conn.end(),set.status=500,resolve({error:"Failed to execute command",details:err.message});return}stream.on("close",(code)=>{conn.end(),resolve({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,resolve({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((resolve)=>{conn.on("ready",()=>{conn.end(),resolve({success:!0,message:"Connection successful"})}),conn.on("error",(err)=>{set.status=500,resolve({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(()=>{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(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((resolve)=>{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}`);resolve({success:!0,message:`Port forwarding started on localhost:${portForward.localPort}`})}),server.on("error",(err)=>{sshClient.end(),set.status=500,resolve({error:"Failed to start local server",details:err.message})})}),sshClient.on("error",(err)=>{set.status=500,resolve({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(()=>{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(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((resolve,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)=>{resolve({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((resolve,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(()=>resolve(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((resolve)=>{sshClient.on("error",(err)=>{session.status="error",session.error=err.message,set.status=500,resolve({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,resolve({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,resolve({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",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});resolve({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,resolve({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,resolve({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(()=>{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{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(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((resolve,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),resolve({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((resolve,reject)=>{sshClient.on("ready",()=>resolve()),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}}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{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")||`${process.env.HOME}/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
- `).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",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=`
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!) {
23
23
  registerInstalledAgent(workspaceId: $workspaceId, input: $input) {
24
24
  id name hostname
@@ -30,15 +30,15 @@ ${suffix}`}};var MAX_32BIT_INT=4294967296,MAX_32BIT_BIGINT=(()=>{try{return Func
30
30
  id name hostname
31
31
  }
32
32
  }
33
- `,{workspaceId,input:{name:agentName,hostname,platform,architecture,apiUrl:`http://${connConfig.host}:${agentPort}`,tunnelUrl:tunnelUrl||void 0,agentApiKey:apiKey||void 0}});if(result.errors?.length)updateStep(8,"failed",result.errors[0].message);else{let agentRecord=result.data?.registerInstalledAgent;if(agentRecord){if(job.result.backendAgentId=agentRecord.id,apiKey&&hostServices.getConfig){let gwPayload=JSON.stringify({tenantApiUrl:hostServices.getConfig("gateway-auth:globalGatewayUrl")||"",workspacesApiUrl:hostServices.getConfig("gateway-auth:workspaceGatewayUrl")||"",appClientId:hostServices.getConfig("gateway-auth:clientId")||"",appClientSecret:hostServices.getConfig("gateway-auth:clientSecret")||"",workspaceId,agentRecordId:agentRecord.id});await sshExec2(sshClient,`echo '${gwPayload.replace(/'/g,"'\\''")}' > /tmp/.vc-gw-auth.json && curl -sf -X POST http://127.0.0.1:${agentPort}/api/agent/gateway-auth -H 'Content-Type: application/json' -H 'x-agent-api-key: ${apiKey}' -d @/tmp/.vc-gw-auth.json && rm -f /tmp/.vc-gw-auth.json`,15000).catch(()=>{})}updateStep(8,"completed",`Registered as "${agentRecord.name}" (${agentRecord.id.slice(0,8)})`)}}}catch(regErr){updateStep(8,"failed",regErr instanceof Error?regErr.message:"Registration failed")}}else updateStep(8,"skipped","Auto-registration not requested or gateway not configured");if(job.status="completed",job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:complete",{...job});sshClient.end()}catch(error){let errMsg=error instanceof Error?error.message:"Unknown error";if(job.status="failed",job.error=errMsg,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}}function createRemoteAgentInstallRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia5({prefix:"/api/ssh/agent-install"}).get("/jobs",()=>{return{jobs:Array.from(installJobs.values())}}).get("/jobs/:id",({params,set})=>{let job=installJobs.get(params.id);if(!job)return set.status=404,{error:"Install job not found"};return{job}}).post("/start",async({body,set})=>{let{connectionId,agentPort=3005,autoRegister=!1,agentName}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};return installJobs.set(jobId,job),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister,agentName}).catch(()=>{}),{jobId,status:"running"}}).post("/batch-start",async({body,set})=>{let{connectionIds,agentPort=3005,autoRegister=!1}=body;if(!connectionIds?.length)return set.status=400,{error:"No connection IDs provided"};let jobs=[];for(let connectionId of connectionIds){let connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)continue;let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};installJobs.set(jobId,job),jobs.push({connectionId,jobId}),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister}).catch(()=>{})}return{jobs,total:jobs.length}}).post("/uninstall",async({body,set})=>{let{connectionId}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh24.Client,connectConfig=await buildConnectConfig4(connConfig);await new Promise((resolve,reject)=>{sshClient.on("ready",()=>resolve()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})});try{await sshExec2(sshClient,'pkill -f "bun.*index.ts" 2>/dev/null; pkill -f "vibe" 2>/dev/null',1e4).catch(()=>{}),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols/agent",1e4),await sshExec2(sshClient,"rm -f $HOME/.local/bin/vibe",1e4),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols",1e4)}finally{sshClient.end()}return{success:!0,message:"Agent uninstalled"}})}var import_ssh24,REMOTE_PATH='export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$HOME/.local/bin:$PATH"',DEFAULT_REGISTRY="https://registry.npmjs.org",installJobs,INSTALL_STEPS;var init_remote_agent_install=__esm(()=>{import_ssh24=__toESM(require_lib3(),1),installJobs=new Map;INSTALL_STEPS=[{name:"connect"},{name:"detect_os"},{name:"install_bun"},{name:"install_agent"},{name:"configure"},{name:"start"},{name:"verify"},{name:"retrieve"},{name:"register"}]});var exports_ssh_config_scan={};__export(exports_ssh_config_scan,{createSSHConfigScanRoutes:()=>createSSHConfigScanRoutes});import{Elysia as Elysia6}from"elysia";function parseSSHConfig(content){let hosts=[],current=null;for(let rawLine of content.split(`
34
- `)){let line=rawLine.trim();if(!line||line.startsWith("#"))continue;let match=line.match(/^(\S+)\s+(.+)$/);if(!match)continue;let[,key,value]=match,keyLower=key.toLowerCase();if(keyLower==="host"){if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});current={name:value,extra:{}}}else if(current)switch(keyLower){case"hostname":current.hostname=value;break;case"port":current.port=parseInt(value,10);break;case"user":current.user=value;break;case"identityfile":current.identityFile=value.replace(/^~/,process.env.HOME||"~");break;case"proxyjump":current.proxyJump=value;break;default:if(!current.extra)current.extra={};current.extra[key]=value}}if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});return hosts}async function getConnectionById5(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig5(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(conn.privateKeyPath);cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec3(client,command){return new Promise((resolve,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)=>{resolve({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}function createSSHConfigScanRoutes(hostServices){let{storage}=hostServices;return new Elysia6({prefix:"/api/ssh/config-scan"}).post("/local",async({body,set})=>{let{configPath="~/.ssh/config"}=body,resolved=configPath.replace(/^~/,process.env.HOME||"~");try{let file=Bun.file(resolved);if(!await file.exists())return set.status=404,{error:`SSH config not found at ${resolved}`};let content=await file.text(),hosts=parseSSHConfig(content);return{configPath:resolved,hosts,total:hosts.length}}catch(error){return set.status=500,{error:"Failed to read SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/remote",async({body,set})=>{let{connectionId,configPath="~/.ssh/config"}=body;try{let connConfig=await getConnectionById5(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh25.Client,connectConfig=await buildConnectConfig5(connConfig);return new Promise((resolve)=>{sshClient.on("ready",async()=>{try{let{stdout,code}=await sshExec3(sshClient,`cat ${configPath} 2>/dev/null`);if(code!==0||!stdout)return sshClient.end(),set.status=404,resolve({error:`SSH config not found at ${configPath} on remote server`});let hosts=parseSSHConfig(stdout);sshClient.end(),resolve({configPath,remoteHost:connConfig.host,hosts,total:hosts.length})}catch(err){sshClient.end(),set.status=500,resolve({error:"Failed to read remote SSH config",details:err instanceof Error?err.message:"Unknown error"})}}),sshClient.on("error",(err)=>{set.status=500,resolve({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to scan remote SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/batch-create",async({body,set})=>{let{hosts}=body;if(!hosts||!Array.isArray(hosts)||hosts.length===0)return set.status=400,{error:"No hosts provided"};try{let raw=await storage.get("ssh","connections"),connections=raw?JSON.parse(raw):[],created=[],skipped=[];for(let host of hosts){if(connections.find((c)=>c.serverName===host.name)){skipped.push(host.name);continue}let newConn={id:globalThis.crypto.randomUUID(),serverName:host.name,host:host.hostname,port:host.port,username:host.user,privateKeyPath:host.identityFile,createdAt:new Date().toISOString()};connections.push(newConn),created.push(newConn)}return await storage.set("ssh","connections",JSON.stringify(connections)),{created:created.length,skipped:skipped.length,skippedNames:skipped,connections:created.map((c)=>({id:c.id,serverName:c.serverName,host:c.host,port:c.port,username:c.username}))}}catch(error){return set.status=500,{error:"Failed to batch create connections",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh25;var init_ssh_config_scan=__esm(()=>{import_ssh25=__toESM(require_lib3(),1)});import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,telemetryEventName,skipPlatforms}=spec;return{onServerStart:async(_app,hostServices)=>{if(skipPlatforms&&skipPlatforms.includes(process.platform)){process.stderr.write(`[${name}] skipping init on unsupported platform '${process.platform}'
33
+ `,{workspaceId,input:{name:agentName,hostname,platform,architecture,apiUrl:`http://${connConfig.host}:${agentPort}`,tunnelUrl:tunnelUrl||void 0,agentApiKey:apiKey||void 0}});if(result.errors?.length)updateStep(8,"failed",result.errors[0].message);else{let agentRecord=result.data?.registerInstalledAgent;if(agentRecord){if(job.result.backendAgentId=agentRecord.id,apiKey&&hostServices.getConfig){let gwPayload=JSON.stringify({tenantApiUrl:hostServices.getConfig("gateway-auth:globalGatewayUrl")||"",workspacesApiUrl:hostServices.getConfig("gateway-auth:workspaceGatewayUrl")||"",appClientId:hostServices.getConfig("gateway-auth:clientId")||"",appClientSecret:hostServices.getConfig("gateway-auth:clientSecret")||"",workspaceId,agentRecordId:agentRecord.id});await sshExec2(sshClient,`echo '${gwPayload.replace(/'/g,"'\\''")}' > /tmp/.vc-gw-auth.json && curl -sf -X POST http://127.0.0.1:${agentPort}/api/agent/gateway-auth -H 'Content-Type: application/json' -H 'x-agent-api-key: ${apiKey}' -d @/tmp/.vc-gw-auth.json && rm -f /tmp/.vc-gw-auth.json`,15000).catch(()=>{})}updateStep(8,"completed",`Registered as "${agentRecord.name}" (${agentRecord.id.slice(0,8)})`)}}}catch(regErr){updateStep(8,"failed",regErr instanceof Error?regErr.message:"Registration failed")}}else updateStep(8,"skipped","Auto-registration not requested or gateway not configured");if(job.status="completed",job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:complete",{...job});sshClient.end()}catch(error){let errMsg=error instanceof Error?error.message:"Unknown error";if(job.status="failed",job.error=errMsg,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}}function createRemoteAgentInstallRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia5({prefix:"/api/ssh/agent-install"}).get("/jobs",()=>{return{jobs:Array.from(installJobs.values())}}).get("/jobs/:id",({params,set})=>{let job=installJobs.get(params.id);if(!job)return set.status=404,{error:"Install job not found"};return{job}}).post("/start",async({body,set})=>{let{connectionId,agentPort=3005,autoRegister=!1,agentName}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};return installJobs.set(jobId,job),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister,agentName}).catch(()=>{}),{jobId,status:"running"}}).post("/batch-start",async({body,set})=>{let{connectionIds,agentPort=3005,autoRegister=!1}=body;if(!connectionIds?.length)return set.status=400,{error:"No connection IDs provided"};let jobs=[];for(let connectionId of connectionIds){let connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)continue;let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};installJobs.set(jobId,job),jobs.push({connectionId,jobId}),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister}).catch(()=>{})}return{jobs,total:jobs.length}}).post("/uninstall",async({body,set})=>{let{connectionId}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh24.Client,connectConfig=await buildConnectConfig4(connConfig);await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})});try{await sshExec2(sshClient,'pkill -f "bun.*index.ts" 2>/dev/null; pkill -f "vibe" 2>/dev/null',1e4).catch(()=>{}),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols/agent",1e4),await sshExec2(sshClient,"rm -f $HOME/.local/bin/vibe",1e4),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols",1e4)}finally{sshClient.end()}return{success:!0,message:"Agent uninstalled"}})}var import_ssh24,REMOTE_PATH='export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$HOME/.local/bin:$PATH"',DEFAULT_REGISTRY="https://registry.npmjs.org",installJobs,INSTALL_STEPS;var init_remote_agent_install=__esm(()=>{init_expand_path();import_ssh24=__toESM(require_lib3(),1),installJobs=new Map;INSTALL_STEPS=[{name:"connect"},{name:"detect_os"},{name:"install_bun"},{name:"install_agent"},{name:"configure"},{name:"start"},{name:"verify"},{name:"retrieve"},{name:"register"}]});var exports_ssh_config_scan={};__export(exports_ssh_config_scan,{createSSHConfigScanRoutes:()=>createSSHConfigScanRoutes});import{Elysia as Elysia6}from"elysia";function parseSSHConfig(content){let hosts=[],current=null;for(let rawLine of content.split(`
34
+ `)){let line=rawLine.trim();if(!line||line.startsWith("#"))continue;let match=line.match(/^(\S+)\s+(.+)$/);if(!match)continue;let[,key,value]=match,keyLower=key.toLowerCase();if(keyLower==="host"){if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});current={name:value,extra:{}}}else if(current)switch(keyLower){case"hostname":current.hostname=value;break;case"port":current.port=parseInt(value,10);break;case"user":current.user=value;break;case"identityfile":current.identityFile=value.replace(/^~/,process.env.HOME||"~");break;case"proxyjump":current.proxyJump=value;break;default:if(!current.extra)current.extra={};current.extra[key]=value}}if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});return hosts}async function getConnectionById5(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig5(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 sshExec3(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})})})})}function createSSHConfigScanRoutes(hostServices){let{storage}=hostServices;return new Elysia6({prefix:"/api/ssh/config-scan"}).post("/local",async({body,set})=>{let{configPath="~/.ssh/config"}=body,resolved=configPath.replace(/^~/,process.env.HOME||"~");try{let file=Bun.file(resolved);if(!await file.exists())return set.status=404,{error:`SSH config not found at ${resolved}`};let content=await file.text(),hosts=parseSSHConfig(content);return{configPath:resolved,hosts,total:hosts.length}}catch(error){return set.status=500,{error:"Failed to read SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/remote",async({body,set})=>{let{connectionId,configPath="~/.ssh/config"}=body;try{let connConfig=await getConnectionById5(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh25.Client,connectConfig=await buildConnectConfig5(connConfig);return new Promise((resolve2)=>{sshClient.on("ready",async()=>{try{let{stdout,code}=await sshExec3(sshClient,`cat ${configPath} 2>/dev/null`);if(code!==0||!stdout)return sshClient.end(),set.status=404,resolve2({error:`SSH config not found at ${configPath} on remote server`});let hosts=parseSSHConfig(stdout);sshClient.end(),resolve2({configPath,remoteHost:connConfig.host,hosts,total:hosts.length})}catch(err){sshClient.end(),set.status=500,resolve2({error:"Failed to read remote SSH config",details:err instanceof Error?err.message:"Unknown error"})}}),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 scan remote SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/batch-create",async({body,set})=>{let{hosts}=body;if(!hosts||!Array.isArray(hosts)||hosts.length===0)return set.status=400,{error:"No hosts provided"};try{let raw=await storage.get("ssh","connections"),connections=raw?JSON.parse(raw):[],created=[],skipped=[];for(let host of hosts){if(connections.find((c)=>c.serverName===host.name)){skipped.push(host.name);continue}let newConn={id:globalThis.crypto.randomUUID(),serverName:host.name,host:host.hostname,port:host.port,username:host.user,privateKeyPath:host.identityFile,createdAt:new Date().toISOString()};connections.push(newConn),created.push(newConn)}return await storage.set("ssh","connections",JSON.stringify(connections)),{created:created.length,skipped:skipped.length,skippedNames:skipped,connections:created.map((c)=>({id:c.id,serverName:c.serverName,host:c.host,port:c.port,username:c.username}))}}catch(error){return set.status=500,{error:"Failed to batch create connections",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh25;var init_ssh_config_scan=__esm(()=>{init_expand_path();import_ssh25=__toESM(require_lib3(),1)});import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,telemetryEventName,skipPlatforms}=spec;return{onServerStart:async(_app,hostServices)=>{if(skipPlatforms&&skipPlatforms.includes(process.platform)){process.stderr.write(`[${name}] skipping init on unsupported platform '${process.platform}'
35
35
  `);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)}}}function pickOutputMode(flags){if(flags.json)return"json";if(flags.plain)return"plain";if(flags.interactive)return"interactive";return"auto"}function isCi(){return!!process.env.CI||!!process.env.NO_COLOR||process.env.TERM==="dumb"}function stdoutIsTty(){return Boolean(process.stdout.isTTY)}async function runMultimode(opts){let data=await opts.fetchData(),mode=opts.mode??"auto";if(mode==="json"){let shaped=opts.json?opts.json(data):data;process.stdout.write(`${JSON.stringify(shaped,null,2)}
36
36
  `);return}if(mode==="plain"){await opts.plain(data);return}if((mode==="interactive"||stdoutIsTty()&&!isCi())&&!!opts.interactive&&opts.interactive)try{await opts.interactive(data);return}catch{}await opts.plain(data)}function maybePrintJson(flags,data){if(!flags.json)return!1;return process.stdout.write(`${JSON.stringify(data,null,2)}
37
37
  `),!0}var SENSITIVE_KEY_RE=/(token|secret|password|apikey|api_key|key|auth|credential|email)/i;function redact(value){return redactInner(value)}function redactInner(value){if(value===null||value===void 0)return value;if(Array.isArray(value))return value.map(redactInner);if(typeof value==="object"){let out={};for(let[k,v]of Object.entries(value)){if(SENSITIVE_KEY_RE.test(k)){out[k]="[redacted]";continue}out[k]=redactInner(v)}return out}return value}var TelemetryEmitter=class{constructor(pluginName,pluginVersion,hostServices){this.pluginName=pluginName,this.pluginVersion=pluginVersion,this.hostServices=hostServices}pluginName;pluginVersion;hostServices;emit(eventName,payload){let target=this.hostServices?.telemetry;if(!target)return;target.emit(eventName,{plugin:this.pluginName,version:this.pluginVersion,timestamp:new Date().toISOString(),...payload??{}})}emitReady(context){this.emit(`${this.pluginName}.ready`,context)}emitError(error,context){this.emit(`${this.pluginName}.error`,{message:error.message,...context??{}})}emitEvent(type,payload){this.emit(type,payload)}};async function loadCore(){return await import("@opentui/core")}async function interactiveTable(opts){if(opts.rows.length===0)return null;let core=await loadCore(),{createCliRenderer,BoxRenderable,TextRenderable,SelectRenderable,SelectRenderableEvents}=core,renderer=await createCliRenderer({exitOnCtrlC:!0,targetFps:30}),ctx=renderer.root.ctx,root=new BoxRenderable(ctx,{width:"100%",height:"100%",flexDirection:"column",backgroundColor:"#0b0d12"}),title=new TextRenderable(ctx,{content:` ${opts.title}`,fg:"#8be9fd",height:1}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"\u2191/\u2193 navigate \xB7 Enter select \xB7 q quit"}`,fg:"#6c7086",height:1}),body=new BoxRenderable(ctx,{width:"100%",flexGrow:1,flexDirection:"row"}),list=new SelectRenderable(ctx,{width:opts.listWidth??28,height:"100%",options:opts.rows.map((r)=>({name:r.label,description:r.hint??"",value:r.id})),selectedIndex:0,showDescription:!0,backgroundColor:"#0b0d12",textColor:"#cdd6f4",focusedBackgroundColor:"#0b0d12",focusedTextColor:"#cdd6f4",selectedBackgroundColor:"#1f2335",selectedTextColor:"#a6e3a1",descriptionColor:"#6c7086",selectedDescriptionColor:"#9399b2",showScrollIndicator:!0,wrapSelection:!0}),detailPane=new BoxRenderable(ctx,{flexGrow:1,height:"100%",flexDirection:"column",paddingLeft:2,paddingRight:2,backgroundColor:"#11141c"}),detailText=new TextRenderable(ctx,{content:"",fg:"#cdd6f4"});detailPane.add(detailText),body.add(list),body.add(detailPane),root.add(title),root.add(body),root.add(footer),renderer.root.add(root),list.focus();let renderDetail=(idx)=>{let row=opts.rows[idx];if(!row)return;detailText.content=`
38
38
  ${row.detail}
39
39
  `};return renderDetail(0),list.on(SelectRenderableEvents.SELECTION_CHANGED,(idx)=>{renderDetail(idx)}),await new Promise((resolve)=>{let cleanup=(chosen)=>{try{renderer.destroy()}catch{}resolve(chosen)};list.on(SelectRenderableEvents.ITEM_SELECTED,()=>{let row=opts.rows[list.getSelectedIndex()];cleanup(row??null)}),renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q")cleanup(null)})}).then(async(chosen)=>{if(chosen&&opts.onSelect)await opts.onSelect(chosen);return chosen})}async function interactiveDetail(opts){let core=await loadCore(),{createCliRenderer,BoxRenderable,TextRenderable}=core,renderer=await createCliRenderer({exitOnCtrlC:!0,targetFps:30}),ctx=renderer.root.ctx,root=new BoxRenderable(ctx,{width:"100%",height:"100%",flexDirection:"column",backgroundColor:"#0b0d12"}),title=new TextRenderable(ctx,{content:` ${opts.title}`,fg:"#8be9fd",height:1}),bodyBox=new BoxRenderable(ctx,{width:"100%",flexGrow:1,paddingLeft:2,paddingRight:2,backgroundColor:"#11141c"}),bodyText=new TextRenderable(ctx,{content:`
40
40
  ${opts.body}
41
- `,fg:"#cdd6f4"}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"q to quit"}`,fg:"#6c7086",height:1});bodyBox.add(bodyText),root.add(title),root.add(bodyBox),root.add(footer),renderer.root.add(root),await new Promise((resolve)=>{let cleanup=()=>{try{renderer.destroy()}catch{}resolve()};renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q"||key.name==="return")cleanup()})})}var AGENT_BASE_URL=process.env.VIBE_AGENT_URL??"http://localhost:3005",API_KEY=process.env.VIBE_AGENT_API_KEY??"";async function apiFetch(urlPath,options){return fetch(`${AGENT_BASE_URL}${urlPath}`,{...options,headers:{"Content-Type":"application/json","x-agent-api-key":API_KEY,...options?.headers}})}var cleanupPortForwards,cleanupTerminals,PLUGIN_NAME="ssh",PLUGIN_VERSION="2026.509.1",createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tool.ready",onInit:(hostServices)=>{new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,hostServices).emitEvent("tool.ready",{provider:"ssh"})}});return{capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"SSH connection management, remote terminals, port forwarding, and remote agent installation",tags:["backend","cli","integration","provider"],cliCommand:"ssh",apiPrefix:"/api/ssh",async onServerStart(app,hostServices){if(await lifecycle.onServerStart(app,hostServices),process.platform==="win32"){process.stderr.write(" Plugin 'ssh' is not supported on Windows yet \u2014 skipping route + provider registration. "+`See https://github.com/algoshred/vibe-plugin-tool-ssh for status.
41
+ `,fg:"#cdd6f4"}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"q to quit"}`,fg:"#6c7086",height:1});bodyBox.add(bodyText),root.add(title),root.add(bodyBox),root.add(footer),renderer.root.add(root),await new Promise((resolve)=>{let cleanup=()=>{try{renderer.destroy()}catch{}resolve()};renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q"||key.name==="return")cleanup()})})}var AGENT_BASE_URL=process.env.VIBE_AGENT_URL??"http://localhost:3005",API_KEY=process.env.VIBE_AGENT_API_KEY??"";async function apiFetch(urlPath,options){return fetch(`${AGENT_BASE_URL}${urlPath}`,{...options,headers:{"Content-Type":"application/json","x-agent-api-key":API_KEY,...options?.headers}})}var cleanupPortForwards,cleanupTerminals,PLUGIN_NAME="ssh",PLUGIN_VERSION="2026.509.1",createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tool.ready",onInit:(hostServices)=>{new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,hostServices).emitEvent("tool.ready",{provider:"ssh"})}});return{capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0,broadcast:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"SSH connection management, remote terminals, port forwarding, and remote agent installation",tags:["backend","cli","integration","provider"],cliCommand:"ssh",apiPrefix:"/api/ssh",async onServerStart(app,hostServices){if(await lifecycle.onServerStart(app,hostServices),process.platform==="win32"){process.stderr.write(" Plugin 'ssh' is not supported on Windows yet \u2014 skipping route + provider registration. "+`See https://github.com/algoshred/vibe-plugin-tool-ssh for status.
42
42
  `);return}let elysiaApp=app,agentHost=hostServices,{createSSHRoutes:createSSHRoutes2}=await Promise.resolve().then(() => (init_ssh(),exports_ssh)),{createPortForwardRoutes:createPortForwardRoutes2,cleanupAllTunnels:cleanupAllTunnels2}=await Promise.resolve().then(() => (init_port_forward(),exports_port_forward)),{createRemoteTerminalRoutes:createRemoteTerminalRoutes2,cleanupAllTerminals:cleanupAllTerminals2,getTerminalInfo:getTerminalInfo2,listTerminalSessions:listTerminalSessions2}=await Promise.resolve().then(() => (init_remote_terminal(),exports_remote_terminal)),{createRemoteAgentInstallRoutes:createRemoteAgentInstallRoutes2}=await Promise.resolve().then(() => (init_remote_agent_install(),exports_remote_agent_install)),{createSSHConfigScanRoutes:createSSHConfigScanRoutes2}=await Promise.resolve().then(() => (init_ssh_config_scan(),exports_ssh_config_scan));if(elysiaApp.use(createSSHRoutes2(agentHost)),elysiaApp.use(createPortForwardRoutes2(agentHost)),elysiaApp.use(createRemoteTerminalRoutes2(agentHost)),elysiaApp.use(createRemoteAgentInstallRoutes2(agentHost)),elysiaApp.use(createSSHConfigScanRoutes2(agentHost)),cleanupPortForwards=cleanupAllTunnels2,cleanupTerminals=cleanupAllTerminals2,agentHost.serviceRegistry){let sshSessionProvider={name:"ssh",getTerminalInfo:(sessionId)=>getTerminalInfo2(sessionId),list:async()=>{return listTerminalSessions2().map((s)=>({id:s.id,name:`ssh-terminal-${s.id.slice(0,8)}`,status:s.status==="active"?"active":"inactive",provider:"ssh",createdAt:s.startedAt}))},get:async(sessionId)=>{let s=listTerminalSessions2().find((x)=>x.id===sessionId);if(!s)return null;return{id:s.id,name:`ssh-terminal-${s.id.slice(0,8)}`,status:s.status==="active"?"active":"inactive",provider:"ssh",createdAt:s.startedAt}}};agentHost.serviceRegistry.registerProvider("session",sshSessionProvider,"ssh")}process.stdout.write(` Plugin 'ssh' registered routes: /api/ssh, /api/port-forward, /api/ssh/terminal, /api/ssh/agent-install, /api/ssh/config-scan
43
43
  `)},async onServerStop(){if(cleanupTerminals)cleanupTerminals(),cleanupTerminals=void 0;if(cleanupPortForwards)cleanupPortForwards(),cleanupPortForwards=void 0;process.stdout.write(` Plugin 'ssh' cleaned up active connections and terminals
44
44
  `)},onCliSetup(programArg){let ssh=programArg.command("ssh").description("SSH connection and remote terminal management");ssh.hook("preAction",()=>{if(process.platform==="win32")process.stderr.write(`SSH plugin is not supported on Windows yet \u2014 see issue tracker.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Expand a leading `~` to the agent process's home directory and resolve
3
+ * to an absolute path. Tilde expansion is normally done by the shell, so
4
+ * paths that arrive via JSON (e.g. UI-supplied `privateKeyPath`) keep the
5
+ * literal `~` and fail with `ENOENT` on `Bun.file()` / `fs.open()`.
6
+ */
7
+ export declare function expandPath(p: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-tool-ssh",
3
- "version": "2026.525.1",
3
+ "version": "2026.528.1",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "engines": {