@vibecontrols/vibe-plugin-tool-git 2026.508.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @burdenoff/vibe-plugin-git v1.0.0
3
+ *
4
+ * Visual Git Client via Ungit — reverse-proxied through the VibeControls
5
+ * agent. Manages the Ungit child process lifecycle (install, start, stop)
6
+ * and proxies all traffic at /ungit/* with session cookie auth.
7
+ *
8
+ * Registers:
9
+ * - Elysia routes: /api/ungit/* (REST API)
10
+ * - Proxy routes: /ungit/* (reverse proxy to ungit)
11
+ * - CLI command: vibe ungit {status,install,start,stop}
12
+ *
13
+ * Install: vibe plugin install @burdenoff/vibe-plugin-git
14
+ */
15
+ import type { VibePlugin } from "./types.js";
16
+ export type { VibePlugin, HostServices, StorageProvider, EventBus, ServiceRegistry, UngitStatus, } from "./types.js";
17
+ export declare const vibePlugin: VibePlugin;
18
+ export default vibePlugin;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
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&&currentPid){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(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 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
+ ${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 SECRET_RX=/(token|secret|password|apikey|api_key)/i;function redact(value){if(value===null||value===void 0)return value;if(Array.isArray(value))return value.map(redact);if(typeof value!=="object")return value;let out={};for(let[k,v]of Object.entries(value))out[k]=SECRET_RX.test(k)?"[redacted]":redact(v);return out}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,vibePlugin={capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0},name:"ungit",version:"1.0.0",description:"Visual Git Client (Ungit)",tags:["frontend","integration"],hasUI:!0,cliCommand:"ungit",apiPrefix:"/api/ungit",publicPaths:["/ungit/"],async onServerStart(app,hostServices){hostServices?.telemetry?.emit("tool.ready",{provider:"git"});let{createUngitRoutes:createUngitRoutes2}=await Promise.resolve().then(() => (init_routes(),exports_routes));app.use(createUngitRoutes2(hostServices));try{agentApiKey=app.decorator?.apiKey??null}catch{agentApiKey=process.env.AGENT_API_KEY??null}let{createUngitProxy:createUngitProxy2}=await Promise.resolve().then(() => (init_proxy(),exports_proxy));app.use(createUngitProxy2(()=>getRunningPort(),(key)=>{if(!agentApiKey)return!1;return key===agentApiKey})),console.log(" Plugin 'ungit' registered routes: /api/ungit, /ungit")},async onServerStop(){await stopUngit(),console.log(" Plugin 'ungit' stopped")},onCliSetup(program){let cmd=program.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)=>{console.log(JSON.stringify(data,null,2))},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)console.log("Installing Ungit...");let data=await(await apiFetch("/api/ungit/install",{method:"POST"})).json();if(maybePrintJson(opts,{ok:!0,action:"install",result:data}))return;console.log(JSON.stringify(data,null,2))}),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;console.log(JSON.stringify(data,null,2))}),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;console.log(JSON.stringify(data,null,2))}),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;console.log(JSON.stringify(data,null,2))})}},src_default=vibePlugin;export{vibePlugin,src_default as default};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Ungit Process Lifecycle Manager
3
+ *
4
+ * Manages starting, stopping, and monitoring the Ungit child process.
5
+ * Ungit binds to 127.0.0.1 only -- access is via the agent reverse proxy.
6
+ */
7
+ import type { UngitStatus } from "../types.js";
8
+ /**
9
+ * Find an available port in the range [DEFAULT_PORT, PORT_RANGE_END].
10
+ */
11
+ export declare function findAvailablePort(preferred?: number): Promise<number>;
12
+ /**
13
+ * Check whether Ungit is installed (globally or locally).
14
+ */
15
+ export declare function checkInstallation(): Promise<{
16
+ installed: boolean;
17
+ binaryPath?: string;
18
+ }>;
19
+ /**
20
+ * Install Ungit globally via npm.
21
+ */
22
+ export declare function installUngit(): Promise<{
23
+ success: boolean;
24
+ error?: string;
25
+ }>;
26
+ /**
27
+ * Start Ungit as a child process.
28
+ */
29
+ export declare function startUngit(options?: {
30
+ workingDir?: string;
31
+ port?: number;
32
+ }): Promise<{
33
+ pid: number;
34
+ port: number;
35
+ }>;
36
+ /**
37
+ * Stop the Ungit process.
38
+ */
39
+ export declare function stopUngit(): Promise<void>;
40
+ /**
41
+ * Get the current status of Ungit.
42
+ */
43
+ export declare function getStatus(): UngitStatus;
44
+ /**
45
+ * Get the port Ungit is currently running on.
46
+ */
47
+ export declare function getRunningPort(): number | null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ungit Reverse Proxy
3
+ *
4
+ * Proxies all requests from /ungit/* to the local Ungit instance.
5
+ * Handles:
6
+ * - Session cookie authentication (Ungit internal requests can't send API key headers)
7
+ * - HTTP reverse proxying with streaming
8
+ * - WebSocket bridging for socket.io (Ungit uses socket.io for real-time updates)
9
+ * - Header stripping (X-Frame-Options, CSP) to allow iframe embedding
10
+ */
11
+ /**
12
+ * Create the reverse proxy Elysia instance.
13
+ *
14
+ * @param getPort - Returns the port Ungit is running on, or null if not running
15
+ * @param validateApiKey - Validates an API key string against the agent's key
16
+ */
17
+ export declare function createUngitProxy(getPort: () => number | null, validateApiKey: (key: string) => boolean): any;
18
+ export default createUngitProxy;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * REST API routes for the Ungit plugin.
3
+ *
4
+ * Prefix: /api/ungit
5
+ *
6
+ * Routes:
7
+ * GET /status - Current Ungit status (installed, running, port, pid)
8
+ * POST /install - Install Ungit globally via npm
9
+ * POST /start - Start Ungit with optional working directory and port
10
+ * POST /stop - Stop the Ungit process
11
+ * POST /restart - Restart Ungit with optional new working directory
12
+ */
13
+ import { Elysia } from "elysia";
14
+ import type { HostServices } from "./types.js";
15
+ export declare function createUngitRoutes(_hostServices: HostServices): Elysia<"/api/ungit", {
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
+ }>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Type declarations for the vibe-plugin-git (Ungit) plugin.
3
+ *
4
+ * All interfaces are defined locally so the plugin does not hard-import
5
+ * from the core agent package. At runtime the host agent injects concrete
6
+ * implementations via HostServices.
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
+ export interface UngitStatus {
56
+ installed: boolean;
57
+ running: boolean;
58
+ pid?: number;
59
+ port?: number;
60
+ workingDir?: string;
61
+ error?: string;
62
+ }
63
+ export interface StartBody {
64
+ workingDir?: string;
65
+ port?: number;
66
+ }
@@ -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 ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@vibecontrols/vibe-plugin-tool-git",
3
+ "version": "2026.508.3",
4
+ "main": "./dist/index.js",
5
+ "type": "module",
6
+ "engines": {
7
+ "bun": ">=1.3.0"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --minify-syntax --minify-whitespace --external elysia --external @opentui/core && tsc --emitDeclarationOnly --declaration --outDir ./dist",
11
+ "dev": "tsc --watch",
12
+ "lint": "eslint ./src",
13
+ "format": "bunx prettier . --write",
14
+ "format:check": "bunx prettier . --check",
15
+ "type:check": "tsc --noEmit",
16
+ "clean": "rimraf dist",
17
+ "prebuild": "bun run clean",
18
+ "prepublishOnly": "bun run build",
19
+ "sanity": "bun run format:check && bun run lint && bun run type:check && bun run build"
20
+ },
21
+ "keywords": [
22
+ "vibecontrols",
23
+ "vibe",
24
+ "vibe-plugin",
25
+ "ungit",
26
+ "git",
27
+ "visual-git",
28
+ "bun"
29
+ ],
30
+ "author": {
31
+ "name": "Vignesh T.V",
32
+ "email": "vignesh@burdenoff.com",
33
+ "url": "https://github.com/tvvignesh"
34
+ },
35
+ "license": "SEE LICENSE IN LICENSE",
36
+ "description": "Visual Git Client (Ungit) — reverse-proxied through the VibeControls agent",
37
+ "dependencies": {
38
+ "ungit": "^1.5.28"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^10.0.1",
42
+ "@types/bun": "^1.2.10",
43
+ "commander": "^14.0.3",
44
+ "elysia": "^1.3.0",
45
+ "eslint": "^9.30.1",
46
+ "globals": "^17.3.0",
47
+ "prettier": "^3.6.2",
48
+ "rimraf": "^6.0.1",
49
+ "typescript": "^5.8.3",
50
+ "typescript-eslint": "^8.56.0"
51
+ },
52
+ "peerDependencies": {
53
+ "elysia": ">=1.3.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "elysia": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/algoshred/vibe-plugin-tool-git.git"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/algoshred/vibe-plugin-tool-git/issues"
66
+ },
67
+ "homepage": "https://vibecontrols.com",
68
+ "publishConfig": {
69
+ "access": "restricted",
70
+ "registry": "https://verdaccio.tooling.internal.burdenoff.com/"
71
+ },
72
+ "files": [
73
+ "dist/**/*",
74
+ "!dist/**/*.map",
75
+ "README.md",
76
+ "LICENSE"
77
+ ],
78
+ "overrides": {
79
+ "lodash": "4.18.1",
80
+ "lodash-es": ">=4.17.21",
81
+ "dompurify": ">=3.3.4",
82
+ "fast-xml-parser": ">=4.5.5",
83
+ "@xmldom/xmldom": ">=0.8.12",
84
+ "swiper": ">=12.1.2",
85
+ "brace-expansion": ">=1.1.13",
86
+ "follow-redirects": ">=1.15.12",
87
+ "smol-toml": ">=1.6.1",
88
+ "axios": ">=1.15.0",
89
+ "flatted": ">=3.4.1",
90
+ "yaml": ">=2.8.3",
91
+ "picomatch": ">=2.3.2",
92
+ "immutable": ">=3.8.3",
93
+ "minimatch": ">=3.1.3"
94
+ }
95
+ }