@vibecontrols/vibe-plugin-gitops 2026.601.4 → 2026.731.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/manager.d.ts +28 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,onNuke,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)},onNuke:async(hostServices,ctx)=>{if(onNuke)return onNuke(hostServices,ctx)}}}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)}},BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};function currentSupportedOS(p=process.platform){if(p==="darwin")return"darwin";if(p==="win32")return"win32";return"linux"}async function provisionMetaProviders(host,metaProviders,platform2=currentSupportedOS()){let plugins=host.plugins;if(!plugins||!metaProviders?.length)return;let skip=plugins.skipInstall?.()??!1;for(let ref of metaProviders){if(!ref.defaultOn?.includes(platform2))continue;try{if(!skip)await plugins.install?.(ref.packageName,{refreshLatest:!0});if(await plugins.loadProvider?.(ref.packageName),await plugins.runPrereqs?.(ref.packageName,{skipInstall:skip}),ref.providerType&&ref.pluginName)await plugins.electDefault?.(ref.providerType,ref.pluginName)}catch{}}}import{existsSync,readFileSync}from"fs";import{join,resolve}from"path";var DEFAULT_AGENT_URL="http://localhost:3005";function agentBaseUrl(){return process.env.AGENT_BASE_URL??DEFAULT_AGENT_URL}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 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/gitops${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/gitops${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()}function registerGitopsCommands(program,_hostServices){let cmd=program.command("gitops").description("Manage GitOps across providers (GitHub, GitLab, Bitbucket, AzDO)"),providersCmd=cmd.command("providers").description("Manage GitOps providers");providersCmd.command("ls").description("List registered providers and their auth state").action(async()=>{let data=await apiGet("/providers");for(let p of data.providers){let star=p.isDefault?"* ":" ",auth=p.authenticated?"authed":"no-auth",ok=p.health.ok?"ok":`error (${p.health.message??""})`;console.log(`${star}${p.name} [${auth}, ${ok}]`)}}),providersCmd.command("set-default <name>").description("Set the active provider").action(async(name)=>{let res=await apiPost("/providers/active",{provider:name});console.log(JSON.stringify(res,null,2))});let authCmd=cmd.command("auth").description("Manage provider credentials");authCmd.command("set <provider> <token>").description("Save a PAT for the named provider").action(async(provider,token)=>{let res=await apiPost(`/${provider}/auth`,{kind:"pat",token});console.log(JSON.stringify(res,null,2))}),authCmd.command("validate <provider>").description("Validate stored credentials").action(async(provider)=>{let res=await apiGet(`/${provider}/auth/validate`);console.log(JSON.stringify(res,null,2))}),authCmd.command("revoke <provider>").description("Delete stored credentials").action(async(provider)=>{let res=await fetch(`${agentBaseUrl()}/api/gitops/${provider}/auth`,{method:"DELETE",headers:authHeaders()});console.log(`status=${res.status}`)}),cmd.command("repos <provider>").description("List repos for the named provider").option("--org <name>","Org / group / workspace name").option("--limit <n>","Maximum repos to fetch (default 30)").action(async(provider,opts)=>{let qs=new URLSearchParams;if(opts.org)qs.set("org",opts.org);if(opts.limit)qs.set("limit",String(opts.limit));let suffix=qs.toString()?`?${qs.toString()}`:"",data=await apiGet(`/${provider}/repos${suffix}`);for(let r of data.items)console.log(`${r.fqn} ${r.visibility} ${r.updatedAt}`)}),cmd.command("runs <provider> <fqn>").description("List recent CI runs for a repo").option("--branch <name>","Limit to a branch").option("--limit <n>","Maximum runs to fetch (default 20)").action(async(provider,fqn,opts)=>{let qs=new URLSearchParams;if(opts.branch)qs.set("branch",opts.branch);if(opts.limit)qs.set("limit",String(opts.limit));let suffix=qs.toString()?`?${qs.toString()}`:"",data=await apiGet(`/${provider}/repos/${encodeURIComponent(fqn)}/runs${suffix}`);for(let r of data){let dur=r.durationSeconds?`${r.durationSeconds}s`:"-";console.log(`${r.id} ${r.pipelineName} ${r.status} ${r.conclusion??""} ${dur} ${r.url}`)}}),cmd.command("analytics <provider> <fqn>").description("Pipeline duration + success-rate analytics for a repo").action(async(provider,fqn)=>{let data=await apiGet(`/${provider}/repos/${encodeURIComponent(fqn)}/pipelines/analytics`);console.log(JSON.stringify(data,null,2))}),cmd.command("doctor").description("Provider health summary").action(async()=>{let data=await apiGet("/providers");for(let p of data.providers)console.log(`${p.name}: ${p.health.ok?"ok":`fail - ${p.health.message??"?"}`}`)})}var DEFAULT_PROVIDER_CONFIG_KEY="provider:default:gitops";class GitOpsError extends Error{code;upstreamStatus;retryAfterSeconds;details;constructor(code,message,opts){super(message);this.name="GitOpsError",this.code=code,this.upstreamStatus=opts?.upstreamStatus,this.retryAfterSeconds=opts?.retryAfterSeconds,this.details=opts?.details}toJSON(){return{error:this.message,code:this.code,upstreamStatus:this.upstreamStatus,retryAfterSeconds:this.retryAfterSeconds,details:this.details}}static fromUnknown(err){if(err instanceof GitOpsError)return err;if(err instanceof Error)return new GitOpsError("UPSTREAM",err.message);return new GitOpsError("UPSTREAM",String(err))}}function httpStatusForCode(code){switch(code){case"AUTH":return 401;case"FORBIDDEN":return 403;case"NOT_FOUND":return 404;case"RATE_LIMITED":return 429;case"UPSTREAM":return 502;case"INVALID":return 400}}var LOG_SOURCE="gitops-manager";class GitopsManager{registry;host;log=new BoundLogger(void 0,LOG_SOURCE);init(host){this.host=host,this.registry=host.serviceRegistry,this.log=new BoundLogger(host.logger,LOG_SOURCE),this.log.info("GitOps manager initialised")}resolveProvider(name){if(!this.registry)throw new GitOpsError("UPSTREAM","Service registry not available");let explicit=name?.trim();if(explicit){let p2=this.registry.getProviderByName("gitops",explicit);if(!p2)throw new GitOpsError("NOT_FOUND",`Gitops provider '${explicit}' is not registered`);return p2}let fallback=this.defaultProviderNameSync();if(!fallback)throw new GitOpsError("INVALID","No default gitops provider configured and none specified");let p=this.registry.getProviderByName("gitops",fallback);if(!p)throw new GitOpsError("NOT_FOUND",`Default gitops provider '${fallback}' is not registered`);return p}defaultProviderNameSync(){let entries=this.registry?.listProvidersForType("gitops")??[],flagged=entries.find((e)=>e.isDefault);if(flagged)return flagged.pluginName;if(entries.length===1)return entries[0].pluginName;return}async defaultProviderName(){let fromConfig=await this.host?.getConfig?.(DEFAULT_PROVIDER_CONFIG_KEY);if(fromConfig)return fromConfig;return this.defaultProviderNameSync()}listProviderEntries(){return this.registry?.listProvidersForType("gitops")??[]}async listProviders(){let entries=this.listProviderEntries(),out=[];for(let entry of entries){let p=this.registry?.getProviderByName("gitops",entry.pluginName);if(!p){out.push({name:entry.pluginName,isDefault:entry.isDefault,registered:!1,authenticated:!1,health:{ok:!1,message:"unavailable"}});continue}let health;try{health=await p.healthCheck()}catch(err){health={ok:!1,message:err instanceof Error?err.message:String(err)}}let authed=!1;try{authed=(await p.validateCredentials()).ok}catch{}out.push({name:entry.pluginName.replace(/^gitops-/,""),isDefault:entry.isDefault,registered:!0,authenticated:authed,health})}return out}async getActiveProvider(){return{name:await this.defaultProviderName()}}async setActiveProvider(name){if(this.resolveProvider(name),this.registry?.setProviderDefault)this.registry.setProviderDefault("gitops",name);let hostWithSetConfig=this.host;if(hostWithSetConfig?.setConfig)await hostWithSetConfig.setConfig(DEFAULT_PROVIDER_CONFIG_KEY,name);this.log.info(`Default gitops provider set to ${name}`)}async saveCredentials(provider,input){await this.resolveProvider(provider).saveCredentials(input)}async validateCredentials(provider){return this.resolveProvider(provider).validateCredentials()}async rotateCredentials(provider,input){return this.resolveProvider(provider).rotateCredentials(input)}async revokeCredentials(provider){await this.resolveProvider(provider).revokeCredentials()}async healthCheck(provider){return this.resolveProvider(provider).healthCheck()}async listRepos(provider,opts){return this.resolveProvider(provider).listRepos(opts)}async getRepo(provider,fqn){return this.resolveProvider(provider).getRepo(fqn)}async listBranches(provider,fqn){return this.resolveProvider(provider).listBranches(fqn)}async listLanguages(provider,fqn){return this.resolveProvider(provider).listLanguages(fqn)}async listContributors(provider,fqn,opts){return this.resolveProvider(provider).listContributors(fqn,opts)}async listPullRequests(provider,fqn,opts){return this.resolveProvider(provider).listPullRequests(fqn,opts)}async getPullRequest(provider,fqn,id){return this.resolveProvider(provider).getPullRequest(fqn,id)}async pullRequestAnalytics(provider,fqn){return this.resolveProvider(provider).pullRequestAnalytics(fqn)}async listIssues(provider,fqn,opts){return this.resolveProvider(provider).listIssues(fqn,opts)}async labelStats(provider,fqn){return this.resolveProvider(provider).labelStats(fqn)}async listPipelines(provider,fqn){return this.resolveProvider(provider).listPipelines(fqn)}async listRecentRuns(provider,fqn,opts){return this.resolveProvider(provider).listRecentRuns(fqn,opts)}async getRun(provider,fqn,runId){return this.resolveProvider(provider).getRun(fqn,runId)}async pipelineAnalytics(provider,fqn){return this.resolveProvider(provider).pipelineAnalytics(fqn)}async listEnvironments(provider,fqn){return this.resolveProvider(provider).listEnvironments?.(fqn)??[]}async listDeployments(provider,fqn,env){return this.resolveProvider(provider).listDeployments?.(fqn,env)??[]}async listSecurityAlerts(provider,fqn,opts){return this.resolveProvider(provider).listSecurityAlerts(fqn,opts)}async orgRollup(provider,org){return this.resolveProvider(provider).orgRollup(org)}async listWebhooks(provider,fqn){return this.resolveProvider(provider).listWebhooks?.(fqn)??[]}}import{Elysia as Elysia2,t}from"elysia";function decodeFqn(raw){try{let decoded=decodeURIComponent(raw);if(decoded.includes("/"))return decoded}catch{}try{let b64=raw.replace(/-/g,"+").replace(/_/g,"/"),padded=b64+"===".slice((b64.length+3)%4);return atob(padded)}catch{return raw}}function err(set,e){let g=GitOpsError.fromUnknown(e);return set.status=httpStatusForCode(g.code),g.toJSON()}function createGitopsManagerRoutes(manager){return new Elysia2().get("/providers",async({set})=>{try{return{providers:await manager.listProviders()}}catch(e){return err(set,e)}}).get("/providers/active",async({set})=>{try{return await manager.getActiveProvider()}catch(e){return err(set,e)}}).post("/providers/active",async({body,set})=>{try{return await manager.setActiveProvider(body.provider),{success:!0,provider:body.provider}}catch(e){return err(set,e)}},{body:t.Object({provider:t.String()})}).post("/:provider/auth",async({params,body,set})=>{try{await manager.saveCredentials(params.provider,body);let validation=await manager.validateCredentials(params.provider);if(!validation.ok)set.status=400;return validation}catch(e){return err(set,e)}},{body:t.Object({kind:t.Union([t.Literal("pat"),t.Literal("oauth"),t.Literal("app")]),token:t.String(),meta:t.Optional(t.Record(t.String(),t.String()))})}).get("/:provider/auth/validate",async({params,set})=>{try{return await manager.validateCredentials(params.provider)}catch(e){return err(set,e)}}).post("/:provider/auth/rotate",async({params,body,set})=>{try{return await manager.rotateCredentials(params.provider,body)}catch(e){return err(set,e)}},{body:t.Object({kind:t.Union([t.Literal("pat"),t.Literal("oauth"),t.Literal("app")]),token:t.String(),meta:t.Optional(t.Record(t.String(),t.String()))})}).delete("/:provider/auth",async({params,set})=>{try{return await manager.revokeCredentials(params.provider),{success:!0}}catch(e){return err(set,e)}}).get("/:provider/health",async({params,set})=>{try{return await manager.healthCheck(params.provider)}catch(e){return err(set,e)}}).get("/:provider/repos",async({params,query,set})=>{try{return await manager.listRepos(params.provider,{org:query.org,limit:query.limit?Number(query.limit):void 0,cursor:query.cursor})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn",async({params,set})=>{try{return await manager.getRepo(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/branches",async({params,set})=>{try{return await manager.listBranches(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/languages",async({params,set})=>{try{return await manager.listLanguages(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/contributors",async({params,query,set})=>{try{return await manager.listContributors(params.provider,decodeFqn(params.fqn),{limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls/analytics",async({params,set})=>{try{return await manager.pullRequestAnalytics(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls/:n",async({params,set})=>{try{return await manager.getPullRequest(params.provider,decodeFqn(params.fqn),Number(params.n))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls",async({params,query,set})=>{try{let stateRaw=query.state,state=stateRaw==="open"||stateRaw==="closed"||stateRaw==="all"?stateRaw:void 0;return await manager.listPullRequests(params.provider,decodeFqn(params.fqn),{state,limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/issues",async({params,query,set})=>{try{let stateRaw=query.state,state=stateRaw==="open"||stateRaw==="closed"?stateRaw:void 0;return await manager.listIssues(params.provider,decodeFqn(params.fqn),{state,limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/labels",async({params,set})=>{try{return await manager.labelStats(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pipelines/analytics",async({params,set})=>{try{return await manager.pipelineAnalytics(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pipelines",async({params,set})=>{try{return await manager.listPipelines(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/runs/:id",async({params,set})=>{try{return await manager.getRun(params.provider,decodeFqn(params.fqn),params.id)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/runs",async({params,query,set})=>{try{return await manager.listRecentRuns(params.provider,decodeFqn(params.fqn),{limit:query.limit?Number(query.limit):void 0,branch:query.branch})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/environments",async({params,set})=>{try{return await manager.listEnvironments(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/deployments",async({params,query,set})=>{try{return await manager.listDeployments(params.provider,decodeFqn(params.fqn),query.env)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/security/alerts",async({params,query,set})=>{try{let kindRaw=query.kind,kind=kindRaw==="code-scan"||kindRaw==="secret-scan"||kindRaw==="dependency"||kindRaw==="advisory"?kindRaw:void 0;return await manager.listSecurityAlerts(params.provider,decodeFqn(params.fqn),{kind})}catch(e){return err(set,e)}}).get("/:provider/orgs/:org/rollup",async({params,set})=>{try{return await manager.orgRollup(params.provider,params.org)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/webhooks",async({params,set})=>{try{return await manager.listWebhooks(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}})}var PLUGIN_NAME="gitops",PLUGIN_VERSION="0.1.0",GITOPS_PROVIDERS=[{packageName:"@vibecontrols/vibe-plugin-gitops-github",pluginName:"gitops-github"},{packageName:"@vibecontrols/vibe-plugin-gitops-gitlab",pluginName:"gitops-gitlab"},{packageName:"@vibecontrols/vibe-plugin-gitops-bitbucket",pluginName:"gitops-bitbucket"},{packageName:"@vibecontrols/vibe-plugin-gitops-azdevops",pluginName:"gitops-azdevops"}],createPlugin=(_ctx)=>{let manager=new GitopsManager,telemetry=new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION),lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"gitops.meta.ready",onInit:(hostServices)=>{manager.init(hostServices),telemetry.emit("gitops.manager.ready")}});return{capabilities:{storage:"rw",broadcast:!0,audit:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"GitOps meta plugin \u2014 dispatches repo/PR/CI/security queries to registered providers (GitHub, GitLab, Bitbucket, Azure DevOps)",tags:["backend","cli","integration"],cliCommand:"gitops",apiPrefix:"/api/gitops",metaProviders:GITOPS_PROVIDERS,provisionProviders:(hostServices)=>provisionMetaProviders(hostServices,GITOPS_PROVIDERS),createRoutes:()=>createGitopsManagerRoutes(manager),onServerStart:lifecycle.onServerStart,onServerStop:lifecycle.onServerStop,onCliSetup:(program,hostServices)=>{registerGitopsCommands(program,hostServices)}}},src_default=createPlugin;export{src_default as default,createPlugin,GitopsManager,GitOpsError};
|
|
3
|
+
`);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)},onNuke:async(hostServices,ctx)=>{if(onNuke)return onNuke(hostServices,ctx)}}}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)}},BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};function currentSupportedOS(p=process.platform){if(p==="darwin")return"darwin";if(p==="win32")return"win32";return"linux"}async function provisionMetaProviders(host,metaProviders,platform2=currentSupportedOS()){let plugins=host.plugins;if(!plugins||!metaProviders?.length)return;let skip=plugins.skipInstall?.()??!1;for(let ref of metaProviders){if(!ref.defaultOn?.includes(platform2))continue;try{if(!skip)await plugins.install?.(ref.packageName,{refreshLatest:!0});if(await plugins.loadProvider?.(ref.packageName),await plugins.runPrereqs?.(ref.packageName,{skipInstall:skip}),ref.providerType&&ref.pluginName)await plugins.electDefault?.(ref.providerType,ref.pluginName)}catch{}}}import{existsSync,readFileSync}from"fs";import{join,resolve}from"path";var DEFAULT_AGENT_URL="http://localhost:3005";function agentBaseUrl(){return process.env.AGENT_BASE_URL??DEFAULT_AGENT_URL}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 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/gitops${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/gitops${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()}function registerGitopsCommands(program,_hostServices){let cmd=program.command("gitops").description("Manage GitOps across providers (GitHub, GitLab, Bitbucket, AzDO)"),providersCmd=cmd.command("providers").description("Manage GitOps providers");providersCmd.command("ls").description("List registered providers and their auth state").action(async()=>{let data=await apiGet("/providers");for(let p of data.providers){let star=p.isDefault?"* ":" ",auth=p.authenticated?"authed":"no-auth",ok=p.health.ok?"ok":`error (${p.health.message??""})`;console.log(`${star}${p.name} [${auth}, ${ok}]`)}}),providersCmd.command("set-default <name>").description("Set the active provider").action(async(name)=>{let res=await apiPost("/providers/active",{provider:name});console.log(JSON.stringify(res,null,2))});let authCmd=cmd.command("auth").description("Manage provider credentials");authCmd.command("set <provider> <token>").description("Save a PAT for the named provider").action(async(provider,token)=>{let res=await apiPost(`/${provider}/auth`,{kind:"pat",token});console.log(JSON.stringify(res,null,2))}),authCmd.command("validate <provider>").description("Validate stored credentials").action(async(provider)=>{let res=await apiGet(`/${provider}/auth/validate`);console.log(JSON.stringify(res,null,2))}),authCmd.command("revoke <provider>").description("Delete stored credentials").action(async(provider)=>{let res=await fetch(`${agentBaseUrl()}/api/gitops/${provider}/auth`,{method:"DELETE",headers:authHeaders()});console.log(`status=${res.status}`)}),cmd.command("repos <provider>").description("List repos for the named provider").option("--org <name>","Org / group / workspace name").option("--limit <n>","Maximum repos to fetch (default 30)").action(async(provider,opts)=>{let qs=new URLSearchParams;if(opts.org)qs.set("org",opts.org);if(opts.limit)qs.set("limit",String(opts.limit));let suffix=qs.toString()?`?${qs.toString()}`:"",data=await apiGet(`/${provider}/repos${suffix}`);for(let r of data.items)console.log(`${r.fqn} ${r.visibility} ${r.updatedAt}`)}),cmd.command("runs <provider> <fqn>").description("List recent CI runs for a repo").option("--branch <name>","Limit to a branch").option("--limit <n>","Maximum runs to fetch (default 20)").action(async(provider,fqn,opts)=>{let qs=new URLSearchParams;if(opts.branch)qs.set("branch",opts.branch);if(opts.limit)qs.set("limit",String(opts.limit));let suffix=qs.toString()?`?${qs.toString()}`:"",data=await apiGet(`/${provider}/repos/${encodeURIComponent(fqn)}/runs${suffix}`);for(let r of data){let dur=r.durationSeconds?`${r.durationSeconds}s`:"-";console.log(`${r.id} ${r.pipelineName} ${r.status} ${r.conclusion??""} ${dur} ${r.url}`)}}),cmd.command("analytics <provider> <fqn>").description("Pipeline duration + success-rate analytics for a repo").action(async(provider,fqn)=>{let data=await apiGet(`/${provider}/repos/${encodeURIComponent(fqn)}/pipelines/analytics`);console.log(JSON.stringify(data,null,2))}),cmd.command("doctor").description("Provider health summary").action(async()=>{let data=await apiGet("/providers");for(let p of data.providers)console.log(`${p.name}: ${p.health.ok?"ok":`fail - ${p.health.message??"?"}`}`)})}var DEFAULT_PROVIDER_CONFIG_KEY="provider:default:gitops";class GitOpsError extends Error{code;upstreamStatus;retryAfterSeconds;details;constructor(code,message,opts){super(message);this.name="GitOpsError",this.code=code,this.upstreamStatus=opts?.upstreamStatus,this.retryAfterSeconds=opts?.retryAfterSeconds,this.details=opts?.details}toJSON(){return{error:this.message,code:this.code,upstreamStatus:this.upstreamStatus,retryAfterSeconds:this.retryAfterSeconds,details:this.details}}static fromUnknown(err){if(err instanceof GitOpsError)return err;if(err instanceof Error)return new GitOpsError("UPSTREAM",err.message);return new GitOpsError("UPSTREAM",String(err))}}function httpStatusForCode(code){switch(code){case"AUTH":return 401;case"FORBIDDEN":return 403;case"NOT_FOUND":return 404;case"RATE_LIMITED":return 429;case"UPSTREAM":return 502;case"INVALID":return 400}}var LOG_SOURCE="gitops-manager";function providerNameFromRef(ref){if(ref.pluginName)return ref.pluginName.replace(/^gitops-/,"");return ref.packageName.replace(/^.*vibe-plugin-gitops-/,"")}class GitopsManager{registry;host;log=new BoundLogger(void 0,LOG_SOURCE);providerRefs=[];init(host,providerRefs){this.host=host,this.registry=host.serviceRegistry,this.providerRefs=providerRefs??[],this.log=new BoundLogger(host.logger,LOG_SOURCE),this.log.info("GitOps manager initialised")}async ensureProvider(name){let wanted=name.trim();if(!this.registry)throw new GitOpsError("UPSTREAM","Service registry not available");let existing=this.registry.getProviderByName("gitops",wanted);if(existing)return existing;let ref=this.providerRefs.find((r)=>providerNameFromRef(r)===wanted);if(!ref)throw new GitOpsError("NOT_FOUND",`Gitops provider '${wanted}' is not registered`);let plugins=this.host?.plugins;if(!plugins?.loadProvider)throw new GitOpsError("NOT_FOUND",`Gitops provider '${wanted}' is not registered (plugin host unavailable for on-demand install)`);this.log.info(`On-demand installing gitops provider '${wanted}' (${ref.packageName})`);let skip=plugins.skipInstall?.()??!1;try{if(!skip)await plugins.install?.(ref.packageName,{refreshLatest:!0});await plugins.loadProvider(ref.packageName),await plugins.runPrereqs?.(ref.packageName,{skipInstall:skip})}catch(err){throw new GitOpsError("UPSTREAM",`Failed to install gitops provider '${wanted}': ${err instanceof Error?err.message:String(err)}`)}let installed=this.registry.getProviderByName("gitops",wanted);if(!installed)throw new GitOpsError("NOT_FOUND",`Gitops provider '${wanted}' did not register after on-demand install`);return installed}resolveProvider(name){if(!this.registry)throw new GitOpsError("UPSTREAM","Service registry not available");let explicit=name?.trim();if(explicit){let p2=this.registry.getProviderByName("gitops",explicit);if(!p2)throw new GitOpsError("NOT_FOUND",`Gitops provider '${explicit}' is not registered`);return p2}let fallback=this.defaultProviderNameSync();if(!fallback)throw new GitOpsError("INVALID","No default gitops provider configured and none specified");let p=this.registry.getProviderByName("gitops",fallback);if(!p)throw new GitOpsError("NOT_FOUND",`Default gitops provider '${fallback}' is not registered`);return p}defaultProviderNameSync(){let entries=this.registry?.listProvidersForType("gitops")??[],flagged=entries.find((e)=>e.isDefault);if(flagged)return flagged.pluginName;if(entries.length===1)return entries[0].pluginName;return}async defaultProviderName(){let fromConfig=await this.host?.getConfig?.(DEFAULT_PROVIDER_CONFIG_KEY);if(fromConfig)return fromConfig;return this.defaultProviderNameSync()}listProviderEntries(){return this.registry?.listProvidersForType("gitops")??[]}async listProviders(){let entries=this.listProviderEntries(),out=[];for(let entry of entries){let p=this.registry?.getProviderByName("gitops",entry.pluginName);if(!p){out.push({name:entry.pluginName,isDefault:entry.isDefault,registered:!1,authenticated:!1,health:{ok:!1,message:"unavailable"}});continue}let health;try{health=await p.healthCheck()}catch(err){health={ok:!1,message:err instanceof Error?err.message:String(err)}}let authed=!1;try{authed=(await p.validateCredentials()).ok}catch{}out.push({name:entry.pluginName.replace(/^gitops-/,""),isDefault:entry.isDefault,registered:!0,authenticated:authed,health})}return out}async getActiveProvider(){return{name:await this.defaultProviderName()}}async setActiveProvider(name){if(await this.ensureProvider(name),this.registry?.setProviderDefault)this.registry.setProviderDefault("gitops",name);let hostWithSetConfig=this.host;if(hostWithSetConfig?.setConfig)await hostWithSetConfig.setConfig(DEFAULT_PROVIDER_CONFIG_KEY,name);this.log.info(`Default gitops provider set to ${name}`)}async saveCredentials(provider,input){await(await this.ensureProvider(provider)).saveCredentials(input)}async validateCredentials(provider){return this.resolveProvider(provider).validateCredentials()}async rotateCredentials(provider,input){return this.resolveProvider(provider).rotateCredentials(input)}async revokeCredentials(provider){await this.resolveProvider(provider).revokeCredentials()}async healthCheck(provider){return this.resolveProvider(provider).healthCheck()}async listRepos(provider,opts){return this.resolveProvider(provider).listRepos(opts)}async getRepo(provider,fqn){return this.resolveProvider(provider).getRepo(fqn)}async listBranches(provider,fqn){return this.resolveProvider(provider).listBranches(fqn)}async listLanguages(provider,fqn){return this.resolveProvider(provider).listLanguages(fqn)}async listContributors(provider,fqn,opts){return this.resolveProvider(provider).listContributors(fqn,opts)}async listPullRequests(provider,fqn,opts){return this.resolveProvider(provider).listPullRequests(fqn,opts)}async getPullRequest(provider,fqn,id){return this.resolveProvider(provider).getPullRequest(fqn,id)}async pullRequestAnalytics(provider,fqn){return this.resolveProvider(provider).pullRequestAnalytics(fqn)}async listIssues(provider,fqn,opts){return this.resolveProvider(provider).listIssues(fqn,opts)}async labelStats(provider,fqn){return this.resolveProvider(provider).labelStats(fqn)}async listPipelines(provider,fqn){return this.resolveProvider(provider).listPipelines(fqn)}async listRecentRuns(provider,fqn,opts){return this.resolveProvider(provider).listRecentRuns(fqn,opts)}async getRun(provider,fqn,runId){return this.resolveProvider(provider).getRun(fqn,runId)}async pipelineAnalytics(provider,fqn){return this.resolveProvider(provider).pipelineAnalytics(fqn)}async listEnvironments(provider,fqn){return this.resolveProvider(provider).listEnvironments?.(fqn)??[]}async listDeployments(provider,fqn,env){return this.resolveProvider(provider).listDeployments?.(fqn,env)??[]}async listSecurityAlerts(provider,fqn,opts){return this.resolveProvider(provider).listSecurityAlerts(fqn,opts)}async orgRollup(provider,org){return this.resolveProvider(provider).orgRollup(org)}async listWebhooks(provider,fqn){return this.resolveProvider(provider).listWebhooks?.(fqn)??[]}}import{Elysia as Elysia2,t}from"elysia";function decodeFqn(raw){try{let decoded=decodeURIComponent(raw);if(decoded.includes("/"))return decoded}catch{}try{let b64=raw.replace(/-/g,"+").replace(/_/g,"/"),padded=b64+"===".slice((b64.length+3)%4);return atob(padded)}catch{return raw}}function err(set,e){let g=GitOpsError.fromUnknown(e);return set.status=httpStatusForCode(g.code),g.toJSON()}function createGitopsManagerRoutes(manager){return new Elysia2().get("/providers",async({set})=>{try{return{providers:await manager.listProviders()}}catch(e){return err(set,e)}}).get("/providers/active",async({set})=>{try{return await manager.getActiveProvider()}catch(e){return err(set,e)}}).post("/providers/active",async({body,set})=>{try{return await manager.setActiveProvider(body.provider),{success:!0,provider:body.provider}}catch(e){return err(set,e)}},{body:t.Object({provider:t.String()})}).post("/:provider/auth",async({params,body,set})=>{try{await manager.saveCredentials(params.provider,body);let validation=await manager.validateCredentials(params.provider);if(!validation.ok)set.status=400;return validation}catch(e){return err(set,e)}},{body:t.Object({kind:t.Union([t.Literal("pat"),t.Literal("oauth"),t.Literal("app")]),token:t.String(),meta:t.Optional(t.Record(t.String(),t.String()))})}).get("/:provider/auth/validate",async({params,set})=>{try{return await manager.validateCredentials(params.provider)}catch(e){return err(set,e)}}).post("/:provider/auth/rotate",async({params,body,set})=>{try{return await manager.rotateCredentials(params.provider,body)}catch(e){return err(set,e)}},{body:t.Object({kind:t.Union([t.Literal("pat"),t.Literal("oauth"),t.Literal("app")]),token:t.String(),meta:t.Optional(t.Record(t.String(),t.String()))})}).delete("/:provider/auth",async({params,set})=>{try{return await manager.revokeCredentials(params.provider),{success:!0}}catch(e){return err(set,e)}}).get("/:provider/health",async({params,set})=>{try{return await manager.healthCheck(params.provider)}catch(e){return err(set,e)}}).get("/:provider/repos",async({params,query,set})=>{try{return await manager.listRepos(params.provider,{org:query.org,limit:query.limit?Number(query.limit):void 0,cursor:query.cursor})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn",async({params,set})=>{try{return await manager.getRepo(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/branches",async({params,set})=>{try{return await manager.listBranches(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/languages",async({params,set})=>{try{return await manager.listLanguages(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/contributors",async({params,query,set})=>{try{return await manager.listContributors(params.provider,decodeFqn(params.fqn),{limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls/analytics",async({params,set})=>{try{return await manager.pullRequestAnalytics(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls/:n",async({params,set})=>{try{return await manager.getPullRequest(params.provider,decodeFqn(params.fqn),Number(params.n))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pulls",async({params,query,set})=>{try{let stateRaw=query.state,state=stateRaw==="open"||stateRaw==="closed"||stateRaw==="all"?stateRaw:void 0;return await manager.listPullRequests(params.provider,decodeFqn(params.fqn),{state,limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/issues",async({params,query,set})=>{try{let stateRaw=query.state,state=stateRaw==="open"||stateRaw==="closed"?stateRaw:void 0;return await manager.listIssues(params.provider,decodeFqn(params.fqn),{state,limit:query.limit?Number(query.limit):void 0})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/labels",async({params,set})=>{try{return await manager.labelStats(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pipelines/analytics",async({params,set})=>{try{return await manager.pipelineAnalytics(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/pipelines",async({params,set})=>{try{return await manager.listPipelines(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/runs/:id",async({params,set})=>{try{return await manager.getRun(params.provider,decodeFqn(params.fqn),params.id)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/runs",async({params,query,set})=>{try{return await manager.listRecentRuns(params.provider,decodeFqn(params.fqn),{limit:query.limit?Number(query.limit):void 0,branch:query.branch})}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/environments",async({params,set})=>{try{return await manager.listEnvironments(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/deployments",async({params,query,set})=>{try{return await manager.listDeployments(params.provider,decodeFqn(params.fqn),query.env)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/security/alerts",async({params,query,set})=>{try{let kindRaw=query.kind,kind=kindRaw==="code-scan"||kindRaw==="secret-scan"||kindRaw==="dependency"||kindRaw==="advisory"?kindRaw:void 0;return await manager.listSecurityAlerts(params.provider,decodeFqn(params.fqn),{kind})}catch(e){return err(set,e)}}).get("/:provider/orgs/:org/rollup",async({params,set})=>{try{return await manager.orgRollup(params.provider,params.org)}catch(e){return err(set,e)}}).get("/:provider/repos/:fqn/webhooks",async({params,set})=>{try{return await manager.listWebhooks(params.provider,decodeFqn(params.fqn))}catch(e){return err(set,e)}})}var PLUGIN_NAME="gitops",PLUGIN_VERSION="0.1.0",GITOPS_PROVIDERS=[{packageName:"@vibecontrols/vibe-plugin-gitops-github",pluginName:"gitops-github"},{packageName:"@vibecontrols/vibe-plugin-gitops-gitlab",pluginName:"gitops-gitlab"},{packageName:"@vibecontrols/vibe-plugin-gitops-bitbucket",pluginName:"gitops-bitbucket"},{packageName:"@vibecontrols/vibe-plugin-gitops-azdevops",pluginName:"gitops-azdevops"}],createPlugin=(_ctx)=>{let manager=new GitopsManager,telemetry=new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION),lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"gitops.meta.ready",onInit:(hostServices)=>{manager.init(hostServices,GITOPS_PROVIDERS),telemetry.emit("gitops.manager.ready")}});return{capabilities:{storage:"rw",broadcast:!0,audit:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"GitOps meta plugin \u2014 dispatches repo/PR/CI/security queries to registered providers (GitHub, GitLab, Bitbucket, Azure DevOps)",tags:["backend","cli","integration"],cliCommand:"gitops",apiPrefix:"/api/gitops",metaProviders:GITOPS_PROVIDERS,provisionProviders:(hostServices)=>provisionMetaProviders(hostServices,GITOPS_PROVIDERS),createRoutes:()=>createGitopsManagerRoutes(manager),onServerStart:lifecycle.onServerStart,onServerStop:lifecycle.onServerStop,onCliSetup:(program,hostServices)=>{registerGitopsCommands(program,hostServices)}}},src_default=createPlugin;export{src_default as default,createPlugin,GitopsManager,GitOpsError};
|
package/dist/manager.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* configured default provider when no explicit name is given. Mirrors the
|
|
7
7
|
* shape of TunnelManager in vibe-plugin-tunnel.
|
|
8
8
|
*/
|
|
9
|
-
import type { HostServices } from "@vibecontrols/plugin-sdk/contract";
|
|
9
|
+
import type { HostServices, MetaProviderRef } from "@vibecontrols/plugin-sdk/contract";
|
|
10
10
|
import { type AuthInput, type AuthValidation, type Branch, type Contributor, type Deployment, type Environment, type HealthSnapshot, type IssueSummary, type NormalisedRepo, type OrgRollup, type Pipeline, type PipelineAnalytics, type PipelineRun, type PullRequest, type PullRequestAnalytics, type RepoFqn, type RepoPage, type SecurityAlert, type Webhook } from "./types.js";
|
|
11
11
|
export interface ProviderSnapshot {
|
|
12
12
|
name: string;
|
|
@@ -19,7 +19,33 @@ export declare class GitopsManager {
|
|
|
19
19
|
private registry?;
|
|
20
20
|
private host?;
|
|
21
21
|
private log;
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Known provider package refs (github/gitlab/bitbucket/azdevops), passed in
|
|
24
|
+
* from the meta plugin's `GITOPS_PROVIDERS`. Used to install a provider on
|
|
25
|
+
* demand when the operator connects it but it was skipped at boot (opt-in
|
|
26
|
+
* providers are NOT auto-provisioned to keep the agent lean).
|
|
27
|
+
*/
|
|
28
|
+
private providerRefs;
|
|
29
|
+
init(host: HostServices, providerRefs?: ReadonlyArray<MetaProviderRef>): void;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a provider, installing it on demand if it is a known opt-in
|
|
32
|
+
* provider that is not registered yet.
|
|
33
|
+
*
|
|
34
|
+
* `provisionMetaProviders` deliberately SKIPS opt-in providers at boot (none
|
|
35
|
+
* of the gitops providers declare `defaultOn`, so the agent stays lean and
|
|
36
|
+
* doesn't pull all four SCM integrations). That means the FIRST time an
|
|
37
|
+
* operator connects a provider via the GitOps tab (`saveCredentials` /
|
|
38
|
+
* `setActiveProvider`), it isn't in the service registry yet and a plain
|
|
39
|
+
* lookup throws `NOT_FOUND`. Rather than surface that to the user, we
|
|
40
|
+
* transparently run the same install → load → prereqs sequence
|
|
41
|
+
* `provisionMetaProviders` would have, then re-resolve — "behind the scenes".
|
|
42
|
+
*
|
|
43
|
+
* The install is one-time: once loaded, the provider plugin is persisted to
|
|
44
|
+
* the agent's plugin manifest and re-registers on every subsequent boot, so
|
|
45
|
+
* later calls hit the fast `getProviderByName` path. Unknown provider names
|
|
46
|
+
* (not in `providerRefs`) still throw `NOT_FOUND`.
|
|
47
|
+
*/
|
|
48
|
+
private ensureProvider;
|
|
23
49
|
private resolveProvider;
|
|
24
50
|
private defaultProviderNameSync;
|
|
25
51
|
private defaultProviderName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecontrols/vibe-plugin-gitops",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.731.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
21
21
|
"format": "bunx prettier . --write",
|
|
22
22
|
"format:check": "bunx prettier . --check",
|
|
23
|
-
"type:check": "tsc --noEmit",
|
|
23
|
+
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
24
24
|
"test": "bun test",
|
|
25
25
|
"clean": "rimraf dist",
|
|
26
26
|
"prebuild": "bun run clean",
|
|
27
27
|
"prepublishOnly": "bun run build",
|
|
28
|
-
"sanity": "bun run format:check && bun run lint:sanity && bun run type:check && bun run build"
|
|
28
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run format:check && bun run lint:sanity && bun run type:check && bun run build'"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@vibecontrols/plugin-sdk": "2026.601.1",
|