gibil 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +33 -33
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`)}function
|
|
4
|
-
${
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
${
|
|
2
|
+
var Yn=Object.defineProperty;var N=(t,e)=>()=>(t&&(e=t(t=0)),e);var ne=(t,e)=>{for(var r in e)Yn(t,r,{get:e[r],enumerable:!0})};import _e from"picocolors";function ct(t,e){let r=Math.max(t.length+4,...e.map(c=>Ht(c).length+4)),n=`${m("\u256D")}${m("\u2500".repeat(r))}${m("\u256E")}`,i=`${m("\u2570")}${m("\u2500".repeat(r))}${m("\u256F")}`,s=`${m("\u2502")} ${L} ${h(t)}${" ".repeat(r-Ht(t).length-4)}${m("\u2502")}`,a=`${m("\u251C")}${m("\u2500".repeat(r))}${m("\u2524")}`,l=e.map(c=>{let u=r-Ht(c).length-2;return`${m("\u2502")} ${c}${" ".repeat(Math.max(0,u))}${m("\u2502")}`});return[n,s,a,...l,i].join(`
|
|
3
|
+
`)}function Ht(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var oe,Zn,te,Xn,m,h,L,R,Be,vr,at,yr,wr,hr,Ue,T,O=N(()=>{"use strict";oe=t=>_e.red(t),Zn=t=>_e.yellow(t),te=t=>_e.green(t),Xn=t=>_e.red(t),m=t=>_e.dim(t),h=t=>_e.bold(t),L="\u{1F98E}",R=te("\u2713"),Be=Xn("\u2716"),vr=Zn("\u26A0"),at="\u{12248}",yr=`
|
|
4
|
+
${oe(" /\\")}
|
|
5
|
+
${oe(" / \\")}
|
|
6
|
+
${oe(" / \u{1F525} \\")}
|
|
7
|
+
${oe(" / \\")}
|
|
8
8
|
${m(" ~~~~~~~~")}
|
|
9
|
-
${h(" g i b i l")} ${m(
|
|
10
|
-
`,
|
|
9
|
+
${h(" g i b i l")} ${m(at)}
|
|
10
|
+
`,wr=`${L} ${h("gibil")} ${m(at)}`,hr=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],Ue=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=oe(hr[this.frame%hr.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
12
|
`)}succeed(e){this.stop(),process.stderr.write(`\r ${R} ${e??this.text}
|
|
13
|
-
`)}fail(e){this.stop(),process.stderr.write(`\r ${
|
|
14
|
-
`)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};
|
|
15
|
-
Your Hetzner token may be invalid or expired. Run: gibil init --force`:
|
|
16
|
-
A server with this name already exists. Try a different --name or run: gibil destroy <name>`:
|
|
17
|
-
This server type may not be available in your region. Run: gibil init --force`:
|
|
18
|
-
Rate limited by Hetzner. Wait a moment and retry your command
|
|
13
|
+
`)}fail(e){this.stop(),process.stderr.write(`\r ${Be} ${e??this.text}
|
|
14
|
+
`)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};T={welcome:`${L} Your first fire. Welcome to Gibil.`,noInstances:`${L} No fires burning. Gibil sleeps.`,destroyAll:`${L} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${L} "${t}" \u2014 fire out.`,authSuccess:`${L} Logged in. The forge is yours.`,authLogout:`${L} Logged out. The forge cools.`,createReady:(t,e)=>`${L} "${t}" forged ${m(`(${e}s)`)}`,fleetReady:(t,e)=>`${L} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${L} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${L} The forge is ready. Run ${h("gibil create")} to light your first fire.`,setupNeeded:`${L} No forge configured. Run ${h("gibil init")} to get started.`}});function I(t){Mt=t}function ie(t){return Mt&&t!=="error"?!1:br[t]>=br[Qn]}var Qn,Mt,br,o,E=N(()=>{"use strict";O();Qn="info",Mt=!1,br={debug:0,info:1,warn:2,error:3,silent:4};o={debug(t,...e){ie("debug")&&console.debug(`${m("[debug]")} ${t}`,...e)},info(t,...e){ie("info")&&console.log(t,...e)},warn(t,...e){ie("warn")&&console.warn(`${vr} ${t}`,...e)},error(t,...e){ie("error")&&console.error(`${Be} ${t}`,...e)},success(t){ie("info")&&console.log(`${R} ${t}`)},step(t){ie("info")&&console.log(` ${m("\u203A")} ${t}`)},flame(t){ie("info")&&console.log(t)},detail(t,e){ie("info")&&console.log(` ${m(t+":")} ${e}`)},spin(t){return Mt?new Ue(t):new Ue(t).start()},json(t){console.log(JSON.stringify(t,null,2))}}});async function se(t,e){let{intervalMs:r,timeoutMs:n,swallowErrors:i}=e,s=Date.now();for(;Date.now()-s<n;){try{if(await t())return{timedOut:!1}}catch(a){if(!i)throw a}await new Promise(a=>setTimeout(a,r))}return{timedOut:!0}}var Je=N(()=>{"use strict"});var Pe,Lt=N(()=>{"use strict";E();Pe=class{constructor(e){this.config=e}async request(e,r,n){let{baseUrl:i,token:s,providerName:a,parseError:l,hint:c}=this.config,u=`${i}${r}`;o.debug(`${e} ${u}`);let d=await fetch(u,{method:e,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:n?JSON.stringify(n):void 0,signal:AbortSignal.timeout(3e4)});if(!d.ok){let f=await d.text(),g=l(f);throw new Error(`${a} API error (${d.status}): ${g}${c(d.status,g)}`)}return d.status===204?{}:await d.json()}}});var ke,lt,Kt=N(()=>{"use strict";ke=["small","medium","large"],lt={small:{target:{vcpu:2,ramGb:4},providers:{hetzner:{vcpu:2,ramGb:4,diskGb:40,nativeType:"cax11"},vultr:{vcpu:2,ramGb:4,diskGb:80,nativeType:"vc2-2c-4gb"}}},medium:{target:{vcpu:4,ramGb:8},providers:{hetzner:{vcpu:4,ramGb:8,diskGb:80,nativeType:"cax21"},vultr:{vcpu:4,ramGb:8,diskGb:160,nativeType:"vc2-4c-8gb"}}},large:{target:{vcpu:8,ramGb:16},providers:{hetzner:{vcpu:8,ramGb:16,diskGb:160,nativeType:"cax31"},vultr:{vcpu:6,ramGb:16,diskGb:320,nativeType:"vc2-6c-16gb"}}}}});var Sr={};ne(Sr,{HETZNER_META:()=>ut,PROVIDER_CATALOG:()=>Dt,VULTR_META:()=>dt});function $r(t){return ke.map(e=>{let r=lt[e].providers[t];return{name:e,vcpu:r.vcpu,ramGb:r.ramGb,diskGb:r.diskGb,nativeType:r.nativeType}})}var ut,dt,Dt,Ve=N(()=>{"use strict";Kt();ut={name:"hetzner",label:"Hetzner Cloud",defaultRegion:"fsn1",sizes:$r("hetzner")},dt={name:"vultr",label:"Vultr",defaultRegion:"nrt",sizes:$r("vultr")},Dt={hetzner:ut,vultr:dt}});import{homedir as eo}from"os";import{join as Q,resolve as to}from"path";import{existsSync as ro}from"fs";function X(){return process.env.GIBIL_HOME??Q(eo(),".gibil")}function mt(){let t=process.argv[1];if(t){let e=to(t);if(ro(e))return{command:process.execPath,args:[e,"mcp"]}}return{command:"gibil",args:["mcp"]}}var P,F=N(()=>{"use strict";P={get root(){return X()},get instances(){return Q(X(),"instances")},get keys(){return Q(X(),"keys")},get jobs(){return Q(X(),"jobs")},get knownHostsDir(){return Q(X(),"known_hosts")},instanceFile:t=>Q(X(),"instances",`${t}.json`),keyDir:t=>Q(X(),"keys",t),privateKey:t=>Q(X(),"keys",t,"id_ed25519"),publicKey:t=>Q(X(),"keys",t,"id_ed25519.pub"),knownHostsFile:t=>Q(X(),"known_hosts",t)}});var Ye={};ne(Ye,{clearApiKey:()=>Ft,fetchUsage:()=>Vt,getApiKey:()=>K,getApiUrl:()=>co,getApiUrlFromConfig:()=>ft,getDefaultAgent:()=>We,getDefaultProvider:()=>Gt,getHetznerToken:()=>uo,getProviderToken:()=>Ee,getServerDefaults:()=>mo,saveApiKey:()=>zt,saveDefaultAgent:()=>pt,saveHetznerToken:()=>Ut,saveProviderToken:()=>qe,saveServerDefaults:()=>Bt,setDefaultProvider:()=>Te,trackUsage:()=>Jt,verifyApiKey:()=>ae});import{readFile as no,writeFile as oo,mkdir as io}from"fs/promises";import{existsSync as so}from"fs";import{join as ao}from"path";function xr(){return ao(P.root,"config.json")}async function G(){let t=xr();if(!so(t))return{};let e=await no(t,"utf-8"),r=JSON.parse(e);return r.hetzner_token&&!r.providers?.hetzner?.token&&(r.providers={...r.providers??{},hetzner:{token:r.hetzner_token,default_server_type:r.default_server_type,default_location:r.default_location,...r.providers?.hetzner??{}}}),r}async function Ie(t){await io(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 oo(xr(),JSON.stringify(e,null,2),{mode:384})}async function zt(t){let e=await G();e.api_key=t,await Ie(e)}async function K(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await G()).api_key??null}async function Ft(){let t=await G();delete t.api_key,await Ie(t)}function co(){return process.env.GIBIL_API_URL??_r}async function ft(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await G()).api_url??_r}async function Ee(t){let e=process.env[lo[t]];return e||((await G()).providers?.[t]?.token??null)}async function qe(t,e){let r=await G();r.providers||(r.providers={}),r.providers[t]={...r.providers[t]??{},token:e},await Ie(r)}async function Gt(){return(await G()).default_provider??"hetzner"}async function Te(t){let e=await G();e.default_provider=t,await Ie(e)}async function Ut(t){await qe("hetzner",t)}async function uo(){return Ee("hetzner")}async function Bt(t,e){let r=await G();r.providers||(r.providers={}),r.providers.hetzner={...r.providers.hetzner??{},default_server_type:t,default_location:e},await Ie(r)}async function mo(){let t=await G(),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 pt(t){let e=await G();t?e.default_agent=t:delete e.default_agent,await Ie(e)}async function We(){return(await G()).default_agent??null}async function ae(t){let e=await ft(),r=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(r.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!r.ok){let n=await r.text();throw new Error(`API error (${r.status}): ${n}`)}return await r.json()}async function Jt(t,e,r,n){let i=await ft(),s=await fetch(`${i}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:r,server_type:n})});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 Vt(t){let e=await ft(),r=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!r.ok){let n=await r.text();throw new Error(`Failed to fetch usage (${r.status}): ${n}`)}return await r.json()}var _r,lo,D=N(()=>{"use strict";F();_r="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1";lo={hetzner:"HETZNER_API_TOKEN",vultr:"VULTR_API_KEY"}});var Pr={};ne(Pr,{HetznerProvider:()=>Wt});function po(t){try{return JSON.parse(t).error?.message??t}catch{return t}}function go(t,e){return t===401||t===403?`
|
|
15
|
+
Your Hetzner token may be invalid or expired. Run: gibil init --force`:t===409&&e.includes("name")?`
|
|
16
|
+
A server with this name already exists. Try a different --name or run: gibil destroy <name>`:t===422&&(e.includes("location")||e.includes("server_type"))?`
|
|
17
|
+
This server type may not be available in your region. Run: gibil init --force`:t===429?`
|
|
18
|
+
Rate limited by Hetzner. Wait a moment and retry your command.`:""}function qt(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 ho(t){return{id:t.id,name:t.name,fingerprint:t.fingerprint??""}}var fo,Wt,kr=N(()=>{"use strict";E();Je();Lt();Ve();fo="https://api.hetzner.cloud/v1";Wt=class t{http;constructor(e){this.http=new Pe({baseUrl:fo,token:e,providerName:"Hetzner",parseError:po,hint:go})}static async create(e){let{getHetznerToken:r}=await Promise.resolve().then(()=>(D(),Ye)),n=e??await r();if(!n)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(n)}async request(e,r,n){return this.http.request(e,r,n)}async createServer(e,r,n,i,s){if(!i||!s){let{getServerDefaults:d}=await Promise.resolve().then(()=>(D(),Ye)),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 r=="string"?parseInt(r,10):r,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})}`),n&&(u.user_data=n);try{let d=await this.request("POST","/servers",u);return qt(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 r=await this.request("GET",`/servers/${e}`);return qt(r.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(qt)}async waitForReady(e,r=12e4){let n,{timedOut:i}=await se(async()=>{let s=await this.getServer(e);return n=s,s.status==="running"&&s.ipv4!=="0.0.0.0"?!0:(o.debug(`Server ${e} status: ${s.status}, waiting...`),!1)},{intervalMs:3e3,timeoutMs:r});if(i||!n)throw new Error(`Server ${e} did not become ready within ${r/1e3}s`);return n}async createSSHKey(e,r){let n=await this.request("POST","/ssh_keys",{name:e,public_key:r});return ho(n.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}sizes(){return ut.sizes}}});var Er={};ne(Er,{buildAffiliateNudgeLine:()=>wo,buildAffiliateProviderNudge:()=>Yt,getAffiliateProgram:()=>gt,getAffiliateProgramNames:()=>yo});function vo(){let t=process.env.GIBIL_NO_REFERRAL?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}function gt(t){return vo()?null:Ir.programs[t]??null}function yo(){return Object.keys(Ir.programs)}function wo(t){return t?`
|
|
19
19
|
|
|
20
20
|
No ${t.label} account? Get ${t.credit} free credits \u2192 ${t.ref_url}
|
|
21
|
-
${t.disclosure}`:""}function
|
|
22
|
-
Your Vultr API key may be invalid. Re-run: gibil init --provider vultr --token <new-key>`:
|
|
23
|
-
Rate limited by Vultr. Wait a moment and retry
|
|
24
|
-
`}function
|
|
21
|
+
${t.disclosure}`:""}function Yt(t,e){if(e)return null;let r=gt(t);return r?`New here? ${r.credit} free credits \u2192 ${r.ref_url}`:null}var Ir,ht=N(()=>{"use strict";Ir={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 Tr={};ne(Tr,{VultrProvider:()=>Xt});function $o(t){try{return JSON.parse(t).error??t}catch{return t}}function So(t,e){return t===401||t===403?`
|
|
22
|
+
Your Vultr API key may be invalid. Re-run: gibil init --provider vultr --token <new-key>`:t===429?`
|
|
23
|
+
Rate limited by Vultr. Wait a moment and retry.`:""}function Zt(t){let e=t.status;t.status==="active"&&t.power_status==="running"?e="running":t.status==="pending"&&(e="initializing");let r={};for(let n of t.tags??[])r[n]="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:r,created:t.date_created}}function _o(t){return{id:t.id,name:t.name,fingerprint:""}}var bo,xo,Xt,Cr=N(()=>{"use strict";E();Je();Lt();Ve();bo="https://api.vultr.com/v2";xo=2284;Xt=class t{http;constructor(e){this.http=new Pe({baseUrl:bo,token:e,providerName:"Vultr",parseError:$o,hint:So})}static async create(e){let r=e;if(!r){let{getProviderToken:n}=await Promise.resolve().then(()=>(D(),Ye));r=await n("vultr")??void 0}if(!r){let{getAffiliateProgram:n,buildAffiliateNudgeLine:i}=await Promise.resolve().then(()=>(ht(),Er)),s=i(n("vultr"));throw new Error(`VULTR_API_KEY is required. Run 'gibil init --provider vultr' or set it in your environment.${s}`)}return new t(r)}async request(e,r,n){return this.http.request(e,r,n)}async createServer(e,r,n,i,s){let a=i??"vc2-2c-4gb",l=s??"nrt",c={region:l,plan:a,os_id:xo,label:e,tags:["gibil",`gibil-${e}`],sshkey_id:[String(r)]};n&&(c.user_data=Buffer.from(n,"utf-8").toString("base64"));try{let u=await this.request("POST","/instances",c);return Zt(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 r=await this.request("GET",`/instances/${e}`);return Zt(r.instance)}async listServers(e="gibil=true"){let r=e.split("=")[0]||"gibil";return((await this.request("GET",`/instances?tag=${encodeURIComponent(r)}&per_page=50`)).instances??[]).map(Zt)}async waitForReady(e,r=12e4){let n,{timedOut:i}=await se(async()=>{let s=await this.getServer(e);return n=s,s.status==="running"&&s.ipv4!=="0.0.0.0"?!0:(o.debug(`Vultr instance ${e} status: ${s.status}, waiting...`),!1)},{intervalMs:3e3,timeoutMs:r});if(i||!n)throw new Error(`Vultr instance ${e} did not become ready within ${r/1e3}s`);return n}async createSSHKey(e,r){let n=await this.request("POST","/ssh-keys",{name:e,ssh_key:r});return _o(n.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh-keys/${e}`)}sizes(){return dt.sizes}}});var Ar={};ne(Ar,{ProviderRegistry:()=>vt,providerRegistry:()=>U});var vt,U,Ce=N(()=>{"use strict";vt=class{factories=new Map;register(e,r){this.factories.set(e,r)}async get(e){let r=this.factories.get(e);if(!r)throw new Error(`Provider "${e}" is not registered. Registered: ${this.listRegistered().join(", ")||"(none)"}`);return r()}async forInstance(e){let r=e.provider??"hetzner";return this.get(r)}listRegistered(){return[...this.factories.keys()]}},U=new vt;U.register("hetzner",async()=>{let{HetznerProvider:t}=await Promise.resolve().then(()=>(kr(),Pr));return t.create()});U.register("vultr",async()=>{let{VultrProvider:t}=await Promise.resolve().then(()=>(Cr(),Tr));return t.create()})});import{readFile as Ao,writeFile as jo,mkdir as Rr,rm as Hr,readdir as No,rename as Ro}from"fs/promises";import{existsSync as Or}from"fs";import{join as Qt}from"path";async function tr(t,e,r){let n=`${t}.tmp`;await jo(n,e,r);try{await Ro(n,t)}catch(i){throw await Hr(n,{force:!0}).catch(s=>{console.warn(`Warning: failed to clean up temp file ${n}: ${s}`)}),i}}var er,ce,pe,rr,wt,C,ge,ee,H=N(()=>{"use strict";F();er=class{instancesDir;keysDir;constructor(e){let r=e??P.root;this.instancesDir=Qt(r,"instances"),this.keysDir=Qt(r,"keys")}async ensureDirectories(){await Rr(this.instancesDir,{recursive:!0,mode:448}),await Rr(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return Qt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await tr(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let r=this.instanceFile(e);if(!Or(r))return null;let n=await Ao(r,"utf-8");return JSON.parse(n)}async loadOrThrow(e){let r=await this.load(e);if(!r)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return r}async loadActiveOrThrow(e){let r=await this.loadOrThrow(e);if(new Date>new Date(r.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${r.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return r}async delete(e){let r=this.instanceFile(e);Or(r)&&await Hr(r)}async list(){await this.ensureDirectories();let e=await No(this.instancesDir),r=[];for(let n of e){if(!n.endsWith(".json"))continue;let i=n.replace(".json",""),s=await this.load(i);s&&r.push(s)}return r}},ce=new er,pe=t=>ce.save(t),rr=t=>ce.load(t),wt=t=>ce.loadOrThrow(t),C=t=>ce.loadActiveOrThrow(t),ge=t=>ce.delete(t),ee=()=>ce.list()});var Xe={};ne(Xe,{GIBIL_SIZE_TARGETS:()=>Xo,SIZE_NAMES:()=>ke,isSizeName:()=>Qo,resolveSize:()=>ei});function Qo(t){return ke.includes(t)}function ei(t,e){let r=t.sizes().find(n=>n.name===e);if(!r){let n=t.sizes().map(i=>i.name).join(", ")||"(none)";throw new Error(`Size "${e}" is not available on this provider. Available: ${n}`)}return r.nativeType}var Xo,Qe=N(()=>{"use strict";Kt();Xo=Object.fromEntries(ke.map(t=>[t,lt[t].target]))});var on={};ne(on,{JobStore:()=>It,deleteJob:()=>mi,deleteJobsByInstance:()=>tt,listJobs:()=>He,listJobsByInstance:()=>fi,loadJob:()=>di,loadJobOrThrow:()=>ue,saveJob:()=>q});import{readFile as ci,mkdir as tn,rm as li,readdir as ui}from"fs/promises";import{existsSync as rn}from"fs";import{join as nn}from"path";var It,ye,q,di,ue,mi,He,fi,tt,we=N(()=>{"use strict";F();H();It=class{jobsDir;constructor(e){let r=e??P.root;this.jobsDir=nn(r,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return nn(this.jobsDir,`${e}.json`)}async save(e){await tn(this.jobsDir,{recursive:!0,mode:448}),await tr(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let r=this.jobFile(e);if(!rn(r))return null;let n=await ci(r,"utf-8");return JSON.parse(n)}async loadOrThrow(e){let r=await this.load(e);if(!r)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return r}async delete(e){let r=this.jobFile(e);rn(r)&&await li(r)}async list(){await tn(this.jobsDir,{recursive:!0,mode:448});let e=await ui(this.jobsDir),r=[];for(let n of e){if(!n.endsWith(".json"))continue;let i=n.replace(".json",""),s=await this.load(i);s&&r.push(s)}return r}async listByInstance(e){return(await this.list()).filter(n=>n.instance===e)}async deleteByInstance(e){let r=await this.listByInstance(e);for(let n of r)await this.delete(n.id)}},ye=new It,q=t=>ye.save(t),di=t=>ye.load(t),ue=t=>ye.loadOrThrow(t),mi=t=>ye.delete(t),He=()=>ye.list(),fi=t=>ye.listByInstance(t),tt=t=>ye.deleteByInstance(t)});import{Command as cs}from"commander";import{readFileSync as ls}from"fs";import{fileURLToPath as us}from"url";import{dirname as ds,join as ms}from"path";Ce();F();import{mkdir as Po,rm as jr,readFile as ko,chmod as Io}from"fs/promises";import{existsSync as Nr}from"fs";import{execFile as Eo}from"child_process";import{promisify as To}from"util";var Co=To(Eo);async function yt(t){let e=P.keyDir(t);Nr(e)&&await jr(e,{recursive:!0}),await Po(e,{recursive:!0});let r=P.privateKey(t),n=P.publicKey(t);await Co("ssh-keygen",["-t","ed25519","-f",r,"-N","","-C",`gibil-${t}`]),await Io(r,384);let i=await ko(n,"utf-8");return{privateKeyPath:r,publicKeyPath:n,publicKey:i.trim()}}async function re(t){let e=P.keyDir(t);Nr(e)&&await jr(e,{recursive:!0})}F();E();Je();O();H();import{Client as Kr}from"ssh2";import{readFile as Dr}from"fs/promises";import{createHash as Oo,timingSafeEqual as Ho}from"crypto";function nr(t){return`SHA256:${Oo("sha256").update(t).digest("base64").replace(/=+$/,"")}`}function Mo(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 Mr(t,e,r){let n=Mo(r),i=r.toString("base64");return`${e===22?t:`[${t}]:${e}`} ${n} ${i}
|
|
24
|
+
`}function Lo(t){return t.toString("base64")}function bt(t){return Buffer.from(t,"base64")}function Ko(t,e){if(t.length!==e.length)return!1;let r=Buffer.from(t,"utf8"),n=Buffer.from(e,"utf8");return r.length!==n.length?!1:Ho(r,n)}function or(t,e){let r=Lo(e),n=nr(e);return t?Ko(t,r)?{outcome:"verified",fingerprint:n,presentedBase64:r}:{outcome:"mismatch",fingerprint:n,presentedBase64:r}:{outcome:"pinned",fingerprint:n,presentedBase64:r}}async function Lr(t,e,r){let n=await t.load(e);if(!n)throw new Error(`Instance "${e}" not found \u2014 cannot persist host key.`);await t.save({...n,hostPublicKey:r})}var zr=(t,e)=>Lr(ce,t,e);async function x(t){let{instanceName:e,ip:r,command:n,stream:i=!1,timeoutMs:s=3e4,port:a=22}=t,l=await Dr(P.privateKey(e),"utf-8"),c=(await rr(e))?.hostPublicKey,u=null;return new Promise((d,f)=>{let g=new Kr,p="",v="",w=null,b=!1;g.on("ready",()=>{o.debug(`SSH connected to ${r}`),(u?.outcome==="pinned"?zr(e,u.presentedBase64).catch(_=>{o.debug(`Failed to persist host key for ${e}: ${_}`)}):Promise.resolve()).finally(()=>{g.exec(n,(_,y)=>{if(_)return g.end(),f(_);w=setTimeout(()=>{b||(b=!0,g.destroy(),f(new Error(`Command timed out after ${s/1e3}s on ${r}`)))},s),y.on("data",$=>{let j=$.toString();p+=j,i&&process.stdout.write(j)}),y.stderr.on("data",$=>{let j=$.toString();v+=j,i&&process.stderr.write(j)}),y.on("close",$=>{w&&clearTimeout(w),!b&&(b=!0,g.end(),d({stdout:p,stderr:v,exitCode:$??0}))})})})}).on("error",k=>{if(w&&clearTimeout(w),b)return;if(b=!0,u?.outcome==="mismatch"){let y=c?nr(bt(c)):"(unknown)";return f(new Error(`SSH host key for ${r} does not match the pinned fingerprint for "${e}".
|
|
25
25
|
Pinned: ${y}
|
|
26
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
|
|
28
|
-
`)}function ko(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 Io(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function Eo(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}=${B(s)}`;return r+=` ${B(t.image)}`,e.push(r),e.push(""),e}var Rn={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},Ee={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},M=Object.keys(Rn);function To(t){return Rn[t]??null}function B(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as Co}from"fs/promises";import{existsSync as On,statSync as Ao}from"fs";import{join as jo}from"path";import{parse as Mn}from"yaml";async function Te(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 c=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:s});if(!c.ok)return null;let l=await c.text();return Ro(l)}catch{return null}}var No=".gibil.yml";async function Ce(t){let e;if(On(t)&&Ao(t).isFile()?e=t:e=jo(t,No),!On(e))return null;let n=await Co(e,"utf-8"),r=Mn(n);return Kn(r)}function Ro(t){let e=Mn(t);return Kn(e)}function Kn(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:Hn(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=Hn(e.env,"top-level")),n}function Hn(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}H();import{randomBytes as Oo}from"crypto";function ve(t=6){return Oo(Math.ceil(t/2)).toString("hex").slice(0,t)}function Ve(){return`gibil-${ve()}`}function Dn(){return`fleet-${ve(8)}`}function yt(){return`j-${ve(8)}`}z();E();var Ho=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function vt(t){if(!Ho.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 wt(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 q=525600,Mo={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function Ae(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>q)throw new Error(`TTL cannot exceed 1 year (${q} 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=Mo[i],c=r*s;if(c<=0)throw new Error(`TTL must be positive, got "${t}"`);if(c>q)throw new Error(`TTL cannot exceed 1 year (${q} minutes). Got "${t}" = ${c} minutes.`);return c}F();O();import{execSync as bt}from"child_process";import{readFileSync as zo}from"fs";var Zt="key::";function Go(){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=zo(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(Zt))&&(n=i.startsWith(Zt)?i.slice(Zt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function $t(t,e,n){o.step("Generating SSH keys...");let r=await mt(e),i,s;try{o.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${ve(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 c=Go(),l=ye({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:c,agent:n.agent}),a=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,a.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,v={name:e,serverId:u.id,ip:p,sshKeyId:i.id,keyPath:k.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:c,provider:n.providerName??"hetzner"};await ge(v);let w=o.spin("Waiting for SSH...");if(await ht(e,p),w.succeed("SSH ready"),n.repo||n.config){let b=o.spin("Provisioning (runtime, repo, deps)..."),P;n.verbose&&!n.json&&(P=Nn({instanceName:e,ip:p,filePath:"/var/log/cloud-init-output.log"}));let x=36e4,y=5e3,$=Date.now(),T=!1;for(;Date.now()-$<x;){try{if((await _({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(V=>setTimeout(V,y))}if(P?.abort(),T)b.succeed("Provisioning complete");else{b.fail("Provisioning may have failed");try{let V=await _({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(V.stdout)}catch{o.warn("Could not read cloud-init log.")}}}return v}catch(c){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 oe(e).catch(l=>o.warn(`Could not clean up local SSH keys: ${l instanceof Error?l.message:String(l)}`)),c}}function zn(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 Gn(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=Ae(e.ttl??"60"),r=wt(e.fleet??"1","Fleet count");if(r>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&vt(e.name),!e.agent){let d=await Be();d&&(e.agent=d)}if(e.agent&&!M.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${M.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 Ce(e.config):e.repo?s=await Te(e.repo)??await Ce(process.cwd()):s=await Ce(process.cwd()),Object.keys(i).length>0&&(s||(s={}),s.env={...s.env,...i}),e.dryRun){let d=e.name??Ve(),f=e.provider??"hetzner",p=e.serverType??s?.server_type;if(!p&&e.size){let{isSizeName:x}=await Promise.resolve().then(()=>(Ye(),qe));if(!x(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);let{PROVIDER_CATALOG:y}=await Promise.resolve().then(()=>(Fe(),mn));p=y[f]?.sizes.find(T=>T.name===e.size)?.nativeType}p||(p=f==="vultr"?"vc2-2c-4gb":"cax11");let g=p,v=e.location??s?.location??(f==="vultr"?"nrt":"nbg1"),w=s?.image??"node:20",b=ye({repo:e.repo,config:s??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),P={name:d,serverType:g,location:v,image:w,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:b};e.json?o.json(P):(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:")} ${v}`),o.info(` ${m("Image:")} ${w}`),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(b));return}let c=await D();if(c){o.info("Verifying API key...");let d=await le(c);o.info(` Authenticated as ${d.user.email} (${d.user.plan})`)}if(e.agent&&!Ee[e.agent]?.some(f=>s?.env?.[f]||i[f])){let f=Ee[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",a=await U.get(l),u=e.serverType;if(!u&&e.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ye(),qe));if(!d(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);u=f(a,e.size)}if(r===1){let d=e.name??Ve(),f=Date.now(),p=o.spin(`Forging "${d}"...`),g=await $t(a,d,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,agent:e.agent,verbose:e.verbose}),v=((Date.now()-f)/1e3).toFixed(1);p.succeed(C.createReady(d,v)),c&&await re(c,"create",g.name,e.serverType).catch(w=>o.debug(`Usage tracking failed: ${w instanceof Error?w.message:String(w)}`)),e.json?o.json(zn(g)):(o.info(""),o.info(ot("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=Dn(),f=e.name??"gibil",p=Date.now(),g=o.spin(`Forging fleet "${d}" \u2014 ${r} servers...`),v=Array.from({length:r},(y,$)=>`${f}-${$+1}-${d.slice(6)}`),w=await Promise.allSettled(v.map(y=>$t(a,y,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,fleetId:d,agent:e.agent,verbose:e.verbose}))),b=[],P=[];for(let y=0;y<w.length;y++){let $=w[y];$.status==="fulfilled"?b.push($.value):P.push(`${v[y]}: ${$.reason instanceof Error?$.reason.message:String($.reason)}`)}let x=((Date.now()-p)/1e3).toFixed(1);if(g.succeed(C.fleetReady(b.length,r)+` ${m(`(${x}s)`)}`),c&&await Promise.all(b.map(y=>re(c,"create",y.name,e.serverType).catch($=>o.debug(`Usage tracking failed for ${y.name}: ${$ instanceof Error?$.message:String($)}`)))),e.json)o.json({fleet_id:d,instances:b.map(zn),errors:P});else{o.info("");for(let y of b)o.info(` ${R} ${h(y.name)} ${m("\u2192")} ${y.ip}`);for(let y of P)o.info(` ${Ge} ${y}`);o.info("")}}})}H();import{spawn as ti}from"child_process";import{spawn as Jo}from"child_process";import{existsSync as Vo}from"fs";z();import{writeFile as Fo,mkdir as Uo,rm as Bo}from"fs/promises";var Fn={dir:k.knownHostsDir,file:k.knownHostsFile};async function je(t,e=Fn){if(!t.hostPublicKey)return null;await Uo(e.dir,{recursive:!0,mode:448});let n=e.file(t.name),r=gt(t.hostPublicKey),i=En(t.ip,22,r);return await Fo(n,i,{mode:384}),n}async function Un(t,e=Fn){await Bo(e.file(t),{force:!0})}E();O();function We(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;Xt(n,t),Xt(r,t)}else Xt(t,t)}function Xt(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 Bn(t,e){if(!Vo(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=await je(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){We(i);let s=i.includes(":")?i:`${i}:localhost:${i}`,c=i.includes(":")?i.split(":")[0]:i;r.push(c);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}`),Jo("ssh",l,{stdio:"ignore",detached:!0}).unref()}return r}E();O();E();H();be();var ei=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function $e(t){if(!(t instanceof Error))return!1;let e=t.message;return ei.some(n=>e.includes(n))}async function W(t){try{let{providerRegistry:e}=await Promise.resolve().then(()=>(Ie(),Sn));return await(await e.forInstance(t)).getServer(t.serverId),"still_exists"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await oe(t.name),await Ze(t.name),await he(t.name),o.warn(`Instance "${t.name}" no longer exists on the provider \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function ni(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){We(n);let r=n.includes(":")?n:`${n}:localhost:${n}`;e.push("-L",r)}return e.push(`root@${t.ip}`),e}function Wn(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 je(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=ni({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 a=l.includes(":")?l.split(":")[0]:l;o.info(` Forwarding ${h(`localhost:${a}`)} \u2192 ${e}:${l}`)}o.info(""),o.info(m(" Tunnel active while SSH session is open. Ctrl+C to stop.")),o.info("")}ti("ssh",s,{stdio:"inherit"}).on("exit",l=>{let a=()=>process.exit(l??0);l===255?W(r).then(a,a):a()})})}H();be();E();function Zn(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(" "),c=r.timeout?wt(r.timeout,"Timeout")*1e3:3e4;if(r.background){let a=yt(),u="/root/.gibil-jobs",d=`${u}/${a}.log`,f=`${u}/${a}.exit`,p=`${u}/${a}.pid`,g=`${u}/${a}.sh`,v=["#!/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
|
-
`),w=Buffer.from(v).toString("base64"),b=`mkdir -p ${u} && echo '${w}' | base64 -d > ${
|
|
30
|
-
${
|
|
31
|
-
${
|
|
32
|
-
${m(`${r.length} server(s)`)}`)})}function fr(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 vi(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return fr(n)}H();E();function mr(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=Ae(n.ttl);try{await _({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(c){throw $e(c)&&await W(r)==="cleaned"&&process.exit(1),c}let s=new Date(Date.now()+i*6e4).toISOString();r.ttlMinutes=i,r.expiresAt=s,await ge(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 wi}from"fs/promises";import{randomBytes as bi}from"crypto";H();E();function pr(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 wi(n.script,"utf-8");o.info(`Uploading and running script "${n.script}" on "${e}"...`);let s=Buffer.from(i).toString("base64"),c=`/tmp/gibil-script-${bi(4).toString("hex")}.sh`,l;try{l=await _({instanceName:e,ip:r.ip,command:`echo '${s}' | base64 -d > ${c} && chmod +x ${c} && ${c}; EXIT=$?; rm -f ${c}; exit $EXIT`,stream:!n.json})}catch(a){throw $e(a)&&await W(r)==="cleaned"&&process.exit(1),a}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)})}F();E();O();import{createInterface as $i}from"readline";function gr(t){let e=$i({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,r=>{e.close(),n(r.trim())})})}function hr(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 gr("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 le(r);await Rt(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 gr("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 Mt(r),o.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await Ot(),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 D();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 le(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.`)}})}F();E();function yr(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 D();n||(o.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let r=await Dt(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 Pi}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as ki}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as S}from"zod";Ie();import{execSync as Si}from"child_process";import{existsSync as Z}from"fs";E();O();F();function xi(){try{let t=Si("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 tn(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 en(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function _i(){return Z("pnpm-lock.yaml")?"pnpm install":Z("bun.lockb")||Z("bun.lock")?"bun install":Z("yarn.lock")?"yarn install":Z("package-lock.json")?"npm install":Z("Cargo.lock")?"cargo build":Z("go.sum")?"go mod download":Z("uv.lock")?"uv sync":Z("poetry.lock")?"poetry install":Z("requirements.txt")?"pip install -r requirements.txt":Z("Gemfile.lock")?"bundle install":null}async function vr(t,e,n){let r=en(e),i=Date.now(),s=o.spin(`Forging "${r}" for branch ${h(e)}...`),c=await $t(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 _({instanceName:r,ip:c.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 _({instanceName:r,ip:c.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=_i();if(d){let f=o.spin(`Installing deps (${d})...`),p=await _({instanceName:r,ip:c.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 _({instanceName:r,ip:c.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 _({instanceName:r,ip:c.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 Bn(c,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:c.ip,ttl_minutes:n.ttlMinutes,ssh:`gibil ssh ${r}`})):(o.info(""),o.info(ot(`${e}`,[`Server: ${r}`,`Branch: ${e}`,`IP: ${c.ip}`,`TTL: ${n.ttlMinutes} minutes`,"",`SSH: gibil ssh ${r}`,`Test: gibil run ${r} "pnpm test"`,`Done: gibil destroy ${r}`]))),c}function wr(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)tn(d);let r=Ae(n.ttl),i=n.repo??xi(),s=null;if(s=await Te(i)??await Ce(process.cwd()),!n.agent){let d=await Be();d&&(n.agent=d)}if(n.agent){if(!M.includes(n.agent))throw new Error(`Unknown agent "${n.agent}". Supported: ${M.join(", ")}`);if(!Ee[n.agent]?.some(f=>s?.env?.[f])){let f=Ee[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=en(d),p=n.serverType??s?.server_type??"cax11",g=n.location??s?.location??"nbg1",v=s?.image??"node:20",w=ye({repo:i,config:s??void 0,ttlMinutes:r,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:n.agent}),b={name:f,serverType:p,location:g,image:v,ttlMinutes:r,repo:i,agent:n.agent,cloudInitScript:w};n.json?o.json(b):(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:")} ${v}`),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(w))}return}let c=await D();if(c){let d=await le(c);o.info(`Authenticated as ${d.user.email} (${d.user.plan})`)}let l=n.provider??"hetzner",a=await U.get(l),u=n.serverType;if(!u&&n.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ye(),qe));if(!d(n.size))throw new Error(`Unknown size "${n.size}". Valid sizes: small, medium, large.`);u=f(a,n.size)}if(e.length===1){let d=await vr(a,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});c&&await re(c,"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=>vr(a,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 v=d[g];v.status==="rejected"&&o.error(` ${e[g]}: ${v.reason instanceof Error?v.reason.message:String(v.reason)}`)}o.info(""),o.info(m(`Destroy all: gibil destroy ${e.map(en).join(" ")}`))}if(c)for(let g of d)g.status==="fulfilled"&&await re(c,"create",g.value.name).catch(()=>{});p.length>0&&process.exit(1)}})}function br(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}Ie();H();z();import{execSync as _t}from"child_process";import{readFileSync as Ii}from"fs";be();H();be();E();async function nn(t){let e=await de(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 _({instanceName:e.instance,ip:n.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING"){try{if((await _({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 Y(e);let w=Math.round((v.getTime()-new Date(e.startedAt).getTime())/1e3),b;try{b=(await _({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:w,stdout:b}}}catch{}return{status:"running"}}let a=parseInt(l,10),u=await _({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4}),d=a===0?"done":"failed",f=new Date,p=Math.round((f.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=d,e.exitCode=a,e.completedAt=f.toISOString(),await Y(e),{status:d,exitCode:a,stdout:u.stdout,durationS:p}}function $r(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 de(n),s=await nn(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 Ne(),i=await te(),s=new Set(i.map(c=>c.name));for(let c of r)c.status==="running"&&!s.has(c.instance)&&(c.status="orphaned",c.completedAt=new Date().toISOString(),await Y(c));if(r.length===0){n.json?o.json([]):o.info("No background jobs.");return}if(n.json)o.json(r.map(c=>({job_id:c.id,instance:c.instance,command:c.command,status:c.status,started_at:c.startedAt,exit_code:c.exitCode})));else for(let c of r){let l=c.status==="running"?"\u27F3 running":c.status==="done"?"\u2713 done":c.status==="orphaned"?"\u26A0 orphaned":`\u2717 ${c.status}`;o.info(` ${c.id} ${l} ${c.instance} ${c.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 de(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 _({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 Y(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 de(n),s=await A(i.instance),c=`/root/.gibil-jobs/${n}.log`,l=r.follow?`tail -f ${c}`:`cat ${c} 2>/dev/null || echo '(no output yet)'`,a=r.follow?3e5:1e4,u=await _({instanceName:i.instance,ip:s.ip,command:l,stream:!r.json,timeoutMs:a});r.json&&o.json({job_id:n,stdout:u.stdout})})}function N(t,e,n){let r=`[${t}] ${e}`;return n&&(r+=`
|
|
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 _="";k.code==="ECONNREFUSED"?_=" (instance may have been destroyed or is still booting)":k.code==="EHOSTUNREACH"?_=" (IP unreachable \u2014 instance may not be running)":k.code==="ETIMEDOUT"&&(_=" (connection timed out \u2014 check if instance is running with 'gibil list')"),f(new Error(`SSH connection to ${r} failed: ${k.message}${_}`))}).connect({host:r,port:a,username:"root",privateKey:l,readyTimeout:s,hostVerifier:k=>(u=or(c,k),u.outcome!=="mismatch"),agent:process.env.SSH_AUTH_SOCK,agentForward:!!process.env.SSH_AUTH_SOCK})})}function Fr(t){let{instanceName:e,ip:r,filePath:n,timeoutMs:i=3e4}=t,s=null,a=!1;return(async()=>{try{let l=await Dr(P.privateKey(e),"utf-8"),c=(await rr(e))?.hostPublicKey,u=null;s=new Kr,await new Promise((d,f)=>{s.on("ready",()=>{(u?.outcome==="pinned"?zr(e,u.presentedBase64).catch(()=>{}):Promise.resolve()).finally(()=>{s.exec(`tail -f ${n} 2>/dev/null`,(p,v)=>{if(p)return s.end(),f(p);v.on("data",w=>{a||process.stdout.write(m(w.toString()))}),v.stderr.on("data",w=>{a||process.stderr.write(m(w.toString()))}),v.on("close",()=>{s.end(),d()})})})}).on("error",g=>{a||o.debug(`Verbose log tail failed: ${g.message}`),f(g)}).connect({host:r,port:22,username:"root",privateKey:l,readyTimeout:i,hostVerifier:g=>(u=or(c,g),u.outcome!=="mismatch")})})}catch{}})(),{abort(){if(a=!0,s)try{s.end()}catch{}}}}async function $t(t,e,r=12e4){let{timedOut:n}=await se(async()=>{try{return await x({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4}),!0}catch(i){throw o.debug(`SSH not ready on ${e}, retrying...`),i}},{intervalMs:5e3,timeoutMs:r,swallowErrors:!0});if(n)throw new Error(`SSH did not become available on ${e} within ${r/1e3}s`)}Je();E();async function Gr(t,e={}){let r=e.sshExec??x,n=e.waitForSSH??$t,i=e.tailRemoteLog??Fr,{name:s,ip:a,repo:l,config:c,verbose:u,json:d}=t,f=o.spin("Waiting for SSH...");if(await n(s,a),f.succeed("SSH ready"),l||c){let g=o.spin("Provisioning (runtime, repo, deps)..."),p;u&&!d&&(p=i({instanceName:s,ip:a,filePath:"/var/log/cloud-init-output.log"}));let{timedOut:v}=await se(async()=>(await r({instanceName:s,ip:a,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready",{intervalMs:5e3,timeoutMs:36e4,swallowErrors:!0}),w=!v;if(p?.abort(),w)g.succeed("Provisioning complete");else{g.fail("Provisioning may have failed");try{let b=await r({instanceName:s,ip:a,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});o.info(b.stdout)}catch{o.warn("Could not read cloud-init log.")}}return{ready:w}}return{ready:!0}}function he(t){let{repo:e,config:r,ttlMinutes:n,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=${B(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=r?.image??"node:20";if(a.push(...Do(l)),t.agent){let c=Go(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(r?.services&&r.services.length>0){a.push(...zo()),a.push("");for(let c of r.services)a.push(...Fo(c))}if(r?.env){a.push("# Environment variables");for(let[c,u]of Object.entries(r.env))a.push(`export ${c}=${B(u)}`),a.push(`echo ${B(`${c}=${u}`)} >> /etc/environment`);a.push("")}if(a.push("# Configure git"),s?(a.push(`git config --global user.email ${B(s.email)}`),a.push(`git config --global user.name ${B(s.name)}`),s.signingKey&&(a.push("git config --global gpg.format ssh"),a.push(`git config --global user.signingkey ${B("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 ${B(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 ${B(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(n&&n>0&&(a.push("# Auto-destroy after TTL"),a.push(`echo "shutdown -h now" | at now + ${n} minutes 2>/dev/null || true`),a.push(`(sleep ${n*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&&r?.tasks&&r.tasks.length>0){a.push("# Run project tasks"),a.push("cd /root/project");for(let c of r.tasks)a.push(`echo '\u25B6 Running task: '${B(c.name)}`),a.push(`if ! ${c.command}; then`),a.push(` echo '\u2717 Task failed: '${B(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 Do(t){let e=[];if(t.startsWith("node:")){let r=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${r}.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 r=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${r} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let r=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${r}.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 zo(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function Fo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let n=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(n+=` -p ${t.port}:${t.port}`),t.env)for(let[i,s]of Object.entries(t.env))n+=` -e ${i}=${B(s)}`;return n+=` ${B(t.image)}`,e.push(n),e.push(""),e}var Ur={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},Ae={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},M=Object.keys(Ur);function Go(t){return Ur[t]??null}function B(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as Uo}from"fs/promises";import{existsSync as Br,statSync as Bo}from"fs";import{join as Jo}from"path";import{parse as Vr}from"yaml";async function je(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,r,n]=e,i=`https://raw.githubusercontent.com/${r}/${n}/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 qo(l)}catch{return null}}var Vo=".gibil.yml";async function Ne(t){let e;if(Br(t)&&Bo(t).isFile()?e=t:e=Jo(t,Vo),!Br(e))return null;let r=await Uo(e,"utf-8"),n=Vr(r);return qr(n)}function qo(t){let e=Vr(t);return qr(e)}function qr(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,r={};return typeof e.name=="string"&&(r.name=e.name),typeof e.image=="string"&&(r.image=e.image),typeof e.server_type=="string"&&(r.server_type=e.server_type),typeof e.location=="string"&&(r.location=e.location),Array.isArray(e.services)&&(r.services=e.services.map(n=>{let i=n;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:Jr(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(r.tasks=e.tasks.map(n=>{let i=n;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&&(r.env=Jr(e.env,"top-level")),r}function Jr(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 r={};for(let[n,i]of Object.entries(t))if(typeof i=="string")r[n]=i;else if(typeof i=="number"||typeof i=="boolean")r[n]=String(i);else throw new Error(`env.${n} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(r).length>0?r:void 0}H();import{randomBytes as Wo}from"crypto";function ve(t=6){return Wo(Math.ceil(t/2)).toString("hex").slice(0,t)}function Ze(){return`gibil-${ve()}`}function Wr(){return`fleet-${ve(8)}`}function St(){return`j-${ve(8)}`}F();E();var Yo=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function xt(t){if(!Yo.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 _t(t,e){let r=parseInt(t,10);if(isNaN(r)||r<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return r}var V=525600,Zo={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function Re(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>V)throw new Error(`TTL cannot exceed 1 year (${V} minutes). Got ${l} minutes.`);return l}let r=e.match(/^(\d+)(mo|[mhdwy])$/);if(!r)throw new Error(`Invalid TTL "${t}". Use a number (minutes) or a duration: 2h, 7d, 1w, 1mo, 3mo, 6mo, 1y`);let n=parseInt(r[1],10),i=r[2],s=Zo[i],a=n*s;if(a<=0)throw new Error(`TTL must be positive, got "${t}"`);if(a>V)throw new Error(`TTL cannot exceed 1 year (${V} minutes). Got "${t}" = ${a} minutes.`);return a}D();D();async function le(t,e,r,n){t&&await Jt(t,e,r,n?.serverType).catch(n?.onTrackFailure??(()=>{}))}O();import{execSync as Pt}from"child_process";import{readFileSync as ti}from"fs";var ir="key::";function ri(){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 r;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{r=ti(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(ir))&&(r=i.startsWith(ir)?i.slice(ir.length):i)}}}catch{}return{name:t,email:e,signingKey:r}}catch{return}}async function kt(t,e,r){o.step("Generating SSH keys...");let n=await yt(e),i,s;try{o.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${ve(4)}`,n.publicKey),r.repo&&r.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=ri(),l=he({repo:r.repo,config:r.config??void 0,ttlMinutes:r.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a,agent:r.agent}),c=o.spin(`Creating server on ${r.providerName??"hetzner"}...`),u=await t.createServer(e,i.id,l,r.serverType??r.config?.server_type,r.location??r.config?.location);s=u.id,c.succeed("Server created");let d=o.spin("VM booting..."),g=(await t.waitForReady(u.id)).ipv4;d.succeed(`VM running at ${g}`);let p=new Date,v={name:e,serverId:u.id,ip:g,sshKeyId:i.id,keyPath:P.privateKey(e),status:"running",createdAt:p.toISOString(),ttlMinutes:r.ttlMinutes,expiresAt:new Date(p.getTime()+r.ttlMinutes*6e4).toISOString(),repo:r.repo,fleetId:r.fleetId,gitIdentity:a,provider:r.providerName??"hetzner"};return await pe(v),await Gr({name:e,ip:g,repo:r.repo,config:r.config??void 0,verbose:r.verbose,json:r.json}),v}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 re(e).catch(l=>o.warn(`Could not clean up local SSH keys: ${l instanceof Error?l.message:String(l)}`)),a}}function Yr(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 Zr(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 r=Re(e.ttl??"60"),n=_t(e.fleet??"1","Fleet count");if(n>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&xt(e.name),!e.agent){let d=await We();d&&(e.agent=d)}if(e.agent&&!M.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${M.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 Ne(e.config):e.repo?s=await je(e.repo)??await Ne(process.cwd()):s=await Ne(process.cwd()),Object.keys(i).length>0&&(s||(s={}),s.env={...s.env,...i}),e.dryRun){let d=e.name??Ze(),f=e.provider??"hetzner",g=e.serverType??s?.server_type;if(!g&&e.size){let{isSizeName:_}=await Promise.resolve().then(()=>(Qe(),Xe));if(!_(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);let{PROVIDER_CATALOG:y}=await Promise.resolve().then(()=>(Ve(),Sr));g=y[f]?.sizes.find(j=>j.name===e.size)?.nativeType}g||(g=f==="vultr"?"vc2-2c-4gb":"cax11");let p=g,v=e.location??s?.location??(f==="vultr"?"nrt":"nbg1"),w=s?.image??"node:20",b=he({repo:e.repo,config:s??void 0,ttlMinutes:r,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),k={name:d,serverType:p,location:v,image:w,ttlMinutes:r,repo:e.repo,agent:e.agent,cloudInitScript:b};e.json?o.json(k):(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:")} ${p}`),o.info(` ${m("Location:")} ${v}`),o.info(` ${m("Image:")} ${w}`),o.info(` ${m("TTL:")} ${r} 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(b));return}let a=await K();if(a){o.info("Verifying API key...");let d=await ae(a);o.info(` Authenticated as ${d.user.email} (${d.user.plan})`)}if(e.agent&&!Ae[e.agent]?.some(f=>s?.env?.[f]||i[f])){let f=Ae[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 U.get(l),u=e.serverType;if(!u&&e.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Qe(),Xe));if(!d(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);u=f(c,e.size)}if(n===1){let d=e.name??Ze(),f=Date.now(),g=o.spin(`Forging "${d}"...`),p=await kt(c,d,{repo:e.repo,ttlMinutes:r,config:s,providerName:l,serverType:u,location:e.location,agent:e.agent,verbose:e.verbose}),v=((Date.now()-f)/1e3).toFixed(1);g.succeed(T.createReady(d,v)),await le(a,"create",p.name,{serverType:e.serverType,onTrackFailure:w=>o.debug(`Usage tracking failed: ${w instanceof Error?w.message:String(w)}`)}),e.json?o.json(Yr(p)):(o.info(""),o.info(ct("Server ready",[`${m("Name:")} ${h(p.name)}`,`${m("IP:")} ${p.ip}`,`${m("TTL:")} ${r} minutes`,`${m("SSH:")} ${h(`gibil ssh ${p.name}`)}`])),o.info(""),o.info(m(" Try:")),o.info(` ${h(`gibil run ${p.name} "<your test command>"`)}`),o.info(` ${h(`gibil ssh ${p.name}`)}`),o.info(` ${h(`gibil destroy ${p.name}`)}`),o.info(""))}else{let d=Wr(),f=e.name??"gibil",g=Date.now(),p=o.spin(`Forging fleet "${d}" \u2014 ${n} servers...`),v=Array.from({length:n},(y,$)=>`${f}-${$+1}-${d.slice(6)}`),w=await Promise.allSettled(v.map(y=>kt(c,y,{repo:e.repo,ttlMinutes:r,config:s,providerName:l,serverType:u,location:e.location,fleetId:d,agent:e.agent,verbose:e.verbose}))),b=[],k=[];for(let y=0;y<w.length;y++){let $=w[y];$.status==="fulfilled"?b.push($.value):k.push(`${v[y]}: ${$.reason instanceof Error?$.reason.message:String($.reason)}`)}let _=((Date.now()-g)/1e3).toFixed(1);if(p.succeed(T.fleetReady(b.length,n)+` ${m(`(${_}s)`)}`),await Promise.all(b.map(y=>le(a,"create",y.name,{serverType:e.serverType,onTrackFailure:$=>o.debug(`Usage tracking failed for ${y.name}: ${$ instanceof Error?$.message:String($)}`)}))),e.json)o.json({fleet_id:d,instances:b.map(Yr),errors:k});else{o.info("");for(let y of b)o.info(` ${R} ${h(y.name)} ${m("\u2192")} ${y.ip}`);for(let y of k)o.info(` ${Be} ${y}`);o.info("")}}})}H();import{spawn as gi}from"child_process";import{spawn as si}from"child_process";import{existsSync as ai}from"fs";F();import{writeFile as ni,mkdir as oi,rm as ii}from"fs/promises";var Xr={dir:P.knownHostsDir,file:P.knownHostsFile};async function Oe(t,e=Xr){if(!t.hostPublicKey)return null;await oi(e.dir,{recursive:!0,mode:448});let r=e.file(t.name),n=bt(t.hostPublicKey),i=Mr(t.ip,22,n);return await ni(r,i,{mode:384}),r}async function Qr(t,e=Xr){await ii(e.file(t),{force:!0})}E();O();function et(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[r,,n]=e;sr(r,t),sr(n,t)}else sr(t,t)}function sr(t,e){let r=parseInt(t,10);if(isNaN(r)||r<1||r>65535||String(r)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}async function en(t,e){if(!ai(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let r=await Oe(t);r||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 n=[];for(let i of e){et(i);let s=i.includes(":")?i:`${i}:localhost:${i}`,a=i.includes(":")?i.split(":")[0]:i;n.push(a);let l=["-f","-N","-L",s,"-i",t.keyPath,"-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];r?l.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${r}`):l.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null"),l.push(`root@${t.ip}`),si("ssh",l,{stdio:"ignore",detached:!0}).unref()}return n}E();O();E();H();we();var pi=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function be(t){if(!(t instanceof Error))return!1;let e=t.message;return pi.some(r=>e.includes(r))}async function W(t){try{let{providerRegistry:e}=await Promise.resolve().then(()=>(Ce(),Ar));return await(await e.forInstance(t)).getServer(t.serverId),"still_exists"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await re(t.name),await tt(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 hi(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 r of t.ports){et(r);let n=r.includes(":")?r:`${r}:localhost:${r}`;e.push("-L",n)}return e.push(`root@${t.ip}`),e}function sn(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,r)=>{let n=await C(e),i=await Oe(n);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=hi({ip:n.ip,keyPath:n.keyPath,knownHostsPath:i??void 0,ports:r.port});if(r.port&&r.port.length>0){o.info("");for(let l of r.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("")}gi("ssh",s,{stdio:"inherit"}).on("exit",l=>{let c=()=>process.exit(l??0);l===255?W(n).then(c,c):c()})})}H();we();E();function an(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,r,n)=>{n.json&&I(!0);let i=await C(e),s=r.join(" "),a=n.timeout?_t(n.timeout,"Timeout")*1e3:3e4;if(n.background){let c=St(),u="/root/.gibil-jobs",d=`${u}/${c}.log`,f=`${u}/${c}.exit`,g=`${u}/${c}.pid`,p=`${u}/${c}.sh`,v=["#!/bin/bash",`nohup bash -c '${s.replace(/'/g,"'\\''")}' > ${d} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${g}`,`(wait $BGPID 2>/dev/null; echo $? > ${f}) &`,"echo $BGPID"].join(`
|
|
29
|
+
`),w=Buffer.from(v).toString("base64"),b=`mkdir -p ${u} && echo '${w}' | base64 -d > ${p} && chmod +x ${p} && bash ${p}`,k;try{k=await x({instanceName:e,ip:i.ip,command:b,timeoutMs:1e4})}catch(y){throw be(y)&&await W(i)==="cleaned"&&process.exit(1),y}let _=parseInt(k.stdout.trim(),10);isNaN(_)&&(o.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await q({id:c,instance:e,command:s,pid:_,status:"running",startedAt:new Date().toISOString()}),n.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 x({instanceName:e,ip:i.ip,command:s,stream:!n.json,timeoutMs:a})}catch(c){throw be(c)&&await W(i)==="cleaned"&&process.exit(1),c}n.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)})}Ce();import{createInterface as Ci}from"readline";H();we();E();D();O();F();import{createHash as cn}from"crypto";import{readFile as dn,writeFile as vi,mkdir as yi}from"fs/promises";import{existsSync as Et,readFileSync as wi}from"fs";import{hostname as ln,userInfo as bi,platform as $i,arch as Si}from"os";import{join as Ke,dirname as mn}from"path";import{fork as xi}from"child_process";import{fileURLToPath as fn}from"url";function pn(){return Ke(P.root,"device_id")}function _i(){return Ke(P.root,"config.json")}var Pi=process.env.GIBIL_TELEMETRY_URL??"https://zopdxjruwktjyjunitrv.supabase.co/functions/v1/telemetry-ingest",ki="tk_alpha_09a93302e0f3e73417a9e9dbfc500a61",rt=null,Me=null;function Ii(){try{let t=bi(),e=`${ln()}:${t.username}:${t.homedir}`;return cn("sha256").update(e).digest("hex").slice(0,16)}catch{return cn("sha256").update(`${ln()}:${Date.now()}`).digest("hex").slice(0,16)}}async function gn(){if(rt)return rt;let t=pn();if(Et(t)){let r=(await dn(t,"utf-8")).trim();if(r.length>0)return rt=r,rt}let e=Ii();return await yi(P.root,{recursive:!0,mode:448}),await vi(t,e,{mode:384}),rt=e,e}async function ar(){if(Me!==null)return Me;let t=process.env.GIBIL_TELEMETRY;if(t!==void 0)return Me=!["0","false","off","no"].includes(t.toLowerCase()),Me;try{let e=_i();if(Et(e)&&JSON.parse(await dn(e,"utf-8")).telemetry===!1)return Me=!1,!1}catch{}return Me=!0,!0}async function hn(){return Et(pn())?!1:(await gn(),!0)}var Le=null;function Ei(){if(Le)return Le;let t=mn(fn(import.meta.url));for(let e of["../package.json","../../package.json"])try{return Le=JSON.parse(wi(Ke(t,e),"utf-8")).version??"0.0.0",Le}catch{}return Le="0.0.0",Le}async function nt(t){if(!await ar())return;let e=await gn();if(!e)return;let r={...t,device_id:e,cli_version:Ei(),timestamp:new Date().toISOString(),os:$i(),arch:Si(),node_version:process.version},n=mn(fn(import.meta.url)),s=[Ke(n,"telemetry-send.js"),Ke(n,"..","utils","telemetry-send.js"),Ke(n,"telemetry-send.ts")].find(a=>Et(a));if(s)try{xi(s,{detached:!0,stdio:"ignore",...s.endsWith(".ts")?{execArgv:["--import","tsx"]}:{},env:{...process.env,TELEMETRY_PAYLOAD:JSON.stringify(r),TELEMETRY_ENDPOINT:Pi,TELEMETRY_INGEST_KEY:ki}}).unref()}catch{}}var un=new Map,Ti=5e3;async function vn(t){let e=Date.now(),r=un.get(t)??0;e-r<Ti||(un.set(t,e),await nt({event:"mcp_tool",tool:t}))}function yn(t){return t.slice(3).filter(e=>e.startsWith("-")).map(e=>e.replace(/=.*$/,""))}async function Ai(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 r=Ci({input:process.stdin,output:process.stdout}),n=await new Promise(i=>r.question("> ",i));return r.close(),n.trim().toLowerCase()==="yes"}async function wn(t){let e=await wt(t),r=await U.forInstance(e);o.info(`Destroying instance "${t}" (server ${e.serverId})...`);try{await r.destroyServer(e.serverId)}catch(s){o.warn(`Could not delete server ${e.serverId}: ${s instanceof Error?s.message:String(s)}`)}try{await r.deleteSSHKey(e.sshKeyId)}catch(s){o.warn(`Could not delete SSH key ${e.sshKeyId}: ${s instanceof Error?s.message:String(s)}`)}await re(t),await tt(t),await Qr(t),await ge(t);let n=await K();await le(n,"destroy",t,{onTrackFailure: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 nt({event:"lifecycle",duration_minutes:i}),o.info(` ${R} ${T.destroySingle(t)}`)}function bn(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,r)=>{if(r.json&&I(!0),r.all){let n=await ee();if(n.length===0){r.json?o.json({destroyed:[],failed:[]}):o.info(T.noInstances);return}if(!r.yes&&(r.json&&(o.json({error:"confirmation required",message:`--all would destroy ${n.length} server(s). Re-run with --yes to confirm.`,instances:n.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 Ai(n))){o.info(""),o.info("Cancelled. No servers were destroyed.");return}o.info(`Destroying ${n.length} instance(s)...`);let i=3,s=[];for(let c=0;c<n.length;c+=i){let u=n.slice(c,c+i),d=await Promise.allSettled(u.map(f=>wn(f.name)));s.push(...d)}let a=[],l=[];for(let c=0;c<s.length;c++)if(s[c].status==="fulfilled")a.push(n[c].name);else{let u=s[c].reason;l.push(`${n[c].name}: ${u instanceof Error?u.message:String(u)}`)}r.json?o.json({destroyed:a,failed:l}):l.length===0?o.info(`
|
|
30
|
+
${T.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 wn(e),r.json&&o.json({destroyed:[e]})})}H();E();O();function $n(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 r=await ee();if(r.length===0){e.json?o.json({instances:[]}):o.info(T.noInstances);return}let n=r.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:n});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 n){let s=Sn(i.ttl_remaining),a=ji(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),g=i.status==="running"?te(u):oe(u),p=i.ttl_remaining<=300?oe(d):d;o.info(`${h(l)} ${m(c)} ${i.ip.padEnd(18)} ${g} ${p} ${m(f)}`)}o.info(`
|
|
32
|
+
${m(`${n.length} server(s)`)}`)})}function Sn(t){if(t<=0)return"expired";let e=Math.floor(t/60),r=Math.floor(e/60),n=Math.floor(r/24);if(n>=1){let i=r%24;return i>0?`${n}d ${i}h`:`${n}d`}return r>=1?`${r}h ${e%60}m`:`${e}m ${t%60}s`}function ji(t){let e=Date.now()-new Date(t).getTime(),r=Math.floor(e/1e3);return Sn(r)}H();E();function xn(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,r)=>{r.json&&I(!0);let n=await C(e),i=Re(r.ttl);try{await x({instanceName:e,ip:n.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 W(n)==="cleaned"&&process.exit(1),a}let s=new Date(Date.now()+i*6e4).toISOString();n.ttlMinutes=i,n.expiresAt=s,await pe(n),r.json?o.json({name:n.name,ttl_minutes:i,expires_at:s}):o.info(`\u2713 Extended "${e}" TTL to ${i} minutes (expires ${s})`)})}import{readFile as Ni}from"fs/promises";import{randomBytes as Ri}from"crypto";H();E();function _n(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,r)=>{r.json&&I(!0);let n=await C(e),i=await Ni(r.script,"utf-8");o.info(`Uploading and running script "${r.script}" on "${e}"...`);let s=Buffer.from(i).toString("base64"),a=`/tmp/gibil-script-${Ri(4).toString("hex")}.sh`,l;try{l=await x({instanceName:e,ip:n.ip,command:`echo '${s}' | base64 -d > ${a} && chmod +x ${a} && ${a}; EXIT=$?; rm -f ${a}; exit $EXIT`,stream:!r.json})}catch(c){throw be(c)&&await W(n)==="cleaned"&&process.exit(1),c}r.json?o.json({instance:e,script:r.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)})}D();E();O();import{createInterface as Oi}from"readline";function Pn(t){let e=Oi({input:process.stdin,output:process.stderr});return new Promise(r=>{e.question(t,n=>{e.close(),r(n.trim())})})}function kn(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 r=>{r.json&&I(!0);let n=r.key??process.env.GIBIL_API_KEY;n||(n=await Pn("Enter your API key: ")),n||(o.error("No API key provided."),process.exit(1)),n.startsWith("pk_")||(o.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),o.info("Verifying API key...");try{let i=await ae(n);await zt(n),r.json?o.json({authenticated:!0,email:i.user.email,plan:i.user.plan}):(o.info(T.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 r=>{let n=r.token;n||(n=await Pn("Enter your Hetzner API token: ")),n||(o.error("No token provided."),process.exit(1));try{let s=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${n}`}})).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 Ut(n),o.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await Ft(),o.info(T.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async r=>{r.json&&I(!0);let n=await K();if(!n){r.json?o.json({authenticated:!1}):o.info(`Not logged in. Run ${h("gibil auth login")} to authenticate.`);return}try{let i=await ae(n);r.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{r.json?o.json({authenticated:!1,error:"Key verification failed"}):o.error(`Stored API key is invalid. Run ${h("gibil auth login")} to re-authenticate.`)}})}D();E();function In(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 r=await K();r||(o.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let n=await Vt(r);e.json?o.json(n):(o.info("Plan: alpha (free)"),o.info(`VM hours used: ${n.vm_hours_used.toFixed(1)}h`),o.info(`Active instances: ${n.active_instances}`))}catch(n){o.error(n instanceof Error?n.message:String(n)),process.exit(1)}})}import{McpServer as Ki}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Di}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as S}from"zod";Ce();import{execSync as Hi}from"child_process";import{existsSync as Y}from"fs";E();O();D();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 lr(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 cr(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function Li(){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 En(t,e,r){let n=cr(e),i=Date.now(),s=o.spin(`Forging "${n}" for branch ${h(e)}...`),a=await kt(t,n,{repo:r.repo,ttlMinutes:r.ttlMinutes,config:r.config,providerName:r.providerName,serverType:r.serverType,location:r.location,agent:r.agent,verbose:r.verbose}),l=o.spin(`Checking out ${h(e)}...`);if((await x({instanceName:n,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 x({instanceName:n,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(!(!r.noTasks&&r.config?.tasks&&r.config.tasks.length>0)){let d=Li();if(d){let f=o.spin(`Installing deps (${d})...`),g=await x({instanceName:n,ip:a.ip,command:`cd /root/project && ${d}`,timeoutMs:3e5});g.exitCode!==0?(f.fail("Dep install failed"),g.stderr&&o.info(m(g.stderr.trim().slice(-500)))):f.succeed("Deps installed")}}if(r.run)if(r.port&&r.port.length>0)o.info(`Starting: ${h(r.run)} (background)`),await x({instanceName:n,ip:a.ip,command:`cd /root/project && nohup ${r.run} > /tmp/gibil-run.log 2>&1 &`,timeoutMs:3e4}),await new Promise(d=>setTimeout(d,3e3));else{o.info(""),o.info(`Running: ${h(r.run)}`);let d=await x({instanceName:n,ip:a.ip,command:`cd /root/project && ${r.run}`,stream:!r.json,timeoutMs:3e5});r.json&&o.info(d.stdout),d.exitCode!==0&&o.info(m(`Exit code: ${d.exitCode}`))}if(r.port&&r.port.length>0){let d=await en(a,r.port);o.info("");for(let f of d)o.info(` ${h(`http://localhost:${f}`)} \u2192 ${n}:${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(T.createReady(n,u)),r.json?console.log(JSON.stringify({name:n,branch:e,ip:a.ip,ttl_minutes:r.ttlMinutes,ssh:`gibil ssh ${n}`})):(o.info(""),o.info(ct(`${e}`,[`Server: ${n}`,`Branch: ${e}`,`IP: ${a.ip}`,`TTL: ${r.ttlMinutes} minutes`,"",`SSH: gibil ssh ${n}`,`Test: gibil run ${n} "pnpm test"`,`Done: gibil destroy ${n}`]))),a}function Tn(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,r)=>{r.json&&I(!0);for(let d of e)lr(d);let n=Re(r.ttl),i=r.repo??Mi(),s=null;if(s=await je(i)??await Ne(process.cwd()),!r.agent){let d=await We();d&&(r.agent=d)}if(r.agent){if(!M.includes(r.agent))throw new Error(`Unknown agent "${r.agent}". Supported: ${M.join(", ")}`);if(!Ae[r.agent]?.some(f=>s?.env?.[f])){let f=Ae[r.agent]?.join(" or ")??"";o.warn(`${r.agent} needs ${f}. SSH in and export it (recommended) or pass with --env.`)}}if(r.dryRun){for(let d of e){let f=cr(d),g=r.serverType??s?.server_type??"cax11",p=r.location??s?.location??"nbg1",v=s?.image??"node:20",w=he({repo:i,config:s??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:r.agent}),b={name:f,serverType:g,location:p,image:v,ttlMinutes:n,repo:i,agent:r.agent,cloudInitScript:w};r.json?o.json(b):(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:")} ${g}`),o.info(` ${m("Location:")} ${p}`),o.info(` ${m("Image:")} ${v}`),o.info(` ${m("TTL:")} ${n} minutes`),o.info(` ${m("Repo:")} ${i}`),r.agent&&o.info(` ${m("Agent:")} ${r.agent}`),o.info(""),o.info("Cloud-init script:"),o.info("\u2500".repeat(17)),o.info(w))}return}let a=await K();if(a){let d=await ae(a);o.info(`Authenticated as ${d.user.email} (${d.user.plan})`)}let l=r.provider??"hetzner",c=await U.get(l),u=r.serverType;if(!u&&r.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Qe(),Xe));if(!d(r.size))throw new Error(`Unknown size "${r.size}". Valid sizes: small, medium, large.`);u=f(c,r.size)}if(e.length===1){let d=await En(c,e[0],{repo:i,ttlMinutes:n,config:s,run:r.run,json:r.json,noTasks:r.noTasks,providerName:l,serverType:u,location:r.location,agent:r.agent,port:r.port,verbose:r.verbose});await le(a,"create",d.name)}else{o.info(`Forging ${h(String(e.length))} branches in parallel...`),o.info("");let d=await Promise.allSettled(e.map(p=>En(c,p,{repo:i,ttlMinutes:n,config:s,run:r.run,json:r.json,noTasks:r.noTasks,providerName:l,serverType:u,location:r.location,agent:r.agent,port:r.port,verbose:r.verbose}))),f=d.filter(p=>p.status==="fulfilled"),g=d.filter(p=>p.status==="rejected");if(!r.json){if(o.info(""),o.info(`${f.length}/${e.length} branches ready.`),g.length>0)for(let p=0;p<d.length;p++){let v=d[p];v.status==="rejected"&&o.error(` ${e[p]}: ${v.reason instanceof Error?v.reason.message:String(v.reason)}`)}o.info(""),o.info(m(`Destroy all: gibil destroy ${e.map(cr).join(" ")}`))}for(let p of d)p.status==="fulfilled"&&await le(a,"create",p.value.name);g.length>0&&process.exit(1)}})}function Cn(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}Ce();H();F();import{execSync as Tt}from"child_process";import{readFileSync as zi}from"fs";we();H();we();E();async function ur(t){let e=await ue(t);if(e.status!=="running")return{status:e.status,exitCode:e.exitCode};let r=await C(e.instance),n="/root/.gibil-jobs",i=`${n}/${t}.exit`,s=`${n}/${t}.log`,l=(await x({instanceName:e.instance,ip:r.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING"){try{if((await x({instanceName:e.instance,ip:r.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 q(e);let w=Math.round((v.getTime()-new Date(e.startedAt).getTime())/1e3),b;try{b=(await x({instanceName:e.instance,ip:r.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:w,stdout:b}}}catch{}return{status:"running"}}let c=parseInt(l,10),u=await x({instanceName:e.instance,ip:r.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4}),d=c===0?"done":"failed",f=new Date,g=Math.round((f.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=d,e.exitCode=c,e.completedAt=f.toISOString(),await q(e),{status:d,exitCode:c,stdout:u.stdout,durationS:g}}function An(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(r,n)=>{n.json&&I(!0);let i=await ue(r),s=await ur(r);n.json?o.json({job_id:r,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 ${r} is still running on "${i.instance}"`),o.info(` Command: ${i.command}`),o.info(` Started: ${i.startedAt}`)):s.status==="orphaned"?(o.warn(`Job ${r} 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 ${r}: ${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 r=>{r.json&&I(!0);let n=await He(),i=await ee(),s=new Set(i.map(a=>a.name));for(let a of n)a.status==="running"&&!s.has(a.instance)&&(a.status="orphaned",a.completedAt=new Date().toISOString(),await q(a));if(n.length===0){r.json?o.json([]):o.info("No background jobs.");return}if(r.json)o.json(n.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 n){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(r,n)=>{n.json&&I(!0);let i=await ue(r);if(i.status!=="running"){n.json?o.json({job_id:r,status:i.status,message:"Job is not running"}):o.info(`Job ${r} 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 q(i),n.json?o.json({job_id:r,status:"cancelled"}):o.info(`Job ${r} 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(r,n)=>{n.json&&I(!0);let i=await ue(r),s=await C(i.instance),a=`/root/.gibil-jobs/${r}.log`,l=n.follow?`tail -f ${a}`:`cat ${a} 2>/dev/null || echo '(no output yet)'`,c=n.follow?3e5:1e4,u=await x({instanceName:i.instance,ip:s.ip,command:l,stream:!n.json,timeoutMs:c});n.json&&o.json({job_id:r,stdout:u.stdout})})}function z(t,e,r){let n=`[${t}] ${e}`;return r&&(n+=`
|
|
33
33
|
|
|
34
|
-
Suggestion: ${
|
|
35
|
-
${(await
|
|
36
|
-
`),$=Buffer.from(y).toString("base64"),
|
|
37
|
-
`)||"(no output)"}],isError:f.exitCode!==0}}),
|
|
38
|
-
`))if(e.startsWith("ip=")){let
|
|
34
|
+
Suggestion: ${r}`),n}function ot(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 Ct="key::";function Fi(){try{let t=Tt("git config user.name",{encoding:"utf-8"}).trim(),e=Tt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let r;try{if(Tt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=Tt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{r=zi(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(Ct))&&(r=i.startsWith(Ct)?i.slice(Ct.length):i)}}}catch{}return{name:t,email:e,signingKey:r}}catch{return}}async function De(t,e){if(t)return t;if(e)return C(e);let n=(await ee()).filter(i=>new Date<new Date(i.expiresAt));if(n.length===0)throw new Error("No active servers. Use create_server first.");if(n.length===1)return n[0];throw new Error(`Multiple servers running: ${n.map(i=>i.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function $e(t,e,r=3e4){return x({instanceName:t.name,ip:t.ip,command:e,stream:!1,timeoutMs:r})}async function dr(t,e,{errorPrefix:r,suggestion:n,shapeOutput:i,timeoutMs:s}){let a=await $e(t,e,s);return a.exitCode!==0?{content:[{type:"text",text:z("command_failed",`${r}: ${a.stderr}`,n)}],isError:!0}:{content:[{type:"text",text:i?i(a.stdout):a.stdout}]}}function J(t){return`'${t.replace(/'/g,"'\\''")}'`}function Gi(t){let e=t.trim(),r=e.split(",");if(r.length<5)throw new Error(`Unexpected vm_stats output (expected 5 comma-separated sections, got ${r.length}): ${e}`);let n=parseInt(r[0],10);if(isNaN(n))throw new Error(`Failed to parse CPU cores: ${r[0]}`);let i=r[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: ${r[1]}`);let c=r[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: ${r[2]}`);let g=r[3].trim().split(/\s+/),p=_=>Math.round(parseFloat(_.replace(/G$/i,""))),v=p(g[0]),w=p(g[1]),b=p(g[2]);if(isNaN(v)||isNaN(w)||isNaN(b))throw new Error(`Failed to parse disk: ${r[3]}`);let k=parseInt(r[4].trim(),10);if(isNaN(k))throw new Error(`Failed to parse uptime: ${r[4]}`);return{cpu:{cores:n,load_1m:s,load_5m:a,load_15m:l},memory:{total_mb:u,used_mb:d,available_mb:f},disk:{total_gb:v,used_gb:w,available_gb:b},uptime_seconds:k}}async function jn(t){let e=null;if(t&&(e=await C(t),e.gitIdentity)){let{name:l,email:c,signingKey:u}=e.gitIdentity,d=[`git config --global user.name ${J(l)}`,`git config --global user.email ${J(c)}`];u&&d.push("git config --global gpg.format ssh",`git config --global user.signingkey ${J(Ct+u)}`,"git config --global commit.gpgsign true"),$e(e,d.join(" && ")).catch(()=>{})}let r=t?`gibil-${t}`:"gibil",n=new Ki({name:r,version:"0.4.0"}),i=n.tool.bind(n);n.tool=((...l)=>{let c=l[0],u=l.length-1;if(typeof l[u]=="function"){let d=l[u];l[u]=async(...f)=>(vn(c).catch(()=>{}),d(...f))}return i(...l)}),e||(n.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. Optionally checks out a branch and installs a coding agent (claude, aider, codex) so the server can run agent tasks directly. 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"),branch:S.string().optional().describe("Git branch to check out after the repo is cloned. Requires 'repo'."),agent:S.string().optional().describe(`Coding agent to install on the server. One of: ${M.join(", ")}. Forward its API key via 'env' (e.g. ANTHROPIC_API_KEY for claude, OPENAI_API_KEY for codex).`),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:l,repo:c,branch:u,agent:d,ttl:f,provider:g,size:p,server_type:v,location:w,env:b})=>{let k=null,_=null,y=null,$=null;try{if(y=l??Ze(),l&&xt(l),u&&lr(u),u&&!c)return{content:[{type:"text",text:z("validation","'branch' requires 'repo'","Pass a repo URL so there's something to check the branch out from.")}],isError:!0};if(d&&!M.includes(d))return{content:[{type:"text",text:z("validation",`Unknown agent "${d}"`,`Supported agents: ${M.join(", ")}`)}],isError:!0};let{getDefaultProvider:j}=await Promise.resolve().then(()=>(D(),Ye)),Se=g??await j();$=await U.get(Se);let Z=v;if(!Z&&p){let{resolveSize:Fe}=await Promise.resolve().then(()=>(Qe(),Xe));Z=Fe($,p)}let Jn=await yt(y),jt=await $.createSSHKey(`gibil-${y}-${ve(4)}`,Jn.publicKey);k=jt;let fr=Fi(),me=c?await je(c):null;b&&Object.keys(b).length>0&&(me||(me={}),me.env={...me.env,...b});let Vn=(me?.services?.length??0)>0,xe=f??(Vn?120:60);if(xe<1||xe>V)return{content:[{type:"text",text:z("validation",`TTL must be between 1 and ${V} minutes (1 year)`,`Pass a ttl value between 1 and ${V}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let qn=he({repo:c,config:me??void 0,ttlMinutes:xe,githubToken:process.env.GITHUB_TOKEN,gitIdentity:fr,agent:d}),Nt=await $.createServer(y,jt.id,qn,Z,w);_=Nt.id;let fe=(await $.waitForReady(Nt.id)).ipv4,pr=new Date,Wn={name:y,serverId:Nt.id,ip:fe,sshKeyId:jt.id,keyPath:P.privateKey(y),status:"running",createdAt:pr.toISOString(),ttlMinutes:xe,expiresAt:new Date(pr.getTime()+xe*6e4).toISOString(),repo:c,gitIdentity:fr,provider:Se};await pe(Wn),await $t(y,fe);let Rt="ready";if(c||me){let st=Date.now(),gr=!1;for(;Date.now()-st<36e4;){try{if((await x({instanceName:y,ip:fe,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){gr=!0;break}}catch{}await new Promise(Ot=>setTimeout(Ot,5e3))}if(!gr){Rt="timeout";try{Rt=`timeout \u2014 cloud-init log:
|
|
35
|
+
${(await x({instanceName:y,ip:fe,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'no log'",timeoutMs:1e4})).stdout}`}catch{}}}let it;if(u){let Fe=u.replace(/'/g,"'\\''");try{if((await x({instanceName:y,ip:fe,command:"cd /root/project && git rev-parse --abbrev-ref HEAD",timeoutMs:1e4})).stdout.trim()===u)it=`already on ${u}`;else{let st=await x({instanceName:y,ip:fe,command:`cd /root/project && git fetch origin '${Fe}' && git checkout '${Fe}'`,timeoutMs:6e4});it=st.exitCode===0?`checked out ${u}`:`checkout failed: ${st.stderr.trim()||"unknown error"}`}}catch(Ge){it=`checkout failed: ${Ge instanceof Error?Ge.message:String(Ge)}`}}return{content:[{type:"text",text:JSON.stringify({name:y,ip:fe,ttl_minutes:xe,status:"running",provisioning:Rt,...u?{branch:u,checkout:it}:{},...d?{agent:d}:{},working_directory:c?"/root/project":"/root",hint:d?`Server ready with ${d} installed. Run a task with vm_bash, e.g.: vm_bash({ command: "cd /root/project && ${d} -p 'fix the failing test'", background: true })`: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(j){$&&_&&await $.destroyServer(_).catch(()=>{}),$&&k&&await $.deleteSSHKey(k.id).catch(()=>{}),y&&(await re(y).catch(()=>{}),await ge(y).catch(()=>{}));let Se=j instanceof Error?j.message:String(j),Z=ot(j);return{content:[{type:"text",text:z(Z,`Failed to create server: ${Se}`,Z==="provider_error"?"Check your HETZNER_API_TOKEN and plan limits":"Verify parameters and try again")}],isError:!0}}}),n.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:l})=>{try{let c=await wt(l),u=await U.forInstance(c);await u.destroyServer(c.serverId).catch(()=>{}),await u.deleteSSHKey(c.sshKeyId).catch(()=>{}),await re(l).catch(()=>{});let{deleteJobsByInstance:d}=await Promise.resolve().then(()=>(we(),on));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=ot(c);return{content:[{type:"text",text:z(d,`Failed to destroy server "${l}": ${u}`,d==="instance_not_found"?"Check server name with list_servers":"Check your HETZNER_API_TOKEN")}],isError:!0}}}),n.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let l=await ee();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)}]}}),n.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:l,ttl:c})=>{try{if(c<1||c>V)return{content:[{type:"text",text:z("validation",`TTL must be between 1 and ${V} minutes (1 year)`,`Pass a ttl value between 1 and ${V}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let u=Math.floor(c),d=await C(l),f=await $e(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:z("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=ot(u);return{content:[{type:"text",text:z(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=S.string().optional().describe("Server name (auto-selects if only one is running)");n.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 l=>{let c=await De(e,l.server),u=l.working_dir??"/root/project",d=`cd ${J(u)} 2>/dev/null || cd /root && ${l.command}`;if(l.background){let p=St(),v="/root/.gibil-jobs",w=`${v}/${p}.log`,b=`${v}/${p}.exit`,k=`${v}/${p}.pid`,_=`${v}/${p}.sh`,y=["#!/bin/bash",`nohup bash -c '${d.replace(/'/g,"'\\''")}' > ${w} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${k}`,`(wait $BGPID 2>/dev/null; echo $? > ${b}) &`,"echo $BGPID"].join(`
|
|
36
|
+
`),$=Buffer.from(y).toString("base64"),j=`mkdir -p ${v} && echo '${$}' | base64 -d > ${_} && chmod +x ${_} && bash ${_}`,Se=await $e(c,j,1e4),Z=parseInt(Se.stdout.trim(),10);return isNaN(Z)?{content:[{type:"text",text:z("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 q({id:p,instance:c.name,command:l.command,pid:Z,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:p,instance:c.name,status:"running",pid:Z,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let f=await $e(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}}),n.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 l=>{try{let c=await ue(l.job_id),u=await ur(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=ot(c);return{content:[{type:"text",text:z(d,u,"Check job_id is correct \u2014 use vm_job_list to see all jobs")}],isError:!0}}}),n.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 He()).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)}]}}),n.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 He(),c=await ee(),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 q(f),d.push(f.id));return{content:[{type:"text",text:JSON.stringify({swept_count:d.length,swept_job_ids:d},null,2)}]}}),n.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 l=>{let c=await De(e,l.server),u=J(l.path),d=`cat -n ${u}`;return 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`),dr(c,d,{errorPrefix:`Failed to read ${l.path}`,suggestion:"Check the file path exists on the server \u2014 use vm_ls to browse"})}),n.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 l=>{let c=await De(e,l.server),u=Buffer.from(l.content).toString("base64"),d=J(l.path),f=`mkdir -p "$(dirname ${d})" && echo '${u}' | base64 -d > ${d}`;return dr(c,f,{errorPrefix:`Failed to write ${l.path}`,suggestion:"Check the path is valid and the disk is not full",shapeOutput:()=>`Wrote ${l.path}`})}),n.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 l=>{let c=await De(e,l.server),u=l.path??"/root/project",d;return l.glob?d=`cd ${J(u)} && find . -path ${J("./"+l.glob)} -type f 2>/dev/null | sort | head -200`:d=`ls -la ${J(u)}`,dr(c,d,{errorPrefix:`Failed to list ${u}`,suggestion:"Check the directory path exists on the server"})}),n.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 l=>{let c=await De(e,l.server),u=l.path??"/root/project",d=J(l.pattern),f=J(u),g;if(l.include){let w=J(l.include);g=`cd ${f} && (rg -n --glob ${w} ${d} 2>/dev/null || grep -rn --include=${w} ${d} .) | head -100`}else g=`cd ${f} && (rg -n ${d} 2>/dev/null || grep -rn ${d} .) | head -100`;return{content:[{type:"text",text:(await $e(c,g)).stdout||"(no matches)"}]}}),n.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 De(e,l.server),d=await $e(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:z("command_failed",`Failed to collect stats: ${d.stderr}`,"Check the server is accessible with vm_bash")}],isError:!0};let f=Gi(d.stdout);return{content:[{type:"text",text:JSON.stringify(f,null,2)}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=ot(c);return{content:[{type:"text",text:z(d,`Failed to get stats: ${u}`,"Check the server is accessible with vm_bash")}],isError:!0}}});let a=new Di;await n.connect(a)}E();F();function Nn(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,r)=>{if(r.printConfig){let n={mcpServers:{gibil:mt()}};console.log(JSON.stringify(n,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 jn(e)}catch(n){o.error(n instanceof Error?n.message:String(n)),process.exit(1)}})}D();E();F();O();import{createInterface as Wi}from"readline";import{existsSync as Yi,readFileSync as Zi,writeFileSync as Xi}from"fs";import{join as Ln}from"path";import{homedir as Qi}from"os";ht();E();var Rn="https://1.1.1.1/cdn-cgi/trace";function Ui(){let t=process.env.GIBIL_SKIP_IP_DETECTION?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}async function On(t){if(Ui())return o.debug("Public IP detection skipped (GIBIL_SKIP_IP_DETECTION set)"),null;o.debug(`Detecting public IP via ${Rn}`);let e=new AbortController,r=setTimeout(()=>e.abort(),t.timeoutMs);try{let n=await fetch(Rn,{signal:e.signal});if(!n.ok)return null;let i=await n.text();return Bi(i)}catch{return null}finally{clearTimeout(r)}}function Bi(t){for(let e of t.split(`
|
|
38
|
+
`))if(e.startsWith("ip=")){let r=e.slice(3).trim();return r.length>0?r:null}return null}var Ji="https://console.vultr.com/user/apiaccess/",Vi="https://whatismyip.com";function Hn(t){let e=t.program?.console_url??Ji;if(t.hasAccount)return[{heading:"Vultr API Key",body:["Get your personal access token from the API access page."],link:e,gate:!1}];let r=[];return t.program&&r.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}),r.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}),r.push({heading:"Step 2 of 3 \u2014 Whitelist your IP",body:qi(t.detectedIp),gate:!0}),r.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}),r}function qi(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 ${Vi}`),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 ze(t){let e=Wi({input:process.stdin,output:process.stderr});return new Promise(r=>{e.question(t,n=>{e.close(),r(n.trim())})})}var de=["hetzner","vultr"];async function es(){let t=[];for(let r of de)await Ee(r)&&t.push(r);let e=!!await K();return{providers:t,apiKey:e}}function Mn(t){return t==="hetzner"?"Hetzner":"Vultr"}async function ts(){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 ze(" Provider [hetzner]: ")).toLowerCase();return t==="vultr"?"vultr":(t===""||t==="hetzner"||o.warn(`Unknown provider "${t}". Defaulting to hetzner.`),"hetzner")}async function mr(t,e){let r=e;if(!r&&(t==="vultr"?r=await rs():(o.info(""),o.info(h("Hetzner API Token")),o.info(m(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),o.info(""),r=await ze(" Hetzner API token: ")),!r)){let i=t==="hetzner"?"Hetzner API token":"Vultr API key";o.error(`No ${i.toLowerCase()} provided.`),process.exit(1)}let n=o.spin(`Verifying ${t} token...`);try{await Kn(t,r),n.succeed(`${t} token verified`)}catch(i){n.fail(i instanceof Error?i.message:String(i)),process.exit(1)}return await qe(t,r),r}async function rs(){o.info(""),o.info(h("Vultr API Key")),o.info("");let t=(await ze(" Do you have a Vultr account? [Y/n]: ")).toLowerCase().trim(),e=t!=="n"&&t!=="no",r=null;e||(r=await On({timeoutMs:2e3}));let n=gt("vultr"),i=Hn({hasAccount:e,detectedIp:r,program:n});for(let s of i)await ns(s);return o.info(""),ze(" Vultr API key: ")}async function ns(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 ze(m(" (press Enter when done) "))}async function Kn(t,e){if(t==="hetzner"){let n=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${e}`}})).json();if(n.error)throw new Error(`Invalid Hetzner token: ${n.error.message}`)}else if(t==="vultr"){let r=await fetch("https://api.vultr.com/v2/account",{headers:{Authorization:`Bearer ${e}`}});if(!r.ok){let n=await r.text(),i=`Invalid Vultr API key (${r.status}): ${n}`;throw r.status===401||r.status===403?new Error(`${i}
|
|
39
39
|
|
|
40
40
|
Most likely cause: your IP isn't whitelisted under "Access Control".
|
|
41
|
-
Open https://console.vultr.com/user/apiaccess/ \u2192 Access Control \u2192 add your IP.`):new Error(i)}}}function
|
|
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: ${M.join(", ")}`)),o.info(m(" Press Enter to skip \u2014 you can always use --agent later.")),o.info("");let l=(await
|
|
43
|
-
${
|
|
41
|
+
Open https://console.vultr.com/user/apiaccess/ \u2192 Access Control \u2192 add your IP.`):new Error(i)}}}function Dn(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 Kn(c,e.token)}catch(d){u.fail(d instanceof Error?d.message:String(d)),process.exit(1)}u.succeed(`${c} token verified`),await qe(c,e.token),e.setDefault&&(await Te(c),o.info(`${R} ${c} is now the default provider.`)),o.info(`${R} ${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 mr(c,e.token),e.setDefault&&(await Te(c),o.info(`${R} ${c} is now the default provider.`)),o.info(`${R} ${c} added. Try: gibil create --provider ${c}`);return}console.error(yr);let r=await es();if(r.providers.length>0&&!e.force){o.info(`${R} Already configured.`);for(let u of r.providers)o.detail(Mn(u),te("connected"));o.detail("Gibil API",r.apiKey?te("connected"):m("not configured (optional)")),o.info(""),o.info(` Run ${h("gibil init --force")} to reconfigure.`);let c=de.filter(u=>!r.providers.includes(u));c.length>0&&o.info(` Run ${h(`gibil init --add ${c[0]}`)} to add ${Mn(c[0])}.`),o.info(` Run ${h("gibil create")} to forge a server.`);return}let n=await ts(),i;if(n==="vultr"?(await mr("vultr"),await Te("vultr")):(i=await mr("hetzner"),await Te("hetzner")),n==="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 g of f)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}`}}),u=g.type,d=g.location;break}}catch{}await Bt(u,d),c.succeed(`Default server type: ${u} (${d})`)}let s=o.spin("Configuring MCP for Claude Code...");try{let c=Ln(Qi(),".claude.json"),u={};try{u=JSON.parse(Zi(c,"utf-8"))}catch{}u.mcpServers||(u.mcpServers={}),u.mcpServers.gibil=mt(),Xi(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: ${M.join(", ")}`)),o.info(m(" Press Enter to skip \u2014 you can always use --agent later.")),o.info("");let l=(await ze(" Default agent [none]: ")).toLowerCase().trim();l&&M.includes(l)?(await pt(l),o.info(` ${R} Default agent: ${te(l)}`)):l?o.info(m(` Unknown agent "${l}", skipping. Use --agent with: ${M.join(", ")}`)):(await pt(null),o.info(m(" No default agent. Use --agent claude (or aider, codex) when creating servers."))),o.info(""),o.info(T.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 zn(){if(process.env.HETZNER_API_TOKEN||process.env.VULTR_API_KEY)return!1;let t=Ln(P.root,"config.json");return!Yi(t)}H();import{spawn as os}from"child_process";E();O();function Fn(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 is(t){let{local:e,host:r,remote:n}=Fn(t);return et(`${e}:${r}:${n}`),`${e}:${r}:${n}`}function Gn(t){t.command("forward <name> <ports...>").description("Forward local ports to a running ephemeral machine via SSH").action(async(e,r)=>{let n=await C(e),i=r.map(c=>({spec:c,mapping:is(c),...Fn(c)})),s=await Oe(n);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",n.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@${n.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(""),os("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..."),W(n).then(u,u)):(o.info(" Tunnel closed."),u())})})}Ve();D();E();O();ht();async function ss(){let t=await Gt(),e=await Promise.all(Object.values(Dt).map(async r=>{let n=await Ee(r.name);return{name:r.name,label:r.label,defaultRegion:r.defaultRegion,configured:n!==null,sizes:r.sizes}}));return{default:t,providers:e}}function At(t,e){return t.length>=e?t:t+" ".repeat(e-t.length)}function as(t){for(let e of t.providers){let r=e.name===t.default,n=e.configured?te("configured"):m("not configured"),i=r?h(" (default)"):"";if(o.info(""),o.info(`${h(e.label)}${i} ${m("\xB7")} region ${e.defaultRegion} ${m("\xB7")} ${n}`),!e.configured){o.info(m(` Run: gibil init --add ${e.name}`));let s=Yt(e.name,e.configured);s&&o.info(m(` ${s}`))}for(let s of e.sizes){let a=At(s.name,8),l=At(`${s.vcpu} vCPU`,7),c=At(`${s.ramGb} GB`,6),u=At(`${s.diskGb} GB SSD`,11);o.info(` ${a} ${l} ${c} ${u} ${m("\u2192")} ${s.nativeType}`)}}o.info("")}function Un(t){t.command("providers").description("List supported providers, regions, and sizes").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let r=await ss();e.json?o.json(r):as(r)})}E();O();try{await import("dotenv/config")}catch{}var fs=ds(us(import.meta.url)),Bn={version:"0.0.0"};for(let t of["../package.json","../../package.json"])try{Bn=JSON.parse(ls(ms(fs,t),"utf-8"));break}catch{}var A=new cs;A.name("gibil").description("Your own machine, on demand. Forge, use, burn.").version(`${Bn.version} ${at}`,"-v, --version").addHelpText("before",`
|
|
43
|
+
${wr}
|
|
44
44
|
`).addHelpText("after",`
|
|
45
45
|
${m("Docs:")} https://gibil.dev/docs
|
|
46
|
-
`);
|
|
46
|
+
`);Dn(A);Zr(A);sn(A);an(A);bn(A);$n(A);xn(A);_n(A);kn(A);In(A);An(A);Nn(A);Tn(A);Gn(A);Un(A);async function ps(){Cn();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 zn()&&(o.info(""),o.info(T.setupNeeded),o.info(""),process.exit(1)),await hn()&&await ar()&&o.info(m("gibil collects anonymous usage stats to improve the CLI. To disable: GIBIL_TELEMETRY=0"));let n=t[0]??"help",i=yn(process.argv),s=Date.now(),a=0;try{await A.parseAsync(process.argv)}catch(c){a=1,c instanceof Error&&o.error(c.message)}["auth","config","--help","-h","--version","-v","help"].includes(n)||await nt({event:"command",command:n,flags:i,exit_code:a,duration_ms:Date.now()-s}),a!==0&&process.exit(1)}ps();
|