gibil 0.4.1 → 0.5.0

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