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