@vibecontrols/vibe-plugin-tunnel 2026.429.2 → 2026.507.2
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 +9 -1
- package/dist/utils/interactive.d.ts +57 -0
- package/dist/utils/multimode.d.ts +66 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __require=import.meta.require;function agentBaseUrl(){return process.env.AGENT_BASE_URL??"http://localhost:3005"}function authHeaders(){let fromEnv=process.env.AGENT_API_KEY??process.env.X_AGENT_API_KEY;if(fromEnv)return{"x-agent-api-key":fromEnv};try{let{readFileSync,existsSync}=__require("fs"),{join,resolve}=__require("path"),dir=process.env.VIBECONTROLS_HOME??join(process.cwd(),".boff","vibecontrols"),configPath=join(resolve(dir),"agents",process.env.VIBECONTROLS_AGENT_ID??"default","config.json");if(existsSync(configPath)){let cfg=JSON.parse(readFileSync(configPath,"utf-8"));if(cfg["static-api-key"])return{"x-agent-api-key":cfg["static-api-key"]}}}catch{}return{}}async function apiGet(path){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{headers:authHeaders()});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`GET ${path} failed (${res.status}): ${text}`)}return await res.json()}async function apiPost(path,body){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{method:"POST",headers:{"Content-Type":"application/json",...authHeaders()},body:body?JSON.stringify(body):void 0});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`POST ${path} failed (${res.status}): ${text}`)}return await res.json()}async function apiDelete(path){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{method:"DELETE",headers:authHeaders()});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`DELETE ${path} failed (${res.status}): ${text}`)}return await res.json()}function registerTunnelCommands(program,_hostServices){let cmd=program.command("tunnel").description("Manage tunnels across providers");cmd.command("list").description("List tunnels (optionally filtered by provider)").option("--provider <name>","Limit to a specific provider").action(async(opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiGet(`/${qs}`);if(result.tunnels.length===0){console.log("No tunnels.");return}for(let t of result.tunnels)console.log(`${t.id} ${t.providerName} ${t.status} ${t.url} ${t.protocol}:${t.localPort}`)}),cmd.command("get <tunnelId>").description("Get tunnel status").option("--provider <name>").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiGet(`/${tunnelId}${qs}`);console.log(JSON.stringify(result,null,2))}),cmd.command("start <tunnelId>").description("Start a prepared tunnel session").option("--provider <name>").action(async(tunnelId,opts)=>{let result=await apiPost(`/${tunnelId}/start`,{provider:opts.provider});console.log(JSON.stringify(result,null,2))}),cmd.command("stop <tunnelId>").description("Stop an active tunnel").option("--provider <name>").action(async(tunnelId,opts)=>{let result=await apiPost(`/${tunnelId}/stop`,{provider:opts.provider});console.log(JSON.stringify(result,null,2))}),cmd.command("delete <tunnelId>").description("Delete a tunnel").option("--provider <name>").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiDelete(`/${tunnelId}${qs}`);console.log(JSON.stringify(result,null,2))}),cmd.command("sessions <tunnelId>").description("List sessions for a tunnel").option("--provider <name>").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiGet(`/${tunnelId}/sessions${qs}`);console.log(JSON.stringify(result,null,2))}),cmd.command("status").description("Manager + per-provider health summary").action(async()=>{let result=await apiGet("/health");console.log(JSON.stringify(result,null,2))});let providersCmd=cmd.command("providers").description("Manage tunnel providers");providersCmd.command("list").description("List registered tunnel providers and capabilities").action(async()=>{let result=await apiGet("/providers");for(let p of result.providers){let star=p.isDefault?"* ":" ",ok=p.health.ok?"ok":`error (${p.health.message??""})`;console.log(`${star}${p.name} [${ok}]`)}}),providersCmd.command("set-default <name>").description("Set the default tunnel provider").action(async(name)=>{let result=await apiPost("/default",{provider:name});console.log(JSON.stringify(result,null,2))});let domainsCmd=cmd.command("domains").description("Manage custom domains on a tunnel");domainsCmd.command("add <tunnelId> <domain>").option("--provider <name>").action(async(tunnelId,domain,opts)=>{let result=await apiPost(`/${tunnelId}/domains`,{domain,provider:opts.provider});console.log(JSON.stringify(result,null,2))}),domainsCmd.command("rm <tunnelId> <domain>").option("--provider <name>").action(async(tunnelId,domain,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiDelete(`/${tunnelId}/domains/${encodeURIComponent(domain)}${qs}`);console.log(JSON.stringify(result,null,2))}),cmd.command("doctor").description("Diagnose tunnel providers").action(async()=>{try{let health=await apiGet("/health");console.log(`manager: ${health.manager}`);for(let p of health.providers)console.log(` ${p.name}: ${p.ok?"ok":`fail - ${p.message}`}`)}catch(err){console.error(`Failed to reach agent: ${err}`),process.exitCode=1}})}var DEFAULT_PROVIDER_CONFIG_KEY="provider:default:tunnel";var LOG_SOURCE="tunnel-manager";class TunnelManager{registry;host;log={info:()=>{},warn:()=>{},error:()=>{},debug:()=>{}};init(host){if(this.host=host,this.registry=host.serviceRegistry,host.logger)this.log=host.logger;this.log.info(LOG_SOURCE,"Tunnel manager initialized")}async resolveProvider(name){if(!this.registry)throw Error("Service registry not available");let explicit=name?.trim();if(explicit){let provider2=this.registry.getProviderByName("tunnel",explicit);if(!provider2)throw Error(`Tunnel provider '${explicit}' is not registered`);return provider2}let fallback=await this.defaultProviderName();if(!fallback)throw Error("No default tunnel provider configured and none specified");let provider=this.registry.getProviderByName("tunnel",fallback);if(!provider)throw Error(`Default tunnel provider '${fallback}' is not registered`);return provider}async defaultProviderName(){let fromConfig=await this.host?.getConfig?.(DEFAULT_PROVIDER_CONFIG_KEY);if(fromConfig)return fromConfig;let entries=this.registry?.listProvidersForType("tunnel")??[],flagged=entries.find((e)=>e.isDefault);if(flagged)return flagged.pluginName;if(entries.length===1)return entries[0].pluginName;return}listProviderEntries(){return this.registry?.listProvidersForType("tunnel")??[]}async listAllTunnels(filterProvider){let entries=this.listProviderEntries(),targeted=filterProvider?entries.filter((e)=>e.pluginName===filterProvider):entries,results=[];for(let entry of targeted){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;try{let tunnels=await provider.list();results.push({provider:entry.pluginName,tunnels})}catch(err){this.log.warn(LOG_SOURCE,`listAllTunnels: ${entry.pluginName} failed`,{error:String(err)})}}return results}async listProviders(){let entries=this.listProviderEntries(),snapshots=[];for(let entry of entries){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;let health;try{health=await provider.healthCheck()}catch(err){health={ok:!1,message:String(err)}}snapshots.push({name:entry.pluginName,isDefault:entry.isDefault,capabilities:provider.getCapabilities(),health})}return snapshots}async getTunnel(id,opts={}){if(opts.provider){let provider=await this.resolveProvider(opts.provider),tunnel=await provider.getStatus(id);return tunnel?{provider:provider.name,tunnel}:null}for(let entry of this.listProviderEntries()){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;try{let tunnel=await provider.getStatus(id);if(tunnel)return{provider:entry.pluginName,tunnel}}catch{}}return null}async issueSession(req){let{provider:providerName,...rest}=req,provider=await this.resolveProvider(providerName);return{...await provider.issueSession(rest),provider:provider.name}}async startTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).start(id)}async stopTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).stop(id)}async deleteTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).delete(id)}async rotate(id,opts={}){return(await this.resolveProvider(opts.provider)).rotate(id)}async attachDomain(id,domain,opts={}){return(await this.resolveProvider(opts.provider)).attachCustomDomain(id,domain)}async detachDomain(id,domain,opts={}){return(await this.resolveProvider(opts.provider)).detachCustomDomain(id,domain)}async listSessions(id,opts={}){return(await this.resolveProvider(opts.provider)).listSessions(id)}async getHealth(){let entries=this.listProviderEntries(),providers=[];for(let entry of entries){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider){providers.push({name:entry.pluginName,ok:!1,message:"unavailable"});continue}try{let result=await provider.healthCheck();providers.push({name:entry.pluginName,ok:result.ok,message:result.message})}catch(err){providers.push({name:entry.pluginName,ok:!1,message:String(err)})}}return{manager:"ok",providers}}async setDefault(providerName){if(await this.resolveProvider(providerName),this.registry?.setProviderDefault)this.registry.setProviderDefault("tunnel",providerName);if(this.host?.setConfig)await this.host.setConfig(DEFAULT_PROVIDER_CONFIG_KEY,providerName);this.log.info(LOG_SOURCE,`Default tunnel provider set to ${providerName}`)}}import{Elysia,t}from"elysia";function readProvider(query){return query?.provider??void 0}function createTunnelManagerRoutes(manager){return new Elysia().get("/providers",async({set})=>{try{return{providers:await manager.listProviders()}}catch(err){return set.status=500,{error:"Failed to list providers",details:String(err)}}}).post("/default",async({body,set})=>{try{return await manager.setDefault(body.provider),{success:!0,provider:body.provider}}catch(err){return set.status=400,{error:String(err)}}},{body:t.Object({provider:t.String()})}).get("/health",async({set})=>{try{return await manager.getHealth()}catch(err){return set.status=500,{error:"Failed to get health",details:String(err)}}}).get("/",async({query,set})=>{try{let providerName=readProvider(query),results=await manager.listAllTunnels(providerName);return{tunnels:results.flatMap((r)=>r.tunnels.map((t2)=>({...t2,providerName:r.provider}))),byProvider:results}}catch(err){return set.status=500,{error:"Failed to list tunnels",details:String(err)}}}).post("/issue-session",async({body,set})=>{try{return await manager.issueSession({provider:body.provider,protocol:body.protocol,localPort:body.localPort,localHost:body.localHost,subdomain:body.subdomain,customDomain:body.customDomain,ttlSeconds:body.ttlSeconds,metadata:body.metadata,controlPlanePayload:body.controlPlanePayload})}catch(err){return set.status=500,{error:"Failed to issue session",details:String(err)}}},{body:t.Object({provider:t.Optional(t.String()),protocol:t.Union([t.Literal("http"),t.Literal("https"),t.Literal("tcp"),t.Literal("udp")]),localPort:t.Number(),localHost:t.Optional(t.String()),subdomain:t.Optional(t.String()),customDomain:t.Optional(t.String()),ttlSeconds:t.Optional(t.Number()),metadata:t.Optional(t.Any()),controlPlanePayload:t.Optional(t.Any())})}).get("/:id",async({params,query,set})=>{try{let result=await manager.getTunnel(params.id,{provider:readProvider(query)});if(!result)return set.status=404,{error:"Tunnel not found"};return result}catch(err){return set.status=500,{error:"Failed to get tunnel",details:String(err)}}}).get("/:id/sessions",async({params,query,set})=>{try{return{sessions:await manager.listSessions(params.id,{provider:readProvider(query)})}}catch(err){return set.status=500,{error:"Failed to list sessions",details:String(err)}}}).post("/:id/start",async({params,body,set})=>{try{return{success:!0,tunnel:await manager.startTunnel(params.id,{provider:body?.provider})}}catch(err){return set.status=500,{error:"Failed to start tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).post("/:id/stop",async({params,body,set})=>{try{return await manager.stopTunnel(params.id,{provider:body?.provider}),{success:!0}}catch(err){return set.status=500,{error:"Failed to stop tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).post("/:id/rotate",async({params,body,set})=>{try{return{success:!0,session:await manager.rotate(params.id,{provider:body?.provider})}}catch(err){return set.status=500,{error:"Failed to rotate tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).delete("/:id",async({params,query,set})=>{try{return await manager.deleteTunnel(params.id,{provider:readProvider(query)}),{success:!0}}catch(err){return set.status=500,{error:"Failed to delete tunnel",details:String(err)}}}).post("/:id/domains",async({params,body,set})=>{try{return await manager.attachDomain(params.id,body.domain,{provider:body.provider}),{success:!0}}catch(err){return set.status=500,{error:"Failed to attach domain",details:String(err)}}},{body:t.Object({domain:t.String(),provider:t.Optional(t.String())})}).delete("/:id/domains/:domain",async({params,query,set})=>{try{return await manager.detachDomain(params.id,params.domain,{provider:readProvider(query)}),{success:!0}}catch(err){return set.status=500,{error:"Failed to detach domain",details:String(err)}}})}var manager=new TunnelManager,vibePlugin={name:"tunnel",version:"0.1.0",description:"VibeTunnels manager \u2014 dispatches to registered tunnel providers",tags:["backend","cli","integration"],cliCommand:"tunnel",apiPrefix:"/api/tunnels",createRoutes:()=>createTunnelManagerRoutes(manager),onServerStart:(_app,hostServices)=>{manager.init(hostServices)},onCliSetup:(program,hostServices)=>{registerTunnelCommands(program,hostServices)}},src_default=vibePlugin;export{vibePlugin,src_default as default,TunnelManager};
|
|
2
|
+
var __require=import.meta.require;function isInteractive(){return!!process.stdout.isTTY&&!!process.stdin.isTTY}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"}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)}
|
|
3
|
+
`);return}if(mode==="plain"){await opts.plain(data);return}if((mode==="interactive"||isInteractive()&&!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)}
|
|
4
|
+
`),!0}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=`
|
|
5
|
+
${row.detail}
|
|
6
|
+
`};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:`
|
|
7
|
+
${opts.body}
|
|
8
|
+
`,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 DEFAULT_AGENT_URL="http://localhost:3005";function agentBaseUrl(){return process.env.AGENT_BASE_URL??DEFAULT_AGENT_URL}function redactSecrets(value){if(Array.isArray(value))return value.map(redactSecrets);if(value&&typeof value==="object"){let out={};for(let[k,v]of Object.entries(value))if(/(token|secret|password|apikey|api_key)/i.test(k))out[k]="[REDACTED]";else out[k]=redactSecrets(v);return out}return value}function authHeaders(){let fromEnv=process.env.AGENT_API_KEY??process.env.X_AGENT_API_KEY;if(fromEnv)return{"x-agent-api-key":fromEnv};try{let{readFileSync,existsSync}=__require("fs"),{join,resolve}=__require("path"),dir=process.env.VIBECONTROLS_HOME??join(process.cwd(),".boff","vibecontrols"),configPath=join(resolve(dir),"agents",process.env.VIBECONTROLS_PROFILE??"default","config.json");if(existsSync(configPath)){let cfg=JSON.parse(readFileSync(configPath,"utf-8"));if(cfg["static-api-key"])return{"x-agent-api-key":cfg["static-api-key"]}}}catch{}return{}}async function apiGet(path){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{headers:authHeaders()});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`GET ${path} failed (${res.status}): ${text}`)}return await res.json()}async function apiPost(path,body){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{method:"POST",headers:{"Content-Type":"application/json",...authHeaders()},body:body?JSON.stringify(body):void 0});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`POST ${path} failed (${res.status}): ${text}`)}return await res.json()}async function apiDelete(path){let res=await fetch(`${agentBaseUrl()}/api/tunnels${path}`,{method:"DELETE",headers:authHeaders()});if(!res.ok){let text=await res.text().catch(()=>"");throw Error(`DELETE ${path} failed (${res.status}): ${text}`)}return await res.json()}function tunnelDetail(t){let lines=[`id: ${t.id}`,`provider: ${t.providerName??"-"}`,`status: ${t.status??"-"}`,`url: ${t.url??"-"}`,`protocol: ${t.protocol??"-"}`,`localPort: ${t.localPort??"-"}`];if(t.domains&&Array.isArray(t.domains)&&t.domains.length>0)lines.push(`domains: ${t.domains.join(", ")}`);return lines.join(`
|
|
9
|
+
`)}function registerTunnelCommands(program,_hostServices){let cmd=program.command("tunnel").description("Manage tunnels across providers");cmd.command("list").description("List tunnels (optionally filtered by provider)").option("--provider <name>","Limit to a specific provider").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"";await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet(`/${qs}`),plain:(result)=>{if(result.tunnels.length===0){console.log("No tunnels.");return}for(let t of result.tunnels)console.log(`${t.id} ${t.providerName} ${t.status} ${t.url} ${t.protocol}:${t.localPort}`)},interactive:async(result)=>{if(result.tunnels.length===0){console.log("No tunnels.");return}let rows=result.tunnels.map((t)=>({id:String(t.id),label:`${t.id} ${t.providerName??""}`.trim(),hint:t.status??"",detail:tunnelDetail(t)}));await interactiveTable({title:`tunnels \u2014 ${result.tunnels.length}`,rows})},json:(result)=>redactSecrets(result)})}),cmd.command("get <tunnelId>").description("Get tunnel status").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"";await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet(`/${tunnelId}${qs}`),plain:(result)=>{console.log(JSON.stringify(result,null,2))},interactive:async(result)=>{await interactiveDetail({title:`tunnel ${tunnelId}`,body:tunnelDetail(result)})},json:(result)=>redactSecrets(result)})}),cmd.command("start <tunnelId>").description("Start a prepared tunnel session").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,opts)=>{let result=await apiPost(`/${tunnelId}/start`,{provider:opts.provider});if(maybePrintJson(opts,{ok:!0,action:"start",tunnelId,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))}),cmd.command("stop <tunnelId>").description("Stop an active tunnel").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,opts)=>{let result=await apiPost(`/${tunnelId}/stop`,{provider:opts.provider});if(maybePrintJson(opts,{ok:!0,action:"stop",tunnelId,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))}),cmd.command("delete <tunnelId>").description("Delete a tunnel").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiDelete(`/${tunnelId}${qs}`);if(maybePrintJson(opts,{ok:!0,action:"delete",tunnelId,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))}),cmd.command("sessions <tunnelId>").description("List sessions for a tunnel").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"";await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet(`/${tunnelId}/sessions${qs}`),plain:(result)=>{console.log(JSON.stringify(result,null,2))},interactive:async(result)=>{let sessions=Array.isArray(result?.sessions)?result.sessions:Array.isArray(result)?result:[];if(sessions.length===0){await interactiveDetail({title:`sessions \u2014 ${tunnelId}`,body:"No sessions."});return}let rows=sessions.map((s,i)=>({id:String(s.id??i),label:String(s.id??`session-${i}`),hint:s.status??"",detail:JSON.stringify(s,null,2)}));await interactiveTable({title:`sessions \u2014 ${tunnelId} (${sessions.length})`,rows})},json:(result)=>redactSecrets(result)})}),cmd.command("status").description("Manager + per-provider health summary").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet("/health"),plain:(result)=>{console.log(JSON.stringify(result,null,2))},interactive:async(result)=>{await interactiveDetail({title:"tunnel status",body:JSON.stringify(result,null,2)})},json:(result)=>redactSecrets(result)})});let providersCmd=cmd.command("providers").description("Manage tunnel providers");providersCmd.command("list").description("List registered tunnel providers and capabilities").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet("/providers"),plain:(result)=>{for(let p of result.providers){let star=p.isDefault?"* ":" ",ok=p.health.ok?"ok":`error (${p.health.message??""})`;console.log(`${star}${p.name} [${ok}]`)}},interactive:async(result)=>{if(result.providers.length===0){await interactiveDetail({title:"providers",body:"No providers."});return}let rows=result.providers.map((p)=>({id:String(p.name),label:`${p.isDefault?"* ":" "}${p.name}`,hint:p.health?.ok?"ok":"error",detail:JSON.stringify(p,null,2)}));await interactiveTable({title:`providers \u2014 ${result.providers.length}`,rows})},json:(result)=>redactSecrets(result)})}),providersCmd.command("set-default <name>").description("Set the default tunnel provider").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(name,opts)=>{let result=await apiPost("/default",{provider:name});if(maybePrintJson(opts,{ok:!0,action:"set-default",provider:name,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))});let domainsCmd=cmd.command("domains").description("Manage custom domains on a tunnel");domainsCmd.command("add <tunnelId> <domain>").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,domain,opts)=>{let result=await apiPost(`/${tunnelId}/domains`,{domain,provider:opts.provider});if(maybePrintJson(opts,{ok:!0,action:"domains-add",tunnelId,domain,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))}),domainsCmd.command("rm <tunnelId> <domain>").option("--provider <name>").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(tunnelId,domain,opts)=>{let qs=opts.provider?`?provider=${encodeURIComponent(opts.provider)}`:"",result=await apiDelete(`/${tunnelId}/domains/${encodeURIComponent(domain)}${qs}`);if(maybePrintJson(opts,{ok:!0,action:"domains-rm",tunnelId,domain,result:redactSecrets(result)}))return;console.log(JSON.stringify(result,null,2))}),cmd.command("doctor").description("Diagnose tunnel providers").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{try{await runMultimode({mode:pickOutputMode(opts),fetchData:()=>apiGet("/health"),plain:(health)=>{console.log(`manager: ${health.manager}`);for(let p of health.providers)console.log(` ${p.name}: ${p.ok?"ok":`fail - ${p.message}`}`)},interactive:async(health)=>{let lines=[`manager: ${health.manager}`];for(let p of health.providers)lines.push(` ${p.name}: ${p.ok?"ok":`fail - ${p.message}`}`);await interactiveDetail({title:"tunnel doctor",body:lines.join(`
|
|
10
|
+
`)})},json:(health)=>redactSecrets(health)})}catch(err){console.error(`Failed to reach agent: ${err}`),process.exitCode=1}})}var DEFAULT_PROVIDER_CONFIG_KEY="provider:default:tunnel";var LOG_SOURCE="tunnel-manager";class TunnelManager{registry;host;log={info:()=>{},warn:()=>{},error:()=>{},debug:()=>{}};init(host){if(this.host=host,this.registry=host.serviceRegistry,host.logger)this.log=host.logger;this.log.info(LOG_SOURCE,"Tunnel manager initialized")}async resolveProvider(name){if(!this.registry)throw Error("Service registry not available");let explicit=name?.trim();if(explicit){let provider2=this.registry.getProviderByName("tunnel",explicit);if(!provider2)throw Error(`Tunnel provider '${explicit}' is not registered`);return provider2}let fallback=await this.defaultProviderName();if(!fallback)throw Error("No default tunnel provider configured and none specified");let provider=this.registry.getProviderByName("tunnel",fallback);if(!provider)throw Error(`Default tunnel provider '${fallback}' is not registered`);return provider}async defaultProviderName(){let fromConfig=await this.host?.getConfig?.(DEFAULT_PROVIDER_CONFIG_KEY);if(fromConfig)return fromConfig;let entries=this.registry?.listProvidersForType("tunnel")??[],flagged=entries.find((e)=>e.isDefault);if(flagged)return flagged.pluginName;if(entries.length===1)return entries[0].pluginName;return}listProviderEntries(){return this.registry?.listProvidersForType("tunnel")??[]}async listAllTunnels(filterProvider){let entries=this.listProviderEntries(),targeted=filterProvider?entries.filter((e)=>e.pluginName===filterProvider):entries,results=[];for(let entry of targeted){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;try{let tunnels=await provider.list();results.push({provider:entry.pluginName,tunnels})}catch(err){this.log.warn(LOG_SOURCE,`listAllTunnels: ${entry.pluginName} failed`,{error:String(err)})}}return results}async listProviders(){let entries=this.listProviderEntries(),snapshots=[];for(let entry of entries){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;let health;try{health=await provider.healthCheck()}catch(err){health={ok:!1,message:String(err)}}snapshots.push({name:entry.pluginName,isDefault:entry.isDefault,capabilities:provider.getCapabilities(),health})}return snapshots}async getTunnel(id,opts={}){if(opts.provider){let provider=await this.resolveProvider(opts.provider),tunnel=await provider.getStatus(id);return tunnel?{provider:provider.name,tunnel}:null}for(let entry of this.listProviderEntries()){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider)continue;try{let tunnel=await provider.getStatus(id);if(tunnel)return{provider:entry.pluginName,tunnel}}catch{}}return null}async issueSession(req){let{provider:providerName,...rest}=req,provider=await this.resolveProvider(providerName);return{...await provider.issueSession(rest),provider:provider.name}}async startTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).start(id)}async stopTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).stop(id)}async deleteTunnel(id,opts={}){return(await this.resolveProvider(opts.provider)).delete(id)}async rotate(id,opts={}){return(await this.resolveProvider(opts.provider)).rotate(id)}async attachDomain(id,domain,opts={}){return(await this.resolveProvider(opts.provider)).attachCustomDomain(id,domain)}async detachDomain(id,domain,opts={}){return(await this.resolveProvider(opts.provider)).detachCustomDomain(id,domain)}async listSessions(id,opts={}){return(await this.resolveProvider(opts.provider)).listSessions(id)}async getHealth(){let entries=this.listProviderEntries(),providers=[];for(let entry of entries){let provider=this.registry?.getProviderByName("tunnel",entry.pluginName);if(!provider){providers.push({name:entry.pluginName,ok:!1,message:"unavailable"});continue}try{let result=await provider.healthCheck();providers.push({name:entry.pluginName,ok:result.ok,message:result.message})}catch(err){providers.push({name:entry.pluginName,ok:!1,message:String(err)})}}return{manager:"ok",providers}}async setDefault(providerName){if(await this.resolveProvider(providerName),this.registry?.setProviderDefault)this.registry.setProviderDefault("tunnel",providerName);if(this.host?.setConfig)await this.host.setConfig(DEFAULT_PROVIDER_CONFIG_KEY,providerName);this.log.info(LOG_SOURCE,`Default tunnel provider set to ${providerName}`)}}import{Elysia,t}from"elysia";function readProvider(query){return query?.provider??void 0}function createTunnelManagerRoutes(manager){return new Elysia().get("/providers",async({set})=>{try{return{providers:await manager.listProviders()}}catch(err){return set.status=500,{error:"Failed to list providers",details:String(err)}}}).post("/default",async({body,set})=>{try{return await manager.setDefault(body.provider),{success:!0,provider:body.provider}}catch(err){return set.status=400,{error:String(err)}}},{body:t.Object({provider:t.String()})}).get("/health",async({set})=>{try{return await manager.getHealth()}catch(err){return set.status=500,{error:"Failed to get health",details:String(err)}}}).get("/",async({query,set})=>{try{let providerName=readProvider(query),results=await manager.listAllTunnels(providerName);return{tunnels:results.flatMap((r)=>r.tunnels.map((t2)=>({...t2,providerName:r.provider}))),byProvider:results}}catch(err){return set.status=500,{error:"Failed to list tunnels",details:String(err)}}}).post("/issue-session",async({body,set})=>{try{return await manager.issueSession({provider:body.provider,protocol:body.protocol,localPort:body.localPort,localHost:body.localHost,subdomain:body.subdomain,customDomain:body.customDomain,ttlSeconds:body.ttlSeconds,metadata:body.metadata,controlPlanePayload:body.controlPlanePayload})}catch(err){return set.status=500,{error:"Failed to issue session",details:String(err)}}},{body:t.Object({provider:t.Optional(t.String()),protocol:t.Union([t.Literal("http"),t.Literal("https"),t.Literal("tcp"),t.Literal("udp")]),localPort:t.Number(),localHost:t.Optional(t.String()),subdomain:t.Optional(t.String()),customDomain:t.Optional(t.String()),ttlSeconds:t.Optional(t.Number()),metadata:t.Optional(t.Any()),controlPlanePayload:t.Optional(t.Any())})}).get("/:id",async({params,query,set})=>{try{let result=await manager.getTunnel(params.id,{provider:readProvider(query)});if(!result)return set.status=404,{error:"Tunnel not found"};return result}catch(err){return set.status=500,{error:"Failed to get tunnel",details:String(err)}}}).get("/:id/sessions",async({params,query,set})=>{try{return{sessions:await manager.listSessions(params.id,{provider:readProvider(query)})}}catch(err){return set.status=500,{error:"Failed to list sessions",details:String(err)}}}).post("/:id/start",async({params,body,set})=>{try{return{success:!0,tunnel:await manager.startTunnel(params.id,{provider:body?.provider})}}catch(err){return set.status=500,{error:"Failed to start tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).post("/:id/stop",async({params,body,set})=>{try{return await manager.stopTunnel(params.id,{provider:body?.provider}),{success:!0}}catch(err){return set.status=500,{error:"Failed to stop tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).post("/:id/rotate",async({params,body,set})=>{try{return{success:!0,session:await manager.rotate(params.id,{provider:body?.provider})}}catch(err){return set.status=500,{error:"Failed to rotate tunnel",details:String(err)}}},{body:t.Optional(t.Object({provider:t.Optional(t.String())}))}).delete("/:id",async({params,query,set})=>{try{return await manager.deleteTunnel(params.id,{provider:readProvider(query)}),{success:!0}}catch(err){return set.status=500,{error:"Failed to delete tunnel",details:String(err)}}}).post("/:id/domains",async({params,body,set})=>{try{return await manager.attachDomain(params.id,body.domain,{provider:body.provider}),{success:!0}}catch(err){return set.status=500,{error:"Failed to attach domain",details:String(err)}}},{body:t.Object({domain:t.String(),provider:t.Optional(t.String())})}).delete("/:id/domains/:domain",async({params,query,set})=>{try{return await manager.detachDomain(params.id,params.domain,{provider:readProvider(query)}),{success:!0}}catch(err){return set.status=500,{error:"Failed to detach domain",details:String(err)}}})}var manager=new TunnelManager,vibePlugin={name:"tunnel",version:"0.1.0",description:"VibeTunnels manager \u2014 dispatches to registered tunnel providers",tags:["backend","cli","integration"],cliCommand:"tunnel",apiPrefix:"/api/tunnels",createRoutes:()=>createTunnelManagerRoutes(manager),onServerStart:(_app,hostServices)=>{manager.init(hostServices)},onCliSetup:(program,hostServices)=>{registerTunnelCommands(program,hostServices)}},src_default=vibePlugin;export{vibePlugin,src_default as default,TunnelManager};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable opentui primitives for read-style commands.
|
|
3
|
+
*
|
|
4
|
+
* • interactiveTable — a focusable single-column list with a detail pane
|
|
5
|
+
* on the right. The user picks rows with ↑/↓; we render the detail of
|
|
6
|
+
* the focused row in a fixed-width panel. Enter exits and runs an
|
|
7
|
+
* optional callback (e.g. print full detail to stdout). q/Esc cancels.
|
|
8
|
+
*
|
|
9
|
+
* • interactiveDetail — a static key/value panel (great for `vibe profile
|
|
10
|
+
* show`, `vibe info`, etc.). Mostly identical to plain output but inside
|
|
11
|
+
* the alternate screen so it's pretty.
|
|
12
|
+
*
|
|
13
|
+
* Both helpers throw if @opentui/core fails to import — callers should pair
|
|
14
|
+
* them with a plain fallback through `runMultimode`.
|
|
15
|
+
*/
|
|
16
|
+
export interface TableRow {
|
|
17
|
+
/** Stable identifier — exposed to onSelect. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Short label rendered in the left pane. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Optional one-line muted hint shown below the label. */
|
|
22
|
+
hint?: string;
|
|
23
|
+
/** Detail rendered in the right pane (multiline, ANSI ok). */
|
|
24
|
+
detail: string;
|
|
25
|
+
}
|
|
26
|
+
export interface InteractiveTableOptions {
|
|
27
|
+
title: string;
|
|
28
|
+
rows: TableRow[];
|
|
29
|
+
/** Footer hint — overrides the default `↑/↓ navigate · Enter select · q quit`. */
|
|
30
|
+
footer?: string;
|
|
31
|
+
/** Width of the left list pane in columns. Default: 28. */
|
|
32
|
+
listWidth?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Called when the user hits Enter on a row. If omitted, Enter quits.
|
|
35
|
+
* The handler runs AFTER the renderer is destroyed, so it's free to
|
|
36
|
+
* write to stdout.
|
|
37
|
+
*/
|
|
38
|
+
onSelect?: (row: TableRow) => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Render a focusable list with a live detail pane. Resolves once the user
|
|
42
|
+
* hits Enter or quits.
|
|
43
|
+
*/
|
|
44
|
+
export declare function interactiveTable(opts: InteractiveTableOptions): Promise<TableRow | null>;
|
|
45
|
+
export interface InteractiveDetailOptions {
|
|
46
|
+
title: string;
|
|
47
|
+
/** Pre-rendered ANSI body (may be multiline). */
|
|
48
|
+
body: string;
|
|
49
|
+
/** Footer hint. Default: `q to quit`. */
|
|
50
|
+
footer?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Static key-value detail screen. Mostly used for `show` style commands
|
|
54
|
+
* when the user wants the alt-screen polish; equivalent content is
|
|
55
|
+
* available via the plain renderer.
|
|
56
|
+
*/
|
|
57
|
+
export declare function interactiveDetail(opts: InteractiveDetailOptions): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-mode output dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Every read-style command (list/show/status/dashboard) should funnel its
|
|
5
|
+
* output through `runMultimode`. The data is fetched ONCE by `fetchData`,
|
|
6
|
+
* then handed to one of three renderers:
|
|
7
|
+
*
|
|
8
|
+
* • interactive — opentui UI (default in TTY when an interactive renderer
|
|
9
|
+
* is provided AND @opentui/core imports cleanly)
|
|
10
|
+
* • plain — ANSI text written to stdout (the legacy / pipe-friendly
|
|
11
|
+
* output)
|
|
12
|
+
* • json — `JSON.stringify(data, null, 2)` to stdout (or a
|
|
13
|
+
* custom shaper) — friendly for jq/scripting
|
|
14
|
+
*
|
|
15
|
+
* Mutating commands (start/stop/install/...) typically don't need this —
|
|
16
|
+
* they print progress and exit. They MAY still call `runMultimode` to emit
|
|
17
|
+
* a result object in JSON when `--json` is set; see `pickOutputMode`.
|
|
18
|
+
*
|
|
19
|
+
* The selection rules:
|
|
20
|
+
*
|
|
21
|
+
* ┌─────────────────────┬──────────────────────────────────────────────┐
|
|
22
|
+
* │ explicit --json │ json renderer (or default JSON.stringify) │
|
|
23
|
+
* │ explicit --plain │ plain renderer │
|
|
24
|
+
* │ stdout is not a TTY │ plain renderer │
|
|
25
|
+
* │ NO_COLOR, CI=true │ plain renderer │
|
|
26
|
+
* │ no interactive fn │ plain renderer │
|
|
27
|
+
* │ otherwise │ interactive renderer (falls back to plain │
|
|
28
|
+
* │ │ if @opentui/core fails to import) │
|
|
29
|
+
* └─────────────────────┴──────────────────────────────────────────────┘
|
|
30
|
+
*/
|
|
31
|
+
export type OutputMode = "auto" | "interactive" | "plain" | "json";
|
|
32
|
+
export interface OutputFlags {
|
|
33
|
+
json?: boolean;
|
|
34
|
+
plain?: boolean;
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface MultimodeOptions<T> {
|
|
38
|
+
/** Pure data fetcher. Called once. */
|
|
39
|
+
fetchData: () => Promise<T> | T;
|
|
40
|
+
/** Plain-text renderer. Required — every command needs a pipe-friendly fallback. */
|
|
41
|
+
plain: (data: T) => void | Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional opentui renderer. Only used when stdout is a TTY and opentui
|
|
44
|
+
* imports cleanly. If omitted or it fails, we fall back to `plain`.
|
|
45
|
+
*/
|
|
46
|
+
interactive?: (data: T) => Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Optional JSON shaper. Defaults to `JSON.stringify(data, null, 2)`.
|
|
49
|
+
* Override when you need to redact secrets or reshape for scripting.
|
|
50
|
+
*/
|
|
51
|
+
json?: (data: T) => unknown;
|
|
52
|
+
/** Output mode (resolved from CLI flags by `pickOutputMode`). */
|
|
53
|
+
mode?: OutputMode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the desired output mode from CLI flags. The caller should pass the
|
|
57
|
+
* merged opts of the local command + the global program (since `--json`
|
|
58
|
+
* lives on the program level too).
|
|
59
|
+
*/
|
|
60
|
+
export declare function pickOutputMode(flags: OutputFlags): OutputMode;
|
|
61
|
+
export declare function runMultimode<T>(opts: MultimodeOptions<T>): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Convenience: emit the data shape as JSON (used by mutating commands that
|
|
64
|
+
* still want a `--json` opt-in for scripting). Returns true if it printed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function maybePrintJson(flags: OutputFlags, data: unknown): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecontrols/vibe-plugin-tunnel",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.507.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"license": "SEE LICENSE IN LICENSE",
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target bun --minify-syntax --minify-whitespace --external elysia && tsc --emitDeclarationOnly --declaration --outDir ./dist",
|
|
18
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun --minify-syntax --minify-whitespace --external elysia --external @opentui/core && tsc --emitDeclarationOnly --declaration --outDir ./dist",
|
|
19
19
|
"dev": "tsc --watch",
|
|
20
20
|
"lint": "eslint ./src",
|
|
21
21
|
"format": "bunx prettier . --write",
|