gibil 0.4.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +37 -33
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1,42 +1,46 @@
1
1
  #!/usr/bin/env node
2
- var Pr=Object.defineProperty;var z=(t,e)=>()=>(t&&(e=t(t=0)),e);var re=(t,e)=>{for(var n in e)Pr(t,n,{get:e[n],enumerable:!0})};import $e from"picocolors";function Qe(t,e){let n=Math.max(t.length+4,...e.map(l=>It(l).length+4)),o=`${f("\u256D")}${f("\u2500".repeat(n))}${f("\u256E")}`,i=`${f("\u2570")}${f("\u2500".repeat(n))}${f("\u256F")}`,s=`${f("\u2502")} ${R} ${h(t)}${" ".repeat(n-It(t).length-4)}${f("\u2502")}`,a=`${f("\u251C")}${f("\u2500".repeat(n))}${f("\u2524")}`,c=e.map(l=>{let d=n-It(l).length-2;return`${f("\u2502")} ${l}${" ".repeat(Math.max(0,d))}${f("\u2502")}`});return[o,s,a,...c,i].join(`
3
- `)}function It(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var oe,kr,X,Er,f,h,R,A,Le,en,Xe,tn,nn,Qt,De,T,O=z(()=>{"use strict";oe=t=>$e.red(t),kr=t=>$e.yellow(t),X=t=>$e.green(t),Er=t=>$e.red(t),f=t=>$e.dim(t),h=t=>$e.bold(t),R="\u{1F98E}",A=X("\u2713"),Le=Er("\u2716"),en=kr("\u26A0"),Xe="\u{12248}",tn=`
4
- ${oe(" /\\")}
5
- ${oe(" / \\")}
6
- ${oe(" / \u{1F525} \\")}
7
- ${oe(" / \\")}
8
- ${f(" ~~~~~~~~")}
9
- ${h(" g i b i l")} ${f(Xe)}
10
- `,nn=`${R} ${h("gibil")} ${f(Xe)}`,Qt=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],De=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=oe(Qt[this.frame%Qt.length]);process.stderr.write(`\r ${e} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
2
+ var Or=Object.defineProperty;var D=(t,e)=>()=>(t&&(e=t(t=0)),e);var ie=(t,e)=>{for(var n in e)Or(t,n,{get:e[n],enumerable:!0})};import Se from"picocolors";function et(t,e){let n=Math.max(t.length+4,...e.map(c=>kt(c).length+4)),r=`${m("\u256D")}${m("\u2500".repeat(n))}${m("\u256E")}`,i=`${m("\u2570")}${m("\u2500".repeat(n))}${m("\u256F")}`,s=`${m("\u2502")} ${H} ${h(t)}${" ".repeat(n-kt(t).length-4)}${m("\u2502")}`,a=`${m("\u251C")}${m("\u2500".repeat(n))}${m("\u2524")}`,l=e.map(c=>{let u=n-kt(c).length-2;return`${m("\u2502")} ${c}${" ".repeat(Math.max(0,u))}${m("\u2502")}`});return[r,s,a,...l,i].join(`
3
+ `)}function kt(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var se,Hr,ee,Mr,m,h,H,N,Le,rn,Qe,on,sn,nn,De,C,R=D(()=>{"use strict";se=t=>Se.red(t),Hr=t=>Se.yellow(t),ee=t=>Se.green(t),Mr=t=>Se.red(t),m=t=>Se.dim(t),h=t=>Se.bold(t),H="\u{1F98E}",N=ee("\u2713"),Le=Mr("\u2716"),rn=Hr("\u26A0"),Qe="\u{12248}",on=`
4
+ ${se(" /\\")}
5
+ ${se(" / \\")}
6
+ ${se(" / \u{1F525} \\")}
7
+ ${se(" / \\")}
8
+ ${m(" ~~~~~~~~")}
9
+ ${h(" g i b i l")} ${m(Qe)}
10
+ `,sn=`${H} ${h("gibil")} ${m(Qe)}`,nn=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],De=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=se(nn[this.frame%nn.length]);process.stderr.write(`\r ${e} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
11
11
  `),this)}update(e){this.text=e,process.stderr.isTTY||process.stderr.write(` ${e}
12
- `)}succeed(e){this.stop(),process.stderr.write(`\r ${A} ${e??this.text}
12
+ `)}succeed(e){this.stop(),process.stderr.write(`\r ${N} ${e??this.text}
13
13
  `)}fail(e){this.stop(),process.stderr.write(`\r ${Le} ${e??this.text}
14
- `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};T={welcome:`${R} Your first fire. Welcome to Gibil.`,noInstances:`${R} No fires burning. Gibil sleeps.`,destroyAll:`${R} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${R} "${t}" \u2014 fire out.`,authSuccess:`${R} Logged in. The forge is yours.`,authLogout:`${R} Logged out. The forge cools.`,createReady:(t,e)=>`${R} "${t}" forged ${f(`(${e}s)`)}`,fleetReady:(t,e)=>`${R} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${R} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${R} The forge is ready. Run ${h("gibil create")} to light your first fire.`,setupNeeded:`${R} No forge configured. Run ${h("gibil init")} to get started.`}});function I(t){Pt=t}function ie(t){return Pt&&t!=="error"?!1:rn[t]>=rn[Tr]}var Tr,Pt,rn,r,E=z(()=>{"use strict";O();Tr="info",Pt=!1,rn={debug:0,info:1,warn:2,error:3,silent:4};r={debug(t,...e){ie("debug")&&console.debug(`${f("[debug]")} ${t}`,...e)},info(t,...e){ie("info")&&console.log(t,...e)},warn(t,...e){ie("warn")&&console.warn(`${en} ${t}`,...e)},error(t,...e){ie("error")&&console.error(`${Le} ${t}`,...e)},success(t){ie("info")&&console.log(`${A} ${t}`)},step(t){ie("info")&&console.log(` ${f("\u203A")} ${t}`)},flame(t){ie("info")&&console.log(t)},detail(t,e){ie("info")&&console.log(` ${f(t+":")} ${e}`)},spin(t){return Pt?new De(t):new De(t).start()},json(t){console.log(JSON.stringify(t,null,2))}}});var on={};re(on,{HETZNER_META:()=>et,PROVIDER_CATALOG:()=>kt,VULTR_META:()=>tt});var et,tt,kt,ze=z(()=>{"use strict";et={name:"hetzner",label:"Hetzner Cloud",defaultRegion:"fsn1",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:40,nativeType:"cax11"},{name:"medium",vcpu:4,ramGb:8,diskGb:80,nativeType:"cax21"},{name:"large",vcpu:8,ramGb:16,diskGb:160,nativeType:"cax31"}]},tt={name:"vultr",label:"Vultr",defaultRegion:"nrt",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:80,nativeType:"vc2-2c-4gb"},{name:"medium",vcpu:4,ramGb:8,diskGb:160,nativeType:"vc2-4c-8gb"},{name:"large",vcpu:6,ramGb:16,diskGb:320,nativeType:"vc2-6c-16gb"}]},kt={hetzner:et,vultr:tt}});import{homedir as Cr}from"os";import{join as ae,resolve as jr}from"path";import{existsSync as Ar}from"fs";function nt(){let t=process.argv[1];if(t){let e=jr(t);if(Ar(e))return{command:process.execPath,args:[e,"mcp"]}}return{command:"gibil",args:["mcp"]}}var se,P,B=z(()=>{"use strict";se=process.env.GIBIL_HOME??ae(Cr(),".gibil"),P={root:se,instances:ae(se,"instances"),keys:ae(se,"keys"),jobs:ae(se,"jobs"),instanceFile:t=>ae(se,"instances",`${t}.json`),keyDir:t=>ae(se,"keys",t),privateKey:t=>ae(se,"keys",t,"id_ed25519"),publicKey:t=>ae(se,"keys",t,"id_ed25519.pub")}});var He={};re(He,{clearApiKey:()=>Ct,fetchUsage:()=>Rt,getApiKey:()=>M,getApiUrl:()=>Lr,getApiUrlFromConfig:()=>rt,getDefaultAgent:()=>Ge,getDefaultProvider:()=>jt,getHetznerToken:()=>Kr,getProviderToken:()=>xe,getServerDefaults:()=>Gr,saveApiKey:()=>Tt,saveDefaultAgent:()=>ot,saveHetznerToken:()=>At,saveProviderToken:()=>Ke,saveServerDefaults:()=>Nt,setDefaultProvider:()=>_e,trackUsage:()=>Q,verifyApiKey:()=>ce});import{readFile as Nr,writeFile as Rr,mkdir as Or}from"fs/promises";import{existsSync as Mr}from"fs";import{join as Dr}from"path";async function K(){if(!Mr(Et))return{};let t=await Nr(Et,"utf-8"),e=JSON.parse(t);return e.hetzner_token&&!e.providers?.hetzner?.token&&(e.providers={...e.providers??{},hetzner:{token:e.hetzner_token,default_server_type:e.default_server_type,default_location:e.default_location,...e.providers?.hetzner??{}}}),e}async function Se(t){await Or(P.root,{recursive:!0,mode:448});let e={...t};e.providers?.hetzner&&(delete e.hetzner_token,delete e.default_server_type,delete e.default_location),await Rr(Et,JSON.stringify(e,null,2),{mode:384})}async function Tt(t){let e=await K();e.api_key=t,await Se(e)}async function M(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await K()).api_key??null}async function Ct(){let t=await K();delete t.api_key,await Se(t)}function Lr(){return process.env.GIBIL_API_URL??sn}async function rt(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await K()).api_url??sn}async function xe(t){let e=process.env[zr[t]];return e||((await K()).providers?.[t]?.token??null)}async function Ke(t,e){let n=await K();n.providers||(n.providers={}),n.providers[t]={...n.providers[t]??{},token:e},await Se(n)}async function jt(){return(await K()).default_provider??"hetzner"}async function _e(t){let e=await K();e.default_provider=t,await Se(e)}async function At(t){await Ke("hetzner",t)}async function Kr(){return xe("hetzner")}async function Nt(t,e){let n=await K();n.providers||(n.providers={}),n.providers.hetzner={...n.providers.hetzner??{},default_server_type:t,default_location:e},await Se(n)}async function Gr(){let t=await K(),e=t.providers?.hetzner;return{serverType:e?.default_server_type??t.default_server_type??"cax11",location:e?.default_location??t.default_location??"fsn1"}}async function ot(t){let e=await K();t?e.default_agent=t:delete e.default_agent,await Se(e)}async function Ge(){return(await K()).default_agent??null}async function ce(t){let e=await rt(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let o=await n.text();throw new Error(`API error (${n.status}): ${o}`)}return await n.json()}async function Q(t,e,n,o){let i=await rt(),s=await fetch(`${i}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:o})});if(s.status===429)throw new Error("Usage limit reached. Please try again later or contact support.");if(!s.ok){let a=await s.text();throw new Error(`Usage tracking failed (${s.status}): ${a}`)}}async function Rt(t){let e=await rt(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let o=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${o}`)}return await n.json()}var Et,sn,zr,G=z(()=>{"use strict";B();Et=Dr(P.root,"config.json"),sn="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1";zr={hetzner:"HETZNER_API_TOKEN",vultr:"VULTR_API_KEY"}});var an={};re(an,{HetznerProvider:()=>Mt});function Ot(t){return{id:t.id,name:t.name,status:t.status,ipv4:t.public_net.ipv4.ip,ipv6:t.public_net.ipv6.ip,serverType:t.server_type?.name??"unknown",location:t.datacenter?.location?.name??"unknown",labels:t.labels,created:t.created}}function Fr(t){return{id:t.id,name:t.name,fingerprint:t.fingerprint??""}}var Hr,Mt,cn=z(()=>{"use strict";E();ze();Hr="https://api.hetzner.cloud/v1";Mt=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(G(),He)),o=e??await n();if(!o)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(o)}async request(e,n,o){let i=`${Hr}${n}`;r.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:o?JSON.stringify(o):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),c;try{c=JSON.parse(a).error?.message??a}catch{c=a}let l="";throw s.status===401||s.status===403?l=`
15
- Your Hetzner token may be invalid or expired. Run: gibil init --force`:s.status===409&&c.includes("name")?l=`
16
- A server with this name already exists. Try a different --name or run: gibil destroy <name>`:s.status===422&&(c.includes("location")||c.includes("server_type"))?l=`
17
- This server type may not be available in your region. Run: gibil init --force`:s.status===429&&(l=`
18
- Rate limited by Hetzner. Wait a moment and retry your command.`),new Error(`Hetzner API error (${s.status}): ${c}${l}`)}return s.status===204?{}:await s.json()}async createServer(e,n,o,i,s){if(!i||!s){let{getServerDefaults:u}=await Promise.resolve().then(()=>(G(),He)),m=await u();i=i??m.serverType,s=s??m.location}if(i.startsWith("cax")&&!["fsn1","nbg1"].includes(s))throw new Error(`ARM server type "${i}" is not available in "${s}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let l=typeof n=="string"?parseInt(n,10):n,d={name:e,server_type:i,image:"ubuntu-24.04",ssh_keys:[l],labels:{gibil:"true","gibil-name":e},location:s};r.debug(`createServer payload: ${JSON.stringify({name:e,server_type:i,image:"ubuntu-24.04",location:s})}`),o&&(d.user_data=o);try{let u=await this.request("POST","/servers",d);return Ot(u.server)}catch(u){let m=`(server_type=${i}, location=${s}). Try a different --server-type or --location.`;throw u instanceof Error?new Error(`${u.message} ${m}`):u}}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){let n=await this.request("GET",`/servers/${e}`);return Ot(n.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(Ot)}async waitForReady(e,n=12e4){let o=Date.now(),i=3e3;for(;Date.now()-o<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;r.debug(`Server ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let o=await this.request("POST","/ssh_keys",{name:e,public_key:n});return Fr(o.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}sizes(){return et.sizes}}});var un={};re(un,{buildAffiliateNudgeLine:()=>Jr,buildAffiliateProviderNudge:()=>Dt,getAffiliateProgram:()=>it,getAffiliateProgramNames:()=>Br});function Ur(){let t=process.env.GIBIL_NO_REFERRAL?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}function it(t){return Ur()?null:ln.programs[t]??null}function Br(){return Object.keys(ln.programs)}function Jr(t){return t?`
14
+ `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};C={welcome:`${H} Your first fire. Welcome to Gibil.`,noInstances:`${H} No fires burning. Gibil sleeps.`,destroyAll:`${H} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${H} "${t}" \u2014 fire out.`,authSuccess:`${H} Logged in. The forge is yours.`,authLogout:`${H} Logged out. The forge cools.`,createReady:(t,e)=>`${H} "${t}" forged ${m(`(${e}s)`)}`,fleetReady:(t,e)=>`${H} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${H} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${H} The forge is ready. Run ${h("gibil create")} to light your first fire.`,setupNeeded:`${H} No forge configured. Run ${h("gibil init")} to get started.`}});function I(t){It=t}function ae(t){return It&&t!=="error"?!1:an[t]>=an[Kr]}var Kr,It,an,o,E=D(()=>{"use strict";R();Kr="info",It=!1,an={debug:0,info:1,warn:2,error:3,silent:4};o={debug(t,...e){ae("debug")&&console.debug(`${m("[debug]")} ${t}`,...e)},info(t,...e){ae("info")&&console.log(t,...e)},warn(t,...e){ae("warn")&&console.warn(`${rn} ${t}`,...e)},error(t,...e){ae("error")&&console.error(`${Le} ${t}`,...e)},success(t){ae("info")&&console.log(`${N} ${t}`)},step(t){ae("info")&&console.log(` ${m("\u203A")} ${t}`)},flame(t){ae("info")&&console.log(t)},detail(t,e){ae("info")&&console.log(` ${m(t+":")} ${e}`)},spin(t){return It?new De(t):new De(t).start()},json(t){console.log(JSON.stringify(t,null,2))}}});var cn={};ie(cn,{HETZNER_META:()=>tt,PROVIDER_CATALOG:()=>Et,VULTR_META:()=>nt});var tt,nt,Et,ze=D(()=>{"use strict";tt={name:"hetzner",label:"Hetzner Cloud",defaultRegion:"fsn1",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:40,nativeType:"cax11"},{name:"medium",vcpu:4,ramGb:8,diskGb:80,nativeType:"cax21"},{name:"large",vcpu:8,ramGb:16,diskGb:160,nativeType:"cax31"}]},nt={name:"vultr",label:"Vultr",defaultRegion:"nrt",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:80,nativeType:"vc2-2c-4gb"},{name:"medium",vcpu:4,ramGb:8,diskGb:160,nativeType:"vc2-4c-8gb"},{name:"large",vcpu:6,ramGb:16,diskGb:320,nativeType:"vc2-6c-16gb"}]},Et={hetzner:tt,vultr:nt}});import{homedir as Dr}from"os";import{join as Z,resolve as Lr}from"path";import{existsSync as zr}from"fs";function W(){return process.env.GIBIL_HOME??Z(Dr(),".gibil")}function rt(){let t=process.argv[1];if(t){let e=Lr(t);if(zr(e))return{command:process.execPath,args:[e,"mcp"]}}return{command:"gibil",args:["mcp"]}}var P,L=D(()=>{"use strict";P={get root(){return W()},get instances(){return Z(W(),"instances")},get keys(){return Z(W(),"keys")},get jobs(){return Z(W(),"jobs")},get knownHostsDir(){return Z(W(),"known_hosts")},instanceFile:t=>Z(W(),"instances",`${t}.json`),keyDir:t=>Z(W(),"keys",t),privateKey:t=>Z(W(),"keys",t,"id_ed25519"),publicKey:t=>Z(W(),"keys",t,"id_ed25519.pub"),knownHostsFile:t=>Z(W(),"known_hosts",t)}});var Ue={};ie(Ue,{clearApiKey:()=>Ct,fetchUsage:()=>Rt,getApiKey:()=>M,getApiUrl:()=>Vr,getApiUrlFromConfig:()=>ot,getDefaultAgent:()=>Fe,getDefaultProvider:()=>At,getHetznerToken:()=>Yr,getProviderToken:()=>_e,getServerDefaults:()=>Wr,saveApiKey:()=>Tt,saveDefaultAgent:()=>it,saveHetznerToken:()=>jt,saveProviderToken:()=>Ge,saveServerDefaults:()=>Nt,setDefaultProvider:()=>Pe,trackUsage:()=>te,verifyApiKey:()=>ce});import{readFile as Gr,writeFile as Fr,mkdir as Ur}from"fs/promises";import{existsSync as Br}from"fs";import{join as Jr}from"path";function ln(){return Jr(P.root,"config.json")}async function z(){let t=ln();if(!Br(t))return{};let e=await Gr(t,"utf-8"),n=JSON.parse(e);return n.hetzner_token&&!n.providers?.hetzner?.token&&(n.providers={...n.providers??{},hetzner:{token:n.hetzner_token,default_server_type:n.default_server_type,default_location:n.default_location,...n.providers?.hetzner??{}}}),n}async function xe(t){await Ur(P.root,{recursive:!0,mode:448});let e={...t};e.providers?.hetzner&&(delete e.hetzner_token,delete e.default_server_type,delete e.default_location),await Fr(ln(),JSON.stringify(e,null,2),{mode:384})}async function Tt(t){let e=await z();e.api_key=t,await xe(e)}async function M(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await z()).api_key??null}async function Ct(){let t=await z();delete t.api_key,await xe(t)}function Vr(){return process.env.GIBIL_API_URL??un}async function ot(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await z()).api_url??un}async function _e(t){let e=process.env[qr[t]];return e||((await z()).providers?.[t]?.token??null)}async function Ge(t,e){let n=await z();n.providers||(n.providers={}),n.providers[t]={...n.providers[t]??{},token:e},await xe(n)}async function At(){return(await z()).default_provider??"hetzner"}async function Pe(t){let e=await z();e.default_provider=t,await xe(e)}async function jt(t){await Ge("hetzner",t)}async function Yr(){return _e("hetzner")}async function Nt(t,e){let n=await z();n.providers||(n.providers={}),n.providers.hetzner={...n.providers.hetzner??{},default_server_type:t,default_location:e},await xe(n)}async function Wr(){let t=await z(),e=t.providers?.hetzner;return{serverType:e?.default_server_type??t.default_server_type??"cax11",location:e?.default_location??t.default_location??"fsn1"}}async function it(t){let e=await z();t?e.default_agent=t:delete e.default_agent,await xe(e)}async function Fe(){return(await z()).default_agent??null}async function ce(t){let e=await ot(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let r=await n.text();throw new Error(`API error (${n.status}): ${r}`)}return await n.json()}async function te(t,e,n,r){let i=await ot(),s=await fetch(`${i}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:r})});if(s.status===429)throw new Error("Usage limit reached. Please try again later or contact support.");if(!s.ok){let a=await s.text();throw new Error(`Usage tracking failed (${s.status}): ${a}`)}}async function Rt(t){let e=await ot(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let r=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${r}`)}return await n.json()}var un,qr,G=D(()=>{"use strict";L();un="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1";qr={hetzner:"HETZNER_API_TOKEN",vultr:"VULTR_API_KEY"}});var dn={};ie(dn,{HetznerProvider:()=>Ht});function Ot(t){return{id:t.id,name:t.name,status:t.status,ipv4:t.public_net.ipv4.ip,ipv6:t.public_net.ipv6.ip,serverType:t.server_type?.name??"unknown",location:t.datacenter?.location?.name??"unknown",labels:t.labels,created:t.created}}function Xr(t){return{id:t.id,name:t.name,fingerprint:t.fingerprint??""}}var Zr,Ht,fn=D(()=>{"use strict";E();ze();Zr="https://api.hetzner.cloud/v1";Ht=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(G(),Ue)),r=e??await n();if(!r)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(r)}async request(e,n,r){let i=`${Zr}${n}`;o.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),l;try{l=JSON.parse(a).error?.message??a}catch{l=a}let c="";throw s.status===401||s.status===403?c=`
15
+ Your Hetzner token may be invalid or expired. Run: gibil init --force`:s.status===409&&l.includes("name")?c=`
16
+ A server with this name already exists. Try a different --name or run: gibil destroy <name>`:s.status===422&&(l.includes("location")||l.includes("server_type"))?c=`
17
+ This server type may not be available in your region. Run: gibil init --force`:s.status===429&&(c=`
18
+ Rate limited by Hetzner. Wait a moment and retry your command.`),new Error(`Hetzner API error (${s.status}): ${l}${c}`)}return s.status===204?{}:await s.json()}async createServer(e,n,r,i,s){if(!i||!s){let{getServerDefaults:d}=await Promise.resolve().then(()=>(G(),Ue)),f=await d();i=i??f.serverType,s=s??f.location}if(i.startsWith("cax")&&!["fsn1","nbg1"].includes(s))throw new Error(`ARM server type "${i}" is not available in "${s}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let c=typeof n=="string"?parseInt(n,10):n,u={name:e,server_type:i,image:"ubuntu-24.04",ssh_keys:[c],labels:{gibil:"true","gibil-name":e},location:s};o.debug(`createServer payload: ${JSON.stringify({name:e,server_type:i,image:"ubuntu-24.04",location:s})}`),r&&(u.user_data=r);try{let d=await this.request("POST","/servers",u);return Ot(d.server)}catch(d){let f=`(server_type=${i}, location=${s}). Try a different --server-type or --location.`;throw d instanceof Error?new Error(`${d.message} ${f}`):d}}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){let n=await this.request("GET",`/servers/${e}`);return Ot(n.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(Ot)}async waitForReady(e,n=12e4){let r=Date.now(),i=3e3;for(;Date.now()-r<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;o.debug(`Server ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let r=await this.request("POST","/ssh_keys",{name:e,public_key:n});return Xr(r.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}sizes(){return tt.sizes}}});var pn={};ie(pn,{buildAffiliateNudgeLine:()=>to,buildAffiliateProviderNudge:()=>Mt,getAffiliateProgram:()=>st,getAffiliateProgramNames:()=>eo});function Qr(){let t=process.env.GIBIL_NO_REFERRAL?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}function st(t){return Qr()?null:mn.programs[t]??null}function eo(){return Object.keys(mn.programs)}function to(t){return t?`
19
19
 
20
20
  No ${t.label} account? Get ${t.credit} free credits \u2192 ${t.ref_url}
21
- ${t.disclosure}`:""}function Dt(t,e){if(e)return null;let n=it(t);return n?`New here? ${n.credit} free credits \u2192 ${n.ref_url}`:null}var ln,st=z(()=>{"use strict";ln={version:1,programs:{vultr:{label:"Vultr",ref_url:"https://www.vultr.com/?ref=9900033-9J",credit:"$300",console_url:"https://console.vultr.com/user/apiaccess/",disclosure:"Referral link \u2014 Gibil gets a kickback that helps fund development."}}}});var dn={};re(dn,{VultrProvider:()=>zt});function Lt(t){let e=t.status;t.status==="active"&&t.power_status==="running"?e="running":t.status==="pending"&&(e="initializing");let n={};for(let o of t.tags??[])n[o]="true";return{id:t.id,name:t.label,status:e,ipv4:t.main_ip,ipv6:t.v6_main_ip,serverType:t.plan,location:t.region,labels:n,created:t.date_created}}function Yr(t){return{id:t.id,name:t.name,fingerprint:""}}var Vr,qr,zt,mn=z(()=>{"use strict";E();ze();Vr="https://api.vultr.com/v2",qr=2284;zt=class t{apiKey;constructor(e){this.apiKey=e}static async create(e){let n=e;if(!n){let{getProviderToken:o}=await Promise.resolve().then(()=>(G(),He));n=await o("vultr")??void 0}if(!n){let{getAffiliateProgram:o,buildAffiliateNudgeLine:i}=await Promise.resolve().then(()=>(st(),un)),s=i(o("vultr"));throw new Error(`VULTR_API_KEY is required. Run 'gibil init --provider vultr' or set it in your environment.${s}`)}return new t(n)}async request(e,n,o){let i=`${Vr}${n}`;r.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:o?JSON.stringify(o):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),c;try{c=JSON.parse(a).error??a}catch{c=a}let l="";throw s.status===401||s.status===403?l=`
22
- Your Vultr API key may be invalid. Re-run: gibil init --provider vultr --token <new-key>`:s.status===429&&(l=`
23
- Rate limited by Vultr. Wait a moment and retry.`),new Error(`Vultr API error (${s.status}): ${c}${l}`)}return s.status===204?{}:await s.json()}async createServer(e,n,o,i,s){let a=i??"vc2-2c-4gb",c=s??"nrt",l={region:c,plan:a,os_id:qr,label:e,tags:["gibil",`gibil-${e}`],sshkey_id:[String(n)]};o&&(l.user_data=Buffer.from(o,"utf-8").toString("base64"));try{let d=await this.request("POST","/instances",l);return Lt(d.instance)}catch(d){let u=`(plan=${a}, region=${c}). Try a different --server-type or --location.`;throw d instanceof Error?new Error(`${d.message} ${u}`):d}}async destroyServer(e){await this.request("DELETE",`/instances/${e}`)}async getServer(e){let n=await this.request("GET",`/instances/${e}`);return Lt(n.instance)}async listServers(e="gibil=true"){let n=e.split("=")[0]||"gibil";return((await this.request("GET",`/instances?tag=${encodeURIComponent(n)}&per_page=50`)).instances??[]).map(Lt)}async waitForReady(e,n=12e4){let o=Date.now(),i=3e3;for(;Date.now()-o<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;r.debug(`Vultr instance ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Vultr instance ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let o=await this.request("POST","/ssh-keys",{name:e,ssh_key:n});return Yr(o.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh-keys/${e}`)}sizes(){return tt.sizes}}});var fn={};re(fn,{ProviderRegistry:()=>at,providerRegistry:()=>H});var at,H,Ie=z(()=>{"use strict";at=class{factories=new Map;register(e,n){this.factories.set(e,n)}async get(e){let n=this.factories.get(e);if(!n)throw new Error(`Provider "${e}" is not registered. Registered: ${this.listRegistered().join(", ")||"(none)"}`);return n()}async forInstance(e){let n=e.provider??"hetzner";return this.get(n)}listRegistered(){return[...this.factories.keys()]}},H=new at;H.register("hetzner",async()=>{let{HetznerProvider:t}=await Promise.resolve().then(()=>(cn(),an));return t.create()});H.register("vultr",async()=>{let{VultrProvider:t}=await Promise.resolve().then(()=>(mn(),dn));return t.create()})});import{readFile as mo,writeFile as fo,mkdir as _n,rm as Pn,readdir as po,rename as go}from"fs/promises";import{existsSync as In}from"fs";import{join as Kt}from"path";async function Ht(t,e,n){let o=`${t}.tmp`;await fo(o,e,n);try{await go(o,t)}catch(i){throw await Pn(o,{force:!0}).catch(s=>{console.warn(`Warning: failed to clean up temp file ${o}: ${s}`)}),i}}var Gt,Fe,pe,ut,C,ge,Z,D=z(()=>{"use strict";B();Gt=class{instancesDir;keysDir;constructor(e){let n=e??P.root;this.instancesDir=Kt(n,"instances"),this.keysDir=Kt(n,"keys")}async ensureDirectories(){await _n(this.instancesDir,{recursive:!0,mode:448}),await _n(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return Kt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await Ht(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!In(n))return null;let o=await mo(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);In(n)&&await Pn(n)}async list(){await this.ensureDirectories();let e=await po(this.instancesDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}},Fe=new Gt,pe=t=>Fe.save(t),ut=t=>Fe.loadOrThrow(t),C=t=>Fe.loadActiveOrThrow(t),ge=t=>Fe.delete(t),Z=()=>Fe.list()});var Be={};re(Be,{GIBIL_SIZE_TARGETS:()=>wo,SIZE_NAMES:()=>En,isSizeName:()=>bo,resolveSize:()=>$o});function bo(t){return En.includes(t)}function $o(t,e){let n=t.sizes().find(o=>o.name===e);if(!n){let o=t.sizes().map(i=>i.name).join(", ")||"(none)";throw new Error(`Size "${e}" is not available on this provider. Available: ${o}`)}return n.nativeType}var En,wo,Je=z(()=>{"use strict";En=["small","medium","large"],wo={small:{vcpu:2,ramGb:4},medium:{vcpu:4,ramGb:8},large:{vcpu:8,ramGb:16}}});var On={};re(On,{JobStore:()=>ht,deleteJob:()=>Co,deleteJobsByInstance:()=>qe,listJobs:()=>Ce,listJobsByInstance:()=>jo,loadJob:()=>To,loadJobOrThrow:()=>le,saveJob:()=>V});import{readFile as Po,mkdir as An,rm as ko,readdir as Eo}from"fs/promises";import{existsSync as Nn}from"fs";import{join as Rn}from"path";var ht,ve,V,To,le,Co,Ce,jo,qe,ye=z(()=>{"use strict";B();D();ht=class{jobsDir;constructor(e){let n=e??P.root;this.jobsDir=Rn(n,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return Rn(this.jobsDir,`${e}.json`)}async save(e){await An(this.jobsDir,{recursive:!0,mode:448}),await Ht(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.jobFile(e);if(!Nn(n))return null;let o=await Po(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(e){let n=this.jobFile(e);Nn(n)&&await ko(n)}async list(){await An(this.jobsDir,{recursive:!0,mode:448});let e=await Eo(this.jobsDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}async listByInstance(e){return(await this.list()).filter(o=>o.instance===e)}async deleteByInstance(e){let n=await this.listByInstance(e);for(let o of n)await this.delete(o.id)}},ve=new ht,V=t=>ve.save(t),To=t=>ve.load(t),le=t=>ve.loadOrThrow(t),Co=t=>ve.delete(t),Ce=()=>ve.list(),jo=t=>ve.listByInstance(t),qe=t=>ve.deleteByInstance(t)});import{Command as Ii}from"commander";import{readFileSync as Pi}from"fs";import{fileURLToPath as ki}from"url";import{dirname as Ei,join as Ti}from"path";Ie();B();import{mkdir as Wr,rm as pn,readFile as Zr,chmod as Xr}from"fs/promises";import{existsSync as gn}from"fs";import{execFile as Qr}from"child_process";import{promisify as eo}from"util";var to=eo(Qr);async function ct(t){let e=P.keyDir(t);gn(e)&&await pn(e,{recursive:!0}),await Wr(e,{recursive:!0});let n=P.privateKey(t),o=P.publicKey(t);await to("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await Xr(n,384);let i=await Zr(o,"utf-8");return{privateKeyPath:n,publicKeyPath:o,publicKey:i.trim()}}async function ee(t){let e=P.keyDir(t);gn(e)&&await pn(e,{recursive:!0})}B();E();O();import{Client as hn}from"ssh2";import{readFile as vn}from"fs/promises";async function x(t){let{instanceName:e,ip:n,command:o,stream:i=!1,timeoutMs:s=3e4}=t,a=await vn(P.privateKey(e),"utf-8");return new Promise((c,l)=>{let d=new hn,u="",m="",g=null,p=!1;d.on("ready",()=>{r.debug(`SSH connected to ${n}`),d.exec(o,(v,y)=>{if(v)return d.end(),l(v);g=setTimeout(()=>{p||(p=!0,d.destroy(),l(new Error(`Command timed out after ${s/1e3}s on ${n}`)))},s),y.on("data",b=>{let w=b.toString();u+=w,i&&process.stdout.write(w)}),y.stderr.on("data",b=>{let w=b.toString();m+=w,i&&process.stderr.write(w)}),y.on("close",b=>{g&&clearTimeout(g),!p&&(p=!0,d.end(),c({stdout:u,stderr:m,exitCode:b??0}))})})}).on("error",v=>{if(g&&clearTimeout(g),p)return;p=!0;let y="";v.code==="ECONNREFUSED"?y=" (instance may have been destroyed or is still booting)":v.code==="EHOSTUNREACH"?y=" (IP unreachable \u2014 instance may not be running)":v.code==="ETIMEDOUT"&&(y=" (connection timed out \u2014 check if instance is running with 'gibil list')"),l(new Error(`SSH connection to ${n} failed: ${v.message}${y}`))}).connect({host:n,port:22,username:"root",privateKey:a,readyTimeout:s,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}function yn(t){let{instanceName:e,ip:n,filePath:o,timeoutMs:i=3e4}=t,s=null,a=!1;return(async()=>{try{let c=await vn(P.privateKey(e),"utf-8");s=new hn,await new Promise((l,d)=>{s.on("ready",()=>{s.exec(`tail -f ${o} 2>/dev/null`,(u,m)=>{if(u)return s.end(),d(u);m.on("data",g=>{a||process.stdout.write(f(g.toString()))}),m.stderr.on("data",g=>{a||process.stderr.write(f(g.toString()))}),m.on("close",()=>{s.end(),l()})})}).on("error",u=>{a||r.debug(`Verbose log tail failed: ${u.message}`),d(u)}).connect({host:n,port:22,username:"root",privateKey:c,readyTimeout:i,hostVerifier:()=>!0})})}catch{}})(),{abort(){if(a=!0,s)try{s.end()}catch{}}}}async function lt(t,e,n=12e4){let o=Date.now(),i=5e3;for(;Date.now()-o<n;)try{await x({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{r.debug(`SSH not ready on ${e}, retrying...`),await new Promise(s=>setTimeout(s,i))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function fe(t){let{repo:e,config:n,ttlMinutes:o,githubToken:i,gitIdentity:s}=t,a=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];i&&a.push(`export GITHUB_TOKEN=${F(i)}`),a.push("","# Base packages","apt-get update -qq","apt-get install -y -qq git curl wget build-essential unzip > /dev/null 2>&1","","# Install GitHub CLI","if ! type gh > /dev/null 2>&1; then"," curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null"," chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg"," ARCH=$(dpkg --print-architecture)",' echo "deb [arch=${ARCH} signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list'," apt-get update -qq && apt-get install -y -qq gh > /dev/null 2>&1","fi","");let c=n?.image??"node:20";if(a.push(...no(c)),t.agent){let l=io(t.agent);l&&(a.push(`# Install ${t.agent} + tmux`),t.agent==="aider"&&a.push("apt-get install -y -qq python3-pip > /dev/null 2>&1"),a.push(`${l} > /dev/null 2>&1`,"apt-get install -y -qq tmux > /dev/null 2>&1",""))}if(n?.services&&n.services.length>0){a.push(...ro()),a.push("");for(let l of n.services)a.push(...oo(l))}if(n?.env){a.push("# Environment variables");for(let[l,d]of Object.entries(n.env))a.push(`export ${l}=${F(d)}`),a.push(`echo ${F(`${l}=${d}`)} >> /etc/environment`);a.push("")}if(a.push("# Configure git"),s?(a.push(`git config --global user.email ${F(s.email)}`),a.push(`git config --global user.name ${F(s.name)}`),s.signingKey&&(a.push("git config --global gpg.format ssh"),a.push(`git config --global user.signingkey ${F("key::"+s.signingKey)}`),a.push("git config --global commit.gpgsign true"),a.push("git config --global tag.gpgsign true"),a.push("mkdir -p /root/.ssh"),a.push(`echo ${F(s.email+" "+s.signingKey)} > /root/.ssh/allowed_signers`),a.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(a.push("git config --global user.email 'gibil@bot.dev'"),a.push("git config --global user.name 'Gibil Bot'")),a.push(""),e){let l=e.match(/github\.com\/([^/]+\/[^/.]+)/);if(a.push("# Clone repository"),a.push("cd /root"),l){let d=l[1];a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${d}.git"`),a.push("else"),a.push(` CLONE_URL='https://github.com/${d}.git'`),a.push("fi"),a.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else a.push(`timeout 300 git clone ${F(e)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`);a.push("cd /root/project"),a.push(""),a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),l&&a.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${l[1]}.git"`),a.push("fi"),a.push("")}if(o&&o>0&&(a.push("# Auto-destroy after TTL"),a.push(`echo "shutdown -h now" | at now + ${o} minutes 2>/dev/null || true`),a.push(`(sleep ${o*60} && shutdown -h now) &`),a.push("")),a.push("# Clean up cloud-init secrets"),a.push("rm -f /var/lib/cloud/instance/user-data.txt"),a.push(""),a.push("# Signal that infrastructure is ready"),a.push("touch /root/.gibil-ready"),a.push('echo "Gibil infrastructure ready"'),a.push(""),e&&n?.tasks&&n.tasks.length>0){a.push("# Run project tasks"),a.push("cd /root/project");for(let l of n.tasks)a.push(`echo '\u25B6 Running task: '${F(l.name)}`),a.push(`if ! ${l.command}; then`),a.push(` echo '\u2717 Task failed: '${F(l.name)}`),a.push(" touch /root/.gibil-tasks-failed"),a.push("fi");a.push(""),a.push("# Signal tasks complete"),a.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),a.push(" touch /root/.gibil-tasks-done"),a.push(' echo "Gibil tasks complete"'),a.push("else"),a.push(' echo "Gibil tasks finished with errors"'),a.push("fi")}return a.join(`
24
- `)}function no(t){let e=[];if(t.startsWith("node:")){let n=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("")}else if(t.startsWith("python:")){let n=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let n=t.split(":")[1]??"1.22";e.push("# Install Go"),e.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),e.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),e.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),e.push("export PATH=$PATH:/usr/local/go/bin"),e.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),e.push("")}else e.push("# Install Node.js (default)"),e.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("");return e}function ro(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function oo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let o=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(o+=` -p ${t.port}:${t.port}`),t.env)for(let[i,s]of Object.entries(t.env))o+=` -e ${i}=${F(s)}`;return o+=` ${F(t.image)}`,e.push(o),e.push(""),e}var wn={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},Pe={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},W=Object.keys(wn);function io(t){return wn[t]??null}function F(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as so}from"fs/promises";import{existsSync as bn,statSync as ao}from"fs";import{join as co}from"path";import{parse as Sn}from"yaml";async function ke(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,n,o]=e,i=`https://raw.githubusercontent.com/${n}/${o}/HEAD/.gibil.yml`;try{let s={};process.env.GITHUB_TOKEN&&(s.Authorization=`token ${process.env.GITHUB_TOKEN}`);let a=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:s});if(!a.ok)return null;let c=await a.text();return uo(c)}catch{return null}}var lo=".gibil.yml";async function Ee(t){let e;if(bn(t)&&ao(t).isFile()?e=t:e=co(t,lo),!bn(e))return null;let n=await so(e,"utf-8"),o=Sn(n);return xn(o)}function uo(t){let e=Sn(t);return xn(e)}function xn(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:i.name,image:i.image,port:typeof i.port=="number"?i.port:void 0,env:$n(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:i.name,command:i.command}})),e.env!==void 0&&(n.env=$n(e.env,"top-level")),n}function $n(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[o,i]of Object.entries(t))if(typeof i=="string")n[o]=i;else if(typeof i=="number"||typeof i=="boolean")n[o]=String(i);else throw new Error(`env.${o} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(n).length>0?n:void 0}D();import{randomBytes as ho}from"crypto";function he(t=6){return ho(Math.ceil(t/2)).toString("hex").slice(0,t)}function Ue(){return`gibil-${he()}`}function kn(){return`fleet-${he(8)}`}function dt(){return`j-${he(8)}`}B();E();var vo=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function mt(t){if(!vo.test(t))throw new Error(`Invalid instance name "${t}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return t}function ft(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}var J=525600,yo={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function Te(t){let e=t.trim().toLowerCase();if(/^\d+$/.test(e)){let c=parseInt(e,10);if(c<=0)throw new Error(`TTL must be positive, got "${t}"`);if(c>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got ${c} minutes.`);return c}let n=e.match(/^(\d+)(mo|[mhdwy])$/);if(!n)throw new Error(`Invalid TTL "${t}". Use a number (minutes) or a duration: 2h, 7d, 1w, 1mo, 3mo, 6mo, 1y`);let o=parseInt(n[1],10),i=n[2],s=yo[i],a=o*s;if(a<=0)throw new Error(`TTL must be positive, got "${t}"`);if(a>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got "${t}" = ${a} minutes.`);return a}G();O();import{execSync as pt}from"child_process";import{readFileSync as So}from"fs";var Ft="key::";function xo(){try{let t=pt("git config user.name",{encoding:"utf-8"}).trim(),e=pt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(pt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=pt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=So(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(Ft))&&(n=i.startsWith(Ft)?i.slice(Ft.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function gt(t,e,n){r.step("Generating SSH keys...");let o=await ct(e),i,s;try{r.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${he(4)}`,o.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&r.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let a=xo(),c=fe({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a,agent:n.agent}),l=r.spin(`Creating server on ${n.providerName??"hetzner"}...`),d=await t.createServer(e,i.id,c,n.serverType??n.config?.server_type,n.location??n.config?.location);s=d.id,l.succeed("Server created");let u=r.spin("VM booting..."),g=(await t.waitForReady(d.id)).ipv4;u.succeed(`VM running at ${g}`);let p=new Date,v={name:e,serverId:d.id,ip:g,sshKeyId:i.id,keyPath:P.privateKey(e),status:"running",createdAt:p.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(p.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:a,provider:n.providerName??"hetzner"};await pe(v);let y=r.spin("Waiting for SSH...");if(await lt(e,g),y.succeed("SSH ready"),n.repo||n.config){let b=r.spin("Provisioning (runtime, repo, deps)..."),w;n.verbose&&!n.json&&(w=yn({instanceName:e,ip:g,filePath:"/var/log/cloud-init-output.log"}));let _=36e4,$=5e3,k=Date.now(),N=!1;for(;Date.now()-k<_;){try{if((await x({instanceName:e,ip:g,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){N=!0;break}}catch{}await new Promise(ne=>setTimeout(ne,$))}if(w?.abort(),N)b.succeed("Provisioning complete");else{b.fail("Provisioning may have failed");try{let ne=await x({instanceName:e,ip:g,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});r.info(ne.stdout)}catch{r.warn("Could not read cloud-init log.")}}}return v}catch(a){throw r.error(`Failed to create instance "${e}", cleaning up...`),s&&await t.destroyServer(s).catch(c=>r.warn(`Could not destroy server ${s}: ${c instanceof Error?c.message:String(c)}`)),i&&await t.deleteSSHKey(i.id).catch(c=>r.warn(`Could not delete SSH key ${i.id}: ${c instanceof Error?c.message:String(c)}`)),await ee(e).catch(c=>r.warn(`Could not clean up local SSH keys: ${c instanceof Error?c.message:String(c)}`)),a}}function Tn(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`ssh -i ${t.keyPath} -o StrictHostKeyChecking=no root@${t.ip}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}function Cn(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <duration>","Auto-destroy timer (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)","60").option("-c, --config <path>","Path to .gibil.yml config").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("-q, --quiet","Suppress non-essential output").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async e=>{e.json&&I(!0);let n=Te(e.ttl??"60"),o=ft(e.fleet??"1","Fleet count");if(o>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&mt(e.name),!e.agent){let u=await Ge();u&&(e.agent=u)}if(e.agent&&!W.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${W.join(", ")}`);let i={};if(e.env)for(let u of e.env){let m=u.indexOf("=");if(m<=0)throw new Error(`Invalid --env format: "${u}". Use KEY=VALUE.`);i[u.slice(0,m)]=u.slice(m+1)}i.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=i.GITHUB_TOKEN);let s=null;if(e.config?s=await Ee(e.config):e.repo?s=await ke(e.repo)??await Ee(process.cwd()):s=await Ee(process.cwd()),Object.keys(i).length>0&&(s||(s={}),s.env={...s.env,...i}),e.dryRun){let u=e.name??Ue(),m=e.provider??"hetzner",g=e.serverType??s?.server_type;if(!g&&e.size){let{isSizeName:_}=await Promise.resolve().then(()=>(Je(),Be));if(!_(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);let{PROVIDER_CATALOG:$}=await Promise.resolve().then(()=>(ze(),on));g=$[m]?.sizes.find(N=>N.name===e.size)?.nativeType}g||(g=m==="vultr"?"vc2-2c-4gb":"cax11");let p=g,v=e.location??s?.location??(m==="vultr"?"nrt":"nbg1"),y=s?.image??"node:20",b=fe({repo:e.repo,config:s??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),w={name:u,serverType:p,location:v,image:y,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:b};e.json?r.json(w):(r.info(""),r.info(h("Dry run \u2014 no server will be created")),r.info(""),r.info(` ${f("Name:")} ${u}`),r.info(` ${f("Server type:")} ${p}`),r.info(` ${f("Location:")} ${v}`),r.info(` ${f("Image:")} ${y}`),r.info(` ${f("TTL:")} ${n} minutes`),e.repo&&r.info(` ${f("Repo:")} ${e.repo}`),e.agent&&r.info(` ${f("Agent:")} ${e.agent}`),r.info(""),r.info("Cloud-init script:"),r.info("\u2500".repeat(17)),r.info(b));return}let a=await M();if(a){r.info("Verifying API key...");let u=await ce(a);r.info(` Authenticated as ${u.user.email} (${u.user.plan})`)}if(e.agent&&!Pe[e.agent]?.some(m=>s?.env?.[m]||i[m])){let m=Pe[e.agent]?.join(" or ")??"";r.warn(`${e.agent} needs ${m}. SSH in and export it (recommended) or pass with --env.`)}let c=e.provider??"hetzner",l=await H.get(c),d=e.serverType;if(!d&&e.size){let{isSizeName:u,resolveSize:m}=await Promise.resolve().then(()=>(Je(),Be));if(!u(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);d=m(l,e.size)}if(o===1){let u=e.name??Ue(),m=Date.now(),g=r.spin(`Forging "${u}"...`),p=await gt(l,u,{repo:e.repo,ttlMinutes:n,config:s,providerName:c,serverType:d,location:e.location,agent:e.agent,verbose:e.verbose}),v=((Date.now()-m)/1e3).toFixed(1);g.succeed(T.createReady(u,v)),a&&await Q(a,"create",p.name,e.serverType).catch(y=>r.debug(`Usage tracking failed: ${y instanceof Error?y.message:String(y)}`)),e.json?r.json(Tn(p)):(r.info(""),r.info(Qe("Server ready",[`${f("Name:")} ${h(p.name)}`,`${f("IP:")} ${p.ip}`,`${f("TTL:")} ${n} minutes`,`${f("SSH:")} ${h(`gibil ssh ${p.name}`)}`])),r.info(""),r.info(f(" Try:")),r.info(` ${h(`gibil run ${p.name} "<your test command>"`)}`),r.info(` ${h(`gibil ssh ${p.name}`)}`),r.info(` ${h(`gibil destroy ${p.name}`)}`),r.info(""))}else{let u=kn(),m=e.name??"gibil",g=Date.now(),p=r.spin(`Forging fleet "${u}" \u2014 ${o} servers...`),v=Array.from({length:o},($,k)=>`${m}-${k+1}-${u.slice(6)}`),y=await Promise.allSettled(v.map($=>gt(l,$,{repo:e.repo,ttlMinutes:n,config:s,providerName:c,serverType:d,location:e.location,fleetId:u,agent:e.agent,verbose:e.verbose}))),b=[],w=[];for(let $=0;$<y.length;$++){let k=y[$];k.status==="fulfilled"?b.push(k.value):w.push(`${v[$]}: ${k.reason instanceof Error?k.reason.message:String(k.reason)}`)}let _=((Date.now()-g)/1e3).toFixed(1);if(p.succeed(T.fleetReady(b.length,o)+` ${f(`(${_}s)`)}`),a&&await Promise.all(b.map($=>Q(a,"create",$.name,e.serverType).catch(k=>r.debug(`Usage tracking failed for ${$.name}: ${k instanceof Error?k.message:String(k)}`)))),e.json)r.json({fleet_id:u,instances:b.map(Tn),errors:w});else{r.info("");for(let $ of b)r.info(` ${A} ${h($.name)} ${f("\u2192")} ${$.ip}`);for(let $ of w)r.info(` ${Le} ${$}`);r.info("")}}})}D();import{spawn as No}from"child_process";import{spawn as _o}from"child_process";import{existsSync as Io}from"fs";function Ve(t){if(t.includes(":")){let e=t.split(":");if(e.length!==3)throw new Error(`Invalid port mapping "${t}". Use PORT or LOCAL:HOST:REMOTE.`);let[n,,o]=e;Ut(n,t),Ut(o,t)}else Ut(t,t)}function Ut(t,e){let n=parseInt(t,10);if(isNaN(n)||n<1||n>65535||String(n)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}function jn(t,e){if(!Io(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=[];for(let o of e){Ve(o);let i=o.includes(":")?o:`${o}:localhost:${o}`,s=o.includes(":")?o.split(":")[0]:o;n.push(s),_o("ssh",["-f","-N","-L",i,"-i",t.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes",`root@${t.ip}`],{stdio:"ignore",detached:!0}).unref()}return n}E();O();E();D();ye();var Ao=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function we(t){if(!(t instanceof Error))return!1;let e=t.message;return Ao.some(n=>e.includes(n))}async function q(t){try{let{providerRegistry:e}=await Promise.resolve().then(()=>(Ie(),fn));return await(await e.forInstance(t)).getServer(t.serverId),"still_exists"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await ee(t.name),await qe(t.name),await ge(t.name),r.warn(`Instance "${t.name}" no longer exists on the provider \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function Mn(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000 --port 5432)").action(async(e,n)=>{let o=await C(e),i=["-A","-i",o.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];if(n.port&&n.port.length>0){for(let a of n.port){Ve(a);let c=a.includes(":")?a:`${a}:localhost:${a}`;i.push("-L",c)}r.info("");for(let a of n.port){let c=a.includes(":")?a.split(":")[0]:a;r.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${a}`)}r.info(""),r.info(f(" Tunnel active while SSH session is open. Ctrl+C to stop.")),r.info("")}i.push(`root@${o.ip}`),No("ssh",i,{stdio:"inherit"}).on("exit",a=>{let c=()=>process.exit(a??0);a===255?q(o).then(c,c):c()})})}D();ye();E();function Dn(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").option("--timeout <seconds>","Command timeout in seconds (default: 30)").option("-b, --background","Run in background, return job ID immediately").action(async(e,n,o)=>{o.json&&I(!0);let i=await C(e),s=n.join(" "),a=o.timeout?ft(o.timeout,"Timeout")*1e3:3e4;if(o.background){let l=dt(),d="/root/.gibil-jobs",u=`${d}/${l}.log`,m=`${d}/${l}.exit`,g=`${d}/${l}.pid`,p=`${d}/${l}.sh`,v=["#!/bin/bash",`nohup bash -c '${s.replace(/'/g,"'\\''")}' > ${u} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${g}`,`(wait $BGPID 2>/dev/null; echo $? > ${m}) &`,"echo $BGPID"].join(`
25
- `),y=Buffer.from(v).toString("base64"),b=`mkdir -p ${d} && echo '${y}' | base64 -d > ${p} && chmod +x ${p} && bash ${p}`,w;try{w=await x({instanceName:e,ip:i.ip,command:b,timeoutMs:1e4})}catch($){throw we($)&&await q(i)==="cleaned"&&process.exit(1),$}let _=parseInt(w.stdout.trim(),10);isNaN(_)&&(r.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await V({id:l,instance:e,command:s,pid:_,status:"running",startedAt:new Date().toISOString()}),o.json?r.json({job_id:l,instance:e,status:"running",pid:_}):(r.info(`Background job started: ${l} (PID ${_})`),r.info(` Poll: gibil job ${l}`));return}r.info(`Running on "${e}" (${i.ip}): ${s}`);let c;try{c=await x({instanceName:e,ip:i.ip,command:s,stream:!o.json,timeoutMs:a})}catch(l){throw we(l)&&await q(i)==="cleaned"&&process.exit(1),l}o.json?r.json({instance:e,command:s,stdout:c.stdout,stderr:c.stderr,exit_code:c.exitCode}):c.exitCode!==0&&r.error(`Command exited with code ${c.exitCode}`),process.exit(c.exitCode??1)})}Ie();import{createInterface as Jo}from"readline";D();ye();E();G();O();B();import{createHash as Ln}from"crypto";import{readFile as Hn,writeFile as Ro,mkdir as Oo}from"fs/promises";import{existsSync as yt,readFileSync as Mo}from"fs";import{hostname as zn,userInfo as Do,platform as Lo,arch as zo}from"os";import{join as Ne,dirname as Fn}from"path";import{fork as Ko}from"child_process";import{fileURLToPath as Un}from"url";var vt=Ne(P.root,"device_id"),Kn=Ne(P.root,"config.json"),Go=process.env.GIBIL_TELEMETRY_URL??"https://zopdxjruwktjyjunitrv.supabase.co/functions/v1/telemetry-ingest",Ho="tk_alpha_09a93302e0f3e73417a9e9dbfc500a61",Ye=null,je=null;function Fo(){try{let t=Do(),e=`${zn()}:${t.username}:${t.homedir}`;return Ln("sha256").update(e).digest("hex").slice(0,16)}catch{return Ln("sha256").update(`${zn()}:${Date.now()}`).digest("hex").slice(0,16)}}async function Bn(){if(Ye)return Ye;if(yt(vt)){let e=(await Hn(vt,"utf-8")).trim();if(e.length>0)return Ye=e,Ye}let t=Fo();return await Oo(P.root,{recursive:!0,mode:448}),await Ro(vt,t,{mode:384}),Ye=t,t}async function Bt(){if(je!==null)return je;let t=process.env.GIBIL_TELEMETRY;if(t!==void 0)return je=!["0","false","off","no"].includes(t.toLowerCase()),je;try{if(yt(Kn)&&JSON.parse(await Hn(Kn,"utf-8")).telemetry===!1)return je=!1,!1}catch{}return je=!0,!0}async function Jn(){return yt(vt)?!1:(await Bn(),!0)}var Ae=null;function Uo(){if(Ae)return Ae;let t=Fn(Un(import.meta.url));for(let e of["../package.json","../../package.json"])try{return Ae=JSON.parse(Mo(Ne(t,e),"utf-8")).version??"0.0.0",Ae}catch{}return Ae="0.0.0",Ae}async function We(t){if(!await Bt())return;let e=await Bn();if(!e)return;let n={...t,device_id:e,cli_version:Uo(),timestamp:new Date().toISOString(),os:Lo(),arch:zo(),node_version:process.version},o=Fn(Un(import.meta.url)),s=[Ne(o,"telemetry-send.js"),Ne(o,"..","utils","telemetry-send.js"),Ne(o,"telemetry-send.ts")].find(a=>yt(a));if(s)try{Ko(s,{detached:!0,stdio:"ignore",...s.endsWith(".ts")?{execArgv:["--import","tsx"]}:{},env:{...process.env,TELEMETRY_PAYLOAD:JSON.stringify(n),TELEMETRY_ENDPOINT:Go,TELEMETRY_INGEST_KEY:Ho}}).unref()}catch{}}var Gn=new Map,Bo=5e3;async function Vn(t){let e=Date.now(),n=Gn.get(t)??0;e-n<Bo||(Gn.set(t,e),await We({event:"mcp_tool",tool:t}))}function qn(t){return t.slice(3).filter(e=>e.startsWith("-")).map(e=>e.replace(/=.*$/,""))}async function Vo(t){let e=t.length;r.info(""),r.info(h(`This will destroy ${e} running server${e===1?"":"s"}:`)),r.info("");for(let i of t){let s=Math.round((Date.now()-new Date(i.createdAt).getTime())/6e4),a=s<60?`${s}m old`:`${Math.floor(s/60)}h ${s%60}m old`;r.info(` ${f("\u2022")} ${h(i.name)} ${f("\u2192")} ${i.ip} ${f(`(${a})`)}`)}r.info(""),r.info(f("Type 'yes' to confirm, anything else to cancel:"));let n=Jo({input:process.stdin,output:process.stdout}),o=await new Promise(i=>n.question("> ",i));return n.close(),o.trim().toLowerCase()==="yes"}async function Yn(t){let e=await ut(t),n=await H.forInstance(e);r.info(`Destroying instance "${t}" (server ${e.serverId})...`);try{await n.destroyServer(e.serverId)}catch(s){r.warn(`Could not delete server ${e.serverId}: ${s instanceof Error?s.message:String(s)}`)}try{await n.deleteSSHKey(e.sshKeyId)}catch(s){r.warn(`Could not delete SSH key ${e.sshKeyId}: ${s instanceof Error?s.message:String(s)}`)}await ee(t),await qe(t),await ge(t);let o=await M();o&&await Q(o,"destroy",t).catch(s=>r.warn(`Usage tracking failed (billing may be inaccurate): ${s instanceof Error?s.message:String(s)}`));let i=Math.round((Date.now()-new Date(e.createdAt).getTime())/6e4);await We({event:"lifecycle",duration_minutes:i}),r.info(` ${A} ${T.destroySingle(t)}`)}function Wn(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").option("-y, --yes","Skip the confirmation prompt for --all (required in non-interactive contexts)").action(async(e,n)=>{if(n.json&&I(!0),n.all){let o=await Z();if(o.length===0){n.json?r.json({destroyed:[],failed:[]}):r.info(T.noInstances);return}if(!n.yes&&(n.json&&(r.json({error:"confirmation required",message:`--all would destroy ${o.length} server(s). Re-run with --yes to confirm.`,instances:o.map(d=>({name:d.name,ip:d.ip}))}),process.exit(1)),process.stdin.isTTY||(r.error("stdin is not a TTY. Re-run with --yes to confirm in non-interactive contexts."),process.exit(1)),!await Vo(o))){r.info(""),r.info("Cancelled. No servers were destroyed.");return}r.info(`Destroying ${o.length} instance(s)...`);let i=3,s=[];for(let l=0;l<o.length;l+=i){let d=o.slice(l,l+i),u=await Promise.allSettled(d.map(m=>Yn(m.name)));s.push(...u)}let a=[],c=[];for(let l=0;l<s.length;l++)if(s[l].status==="fulfilled")a.push(o[l].name);else{let d=s[l].reason;c.push(`${o[l].name}: ${d instanceof Error?d.message:String(d)}`)}n.json?r.json({destroyed:a,failed:c}):c.length===0?r.info(`
26
- ${T.destroyAll}`):r.info(`
27
- ${a.length} destroyed, ${c.length} failed`)}else e||(r.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1)),await Yn(e),n.json&&r.json({destroyed:[e]})})}D();E();O();function Zn(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await Z();if(n.length===0){e.json?r.json({instances:[]}):r.info(T.noInstances);return}let o=n.map(i=>{let s=Math.max(0,Math.floor((new Date(i.expiresAt).getTime()-Date.now())/1e3));return{name:i.name,ip:i.ip,ssh:`ssh -i ${i.keyPath} -o StrictHostKeyChecking=no root@${i.ip}`,status:i.status,ttl_remaining:s,created_at:i.createdAt,fleet_id:i.fleetId,provider:i.provider??"hetzner"}});if(e.json){r.json({instances:o});return}r.info(f(`${"NAME".padEnd(28)} ${"PROVIDER".padEnd(10)} ${"IP".padEnd(18)} ${"STATUS".padEnd(11)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),r.info(f("\u2500".repeat(90)));for(let i of o){let s=Xn(i.ttl_remaining),a=qo(i.created_at),c=i.name.padEnd(28),l=i.provider.padEnd(10),d=i.status.padEnd(11),u=s.padEnd(10),m=a.padEnd(10),g=i.status==="running"?X(d):oe(d),p=i.ttl_remaining<=300?oe(u):u;r.info(`${h(c)} ${f(l)} ${i.ip.padEnd(18)} ${g} ${p} ${f(m)}`)}r.info(`
28
- ${f(`${o.length} server(s)`)}`)})}function Xn(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=Math.floor(e/60),o=Math.floor(n/24);if(o>=1){let i=n%24;return i>0?`${o}d ${i}h`:`${o}d`}return n>=1?`${n}h ${e%60}m`:`${e}m ${t%60}s`}function qo(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return Xn(n)}D();E();function Qn(t){t.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <duration>","New TTL from now (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let o=await C(e),i=Te(n.ttl);try{await x({instanceName:e,ip:o.ip,command:["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`,`(sleep ${i*60} && shutdown -h now) &`].join(" && ")})}catch(a){throw we(a)&&await q(o)==="cleaned"&&process.exit(1),a}let s=new Date(Date.now()+i*6e4).toISOString();o.ttlMinutes=i,o.expiresAt=s,await pe(o),n.json?r.json({name:o.name,ttl_minutes:i,expires_at:s}):r.info(`\u2713 Extended "${e}" TTL to ${i} minutes (expires ${s})`)})}import{readFile as Yo}from"fs/promises";import{randomBytes as Wo}from"crypto";D();E();function er(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let o=await C(e),i=await Yo(n.script,"utf-8");r.info(`Uploading and running script "${n.script}" on "${e}"...`);let s=Buffer.from(i).toString("base64"),a=`/tmp/gibil-script-${Wo(4).toString("hex")}.sh`,c;try{c=await x({instanceName:e,ip:o.ip,command:`echo '${s}' | base64 -d > ${a} && chmod +x ${a} && ${a}; EXIT=$?; rm -f ${a}; exit $EXIT`,stream:!n.json})}catch(l){throw we(l)&&await q(o)==="cleaned"&&process.exit(1),l}n.json?r.json({instance:e,script:n.script,stdout:c.stdout,stderr:c.stderr,exit_code:c.exitCode}):c.exitCode!==0&&r.error(`Script exited with code ${c.exitCode}`),process.exit(c.exitCode??1)})}G();E();O();import{createInterface as Zo}from"readline";function tr(t){let e=Zo({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,o=>{e.close(),n(o.trim())})})}function nr(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=n.key??process.env.GIBIL_API_KEY;o||(o=await tr("Enter your API key: ")),o||(r.error("No API key provided."),process.exit(1)),o.startsWith("pk_")||(r.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),r.info("Verifying API key...");try{let i=await ce(o);await Tt(o),n.json?r.json({authenticated:!0,email:i.user.email,plan:i.user.plan}):(r.info(T.authSuccess),r.detail("Email",i.user.email),r.detail("Plan","alpha (free)"))}catch(i){r.error(i instanceof Error?i.message:String(i)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let o=n.token;o||(o=await tr("Enter your Hetzner API token: ")),o||(r.error("No token provided."),process.exit(1));try{let s=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${o}`}})).json();s.error&&(r.error(`Invalid token: ${s.error.message}`),process.exit(1))}catch(i){r.error(`Could not verify token: ${i instanceof Error?i.message:"Check your network."}`),process.exit(1)}await At(o),r.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await Ct(),r.info(T.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=await M();if(!o){n.json?r.json({authenticated:!1}):r.info(`Not logged in. Run ${h("gibil auth login")} to authenticate.`);return}try{let i=await ce(o);n.json?r.json({authenticated:!0,email:i.user.email,plan:i.user.plan,limits:i.limits}):(r.success(`Authenticated as ${i.user.email}`),r.detail("Plan",i.user.plan),r.detail("Concurrent servers",String(i.limits.max_concurrent)),r.detail("Hours remaining",String(i.limits.remaining_hours)))}catch{n.json?r.json({authenticated:!1,error:"Key verification failed"}):r.error(`Stored API key is invalid. Run ${h("gibil auth login")} to re-authenticate.`)}})}G();E();function rr(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await M();n||(r.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let o=await Rt(n);e.json?r.json(o):(r.info("Plan: alpha (free)"),r.info(`VM hours used: ${o.vm_hours_used.toFixed(1)}h`),r.info(`Active instances: ${o.active_instances}`))}catch(o){r.error(o instanceof Error?o.message:String(o)),process.exit(1)}})}import{McpServer as Xo}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Qo}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as S}from"zod";Ie();D();B();import{execSync as wt}from"child_process";import{readFileSync as ei}from"fs";ye();D();ye();E();async function Jt(t){let e=await le(t);if(e.status!=="running")return{status:e.status,exitCode:e.exitCode};let n=await C(e.instance),o="/root/.gibil-jobs",i=`${o}/${t}.exit`,s=`${o}/${t}.log`,c=(await x({instanceName:e.instance,ip:n.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(c==="RUNNING"){try{if((await x({instanceName:e.instance,ip:n.ip,command:`kill -0 ${e.pid} 2>/dev/null && echo "alive" || echo "dead"`,timeoutMs:1e4})).stdout.trim()==="dead"){let v=new Date;e.status="orphaned",e.completedAt=v.toISOString(),await V(e);let y=Math.round((v.getTime()-new Date(e.startedAt).getTime())/1e3),b;try{b=(await x({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:y,stdout:b}}}catch{}return{status:"running"}}let l=parseInt(c,10),d=await x({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4}),u=l===0?"done":"failed",m=new Date,g=Math.round((m.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=u,e.exitCode=l,e.completedAt=m.toISOString(),await V(e),{status:u,exitCode:l,stdout:d.stdout,durationS:g}}function or(t){let e=t.command("job").description("Manage background jobs");e.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,o)=>{o.json&&I(!0);let i=await le(n),s=await Jt(n);o.json?r.json({job_id:n,instance:i.instance,command:i.command,status:s.status,exit_code:s.exitCode,started_at:i.startedAt,duration_s:s.durationS,...s.stdout!==void 0?{stdout:s.stdout}:{}}):s.status==="running"?(r.info(`Job ${n} is still running on "${i.instance}"`),r.info(` Command: ${i.command}`),r.info(` Started: ${i.startedAt}`)):s.status==="orphaned"?(r.warn(`Job ${n} is orphaned \u2014 process died without writing exit code`),r.info(` Instance: ${i.instance}`),r.info(` Command: ${i.command}`),s.durationS!==void 0&&r.info(` Duration: ${s.durationS}s`),s.stdout&&(r.info(" Output:"),process.stdout.write(s.stdout))):(r.info(`Job ${n}: ${s.status} (exit code ${s.exitCode}, ${s.durationS}s)`),s.stdout&&process.stdout.write(s.stdout))}),e.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=await Ce(),i=await Z(),s=new Set(i.map(a=>a.name));for(let a of o)a.status==="running"&&!s.has(a.instance)&&(a.status="orphaned",a.completedAt=new Date().toISOString(),await V(a));if(o.length===0){n.json?r.json([]):r.info("No background jobs.");return}if(n.json)r.json(o.map(a=>({job_id:a.id,instance:a.instance,command:a.command,status:a.status,started_at:a.startedAt,exit_code:a.exitCode})));else for(let a of o){let c=a.status==="running"?"\u27F3 running":a.status==="done"?"\u2713 done":a.status==="orphaned"?"\u26A0 orphaned":`\u2717 ${a.status}`;r.info(` ${a.id} ${c} ${a.instance} ${a.command}`)}}),e.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,o)=>{o.json&&I(!0);let i=await le(n);if(i.status!=="running"){o.json?r.json({job_id:n,status:i.status,message:"Job is not running"}):r.info(`Job ${n} is not running (status: ${i.status})`);return}let s=await C(i.instance);await x({instanceName:i.instance,ip:s.ip,command:`kill -- -${i.pid} 2>/dev/null || kill ${i.pid} 2>/dev/null || true`,timeoutMs:1e4}),i.status="cancelled",i.completedAt=new Date().toISOString(),await V(i),o.json?r.json({job_id:n,status:"cancelled"}):r.info(`Job ${n} cancelled.`)}),e.command("logs <id>").description("Fetch output of a background job").option("--json","Output result as JSON").option("-f, --follow","Follow log output (tail -f)").action(async(n,o)=>{o.json&&I(!0);let i=await le(n),s=await C(i.instance),a=`/root/.gibil-jobs/${n}.log`,c=o.follow?`tail -f ${a}`:`cat ${a} 2>/dev/null || echo '(no output yet)'`,l=o.follow?3e5:1e4,d=await x({instanceName:i.instance,ip:s.ip,command:c,stream:!o.json,timeoutMs:l});o.json&&r.json({job_id:n,stdout:d.stdout})})}function L(t,e,n){let o=`[${t}] ${e}`;return n&&(o+=`
21
+ ${t.disclosure}`:""}function Mt(t,e){if(e)return null;let n=st(t);return n?`New here? ${n.credit} free credits \u2192 ${n.ref_url}`:null}var mn,at=D(()=>{"use strict";mn={version:1,programs:{vultr:{label:"Vultr",ref_url:"https://www.vultr.com/?ref=9900033-9J",credit:"$300",console_url:"https://console.vultr.com/user/apiaccess/",disclosure:"Referral link \u2014 Gibil gets a kickback that helps fund development."}}}});var gn={};ie(gn,{VultrProvider:()=>Dt});function Kt(t){let e=t.status;t.status==="active"&&t.power_status==="running"?e="running":t.status==="pending"&&(e="initializing");let n={};for(let r of t.tags??[])n[r]="true";return{id:t.id,name:t.label,status:e,ipv4:t.main_ip,ipv6:t.v6_main_ip,serverType:t.plan,location:t.region,labels:n,created:t.date_created}}function oo(t){return{id:t.id,name:t.name,fingerprint:""}}var no,ro,Dt,hn=D(()=>{"use strict";E();ze();no="https://api.vultr.com/v2",ro=2284;Dt=class t{apiKey;constructor(e){this.apiKey=e}static async create(e){let n=e;if(!n){let{getProviderToken:r}=await Promise.resolve().then(()=>(G(),Ue));n=await r("vultr")??void 0}if(!n){let{getAffiliateProgram:r,buildAffiliateNudgeLine:i}=await Promise.resolve().then(()=>(at(),pn)),s=i(r("vultr"));throw new Error(`VULTR_API_KEY is required. Run 'gibil init --provider vultr' or set it in your environment.${s}`)}return new t(n)}async request(e,n,r){let i=`${no}${n}`;o.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),l;try{l=JSON.parse(a).error??a}catch{l=a}let c="";throw s.status===401||s.status===403?c=`
22
+ Your Vultr API key may be invalid. Re-run: gibil init --provider vultr --token <new-key>`:s.status===429&&(c=`
23
+ Rate limited by Vultr. Wait a moment and retry.`),new Error(`Vultr API error (${s.status}): ${l}${c}`)}return s.status===204?{}:await s.json()}async createServer(e,n,r,i,s){let a=i??"vc2-2c-4gb",l=s??"nrt",c={region:l,plan:a,os_id:ro,label:e,tags:["gibil",`gibil-${e}`],sshkey_id:[String(n)]};r&&(c.user_data=Buffer.from(r,"utf-8").toString("base64"));try{let u=await this.request("POST","/instances",c);return Kt(u.instance)}catch(u){let d=`(plan=${a}, region=${l}). Try a different --server-type or --location.`;throw u instanceof Error?new Error(`${u.message} ${d}`):u}}async destroyServer(e){await this.request("DELETE",`/instances/${e}`)}async getServer(e){let n=await this.request("GET",`/instances/${e}`);return Kt(n.instance)}async listServers(e="gibil=true"){let n=e.split("=")[0]||"gibil";return((await this.request("GET",`/instances?tag=${encodeURIComponent(n)}&per_page=50`)).instances??[]).map(Kt)}async waitForReady(e,n=12e4){let r=Date.now(),i=3e3;for(;Date.now()-r<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;o.debug(`Vultr instance ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Vultr instance ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let r=await this.request("POST","/ssh-keys",{name:e,ssh_key:n});return oo(r.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh-keys/${e}`)}sizes(){return nt.sizes}}});var yn={};ie(yn,{ProviderRegistry:()=>ct,providerRegistry:()=>F});var ct,F,ke=D(()=>{"use strict";ct=class{factories=new Map;register(e,n){this.factories.set(e,n)}async get(e){let n=this.factories.get(e);if(!n)throw new Error(`Provider "${e}" is not registered. Registered: ${this.listRegistered().join(", ")||"(none)"}`);return n()}async forInstance(e){let n=e.provider??"hetzner";return this.get(n)}listRegistered(){return[...this.factories.keys()]}},F=new ct;F.register("hetzner",async()=>{let{HetznerProvider:t}=await Promise.resolve().then(()=>(fn(),dn));return t.create()});F.register("vultr",async()=>{let{VultrProvider:t}=await Promise.resolve().then(()=>(hn(),gn));return t.create()})});import{readFile as fo,writeFile as mo,mkdir as bn,rm as Sn,readdir as po,rename as go}from"fs/promises";import{existsSync as $n}from"fs";import{join as Lt}from"path";async function Gt(t,e,n){let r=`${t}.tmp`;await mo(r,e,n);try{await go(r,t)}catch(i){throw await Sn(r,{force:!0}).catch(s=>{console.warn(`Warning: failed to clean up temp file ${r}: ${s}`)}),i}}var zt,le,pe,Ft,ut,A,ge,X,O=D(()=>{"use strict";L();zt=class{instancesDir;keysDir;constructor(e){let n=e??P.root;this.instancesDir=Lt(n,"instances"),this.keysDir=Lt(n,"keys")}async ensureDirectories(){await bn(this.instancesDir,{recursive:!0,mode:448}),await bn(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return Lt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await Gt(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!$n(n))return null;let r=await fo(n,"utf-8");return JSON.parse(r)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);$n(n)&&await Sn(n)}async list(){await this.ensureDirectories();let e=await po(this.instancesDir),n=[];for(let r of e){if(!r.endsWith(".json"))continue;let i=r.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}},le=new zt,pe=t=>le.save(t),Ft=t=>le.load(t),ut=t=>le.loadOrThrow(t),A=t=>le.loadActiveOrThrow(t),ge=t=>le.delete(t),X=()=>le.list()});var Je={};ie(Je,{GIBIL_SIZE_TARGETS:()=>No,SIZE_NAMES:()=>On,isSizeName:()=>Ro,resolveSize:()=>Oo});function Ro(t){return On.includes(t)}function Oo(t,e){let n=t.sizes().find(r=>r.name===e);if(!n){let r=t.sizes().map(i=>i.name).join(", ")||"(none)";throw new Error(`Size "${e}" is not available on this provider. Available: ${r}`)}return n.nativeType}var On,No,Ve=D(()=>{"use strict";On=["small","medium","large"],No={small:{vcpu:2,ramGb:4},medium:{vcpu:4,ramGb:8},large:{vcpu:8,ramGb:16}}});var Un={};ie(Un,{JobStore:()=>vt,deleteJob:()=>Vo,deleteJobsByInstance:()=>Ye,listJobs:()=>je,listJobsByInstance:()=>qo,loadJob:()=>Jo,loadJobOrThrow:()=>ue,saveJob:()=>V});import{readFile as Fo,mkdir as zn,rm as Uo,readdir as Bo}from"fs/promises";import{existsSync as Gn}from"fs";import{join as Fn}from"path";var vt,ve,V,Jo,ue,Vo,je,qo,Ye,we=D(()=>{"use strict";L();O();vt=class{jobsDir;constructor(e){let n=e??P.root;this.jobsDir=Fn(n,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return Fn(this.jobsDir,`${e}.json`)}async save(e){await zn(this.jobsDir,{recursive:!0,mode:448}),await Gt(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.jobFile(e);if(!Gn(n))return null;let r=await Fo(n,"utf-8");return JSON.parse(r)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(e){let n=this.jobFile(e);Gn(n)&&await Uo(n)}async list(){await zn(this.jobsDir,{recursive:!0,mode:448});let e=await Bo(this.jobsDir),n=[];for(let r of e){if(!r.endsWith(".json"))continue;let i=r.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}async listByInstance(e){return(await this.list()).filter(r=>r.instance===e)}async deleteByInstance(e){let n=await this.listByInstance(e);for(let r of n)await this.delete(r.id)}},ve=new vt,V=t=>ve.save(t),Jo=t=>ve.load(t),ue=t=>ve.loadOrThrow(t),Vo=t=>ve.delete(t),je=()=>ve.list(),qo=t=>ve.listByInstance(t),Ye=t=>ve.deleteByInstance(t)});import{Command as Ui}from"commander";import{readFileSync as Bi}from"fs";import{fileURLToPath as Ji}from"url";import{dirname as Vi,join as qi}from"path";ke();L();import{mkdir as io,rm as vn,readFile as so,chmod as ao}from"fs/promises";import{existsSync as wn}from"fs";import{execFile as co}from"child_process";import{promisify as lo}from"util";var uo=lo(co);async function lt(t){let e=P.keyDir(t);wn(e)&&await vn(e,{recursive:!0}),await io(e,{recursive:!0});let n=P.privateKey(t),r=P.publicKey(t);await uo("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await ao(n,384);let i=await so(r,"utf-8");return{privateKeyPath:n,publicKeyPath:r,publicKey:i.trim()}}async function ne(t){let e=P.keyDir(t);wn(e)&&await vn(e,{recursive:!0})}L();E();R();O();import{Client as Pn}from"ssh2";import{readFile as kn}from"fs/promises";import{createHash as ho,timingSafeEqual as yo}from"crypto";function Ut(t){return`SHA256:${ho("sha256").update(t).digest("base64").replace(/=+$/,"")}`}function vo(t){if(t.length<4)throw new Error("Host key buffer too short");let e=t.readUInt32BE(0);if(e===0||4+e>t.length)throw new Error("Host key buffer length prefix is invalid");return t.subarray(4,4+e).toString("ascii")}function xn(t,e,n){let r=vo(n),i=n.toString("base64");return`${e===22?t:`[${t}]:${e}`} ${r} ${i}
24
+ `}function wo(t){return t.toString("base64")}function dt(t){return Buffer.from(t,"base64")}function bo(t,e){if(t.length!==e.length)return!1;let n=Buffer.from(t,"utf8"),r=Buffer.from(e,"utf8");return n.length!==r.length?!1:yo(n,r)}function Bt(t,e){let n=wo(e),r=Ut(e);return t?bo(t,n)?{outcome:"verified",fingerprint:r,presentedBase64:n}:{outcome:"mismatch",fingerprint:r,presentedBase64:n}:{outcome:"pinned",fingerprint:r,presentedBase64:n}}async function _n(t,e,n){let r=await t.load(e);if(!r)throw new Error(`Instance "${e}" not found \u2014 cannot persist host key.`);await t.save({...r,hostPublicKey:n})}var In=(t,e)=>_n(le,t,e);async function k(t){let{instanceName:e,ip:n,command:r,stream:i=!1,timeoutMs:s=3e4,port:a=22}=t,l=await kn(P.privateKey(e),"utf-8"),c=(await Ft(e))?.hostPublicKey,u=null;return new Promise((d,f)=>{let p=new Pn,g="",y="",v=null,S=!1;p.on("ready",()=>{o.debug(`SSH connected to ${n}`),(u?.outcome==="pinned"?In(e,u.presentedBase64).catch($=>{o.debug(`Failed to persist host key for ${e}: ${$}`)}):Promise.resolve()).finally(()=>{p.exec(r,($,b)=>{if($)return p.end(),f($);v=setTimeout(()=>{S||(S=!0,p.destroy(),f(new Error(`Command timed out after ${s/1e3}s on ${n}`)))},s),b.on("data",_=>{let T=_.toString();g+=T,i&&process.stdout.write(T)}),b.stderr.on("data",_=>{let T=_.toString();y+=T,i&&process.stderr.write(T)}),b.on("close",_=>{v&&clearTimeout(v),!S&&(S=!0,p.end(),d({stdout:g,stderr:y,exitCode:_??0}))})})})}).on("error",w=>{if(v&&clearTimeout(v),S)return;if(S=!0,u?.outcome==="mismatch"){let b=c?Ut(dt(c)):"(unknown)";return f(new Error(`SSH host key for ${n} does not match the pinned fingerprint for "${e}".
25
+ Pinned: ${b}
26
+ Presented: ${u.fingerprint}
27
+ This usually means a man-in-the-middle attempt, or the instance was rebuilt outside gibil. If the rebuild was expected, run: gibil destroy ${e} && gibil create --name ${e} ...`))}let $="";w.code==="ECONNREFUSED"?$=" (instance may have been destroyed or is still booting)":w.code==="EHOSTUNREACH"?$=" (IP unreachable \u2014 instance may not be running)":w.code==="ETIMEDOUT"&&($=" (connection timed out \u2014 check if instance is running with 'gibil list')"),f(new Error(`SSH connection to ${n} failed: ${w.message}${$}`))}).connect({host:n,port:a,username:"root",privateKey:l,readyTimeout:s,hostVerifier:w=>(u=Bt(c,w),u.outcome!=="mismatch"),agent:process.env.SSH_AUTH_SOCK,agentForward:!!process.env.SSH_AUTH_SOCK})})}function En(t){let{instanceName:e,ip:n,filePath:r,timeoutMs:i=3e4}=t,s=null,a=!1;return(async()=>{try{let l=await kn(P.privateKey(e),"utf-8"),c=(await Ft(e))?.hostPublicKey,u=null;s=new Pn,await new Promise((d,f)=>{s.on("ready",()=>{(u?.outcome==="pinned"?In(e,u.presentedBase64).catch(()=>{}):Promise.resolve()).finally(()=>{s.exec(`tail -f ${r} 2>/dev/null`,(g,y)=>{if(g)return s.end(),f(g);y.on("data",v=>{a||process.stdout.write(m(v.toString()))}),y.stderr.on("data",v=>{a||process.stderr.write(m(v.toString()))}),y.on("close",()=>{s.end(),d()})})})}).on("error",p=>{a||o.debug(`Verbose log tail failed: ${p.message}`),f(p)}).connect({host:n,port:22,username:"root",privateKey:l,readyTimeout:i,hostVerifier:p=>(u=Bt(c,p),u.outcome!=="mismatch")})})}catch{}})(),{abort(){if(a=!0,s)try{s.end()}catch{}}}}async function ft(t,e,n=12e4){let r=Date.now(),i=5e3;for(;Date.now()-r<n;)try{await k({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{o.debug(`SSH not ready on ${e}, retrying...`),await new Promise(s=>setTimeout(s,i))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function he(t){let{repo:e,config:n,ttlMinutes:r,githubToken:i,gitIdentity:s}=t,a=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];i&&a.push(`export GITHUB_TOKEN=${U(i)}`),a.push("","# Base packages","apt-get update -qq","apt-get install -y -qq git curl wget build-essential unzip > /dev/null 2>&1","","# Install GitHub CLI","if ! type gh > /dev/null 2>&1; then"," curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null"," chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg"," ARCH=$(dpkg --print-architecture)",' echo "deb [arch=${ARCH} signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list'," apt-get update -qq && apt-get install -y -qq gh > /dev/null 2>&1","fi","");let l=n?.image??"node:20";if(a.push(...$o(l)),t.agent){let c=_o(t.agent);c&&(a.push(`# Install ${t.agent} + tmux`),t.agent==="aider"&&a.push("apt-get install -y -qq python3-pip > /dev/null 2>&1"),a.push(`${c} > /dev/null 2>&1`,"apt-get install -y -qq tmux > /dev/null 2>&1",""))}if(n?.services&&n.services.length>0){a.push(...So()),a.push("");for(let c of n.services)a.push(...xo(c))}if(n?.env){a.push("# Environment variables");for(let[c,u]of Object.entries(n.env))a.push(`export ${c}=${U(u)}`),a.push(`echo ${U(`${c}=${u}`)} >> /etc/environment`);a.push("")}if(a.push("# Configure git"),s?(a.push(`git config --global user.email ${U(s.email)}`),a.push(`git config --global user.name ${U(s.name)}`),s.signingKey&&(a.push("git config --global gpg.format ssh"),a.push(`git config --global user.signingkey ${U("key::"+s.signingKey)}`),a.push("git config --global commit.gpgsign true"),a.push("git config --global tag.gpgsign true"),a.push("mkdir -p /root/.ssh"),a.push(`echo ${U(s.email+" "+s.signingKey)} > /root/.ssh/allowed_signers`),a.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(a.push("git config --global user.email 'gibil@bot.dev'"),a.push("git config --global user.name 'Gibil Bot'")),a.push(""),e){let c=e.match(/github\.com\/([^/]+\/[^/.]+)/);if(a.push("# Clone repository"),a.push("cd /root"),c){let u=c[1];a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),a.push("else"),a.push(` CLONE_URL='https://github.com/${u}.git'`),a.push("fi"),a.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else a.push(`timeout 300 git clone ${U(e)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`);a.push("cd /root/project"),a.push(""),a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),c&&a.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${c[1]}.git"`),a.push("fi"),a.push("")}if(r&&r>0&&(a.push("# Auto-destroy after TTL"),a.push(`echo "shutdown -h now" | at now + ${r} minutes 2>/dev/null || true`),a.push(`(sleep ${r*60} && shutdown -h now) &`),a.push("")),a.push("# Clean up cloud-init secrets"),a.push("rm -f /var/lib/cloud/instance/user-data.txt"),a.push(""),a.push("# Signal that infrastructure is ready"),a.push("touch /root/.gibil-ready"),a.push('echo "Gibil infrastructure ready"'),a.push(""),e&&n?.tasks&&n.tasks.length>0){a.push("# Run project tasks"),a.push("cd /root/project");for(let c of n.tasks)a.push(`echo '\u25B6 Running task: '${U(c.name)}`),a.push(`if ! ${c.command}; then`),a.push(` echo '\u2717 Task failed: '${U(c.name)}`),a.push(" touch /root/.gibil-tasks-failed"),a.push("fi");a.push(""),a.push("# Signal tasks complete"),a.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),a.push(" touch /root/.gibil-tasks-done"),a.push(' echo "Gibil tasks complete"'),a.push("else"),a.push(' echo "Gibil tasks finished with errors"'),a.push("fi")}return a.join(`
28
+ `)}function $o(t){let e=[];if(t.startsWith("node:")){let n=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("")}else if(t.startsWith("python:")){let n=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let n=t.split(":")[1]??"1.22";e.push("# Install Go"),e.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),e.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),e.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),e.push("export PATH=$PATH:/usr/local/go/bin"),e.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),e.push("")}else e.push("# Install Node.js (default)"),e.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("");return e}function So(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function xo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let r=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(r+=` -p ${t.port}:${t.port}`),t.env)for(let[i,s]of Object.entries(t.env))r+=` -e ${i}=${U(s)}`;return r+=` ${U(t.image)}`,e.push(r),e.push(""),e}var Tn={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},Ie={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},Q=Object.keys(Tn);function _o(t){return Tn[t]??null}function U(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as Po}from"fs/promises";import{existsSync as Cn,statSync as ko}from"fs";import{join as Io}from"path";import{parse as jn}from"yaml";async function Ee(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,n,r]=e,i=`https://raw.githubusercontent.com/${n}/${r}/HEAD/.gibil.yml`;try{let s={};process.env.GITHUB_TOKEN&&(s.Authorization=`token ${process.env.GITHUB_TOKEN}`);let a=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:s});if(!a.ok)return null;let l=await a.text();return To(l)}catch{return null}}var Eo=".gibil.yml";async function Te(t){let e;if(Cn(t)&&ko(t).isFile()?e=t:e=Io(t,Eo),!Cn(e))return null;let n=await Po(e,"utf-8"),r=jn(n);return Nn(r)}function To(t){let e=jn(t);return Nn(e)}function Nn(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(r=>{let i=r;if(typeof i.name!="string"||typeof i.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:i.name,image:i.image,port:typeof i.port=="number"?i.port:void 0,env:An(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(r=>{let i=r;if(typeof i.name!="string"||typeof i.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:i.name,command:i.command}})),e.env!==void 0&&(n.env=An(e.env,"top-level")),n}function An(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[r,i]of Object.entries(t))if(typeof i=="string")n[r]=i;else if(typeof i=="number"||typeof i=="boolean")n[r]=String(i);else throw new Error(`env.${r} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(n).length>0?n:void 0}O();import{randomBytes as Co}from"crypto";function ye(t=6){return Co(Math.ceil(t/2)).toString("hex").slice(0,t)}function Be(){return`gibil-${ye()}`}function Rn(){return`fleet-${ye(8)}`}function mt(){return`j-${ye(8)}`}L();E();var Ao=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function pt(t){if(!Ao.test(t))throw new Error(`Invalid instance name "${t}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return t}function gt(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}var J=525600,jo={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function Ce(t){let e=t.trim().toLowerCase();if(/^\d+$/.test(e)){let l=parseInt(e,10);if(l<=0)throw new Error(`TTL must be positive, got "${t}"`);if(l>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got ${l} minutes.`);return l}let n=e.match(/^(\d+)(mo|[mhdwy])$/);if(!n)throw new Error(`Invalid TTL "${t}". Use a number (minutes) or a duration: 2h, 7d, 1w, 1mo, 3mo, 6mo, 1y`);let r=parseInt(n[1],10),i=n[2],s=jo[i],a=r*s;if(a<=0)throw new Error(`TTL must be positive, got "${t}"`);if(a>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got "${t}" = ${a} minutes.`);return a}G();R();import{execSync as ht}from"child_process";import{readFileSync as Ho}from"fs";var Jt="key::";function Mo(){try{let t=ht("git config user.name",{encoding:"utf-8"}).trim(),e=ht("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(ht("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=ht("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=Ho(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(Jt))&&(n=i.startsWith(Jt)?i.slice(Jt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function yt(t,e,n){o.step("Generating SSH keys...");let r=await lt(e),i,s;try{o.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${ye(4)}`,r.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&o.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let a=Mo(),l=he({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a,agent:n.agent}),c=o.spin(`Creating server on ${n.providerName??"hetzner"}...`),u=await t.createServer(e,i.id,l,n.serverType??n.config?.server_type,n.location??n.config?.location);s=u.id,c.succeed("Server created");let d=o.spin("VM booting..."),p=(await t.waitForReady(u.id)).ipv4;d.succeed(`VM running at ${p}`);let g=new Date,y={name:e,serverId:u.id,ip:p,sshKeyId:i.id,keyPath:P.privateKey(e),status:"running",createdAt:g.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(g.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:a,provider:n.providerName??"hetzner"};await pe(y);let v=o.spin("Waiting for SSH...");if(await ft(e,p),v.succeed("SSH ready"),n.repo||n.config){let S=o.spin("Provisioning (runtime, repo, deps)..."),w;n.verbose&&!n.json&&(w=En({instanceName:e,ip:p,filePath:"/var/log/cloud-init-output.log"}));let $=36e4,b=5e3,_=Date.now(),T=!1;for(;Date.now()-_<$;){try{if((await k({instanceName:e,ip:p,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){T=!0;break}}catch{}await new Promise(oe=>setTimeout(oe,b))}if(w?.abort(),T)S.succeed("Provisioning complete");else{S.fail("Provisioning may have failed");try{let oe=await k({instanceName:e,ip:p,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});o.info(oe.stdout)}catch{o.warn("Could not read cloud-init log.")}}}return y}catch(a){throw o.error(`Failed to create instance "${e}", cleaning up...`),s&&await t.destroyServer(s).catch(l=>o.warn(`Could not destroy server ${s}: ${l instanceof Error?l.message:String(l)}`)),i&&await t.deleteSSHKey(i.id).catch(l=>o.warn(`Could not delete SSH key ${i.id}: ${l instanceof Error?l.message:String(l)}`)),await ne(e).catch(l=>o.warn(`Could not clean up local SSH keys: ${l instanceof Error?l.message:String(l)}`)),a}}function Hn(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`gibil ssh ${t.name}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}function Mn(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <duration>","Auto-destroy timer (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)","60").option("-c, --config <path>","Path to .gibil.yml config").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("-q, --quiet","Suppress non-essential output").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async e=>{e.json&&I(!0);let n=Ce(e.ttl??"60"),r=gt(e.fleet??"1","Fleet count");if(r>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&pt(e.name),!e.agent){let d=await Fe();d&&(e.agent=d)}if(e.agent&&!Q.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${Q.join(", ")}`);let i={};if(e.env)for(let d of e.env){let f=d.indexOf("=");if(f<=0)throw new Error(`Invalid --env format: "${d}". Use KEY=VALUE.`);i[d.slice(0,f)]=d.slice(f+1)}i.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=i.GITHUB_TOKEN);let s=null;if(e.config?s=await Te(e.config):e.repo?s=await Ee(e.repo)??await Te(process.cwd()):s=await Te(process.cwd()),Object.keys(i).length>0&&(s||(s={}),s.env={...s.env,...i}),e.dryRun){let d=e.name??Be(),f=e.provider??"hetzner",p=e.serverType??s?.server_type;if(!p&&e.size){let{isSizeName:$}=await Promise.resolve().then(()=>(Ve(),Je));if(!$(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);let{PROVIDER_CATALOG:b}=await Promise.resolve().then(()=>(ze(),cn));p=b[f]?.sizes.find(T=>T.name===e.size)?.nativeType}p||(p=f==="vultr"?"vc2-2c-4gb":"cax11");let g=p,y=e.location??s?.location??(f==="vultr"?"nrt":"nbg1"),v=s?.image??"node:20",S=he({repo:e.repo,config:s??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),w={name:d,serverType:g,location:y,image:v,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:S};e.json?o.json(w):(o.info(""),o.info(h("Dry run \u2014 no server will be created")),o.info(""),o.info(` ${m("Name:")} ${d}`),o.info(` ${m("Server type:")} ${g}`),o.info(` ${m("Location:")} ${y}`),o.info(` ${m("Image:")} ${v}`),o.info(` ${m("TTL:")} ${n} minutes`),e.repo&&o.info(` ${m("Repo:")} ${e.repo}`),e.agent&&o.info(` ${m("Agent:")} ${e.agent}`),o.info(""),o.info("Cloud-init script:"),o.info("\u2500".repeat(17)),o.info(S));return}let a=await M();if(a){o.info("Verifying API key...");let d=await ce(a);o.info(` Authenticated as ${d.user.email} (${d.user.plan})`)}if(e.agent&&!Ie[e.agent]?.some(f=>s?.env?.[f]||i[f])){let f=Ie[e.agent]?.join(" or ")??"";o.warn(`${e.agent} needs ${f}. SSH in and export it (recommended) or pass with --env.`)}let l=e.provider??"hetzner",c=await F.get(l),u=e.serverType;if(!u&&e.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ve(),Je));if(!d(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);u=f(c,e.size)}if(r===1){let d=e.name??Be(),f=Date.now(),p=o.spin(`Forging "${d}"...`),g=await yt(c,d,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,agent:e.agent,verbose:e.verbose}),y=((Date.now()-f)/1e3).toFixed(1);p.succeed(C.createReady(d,y)),a&&await te(a,"create",g.name,e.serverType).catch(v=>o.debug(`Usage tracking failed: ${v instanceof Error?v.message:String(v)}`)),e.json?o.json(Hn(g)):(o.info(""),o.info(et("Server ready",[`${m("Name:")} ${h(g.name)}`,`${m("IP:")} ${g.ip}`,`${m("TTL:")} ${n} minutes`,`${m("SSH:")} ${h(`gibil ssh ${g.name}`)}`])),o.info(""),o.info(m(" Try:")),o.info(` ${h(`gibil run ${g.name} "<your test command>"`)}`),o.info(` ${h(`gibil ssh ${g.name}`)}`),o.info(` ${h(`gibil destroy ${g.name}`)}`),o.info(""))}else{let d=Rn(),f=e.name??"gibil",p=Date.now(),g=o.spin(`Forging fleet "${d}" \u2014 ${r} servers...`),y=Array.from({length:r},(b,_)=>`${f}-${_+1}-${d.slice(6)}`),v=await Promise.allSettled(y.map(b=>yt(c,b,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,fleetId:d,agent:e.agent,verbose:e.verbose}))),S=[],w=[];for(let b=0;b<v.length;b++){let _=v[b];_.status==="fulfilled"?S.push(_.value):w.push(`${y[b]}: ${_.reason instanceof Error?_.reason.message:String(_.reason)}`)}let $=((Date.now()-p)/1e3).toFixed(1);if(g.succeed(C.fleetReady(S.length,r)+` ${m(`(${$}s)`)}`),a&&await Promise.all(S.map(b=>te(a,"create",b.name,e.serverType).catch(_=>o.debug(`Usage tracking failed for ${b.name}: ${_ instanceof Error?_.message:String(_)}`)))),e.json)o.json({fleet_id:d,instances:S.map(Hn),errors:w});else{o.info("");for(let b of S)o.info(` ${N} ${h(b.name)} ${m("\u2192")} ${b.ip}`);for(let b of w)o.info(` ${Le} ${b}`);o.info("")}}})}O();import{spawn as Wo}from"child_process";import{spawn as zo}from"child_process";import{existsSync as Go}from"fs";L();import{writeFile as Ko,mkdir as Do,rm as Lo}from"fs/promises";var Kn={dir:P.knownHostsDir,file:P.knownHostsFile};async function Ae(t,e=Kn){if(!t.hostPublicKey)return null;await Do(e.dir,{recursive:!0,mode:448});let n=e.file(t.name),r=dt(t.hostPublicKey),i=xn(t.ip,22,r);return await Ko(n,i,{mode:384}),n}async function Dn(t,e=Kn){await Lo(e.file(t),{force:!0})}E();R();function qe(t){if(t.includes(":")){let e=t.split(":");if(e.length!==3)throw new Error(`Invalid port mapping "${t}". Use PORT or LOCAL:HOST:REMOTE.`);let[n,,r]=e;Vt(n,t),Vt(r,t)}else Vt(t,t)}function Vt(t,e){let n=parseInt(t,10);if(isNaN(n)||n<1||n>65535||String(n)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}async function Ln(t,e){if(!Go(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=await Ae(t);n||o.warn(m(` No pinned host key for ${t.name} (instance pre-dates v0.4.x). Tunnels are insecure on this hop; running any \`gibil run ${t.name}\` will pin it.`));let r=[];for(let i of e){qe(i);let s=i.includes(":")?i:`${i}:localhost:${i}`,a=i.includes(":")?i.split(":")[0]:i;r.push(a);let l=["-f","-N","-L",s,"-i",t.keyPath,"-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];n?l.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${n}`):l.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null"),l.push(`root@${t.ip}`),zo("ssh",l,{stdio:"ignore",detached:!0}).unref()}return r}E();R();E();O();we();var Yo=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function be(t){if(!(t instanceof Error))return!1;let e=t.message;return Yo.some(n=>e.includes(n))}async function q(t){try{let{providerRegistry:e}=await Promise.resolve().then(()=>(ke(),yn));return await(await e.forInstance(t)).getServer(t.serverId),"still_exists"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await ne(t.name),await Ye(t.name),await ge(t.name),o.warn(`Instance "${t.name}" no longer exists on the provider \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function Zo(t){let e=["-A","-i",t.keyPath,"-o","LogLevel=ERROR"];if(t.knownHostsPath?e.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${t.knownHostsPath}`):e.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null"),t.ports&&t.ports.length>0)for(let n of t.ports){qe(n);let r=n.includes(":")?n:`${n}:localhost:${n}`;e.push("-L",r)}return e.push(`root@${t.ip}`),e}function Bn(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000 --port 5432)").action(async(e,n)=>{let r=await A(e),i=await Ae(r);i||o.warn(m(` No pinned host key for ${e} (instance pre-dates v0.4.x). Connection is insecure on this hop; running any \`gibil run ${e}\` will pin it.`));let s=Zo({ip:r.ip,keyPath:r.keyPath,knownHostsPath:i??void 0,ports:n.port});if(n.port&&n.port.length>0){o.info("");for(let l of n.port){let c=l.includes(":")?l.split(":")[0]:l;o.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${l}`)}o.info(""),o.info(m(" Tunnel active while SSH session is open. Ctrl+C to stop.")),o.info("")}Wo("ssh",s,{stdio:"inherit"}).on("exit",l=>{let c=()=>process.exit(l??0);l===255?q(r).then(c,c):c()})})}O();we();E();function Jn(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").option("--timeout <seconds>","Command timeout in seconds (default: 30)").option("-b, --background","Run in background, return job ID immediately").action(async(e,n,r)=>{r.json&&I(!0);let i=await A(e),s=n.join(" "),a=r.timeout?gt(r.timeout,"Timeout")*1e3:3e4;if(r.background){let c=mt(),u="/root/.gibil-jobs",d=`${u}/${c}.log`,f=`${u}/${c}.exit`,p=`${u}/${c}.pid`,g=`${u}/${c}.sh`,y=["#!/bin/bash",`nohup bash -c '${s.replace(/'/g,"'\\''")}' > ${d} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${p}`,`(wait $BGPID 2>/dev/null; echo $? > ${f}) &`,"echo $BGPID"].join(`
29
+ `),v=Buffer.from(y).toString("base64"),S=`mkdir -p ${u} && echo '${v}' | base64 -d > ${g} && chmod +x ${g} && bash ${g}`,w;try{w=await k({instanceName:e,ip:i.ip,command:S,timeoutMs:1e4})}catch(b){throw be(b)&&await q(i)==="cleaned"&&process.exit(1),b}let $=parseInt(w.stdout.trim(),10);isNaN($)&&(o.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await V({id:c,instance:e,command:s,pid:$,status:"running",startedAt:new Date().toISOString()}),r.json?o.json({job_id:c,instance:e,status:"running",pid:$}):(o.info(`Background job started: ${c} (PID ${$})`),o.info(` Poll: gibil job ${c}`));return}o.info(`Running on "${e}" (${i.ip}): ${s}`);let l;try{l=await k({instanceName:e,ip:i.ip,command:s,stream:!r.json,timeoutMs:a})}catch(c){throw be(c)&&await q(i)==="cleaned"&&process.exit(1),c}r.json?o.json({instance:e,command:s,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&o.error(`Command exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}ke();import{createInterface as di}from"readline";O();we();E();G();R();L();import{createHash as Vn}from"crypto";import{readFile as Wn,writeFile as Xo,mkdir as Qo}from"fs/promises";import{existsSync as wt,readFileSync as ei}from"fs";import{hostname as qn,userInfo as ti,platform as ni,arch as ri}from"os";import{join as Oe,dirname as Zn}from"path";import{fork as oi}from"child_process";import{fileURLToPath as Xn}from"url";function Qn(){return Oe(P.root,"device_id")}function ii(){return Oe(P.root,"config.json")}var si=process.env.GIBIL_TELEMETRY_URL??"https://zopdxjruwktjyjunitrv.supabase.co/functions/v1/telemetry-ingest",ai="tk_alpha_09a93302e0f3e73417a9e9dbfc500a61",We=null,Ne=null;function ci(){try{let t=ti(),e=`${qn()}:${t.username}:${t.homedir}`;return Vn("sha256").update(e).digest("hex").slice(0,16)}catch{return Vn("sha256").update(`${qn()}:${Date.now()}`).digest("hex").slice(0,16)}}async function er(){if(We)return We;let t=Qn();if(wt(t)){let n=(await Wn(t,"utf-8")).trim();if(n.length>0)return We=n,We}let e=ci();return await Qo(P.root,{recursive:!0,mode:448}),await Xo(t,e,{mode:384}),We=e,e}async function qt(){if(Ne!==null)return Ne;let t=process.env.GIBIL_TELEMETRY;if(t!==void 0)return Ne=!["0","false","off","no"].includes(t.toLowerCase()),Ne;try{let e=ii();if(wt(e)&&JSON.parse(await Wn(e,"utf-8")).telemetry===!1)return Ne=!1,!1}catch{}return Ne=!0,!0}async function tr(){return wt(Qn())?!1:(await er(),!0)}var Re=null;function li(){if(Re)return Re;let t=Zn(Xn(import.meta.url));for(let e of["../package.json","../../package.json"])try{return Re=JSON.parse(ei(Oe(t,e),"utf-8")).version??"0.0.0",Re}catch{}return Re="0.0.0",Re}async function Ze(t){if(!await qt())return;let e=await er();if(!e)return;let n={...t,device_id:e,cli_version:li(),timestamp:new Date().toISOString(),os:ni(),arch:ri(),node_version:process.version},r=Zn(Xn(import.meta.url)),s=[Oe(r,"telemetry-send.js"),Oe(r,"..","utils","telemetry-send.js"),Oe(r,"telemetry-send.ts")].find(a=>wt(a));if(s)try{oi(s,{detached:!0,stdio:"ignore",...s.endsWith(".ts")?{execArgv:["--import","tsx"]}:{},env:{...process.env,TELEMETRY_PAYLOAD:JSON.stringify(n),TELEMETRY_ENDPOINT:si,TELEMETRY_INGEST_KEY:ai}}).unref()}catch{}}var Yn=new Map,ui=5e3;async function nr(t){let e=Date.now(),n=Yn.get(t)??0;e-n<ui||(Yn.set(t,e),await Ze({event:"mcp_tool",tool:t}))}function rr(t){return t.slice(3).filter(e=>e.startsWith("-")).map(e=>e.replace(/=.*$/,""))}async function fi(t){let e=t.length;o.info(""),o.info(h(`This will destroy ${e} running server${e===1?"":"s"}:`)),o.info("");for(let i of t){let s=Math.round((Date.now()-new Date(i.createdAt).getTime())/6e4),a=s<60?`${s}m old`:`${Math.floor(s/60)}h ${s%60}m old`;o.info(` ${m("\u2022")} ${h(i.name)} ${m("\u2192")} ${i.ip} ${m(`(${a})`)}`)}o.info(""),o.info(m("Type 'yes' to confirm, anything else to cancel:"));let n=di({input:process.stdin,output:process.stdout}),r=await new Promise(i=>n.question("> ",i));return n.close(),r.trim().toLowerCase()==="yes"}async function or(t){let e=await ut(t),n=await F.forInstance(e);o.info(`Destroying instance "${t}" (server ${e.serverId})...`);try{await n.destroyServer(e.serverId)}catch(s){o.warn(`Could not delete server ${e.serverId}: ${s instanceof Error?s.message:String(s)}`)}try{await n.deleteSSHKey(e.sshKeyId)}catch(s){o.warn(`Could not delete SSH key ${e.sshKeyId}: ${s instanceof Error?s.message:String(s)}`)}await ne(t),await Ye(t),await Dn(t),await ge(t);let r=await M();r&&await te(r,"destroy",t).catch(s=>o.warn(`Usage tracking failed (billing may be inaccurate): ${s instanceof Error?s.message:String(s)}`));let i=Math.round((Date.now()-new Date(e.createdAt).getTime())/6e4);await Ze({event:"lifecycle",duration_minutes:i}),o.info(` ${N} ${C.destroySingle(t)}`)}function ir(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").option("-y, --yes","Skip the confirmation prompt for --all (required in non-interactive contexts)").action(async(e,n)=>{if(n.json&&I(!0),n.all){let r=await X();if(r.length===0){n.json?o.json({destroyed:[],failed:[]}):o.info(C.noInstances);return}if(!n.yes&&(n.json&&(o.json({error:"confirmation required",message:`--all would destroy ${r.length} server(s). Re-run with --yes to confirm.`,instances:r.map(u=>({name:u.name,ip:u.ip}))}),process.exit(1)),process.stdin.isTTY||(o.error("stdin is not a TTY. Re-run with --yes to confirm in non-interactive contexts."),process.exit(1)),!await fi(r))){o.info(""),o.info("Cancelled. No servers were destroyed.");return}o.info(`Destroying ${r.length} instance(s)...`);let i=3,s=[];for(let c=0;c<r.length;c+=i){let u=r.slice(c,c+i),d=await Promise.allSettled(u.map(f=>or(f.name)));s.push(...d)}let a=[],l=[];for(let c=0;c<s.length;c++)if(s[c].status==="fulfilled")a.push(r[c].name);else{let u=s[c].reason;l.push(`${r[c].name}: ${u instanceof Error?u.message:String(u)}`)}n.json?o.json({destroyed:a,failed:l}):l.length===0?o.info(`
30
+ ${C.destroyAll}`):o.info(`
31
+ ${a.length} destroyed, ${l.length} failed`)}else e||(o.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1)),await or(e),n.json&&o.json({destroyed:[e]})})}O();E();R();function sr(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await X();if(n.length===0){e.json?o.json({instances:[]}):o.info(C.noInstances);return}let r=n.map(i=>{let s=Math.max(0,Math.floor((new Date(i.expiresAt).getTime()-Date.now())/1e3));return{name:i.name,ip:i.ip,ssh:`gibil ssh ${i.name}`,status:i.status,ttl_remaining:s,created_at:i.createdAt,fleet_id:i.fleetId,provider:i.provider??"hetzner"}});if(e.json){o.json({instances:r});return}o.info(m(`${"NAME".padEnd(28)} ${"PROVIDER".padEnd(10)} ${"IP".padEnd(18)} ${"STATUS".padEnd(11)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),o.info(m("\u2500".repeat(90)));for(let i of r){let s=ar(i.ttl_remaining),a=mi(i.created_at),l=i.name.padEnd(28),c=i.provider.padEnd(10),u=i.status.padEnd(11),d=s.padEnd(10),f=a.padEnd(10),p=i.status==="running"?ee(u):se(u),g=i.ttl_remaining<=300?se(d):d;o.info(`${h(l)} ${m(c)} ${i.ip.padEnd(18)} ${p} ${g} ${m(f)}`)}o.info(`
32
+ ${m(`${r.length} server(s)`)}`)})}function ar(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=Math.floor(e/60),r=Math.floor(n/24);if(r>=1){let i=n%24;return i>0?`${r}d ${i}h`:`${r}d`}return n>=1?`${n}h ${e%60}m`:`${e}m ${t%60}s`}function mi(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return ar(n)}O();E();function cr(t){t.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <duration>","New TTL from now (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let r=await A(e),i=Ce(n.ttl);try{await k({instanceName:e,ip:r.ip,command:["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`,`(sleep ${i*60} && shutdown -h now) &`].join(" && ")})}catch(a){throw be(a)&&await q(r)==="cleaned"&&process.exit(1),a}let s=new Date(Date.now()+i*6e4).toISOString();r.ttlMinutes=i,r.expiresAt=s,await pe(r),n.json?o.json({name:r.name,ttl_minutes:i,expires_at:s}):o.info(`\u2713 Extended "${e}" TTL to ${i} minutes (expires ${s})`)})}import{readFile as pi}from"fs/promises";import{randomBytes as gi}from"crypto";O();E();function lr(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let r=await A(e),i=await pi(n.script,"utf-8");o.info(`Uploading and running script "${n.script}" on "${e}"...`);let s=Buffer.from(i).toString("base64"),a=`/tmp/gibil-script-${gi(4).toString("hex")}.sh`,l;try{l=await k({instanceName:e,ip:r.ip,command:`echo '${s}' | base64 -d > ${a} && chmod +x ${a} && ${a}; EXIT=$?; rm -f ${a}; exit $EXIT`,stream:!n.json})}catch(c){throw be(c)&&await q(r)==="cleaned"&&process.exit(1),c}n.json?o.json({instance:e,script:n.script,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&o.error(`Script exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}G();E();R();import{createInterface as hi}from"readline";function ur(t){let e=hi({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,r=>{e.close(),n(r.trim())})})}function dr(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=n.key??process.env.GIBIL_API_KEY;r||(r=await ur("Enter your API key: ")),r||(o.error("No API key provided."),process.exit(1)),r.startsWith("pk_")||(o.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),o.info("Verifying API key...");try{let i=await ce(r);await Tt(r),n.json?o.json({authenticated:!0,email:i.user.email,plan:i.user.plan}):(o.info(C.authSuccess),o.detail("Email",i.user.email),o.detail("Plan","alpha (free)"))}catch(i){o.error(i instanceof Error?i.message:String(i)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let r=n.token;r||(r=await ur("Enter your Hetzner API token: ")),r||(o.error("No token provided."),process.exit(1));try{let s=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();s.error&&(o.error(`Invalid token: ${s.error.message}`),process.exit(1))}catch(i){o.error(`Could not verify token: ${i instanceof Error?i.message:"Check your network."}`),process.exit(1)}await jt(r),o.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await Ct(),o.info(C.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=await M();if(!r){n.json?o.json({authenticated:!1}):o.info(`Not logged in. Run ${h("gibil auth login")} to authenticate.`);return}try{let i=await ce(r);n.json?o.json({authenticated:!0,email:i.user.email,plan:i.user.plan,limits:i.limits}):(o.success(`Authenticated as ${i.user.email}`),o.detail("Plan",i.user.plan),o.detail("Concurrent servers",String(i.limits.max_concurrent)),o.detail("Hours remaining",String(i.limits.remaining_hours)))}catch{n.json?o.json({authenticated:!1,error:"Key verification failed"}):o.error(`Stored API key is invalid. Run ${h("gibil auth login")} to re-authenticate.`)}})}G();E();function fr(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await M();n||(o.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let r=await Rt(n);e.json?o.json(r):(o.info("Plan: alpha (free)"),o.info(`VM hours used: ${r.vm_hours_used.toFixed(1)}h`),o.info(`Active instances: ${r.active_instances}`))}catch(r){o.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}import{McpServer as yi}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as vi}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as x}from"zod";ke();O();L();import{execSync as bt}from"child_process";import{readFileSync as wi}from"fs";we();O();we();E();async function Yt(t){let e=await ue(t);if(e.status!=="running")return{status:e.status,exitCode:e.exitCode};let n=await A(e.instance),r="/root/.gibil-jobs",i=`${r}/${t}.exit`,s=`${r}/${t}.log`,l=(await k({instanceName:e.instance,ip:n.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING"){try{if((await k({instanceName:e.instance,ip:n.ip,command:`kill -0 ${e.pid} 2>/dev/null && echo "alive" || echo "dead"`,timeoutMs:1e4})).stdout.trim()==="dead"){let y=new Date;e.status="orphaned",e.completedAt=y.toISOString(),await V(e);let v=Math.round((y.getTime()-new Date(e.startedAt).getTime())/1e3),S;try{S=(await k({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:v,stdout:S}}}catch{}return{status:"running"}}let c=parseInt(l,10),u=await k({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4}),d=c===0?"done":"failed",f=new Date,p=Math.round((f.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=d,e.exitCode=c,e.completedAt=f.toISOString(),await V(e),{status:d,exitCode:c,stdout:u.stdout,durationS:p}}function mr(t){let e=t.command("job").description("Manage background jobs");e.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n),s=await Yt(n);r.json?o.json({job_id:n,instance:i.instance,command:i.command,status:s.status,exit_code:s.exitCode,started_at:i.startedAt,duration_s:s.durationS,...s.stdout!==void 0?{stdout:s.stdout}:{}}):s.status==="running"?(o.info(`Job ${n} is still running on "${i.instance}"`),o.info(` Command: ${i.command}`),o.info(` Started: ${i.startedAt}`)):s.status==="orphaned"?(o.warn(`Job ${n} is orphaned \u2014 process died without writing exit code`),o.info(` Instance: ${i.instance}`),o.info(` Command: ${i.command}`),s.durationS!==void 0&&o.info(` Duration: ${s.durationS}s`),s.stdout&&(o.info(" Output:"),process.stdout.write(s.stdout))):(o.info(`Job ${n}: ${s.status} (exit code ${s.exitCode}, ${s.durationS}s)`),s.stdout&&process.stdout.write(s.stdout))}),e.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=await je(),i=await X(),s=new Set(i.map(a=>a.name));for(let a of r)a.status==="running"&&!s.has(a.instance)&&(a.status="orphaned",a.completedAt=new Date().toISOString(),await V(a));if(r.length===0){n.json?o.json([]):o.info("No background jobs.");return}if(n.json)o.json(r.map(a=>({job_id:a.id,instance:a.instance,command:a.command,status:a.status,started_at:a.startedAt,exit_code:a.exitCode})));else for(let a of r){let l=a.status==="running"?"\u27F3 running":a.status==="done"?"\u2713 done":a.status==="orphaned"?"\u26A0 orphaned":`\u2717 ${a.status}`;o.info(` ${a.id} ${l} ${a.instance} ${a.command}`)}}),e.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n);if(i.status!=="running"){r.json?o.json({job_id:n,status:i.status,message:"Job is not running"}):o.info(`Job ${n} is not running (status: ${i.status})`);return}let s=await A(i.instance);await k({instanceName:i.instance,ip:s.ip,command:`kill -- -${i.pid} 2>/dev/null || kill ${i.pid} 2>/dev/null || true`,timeoutMs:1e4}),i.status="cancelled",i.completedAt=new Date().toISOString(),await V(i),r.json?o.json({job_id:n,status:"cancelled"}):o.info(`Job ${n} cancelled.`)}),e.command("logs <id>").description("Fetch output of a background job").option("--json","Output result as JSON").option("-f, --follow","Follow log output (tail -f)").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n),s=await A(i.instance),a=`/root/.gibil-jobs/${n}.log`,l=r.follow?`tail -f ${a}`:`cat ${a} 2>/dev/null || echo '(no output yet)'`,c=r.follow?3e5:1e4,u=await k({instanceName:i.instance,ip:s.ip,command:l,stream:!r.json,timeoutMs:c});r.json&&o.json({job_id:n,stdout:u.stdout})})}function K(t,e,n){let r=`[${t}] ${e}`;return n&&(r+=`
29
33
 
30
- Suggestion: ${n}`),o}function Ze(t){let e=t instanceof Error?t.message:String(t);return/no active server|not found|no servers|does not exist/i.test(e)?"instance_not_found":/timeout|timed out|ETIMEDOUT/i.test(e)?"timeout":/ECONNREFUSED|EHOSTUNREACH|SSH connection|ssh2|handshake/i.test(e)?"ssh_connection":/hetzner|api|token|401|403|409|422|429|quota/i.test(e)?"provider_error":/invalid|must be|validation/i.test(e)?"validation":"provider_error"}var bt="key::";function ti(){try{let t=wt("git config user.name",{encoding:"utf-8"}).trim(),e=wt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(wt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=wt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=ei(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(bt))&&(n=i.startsWith(bt)?i.slice(bt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function Re(t,e){if(t)return t;if(e)return C(e);let o=(await Z()).filter(i=>new Date<new Date(i.expiresAt));if(o.length===0)throw new Error("No active servers. Use create_server first.");if(o.length===1)return o[0];throw new Error(`Multiple servers running: ${o.map(i=>i.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function te(t,e,n=3e4){return x({instanceName:t.name,ip:t.ip,command:e,stream:!1,timeoutMs:n})}function U(t){return`'${t.replace(/'/g,"'\\''")}'`}function ni(t){let e=t.trim(),n=e.split(",");if(n.length<5)throw new Error(`Unexpected vm_stats output (expected 5 comma-separated sections, got ${n.length}): ${e}`);let o=parseInt(n[0],10);if(isNaN(o))throw new Error(`Failed to parse CPU cores: ${n[0]}`);let i=n[1].trim().split(/\s+/),s=parseFloat(i[0]),a=parseFloat(i[1]),c=parseFloat(i[2]);if(isNaN(s)||isNaN(a)||isNaN(c))throw new Error(`Failed to parse load averages: ${n[1]}`);let l=n[2].trim().split(/\s+/),d=parseInt(l[0],10),u=parseInt(l[1],10),m=parseInt(l[2],10);if(isNaN(d)||isNaN(u)||isNaN(m))throw new Error(`Failed to parse memory: ${n[2]}`);let g=n[3].trim().split(/\s+/),p=_=>Math.round(parseFloat(_.replace(/G$/i,""))),v=p(g[0]),y=p(g[1]),b=p(g[2]);if(isNaN(v)||isNaN(y)||isNaN(b))throw new Error(`Failed to parse disk: ${n[3]}`);let w=parseInt(n[4].trim(),10);if(isNaN(w))throw new Error(`Failed to parse uptime: ${n[4]}`);return{cpu:{cores:o,load_1m:s,load_5m:a,load_15m:c},memory:{total_mb:d,used_mb:u,available_mb:m},disk:{total_gb:v,used_gb:y,available_gb:b},uptime_seconds:w}}async function ir(t){let e=null;if(t&&(e=await C(t),e.gitIdentity)){let{name:c,email:l,signingKey:d}=e.gitIdentity,u=[`git config --global user.name ${U(c)}`,`git config --global user.email ${U(l)}`];d&&u.push("git config --global gpg.format ssh",`git config --global user.signingkey ${U(bt+d)}`,"git config --global commit.gpgsign true"),te(e,u.join(" && ")).catch(()=>{})}let n=t?`gibil-${t}`:"gibil",o=new Xo({name:n,version:"0.4.0"}),i=o.tool.bind(o);o.tool=((...c)=>{let l=c[0],d=c.length-1;if(typeof c[d]=="function"){let u=c[d];c[d]=async(...m)=>(Vn(l).catch(()=>{}),u(...m))}return i(...c)}),e||(o.tool("create_server","Forge a new ephemeral server with a full Linux environment (Ubuntu 24.04, Node.js 20, pnpm). Clones the repo to /root/project and waits until fully provisioned. Picks the configured default provider unless 'provider' is set (hetzner, vultr). After creation, use vm_bash to run commands, vm_read/vm_write for files, vm_grep to search code. Destroy with destroy_server when done.",{name:S.string().optional().describe("Server name (auto-generated if omitted)"),repo:S.string().optional().describe("Git repo URL to clone on boot"),ttl:S.number().optional().describe("Auto-destroy after N minutes (default: 60, max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)"),provider:S.enum(["hetzner","vultr"]).optional().describe("Cloud provider (default: configured default \u2014 hetzner if unset). vultr gives APAC regions."),size:S.enum(["small","medium","large"]).optional().describe("Gibil size: small (2 vCPU/4 GB), medium (4 vCPU/8 GB), large (8 vCPU/16 GB). Resolved per provider \u2014 see gibil providers."),server_type:S.string().optional().describe("Provider-native SKU (overrides size). Hetzner: cax11/cax21/cax31 (ARM, fsn1/nbg1) or cpx21/cpx31 (x86). Vultr: vc2-2c-4gb/vc2-4c-8gb."),location:S.string().optional().describe("Provider region. Hetzner: fsn1/nbg1/ash. Vultr: nrt/sgp/syd/icn/bom."),env:S.record(S.string(),S.string()).optional().describe("Environment variables to set on the server")},async({name:c,repo:l,ttl:d,provider:u,size:m,server_type:g,location:p,env:v})=>{let y=null,b=null,w=null,_=null;try{w=c??Ue(),c&&mt(c);let{getDefaultProvider:$}=await Promise.resolve().then(()=>(G(),He)),k=u??await $();_=await H.get(k);let N=g;if(!N&&m){let{resolveSize:Zt}=await Promise.resolve().then(()=>(Je(),Be));N=Zt(_,m)}let ne=await ct(w),de=await _.createSSHKey(`gibil-${w}-${he(4)}`,ne.publicKey);y=de;let Yt=ti(),me=l?await ke(l):null;v&&Object.keys(v).length>0&&(me||(me={}),me.env={...me.env,...v});let Sr=(me?.services?.length??0)>0,be=d??(Sr?120:60);if(be<1||be>J)return{content:[{type:"text",text:L("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let xr=fe({repo:l,config:me??void 0,ttlMinutes:be,githubToken:process.env.GITHUB_TOKEN,gitIdentity:Yt}),St=await _.createServer(w,de.id,xr,N,p);b=St.id;let Me=(await _.waitForReady(St.id)).ipv4,Wt=new Date,_r={name:w,serverId:St.id,ip:Me,sshKeyId:de.id,keyPath:P.privateKey(w),status:"running",createdAt:Wt.toISOString(),ttlMinutes:be,expiresAt:new Date(Wt.getTime()+be*6e4).toISOString(),repo:l,gitIdentity:Yt,provider:k};await pe(_r),await lt(w,Me);let xt="ready";if(l||me){let Ir=Date.now(),Xt=!1;for(;Date.now()-Ir<36e4;){try{if((await x({instanceName:w,ip:Me,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){Xt=!0;break}}catch{}await new Promise(_t=>setTimeout(_t,5e3))}if(!Xt){xt="timeout";try{xt=`timeout \u2014 cloud-init log:
31
- ${(await x({instanceName:w,ip:Me,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'no log'",timeoutMs:1e4})).stdout}`}catch{}}}return{content:[{type:"text",text:JSON.stringify({name:w,ip:Me,ttl_minutes:be,status:"running",provisioning:xt,working_directory:l?"/root/project":"/root",hint:l?'Server ready. Run commands with vm_bash, e.g.: vm_bash({ command: "pnpm test" })':"Server ready. Clone a repo or run commands with vm_bash."},null,2)}]}}catch($){_&&b&&await _.destroyServer(b).catch(()=>{}),_&&y&&await _.deleteSSHKey(y.id).catch(()=>{}),w&&(await ee(w).catch(()=>{}),await ge(w).catch(()=>{}));let k=$ instanceof Error?$.message:String($),N=Ze($);return{content:[{type:"text",text:L(N,`Failed to create server: ${k}`,N==="provider_error"?"Check your HETZNER_API_TOKEN and plan limits":"Verify parameters and try again")}],isError:!0}}}),o.tool("destroy_server","Burn a server. Deletes the Hetzner VM, SSH keys, and local metadata. Always destroy servers when done to avoid costs. Works on expired instances too.",{name:S.string().describe("Name of the server to destroy")},async({name:c})=>{try{let l=await ut(c),d=await H.forInstance(l);await d.destroyServer(l.serverId).catch(()=>{}),await d.deleteSSHKey(l.sshKeyId).catch(()=>{}),await ee(c).catch(()=>{});let{deleteJobsByInstance:u}=await Promise.resolve().then(()=>(ye(),On));return await u(c).catch(()=>{}),await ge(c),{content:[{type:"text",text:`Server "${c}" destroyed.`}]}}catch(l){let d=l instanceof Error?l.message:String(l),u=Ze(l);return{content:[{type:"text",text:L(u,`Failed to destroy server "${c}": ${d}`,u==="instance_not_found"?"Check server name with list_servers":"Check your HETZNER_API_TOKEN")}],isError:!0}}}),o.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let c=await Z();if(c.length===0)return{content:[{type:"text",text:"No servers running. Use create_server to forge one."}]};let l=c.map(d=>{let u=Math.max(0,Math.floor((new Date(d.expiresAt).getTime()-Date.now())/1e3));return{name:d.name,ip:d.ip,status:d.status,ttl_remaining_seconds:u,ttl_warning:u<300?"Less than 5 minutes left \u2014 extend with extend_server or finish up":void 0,repo:d.repo}});return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),o.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer. Supports long-lived durations up to 1 year.",{name:S.string().describe("Server name"),ttl:S.number().describe("New TTL in minutes from now (max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 129600 (3mo), 259200 (6mo), 525600 (1y)")},async({name:c,ttl:l})=>{try{if(l<1||l>J)return{content:[{type:"text",text:L("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let d=Math.floor(l),u=await C(c),m=await te(u,["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${d} minutes 2>/dev/null || true`,`(sleep ${d*60} && shutdown -h now) &`].join(" && "));return m.exitCode!==0?{content:[{type:"text",text:L("command_failed",`Failed to extend TTL: ${m.stderr}`,"The remote command failed \u2014 check instance status with list_servers")}],isError:!0}:(u.ttlMinutes=l,u.expiresAt=new Date(Date.now()+l*6e4).toISOString(),await pe(u),{content:[{type:"text",text:`Server "${c}" TTL extended to ${l} minutes.`}]})}catch(d){let u=d instanceof Error?d.message:String(d),m=Ze(d);return{content:[{type:"text",text:L(m,`Failed to extend server "${c}": ${u}`,m==="instance_not_found"?"Check server name with list_servers":"Instance may be unreachable \u2014 wait and retry")}],isError:!0}}}));let s=S.string().optional().describe("Server name (auto-selects if only one is running)");o.tool("vm_bash","Run a shell command on a remote server. Default working directory is /root/project. Use for: installing deps, running tests, git operations, builds. For commands over 2 minutes, set background=true to get a job_id you can poll with vm_job_status.",{command:S.string().describe("Shell command to execute"),working_dir:S.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:S.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:S.boolean().optional().describe("Run in background, return job ID for polling"),server:s},async c=>{let l=await Re(e,c.server),d=c.working_dir??"/root/project",u=`cd ${U(d)} 2>/dev/null || cd /root && ${c.command}`;if(c.background){let p=dt(),v="/root/.gibil-jobs",y=`${v}/${p}.log`,b=`${v}/${p}.exit`,w=`${v}/${p}.pid`,_=`${v}/${p}.sh`,$=["#!/bin/bash",`nohup bash -c '${u.replace(/'/g,"'\\''")}' > ${y} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${w}`,`(wait $BGPID 2>/dev/null; echo $? > ${b}) &`,"echo $BGPID"].join(`
32
- `),k=Buffer.from($).toString("base64"),N=`mkdir -p ${v} && echo '${k}' | base64 -d > ${_} && chmod +x ${_} && bash ${_}`,ne=await te(l,N,1e4),de=parseInt(ne.stdout.trim(),10);return isNaN(de)?{content:[{type:"text",text:L("command_failed","Failed to start background job \u2014 could not capture PID","Check that the server is accessible and the command is valid")}],isError:!0}:(await V({id:p,instance:l.name,command:c.command,pid:de,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:p,instance:l.name,status:"running",pid:de,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let m=await te(l,u,c.timeout_ms??12e4);return{content:[{type:"text",text:[m.stdout,m.stderr].filter(Boolean).join(`
33
- `)||"(no output)"}],isError:m.exitCode!==0}}),o.tool("vm_job_status","Check the status of a background job started with vm_bash(background=true). Returns status, exit code, and output when done.",{job_id:S.string().describe("Job ID returned by vm_bash with background=true")},async c=>{try{let l=await le(c.job_id),d=await Jt(c.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:c.job_id,instance:l.instance,command:l.command,status:d.status,exit_code:d.exitCode,started_at:l.startedAt,duration_s:d.durationS,...d.stdout!==void 0?{stdout:d.stdout}:{}},null,2)}],isError:d.status==="failed"||d.status==="orphaned"}}catch(l){let d=l instanceof Error?l.message:String(l),u=Ze(l);return{content:[{type:"text",text:L(u,d,"Check job_id is correct \u2014 use vm_job_list to see all jobs")}],isError:!0}}}),o.tool("vm_job_list","List all background jobs across all servers. Read-only \u2014 does not modify job state. Use vm_sweep_orphans to mark dead jobs.",{},async()=>{let l=(await Ce()).map(d=>({job_id:d.id,instance:d.instance,command:d.command,status:d.status,started_at:d.startedAt,exit_code:d.exitCode}));return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),o.tool("vm_sweep_orphans","Mark running jobs as orphaned if their server no longer exists. Use after destroy_server to clean up lingering job records.",{},async()=>{let c=await Ce(),l=await Z(),d=new Set(l.map(m=>m.name)),u=[];for(let m of c)m.status==="running"&&!d.has(m.instance)&&(m.status="orphaned",m.completedAt=new Date().toISOString(),await V(m),u.push(m.id));return{content:[{type:"text",text:JSON.stringify({swept_count:u.length,swept_job_ids:u},null,2)}]}}),o.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:S.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:S.number().int().min(1).max(999999).optional().describe("Start at line N (1-based)"),limit:S.number().int().min(1).max(999999).optional().describe("Max lines to return"),server:s},async c=>{let l=await Re(e,c.server),d=U(c.path),u=`cat -n ${d}`;c.offset&&c.limit?u=`awk 'NR>=${c.offset} && NR<=${c.offset+c.limit-1} {printf "%6d\\t%s\\n", NR, $0}' ${d}`:c.offset?u=`awk 'NR>=${c.offset} {printf "%6d\\t%s\\n", NR, $0}' ${d}`:c.limit&&(u=`head -n ${c.limit} ${d} | cat -n`);let m=await te(l,u);return m.exitCode!==0?{content:[{type:"text",text:L("command_failed",`Failed to read ${c.path}: ${m.stderr}`,"Check the file path exists on the server \u2014 use vm_ls to browse")}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),o.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:S.string().describe("Absolute path on the server"),content:S.string().describe("File content to write"),server:s},async c=>{let l=await Re(e,c.server),d=Buffer.from(c.content).toString("base64"),u=U(c.path),m=`mkdir -p "$(dirname ${u})" && echo '${d}' | base64 -d > ${u}`,g=await te(l,m);return g.exitCode!==0?{content:[{type:"text",text:L("command_failed",`Failed to write ${c.path}: ${g.stderr}`,"Check the path is valid and the disk is not full")}],isError:!0}:{content:[{type:"text",text:`Wrote ${c.path}`}]}}),o.tool("vm_ls","List files and directories on a remote server.",{path:S.string().optional().describe("Directory path (default: /root/project)"),glob:S.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:s},async c=>{let l=await Re(e,c.server),d=c.path??"/root/project",u;c.glob?u=`cd ${U(d)} && find . -path ${U("./"+c.glob)} -type f 2>/dev/null | sort | head -200`:u=`ls -la ${U(d)}`;let m=await te(l,u);return m.exitCode!==0?{content:[{type:"text",text:L("command_failed",`Failed to list ${d}: ${m.stderr}`,"Check the directory path exists on the server")}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),o.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:S.string().describe("Regex pattern to search for"),path:S.string().optional().describe("Directory or file to search (default: /root/project)"),include:S.string().optional().describe("File glob to include (e.g. '*.ts')"),server:s},async c=>{let l=await Re(e,c.server),d=c.path??"/root/project",u=U(c.pattern),m=U(d),g;if(c.include){let y=U(c.include);g=`cd ${m} && (rg -n --glob ${y} ${u} 2>/dev/null || grep -rn --include=${y} ${u} .) | head -100`}else g=`cd ${m} && (rg -n ${u} 2>/dev/null || grep -rn ${u} .) | head -100`;return{content:[{type:"text",text:(await te(l,g)).stdout||"(no matches)"}]}}),o.tool("vm_stats","Get server resource usage \u2014 CPU cores, load average, memory, disk, and uptime. Returns structured data for monitoring.",{server:s},async c=>{try{let l=await Re(e,c.server),u=await te(l,`echo "$(nproc),$(cat /proc/loadavg),$(free -m | awk '/Mem:/{print $2,$3,$7}'),$(df -BG / | awk 'NR==2{print $2,$3,$4}'),$(awk '{print int($1)}' /proc/uptime)"`);if(u.exitCode!==0)return{content:[{type:"text",text:L("command_failed",`Failed to collect stats: ${u.stderr}`,"Check the server is accessible with vm_bash")}],isError:!0};let m=ni(u.stdout);return{content:[{type:"text",text:JSON.stringify(m,null,2)}]}}catch(l){let d=l instanceof Error?l.message:String(l),u=Ze(l);return{content:[{type:"text",text:L(u,`Failed to get stats: ${d}`,"Check the server is accessible with vm_bash")}],isError:!0}}});let a=new Qo;await o.connect(a)}E();B();function sr(t){t.command("mcp [name]").description("Start an MCP server (used by Claude Code, Cursor, and other agents)").option("--print-config","Print the MCP JSON config (with resolved binary path) and exit").action(async(e,n)=>{if(n.printConfig){let o={mcpServers:{gibil:nt()}};console.log(JSON.stringify(o,null,2)),console.error(""),console.error("Add or merge the mcpServers entry into one of:"),console.error(" ~/.claude.json (Claude Code, user-level \u2014 usually already has other mcpServers; merge in)"),console.error(" .mcp.json (repo root, shared with your team \u2014 create if it doesn't exist)");return}try{await ir(e)}catch(o){r.error(o instanceof Error?o.message:String(o)),process.exit(1)}})}G();E();B();O();import{createInterface as ci}from"readline";import{existsSync as li,readFileSync as ui,writeFileSync as di}from"fs";import{join as dr}from"path";import{homedir as mi}from"os";st();E();var ar="https://1.1.1.1/cdn-cgi/trace";function ri(){let t=process.env.GIBIL_SKIP_IP_DETECTION?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}async function cr(t){if(ri())return r.debug("Public IP detection skipped (GIBIL_SKIP_IP_DETECTION set)"),null;r.debug(`Detecting public IP via ${ar}`);let e=new AbortController,n=setTimeout(()=>e.abort(),t.timeoutMs);try{let o=await fetch(ar,{signal:e.signal});if(!o.ok)return null;let i=await o.text();return oi(i)}catch{return null}finally{clearTimeout(n)}}function oi(t){for(let e of t.split(`
34
- `))if(e.startsWith("ip=")){let n=e.slice(3).trim();return n.length>0?n:null}return null}var ii="https://console.vultr.com/user/apiaccess/",si="https://whatismyip.com";function lr(t){let e=t.program?.console_url??ii;if(t.hasAccount)return[{heading:"Vultr API Key",body:["Get your personal access token from the API access page."],link:e,gate:!1}];let n=[];return t.program&&n.push({heading:"New to Vultr?",body:[`Sign up with this link to get ${t.program.credit} in free credits \u2014 enough for weeks of Gibil usage on small VMs.`],link:t.program.ref_url,disclosure:t.program.disclosure,gate:!0}),n.push({heading:"Step 1 of 3 \u2014 Open the API page",body:["Sign in to Vultr, then open the API access page below.","You can also reach it via Account \u2192 API in the top-right."],link:e,gate:!0}),n.push({heading:"Step 2 of 3 \u2014 Whitelist your IP",body:ai(t.detectedIp),gate:!0}),n.push({heading:"Step 3 of 3 \u2014 Copy your API key",body:['Scroll to the top of the page. Your "Personal Access Token" is the long string at the top.',"Click the eye icon to reveal it, then copy."],gate:!0}),n}function ai(t){let e=['Vultr blocks API requests until you whitelist an IP. Scroll to "Access Control" and either:'];return t?e.push(` \u2022 Add your current IP \u2014 we detected: ${t}`):e.push(` \u2022 Add your current IP \u2014 find it at ${si}`),e.push(" \u2022 Or add 0.0.0.0/0 (any IP \u2014 fine for personal use, less secure)"),e.push('Click "Update Access Control" to save.'),e}function Oe(t){let e=ci({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,o=>{e.close(),n(o.trim())})})}var ue=["hetzner","vultr"];async function fi(){let t=[];for(let n of ue)await xe(n)&&t.push(n);let e=!!await M();return{providers:t,apiKey:e}}function ur(t){return t==="hetzner"?"Hetzner":"Vultr"}async function pi(){r.info(""),r.info(h("Which provider would you like to set up?")),r.info(f(" hetzner Cheapest EU/US baseline (default)")),r.info(f(" vultr Strongest APAC coverage (Tokyo, Seoul, Singapore, Sydney, Mumbai)")),r.info("");let t=(await Oe(" Provider [hetzner]: ")).toLowerCase();return t==="vultr"?"vultr":(t===""||t==="hetzner"||r.warn(`Unknown provider "${t}". Defaulting to hetzner.`),"hetzner")}async function Vt(t,e){let n=e;if(!n&&(t==="vultr"?n=await gi():(r.info(""),r.info(h("Hetzner API Token")),r.info(f(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),r.info(""),n=await Oe(" Hetzner API token: ")),!n)){let i=t==="hetzner"?"Hetzner API token":"Vultr API key";r.error(`No ${i.toLowerCase()} provided.`),process.exit(1)}let o=r.spin(`Verifying ${t} token...`);try{await mr(t,n),o.succeed(`${t} token verified`)}catch(i){o.fail(i instanceof Error?i.message:String(i)),process.exit(1)}return await Ke(t,n),n}async function gi(){r.info(""),r.info(h("Vultr API Key")),r.info("");let t=(await Oe(" Do you have a Vultr account? [Y/n]: ")).toLowerCase().trim(),e=t!=="n"&&t!=="no",n=null;e||(n=await cr({timeoutMs:2e3}));let o=it("vultr"),i=lr({hasAccount:e,detectedIp:n,program:o});for(let s of i)await hi(s);return r.info(""),Oe(" Vultr API key: ")}async function hi(t){r.info(""),t.heading&&r.info(h(t.heading));for(let e of t.body)r.info(` ${e}`);t.link&&r.info(f(` \u2192 ${t.link}`)),t.disclosure&&r.info(f(` ${t.disclosure}`)),t.gate&&await Oe(f(" (press Enter when done) "))}async function mr(t,e){if(t==="hetzner"){let o=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${e}`}})).json();if(o.error)throw new Error(`Invalid Hetzner token: ${o.error.message}`)}else if(t==="vultr"){let n=await fetch("https://api.vultr.com/v2/account",{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let o=await n.text(),i=`Invalid Vultr API key (${n.status}): ${o}`;throw n.status===401||n.status===403?new Error(`${i}
34
+ Suggestion: ${n}`),r}function Xe(t){let e=t instanceof Error?t.message:String(t);return/no active server|not found|no servers|does not exist/i.test(e)?"instance_not_found":/timeout|timed out|ETIMEDOUT/i.test(e)?"timeout":/ECONNREFUSED|EHOSTUNREACH|SSH connection|ssh2|handshake/i.test(e)?"ssh_connection":/hetzner|api|token|401|403|409|422|429|quota/i.test(e)?"provider_error":/invalid|must be|validation/i.test(e)?"validation":"provider_error"}var $t="key::";function bi(){try{let t=bt("git config user.name",{encoding:"utf-8"}).trim(),e=bt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(bt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=bt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=wi(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith($t))&&(n=i.startsWith($t)?i.slice($t.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function He(t,e){if(t)return t;if(e)return A(e);let r=(await X()).filter(i=>new Date<new Date(i.expiresAt));if(r.length===0)throw new Error("No active servers. Use create_server first.");if(r.length===1)return r[0];throw new Error(`Multiple servers running: ${r.map(i=>i.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function re(t,e,n=3e4){return k({instanceName:t.name,ip:t.ip,command:e,stream:!1,timeoutMs:n})}function B(t){return`'${t.replace(/'/g,"'\\''")}'`}function $i(t){let e=t.trim(),n=e.split(",");if(n.length<5)throw new Error(`Unexpected vm_stats output (expected 5 comma-separated sections, got ${n.length}): ${e}`);let r=parseInt(n[0],10);if(isNaN(r))throw new Error(`Failed to parse CPU cores: ${n[0]}`);let i=n[1].trim().split(/\s+/),s=parseFloat(i[0]),a=parseFloat(i[1]),l=parseFloat(i[2]);if(isNaN(s)||isNaN(a)||isNaN(l))throw new Error(`Failed to parse load averages: ${n[1]}`);let c=n[2].trim().split(/\s+/),u=parseInt(c[0],10),d=parseInt(c[1],10),f=parseInt(c[2],10);if(isNaN(u)||isNaN(d)||isNaN(f))throw new Error(`Failed to parse memory: ${n[2]}`);let p=n[3].trim().split(/\s+/),g=$=>Math.round(parseFloat($.replace(/G$/i,""))),y=g(p[0]),v=g(p[1]),S=g(p[2]);if(isNaN(y)||isNaN(v)||isNaN(S))throw new Error(`Failed to parse disk: ${n[3]}`);let w=parseInt(n[4].trim(),10);if(isNaN(w))throw new Error(`Failed to parse uptime: ${n[4]}`);return{cpu:{cores:r,load_1m:s,load_5m:a,load_15m:l},memory:{total_mb:u,used_mb:d,available_mb:f},disk:{total_gb:y,used_gb:v,available_gb:S},uptime_seconds:w}}async function pr(t){let e=null;if(t&&(e=await A(t),e.gitIdentity)){let{name:l,email:c,signingKey:u}=e.gitIdentity,d=[`git config --global user.name ${B(l)}`,`git config --global user.email ${B(c)}`];u&&d.push("git config --global gpg.format ssh",`git config --global user.signingkey ${B($t+u)}`,"git config --global commit.gpgsign true"),re(e,d.join(" && ")).catch(()=>{})}let n=t?`gibil-${t}`:"gibil",r=new yi({name:n,version:"0.4.0"}),i=r.tool.bind(r);r.tool=((...l)=>{let c=l[0],u=l.length-1;if(typeof l[u]=="function"){let d=l[u];l[u]=async(...f)=>(nr(c).catch(()=>{}),d(...f))}return i(...l)}),e||(r.tool("create_server","Forge a new ephemeral server with a full Linux environment (Ubuntu 24.04, Node.js 20, pnpm). Clones the repo to /root/project and waits until fully provisioned. Picks the configured default provider unless 'provider' is set (hetzner, vultr). After creation, use vm_bash to run commands, vm_read/vm_write for files, vm_grep to search code. Destroy with destroy_server when done.",{name:x.string().optional().describe("Server name (auto-generated if omitted)"),repo:x.string().optional().describe("Git repo URL to clone on boot"),ttl:x.number().optional().describe("Auto-destroy after N minutes (default: 60, max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)"),provider:x.enum(["hetzner","vultr"]).optional().describe("Cloud provider (default: configured default \u2014 hetzner if unset). vultr gives APAC regions."),size:x.enum(["small","medium","large"]).optional().describe("Gibil size: small (2 vCPU/4 GB), medium (4 vCPU/8 GB), large (8 vCPU/16 GB). Resolved per provider \u2014 see gibil providers."),server_type:x.string().optional().describe("Provider-native SKU (overrides size). Hetzner: cax11/cax21/cax31 (ARM, fsn1/nbg1) or cpx21/cpx31 (x86). Vultr: vc2-2c-4gb/vc2-4c-8gb."),location:x.string().optional().describe("Provider region. Hetzner: fsn1/nbg1/ash. Vultr: nrt/sgp/syd/icn/bom."),env:x.record(x.string(),x.string()).optional().describe("Environment variables to set on the server")},async({name:l,repo:c,ttl:u,provider:d,size:f,server_type:p,location:g,env:y})=>{let v=null,S=null,w=null,$=null;try{w=l??Be(),l&&pt(l);let{getDefaultProvider:b}=await Promise.resolve().then(()=>(G(),Ue)),_=d??await b();$=await F.get(_);let T=p;if(!T&&f){let{resolveSize:en}=await Promise.resolve().then(()=>(Ve(),Je));T=en($,f)}let oe=await lt(w),fe=await $.createSSHKey(`gibil-${w}-${ye(4)}`,oe.publicKey);v=fe;let Xt=bi(),me=c?await Ee(c):null;y&&Object.keys(y).length>0&&(me||(me={}),me.env={...me.env,...y});let Ar=(me?.services?.length??0)>0,$e=u??(Ar?120:60);if($e<1||$e>J)return{content:[{type:"text",text:K("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let jr=he({repo:c,config:me??void 0,ttlMinutes:$e,githubToken:process.env.GITHUB_TOKEN,gitIdentity:Xt}),xt=await $.createServer(w,fe.id,jr,T,g);S=xt.id;let Ke=(await $.waitForReady(xt.id)).ipv4,Qt=new Date,Nr={name:w,serverId:xt.id,ip:Ke,sshKeyId:fe.id,keyPath:P.privateKey(w),status:"running",createdAt:Qt.toISOString(),ttlMinutes:$e,expiresAt:new Date(Qt.getTime()+$e*6e4).toISOString(),repo:c,gitIdentity:Xt,provider:_};await pe(Nr),await ft(w,Ke);let _t="ready";if(c||me){let Rr=Date.now(),tn=!1;for(;Date.now()-Rr<36e4;){try{if((await k({instanceName:w,ip:Ke,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){tn=!0;break}}catch{}await new Promise(Pt=>setTimeout(Pt,5e3))}if(!tn){_t="timeout";try{_t=`timeout \u2014 cloud-init log:
35
+ ${(await k({instanceName:w,ip:Ke,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'no log'",timeoutMs:1e4})).stdout}`}catch{}}}return{content:[{type:"text",text:JSON.stringify({name:w,ip:Ke,ttl_minutes:$e,status:"running",provisioning:_t,working_directory:c?"/root/project":"/root",hint:c?'Server ready. Run commands with vm_bash, e.g.: vm_bash({ command: "pnpm test" })':"Server ready. Clone a repo or run commands with vm_bash."},null,2)}]}}catch(b){$&&S&&await $.destroyServer(S).catch(()=>{}),$&&v&&await $.deleteSSHKey(v.id).catch(()=>{}),w&&(await ne(w).catch(()=>{}),await ge(w).catch(()=>{}));let _=b instanceof Error?b.message:String(b),T=Xe(b);return{content:[{type:"text",text:K(T,`Failed to create server: ${_}`,T==="provider_error"?"Check your HETZNER_API_TOKEN and plan limits":"Verify parameters and try again")}],isError:!0}}}),r.tool("destroy_server","Burn a server. Deletes the Hetzner VM, SSH keys, and local metadata. Always destroy servers when done to avoid costs. Works on expired instances too.",{name:x.string().describe("Name of the server to destroy")},async({name:l})=>{try{let c=await ut(l),u=await F.forInstance(c);await u.destroyServer(c.serverId).catch(()=>{}),await u.deleteSSHKey(c.sshKeyId).catch(()=>{}),await ne(l).catch(()=>{});let{deleteJobsByInstance:d}=await Promise.resolve().then(()=>(we(),Un));return await d(l).catch(()=>{}),await ge(l),{content:[{type:"text",text:`Server "${l}" destroyed.`}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,`Failed to destroy server "${l}": ${u}`,d==="instance_not_found"?"Check server name with list_servers":"Check your HETZNER_API_TOKEN")}],isError:!0}}}),r.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let l=await X();if(l.length===0)return{content:[{type:"text",text:"No servers running. Use create_server to forge one."}]};let c=l.map(u=>{let d=Math.max(0,Math.floor((new Date(u.expiresAt).getTime()-Date.now())/1e3));return{name:u.name,ip:u.ip,status:u.status,ttl_remaining_seconds:d,ttl_warning:d<300?"Less than 5 minutes left \u2014 extend with extend_server or finish up":void 0,repo:u.repo}});return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),r.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer. Supports long-lived durations up to 1 year.",{name:x.string().describe("Server name"),ttl:x.number().describe("New TTL in minutes from now (max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 129600 (3mo), 259200 (6mo), 525600 (1y)")},async({name:l,ttl:c})=>{try{if(c<1||c>J)return{content:[{type:"text",text:K("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let u=Math.floor(c),d=await A(l),f=await re(d,["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${u} minutes 2>/dev/null || true`,`(sleep ${u*60} && shutdown -h now) &`].join(" && "));return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to extend TTL: ${f.stderr}`,"The remote command failed \u2014 check instance status with list_servers")}],isError:!0}:(d.ttlMinutes=c,d.expiresAt=new Date(Date.now()+c*6e4).toISOString(),await pe(d),{content:[{type:"text",text:`Server "${l}" TTL extended to ${c} minutes.`}]})}catch(u){let d=u instanceof Error?u.message:String(u),f=Xe(u);return{content:[{type:"text",text:K(f,`Failed to extend server "${l}": ${d}`,f==="instance_not_found"?"Check server name with list_servers":"Instance may be unreachable \u2014 wait and retry")}],isError:!0}}}));let s=x.string().optional().describe("Server name (auto-selects if only one is running)");r.tool("vm_bash","Run a shell command on a remote server. Default working directory is /root/project. Use for: installing deps, running tests, git operations, builds. For commands over 2 minutes, set background=true to get a job_id you can poll with vm_job_status.",{command:x.string().describe("Shell command to execute"),working_dir:x.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:x.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:x.boolean().optional().describe("Run in background, return job ID for polling"),server:s},async l=>{let c=await He(e,l.server),u=l.working_dir??"/root/project",d=`cd ${B(u)} 2>/dev/null || cd /root && ${l.command}`;if(l.background){let g=mt(),y="/root/.gibil-jobs",v=`${y}/${g}.log`,S=`${y}/${g}.exit`,w=`${y}/${g}.pid`,$=`${y}/${g}.sh`,b=["#!/bin/bash",`nohup bash -c '${d.replace(/'/g,"'\\''")}' > ${v} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${w}`,`(wait $BGPID 2>/dev/null; echo $? > ${S}) &`,"echo $BGPID"].join(`
36
+ `),_=Buffer.from(b).toString("base64"),T=`mkdir -p ${y} && echo '${_}' | base64 -d > ${$} && chmod +x ${$} && bash ${$}`,oe=await re(c,T,1e4),fe=parseInt(oe.stdout.trim(),10);return isNaN(fe)?{content:[{type:"text",text:K("command_failed","Failed to start background job \u2014 could not capture PID","Check that the server is accessible and the command is valid")}],isError:!0}:(await V({id:g,instance:c.name,command:l.command,pid:fe,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:g,instance:c.name,status:"running",pid:fe,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let f=await re(c,d,l.timeout_ms??12e4);return{content:[{type:"text",text:[f.stdout,f.stderr].filter(Boolean).join(`
37
+ `)||"(no output)"}],isError:f.exitCode!==0}}),r.tool("vm_job_status","Check the status of a background job started with vm_bash(background=true). Returns status, exit code, and output when done.",{job_id:x.string().describe("Job ID returned by vm_bash with background=true")},async l=>{try{let c=await ue(l.job_id),u=await Yt(l.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:l.job_id,instance:c.instance,command:c.command,status:u.status,exit_code:u.exitCode,started_at:c.startedAt,duration_s:u.durationS,...u.stdout!==void 0?{stdout:u.stdout}:{}},null,2)}],isError:u.status==="failed"||u.status==="orphaned"}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,u,"Check job_id is correct \u2014 use vm_job_list to see all jobs")}],isError:!0}}}),r.tool("vm_job_list","List all background jobs across all servers. Read-only \u2014 does not modify job state. Use vm_sweep_orphans to mark dead jobs.",{},async()=>{let c=(await je()).map(u=>({job_id:u.id,instance:u.instance,command:u.command,status:u.status,started_at:u.startedAt,exit_code:u.exitCode}));return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),r.tool("vm_sweep_orphans","Mark running jobs as orphaned if their server no longer exists. Use after destroy_server to clean up lingering job records.",{},async()=>{let l=await je(),c=await X(),u=new Set(c.map(f=>f.name)),d=[];for(let f of l)f.status==="running"&&!u.has(f.instance)&&(f.status="orphaned",f.completedAt=new Date().toISOString(),await V(f),d.push(f.id));return{content:[{type:"text",text:JSON.stringify({swept_count:d.length,swept_job_ids:d},null,2)}]}}),r.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:x.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:x.number().int().min(1).max(999999).optional().describe("Start at line N (1-based)"),limit:x.number().int().min(1).max(999999).optional().describe("Max lines to return"),server:s},async l=>{let c=await He(e,l.server),u=B(l.path),d=`cat -n ${u}`;l.offset&&l.limit?d=`awk 'NR>=${l.offset} && NR<=${l.offset+l.limit-1} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.offset?d=`awk 'NR>=${l.offset} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.limit&&(d=`head -n ${l.limit} ${u} | cat -n`);let f=await re(c,d);return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to read ${l.path}: ${f.stderr}`,"Check the file path exists on the server \u2014 use vm_ls to browse")}],isError:!0}:{content:[{type:"text",text:f.stdout}]}}),r.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:x.string().describe("Absolute path on the server"),content:x.string().describe("File content to write"),server:s},async l=>{let c=await He(e,l.server),u=Buffer.from(l.content).toString("base64"),d=B(l.path),f=`mkdir -p "$(dirname ${d})" && echo '${u}' | base64 -d > ${d}`,p=await re(c,f);return p.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to write ${l.path}: ${p.stderr}`,"Check the path is valid and the disk is not full")}],isError:!0}:{content:[{type:"text",text:`Wrote ${l.path}`}]}}),r.tool("vm_ls","List files and directories on a remote server.",{path:x.string().optional().describe("Directory path (default: /root/project)"),glob:x.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:s},async l=>{let c=await He(e,l.server),u=l.path??"/root/project",d;l.glob?d=`cd ${B(u)} && find . -path ${B("./"+l.glob)} -type f 2>/dev/null | sort | head -200`:d=`ls -la ${B(u)}`;let f=await re(c,d);return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to list ${u}: ${f.stderr}`,"Check the directory path exists on the server")}],isError:!0}:{content:[{type:"text",text:f.stdout}]}}),r.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:x.string().describe("Regex pattern to search for"),path:x.string().optional().describe("Directory or file to search (default: /root/project)"),include:x.string().optional().describe("File glob to include (e.g. '*.ts')"),server:s},async l=>{let c=await He(e,l.server),u=l.path??"/root/project",d=B(l.pattern),f=B(u),p;if(l.include){let v=B(l.include);p=`cd ${f} && (rg -n --glob ${v} ${d} 2>/dev/null || grep -rn --include=${v} ${d} .) | head -100`}else p=`cd ${f} && (rg -n ${d} 2>/dev/null || grep -rn ${d} .) | head -100`;return{content:[{type:"text",text:(await re(c,p)).stdout||"(no matches)"}]}}),r.tool("vm_stats","Get server resource usage \u2014 CPU cores, load average, memory, disk, and uptime. Returns structured data for monitoring.",{server:s},async l=>{try{let c=await He(e,l.server),d=await re(c,`echo "$(nproc),$(cat /proc/loadavg),$(free -m | awk '/Mem:/{print $2,$3,$7}'),$(df -BG / | awk 'NR==2{print $2,$3,$4}'),$(awk '{print int($1)}' /proc/uptime)"`);if(d.exitCode!==0)return{content:[{type:"text",text:K("command_failed",`Failed to collect stats: ${d.stderr}`,"Check the server is accessible with vm_bash")}],isError:!0};let f=$i(d.stdout);return{content:[{type:"text",text:JSON.stringify(f,null,2)}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,`Failed to get stats: ${u}`,"Check the server is accessible with vm_bash")}],isError:!0}}});let a=new vi;await r.connect(a)}E();L();function gr(t){t.command("mcp [name]").description("Start an MCP server (used by Claude Code, Cursor, and other agents)").option("--print-config","Print the MCP JSON config (with resolved binary path) and exit").action(async(e,n)=>{if(n.printConfig){let r={mcpServers:{gibil:rt()}};console.log(JSON.stringify(r,null,2)),console.error(""),console.error("Add or merge the mcpServers entry into one of:"),console.error(" ~/.claude.json (Claude Code, user-level \u2014 usually already has other mcpServers; merge in)"),console.error(" .mcp.json (repo root, shared with your team \u2014 create if it doesn't exist)");return}try{await pr(e)}catch(r){o.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}G();E();L();R();import{createInterface as Ii}from"readline";import{existsSync as Ei,readFileSync as Ti,writeFileSync as Ci}from"fs";import{join as br}from"path";import{homedir as Ai}from"os";at();E();var hr="https://1.1.1.1/cdn-cgi/trace";function Si(){let t=process.env.GIBIL_SKIP_IP_DETECTION?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}async function yr(t){if(Si())return o.debug("Public IP detection skipped (GIBIL_SKIP_IP_DETECTION set)"),null;o.debug(`Detecting public IP via ${hr}`);let e=new AbortController,n=setTimeout(()=>e.abort(),t.timeoutMs);try{let r=await fetch(hr,{signal:e.signal});if(!r.ok)return null;let i=await r.text();return xi(i)}catch{return null}finally{clearTimeout(n)}}function xi(t){for(let e of t.split(`
38
+ `))if(e.startsWith("ip=")){let n=e.slice(3).trim();return n.length>0?n:null}return null}var _i="https://console.vultr.com/user/apiaccess/",Pi="https://whatismyip.com";function vr(t){let e=t.program?.console_url??_i;if(t.hasAccount)return[{heading:"Vultr API Key",body:["Get your personal access token from the API access page."],link:e,gate:!1}];let n=[];return t.program&&n.push({heading:"New to Vultr?",body:[`Sign up with this link to get ${t.program.credit} in free credits \u2014 enough for weeks of Gibil usage on small VMs.`],link:t.program.ref_url,disclosure:t.program.disclosure,gate:!0}),n.push({heading:"Step 1 of 3 \u2014 Open the API page",body:["Sign in to Vultr, then open the API access page below.","You can also reach it via Account \u2192 API in the top-right."],link:e,gate:!0}),n.push({heading:"Step 2 of 3 \u2014 Whitelist your IP",body:ki(t.detectedIp),gate:!0}),n.push({heading:"Step 3 of 3 \u2014 Copy your API key",body:['Scroll to the top of the page. Your "Personal Access Token" is the long string at the top.',"Click the eye icon to reveal it, then copy."],gate:!0}),n}function ki(t){let e=['Vultr blocks API requests until you whitelist an IP. Scroll to "Access Control" and either:'];return t?e.push(` \u2022 Add your current IP \u2014 we detected: ${t}`):e.push(` \u2022 Add your current IP \u2014 find it at ${Pi}`),e.push(" \u2022 Or add 0.0.0.0/0 (any IP \u2014 fine for personal use, less secure)"),e.push('Click "Update Access Control" to save.'),e}function Me(t){let e=Ii({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,r=>{e.close(),n(r.trim())})})}var de=["hetzner","vultr"];async function ji(){let t=[];for(let n of de)await _e(n)&&t.push(n);let e=!!await M();return{providers:t,apiKey:e}}function wr(t){return t==="hetzner"?"Hetzner":"Vultr"}async function Ni(){o.info(""),o.info(h("Which provider would you like to set up?")),o.info(m(" hetzner Cheapest EU/US baseline (default)")),o.info(m(" vultr Strongest APAC coverage (Tokyo, Seoul, Singapore, Sydney, Mumbai)")),o.info("");let t=(await Me(" Provider [hetzner]: ")).toLowerCase();return t==="vultr"?"vultr":(t===""||t==="hetzner"||o.warn(`Unknown provider "${t}". Defaulting to hetzner.`),"hetzner")}async function Wt(t,e){let n=e;if(!n&&(t==="vultr"?n=await Ri():(o.info(""),o.info(h("Hetzner API Token")),o.info(m(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),o.info(""),n=await Me(" Hetzner API token: ")),!n)){let i=t==="hetzner"?"Hetzner API token":"Vultr API key";o.error(`No ${i.toLowerCase()} provided.`),process.exit(1)}let r=o.spin(`Verifying ${t} token...`);try{await $r(t,n),r.succeed(`${t} token verified`)}catch(i){r.fail(i instanceof Error?i.message:String(i)),process.exit(1)}return await Ge(t,n),n}async function Ri(){o.info(""),o.info(h("Vultr API Key")),o.info("");let t=(await Me(" Do you have a Vultr account? [Y/n]: ")).toLowerCase().trim(),e=t!=="n"&&t!=="no",n=null;e||(n=await yr({timeoutMs:2e3}));let r=st("vultr"),i=vr({hasAccount:e,detectedIp:n,program:r});for(let s of i)await Oi(s);return o.info(""),Me(" Vultr API key: ")}async function Oi(t){o.info(""),t.heading&&o.info(h(t.heading));for(let e of t.body)o.info(` ${e}`);t.link&&o.info(m(` \u2192 ${t.link}`)),t.disclosure&&o.info(m(` ${t.disclosure}`)),t.gate&&await Me(m(" (press Enter when done) "))}async function $r(t,e){if(t==="hetzner"){let r=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${e}`}})).json();if(r.error)throw new Error(`Invalid Hetzner token: ${r.error.message}`)}else if(t==="vultr"){let n=await fetch("https://api.vultr.com/v2/account",{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let r=await n.text(),i=`Invalid Vultr API key (${n.status}): ${r}`;throw n.status===401||n.status===403?new Error(`${i}
35
39
 
36
40
  Most likely cause: your IP isn't whitelisted under "Access Control".
37
- Open https://console.vultr.com/user/apiaccess/ \u2192 Access Control \u2192 add your IP.`):new Error(i)}}}function fr(t){t.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").option("--provider <name>",`Configure a specific provider non-interactively (${ue.join(", ")})`).option("--token <value>","API token for --provider; required when --provider is set").option("--set-default","Mark the configured provider as the default after setup").option("--add <name>",`Add a second provider interactively (${ue.join(", ")})`).action(async e=>{if(e.provider){ue.includes(e.provider)||(r.error(`Unknown provider "${e.provider}". Supported: ${ue.join(", ")}.`),process.exit(1));let l=e.provider;e.token||(r.error("--token is required when --provider is set."),process.exit(1));let d=r.spin(`Verifying ${l} token...`);try{await mr(l,e.token)}catch(u){d.fail(u instanceof Error?u.message:String(u)),process.exit(1)}d.succeed(`${l} token verified`),await Ke(l,e.token),e.setDefault&&(await _e(l),r.info(`${A} ${l} is now the default provider.`)),r.info(`${A} ${l} configured. Try: gibil create --provider ${l}`);return}if(e.add){ue.includes(e.add)||(r.error(`Unknown provider "${e.add}". Supported: ${ue.join(", ")}.`),process.exit(1));let l=e.add;await Vt(l,e.token),e.setDefault&&(await _e(l),r.info(`${A} ${l} is now the default provider.`)),r.info(`${A} ${l} added. Try: gibil create --provider ${l}`);return}console.error(tn);let n=await fi();if(n.providers.length>0&&!e.force){r.info(`${A} Already configured.`);for(let d of n.providers)r.detail(ur(d),X("connected"));r.detail("Gibil API",n.apiKey?X("connected"):f("not configured (optional)")),r.info(""),r.info(` Run ${h("gibil init --force")} to reconfigure.`);let l=ue.filter(d=>!n.providers.includes(d));l.length>0&&r.info(` Run ${h(`gibil init --add ${l[0]}`)} to add ${ur(l[0])}.`),r.info(` Run ${h("gibil create")} to forge a server.`);return}let o=await pi(),i;if(o==="vultr"?(await Vt("vultr"),await _e("vultr")):(i=await Vt("hetzner"),await _e("hetzner")),o==="hetzner"&&i){let l=r.spin("Detecting available server types..."),d="cax11",u="fsn1",m=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let g of m)try{let v=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:g.type,image:"ubuntu-24.04",location:g.location,start_after_create:!1})})).json();if(v.server){await fetch(`https://api.hetzner.cloud/v1/servers/${v.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${i}`}}),d=g.type,u=g.location;break}}catch{}await Nt(d,u),l.succeed(`Default server type: ${d} (${u})`)}let s=r.spin("Configuring MCP for Claude Code...");try{let l=dr(mi(),".claude.json"),d={};try{d=JSON.parse(ui(l,"utf-8"))}catch{}d.mcpServers||(d.mcpServers={}),d.mcpServers.gibil=nt(),di(l,JSON.stringify(d,null,2)+`
38
- `),s.succeed("MCP configured for Claude Code (~/.claude.json)")}catch{s.fail("Could not auto-configure MCP"),r.info(f(" Run gibil mcp --print-config for manual setup"))}r.info(""),r.info(h("Default coding agent (optional)")),r.info(f(` Install a coding agent on every server. Options: ${W.join(", ")}`)),r.info(f(" Press Enter to skip \u2014 you can always use --agent later.")),r.info("");let c=(await Oe(" Default agent [none]: ")).toLowerCase().trim();c&&W.includes(c)?(await ot(c),r.info(` ${A} Default agent: ${X(c)}`)):c?r.info(f(` Unknown agent "${c}", skipping. Use --agent with: ${W.join(", ")}`)):(await ot(null),r.info(f(" No default agent. Use --agent claude (or aider, codex) when creating servers."))),r.info(""),r.info(T.initComplete),r.info(""),r.info(f(" Try it now:")),r.info(` ${h('gibil branch feat/my-feature --run "pnpm test"')}`),r.info(` ${h("gibil ssh feat-my-feature")}`),r.info(` ${h("gibil destroy feat-my-feature")}`),r.info(""),r.info(f(" Or with full control:")),r.info(` ${h("gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10")}`),r.info(` ${h('gibil run demo "npm test"')}`),r.info(` ${h("gibil destroy demo")}`),r.info(""),r.info(f(" Later:")),r.info(` ${h("gibil auth login")} ${f("Add a Gibil API key (optional)")}`),r.info(` ${h("gibil mcp --print-config")} ${f("MCP setup for other editors")}`),r.info("")})}async function pr(){if(process.env.HETZNER_API_TOKEN||process.env.VULTR_API_KEY)return!1;let t=dr(P.root,"config.json");return!li(t)}Ie();import{execSync as vi}from"child_process";import{existsSync as Y}from"fs";E();O();G();function yi(){try{let t=vi("git remote get-url origin",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();if(!t)throw new Error("empty");return t}catch{throw new Error("Not in a git repo, or no remote configured. Use --repo to specify.")}}function wi(t){if(!t||!t.trim())throw new Error("Branch name cannot be empty.");if(/[;&|$`(){}<>!;"'\s\\]/.test(t))throw new Error(`Invalid branch name "${t}". Contains shell-unsafe characters.`)}function qt(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function bi(){return Y("pnpm-lock.yaml")?"pnpm install":Y("bun.lockb")||Y("bun.lock")?"bun install":Y("yarn.lock")?"yarn install":Y("package-lock.json")?"npm install":Y("Cargo.lock")?"cargo build":Y("go.sum")?"go mod download":Y("uv.lock")?"uv sync":Y("poetry.lock")?"poetry install":Y("requirements.txt")?"pip install -r requirements.txt":Y("Gemfile.lock")?"bundle install":null}async function gr(t,e,n){let o=qt(e),i=Date.now(),s=r.spin(`Forging "${o}" for branch ${h(e)}...`),a=await gt(t,o,{repo:n.repo,ttlMinutes:n.ttlMinutes,config:n.config,providerName:n.providerName,serverType:n.serverType,location:n.location,agent:n.agent,verbose:n.verbose}),c=r.spin(`Checking out ${h(e)}...`);if((await x({instanceName:o,ip:a.ip,command:"cd /root/project && git rev-parse --abbrev-ref HEAD",timeoutMs:1e4})).stdout.trim()===e)c.succeed(`Already on ${e}`);else{let u=await x({instanceName:o,ip:a.ip,command:`cd /root/project && git fetch origin '${e.replace(/'/g,"'\\''")}' && git checkout '${e.replace(/'/g,"'\\''")}'`,timeoutMs:6e4});u.exitCode!==0?(c.fail(`Failed to checkout ${e}`),u.stderr&&r.info(f(u.stderr.trim()))):c.succeed(`Checked out ${e}`)}if(!(!n.noTasks&&n.config?.tasks&&n.config.tasks.length>0)){let u=bi();if(u){let m=r.spin(`Installing deps (${u})...`),g=await x({instanceName:o,ip:a.ip,command:`cd /root/project && ${u}`,timeoutMs:3e5});g.exitCode!==0?(m.fail("Dep install failed"),g.stderr&&r.info(f(g.stderr.trim().slice(-500)))):m.succeed("Deps installed")}}if(n.run)if(n.port&&n.port.length>0)r.info(`Starting: ${h(n.run)} (background)`),await x({instanceName:o,ip:a.ip,command:`cd /root/project && nohup ${n.run} > /tmp/gibil-run.log 2>&1 &`,timeoutMs:3e4}),await new Promise(u=>setTimeout(u,3e3));else{r.info(""),r.info(`Running: ${h(n.run)}`);let u=await x({instanceName:o,ip:a.ip,command:`cd /root/project && ${n.run}`,stream:!n.json,timeoutMs:3e5});n.json&&r.info(u.stdout),u.exitCode!==0&&r.info(f(`Exit code: ${u.exitCode}`))}if(n.port&&n.port.length>0){let u=jn(a,n.port);r.info("");for(let m of u)r.info(` ${h(`http://localhost:${m}`)} \u2192 ${o}:${m}`);r.info(""),r.info(f(" Tunnel running in background. Kill with: lsof -ti :PORT | xargs kill"))}let d=((Date.now()-i)/1e3).toFixed(1);return s.succeed(T.createReady(o,d)),n.json?console.log(JSON.stringify({name:o,branch:e,ip:a.ip,ttl_minutes:n.ttlMinutes,ssh:`gibil ssh ${o}`})):(r.info(""),r.info(Qe(`${e}`,[`Server: ${o}`,`Branch: ${e}`,`IP: ${a.ip}`,`TTL: ${n.ttlMinutes} minutes`,"",`SSH: gibil ssh ${o}`,`Test: gibil run ${o} "pnpm test"`,`Done: gibil destroy ${o}`]))),a}function hr(t){t.command("branch <branches...>").description("Spin up a branch on a clean Linux server").option("-r, --repo <git-url>","Git repo URL (auto-detected from cwd)").option("--run <command>","Run a command after checkout").option("--ttl <duration>","Auto-destroy timer (e.g. 30, 2h, 7d, 1mo, 3mo, 1y)","30").option("--json","Output as JSON").option("--no-tasks","Skip .gibil.yml tasks").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async(e,n)=>{n.json&&I(!0);for(let u of e)wi(u);let o=Te(n.ttl),i=n.repo??yi(),s=null;if(s=await ke(i)??await Ee(process.cwd()),!n.agent){let u=await Ge();u&&(n.agent=u)}if(n.agent){if(!W.includes(n.agent))throw new Error(`Unknown agent "${n.agent}". Supported: ${W.join(", ")}`);if(!Pe[n.agent]?.some(m=>s?.env?.[m])){let m=Pe[n.agent]?.join(" or ")??"";r.warn(`${n.agent} needs ${m}. SSH in and export it (recommended) or pass with --env.`)}}if(n.dryRun){for(let u of e){let m=qt(u),g=n.serverType??s?.server_type??"cax11",p=n.location??s?.location??"nbg1",v=s?.image??"node:20",y=fe({repo:i,config:s??void 0,ttlMinutes:o,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:n.agent}),b={name:m,serverType:g,location:p,image:v,ttlMinutes:o,repo:i,agent:n.agent,cloudInitScript:y};n.json?r.json(b):(r.info(""),r.info(h("Dry run \u2014 no server will be created")),r.info(""),r.info(` ${f("Name:")} ${m}`),r.info(` ${f("Branch:")} ${u}`),r.info(` ${f("Server type:")} ${g}`),r.info(` ${f("Location:")} ${p}`),r.info(` ${f("Image:")} ${v}`),r.info(` ${f("TTL:")} ${o} minutes`),r.info(` ${f("Repo:")} ${i}`),n.agent&&r.info(` ${f("Agent:")} ${n.agent}`),r.info(""),r.info("Cloud-init script:"),r.info("\u2500".repeat(17)),r.info(y))}return}let a=await M();if(a){let u=await ce(a);r.info(`Authenticated as ${u.user.email} (${u.user.plan})`)}let c=n.provider??"hetzner",l=await H.get(c),d=n.serverType;if(!d&&n.size){let{isSizeName:u,resolveSize:m}=await Promise.resolve().then(()=>(Je(),Be));if(!u(n.size))throw new Error(`Unknown size "${n.size}". Valid sizes: small, medium, large.`);d=m(l,n.size)}if(e.length===1){let u=await gr(l,e[0],{repo:i,ttlMinutes:o,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:c,serverType:d,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose});a&&await Q(a,"create",u.name).catch(()=>{})}else{r.info(`Forging ${h(String(e.length))} branches in parallel...`),r.info("");let u=await Promise.allSettled(e.map(p=>gr(l,p,{repo:i,ttlMinutes:o,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:c,serverType:d,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose}))),m=u.filter(p=>p.status==="fulfilled"),g=u.filter(p=>p.status==="rejected");if(!n.json){if(r.info(""),r.info(`${m.length}/${e.length} branches ready.`),g.length>0)for(let p=0;p<u.length;p++){let v=u[p];v.status==="rejected"&&r.error(` ${e[p]}: ${v.reason instanceof Error?v.reason.message:String(v.reason)}`)}r.info(""),r.info(f(`Destroy all: gibil destroy ${e.map(qt).join(" ")}`))}if(a)for(let p of u)p.status==="fulfilled"&&await Q(a,"create",p.value.name).catch(()=>{});g.length>0&&process.exit(1)}})}function vr(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}D();import{spawn as $i}from"child_process";E();O();function yr(t){if(t.includes(":")){let e=t.split(":");if(e.length===2)return{local:e[0],host:"localhost",remote:e[1]};if(e.length===3)return{local:e[0],host:e[1],remote:e[2]};throw new Error(`Invalid port spec "${t}". Use PORT, LOCAL:REMOTE, or LOCAL:HOST:REMOTE.`)}return{local:t,host:"localhost",remote:t}}function Si(t){let{local:e,host:n,remote:o}=yr(t);return Ve(`${e}:${n}:${o}`),`${e}:${n}:${o}`}function wr(t){t.command("forward <name> <ports...>").description("Forward local ports to a running ephemeral machine via SSH").action(async(e,n)=>{let o=await C(e),i=n.map(c=>({spec:c,mapping:Si(c),...yr(c)})),s=["-N","-i",o.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];for(let{mapping:c}of i)s.push("-L",c);s.push(`root@${o.ip}`),r.info("");for(let{local:c,host:l,remote:d}of i)r.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${l}:${d}`);r.info(""),r.info(f(" Tunnel active. Press Ctrl+C to stop.")),r.info(""),$i("ssh",s,{stdio:"inherit"}).on("exit",c=>{let l=()=>process.exit(c??0);c===255?(r.warn(" SSH connection failed \u2014 checking if server still exists..."),q(o).then(l,l)):(r.info(" Tunnel closed."),l())})})}ze();G();E();O();st();async function xi(){let t=await jt(),e=await Promise.all(Object.values(kt).map(async n=>{let o=await xe(n.name);return{name:n.name,label:n.label,defaultRegion:n.defaultRegion,configured:o!==null,sizes:n.sizes}}));return{default:t,providers:e}}function $t(t,e){return t.length>=e?t:t+" ".repeat(e-t.length)}function _i(t){for(let e of t.providers){let n=e.name===t.default,o=e.configured?X("configured"):f("not configured"),i=n?h(" (default)"):"";if(r.info(""),r.info(`${h(e.label)}${i} ${f("\xB7")} region ${e.defaultRegion} ${f("\xB7")} ${o}`),!e.configured){r.info(f(` Run: gibil init --add ${e.name}`));let s=Dt(e.name,e.configured);s&&r.info(f(` ${s}`))}for(let s of e.sizes){let a=$t(s.name,8),c=$t(`${s.vcpu} vCPU`,7),l=$t(`${s.ramGb} GB`,6),d=$t(`${s.diskGb} GB SSD`,11);r.info(` ${a} ${c} ${l} ${d} ${f("\u2192")} ${s.nativeType}`)}}r.info("")}function br(t){t.command("providers").description("List supported providers, regions, and sizes").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await xi();e.json?r.json(n):_i(n)})}E();O();try{await import("dotenv/config")}catch{}var Ci=Ei(ki(import.meta.url)),$r={version:"0.0.0"};for(let t of["../package.json","../../package.json"])try{$r=JSON.parse(Pi(Ti(Ci,t),"utf-8"));break}catch{}var j=new Ii;j.name("gibil").description("Your own machine, on demand. Forge, use, burn.").version(`${$r.version} ${Xe}`,"-v, --version").addHelpText("before",`
39
- ${nn}
41
+ Open https://console.vultr.com/user/apiaccess/ \u2192 Access Control \u2192 add your IP.`):new Error(i)}}}function Sr(t){t.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").option("--provider <name>",`Configure a specific provider non-interactively (${de.join(", ")})`).option("--token <value>","API token for --provider; required when --provider is set").option("--set-default","Mark the configured provider as the default after setup").option("--add <name>",`Add a second provider interactively (${de.join(", ")})`).action(async e=>{if(e.provider){de.includes(e.provider)||(o.error(`Unknown provider "${e.provider}". Supported: ${de.join(", ")}.`),process.exit(1));let c=e.provider;e.token||(o.error("--token is required when --provider is set."),process.exit(1));let u=o.spin(`Verifying ${c} token...`);try{await $r(c,e.token)}catch(d){u.fail(d instanceof Error?d.message:String(d)),process.exit(1)}u.succeed(`${c} token verified`),await Ge(c,e.token),e.setDefault&&(await Pe(c),o.info(`${N} ${c} is now the default provider.`)),o.info(`${N} ${c} configured. Try: gibil create --provider ${c}`);return}if(e.add){de.includes(e.add)||(o.error(`Unknown provider "${e.add}". Supported: ${de.join(", ")}.`),process.exit(1));let c=e.add;await Wt(c,e.token),e.setDefault&&(await Pe(c),o.info(`${N} ${c} is now the default provider.`)),o.info(`${N} ${c} added. Try: gibil create --provider ${c}`);return}console.error(on);let n=await ji();if(n.providers.length>0&&!e.force){o.info(`${N} Already configured.`);for(let u of n.providers)o.detail(wr(u),ee("connected"));o.detail("Gibil API",n.apiKey?ee("connected"):m("not configured (optional)")),o.info(""),o.info(` Run ${h("gibil init --force")} to reconfigure.`);let c=de.filter(u=>!n.providers.includes(u));c.length>0&&o.info(` Run ${h(`gibil init --add ${c[0]}`)} to add ${wr(c[0])}.`),o.info(` Run ${h("gibil create")} to forge a server.`);return}let r=await Ni(),i;if(r==="vultr"?(await Wt("vultr"),await Pe("vultr")):(i=await Wt("hetzner"),await Pe("hetzner")),r==="hetzner"&&i){let c=o.spin("Detecting available server types..."),u="cax11",d="fsn1",f=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let p of f)try{let y=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:p.type,image:"ubuntu-24.04",location:p.location,start_after_create:!1})})).json();if(y.server){await fetch(`https://api.hetzner.cloud/v1/servers/${y.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${i}`}}),u=p.type,d=p.location;break}}catch{}await Nt(u,d),c.succeed(`Default server type: ${u} (${d})`)}let s=o.spin("Configuring MCP for Claude Code...");try{let c=br(Ai(),".claude.json"),u={};try{u=JSON.parse(Ti(c,"utf-8"))}catch{}u.mcpServers||(u.mcpServers={}),u.mcpServers.gibil=rt(),Ci(c,JSON.stringify(u,null,2)+`
42
+ `),s.succeed("MCP configured for Claude Code (~/.claude.json)")}catch{s.fail("Could not auto-configure MCP"),o.info(m(" Run gibil mcp --print-config for manual setup"))}o.info(""),o.info(h("Default coding agent (optional)")),o.info(m(` Install a coding agent on every server. Options: ${Q.join(", ")}`)),o.info(m(" Press Enter to skip \u2014 you can always use --agent later.")),o.info("");let l=(await Me(" Default agent [none]: ")).toLowerCase().trim();l&&Q.includes(l)?(await it(l),o.info(` ${N} Default agent: ${ee(l)}`)):l?o.info(m(` Unknown agent "${l}", skipping. Use --agent with: ${Q.join(", ")}`)):(await it(null),o.info(m(" No default agent. Use --agent claude (or aider, codex) when creating servers."))),o.info(""),o.info(C.initComplete),o.info(""),o.info(m(" Try it now:")),o.info(` ${h('gibil branch feat/my-feature --run "pnpm test"')}`),o.info(` ${h("gibil ssh feat-my-feature")}`),o.info(` ${h("gibil destroy feat-my-feature")}`),o.info(""),o.info(m(" Or with full control:")),o.info(` ${h("gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10")}`),o.info(` ${h('gibil run demo "npm test"')}`),o.info(` ${h("gibil destroy demo")}`),o.info(""),o.info(m(" Later:")),o.info(` ${h("gibil auth login")} ${m("Add a Gibil API key (optional)")}`),o.info(` ${h("gibil mcp --print-config")} ${m("MCP setup for other editors")}`),o.info("")})}async function xr(){if(process.env.HETZNER_API_TOKEN||process.env.VULTR_API_KEY)return!1;let t=br(P.root,"config.json");return!Ei(t)}ke();import{execSync as Hi}from"child_process";import{existsSync as Y}from"fs";E();R();G();function Mi(){try{let t=Hi("git remote get-url origin",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();if(!t)throw new Error("empty");return t}catch{throw new Error("Not in a git repo, or no remote configured. Use --repo to specify.")}}function Ki(t){if(!t||!t.trim())throw new Error("Branch name cannot be empty.");if(/[;&|$`(){}<>!;"'\s\\]/.test(t))throw new Error(`Invalid branch name "${t}". Contains shell-unsafe characters.`)}function Zt(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function Di(){return Y("pnpm-lock.yaml")?"pnpm install":Y("bun.lockb")||Y("bun.lock")?"bun install":Y("yarn.lock")?"yarn install":Y("package-lock.json")?"npm install":Y("Cargo.lock")?"cargo build":Y("go.sum")?"go mod download":Y("uv.lock")?"uv sync":Y("poetry.lock")?"poetry install":Y("requirements.txt")?"pip install -r requirements.txt":Y("Gemfile.lock")?"bundle install":null}async function _r(t,e,n){let r=Zt(e),i=Date.now(),s=o.spin(`Forging "${r}" for branch ${h(e)}...`),a=await yt(t,r,{repo:n.repo,ttlMinutes:n.ttlMinutes,config:n.config,providerName:n.providerName,serverType:n.serverType,location:n.location,agent:n.agent,verbose:n.verbose}),l=o.spin(`Checking out ${h(e)}...`);if((await k({instanceName:r,ip:a.ip,command:"cd /root/project && git rev-parse --abbrev-ref HEAD",timeoutMs:1e4})).stdout.trim()===e)l.succeed(`Already on ${e}`);else{let d=await k({instanceName:r,ip:a.ip,command:`cd /root/project && git fetch origin '${e.replace(/'/g,"'\\''")}' && git checkout '${e.replace(/'/g,"'\\''")}'`,timeoutMs:6e4});d.exitCode!==0?(l.fail(`Failed to checkout ${e}`),d.stderr&&o.info(m(d.stderr.trim()))):l.succeed(`Checked out ${e}`)}if(!(!n.noTasks&&n.config?.tasks&&n.config.tasks.length>0)){let d=Di();if(d){let f=o.spin(`Installing deps (${d})...`),p=await k({instanceName:r,ip:a.ip,command:`cd /root/project && ${d}`,timeoutMs:3e5});p.exitCode!==0?(f.fail("Dep install failed"),p.stderr&&o.info(m(p.stderr.trim().slice(-500)))):f.succeed("Deps installed")}}if(n.run)if(n.port&&n.port.length>0)o.info(`Starting: ${h(n.run)} (background)`),await k({instanceName:r,ip:a.ip,command:`cd /root/project && nohup ${n.run} > /tmp/gibil-run.log 2>&1 &`,timeoutMs:3e4}),await new Promise(d=>setTimeout(d,3e3));else{o.info(""),o.info(`Running: ${h(n.run)}`);let d=await k({instanceName:r,ip:a.ip,command:`cd /root/project && ${n.run}`,stream:!n.json,timeoutMs:3e5});n.json&&o.info(d.stdout),d.exitCode!==0&&o.info(m(`Exit code: ${d.exitCode}`))}if(n.port&&n.port.length>0){let d=await Ln(a,n.port);o.info("");for(let f of d)o.info(` ${h(`http://localhost:${f}`)} \u2192 ${r}:${f}`);o.info(""),o.info(m(" Tunnel running in background. Kill with: lsof -ti :PORT | xargs kill"))}let u=((Date.now()-i)/1e3).toFixed(1);return s.succeed(C.createReady(r,u)),n.json?console.log(JSON.stringify({name:r,branch:e,ip:a.ip,ttl_minutes:n.ttlMinutes,ssh:`gibil ssh ${r}`})):(o.info(""),o.info(et(`${e}`,[`Server: ${r}`,`Branch: ${e}`,`IP: ${a.ip}`,`TTL: ${n.ttlMinutes} minutes`,"",`SSH: gibil ssh ${r}`,`Test: gibil run ${r} "pnpm test"`,`Done: gibil destroy ${r}`]))),a}function Pr(t){t.command("branch <branches...>").description("Spin up a branch on a clean Linux server").option("-r, --repo <git-url>","Git repo URL (auto-detected from cwd)").option("--run <command>","Run a command after checkout").option("--ttl <duration>","Auto-destroy timer (e.g. 30, 2h, 7d, 1mo, 3mo, 1y)","30").option("--json","Output as JSON").option("--no-tasks","Skip .gibil.yml tasks").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async(e,n)=>{n.json&&I(!0);for(let d of e)Ki(d);let r=Ce(n.ttl),i=n.repo??Mi(),s=null;if(s=await Ee(i)??await Te(process.cwd()),!n.agent){let d=await Fe();d&&(n.agent=d)}if(n.agent){if(!Q.includes(n.agent))throw new Error(`Unknown agent "${n.agent}". Supported: ${Q.join(", ")}`);if(!Ie[n.agent]?.some(f=>s?.env?.[f])){let f=Ie[n.agent]?.join(" or ")??"";o.warn(`${n.agent} needs ${f}. SSH in and export it (recommended) or pass with --env.`)}}if(n.dryRun){for(let d of e){let f=Zt(d),p=n.serverType??s?.server_type??"cax11",g=n.location??s?.location??"nbg1",y=s?.image??"node:20",v=he({repo:i,config:s??void 0,ttlMinutes:r,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:n.agent}),S={name:f,serverType:p,location:g,image:y,ttlMinutes:r,repo:i,agent:n.agent,cloudInitScript:v};n.json?o.json(S):(o.info(""),o.info(h("Dry run \u2014 no server will be created")),o.info(""),o.info(` ${m("Name:")} ${f}`),o.info(` ${m("Branch:")} ${d}`),o.info(` ${m("Server type:")} ${p}`),o.info(` ${m("Location:")} ${g}`),o.info(` ${m("Image:")} ${y}`),o.info(` ${m("TTL:")} ${r} minutes`),o.info(` ${m("Repo:")} ${i}`),n.agent&&o.info(` ${m("Agent:")} ${n.agent}`),o.info(""),o.info("Cloud-init script:"),o.info("\u2500".repeat(17)),o.info(v))}return}let a=await M();if(a){let d=await ce(a);o.info(`Authenticated as ${d.user.email} (${d.user.plan})`)}let l=n.provider??"hetzner",c=await F.get(l),u=n.serverType;if(!u&&n.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ve(),Je));if(!d(n.size))throw new Error(`Unknown size "${n.size}". Valid sizes: small, medium, large.`);u=f(c,n.size)}if(e.length===1){let d=await _r(c,e[0],{repo:i,ttlMinutes:r,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:l,serverType:u,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose});a&&await te(a,"create",d.name).catch(()=>{})}else{o.info(`Forging ${h(String(e.length))} branches in parallel...`),o.info("");let d=await Promise.allSettled(e.map(g=>_r(c,g,{repo:i,ttlMinutes:r,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:l,serverType:u,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose}))),f=d.filter(g=>g.status==="fulfilled"),p=d.filter(g=>g.status==="rejected");if(!n.json){if(o.info(""),o.info(`${f.length}/${e.length} branches ready.`),p.length>0)for(let g=0;g<d.length;g++){let y=d[g];y.status==="rejected"&&o.error(` ${e[g]}: ${y.reason instanceof Error?y.reason.message:String(y.reason)}`)}o.info(""),o.info(m(`Destroy all: gibil destroy ${e.map(Zt).join(" ")}`))}if(a)for(let g of d)g.status==="fulfilled"&&await te(a,"create",g.value.name).catch(()=>{});p.length>0&&process.exit(1)}})}function kr(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}O();import{spawn as Li}from"child_process";E();R();function Ir(t){if(t.includes(":")){let e=t.split(":");if(e.length===2)return{local:e[0],host:"localhost",remote:e[1]};if(e.length===3)return{local:e[0],host:e[1],remote:e[2]};throw new Error(`Invalid port spec "${t}". Use PORT, LOCAL:REMOTE, or LOCAL:HOST:REMOTE.`)}return{local:t,host:"localhost",remote:t}}function zi(t){let{local:e,host:n,remote:r}=Ir(t);return qe(`${e}:${n}:${r}`),`${e}:${n}:${r}`}function Er(t){t.command("forward <name> <ports...>").description("Forward local ports to a running ephemeral machine via SSH").action(async(e,n)=>{let r=await A(e),i=n.map(c=>({spec:c,mapping:zi(c),...Ir(c)})),s=await Ae(r);s||o.warn(m(` No pinned host key for ${e} (instance pre-dates v0.4.x). Tunnel is insecure on this hop; running any \`gibil run ${e}\` will pin it.`));let a=["-N","-i",r.keyPath,"-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];s?a.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${s}`):a.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null");for(let{mapping:c}of i)a.push("-L",c);a.push(`root@${r.ip}`),o.info("");for(let{local:c,host:u,remote:d}of i)o.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${u}:${d}`);o.info(""),o.info(m(" Tunnel active. Press Ctrl+C to stop.")),o.info(""),Li("ssh",a,{stdio:"inherit"}).on("exit",c=>{let u=()=>process.exit(c??0);c===255?(o.warn(" SSH connection failed \u2014 checking if server still exists..."),q(r).then(u,u)):(o.info(" Tunnel closed."),u())})})}ze();G();E();R();at();async function Gi(){let t=await At(),e=await Promise.all(Object.values(Et).map(async n=>{let r=await _e(n.name);return{name:n.name,label:n.label,defaultRegion:n.defaultRegion,configured:r!==null,sizes:n.sizes}}));return{default:t,providers:e}}function St(t,e){return t.length>=e?t:t+" ".repeat(e-t.length)}function Fi(t){for(let e of t.providers){let n=e.name===t.default,r=e.configured?ee("configured"):m("not configured"),i=n?h(" (default)"):"";if(o.info(""),o.info(`${h(e.label)}${i} ${m("\xB7")} region ${e.defaultRegion} ${m("\xB7")} ${r}`),!e.configured){o.info(m(` Run: gibil init --add ${e.name}`));let s=Mt(e.name,e.configured);s&&o.info(m(` ${s}`))}for(let s of e.sizes){let a=St(s.name,8),l=St(`${s.vcpu} vCPU`,7),c=St(`${s.ramGb} GB`,6),u=St(`${s.diskGb} GB SSD`,11);o.info(` ${a} ${l} ${c} ${u} ${m("\u2192")} ${s.nativeType}`)}}o.info("")}function Tr(t){t.command("providers").description("List supported providers, regions, and sizes").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await Gi();e.json?o.json(n):Fi(n)})}E();R();try{await import("dotenv/config")}catch{}var Yi=Vi(Ji(import.meta.url)),Cr={version:"0.0.0"};for(let t of["../package.json","../../package.json"])try{Cr=JSON.parse(Bi(qi(Yi,t),"utf-8"));break}catch{}var j=new Ui;j.name("gibil").description("Your own machine, on demand. Forge, use, burn.").version(`${Cr.version} ${Qe}`,"-v, --version").addHelpText("before",`
43
+ ${sn}
40
44
  `).addHelpText("after",`
41
- ${f("Docs:")} https://gibil.dev/docs
42
- `);fr(j);Cn(j);Mn(j);Dn(j);Wn(j);Zn(j);Qn(j);er(j);nr(j);rr(j);or(j);sr(j);hr(j);wr(j);br(j);async function ji(){vr();let t=process.argv.slice(2);!(t.length===0||t.includes("init")||t.includes("auth")||t.includes("--help")||t.includes("-h")||t.includes("--version")||t.includes("-v")||t.includes("-V")||t.includes("mcp")||t.includes("ssh")||t.includes("run")||t.includes("exec")||t.includes("list")||t.includes("ls")||t.includes("forward")||t.includes("providers"))&&await pr()&&(r.info(""),r.info(T.setupNeeded),r.info(""),process.exit(1)),await Jn()&&await Bt()&&r.info(f("gibil collects anonymous usage stats to improve the CLI. To disable: GIBIL_TELEMETRY=0"));let o=t[0]??"help",i=qn(process.argv),s=Date.now(),a=0;try{await j.parseAsync(process.argv)}catch(l){a=1,l instanceof Error&&r.error(l.message)}["auth","config","--help","-h","--version","-v","help"].includes(o)||await We({event:"command",command:o,flags:i,exit_code:a,duration_ms:Date.now()-s}),a!==0&&process.exit(1)}ji();
45
+ ${m("Docs:")} https://gibil.dev/docs
46
+ `);Sr(j);Mn(j);Bn(j);Jn(j);ir(j);sr(j);cr(j);lr(j);dr(j);fr(j);mr(j);gr(j);Pr(j);Er(j);Tr(j);async function Wi(){kr();let t=process.argv.slice(2);!(t.length===0||t.includes("init")||t.includes("auth")||t.includes("--help")||t.includes("-h")||t.includes("--version")||t.includes("-v")||t.includes("-V")||t.includes("mcp")||t.includes("ssh")||t.includes("run")||t.includes("exec")||t.includes("list")||t.includes("ls")||t.includes("forward")||t.includes("providers"))&&await xr()&&(o.info(""),o.info(C.setupNeeded),o.info(""),process.exit(1)),await tr()&&await qt()&&o.info(m("gibil collects anonymous usage stats to improve the CLI. To disable: GIBIL_TELEMETRY=0"));let r=t[0]??"help",i=rr(process.argv),s=Date.now(),a=0;try{await j.parseAsync(process.argv)}catch(c){a=1,c instanceof Error&&o.error(c.message)}["auth","config","--help","-h","--version","-v","help"].includes(r)||await Ze({event:"command",command:r,flags:i,exit_code:a,duration_ms:Date.now()-s}),a!==0&&process.exit(1)}Wi();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gibil",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Your own machine, on demand. Forge, use, burn.",
5
5
  "homepage": "https://gibil.dev",
6
6
  "type": "module",