@vibecontrols/vibe-plugin-tool-git 2026.508.3 → 2026.509.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.d.ts +8 -7
- package/dist/index.js +13 -4
- package/dist/routes.d.ts +6 -156
- package/dist/types.d.ts +4 -51
- package/package.json +2 -1
- package/dist/utils/multimode.d.ts +0 -66
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @vibecontrols/vibe-plugin-tool-git
|
|
3
3
|
*
|
|
4
4
|
* Visual Git Client via Ungit — reverse-proxied through the VibeControls
|
|
5
5
|
* agent. Manages the Ungit child process lifecycle (install, start, stop)
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Registers:
|
|
9
9
|
* - Elysia routes: /api/ungit/* (REST API)
|
|
10
10
|
* - Proxy routes: /ungit/* (reverse proxy to ungit)
|
|
11
|
-
* - CLI command: vibe ungit {status,install,start,stop}
|
|
11
|
+
* - CLI command: vibe ungit {status,install,start,stop,restart}
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* Migrated to consume `@vibecontrols/plugin-sdk` for the contract,
|
|
14
|
+
* lifecycle, telemetry, CLI multimode, and redaction helpers.
|
|
14
15
|
*/
|
|
15
|
-
import type
|
|
16
|
-
export type {
|
|
17
|
-
export declare const
|
|
18
|
-
export default
|
|
16
|
+
import { type VibePluginFactory } from "@vibecontrols/plugin-sdk";
|
|
17
|
+
export type { UngitStatus, StartBody } from "./types.js";
|
|
18
|
+
export declare const createPlugin: VibePluginFactory;
|
|
19
|
+
export default createPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __defProp=Object.defineProperty;var __returnValue=(v)=>v;function __exportSetter(name,newValue){this[name]=__returnValue.bind(null,newValue)}var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0,configurable:!0,set:__exportSetter.bind(all,name)})};var __esm=(fn,res)=>()=>(fn&&(res=fn(fn=0)),res);var __require=import.meta.require;async function isPortAvailable(port){try{return Bun.serve({port,hostname:"127.0.0.1",fetch(){return new Response}}).stop(!0),!0}catch{return!1}}async function findAvailablePort(preferred){let start=preferred??DEFAULT_PORT;if(await isPortAvailable(start))return start;for(let port=DEFAULT_PORT;port<=PORT_RANGE_END;port++){if(port===start)continue;if(await isPortAvailable(port))return port}throw Error(`No available port in range ${DEFAULT_PORT}-${PORT_RANGE_END}`)}async function checkInstallation(){try{let localPath=new URL("../../node_modules/.bin/ungit",import.meta.url).pathname;if(await Bun.file(localPath).exists())return{installed:!0,binaryPath:localPath}}catch{}try{let path=Bun.which("ungit");if(path)return{installed:!0,binaryPath:path}}catch{}return{installed:!1}}async function installUngit(){try{let proc=Bun.spawn(["npm","install","-g","ungit"],{stdout:"pipe",stderr:"pipe"}),exitCode=await proc.exited;if(exitCode!==0){let stderr=await new Response(proc.stderr).text();return{success:!1,error:`npm install -g ungit failed (exit ${exitCode}): ${stderr}`}}return{success:!0}}catch(err){return{success:!1,error:err instanceof Error?err.message:"Installation failed"}}}async function startUngit(options){if(childProcess&¤tPid){if(isProcessAlive(currentPid))return{pid:currentPid,port:currentPort};childProcess=null,currentPid=null}if(isStarting)throw Error("Ungit is already starting");isStarting=!0,lastError=null;try{let port=await findAvailablePort(options?.port),workingDir=options?.workingDir||process.env.HOME||"/",install=await checkInstallation();if(!install.installed||!install.binaryPath)throw Error("Ungit is not installed");let args=[install.binaryPath,"--port",String(port),"--no-b","--ungitBindIp","127.0.0.1","--rootPath","/ungit"],proc=Bun.spawn(args,{stdout:"pipe",stderr:"pipe",cwd:workingDir,env:{...process.env,PORT:void 0}});if(childProcess=proc,currentPort=port,currentWorkingDir=workingDir,currentPid=proc.pid,proc.exited.then((code)=>{if(childProcess===proc){if(childProcess=null,currentPid=null,code!==0&&code!==null)lastError=`Ungit exited with code ${code}`}}),await new Promise((resolve)=>setTimeout(resolve,2000)),!isProcessAlive(proc.pid)){let stderr=await new Response(proc.stderr).text();throw Error(`Ungit failed to start: ${stderr}`)}return{pid:proc.pid,port}}catch(err){throw lastError=err instanceof Error?err.message:"Failed to start",err}finally{isStarting=!1}}async function stopUngit(){if(!childProcess||!currentPid){childProcess=null,currentPid=null;return}let proc=childProcess,pid=currentPid;childProcess=null,currentPid=null,currentPort=null,currentWorkingDir=null,lastError=null;try{process.kill(pid,"SIGTERM")}catch{return}let deadline=Date.now()+5000;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve)=>setTimeout(resolve,200));if(isProcessAlive(pid))try{process.kill(pid,"SIGKILL")}catch{}try{await proc.exited}catch{}}function getStatus(){let running=Boolean(currentPid&&isProcessAlive(currentPid));if(!running&&childProcess)childProcess=null,currentPid=null;return{installed:!0,running,pid:running?currentPid??void 0:void 0,port:running?currentPort??void 0:void 0,workingDir:running?currentWorkingDir??void 0:void 0,error:lastError??void 0}}function getRunningPort(){if(!currentPid||!isProcessAlive(currentPid))return null;return currentPort}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}var childProcess=null,currentPort=null,currentWorkingDir=null,currentPid=null,lastError=null,isStarting=!1,DEFAULT_PORT=8448,PORT_RANGE_END=8458;var exports_routes={};__export(exports_routes,{createUngitRoutes:()=>createUngitRoutes});import{Elysia}from"elysia";function createUngitRoutes(_hostServices){return new Elysia({prefix:"/api/ungit"}).get("/status",async()=>{let installInfo=await checkInstallation(),processStatus=getStatus();return{installed:installInfo.installed,installing:isInstalling,running:processStatus.running,pid:processStatus.pid,port:processStatus.port,workingDir:processStatus.workingDir,error:installError||processStatus.error||void 0}}).post("/install",async({set})=>{if(isInstalling)return set.status=409,{error:"Installation already in progress"};let check=await checkInstallation();if(check.installed)return{message:"Ungit is already installed",binaryPath:check.binaryPath};return isInstalling=!0,installError=null,(async()=>{try{let result=await installUngit();if(!result.success)installError=result.error||"Installation failed"}catch(err){installError=err instanceof Error?err.message:"Installation failed"}finally{isInstalling=!1}})(),{message:"Installation started -- poll GET /api/ungit/status"}}).post("/start",async({body,set})=>{let{workingDir,port}=body||{};if(!(await checkInstallation()).installed)return set.status=400,{error:"Ungit is not installed. POST /api/ungit/install first"};try{let result=await startUngit({workingDir,port});return{message:"Ungit started",pid:result.pid,port:result.port,workingDir:workingDir||process.env.HOME||"/"}}catch(err){return set.status=500,{error:err instanceof Error?err.message:"Failed to start"}}}).post("/stop",async()=>{return await stopUngit(),{message:"Ungit stopped"}}).post("/restart",async({body,set})=>{let{workingDir}=body||{};if(!(await checkInstallation()).installed)return set.status=400,{error:"Ungit is not installed"};await stopUngit();try{let result=await startUngit({workingDir});return{message:"Ungit restarted",pid:result.pid,port:result.port,workingDir:workingDir||process.env.HOME||"/"}}catch(err){return set.status=500,{error:err instanceof Error?err.message:"Failed to restart"}}})}var isInstalling=!1,installError=null;var init_routes=()=>{};var exports_proxy={};__export(exports_proxy,{default:()=>proxy_default,createUngitProxy:()=>createUngitProxy});import{Elysia as Elysia2}from"elysia";function generateSessionToken(){let bytes=new Uint8Array(32);return crypto.getRandomValues(bytes),Buffer.from(bytes).toString("base64url")}function createSession(){let token=generateSessionToken(),now=Date.now(),session={token,createdAt:now,expiresAt:now+SESSION_TTL_MS};return sessions.set(token,session),session}function validateSessionToken(token){let session=sessions.get(token);if(!session)return!1;if(Date.now()>session.expiresAt)return sessions.delete(token),!1;return!0}function cleanupSessions(){let now=Date.now();for(let[token,session]of sessions)if(now>session.expiresAt)sessions.delete(token)}function getCookie(cookieHeader,name){if(!cookieHeader)return null;let match=cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));return match?match[1]:null}function isAuthed(request,validateApiKey){let cookieHeader=request.headers.get("cookie"),sessionToken=getCookie(cookieHeader,COOKIE_NAME),apiKeyHeader=request.headers.get("x-agent-api-key"),apiKeyParam=new URL(request.url).searchParams.get("apiKey"),hasValidSession=sessionToken?validateSessionToken(sessionToken):!1,hasValidApiKey=apiKeyHeader!=null&&validateApiKey(apiKeyHeader)||apiKeyParam!=null&&validateApiKey(apiKeyParam);if(!hasValidApiKey&&!hasValidSession){let referer=request.headers.get("referer");if(referer)try{let refKey=new URL(referer).searchParams.get("apiKey");if(refKey&&validateApiKey(refKey))hasValidApiKey=!0}catch{}}return{hasValidSession,hasValidApiKey}}function stripPrefix(pathname){return pathname||"/"}function createUngitProxy(getPort,validateApiKey){return new Elysia2({prefix:"/ungit"}).all("/*",async({request})=>{return handleProxyRequest(request,getPort,validateApiKey)}).all("/",async({request})=>{return handleProxyRequest(request,getPort,validateApiKey)})}async function handleProxyRequest(request,getPort,validateApiKey){let{hasValidSession,hasValidApiKey}=isAuthed(request,validateApiKey);if(!hasValidSession&&!hasValidApiKey)return new Response(JSON.stringify({error:"Unauthorized -- provide a valid API key or session"}),{status:401,headers:{"Content-Type":"application/json"}});let sessionCookieHeader=null;if(!hasValidSession&&hasValidApiKey){let session=createSession();sessionCookieHeader=`${COOKIE_NAME}=${session.token}; Path=/ungit/; HttpOnly; SameSite=None; Secure; Max-Age=${Math.floor(SESSION_TTL_MS/1000)}`}let port=getPort();if(!port)return new Response(JSON.stringify({error:"Ungit is not running"}),{status:503,headers:{"Content-Type":"application/json"}});let response=await handleHttpProxy(request,port);if(sessionCookieHeader){let headers=new Headers(response.headers);return headers.set("Set-Cookie",sessionCookieHeader),new Response(response.body,{status:response.status,statusText:response.statusText,headers})}return response}async function handleHttpProxy(request,port){let url=new URL(request.url),strippedPath=stripPrefix(url.pathname),upstreamUrl=`http://127.0.0.1:${port}${strippedPath}${url.search}`,upstreamHeaders=new Headers,hopByHopHeaders=new Set(["connection","keep-alive","transfer-encoding","te","trailer","upgrade","proxy-authorization","proxy-authenticate"]);request.headers.forEach((value,key)=>{if(!hopByHopHeaders.has(key.toLowerCase()))upstreamHeaders.set(key,value)}),upstreamHeaders.set("Host",`127.0.0.1:${port}`);try{let upstreamResponse=await fetch(upstreamUrl,{method:request.method,headers:upstreamHeaders,body:request.method!=="GET"&&request.method!=="HEAD"?request.body:void 0,redirect:"manual"}),responseHeaders=new Headers;upstreamResponse.headers.forEach((value,key)=>{if(!STRIP_RESPONSE_HEADERS.has(key.toLowerCase()))responseHeaders.set(key,value)});let contentType=upstreamResponse.headers.get("content-type")||"";if(strippedPath==="/ungit/"&&!contentType.includes("text/html")){let body=await upstreamResponse.text();if(body.trimStart().startsWith("<!DOCTYPE")||body.trimStart().startsWith("<html"))return responseHeaders.set("content-type","text/html; charset=utf-8"),responseHeaders.delete("content-length"),new Response(body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders});return new Response(body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders})}return new Response(upstreamResponse.body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders})}catch(err){return new Response(JSON.stringify({error:"Failed to proxy to Ungit",details:err instanceof Error?err.message:"Unknown error"}),{status:502,headers:{"Content-Type":"application/json"}})}}var SESSION_TTL_MS=86400000,COOKIE_NAME="__vibe_ungit_session",sessions,STRIP_RESPONSE_HEADERS,proxy_default;var init_proxy=__esm(()=>{sessions=new Map;setInterval(cleanupSessions,600000);STRIP_RESPONSE_HEADERS=new Set(["x-frame-options","content-security-policy","x-content-type-options"]);proxy_default=createUngitProxy});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(
|
|
4
|
-
`)
|
|
2
|
+
var __defProp=Object.defineProperty;var __returnValue=(v)=>v;function __exportSetter(name,newValue){this[name]=__returnValue.bind(null,newValue)}var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0,configurable:!0,set:__exportSetter.bind(all,name)})};var __esm=(fn,res)=>()=>(fn&&(res=fn(fn=0)),res);var __require=import.meta.require;async function isPortAvailable(port){try{return Bun.serve({port,hostname:"127.0.0.1",fetch(){return new Response}}).stop(!0),!0}catch{return!1}}async function findAvailablePort(preferred){let start=preferred??DEFAULT_PORT;if(await isPortAvailable(start))return start;for(let port=DEFAULT_PORT;port<=PORT_RANGE_END;port++){if(port===start)continue;if(await isPortAvailable(port))return port}throw Error(`No available port in range ${DEFAULT_PORT}-${PORT_RANGE_END}`)}async function checkInstallation(){try{let localPath=new URL("../../node_modules/.bin/ungit",import.meta.url).pathname;if(await Bun.file(localPath).exists())return{installed:!0,binaryPath:localPath}}catch{}try{let path=Bun.which("ungit");if(path)return{installed:!0,binaryPath:path}}catch{}return{installed:!1}}async function installUngit(){try{let proc=Bun.spawn(["npm","install","-g","ungit"],{stdout:"pipe",stderr:"pipe"}),exitCode=await proc.exited;if(exitCode!==0){let stderr=await new Response(proc.stderr).text();return{success:!1,error:`npm install -g ungit failed (exit ${exitCode}): ${stderr}`}}return{success:!0}}catch(err){return{success:!1,error:err instanceof Error?err.message:"Installation failed"}}}async function startUngit(options){if(childProcess&¤tPid){if(isProcessAlive(currentPid))return{pid:currentPid,port:currentPort};childProcess=null,currentPid=null}if(isStarting)throw Error("Ungit is already starting");isStarting=!0,lastError=null;try{let port=await findAvailablePort(options?.port),workingDir=options?.workingDir||process.env.HOME||"/",install=await checkInstallation();if(!install.installed||!install.binaryPath)throw Error("Ungit is not installed");let args=[install.binaryPath,"--port",String(port),"--no-b","--ungitBindIp","127.0.0.1","--rootPath","/ungit"],proc=Bun.spawn(args,{stdout:"pipe",stderr:"pipe",cwd:workingDir,env:{...process.env,PORT:void 0}});if(childProcess=proc,currentPort=port,currentWorkingDir=workingDir,currentPid=proc.pid,proc.exited.then((code)=>{if(childProcess===proc){if(childProcess=null,currentPid=null,code!==0&&code!==null)lastError=`Ungit exited with code ${code}`}}),await new Promise((resolve)=>setTimeout(resolve,2000)),!isProcessAlive(proc.pid)){let stderr=await new Response(proc.stderr).text();throw Error(`Ungit failed to start: ${stderr}`)}return{pid:proc.pid,port}}catch(err){throw lastError=err instanceof Error?err.message:"Failed to start",err}finally{isStarting=!1}}async function stopUngit(){if(!childProcess||!currentPid){childProcess=null,currentPid=null;return}let proc=childProcess,pid=currentPid;childProcess=null,currentPid=null,currentPort=null,currentWorkingDir=null,lastError=null;try{process.kill(pid,"SIGTERM")}catch{return}let deadline=Date.now()+5000;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve)=>setTimeout(resolve,200));if(isProcessAlive(pid))try{process.kill(pid,"SIGKILL")}catch{}try{await proc.exited}catch{}}function getStatus(){let running=Boolean(currentPid&&isProcessAlive(currentPid));if(!running&&childProcess)childProcess=null,currentPid=null;return{installed:!0,running,pid:running?currentPid??void 0:void 0,port:running?currentPort??void 0:void 0,workingDir:running?currentWorkingDir??void 0:void 0,error:lastError??void 0}}function getRunningPort(){if(!currentPid||!isProcessAlive(currentPid))return null;return currentPort}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}var childProcess=null,currentPort=null,currentWorkingDir=null,currentPid=null,lastError=null,isStarting=!1,DEFAULT_PORT=8448,PORT_RANGE_END=8458;import{Elysia as Elysia2}from"elysia";var RoutesBuilder=class{constructor(pluginName,hostServices){this.pluginName=pluginName,this.hostServices=hostServices}pluginName;hostServices;prefix;apiKeyResolver;errorHandlerEnabled=!1;loggingEnabled=!1;withPrefix(prefix){return this.prefix=prefix,this}withAuth(getApiKey){return this.apiKeyResolver=getApiKey,this}withErrorHandler(){return this.errorHandlerEnabled=!0,this}withLogging(){return this.loggingEnabled=!0,this}build(){let opts=this.prefix?{prefix:this.prefix}:void 0,app=new Elysia2(opts);if(this.apiKeyResolver){let resolver=this.apiKeyResolver;app.onBeforeHandle(async({request,set})=>{let supplied=request.headers.get("x-api-key"),expected=await resolver();if(!supplied||supplied!==expected)return set.status=401,{error:"unauthorized"};return})}if(this.errorHandlerEnabled){let pluginName=this.pluginName,logger=this.hostServices?.logger;app.onError(({error,set})=>{let message=error instanceof Error?error.message:String(error);return logger?.error?.(pluginName,"route error",{message}),set.status=500,{error:message}})}if(this.loggingEnabled){let pluginName=this.pluginName,logger=this.hostServices?.logger;app.onRequest(({request})=>{logger?.debug?.(pluginName,"request",{method:request.method,url:request.url})})}return app}};var init_routes=()=>{};var exports_routes={};__export(exports_routes,{createUngitRoutes:()=>createUngitRoutes});function createUngitRoutes(hostServices){return new RoutesBuilder("ungit",hostServices).withPrefix("/api/ungit").withErrorHandler().build().get("/status",async()=>{let installInfo=await checkInstallation(),processStatus=getStatus();return{installed:installInfo.installed,installing:isInstalling,running:processStatus.running,pid:processStatus.pid,port:processStatus.port,workingDir:processStatus.workingDir,error:installError||processStatus.error||void 0}}).post("/install",async({set})=>{if(isInstalling)return set.status=409,{error:"Installation already in progress"};let check=await checkInstallation();if(check.installed)return{message:"Ungit is already installed",binaryPath:check.binaryPath};return isInstalling=!0,installError=null,(async()=>{try{let result=await installUngit();if(!result.success)installError=result.error||"Installation failed"}catch(err){installError=err instanceof Error?err.message:"Installation failed"}finally{isInstalling=!1}})(),{message:"Installation started -- poll GET /api/ungit/status"}}).post("/start",async({body,set})=>{let{workingDir,port}=body||{};if(!(await checkInstallation()).installed)return set.status=400,{error:"Ungit is not installed. POST /api/ungit/install first"};try{let result=await startUngit({workingDir,port});return{message:"Ungit started",pid:result.pid,port:result.port,workingDir:workingDir||process.env.HOME||"/"}}catch(err){return set.status=500,{error:err instanceof Error?err.message:"Failed to start"}}}).post("/stop",async()=>{return await stopUngit(),{message:"Ungit stopped"}}).post("/restart",async({body,set})=>{let{workingDir}=body||{};if(!(await checkInstallation()).installed)return set.status=400,{error:"Ungit is not installed"};await stopUngit();try{let result=await startUngit({workingDir});return{message:"Ungit restarted",pid:result.pid,port:result.port,workingDir:workingDir||process.env.HOME||"/"}}catch(err){return set.status=500,{error:err instanceof Error?err.message:"Failed to restart"}}})}var isInstalling=!1,installError=null;var init_routes2=__esm(()=>{init_routes()});var exports_proxy={};__export(exports_proxy,{default:()=>proxy_default,createUngitProxy:()=>createUngitProxy});import{Elysia as Elysia3}from"elysia";function generateSessionToken(){let bytes=new Uint8Array(32);return crypto.getRandomValues(bytes),Buffer.from(bytes).toString("base64url")}function createSession(){let token=generateSessionToken(),now=Date.now(),session={token,createdAt:now,expiresAt:now+SESSION_TTL_MS};return sessions.set(token,session),session}function validateSessionToken(token){let session=sessions.get(token);if(!session)return!1;if(Date.now()>session.expiresAt)return sessions.delete(token),!1;return!0}function cleanupSessions(){let now=Date.now();for(let[token,session]of sessions)if(now>session.expiresAt)sessions.delete(token)}function getCookie(cookieHeader,name){if(!cookieHeader)return null;let match=cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));return match?match[1]:null}function isAuthed(request,validateApiKey){let cookieHeader=request.headers.get("cookie"),sessionToken=getCookie(cookieHeader,COOKIE_NAME),apiKeyHeader=request.headers.get("x-agent-api-key"),apiKeyParam=new URL(request.url).searchParams.get("apiKey"),hasValidSession=sessionToken?validateSessionToken(sessionToken):!1,hasValidApiKey=apiKeyHeader!=null&&validateApiKey(apiKeyHeader)||apiKeyParam!=null&&validateApiKey(apiKeyParam);if(!hasValidApiKey&&!hasValidSession){let referer=request.headers.get("referer");if(referer)try{let refKey=new URL(referer).searchParams.get("apiKey");if(refKey&&validateApiKey(refKey))hasValidApiKey=!0}catch{}}return{hasValidSession,hasValidApiKey}}function stripPrefix(pathname){return pathname||"/"}function createUngitProxy(getPort,validateApiKey){return new Elysia3({prefix:"/ungit"}).all("/*",async({request})=>{return handleProxyRequest(request,getPort,validateApiKey)}).all("/",async({request})=>{return handleProxyRequest(request,getPort,validateApiKey)})}async function handleProxyRequest(request,getPort,validateApiKey){let{hasValidSession,hasValidApiKey}=isAuthed(request,validateApiKey);if(!hasValidSession&&!hasValidApiKey)return new Response(JSON.stringify({error:"Unauthorized -- provide a valid API key or session"}),{status:401,headers:{"Content-Type":"application/json"}});let sessionCookieHeader=null;if(!hasValidSession&&hasValidApiKey){let session=createSession();sessionCookieHeader=`${COOKIE_NAME}=${session.token}; Path=/ungit/; HttpOnly; SameSite=None; Secure; Max-Age=${Math.floor(SESSION_TTL_MS/1000)}`}let port=getPort();if(!port)return new Response(JSON.stringify({error:"Ungit is not running"}),{status:503,headers:{"Content-Type":"application/json"}});let response=await handleHttpProxy(request,port);if(sessionCookieHeader){let headers=new Headers(response.headers);return headers.set("Set-Cookie",sessionCookieHeader),new Response(response.body,{status:response.status,statusText:response.statusText,headers})}return response}async function handleHttpProxy(request,port){let url=new URL(request.url),strippedPath=stripPrefix(url.pathname),upstreamUrl=`http://127.0.0.1:${port}${strippedPath}${url.search}`,upstreamHeaders=new Headers,hopByHopHeaders=new Set(["connection","keep-alive","transfer-encoding","te","trailer","upgrade","proxy-authorization","proxy-authenticate"]);request.headers.forEach((value,key)=>{if(!hopByHopHeaders.has(key.toLowerCase()))upstreamHeaders.set(key,value)}),upstreamHeaders.set("Host",`127.0.0.1:${port}`);try{let upstreamResponse=await fetch(upstreamUrl,{method:request.method,headers:upstreamHeaders,body:request.method!=="GET"&&request.method!=="HEAD"?request.body:void 0,redirect:"manual"}),responseHeaders=new Headers;upstreamResponse.headers.forEach((value,key)=>{if(!STRIP_RESPONSE_HEADERS.has(key.toLowerCase()))responseHeaders.set(key,value)});let contentType=upstreamResponse.headers.get("content-type")||"";if(strippedPath==="/ungit/"&&!contentType.includes("text/html")){let body=await upstreamResponse.text();if(body.trimStart().startsWith("<!DOCTYPE")||body.trimStart().startsWith("<html"))return responseHeaders.set("content-type","text/html; charset=utf-8"),responseHeaders.delete("content-length"),new Response(body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders});return new Response(body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders})}return new Response(upstreamResponse.body,{status:upstreamResponse.status,statusText:upstreamResponse.statusText,headers:responseHeaders})}catch(err){return new Response(JSON.stringify({error:"Failed to proxy to Ungit",details:err instanceof Error?err.message:"Unknown error"}),{status:502,headers:{"Content-Type":"application/json"}})}}var SESSION_TTL_MS=86400000,COOKIE_NAME="__vibe_ungit_session",sessions,STRIP_RESPONSE_HEADERS,proxy_default;var init_proxy=__esm(()=>{sessions=new Map;setInterval(cleanupSessions,600000);STRIP_RESPONSE_HEADERS=new Set(["x-frame-options","content-security-policy","x-content-type-options"]);proxy_default=createUngitProxy});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}'
|
|
3
|
+
`);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)}
|
|
4
|
+
`);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)}
|
|
5
|
+
`),!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 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:`
|
|
5
6
|
${opts.body}
|
|
6
|
-
`,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
|
|
7
|
+
`,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 agentApiKey=null,PLUGIN_NAME="ungit",PLUGIN_VERSION="2026.509.2",createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tool.ready",onInit:async(hostServices)=>{new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,hostServices).emitEvent("tool.ready",{provider:"git"})}});return{capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"Visual Git Client (Ungit)",tags:["frontend","integration"],hasUI:!0,cliCommand:"ungit",apiPrefix:"/api/ungit",publicPaths:["/ungit/"],async onServerStart(app,hostServices){await lifecycle.onServerStart(app,hostServices);let elysiaApp=app,{createUngitRoutes:createUngitRoutes2}=await Promise.resolve().then(() => (init_routes2(),exports_routes));elysiaApp.use(createUngitRoutes2(hostServices));try{agentApiKey=elysiaApp.decorator?.apiKey??null}catch{agentApiKey=process.env.AGENT_API_KEY??null}let{createUngitProxy:createUngitProxy2}=await Promise.resolve().then(() => (init_proxy(),exports_proxy));elysiaApp.use(createUngitProxy2(()=>getRunningPort(),(key)=>{if(!agentApiKey)return!1;return key===agentApiKey})),process.stdout.write(` Plugin 'ungit' registered routes: /api/ungit, /ungit
|
|
8
|
+
`)},async onServerStop(){await stopUngit(),process.stdout.write(` Plugin 'ungit' stopped
|
|
9
|
+
`)},onCliSetup(programArg){let cmd=programArg.command("ungit").description("Visual Git Client (Ungit)");cmd.command("status").description("Show Ungit status").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{await runMultimode({mode:pickOutputMode(opts),fetchData:async()=>{return await(await apiFetch("/api/ungit/status")).json()},plain:(data)=>{process.stdout.write(`${JSON.stringify(data,null,2)}
|
|
10
|
+
`)},interactive:async(data)=>{await interactiveDetail({title:"ungit \u2014 status",body:JSON.stringify(data,null,2)})},json:(data)=>redact(data)})}),cmd.command("install").description("Install Ungit globally via npm").option("--json","Emit JSON").action(async(opts)=>{if(!opts.json)process.stdout.write(`Installing Ungit...
|
|
11
|
+
`);let data=await(await apiFetch("/api/ungit/install",{method:"POST"})).json();if(maybePrintJson(opts,{ok:!0,action:"install",result:data}))return;process.stdout.write(`${JSON.stringify(data,null,2)}
|
|
12
|
+
`)}),cmd.command("start").description("Start Ungit").option("--dir <dir>","Working directory for git operations").option("--port <port>","Port to bind to").option("--json","Emit JSON").action(async(opts)=>{let body={};if(opts.dir)body.workingDir=opts.dir;if(opts.port)body.port=parseInt(opts.port,10);let data=await(await apiFetch("/api/ungit/start",{method:"POST",body:JSON.stringify(body)})).json();if(maybePrintJson(opts,{ok:!0,action:"start",result:data}))return;process.stdout.write(`${JSON.stringify(data,null,2)}
|
|
13
|
+
`)}),cmd.command("stop").description("Stop Ungit").option("--json","Emit JSON").action(async(opts)=>{let data=await(await apiFetch("/api/ungit/stop",{method:"POST"})).json();if(maybePrintJson(opts,{ok:!0,action:"stop",result:data}))return;process.stdout.write(`${JSON.stringify(data,null,2)}
|
|
14
|
+
`)}),cmd.command("restart").description("Restart Ungit with optional new working directory").option("--dir <dir>","New working directory").option("--json","Emit JSON").action(async(opts)=>{let body={};if(opts.dir)body.workingDir=opts.dir;let data=await(await apiFetch("/api/ungit/restart",{method:"POST",body:JSON.stringify(body)})).json();if(maybePrintJson(opts,{ok:!0,action:"restart",result:data}))return;process.stdout.write(`${JSON.stringify(data,null,2)}
|
|
15
|
+
`)})}}},src_default=createPlugin;export{src_default as default,createPlugin};
|
package/dist/routes.d.ts
CHANGED
|
@@ -9,160 +9,10 @@
|
|
|
9
9
|
* POST /start - Start Ungit with optional working directory and port
|
|
10
10
|
* POST /stop - Stop the Ungit process
|
|
11
11
|
* POST /restart - Restart Ungit with optional new working directory
|
|
12
|
+
*
|
|
13
|
+
* Built on `RoutesBuilder` from `@vibecontrols/plugin-sdk/routes` —
|
|
14
|
+
* concerns like prefix, error handler, and logging fold through the SDK.
|
|
12
15
|
*/
|
|
13
|
-
import { Elysia } from "elysia";
|
|
14
|
-
import type { HostServices } from "
|
|
15
|
-
export declare function createUngitRoutes(
|
|
16
|
-
decorator: {};
|
|
17
|
-
store: {};
|
|
18
|
-
derive: {};
|
|
19
|
-
resolve: {};
|
|
20
|
-
}, {
|
|
21
|
-
typebox: {};
|
|
22
|
-
error: {};
|
|
23
|
-
}, {
|
|
24
|
-
schema: {};
|
|
25
|
-
standaloneSchema: {};
|
|
26
|
-
macro: {};
|
|
27
|
-
macroFn: {};
|
|
28
|
-
parser: {};
|
|
29
|
-
response: {};
|
|
30
|
-
}, {
|
|
31
|
-
api: {
|
|
32
|
-
ungit: {
|
|
33
|
-
status: {
|
|
34
|
-
get: {
|
|
35
|
-
body: unknown;
|
|
36
|
-
params: {};
|
|
37
|
-
query: unknown;
|
|
38
|
-
headers: unknown;
|
|
39
|
-
response: {
|
|
40
|
-
200: {
|
|
41
|
-
installed: boolean;
|
|
42
|
-
installing: boolean;
|
|
43
|
-
running: boolean;
|
|
44
|
-
pid: number | undefined;
|
|
45
|
-
port: number | undefined;
|
|
46
|
-
workingDir: string | undefined;
|
|
47
|
-
error: string | undefined;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
} & {
|
|
55
|
-
api: {
|
|
56
|
-
ungit: {
|
|
57
|
-
install: {
|
|
58
|
-
post: {
|
|
59
|
-
body: unknown;
|
|
60
|
-
params: {};
|
|
61
|
-
query: unknown;
|
|
62
|
-
headers: unknown;
|
|
63
|
-
response: {
|
|
64
|
-
200: {
|
|
65
|
-
error: string;
|
|
66
|
-
message?: undefined;
|
|
67
|
-
binaryPath?: undefined;
|
|
68
|
-
} | {
|
|
69
|
-
message: string;
|
|
70
|
-
binaryPath: string | undefined;
|
|
71
|
-
error?: undefined;
|
|
72
|
-
} | {
|
|
73
|
-
message: string;
|
|
74
|
-
error?: undefined;
|
|
75
|
-
binaryPath?: undefined;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
} & {
|
|
83
|
-
api: {
|
|
84
|
-
ungit: {
|
|
85
|
-
start: {
|
|
86
|
-
post: {
|
|
87
|
-
body: unknown;
|
|
88
|
-
params: {};
|
|
89
|
-
query: unknown;
|
|
90
|
-
headers: unknown;
|
|
91
|
-
response: {
|
|
92
|
-
200: {
|
|
93
|
-
error: string;
|
|
94
|
-
message?: undefined;
|
|
95
|
-
pid?: undefined;
|
|
96
|
-
port?: undefined;
|
|
97
|
-
workingDir?: undefined;
|
|
98
|
-
} | {
|
|
99
|
-
message: string;
|
|
100
|
-
pid: number;
|
|
101
|
-
port: number;
|
|
102
|
-
workingDir: string;
|
|
103
|
-
error?: undefined;
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
} & {
|
|
111
|
-
api: {
|
|
112
|
-
ungit: {
|
|
113
|
-
stop: {
|
|
114
|
-
post: {
|
|
115
|
-
body: unknown;
|
|
116
|
-
params: {};
|
|
117
|
-
query: unknown;
|
|
118
|
-
headers: unknown;
|
|
119
|
-
response: {
|
|
120
|
-
200: {
|
|
121
|
-
message: string;
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
} & {
|
|
129
|
-
api: {
|
|
130
|
-
ungit: {
|
|
131
|
-
restart: {
|
|
132
|
-
post: {
|
|
133
|
-
body: unknown;
|
|
134
|
-
params: {};
|
|
135
|
-
query: unknown;
|
|
136
|
-
headers: unknown;
|
|
137
|
-
response: {
|
|
138
|
-
200: {
|
|
139
|
-
error: string;
|
|
140
|
-
message?: undefined;
|
|
141
|
-
pid?: undefined;
|
|
142
|
-
port?: undefined;
|
|
143
|
-
workingDir?: undefined;
|
|
144
|
-
} | {
|
|
145
|
-
message: string;
|
|
146
|
-
pid: number;
|
|
147
|
-
port: number;
|
|
148
|
-
workingDir: string;
|
|
149
|
-
error?: undefined;
|
|
150
|
-
};
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
}, {
|
|
157
|
-
derive: {};
|
|
158
|
-
resolve: {};
|
|
159
|
-
schema: {};
|
|
160
|
-
standaloneSchema: {};
|
|
161
|
-
response: {};
|
|
162
|
-
}, {
|
|
163
|
-
derive: {};
|
|
164
|
-
resolve: {};
|
|
165
|
-
schema: {};
|
|
166
|
-
standaloneSchema: {};
|
|
167
|
-
response: {};
|
|
168
|
-
}>;
|
|
16
|
+
import type { Elysia } from "elysia";
|
|
17
|
+
import type { HostServices } from "@vibecontrols/plugin-sdk/contract";
|
|
18
|
+
export declare function createUngitRoutes(hostServices: HostServices): Elysia;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,57 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Domain models for the vibe-plugin-tool-git (Ungit) plugin.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Plugin contract types (VibePlugin / HostServices / PluginCapabilities /
|
|
5
|
+
* StorageProvider / ServiceRegistry) are imported from
|
|
6
|
+
* `@vibecontrols/plugin-sdk` — do NOT redeclare them here.
|
|
7
7
|
*/
|
|
8
|
-
import type { Elysia } from "elysia";
|
|
9
|
-
import type { Command } from "commander";
|
|
10
|
-
export interface StorageProvider {
|
|
11
|
-
get(namespace: string, key: string): Promise<string | null>;
|
|
12
|
-
set(namespace: string, key: string, value: string): Promise<void>;
|
|
13
|
-
delete(namespace: string, key: string): Promise<boolean>;
|
|
14
|
-
keys(namespace: string): Promise<string[]>;
|
|
15
|
-
}
|
|
16
|
-
export interface EventBus {
|
|
17
|
-
emit(event: string, payload: unknown): void;
|
|
18
|
-
on(event: string, handler: (payload: unknown) => void): void;
|
|
19
|
-
off(event: string, handler: (payload: unknown) => void): void;
|
|
20
|
-
}
|
|
21
|
-
export interface ServiceRegistry {
|
|
22
|
-
get<T = unknown>(name: string): T | undefined;
|
|
23
|
-
}
|
|
24
|
-
export interface HostServices {
|
|
25
|
-
telemetry?: {
|
|
26
|
-
emit: (name: string, payload?: Record<string, unknown>) => void;
|
|
27
|
-
};
|
|
28
|
-
storage: StorageProvider;
|
|
29
|
-
eventBus?: EventBus;
|
|
30
|
-
serviceRegistry?: ServiceRegistry;
|
|
31
|
-
}
|
|
32
|
-
export interface PluginCapabilities {
|
|
33
|
-
storage?: "none" | "read" | "rw";
|
|
34
|
-
secrets?: "none" | "read" | "rw";
|
|
35
|
-
gateway?: boolean;
|
|
36
|
-
broadcast?: boolean;
|
|
37
|
-
subprocess?: boolean;
|
|
38
|
-
audit?: boolean;
|
|
39
|
-
telemetry?: boolean;
|
|
40
|
-
}
|
|
41
|
-
export interface VibePlugin {
|
|
42
|
-
capabilities?: PluginCapabilities;
|
|
43
|
-
name: string;
|
|
44
|
-
version: string;
|
|
45
|
-
description?: string;
|
|
46
|
-
tags?: string[];
|
|
47
|
-
hasUI?: boolean;
|
|
48
|
-
cliCommand?: string;
|
|
49
|
-
apiPrefix?: string;
|
|
50
|
-
publicPaths?: string[];
|
|
51
|
-
onCliSetup?: (program: Command) => void | Promise<void>;
|
|
52
|
-
onServerStart?: (app: Elysia, hostServices: HostServices) => void | Promise<void>;
|
|
53
|
-
onServerStop?: () => void | Promise<void>;
|
|
54
|
-
}
|
|
55
8
|
export interface UngitStatus {
|
|
56
9
|
installed: boolean;
|
|
57
10
|
running: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecontrols/vibe-plugin-tool-git",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.509.2",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"license": "SEE LICENSE IN LICENSE",
|
|
36
36
|
"description": "Visual Git Client (Ungit) — reverse-proxied through the VibeControls agent",
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@vibecontrols/plugin-sdk": "2026.509.1",
|
|
38
39
|
"ungit": "^1.5.28"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
@@ -1,66 +0,0 @@
|
|
|
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;
|