gibil 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/index.js +23 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <p align="center"><strong>Full servers. Forged in seconds. Gone when done.</strong></p>
8
8
 
9
9
  <p align="center">
10
- <img src="https://img.shields.io/badge/version-0.1.9-blue" alt="Version 0.1.9" />
10
+ <img src="https://img.shields.io/badge/version-0.1.11-blue" alt="Version 0.1.11" />
11
11
  <img src="https://img.shields.io/badge/tests-128%20passing-brightgreen" alt="Tests: 128 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" />
@@ -27,6 +27,16 @@ You're running Claude Code, Cursor, or custom agents — and they're all competi
27
27
 
28
28
  Your agents need their own machines. That's gibil.
29
29
 
30
+ ## 30-Second Demo
31
+
32
+ ```bash
33
+ npm install -g gibil
34
+ gibil init # enter your Hetzner API token
35
+ gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10
36
+ gibil run demo "npm test" # 32/32 tests pass on a fresh server
37
+ gibil destroy demo
38
+ ```
39
+
30
40
  ## Install
31
41
 
32
42
  ```bash
@@ -67,7 +77,7 @@ gibil destroy my-app
67
77
  | `gibil run <name> <cmd>` | Execute a command remotely (`--background` for async) |
68
78
  | `gibil job <cmd>` | Manage background jobs (status, list, cancel, logs) |
69
79
  | `gibil exec <name>` | Upload and run a local script |
70
- | `gibil mcp [name]` | Start MCP server for AI agents |
80
+ | `gibil mcp [name]` | Start MCP server for AI agents (`--print-config` for setup) |
71
81
  | `gibil list` | List all active servers |
72
82
  | `gibil extend <name>` | Extend a server's TTL |
73
83
  | `gibil destroy [name]` | Burn down a server |
package/dist/index.js CHANGED
@@ -1,27 +1,30 @@
1
1
  #!/usr/bin/env node
2
- var Re=Object.defineProperty;var Ct=(e,t)=>()=>(e&&(t=e(e=0)),t);var Bt=(e,t)=>{for(var n in t)Re(e,n,{get:t[n],enumerable:!0})};import{homedir as Ge}from"os";import{join as L}from"path";var D,S,M=Ct(()=>{"use strict";D=L(Ge(),".gibil"),S={root:D,instances:L(D,"instances"),keys:L(D,"keys"),jobs:L(D,"jobs"),instanceFile:e=>L(D,"instances",`${e}.json`),keyDir:e=>L(D,"keys",e),privateKey:e=>L(D,"keys",e,"id_ed25519"),publicKey:e=>L(D,"keys",e,"id_ed25519.pub")}});var Mt={};Bt(Mt,{clearApiKey:()=>Tt,fetchUsage:()=>Ht,getApiKey:()=>P,getApiUrl:()=>qe,getApiUrlFromConfig:()=>pt,getHetznerToken:()=>Ot,getServerDefaults:()=>We,saveApiKey:()=>ot,saveHetznerToken:()=>rt,saveServerDefaults:()=>Nt,trackUsage:()=>X,verifyApiKey:()=>F});import{readFile as Fe,writeFile as Je,mkdir as ze}from"fs/promises";import{existsSync as Ue}from"fs";import{join as Be}from"path";async function G(){if(!Ue(At))return{};let e=await Fe(At,"utf-8");return JSON.parse(e)}async function dt(e){await ze(S.root,{recursive:!0,mode:448}),await Je(At,JSON.stringify(e,null,2),{mode:384})}async function ot(e){let t=await G();t.api_key=e,await dt(t)}async function P(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await G()).api_key??null}async function Tt(){let e=await G();delete e.api_key,await dt(e)}function qe(){return process.env.GIBIL_API_URL??Qt}async function pt(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await G()).api_url??Qt}async function rt(e){let t=await G();t.hetzner_token=e,await dt(t)}async function Ot(){return process.env.HETZNER_API_TOKEN?process.env.HETZNER_API_TOKEN:(await G()).hetzner_token??null}async function Nt(e,t){let n=await G();n.default_server_type=e,n.default_location=t,await dt(n)}async function We(){let e=await G();return{serverType:e.default_server_type??"cax11",location:e.default_location??"fsn1"}}async function F(e){let t=await pt(),n=await fetch(`${t}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:e})});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 X(e,t,n,r){let o=await pt(),c=await fetch(`${o}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:e,event:t,instance_name:n,server_type:r})});if(c.status===429)throw new Error("Plan limit reached. Upgrade at https://gibil.dev/pricing");if(!c.ok){let i=await c.text();throw new Error(`Usage tracking failed (${c.status}): ${i}`)}}async function Ht(e){let t=await pt(),n=await fetch(`${t}/usage-get`,{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let r=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${r}`)}return await n.json()}var At,Qt,J=Ct(()=>{"use strict";M();At=Be(S.root,"config.json"),Qt="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1"});var be={};Bt(be,{JobStore:()=>xt,deleteJob:()=>jn,deleteJobsByInstance:()=>Lt,listJobs:()=>at,listJobsByInstance:()=>Cn,loadJob:()=>_n,loadJobOrThrow:()=>U,saveJob:()=>z});import{readFile as xn,writeFile as Sn,mkdir as fe,rm as In,readdir as kn}from"fs/promises";import{existsSync as he}from"fs";import{join as ye}from"path";var xt,V,z,_n,U,jn,at,Cn,Lt,tt=Ct(()=>{"use strict";M();xt=class{jobsDir;constructor(t){let n=t??S.root;this.jobsDir=ye(n,"jobs")}jobFile(t){return ye(this.jobsDir,`${t}.json`)}async save(t){await fe(this.jobsDir,{recursive:!0,mode:448}),await Sn(this.jobFile(t.id),JSON.stringify(t,null,2),{mode:384})}async load(t){let n=this.jobFile(t);if(!he(n))return null;let r=await xn(n,"utf-8");return JSON.parse(r)}async loadOrThrow(t){let n=await this.load(t);if(!n)throw new Error(`Job "${t}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(t){let n=this.jobFile(t);he(n)&&await In(n)}async list(){await fe(this.jobsDir,{recursive:!0,mode:448});let t=await kn(this.jobsDir),n=[];for(let r of t){if(!r.endsWith(".json"))continue;let o=r.replace(".json",""),c=await this.load(o);c&&n.push(c)}return n}async listByInstance(t){return(await this.list()).filter(r=>r.instance===t)}async deleteByInstance(t){let n=await this.listByInstance(t);for(let r of n)await this.delete(r.id)}},V=new xt,z=e=>V.save(e),_n=e=>V.load(e),U=e=>V.loadOrThrow(e),jn=e=>V.delete(e),at=()=>V.list(),Cn=e=>V.listByInstance(e),Lt=e=>V.deleteByInstance(e)});import{Command as Un}from"commander";import{readFileSync as Bn}from"fs";import{fileURLToPath as qn}from"url";import{dirname as Wn,join as Yn}from"path";import Z from"picocolors";var R=e=>Z.red(e),Ke=e=>Z.yellow(e),q=e=>Z.green(e),De=e=>Z.red(e),p=e=>Z.dim(e),v=e=>Z.bold(e),E="\u{1F98E}";var H=q("\u2713"),nt=De("\u2716"),Wt=Ke("\u26A0"),ut="\u{12248}",Yt=`
3
- ${R(" /\\")}
4
- ${R(" / \\")}
5
- ${R(" / \u{1F525} \\")}
6
- ${R(" / \\")}
2
+ var Re=Object.defineProperty;var _t=(e,t)=>()=>(e&&(t=e(e=0)),t);var Ut=(e,t)=>{for(var n in t)Re(e,n,{get:t[n],enumerable:!0})};import{homedir as Le}from"os";import{join as F}from"path";var L,x,M=_t(()=>{"use strict";L=F(Le(),".gibil"),x={root:L,instances:F(L,"instances"),keys:F(L,"keys"),jobs:F(L,"jobs"),instanceFile:e=>F(L,"instances",`${e}.json`),keyDir:e=>F(L,"keys",e),privateKey:e=>F(L,"keys",e,"id_ed25519"),publicKey:e=>F(L,"keys",e,"id_ed25519.pub")}});var Ht={};Ut(Ht,{clearApiKey:()=>Tt,fetchUsage:()=>Nt,getApiKey:()=>E,getApiUrl:()=>Be,getApiUrlFromConfig:()=>mt,getHetznerToken:()=>At,getServerDefaults:()=>qe,saveApiKey:()=>Pt,saveHetznerToken:()=>it,saveServerDefaults:()=>Ot,trackUsage:()=>tt,verifyApiKey:()=>Q});import{readFile as Fe,writeFile as Ge,mkdir as Je}from"fs/promises";import{existsSync as ze}from"fs";import{join as Ue}from"path";async function G(){if(!ze(Et))return{};let e=await Fe(Et,"utf-8");return JSON.parse(e)}async function dt(e){await Je(x.root,{recursive:!0,mode:448}),await Ge(Et,JSON.stringify(e,null,2),{mode:384})}async function Pt(e){let t=await G();t.api_key=e,await dt(t)}async function E(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await G()).api_key??null}async function Tt(){let e=await G();delete e.api_key,await dt(e)}function Be(){return process.env.GIBIL_API_URL??Xt}async function mt(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await G()).api_url??Xt}async function it(e){let t=await G();t.hetzner_token=e,await dt(t)}async function At(){return process.env.HETZNER_API_TOKEN?process.env.HETZNER_API_TOKEN:(await G()).hetzner_token??null}async function Ot(e,t){let n=await G();n.default_server_type=e,n.default_location=t,await dt(n)}async function qe(){let e=await G();return{serverType:e.default_server_type??"cax11",location:e.default_location??"fsn1"}}async function Q(e){let t=await mt(),n=await fetch(`${t}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:e})});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 tt(e,t,n,r){let o=await mt(),c=await fetch(`${o}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:e,event:t,instance_name:n,server_type:r})});if(c.status===429)throw new Error("Plan limit reached. Upgrade at https://gibil.dev/pricing");if(!c.ok){let i=await c.text();throw new Error(`Usage tracking failed (${c.status}): ${i}`)}}async function Nt(e){let t=await mt(),n=await fetch(`${t}/usage-get`,{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let r=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${r}`)}return await n.json()}var Et,Xt,J=_t(()=>{"use strict";M();Et=Ue(x.root,"config.json"),Xt="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1"});var ye={};Ut(ye,{JobStore:()=>xt,deleteJob:()=>_n,deleteJobsByInstance:()=>Kt,listJobs:()=>ct,listJobsByInstance:()=>jn,loadJob:()=>kn,loadJobOrThrow:()=>U,saveJob:()=>z});import{readFile as $n,writeFile as xn,mkdir as ge,rm as Sn,readdir as In}from"fs/promises";import{existsSync as fe}from"fs";import{join as he}from"path";var xt,Y,z,kn,U,_n,ct,jn,Kt,nt=_t(()=>{"use strict";M();xt=class{jobsDir;constructor(t){let n=t??x.root;this.jobsDir=he(n,"jobs")}jobFile(t){return he(this.jobsDir,`${t}.json`)}async save(t){await ge(this.jobsDir,{recursive:!0,mode:448}),await xn(this.jobFile(t.id),JSON.stringify(t,null,2),{mode:384})}async load(t){let n=this.jobFile(t);if(!fe(n))return null;let r=await $n(n,"utf-8");return JSON.parse(r)}async loadOrThrow(t){let n=await this.load(t);if(!n)throw new Error(`Job "${t}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(t){let n=this.jobFile(t);fe(n)&&await Sn(n)}async list(){await ge(this.jobsDir,{recursive:!0,mode:448});let t=await In(this.jobsDir),n=[];for(let r of t){if(!r.endsWith(".json"))continue;let o=r.replace(".json",""),c=await this.load(o);c&&n.push(c)}return n}async listByInstance(t){return(await this.list()).filter(r=>r.instance===t)}async deleteByInstance(t){let n=await this.listByInstance(t);for(let r of n)await this.delete(r.id)}},Y=new xt,z=e=>Y.save(e),kn=e=>Y.load(e),U=e=>Y.loadOrThrow(e),_n=e=>Y.delete(e),ct=()=>Y.list(),jn=e=>Y.listByInstance(e),Kt=e=>Y.deleteByInstance(e)});import{Command as qn}from"commander";import{readFileSync as Wn}from"fs";import{fileURLToPath as Vn}from"url";import{dirname as Yn,join as Zn}from"path";import X from"picocolors";var D=e=>X.red(e),Me=e=>X.yellow(e),q=e=>X.green(e),De=e=>X.red(e),p=e=>X.dim(e),b=e=>X.bold(e),C="\u{1F98E}";var R=q("\u2713"),rt=De("\u2716"),qt=Me("\u26A0"),ut="\u{12248}",Wt=`
3
+ ${D(" /\\")}
4
+ ${D(" / \\")}
5
+ ${D(" / \u{1F525} \\")}
6
+ ${D(" / \\")}
7
7
  ${p(" ~~~~~~~~")}
8
- ${v(" g i b i l")} ${p(ut)}
9
- `,Vt=`${E} ${v("gibil")} ${p(ut)}`,qt=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],et=class{timer=null;frame=0;text;constructor(t){this.text=t}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let t=R(qt[this.frame%qt.length]);process.stderr.write(`\r ${t} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
8
+ ${b(" g i b i l")} ${p(ut)}
9
+ `,Vt=`${C} ${b("gibil")} ${p(ut)}`,Bt=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],ot=class{timer=null;frame=0;text;constructor(t){this.text=t}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let t=D(Bt[this.frame%Bt.length]);process.stderr.write(`\r ${t} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
10
10
  `),this)}update(t){this.text=t,process.stderr.isTTY||process.stderr.write(` ${t}
11
- `)}succeed(t){this.stop(),process.stderr.write(`\r ${H} ${t??this.text}
12
- `)}fail(t){this.stop(),process.stderr.write(`\r ${nt} ${t??this.text}
13
- `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};function Zt(e,t){let n=Math.max(e.length+4,...t.map(a=>Et(a).length+4)),r=`${p("\u256D")}${p("\u2500".repeat(n))}${p("\u256E")}`,o=`${p("\u2570")}${p("\u2500".repeat(n))}${p("\u256F")}`,c=`${p("\u2502")} ${E} ${v(e)}${" ".repeat(n-Et(e).length-4)}${p("\u2502")}`,i=`${p("\u251C")}${p("\u2500".repeat(n))}${p("\u2524")}`,l=t.map(a=>{let u=n-Et(a).length-2;return`${p("\u2502")} ${a}${" ".repeat(Math.max(0,u))}${p("\u2502")}`});return[r,c,i,...l,o].join(`
14
- `)}function Et(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}var k={welcome:`${E} Your first fire. Welcome to Gibil.`,noInstances:`${E} No fires burning. Gibil sleeps.`,destroyAll:`${E} All fires extinguished. Gibil moves on.`,destroySingle:e=>`${E} "${e}" \u2014 fire out.`,authSuccess:`${E} Logged in. The forge is yours.`,authLogout:`${E} Logged out. The forge cools.`,createReady:(e,t)=>`${E} "${e}" forged ${p(`(${t}s)`)}`,fleetReady:(e,t)=>`${E} Fleet forged \u2014 ${e}/${t} fires lit.`,ttlWarning:(e,t)=>`${E} ${e} \u2014 flame is low (${t}m remaining)`,initComplete:`${E} The forge is ready. Run ${v("gibil create")} to light your first fire.`,setupNeeded:`${E} No forge configured. Run ${v("gibil init")} to get started.`};var Le="info",Pt=!1,Xt={debug:0,info:1,warn:2,error:3,silent:4};function $(e){Pt=e}function K(e){return Pt&&e!=="error"?!1:Xt[e]>=Xt[Le]}var s={debug(e,...t){K("debug")&&console.debug(`${p("[debug]")} ${e}`,...t)},info(e,...t){K("info")&&console.log(e,...t)},warn(e,...t){K("warn")&&console.warn(`${Wt} ${e}`,...t)},error(e,...t){K("error")&&console.error(`${nt} ${e}`,...t)},success(e){K("info")&&console.log(`${H} ${e}`)},step(e){K("info")&&console.log(` ${p("\u203A")} ${e}`)},flame(e){K("info")&&console.log(e)},detail(e,t){K("info")&&console.log(` ${p(e+":")} ${t}`)},spin(e){return Pt?new et(e):new et(e).start()},json(e){console.log(JSON.stringify(e,null,2))}};var Ye="https://api.hetzner.cloud/v1",T=class e{token;constructor(t){this.token=t}static async create(t){let{getHetznerToken:n}=await Promise.resolve().then(()=>(J(),Mt)),r=t??await n();if(!r)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new e(r)}async request(t,n,r){let o=`${Ye}${n}`;s.debug(`${t} ${o}`);let c=await fetch(o,{method:t,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0});if(!c.ok){let i=await c.text(),l;try{l=JSON.parse(i).error?.message??i}catch{l=i}throw new Error(`Hetzner API error (${c.status}): ${l}`)}return c.status===204?{}:await c.json()}async createServer(t,n,r,o,c){if(!o||!c){let{getServerDefaults:u}=await Promise.resolve().then(()=>(J(),Mt)),g=await u();o=o??g.serverType,c=c??g.location}if(o.startsWith("cax")&&!["fsn1","nbg1"].includes(c))throw new Error(`ARM server type "${o}" is not available in "${c}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let a={name:t,server_type:o,image:"ubuntu-24.04",ssh_keys:[n],labels:{gibil:"true","gibil-name":t},location:c};s.debug(`createServer payload: ${JSON.stringify({name:t,server_type:o,image:"ubuntu-24.04",location:c})}`),r&&(a.user_data=r);try{return(await this.request("POST","/servers",a)).server}catch(u){let g=`(server_type=${o}, location=${c}). Try a different --server-type or --location.`;throw u instanceof Error?new Error(`${u.message} ${g}`):u}}async destroyServer(t){await this.request("DELETE",`/servers/${t}`)}async getServer(t){return(await this.request("GET",`/servers/${t}`)).server}async listServers(t="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(t)}&per_page=50`)).servers}async waitForReady(t,n=12e4){let r=Date.now(),o=3e3;for(;Date.now()-r<n;){let c=await this.getServer(t);if(c.status==="running"&&c.public_net.ipv4.ip!=="0.0.0.0")return c;s.debug(`Server ${t} status: ${c.status}, waiting...`),await new Promise(i=>setTimeout(i,o))}throw new Error(`Server ${t} did not become ready within ${n/1e3}s`)}async createSSHKey(t,n){return(await this.request("POST","/ssh_keys",{name:t,public_key:n})).ssh_key}async deleteSSHKey(t){await this.request("DELETE",`/ssh_keys/${t}`)}};M();import{mkdir as Ve,rm as te,readFile as Ze,chmod as Xe}from"fs/promises";import{existsSync as ee}from"fs";import{execFile as Qe}from"child_process";import{promisify as tn}from"util";var en=tn(Qe);async function mt(e){let t=S.keyDir(e);ee(t)&&await te(t,{recursive:!0}),await Ve(t,{recursive:!0});let n=S.privateKey(e),r=S.publicKey(e);await en("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${e}`]),await Xe(n,384);let o=await Ze(r,"utf-8");return{privateKeyPath:n,publicKeyPath:r,publicKey:o.trim()}}async function Q(e){let t=S.keyDir(e);ee(t)&&await te(t,{recursive:!0})}M();import{Client as nn}from"ssh2";import{readFile as on}from"fs/promises";async function I(e){let{instanceName:t,ip:n,command:r,stream:o=!1,timeoutMs:c=3e4}=e,i=await on(S.privateKey(t),"utf-8");return new Promise((l,a)=>{let u=new nn,g="",f="";u.on("ready",()=>{s.debug(`SSH connected to ${n}`),u.exec(r,(d,m)=>{if(d)return u.end(),a(d);m.on("data",h=>{let b=h.toString();g+=b,o&&process.stdout.write(b)}),m.stderr.on("data",h=>{let b=h.toString();f+=b,o&&process.stderr.write(b)}),m.on("close",h=>{u.end(),l({stdout:g,stderr:f,exitCode:h??0})})})}).on("error",d=>{let m="";d.code==="ECONNREFUSED"?m=" (instance may have been destroyed or is still booting)":d.code==="EHOSTUNREACH"?m=" (IP unreachable \u2014 instance may not be running)":d.code==="ETIMEDOUT"&&(m=" (connection timed out \u2014 check if instance is running with 'gibil list')"),a(new Error(`SSH connection to ${n} failed: ${d.message}${m}`))}).connect({host:n,port:22,username:"root",privateKey:i,readyTimeout:c,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}async function gt(e,t,n=12e4){let r=Date.now(),o=5e3;for(;Date.now()-r<n;)try{await I({instanceName:e,ip:t,command:"echo ready",timeoutMs:1e4});return}catch{s.debug(`SSH not ready on ${t}, retrying...`),await new Promise(c=>setTimeout(c,o))}throw new Error(`SSH did not become available on ${t} within ${n/1e3}s`)}function ft(e){let{repo:t,config:n,ttlMinutes:r,githubToken:o,gitIdentity:c}=e,i=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];o&&i.push(`export GITHUB_TOKEN=${A(o)}`),i.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(i.push(...rn(l)),n?.services&&n.services.length>0){i.push(...sn()),i.push("");for(let a of n.services)i.push(...an(a))}if(n?.env){i.push("# Environment variables");for(let[a,u]of Object.entries(n.env))i.push(`export ${a}=${A(u)}`),i.push(`echo ${A(`${a}=${u}`)} >> /etc/environment`);i.push("")}if(i.push("# Configure git"),c?(i.push(`git config --global user.email ${A(c.email)}`),i.push(`git config --global user.name ${A(c.name)}`),c.signingKey&&(i.push("git config --global gpg.format ssh"),i.push(`git config --global user.signingkey ${A("key::"+c.signingKey)}`),i.push("git config --global commit.gpgsign true"),i.push("git config --global tag.gpgsign true"),i.push("mkdir -p /root/.ssh"),i.push(`echo ${A(c.email+" "+c.signingKey)} > /root/.ssh/allowed_signers`),i.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(i.push("git config --global user.email 'gibil@bot.dev'"),i.push("git config --global user.name 'Gibil Bot'")),i.push(""),t){let a=t.match(/github\.com\/([^/]+\/[^/.]+)/);if(i.push("# Clone repository"),i.push("cd /root"),a){let u=a[1];i.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),i.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),i.push("else"),i.push(` CLONE_URL='https://github.com/${u}.git'`),i.push("fi"),i.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else i.push(`timeout 300 git clone ${A(t)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`);i.push("cd /root/project"),i.push(""),i.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),i.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),a&&i.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${a[1]}.git"`),i.push("fi"),i.push("")}if(r&&r>0&&(i.push("# Auto-destroy after TTL"),i.push(`echo "shutdown -h now" | at now + ${r} minutes 2>/dev/null || true`),i.push(`(sleep ${r*60} && shutdown -h now) &`),i.push("")),i.push("# Clean up cloud-init secrets"),i.push("rm -f /var/lib/cloud/instance/user-data.txt"),i.push(""),i.push("# Signal that infrastructure is ready"),i.push("touch /root/.gibil-ready"),i.push('echo "Gibil infrastructure ready"'),i.push(""),t&&n?.tasks&&n.tasks.length>0){i.push("# Run project tasks"),i.push("cd /root/project");for(let a of n.tasks)i.push(`echo '\u25B6 Running task: '${A(a.name)}`),i.push(`if ! ${a.command}; then`),i.push(` echo '\u2717 Task failed: '${A(a.name)}`),i.push(" touch /root/.gibil-tasks-failed"),i.push("fi");i.push(""),i.push("# Signal tasks complete"),i.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),i.push(" touch /root/.gibil-tasks-done"),i.push(' echo "Gibil tasks complete"'),i.push("else"),i.push(' echo "Gibil tasks finished with errors"'),i.push("fi")}return i.join(`
15
- `)}function rn(e){let t=[];if(e.startsWith("node:")){let n=e.split(":")[1]??"20";t.push("# Install Node.js"),t.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),t.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),t.push("npm install -g pnpm@latest > /dev/null 2>&1"),t.push("")}else if(e.startsWith("python:")){let n=e.split(":")[1]??"3.12";t.push("# Install Python"),t.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),t.push("")}else if(e.startsWith("go:")){let n=e.split(":")[1]??"1.22";t.push("# Install Go"),t.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),t.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),t.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),t.push("export PATH=$PATH:/usr/local/go/bin"),t.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),t.push("")}else t.push("# Install Node.js (default)"),t.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),t.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),t.push("npm install -g pnpm@latest > /dev/null 2>&1"),t.push("");return t}function sn(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function an(e){let t=[];t.push(`# Start service: ${e.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let r=`docker run -d --name ${e.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(e.port&&(r+=` -p ${e.port}:${e.port}`),e.env)for(let[o,c]of Object.entries(e.env))r+=` -e ${o}=${A(c)}`;return r+=` ${e.image}`,t.push(r),t.push(""),t}function A(e){return`'${e.replace(/'/g,"'\\''")}'`}import{readFile as cn}from"fs/promises";import{existsSync as ne,statSync as ln}from"fs";import{join as un}from"path";import{parse as re}from"yaml";var dn=".gibil.yml";async function ht(e){let t;if(ne(e)&&ln(e).isFile()?t=e:t=un(e,dn),!ne(t))return null;let n=await cn(t,"utf-8"),r=re(n);return se(r)}function ie(e){let t=re(e);return se(t)}function se(e){if(!e||typeof e!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let t=e,n={};return typeof t.name=="string"&&(n.name=t.name),typeof t.image=="string"&&(n.image=t.image),typeof t.server_type=="string"&&(n.server_type=t.server_type),typeof t.location=="string"&&(n.location=t.location),Array.isArray(t.services)&&(n.services=t.services.map(r=>{let o=r;if(typeof o.name!="string"||typeof o.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:o.name,image:o.image,port:typeof o.port=="number"?o.port:void 0,env:oe(o.env,`service "${o.name}"`)}})),Array.isArray(t.tasks)&&(n.tasks=t.tasks.map(r=>{let o=r;if(typeof o.name!="string"||typeof o.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:o.name,command:o.command}})),t.env!==void 0&&(n.env=oe(t.env,"top-level")),n}function oe(e,t){if(e==null)return;if(typeof e!="object"||Array.isArray(e))throw new Error(`env in ${t} must be a key-value object`);let n={};for(let[r,o]of Object.entries(e))if(typeof o=="string")n[r]=o;else if(typeof o=="number"||typeof o=="boolean")n[r]=String(o);else throw new Error(`env.${r} in ${t} must be a string, number, or boolean \u2014 got ${typeof o}`);return Object.keys(n).length>0?n:void 0}M();import{readFile as pn,writeFile as mn,mkdir as ae,rm as gn,readdir as fn}from"fs/promises";import{existsSync as ce}from"fs";import{join as Rt}from"path";var Kt=class{instancesDir;keysDir;constructor(t){let n=t??S.root;this.instancesDir=Rt(n,"instances"),this.keysDir=Rt(n,"keys")}async ensureDirectories(){await ae(this.instancesDir,{recursive:!0,mode:448}),await ae(this.keysDir,{recursive:!0,mode:448})}instanceFile(t){return Rt(this.instancesDir,`${t}.json`)}async save(t){await this.ensureDirectories(),await mn(this.instanceFile(t.name),JSON.stringify(t,null,2),{mode:384})}async load(t){let n=this.instanceFile(t);if(!ce(n))return null;let r=await pn(n,"utf-8");return JSON.parse(r)}async loadOrThrow(t){let n=await this.load(t);if(!n)throw new Error(`Instance "${t}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(t){let n=await this.loadOrThrow(t);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${t}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${t}" to clean up.`);return n}async delete(t){let n=this.instanceFile(t);ce(n)&&await gn(n)}async list(){await this.ensureDirectories();let t=await fn(this.instancesDir),n=[];for(let r of t){if(!r.endsWith(".json"))continue;let o=r.replace(".json",""),c=await this.load(o);c&&n.push(c)}return n}},it=new Kt;var W=e=>it.save(e);var yt=e=>it.loadOrThrow(e),_=e=>it.loadActiveOrThrow(e),bt=e=>it.delete(e),Y=()=>it.list();import{randomBytes as hn}from"crypto";function Dt(e=6){return hn(Math.ceil(e/2)).toString("hex").slice(0,e)}function wt(){return`gibil-${Dt()}`}function le(){return`fleet-${Dt(8)}`}function vt(){return`j-${Dt(8)}`}M();var yn=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function ue(e){if(!yn.test(e))throw new Error(`Invalid instance name "${e}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return e}function st(e,t){let n=parseInt(e,10);if(isNaN(n)||n<=0)throw new Error(`${t} must be a positive integer, got "${e}"`);return n}J();import{execSync as $t}from"child_process";import{readFileSync as bn}from"fs";function wn(){try{let e=$t("git config user.name",{encoding:"utf-8"}).trim(),t=$t("git config user.email",{encoding:"utf-8"}).trim();if(!e||!t)return;let n;try{if($t("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let o=$t("git config user.signingkey",{encoding:"utf-8"}).trim();if(o)try{n=bn(o,"utf-8").trim()}catch{(o.startsWith("ssh-")||o.startsWith("key::"))&&(n=o.replace(/^key::/,""))}}}catch{}return{name:e,email:t,signingKey:n}}catch{return}}async function de(e,t,n){s.step("Generating SSH keys...");let r=await mt(t),o;try{s.step("Uploading SSH key..."),o=await e.createSSHKey(`gibil-${t}`,r.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&s.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let c=wn(),i=ft({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:c});s.step("Creating server...");let l=await e.createServer(t,o.id,i,n.serverType??n.config?.server_type,n.location??n.config?.location);s.step("Waiting for server...");let u=(await e.waitForReady(l.id)).public_net.ipv4.ip,g=new Date,f={name:t,serverId:l.id,ip:u,sshKeyId:o.id,keyPath:S.privateKey(t),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};if(await W(f),s.step(`Waiting for SSH on ${u}...`),await gt(t,u),n.repo||n.config){s.step("Waiting for provisioning...");let d=36e4,m=5e3,h=Date.now(),b=!1;for(;Date.now()-h<d;){try{if((await I({instanceName:t,ip:u,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){b=!0;break}}catch{}await new Promise(C=>setTimeout(C,m))}if(!b)try{let C=await I({instanceName:t,ip:u,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});s.warn("Provisioning may have failed. Cloud-init log:"),s.info(C.stdout)}catch{s.warn("Provisioning may have failed \u2014 could not read cloud-init log.")}}return f}catch(c){if(s.error(`Failed to create instance "${t}", cleaning up...`),await Q(t).catch(i=>s.warn(`Could not clean up SSH keys: ${i instanceof Error?i.message:String(i)}`)),o){let i=o.id;await e.deleteSSHKey(i).catch(l=>s.warn(`Could not delete Hetzner SSH key ${i}: ${l instanceof Error?l.message:String(l)}`))}throw c}}function pe(e){let t=Math.max(0,Math.floor((new Date(e.expiresAt).getTime()-Date.now())/1e3));return{name:e.name,ip:e.ip,ssh:`ssh -i ${e.keyPath} -o StrictHostKeyChecking=no root@${e.ip}`,status:e.status,ttl_remaining:t,created_at:e.createdAt,fleet_id:e.fleetId}}async function vn(e){let t=e.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!t)return s.debug(`Cannot fetch config from non-GitHub repo: ${e}`),null;let[,n,r]=t,o=`https://raw.githubusercontent.com/${n}/${r}/HEAD/.gibil.yml`;s.debug(`Fetching config from ${o}`);try{let c={};process.env.GITHUB_TOKEN&&(c.Authorization=`token ${process.env.GITHUB_TOKEN}`);let i=await fetch(o,{signal:AbortSignal.timeout(1e4),headers:c});if(!i.ok)return s.debug(`No .gibil.yml found in repo (${i.status})`),null;let l=await i.text();return ie(l)}catch{return s.debug("Failed to fetch repo config, continuing without it"),null}}function me(e){e.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 <minutes>","Auto-destroy after N minutes","60").option("-c, --config <path>","Path to .gibil.yml config").option("--server-type <type>","Hetzner server type (e.g. cpx11, cpx21)").option("--location <loc>","Hetzner location (e.g. fsn1, nbg1)").option("-q, --quiet","Suppress non-essential output").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").action(async t=>{t.json&&$(!0);let n=st(t.ttl??"60","TTL");if(n>10080)throw new Error("TTL cannot exceed 7 days (10080 minutes).");let r=st(t.fleet??"1","Fleet count");if(r>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");t.name&&ue(t.name);let o={};if(t.env)for(let a of t.env){let u=a.indexOf("=");if(u<=0)throw new Error(`Invalid --env format: "${a}". Use KEY=VALUE.`);o[a.slice(0,u)]=a.slice(u+1)}o.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=o.GITHUB_TOKEN);let c=null;t.config?c=await ht(t.config):t.repo?c=await vn(t.repo)??await ht(process.cwd()):c=await ht(process.cwd()),Object.keys(o).length>0&&(c||(c={}),c.env={...c.env,...o});let i=await P();if(i){s.info("Verifying API key...");let a=await F(i);s.info(` Authenticated as ${a.user.email} (${a.user.plan})`)}let l=await T.create();if(r===1){let a=t.name??wt(),u=Date.now(),g=s.spin(`Forging "${a}"...`),f=await de(l,a,{repo:t.repo,ttlMinutes:n,config:c,serverType:t.serverType,location:t.location}),d=((Date.now()-u)/1e3).toFixed(1);g.succeed(k.createReady(a,d)),i&&await X(i,"create",f.name,t.serverType).catch(m=>s.debug(`Usage tracking failed: ${m instanceof Error?m.message:String(m)}`)),t.json?s.json(pe(f)):(s.info(""),s.info(Zt("Server ready",[`${p("Name:")} ${v(f.name)}`,`${p("IP:")} ${f.ip}`,`${p("TTL:")} ${n} minutes`,`${p("SSH:")} ${v(`gibil ssh ${f.name}`)}`])),s.info(""))}else{let a=le(),u=t.name??"gibil",g=Date.now(),f=s.spin(`Forging fleet "${a}" \u2014 ${r} servers...`),d=Array.from({length:r},(w,x)=>`${u}-${x+1}-${a.slice(6)}`),m=await Promise.allSettled(d.map(w=>de(l,w,{repo:t.repo,ttlMinutes:n,config:c,serverType:t.serverType,location:t.location,fleetId:a}))),h=[],b=[];for(let w=0;w<m.length;w++){let x=m[w];x.status==="fulfilled"?h.push(x.value):b.push(`${d[w]}: ${x.reason instanceof Error?x.reason.message:String(x.reason)}`)}let C=((Date.now()-g)/1e3).toFixed(1);if(f.succeed(k.fleetReady(h.length,r)+` ${p(`(${C}s)`)}`),i&&await Promise.all(h.map(w=>X(i,"create",w.name,t.serverType).catch(x=>s.debug(`Usage tracking failed for ${w.name}: ${x instanceof Error?x.message:String(x)}`)))),t.json)s.json({fleet_id:a,instances:h.map(pe),errors:b});else{s.info("");for(let w of h)s.info(` ${H} ${v(w.name)} ${p("\u2192")} ${w.ip}`);for(let w of b)s.info(` ${nt} ${w}`);s.info("")}}})}import{spawn as $n}from"child_process";function ge(e){e.command("ssh <name>").description("SSH into a running ephemeral machine").action(async t=>{let n=await _(t),r=["-A","-i",n.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR",`root@${n.ip}`];$n("ssh",r,{stdio:"inherit"}).on("exit",c=>{process.exit(c??0)})})}tt();function we(e){e.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(t,n,r)=>{r.json&&$(!0);let o=await _(t),c=n.join(" "),i=r.timeout?st(r.timeout,"Timeout")*1e3:3e4;if(r.background){let a=vt(),u="/root/.gibil-jobs",g=`${u}/${a}.log`,f=`${u}/${a}.exit`,d=`${u}/${a}.pid`,m=`${u}/${a}.sh`,h=["#!/bin/bash",`nohup bash -c '${c.replace(/'/g,"'\\''")}' > ${g} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${d}`,`(wait $BGPID 2>/dev/null; echo $? > ${f}) &`,"echo $BGPID"].join(`
16
- `),b=Buffer.from(h).toString("base64"),C=`mkdir -p ${u} && echo '${b}' | base64 -d > ${m} && chmod +x ${m} && bash ${m}`,w=await I({instanceName:t,ip:o.ip,command:C,timeoutMs:1e4}),x=parseInt(w.stdout.trim(),10);isNaN(x)&&(s.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await z({id:a,instance:t,command:c,pid:x,status:"running",startedAt:new Date().toISOString()}),r.json?s.json({job_id:a,instance:t,status:"running",pid:x}):(s.info(`Background job started: ${a} (PID ${x})`),s.info(` Poll: gibil job ${a}`));return}s.info(`Running on "${t}" (${o.ip}): ${c}`);let l=await I({instanceName:t,ip:o.ip,command:c,stream:!r.json,timeoutMs:i});r.json?s.json({instance:t,command:c,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&s.error(`Command exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}tt();J();async function ve(e,t){let n=await yt(t);s.info(`Destroying instance "${t}" (server ${n.serverId})...`);try{await e.destroyServer(n.serverId)}catch(o){s.warn(`Could not delete server ${n.serverId}: ${o instanceof Error?o.message:String(o)}`)}try{await e.deleteSSHKey(n.sshKeyId)}catch(o){s.warn(`Could not delete SSH key ${n.sshKeyId}: ${o instanceof Error?o.message:String(o)}`)}await Q(t),await Lt(t),await bt(t);let r=await P();r&&await X(r,"destroy",t).catch(o=>s.warn(`Usage tracking failed (billing may be inaccurate): ${o instanceof Error?o.message:String(o)}`)),s.info(` ${H} ${k.destroySingle(t)}`)}function $e(e){e.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").action(async(t,n)=>{if(n.json&&$(!0),n.all){let r=await Y();if(r.length===0){n.json?s.json({destroyed:[],failed:[]}):s.info(k.noInstances);return}let o=await T.create();s.info(`Destroying ${r.length} instance(s)...`);let c=await Promise.allSettled(r.map(a=>ve(o,a.name))),i=[],l=[];for(let a=0;a<c.length;a++)if(c[a].status==="fulfilled")i.push(r[a].name);else{let u=c[a].reason;l.push(`${r[a].name}: ${u instanceof Error?u.message:String(u)}`)}n.json?s.json({destroyed:i,failed:l}):l.length===0?s.info(`
11
+ `)}succeed(t){this.stop(),process.stderr.write(`\r ${R} ${t??this.text}
12
+ `)}fail(t){this.stop(),process.stderr.write(`\r ${rt} ${t??this.text}
13
+ `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};function Yt(e,t){let n=Math.max(e.length+4,...t.map(a=>jt(a).length+4)),r=`${p("\u256D")}${p("\u2500".repeat(n))}${p("\u256E")}`,o=`${p("\u2570")}${p("\u2500".repeat(n))}${p("\u256F")}`,c=`${p("\u2502")} ${C} ${b(e)}${" ".repeat(n-jt(e).length-4)}${p("\u2502")}`,i=`${p("\u251C")}${p("\u2500".repeat(n))}${p("\u2524")}`,l=t.map(a=>{let u=n-jt(a).length-2;return`${p("\u2502")} ${a}${" ".repeat(Math.max(0,u))}${p("\u2502")}`});return[r,c,i,...l,o].join(`
14
+ `)}function jt(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}var k={welcome:`${C} Your first fire. Welcome to Gibil.`,noInstances:`${C} No fires burning. Gibil sleeps.`,destroyAll:`${C} All fires extinguished. Gibil moves on.`,destroySingle:e=>`${C} "${e}" \u2014 fire out.`,authSuccess:`${C} Logged in. The forge is yours.`,authLogout:`${C} Logged out. The forge cools.`,createReady:(e,t)=>`${C} "${e}" forged ${p(`(${t}s)`)}`,fleetReady:(e,t)=>`${C} Fleet forged \u2014 ${e}/${t} fires lit.`,ttlWarning:(e,t)=>`${C} ${e} \u2014 flame is low (${t}m remaining)`,initComplete:`${C} The forge is ready. Run ${b("gibil create")} to light your first fire.`,setupNeeded:`${C} No forge configured. Run ${b("gibil init")} to get started.`};var Ke="info",Ct=!1,Zt={debug:0,info:1,warn:2,error:3,silent:4};function $(e){Ct=e}function K(e){return Ct&&e!=="error"?!1:Zt[e]>=Zt[Ke]}var s={debug(e,...t){K("debug")&&console.debug(`${p("[debug]")} ${e}`,...t)},info(e,...t){K("info")&&console.log(e,...t)},warn(e,...t){K("warn")&&console.warn(`${qt} ${e}`,...t)},error(e,...t){K("error")&&console.error(`${rt} ${e}`,...t)},success(e){K("info")&&console.log(`${R} ${e}`)},step(e){K("info")&&console.log(` ${p("\u203A")} ${e}`)},flame(e){K("info")&&console.log(e)},detail(e,t){K("info")&&console.log(` ${p(e+":")} ${t}`)},spin(e){return Ct?new ot(e):new ot(e).start()},json(e){console.log(JSON.stringify(e,null,2))}};var We="https://api.hetzner.cloud/v1",A=class e{token;constructor(t){this.token=t}static async create(t){let{getHetznerToken:n}=await Promise.resolve().then(()=>(J(),Ht)),r=t??await n();if(!r)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new e(r)}async request(t,n,r){let o=`${We}${n}`;s.debug(`${t} ${o}`);let c=await fetch(o,{method:t,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0});if(!c.ok){let i=await c.text(),l;try{l=JSON.parse(i).error?.message??i}catch{l=i}let a="";throw c.status===401||c.status===403?a=`
15
+ Your Hetzner token may be invalid or expired. Run: gibil init --force`:c.status===409&&l.includes("name")?a=`
16
+ A server with this name already exists. Try a different --name or run: gibil destroy <name>`:c.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`),new Error(`Hetzner API error (${c.status}): ${l}${a}`)}return c.status===204?{}:await c.json()}async createServer(t,n,r,o,c){if(!o||!c){let{getServerDefaults:u}=await Promise.resolve().then(()=>(J(),Ht)),m=await u();o=o??m.serverType,c=c??m.location}if(o.startsWith("cax")&&!["fsn1","nbg1"].includes(c))throw new Error(`ARM server type "${o}" is not available in "${c}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let a={name:t,server_type:o,image:"ubuntu-24.04",ssh_keys:[n],labels:{gibil:"true","gibil-name":t},location:c};s.debug(`createServer payload: ${JSON.stringify({name:t,server_type:o,image:"ubuntu-24.04",location:c})}`),r&&(a.user_data=r);try{return(await this.request("POST","/servers",a)).server}catch(u){let m=`(server_type=${o}, location=${c}). Try a different --server-type or --location.`;throw u instanceof Error?new Error(`${u.message} ${m}`):u}}async destroyServer(t){await this.request("DELETE",`/servers/${t}`)}async getServer(t){return(await this.request("GET",`/servers/${t}`)).server}async listServers(t="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(t)}&per_page=50`)).servers}async waitForReady(t,n=12e4){let r=Date.now(),o=3e3;for(;Date.now()-r<n;){let c=await this.getServer(t);if(c.status==="running"&&c.public_net.ipv4.ip!=="0.0.0.0")return c;s.debug(`Server ${t} status: ${c.status}, waiting...`),await new Promise(i=>setTimeout(i,o))}throw new Error(`Server ${t} did not become ready within ${n/1e3}s`)}async createSSHKey(t,n){return(await this.request("POST","/ssh_keys",{name:t,public_key:n})).ssh_key}async deleteSSHKey(t){await this.request("DELETE",`/ssh_keys/${t}`)}};M();import{mkdir as Ve,rm as Qt,readFile as Ye,chmod as Ze}from"fs/promises";import{existsSync as te}from"fs";import{execFile as Xe}from"child_process";import{promisify as Qe}from"util";var tn=Qe(Xe);async function pt(e){let t=x.keyDir(e);te(t)&&await Qt(t,{recursive:!0}),await Ve(t,{recursive:!0});let n=x.privateKey(e),r=x.publicKey(e);await tn("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${e}`]),await Ze(n,384);let o=await Ye(r,"utf-8");return{privateKeyPath:n,publicKeyPath:r,publicKey:o.trim()}}async function et(e){let t=x.keyDir(e);te(t)&&await Qt(t,{recursive:!0})}M();import{Client as en}from"ssh2";import{readFile as nn}from"fs/promises";async function S(e){let{instanceName:t,ip:n,command:r,stream:o=!1,timeoutMs:c=3e4}=e,i=await nn(x.privateKey(t),"utf-8");return new Promise((l,a)=>{let u=new en,m="",g="";u.on("ready",()=>{s.debug(`SSH connected to ${n}`),u.exec(r,(d,f)=>{if(d)return u.end(),a(d);f.on("data",w=>{let I=w.toString();m+=I,o&&process.stdout.write(I)}),f.stderr.on("data",w=>{let I=w.toString();g+=I,o&&process.stderr.write(I)}),f.on("close",w=>{u.end(),l({stdout:m,stderr:g,exitCode:w??0})})})}).on("error",d=>{let f="";d.code==="ECONNREFUSED"?f=" (instance may have been destroyed or is still booting)":d.code==="EHOSTUNREACH"?f=" (IP unreachable \u2014 instance may not be running)":d.code==="ETIMEDOUT"&&(f=" (connection timed out \u2014 check if instance is running with 'gibil list')"),a(new Error(`SSH connection to ${n} failed: ${d.message}${f}`))}).connect({host:n,port:22,username:"root",privateKey:i,readyTimeout:c,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}async function gt(e,t,n=12e4){let r=Date.now(),o=5e3;for(;Date.now()-r<n;)try{await S({instanceName:e,ip:t,command:"echo ready",timeoutMs:1e4});return}catch{s.debug(`SSH not ready on ${t}, retrying...`),await new Promise(c=>setTimeout(c,o))}throw new Error(`SSH did not become available on ${t} within ${n/1e3}s`)}function ft(e){let{repo:t,config:n,ttlMinutes:r,githubToken:o,gitIdentity:c}=e,i=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];o&&i.push(`export GITHUB_TOKEN=${P(o)}`),i.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(i.push(...on(l)),n?.services&&n.services.length>0){i.push(...rn()),i.push("");for(let a of n.services)i.push(...sn(a))}if(n?.env){i.push("# Environment variables");for(let[a,u]of Object.entries(n.env))i.push(`export ${a}=${P(u)}`),i.push(`echo ${P(`${a}=${u}`)} >> /etc/environment`);i.push("")}if(i.push("# Configure git"),c?(i.push(`git config --global user.email ${P(c.email)}`),i.push(`git config --global user.name ${P(c.name)}`),c.signingKey&&(i.push("git config --global gpg.format ssh"),i.push(`git config --global user.signingkey ${P("key::"+c.signingKey)}`),i.push("git config --global commit.gpgsign true"),i.push("git config --global tag.gpgsign true"),i.push("mkdir -p /root/.ssh"),i.push(`echo ${P(c.email+" "+c.signingKey)} > /root/.ssh/allowed_signers`),i.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(i.push("git config --global user.email 'gibil@bot.dev'"),i.push("git config --global user.name 'Gibil Bot'")),i.push(""),t){let a=t.match(/github\.com\/([^/]+\/[^/.]+)/);if(i.push("# Clone repository"),i.push("cd /root"),a){let u=a[1];i.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),i.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),i.push("else"),i.push(` CLONE_URL='https://github.com/${u}.git'`),i.push("fi"),i.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else i.push(`timeout 300 git clone ${P(t)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`);i.push("cd /root/project"),i.push(""),i.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),i.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),a&&i.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${a[1]}.git"`),i.push("fi"),i.push("")}if(r&&r>0&&(i.push("# Auto-destroy after TTL"),i.push(`echo "shutdown -h now" | at now + ${r} minutes 2>/dev/null || true`),i.push(`(sleep ${r*60} && shutdown -h now) &`),i.push("")),i.push("# Clean up cloud-init secrets"),i.push("rm -f /var/lib/cloud/instance/user-data.txt"),i.push(""),i.push("# Signal that infrastructure is ready"),i.push("touch /root/.gibil-ready"),i.push('echo "Gibil infrastructure ready"'),i.push(""),t&&n?.tasks&&n.tasks.length>0){i.push("# Run project tasks"),i.push("cd /root/project");for(let a of n.tasks)i.push(`echo '\u25B6 Running task: '${P(a.name)}`),i.push(`if ! ${a.command}; then`),i.push(` echo '\u2717 Task failed: '${P(a.name)}`),i.push(" touch /root/.gibil-tasks-failed"),i.push("fi");i.push(""),i.push("# Signal tasks complete"),i.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),i.push(" touch /root/.gibil-tasks-done"),i.push(' echo "Gibil tasks complete"'),i.push("else"),i.push(' echo "Gibil tasks finished with errors"'),i.push("fi")}return i.join(`
18
+ `)}function on(e){let t=[];if(e.startsWith("node:")){let n=e.split(":")[1]??"20";t.push("# Install Node.js"),t.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),t.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),t.push("npm install -g pnpm@latest > /dev/null 2>&1"),t.push("")}else if(e.startsWith("python:")){let n=e.split(":")[1]??"3.12";t.push("# Install Python"),t.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),t.push("")}else if(e.startsWith("go:")){let n=e.split(":")[1]??"1.22";t.push("# Install Go"),t.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),t.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),t.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),t.push("export PATH=$PATH:/usr/local/go/bin"),t.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),t.push("")}else t.push("# Install Node.js (default)"),t.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),t.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),t.push("npm install -g pnpm@latest > /dev/null 2>&1"),t.push("");return t}function rn(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function sn(e){let t=[];t.push(`# Start service: ${e.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let r=`docker run -d --name ${e.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(e.port&&(r+=` -p ${e.port}:${e.port}`),e.env)for(let[o,c]of Object.entries(e.env))r+=` -e ${o}=${P(c)}`;return r+=` ${e.image}`,t.push(r),t.push(""),t}function P(e){return`'${e.replace(/'/g,"'\\''")}'`}import{readFile as an}from"fs/promises";import{existsSync as ee,statSync as cn}from"fs";import{join as ln}from"path";import{parse as oe}from"yaml";var un=".gibil.yml";async function ht(e){let t;if(ee(e)&&cn(e).isFile()?t=e:t=ln(e,un),!ee(t))return null;let n=await an(t,"utf-8"),r=oe(n);return ie(r)}function re(e){let t=oe(e);return ie(t)}function ie(e){if(!e||typeof e!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let t=e,n={};return typeof t.name=="string"&&(n.name=t.name),typeof t.image=="string"&&(n.image=t.image),typeof t.server_type=="string"&&(n.server_type=t.server_type),typeof t.location=="string"&&(n.location=t.location),Array.isArray(t.services)&&(n.services=t.services.map(r=>{let o=r;if(typeof o.name!="string"||typeof o.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:o.name,image:o.image,port:typeof o.port=="number"?o.port:void 0,env:ne(o.env,`service "${o.name}"`)}})),Array.isArray(t.tasks)&&(n.tasks=t.tasks.map(r=>{let o=r;if(typeof o.name!="string"||typeof o.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:o.name,command:o.command}})),t.env!==void 0&&(n.env=ne(t.env,"top-level")),n}function ne(e,t){if(e==null)return;if(typeof e!="object"||Array.isArray(e))throw new Error(`env in ${t} must be a key-value object`);let n={};for(let[r,o]of Object.entries(e))if(typeof o=="string")n[r]=o;else if(typeof o=="number"||typeof o=="boolean")n[r]=String(o);else throw new Error(`env.${r} in ${t} must be a string, number, or boolean \u2014 got ${typeof o}`);return Object.keys(n).length>0?n:void 0}M();import{readFile as dn,writeFile as mn,mkdir as se,rm as pn,readdir as gn}from"fs/promises";import{existsSync as ae}from"fs";import{join as Rt}from"path";var Mt=class{instancesDir;keysDir;constructor(t){let n=t??x.root;this.instancesDir=Rt(n,"instances"),this.keysDir=Rt(n,"keys")}async ensureDirectories(){await se(this.instancesDir,{recursive:!0,mode:448}),await se(this.keysDir,{recursive:!0,mode:448})}instanceFile(t){return Rt(this.instancesDir,`${t}.json`)}async save(t){await this.ensureDirectories(),await mn(this.instanceFile(t.name),JSON.stringify(t,null,2),{mode:384})}async load(t){let n=this.instanceFile(t);if(!ae(n))return null;let r=await dn(n,"utf-8");return JSON.parse(r)}async loadOrThrow(t){let n=await this.load(t);if(!n)throw new Error(`Instance "${t}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(t){let n=await this.loadOrThrow(t);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${t}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${t}" to clean up.`);return n}async delete(t){let n=this.instanceFile(t);ae(n)&&await pn(n)}async list(){await this.ensureDirectories();let t=await gn(this.instancesDir),n=[];for(let r of t){if(!r.endsWith(".json"))continue;let o=r.replace(".json",""),c=await this.load(o);c&&n.push(c)}return n}},st=new Mt;var W=e=>st.save(e);var yt=e=>st.loadOrThrow(e),_=e=>st.loadActiveOrThrow(e),bt=e=>st.delete(e),V=()=>st.list();import{randomBytes as fn}from"crypto";function Dt(e=6){return fn(Math.ceil(e/2)).toString("hex").slice(0,e)}function wt(){return`gibil-${Dt()}`}function ce(){return`fleet-${Dt(8)}`}function vt(){return`j-${Dt(8)}`}M();var hn=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function le(e){if(!hn.test(e))throw new Error(`Invalid instance name "${e}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return e}function at(e,t){let n=parseInt(e,10);if(isNaN(n)||n<=0)throw new Error(`${t} must be a positive integer, got "${e}"`);return n}J();import{execSync as $t}from"child_process";import{readFileSync as yn}from"fs";function bn(){try{let e=$t("git config user.name",{encoding:"utf-8"}).trim(),t=$t("git config user.email",{encoding:"utf-8"}).trim();if(!e||!t)return;let n;try{if($t("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let o=$t("git config user.signingkey",{encoding:"utf-8"}).trim();if(o)try{n=yn(o,"utf-8").trim()}catch{(o.startsWith("ssh-")||o.startsWith("key::"))&&(n=o.replace(/^key::/,""))}}}catch{}return{name:e,email:t,signingKey:n}}catch{return}}async function ue(e,t,n){s.step("Generating SSH keys...");let r=await pt(t),o;try{s.step("Uploading SSH key..."),o=await e.createSSHKey(`gibil-${t}`,r.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&s.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let c=bn(),i=ft({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:c}),l=s.spin("Creating server on Hetzner..."),a=await e.createServer(t,o.id,i,n.serverType??n.config?.server_type,n.location??n.config?.location);l.succeed("Server created");let u=s.spin("VM booting..."),g=(await e.waitForReady(a.id)).public_net.ipv4.ip;u.succeed(`VM running at ${g}`);let d=new Date,f={name:t,serverId:a.id,ip:g,sshKeyId:o.id,keyPath:x.privateKey(t),status:"running",createdAt:d.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(d.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:c};await W(f);let w=s.spin("Waiting for SSH...");if(await gt(t,g),w.succeed("SSH ready"),n.repo||n.config){let I=s.spin("Provisioning (runtime, repo, deps)..."),T=36e4,y=5e3,v=Date.now(),Z=!1;for(;Date.now()-v<T;){try{if((await S({instanceName:t,ip:g,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){Z=!0;break}}catch{}await new Promise(N=>setTimeout(N,y))}if(Z)I.succeed("Provisioning complete");else{I.fail("Provisioning may have failed");try{let N=await S({instanceName:t,ip:g,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});s.info(N.stdout)}catch{s.warn("Could not read cloud-init log.")}}}return f}catch(c){if(s.error(`Failed to create instance "${t}", cleaning up...`),await et(t).catch(i=>s.warn(`Could not clean up SSH keys: ${i instanceof Error?i.message:String(i)}`)),o){let i=o.id;await e.deleteSSHKey(i).catch(l=>s.warn(`Could not delete Hetzner SSH key ${i}: ${l instanceof Error?l.message:String(l)}`))}throw c}}function de(e){let t=Math.max(0,Math.floor((new Date(e.expiresAt).getTime()-Date.now())/1e3));return{name:e.name,ip:e.ip,ssh:`ssh -i ${e.keyPath} -o StrictHostKeyChecking=no root@${e.ip}`,status:e.status,ttl_remaining:t,created_at:e.createdAt,fleet_id:e.fleetId}}async function wn(e){let t=e.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!t)return s.debug(`Cannot fetch config from non-GitHub repo: ${e}`),null;let[,n,r]=t,o=`https://raw.githubusercontent.com/${n}/${r}/HEAD/.gibil.yml`;s.debug(`Fetching config from ${o}`);try{let c={};process.env.GITHUB_TOKEN&&(c.Authorization=`token ${process.env.GITHUB_TOKEN}`);let i=await fetch(o,{signal:AbortSignal.timeout(1e4),headers:c});if(!i.ok)return s.debug(`No .gibil.yml found in repo (${i.status})`),null;let l=await i.text();return re(l)}catch{return s.debug("Failed to fetch repo config, continuing without it"),null}}function me(e){e.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 <minutes>","Auto-destroy after N minutes","60").option("-c, --config <path>","Path to .gibil.yml config").option("--server-type <type>","Hetzner server type (e.g. cpx11, cpx21)").option("--location <loc>","Hetzner location (e.g. fsn1, nbg1)").option("-q, --quiet","Suppress non-essential output").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").action(async t=>{t.json&&$(!0);let n=at(t.ttl??"60","TTL");if(n>10080)throw new Error("TTL cannot exceed 7 days (10080 minutes).");let r=at(t.fleet??"1","Fleet count");if(r>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");t.name&&le(t.name);let o={};if(t.env)for(let a of t.env){let u=a.indexOf("=");if(u<=0)throw new Error(`Invalid --env format: "${a}". Use KEY=VALUE.`);o[a.slice(0,u)]=a.slice(u+1)}o.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=o.GITHUB_TOKEN);let c=null;t.config?c=await ht(t.config):t.repo?c=await wn(t.repo)??await ht(process.cwd()):c=await ht(process.cwd()),Object.keys(o).length>0&&(c||(c={}),c.env={...c.env,...o});let i=await E();if(i){s.info("Verifying API key...");let a=await Q(i);s.info(` Authenticated as ${a.user.email} (${a.user.plan})`)}let l=await A.create();if(r===1){let a=t.name??wt(),u=Date.now(),m=s.spin(`Forging "${a}"...`),g=await ue(l,a,{repo:t.repo,ttlMinutes:n,config:c,serverType:t.serverType,location:t.location}),d=((Date.now()-u)/1e3).toFixed(1);m.succeed(k.createReady(a,d)),i&&await tt(i,"create",g.name,t.serverType).catch(f=>s.debug(`Usage tracking failed: ${f instanceof Error?f.message:String(f)}`)),t.json?s.json(de(g)):(s.info(""),s.info(Yt("Server ready",[`${p("Name:")} ${b(g.name)}`,`${p("IP:")} ${g.ip}`,`${p("TTL:")} ${n} minutes`,`${p("SSH:")} ${b(`gibil ssh ${g.name}`)}`])),s.info(""),s.info(p(" Try:")),s.info(` ${b(`gibil run ${g.name} "<your test command>"`)}`),s.info(` ${b(`gibil ssh ${g.name}`)}`),s.info(` ${b(`gibil destroy ${g.name}`)}`),s.info(""))}else{let a=ce(),u=t.name??"gibil",m=Date.now(),g=s.spin(`Forging fleet "${a}" \u2014 ${r} servers...`),d=Array.from({length:r},(y,v)=>`${u}-${v+1}-${a.slice(6)}`),f=await Promise.allSettled(d.map(y=>ue(l,y,{repo:t.repo,ttlMinutes:n,config:c,serverType:t.serverType,location:t.location,fleetId:a}))),w=[],I=[];for(let y=0;y<f.length;y++){let v=f[y];v.status==="fulfilled"?w.push(v.value):I.push(`${d[y]}: ${v.reason instanceof Error?v.reason.message:String(v.reason)}`)}let T=((Date.now()-m)/1e3).toFixed(1);if(g.succeed(k.fleetReady(w.length,r)+` ${p(`(${T}s)`)}`),i&&await Promise.all(w.map(y=>tt(i,"create",y.name,t.serverType).catch(v=>s.debug(`Usage tracking failed for ${y.name}: ${v instanceof Error?v.message:String(v)}`)))),t.json)s.json({fleet_id:a,instances:w.map(de),errors:I});else{s.info("");for(let y of w)s.info(` ${R} ${b(y.name)} ${p("\u2192")} ${y.ip}`);for(let y of I)s.info(` ${rt} ${y}`);s.info("")}}})}import{spawn as vn}from"child_process";function pe(e){e.command("ssh <name>").description("SSH into a running ephemeral machine").action(async t=>{let n=await _(t),r=["-A","-i",n.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR",`root@${n.ip}`];vn("ssh",r,{stdio:"inherit"}).on("exit",c=>{process.exit(c??0)})})}nt();function be(e){e.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(t,n,r)=>{r.json&&$(!0);let o=await _(t),c=n.join(" "),i=r.timeout?at(r.timeout,"Timeout")*1e3:3e4;if(r.background){let a=vt(),u="/root/.gibil-jobs",m=`${u}/${a}.log`,g=`${u}/${a}.exit`,d=`${u}/${a}.pid`,f=`${u}/${a}.sh`,w=["#!/bin/bash",`nohup bash -c '${c.replace(/'/g,"'\\''")}' > ${m} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${d}`,`(wait $BGPID 2>/dev/null; echo $? > ${g}) &`,"echo $BGPID"].join(`
19
+ `),I=Buffer.from(w).toString("base64"),T=`mkdir -p ${u} && echo '${I}' | base64 -d > ${f} && chmod +x ${f} && bash ${f}`,y=await S({instanceName:t,ip:o.ip,command:T,timeoutMs:1e4}),v=parseInt(y.stdout.trim(),10);isNaN(v)&&(s.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await z({id:a,instance:t,command:c,pid:v,status:"running",startedAt:new Date().toISOString()}),r.json?s.json({job_id:a,instance:t,status:"running",pid:v}):(s.info(`Background job started: ${a} (PID ${v})`),s.info(` Poll: gibil job ${a}`));return}s.info(`Running on "${t}" (${o.ip}): ${c}`);let l=await S({instanceName:t,ip:o.ip,command:c,stream:!r.json,timeoutMs:i});r.json?s.json({instance:t,command:c,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&s.error(`Command exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}nt();J();async function we(e,t){let n=await yt(t);s.info(`Destroying instance "${t}" (server ${n.serverId})...`);try{await e.destroyServer(n.serverId)}catch(o){s.warn(`Could not delete server ${n.serverId}: ${o instanceof Error?o.message:String(o)}`)}try{await e.deleteSSHKey(n.sshKeyId)}catch(o){s.warn(`Could not delete SSH key ${n.sshKeyId}: ${o instanceof Error?o.message:String(o)}`)}await et(t),await Kt(t),await bt(t);let r=await E();r&&await tt(r,"destroy",t).catch(o=>s.warn(`Usage tracking failed (billing may be inaccurate): ${o instanceof Error?o.message:String(o)}`)),s.info(` ${R} ${k.destroySingle(t)}`)}function ve(e){e.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").action(async(t,n)=>{if(n.json&&$(!0),n.all){let r=await V();if(r.length===0){n.json?s.json({destroyed:[],failed:[]}):s.info(k.noInstances);return}let o=await A.create();s.info(`Destroying ${r.length} instance(s)...`);let c=await Promise.allSettled(r.map(a=>we(o,a.name))),i=[],l=[];for(let a=0;a<c.length;a++)if(c[a].status==="fulfilled")i.push(r[a].name);else{let u=c[a].reason;l.push(`${r[a].name}: ${u instanceof Error?u.message:String(u)}`)}n.json?s.json({destroyed:i,failed:l}):l.length===0?s.info(`
17
20
  ${k.destroyAll}`):s.info(`
18
- ${i.length} destroyed, ${l.length} failed`)}else{t||(s.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1));let r=await T.create();await ve(r,t),n.json&&s.json({destroyed:[t]})}})}function xe(e){e.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async t=>{t.json&&$(!0);let n=await Y();if(n.length===0){t.json?s.json({instances:[]}):s.info(k.noInstances);return}let r=n.map(o=>{let c=Math.max(0,Math.floor((new Date(o.expiresAt).getTime()-Date.now())/1e3));return{name:o.name,ip:o.ip,ssh:`ssh -i ${o.keyPath} -o StrictHostKeyChecking=no root@${o.ip}`,status:o.status,ttl_remaining:c,created_at:o.createdAt,fleet_id:o.fleetId}});if(t.json){s.json({instances:r});return}s.info(p(`${"NAME".padEnd(30)} ${"IP".padEnd(18)} ${"STATUS".padEnd(12)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),s.info(p("\u2500".repeat(80)));for(let o of r){let c=Se(o.ttl_remaining),i=En(o.created_at),l=o.name.padEnd(30),a=o.status.padEnd(12),u=c.padEnd(10),g=i.padEnd(10),f=o.status==="running"?q(a):R(a),d=o.ttl_remaining<=300?R(u):u;s.info(`${v(l)} ${o.ip.padEnd(18)} ${f} ${d} ${p(g)}`)}s.info(`
19
- ${p(`${r.length} server(s)`)}`)})}function Se(e){if(e<=0)return"expired";let t=Math.floor(e/60),n=e%60;return t>=60?`${Math.floor(t/60)}h ${t%60}m`:`${t}m ${n}s`}function En(e){let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/1e3);return Se(n)}function Ie(e){e.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <minutes>","New TTL in minutes from now").option("--json","Output result as JSON").action(async(t,n)=>{n.json&&$(!0);let r=await _(t),o=parseInt(n.ttl,10);(isNaN(o)||o<=0)&&(s.error("TTL must be a positive number of minutes"),process.exit(1)),await I({instanceName:t,ip:r.ip,command:["pkill -f 'sleep.*shutdown' || true",`(sleep ${o*60} && shutdown -h now) &`].join(" && ")});let c=new Date(Date.now()+o*6e4).toISOString();r.ttlMinutes=o,r.expiresAt=c,await W(r),n.json?s.json({name:r.name,ttl_minutes:o,expires_at:c}):s.info(`\u2713 Extended "${t}" TTL to ${o} minutes (expires ${c})`)})}import{readFile as Pn}from"fs/promises";import{randomBytes as An}from"crypto";function ke(e){e.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(t,n)=>{n.json&&$(!0);let r=await _(t),o=await Pn(n.script,"utf-8");s.info(`Uploading and running script "${n.script}" on "${t}"...`);let c=Buffer.from(o).toString("base64"),i=`/tmp/gibil-script-${An(4).toString("hex")}.sh`,l=await I({instanceName:t,ip:r.ip,command:`echo '${c}' | base64 -d > ${i} && chmod +x ${i} && ${i}; EXIT=$?; rm -f ${i}; exit $EXIT`,stream:!n.json});n.json?s.json({instance:t,script:n.script,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&s.error(`Script exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}J();import{createInterface as Tn}from"readline";function _e(e){let t=Tn({input:process.stdin,output:process.stderr});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}function je(e){let t=e.command("auth").description("Manage authentication");t.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&&$(!0);let r=n.key??process.env.GIBIL_API_KEY;r||(r=await _e("Enter your API key: ")),r||(s.error("No API key provided."),process.exit(1)),r.startsWith("pk_")||(s.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),s.info("Verifying API key...");try{let o=await F(r);await ot(r),n.json?s.json({authenticated:!0,email:o.user.email,plan:o.user.plan}):(s.info(k.authSuccess),s.detail("Email",o.user.email),s.detail("Plan",o.user.plan),s.detail("Limits",`${o.limits.max_concurrent} concurrent servers, ${o.limits.remaining_hours}h remaining`))}catch(o){s.error(o instanceof Error?o.message:String(o)),process.exit(1)}}),t.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 _e("Enter your Hetzner API token: ")),r||(s.error("No token provided."),process.exit(1));try{let c=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();c.error&&(s.error(`Invalid token: ${c.error.message}`),process.exit(1))}catch{s.error("Could not verify token with Hetzner API."),process.exit(1)}await rt(r),s.success("Hetzner token saved to ~/.gibil/config.json")}),t.command("logout").description("Clear stored API key").action(async()=>{await Tt(),s.info(k.authLogout)}),t.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&$(!0);let r=await P();if(!r){n.json?s.json({authenticated:!1}):s.info(`Not logged in. Run ${v("gibil auth login")} to authenticate.`);return}try{let o=await F(r);n.json?s.json({authenticated:!0,email:o.user.email,plan:o.user.plan,limits:o.limits}):(s.success(`Authenticated as ${o.user.email}`),s.detail("Plan",o.user.plan),s.detail("Concurrent servers",String(o.limits.max_concurrent)),s.detail("Hours remaining",String(o.limits.remaining_hours)))}catch{n.json?s.json({authenticated:!1,error:"Key verification failed"}):s.error(`Stored API key is invalid. Run ${v("gibil auth login")} to re-authenticate.`)}})}J();function Ce(e){e.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async t=>{t.json&&$(!0);let n=await P();n||(s.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let r=await Ht(n);if(t.json)s.json(r);else{let o=Math.round(r.vm_hours_used/r.vm_hours_limit*100);s.info(`Plan: ${r.plan}`),s.info(`VM hours: ${r.vm_hours_used.toFixed(1)} / ${r.vm_hours_limit}h (${o}%)`),s.info(`Active instances: ${r.active_instances} / ${r.max_concurrent}`),o>80&&s.warn("Running low on hours. Upgrade at https://gibil.dev/pricing")}}catch(r){s.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}import{McpServer as On}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Nn}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as y}from"zod";M();import{execSync as St}from"child_process";import{readFileSync as Hn}from"fs";tt();tt();async function Gt(e){let t=await U(e);if(t.status!=="running")return{status:t.status,exitCode:t.exitCode};let n=await _(t.instance),r="/root/.gibil-jobs",o=`${r}/${e}.exit`,c=`${r}/${e}.log`,l=(await I({instanceName:t.instance,ip:n.ip,command:`test -f ${o} && cat ${o} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING")return{status:"running"};let a=parseInt(l,10),u=await I({instanceName:t.instance,ip:n.ip,command:`cat ${c} 2>/dev/null || echo ''`,timeoutMs:1e4}),g=a===0?"done":"failed",f=new Date,d=Math.round((f.getTime()-new Date(t.startedAt).getTime())/1e3);return t.status=g,t.exitCode=a,t.completedAt=f.toISOString(),await z(t),{status:g,exitCode:a,stdout:u.stdout,durationS:d}}function Ee(e){let t=e.command("job").description("Manage background jobs");t.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&$(!0);let o=await U(n),c=await Gt(n);r.json?s.json({job_id:n,instance:o.instance,command:o.command,status:c.status,exit_code:c.exitCode,started_at:o.startedAt,duration_s:c.durationS,...c.stdout!==void 0?{stdout:c.stdout}:{}}):c.status==="running"?(s.info(`Job ${n} is still running on "${o.instance}"`),s.info(` Command: ${o.command}`),s.info(` Started: ${o.startedAt}`)):(s.info(`Job ${n}: ${c.status} (exit code ${c.exitCode}, ${c.durationS}s)`),c.stdout&&process.stdout.write(c.stdout))}),t.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&$(!0);let r=await at();if(r.length===0){n.json?s.json([]):s.info("No background jobs.");return}if(n.json)s.json(r.map(o=>({job_id:o.id,instance:o.instance,command:o.command,status:o.status,started_at:o.startedAt,exit_code:o.exitCode})));else for(let o of r){let c=o.status==="running"?"\u27F3 running":o.status==="done"?"\u2713 done":`\u2717 ${o.status}`;s.info(` ${o.id} ${c} ${o.instance} ${o.command}`)}}),t.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&$(!0);let o=await U(n);if(o.status!=="running"){r.json?s.json({job_id:n,status:o.status,message:"Job is not running"}):s.info(`Job ${n} is not running (status: ${o.status})`);return}let c=await _(o.instance);await I({instanceName:o.instance,ip:c.ip,command:`kill -- -${o.pid} 2>/dev/null || kill ${o.pid} 2>/dev/null || true`,timeoutMs:1e4}),o.status="cancelled",o.completedAt=new Date().toISOString(),await z(o),r.json?s.json({job_id:n,status:"cancelled"}):s.info(`Job ${n} cancelled.`)}),t.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&&$(!0);let o=await U(n),c=await _(o.instance),i=`/root/.gibil-jobs/${n}.log`,l=r.follow?`tail -f ${i}`:`cat ${i} 2>/dev/null || echo '(no output yet)'`,a=r.follow?3e5:1e4,u=await I({instanceName:o.instance,ip:c.ip,command:l,stream:!r.json,timeoutMs:a});r.json&&s.json({job_id:n,stdout:u.stdout})})}function Mn(){try{let e=St("git config user.name",{encoding:"utf-8"}).trim(),t=St("git config user.email",{encoding:"utf-8"}).trim();if(!e||!t)return;let n;try{if(St("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let o=St("git config user.signingkey",{encoding:"utf-8"}).trim();if(o)try{n=Hn(o,"utf-8").trim()}catch{(o.startsWith("ssh-")||o.startsWith("key::"))&&(n=o.replace(/^key::/,""))}}}catch{}return{name:e,email:t,signingKey:n}}catch{return}}async function ct(e,t){if(e)return e;if(t)return _(t);let r=(await Y()).filter(o=>new Date<new Date(o.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(o=>o.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function B(e,t,n=3e4){return I({instanceName:e.name,ip:e.ip,command:t,stream:!1,timeoutMs:n})}function O(e){return`'${e.replace(/'/g,"'\\''")}'`}async function Pe(e){let t=null;if(e&&(t=await _(e),t.gitIdentity)){let{name:i,email:l,signingKey:a}=t.gitIdentity,u=[`git config --global user.name ${O(i)}`,`git config --global user.email ${O(l)}`];a&&u.push("git config --global gpg.format ssh",`git config --global user.signingkey ${O("key::"+a)}`,"git config --global commit.gpgsign true"),B(t,u.join(" && ")).catch(()=>{})}let n=e?`gibil-${e}`:"gibil",r=new On({name:n,version:"0.4.0"});t||(r.tool("create_server","Forge a new ephemeral server. Clones repo, installs deps, and waits until fully provisioned before returning. Returns the server name, IP, and provisioning status.",{name:y.string().optional().describe("Server name (auto-generated if omitted)"),repo:y.string().optional().describe("Git repo URL to clone on boot"),ttl:y.number().optional().describe("Auto-destroy after N minutes (default: 60)"),server_type:y.string().optional().describe("Hetzner server type (default: auto-detected)"),location:y.string().optional().describe("Hetzner datacenter (default: auto-detected)"),env:y.record(y.string(),y.string()).optional().describe("Environment variables to set on the server")},async({name:i,repo:l,ttl:a,server_type:u,location:g,env:f})=>{try{let d=i??wt(),m=a??60,h=await T.create(),b=await mt(d),C=await h.createSSHKey(`gibil-${d}`,b.publicKey),w=Mn(),x=f?{env:f}:void 0,kt=ft({repo:l,config:x,ttlMinutes:m,githubToken:process.env.GITHUB_TOKEN,gitIdentity:w}),lt=await h.createServer(d,C.id,kt,u,g),N=(await h.waitForReady(lt.id)).public_net.ipv4.ip,zt=new Date,He={name:d,serverId:lt.id,ip:N,sshKeyId:C.id,keyPath:S.privateKey(d),status:"running",createdAt:zt.toISOString(),ttlMinutes:m,expiresAt:new Date(zt.getTime()+m*6e4).toISOString(),repo:l,gitIdentity:w};await W(He),await gt(d,N);let _t="ready";if(l||x){let Me=Date.now(),Ut=!1;for(;Date.now()-Me<36e4;){try{if((await I({instanceName:d,ip:N,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){Ut=!0;break}}catch{}await new Promise(jt=>setTimeout(jt,5e3))}if(!Ut){_t="timeout";try{_t=`timeout \u2014 cloud-init log:
20
- ${(await I({instanceName:d,ip:N,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:d,ip:N,ttl_minutes:m,status:"running",provisioning:_t},null,2)}]}}catch(d){return{content:[{type:"text",text:`Failed to create server: ${d instanceof Error?d.message:String(d)}`}],isError:!0}}}),r.tool("destroy_server","Burn a server. Deletes the server, SSH keys, and local metadata.",{name:y.string().describe("Name of the server to destroy")},async({name:i})=>{try{let l=await yt(i),a=await T.create();await a.destroyServer(l.serverId).catch(()=>{}),await a.deleteSSHKey(l.sshKeyId).catch(()=>{}),await Q(i).catch(()=>{});let{deleteJobsByInstance:u}=await Promise.resolve().then(()=>(tt(),be));return await u(i).catch(()=>{}),await bt(i),{content:[{type:"text",text:`Server "${i}" destroyed.`}]}}catch(l){return{content:[{type:"text",text:`Failed to destroy: ${l instanceof Error?l.message:String(l)}`}],isError:!0}}}),r.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let l=(await Y()).map(a=>({name:a.name,ip:a.ip,status:a.status,ttl_remaining_seconds:Math.max(0,Math.floor((new Date(a.expiresAt).getTime()-Date.now())/1e3)),repo:a.repo}));return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),r.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer.",{name:y.string().describe("Server name"),ttl:y.number().describe("New TTL in minutes from now")},async({name:i,ttl:l})=>{try{let a=await _(i),u=await B(a,`pkill -f 'sleep.*shutdown' || true && (sleep ${l*60} && shutdown -h now) &`);return u.exitCode!==0?{content:[{type:"text",text:`Failed to extend TTL: ${u.stderr}`}],isError:!0}:(a.ttlMinutes=l,a.expiresAt=new Date(Date.now()+l*6e4).toISOString(),await W(a),{content:[{type:"text",text:`Server "${i}" TTL extended to ${l} minutes.`}]})}catch(a){return{content:[{type:"text",text:`Failed to extend: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}}));let o=y.string().optional().describe("Server name (auto-selects if only one is running)");r.tool("vm_bash","Run a shell command on a remote server. Use for: builds, tests, git, package managers, any shell operation. Set background=true for long-running commands \u2014 returns a job ID you can poll with vm_job_status.",{command:y.string().describe("Shell command to execute"),working_dir:y.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:y.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:y.boolean().optional().describe("Run in background, return job ID for polling"),server:o},async i=>{let l=await ct(t,i.server),u=`cd ${i.working_dir??"/root/project"} 2>/dev/null || cd /root && ${i.command}`;if(i.background){let d=vt(),m="/root/.gibil-jobs",h=`${m}/${d}.log`,b=`${m}/${d}.exit`,C=`${m}/${d}.pid`,w=`${m}/${d}.sh`,x=["#!/bin/bash",`nohup bash -c '${u.replace(/'/g,"'\\''")}' > ${h} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${C}`,`(wait $BGPID 2>/dev/null; echo $? > ${b}) &`,"echo $BGPID"].join(`
21
- `),kt=Buffer.from(x).toString("base64"),lt=`mkdir -p ${m} && echo '${kt}' | base64 -d > ${w} && chmod +x ${w} && bash ${w}`,Jt=await B(l,lt,1e4),N=parseInt(Jt.stdout.trim(),10);return isNaN(N)?{content:[{type:"text",text:"Failed to start background job \u2014 could not capture PID"}],isError:!0}:(await z({id:d,instance:l.name,command:i.command,pid:N,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:d,instance:l.name,status:"running",pid:N},null,2)}]})}let g=await B(l,u,i.timeout_ms??12e4);return{content:[{type:"text",text:[g.stdout,g.stderr].filter(Boolean).join(`
22
- `)||"(no output)"}],isError:g.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:y.string().describe("Job ID returned by vm_bash with background=true")},async i=>{try{let l=await U(i.job_id),a=await Gt(i.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:i.job_id,instance:l.instance,command:l.command,status:a.status,exit_code:a.exitCode,started_at:l.startedAt,duration_s:a.durationS,...a.stdout!==void 0?{stdout:a.stdout}:{}},null,2)}],isError:a.status==="failed"}}catch(l){return{content:[{type:"text",text:`Error: ${l instanceof Error?l.message:String(l)}`}],isError:!0}}}),r.tool("vm_job_list","List all background jobs across all servers.",{},async()=>{let l=(await at()).map(a=>({job_id:a.id,instance:a.instance,command:a.command,status:a.status,started_at:a.startedAt,exit_code:a.exitCode}));return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),r.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:y.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:y.number().optional().describe("Start at line N (1-based)"),limit:y.number().optional().describe("Max lines to return"),server:o},async i=>{let l=await ct(t,i.server),a=O(i.path),u=`cat -n ${a}`;i.offset&&i.limit?u=`sed -n '${i.offset},${i.offset+i.limit-1}p' ${a} | cat -n`:i.offset?u=`tail -n +${i.offset} ${a} | cat -n`:i.limit&&(u=`head -n ${i.limit} ${a} | cat -n`);let g=await B(l,u);return g.exitCode!==0?{content:[{type:"text",text:`Error: ${g.stderr}`}],isError:!0}:{content:[{type:"text",text:g.stdout}]}}),r.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:y.string().describe("Absolute path on the server"),content:y.string().describe("File content to write"),server:o},async i=>{let l=await ct(t,i.server),a=Buffer.from(i.content).toString("base64"),u=O(i.path),g=`mkdir -p "$(dirname ${u})" && echo '${a}' | base64 -d > ${u}`,f=await B(l,g);return f.exitCode!==0?{content:[{type:"text",text:`Error: ${f.stderr}`}],isError:!0}:{content:[{type:"text",text:`Wrote ${i.path}`}]}}),r.tool("vm_ls","List files and directories on a remote server.",{path:y.string().optional().describe("Directory path (default: /root/project)"),glob:y.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:o},async i=>{let l=await ct(t,i.server),a=i.path??"/root/project",u;i.glob?u=`cd ${O(a)} && find . -path './${i.glob}' -type f 2>/dev/null | sort | head -200`:u=`ls -la ${O(a)}`;let g=await B(l,u);return g.exitCode!==0?{content:[{type:"text",text:`Error: ${g.stderr}`}],isError:!0}:{content:[{type:"text",text:g.stdout}]}}),r.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:y.string().describe("Regex pattern to search for"),path:y.string().optional().describe("Directory or file to search (default: /root/project)"),include:y.string().optional().describe("File glob to include (e.g. '*.ts')"),server:o},async i=>{let l=await ct(t,i.server),a=i.path??"/root/project",u=O(i.pattern),g=O(a),f;if(i.include){let h=O(i.include);f=`cd ${g} && (rg -n --glob ${h} ${u} 2>/dev/null || grep -rn --include=${h} ${u} .) | head -100`}else f=`cd ${g} && (rg -n ${u} 2>/dev/null || grep -rn ${u} .) | head -100`;return{content:[{type:"text",text:(await B(l,f)).stdout||"(no matches)"}]}});let c=new Nn;await r.connect(c)}function Ae(e){e.command("mcp [name]").description("Start an MCP server (used by Claude Code, Cursor, and other agents)").action(async t=>{try{await Pe(t)}catch(n){s.error(n instanceof Error?n.message:String(n)),process.exit(1)}})}J();import{createInterface as Rn}from"readline";import{execSync as Kn}from"child_process";import{existsSync as Dn,readFileSync as Ln,writeFileSync as Gn,mkdirSync as Fn}from"fs";import{join as Ft}from"path";import{homedir as Jn}from"os";M();function It(e){let t=Rn({input:process.stdin,output:process.stderr});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}async function zn(){let e=!!await Ot(),t=!!await P();return{hetzner:e,apiKey:t}}function Te(e){e.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").action(async t=>{console.error(Yt);let n=await zn();if(n.hetzner&&!t.force){s.info(`${H} Already configured.`),n.apiKey?(s.detail("Hetzner",q("connected")),s.detail("Gibil API",q("connected"))):(s.detail("Hetzner",q("connected")),s.detail("Gibil API",p("not configured (optional)"))),s.info(""),s.info(` Run ${v("gibil init --force")} to reconfigure.`),s.info(` Run ${v("gibil create")} to forge a server.`);return}s.info(""),s.info(v("Step 1: Hetzner API Token")),s.info(p(" Your servers run on Hetzner Cloud. You need an API token.")),s.info(p(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),s.info("");let r=await It(" Hetzner API token: ");r||(s.error("No token provided. Run gibil init again when ready."),process.exit(1));let o=s.spin("Verifying Hetzner token...");try{let m=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();m.error&&(o.fail(`Invalid token: ${m.error.message}`),process.exit(1)),o.succeed("Hetzner token verified")}catch{o.fail("Could not reach Hetzner API. Check your network."),process.exit(1)}await rt(r);let c=s.spin("Detecting available server types..."),i="cax11",l="fsn1",a=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let d of a)try{let h=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:d.type,image:"ubuntu-24.04",location:d.location,start_after_create:!1})})).json();if(h.server){await fetch(`https://api.hetzner.cloud/v1/servers/${h.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${r}`}}),i=d.type,l=d.location;break}}catch{}await Nt(i,l),c.succeed(`Default server type: ${i} (${l})`),s.info(""),s.info(v("Step 2: Gibil API Key")+p(" (optional)")),s.info(p(" For usage tracking and plan limits.")),s.info(p(" Skip this if you just want to use your own Hetzner token.")),s.info("");let u=await It(" Gibil API key (or press Enter to skip): ");if(u)if(!u.startsWith("pk_"))s.warn('API keys start with "pk_". Skipping.');else{let d=s.spin("Verifying API key...");try{let m=await F(u);await ot(u),d.succeed(`Logged in as ${m.user.email} (${m.user.plan})`)}catch(m){d.fail(`Could not verify key: ${m instanceof Error?m.message:String(m)}`),s.info(p(" Continuing without Gibil API. You can add it later with: gibil auth login"))}}else s.info(p(" Skipped. You can add it later with: gibil auth login"));if(s.info(""),s.info(k.initComplete),s.info(""),s.info(p(" Quick start:")),s.info(` ${v("gibil create")} ${p("Forge a server")}`),s.info(` ${v("gibil create --repo github.com/you/project")} ${p("Clone a repo on boot")}`),s.info(` ${v("gibil ssh <name>")} ${p("Connect to it")}`),s.info(` ${v("gibil destroy <name>")} ${p("Burn it down")}`),s.info(""),(await It(" Install the gibil agent skill? (Y/n): ")).toLowerCase()!=="n"){let d=s.spin("Installing gibil skill...");try{Kn("npx -y skills add https://github.com/AlexikM/gibil-skills --skill gibil -y -g",{stdio:"pipe",timeout:3e4}),d.succeed("Gibil skill installed for your AI agents")}catch{d.fail("Could not install skill automatically"),s.info(p(" Install manually: npx skills add https://github.com/AlexikM/gibil-skills --skill gibil"))}}else s.info(p(" Skipped. Install later: npx skills add https://github.com/AlexikM/gibil-skills --skill gibil"));if(s.info(""),(await It(" Configure gibil MCP server for Claude Code? (Y/n): ")).toLowerCase()!=="n"){let d=s.spin("Configuring MCP server...");try{let m=Ft(Jn(),".claude"),h=Ft(m,".mcp.json");Fn(m,{recursive:!0});let b={};try{b=JSON.parse(Ln(h,"utf-8"))}catch{}b.mcpServers||(b.mcpServers={}),b.mcpServers.gibil={command:"gibil",args:["mcp"]},Gn(h,JSON.stringify(b,null,2)+`
23
- `),d.succeed("MCP server configured \u2014 restart Claude Code to activate")}catch(m){d.fail(`Could not configure MCP: ${m instanceof Error?m.message:String(m)}`),s.info(p(' Add manually to ~/.claude/.mcp.json: { "mcpServers": { "gibil": { "command": "gibil", "args": ["mcp"] } } }'))}}else s.info(p(' Skipped. Add manually to ~/.claude/.mcp.json: { "mcpServers": { "gibil": { "command": "gibil", "args": ["mcp"] } } }'))})}async function Oe(){if(process.env.HETZNER_API_TOKEN)return!1;let e=Ft(S.root,"config.json");return!Dn(e)}try{await import("dotenv/config")}catch{}var Vn=Wn(qn(import.meta.url)),Ne={version:"0.0.0"};for(let e of["../package.json","../../package.json"])try{Ne=JSON.parse(Bn(Yn(Vn,e),"utf-8"));break}catch{}var j=new Un;j.name("gibil").description("Ephemeral dev compute for humans and AI agents").version(`${Ne.version} ${ut}`,"-v, --version").addHelpText("before",`
21
+ ${i.length} destroyed, ${l.length} failed`)}else{t||(s.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1));let r=await A.create();await we(r,t),n.json&&s.json({destroyed:[t]})}})}function $e(e){e.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async t=>{t.json&&$(!0);let n=await V();if(n.length===0){t.json?s.json({instances:[]}):s.info(k.noInstances);return}let r=n.map(o=>{let c=Math.max(0,Math.floor((new Date(o.expiresAt).getTime()-Date.now())/1e3));return{name:o.name,ip:o.ip,ssh:`ssh -i ${o.keyPath} -o StrictHostKeyChecking=no root@${o.ip}`,status:o.status,ttl_remaining:c,created_at:o.createdAt,fleet_id:o.fleetId}});if(t.json){s.json({instances:r});return}s.info(p(`${"NAME".padEnd(30)} ${"IP".padEnd(18)} ${"STATUS".padEnd(12)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),s.info(p("\u2500".repeat(80)));for(let o of r){let c=xe(o.ttl_remaining),i=Cn(o.created_at),l=o.name.padEnd(30),a=o.status.padEnd(12),u=c.padEnd(10),m=i.padEnd(10),g=o.status==="running"?q(a):D(a),d=o.ttl_remaining<=300?D(u):u;s.info(`${b(l)} ${o.ip.padEnd(18)} ${g} ${d} ${p(m)}`)}s.info(`
22
+ ${p(`${r.length} server(s)`)}`)})}function xe(e){if(e<=0)return"expired";let t=Math.floor(e/60),n=e%60;return t>=60?`${Math.floor(t/60)}h ${t%60}m`:`${t}m ${n}s`}function Cn(e){let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/1e3);return xe(n)}function Se(e){e.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <minutes>","New TTL in minutes from now").option("--json","Output result as JSON").action(async(t,n)=>{n.json&&$(!0);let r=await _(t),o=parseInt(n.ttl,10);(isNaN(o)||o<=0)&&(s.error("TTL must be a positive number of minutes"),process.exit(1)),await S({instanceName:t,ip:r.ip,command:["pkill -f 'sleep.*shutdown' || true",`(sleep ${o*60} && shutdown -h now) &`].join(" && ")});let c=new Date(Date.now()+o*6e4).toISOString();r.ttlMinutes=o,r.expiresAt=c,await W(r),n.json?s.json({name:r.name,ttl_minutes:o,expires_at:c}):s.info(`\u2713 Extended "${t}" TTL to ${o} minutes (expires ${c})`)})}import{readFile as En}from"fs/promises";import{randomBytes as Pn}from"crypto";function Ie(e){e.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(t,n)=>{n.json&&$(!0);let r=await _(t),o=await En(n.script,"utf-8");s.info(`Uploading and running script "${n.script}" on "${t}"...`);let c=Buffer.from(o).toString("base64"),i=`/tmp/gibil-script-${Pn(4).toString("hex")}.sh`,l=await S({instanceName:t,ip:r.ip,command:`echo '${c}' | base64 -d > ${i} && chmod +x ${i} && ${i}; EXIT=$?; rm -f ${i}; exit $EXIT`,stream:!n.json});n.json?s.json({instance:t,script:n.script,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&s.error(`Script exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}J();import{createInterface as Tn}from"readline";function ke(e){let t=Tn({input:process.stdin,output:process.stderr});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}function _e(e){let t=e.command("auth").description("Manage authentication");t.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&&$(!0);let r=n.key??process.env.GIBIL_API_KEY;r||(r=await ke("Enter your API key: ")),r||(s.error("No API key provided."),process.exit(1)),r.startsWith("pk_")||(s.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),s.info("Verifying API key...");try{let o=await Q(r);await Pt(r),n.json?s.json({authenticated:!0,email:o.user.email,plan:o.user.plan}):(s.info(k.authSuccess),s.detail("Email",o.user.email),s.detail("Plan",o.user.plan),s.detail("Limits",`${o.limits.max_concurrent} concurrent servers, ${o.limits.remaining_hours}h remaining`))}catch(o){s.error(o instanceof Error?o.message:String(o)),process.exit(1)}}),t.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 ke("Enter your Hetzner API token: ")),r||(s.error("No token provided."),process.exit(1));try{let c=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();c.error&&(s.error(`Invalid token: ${c.error.message}`),process.exit(1))}catch{s.error("Could not verify token with Hetzner API."),process.exit(1)}await it(r),s.success("Hetzner token saved to ~/.gibil/config.json")}),t.command("logout").description("Clear stored API key").action(async()=>{await Tt(),s.info(k.authLogout)}),t.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&$(!0);let r=await E();if(!r){n.json?s.json({authenticated:!1}):s.info(`Not logged in. Run ${b("gibil auth login")} to authenticate.`);return}try{let o=await Q(r);n.json?s.json({authenticated:!0,email:o.user.email,plan:o.user.plan,limits:o.limits}):(s.success(`Authenticated as ${o.user.email}`),s.detail("Plan",o.user.plan),s.detail("Concurrent servers",String(o.limits.max_concurrent)),s.detail("Hours remaining",String(o.limits.remaining_hours)))}catch{n.json?s.json({authenticated:!1,error:"Key verification failed"}):s.error(`Stored API key is invalid. Run ${b("gibil auth login")} to re-authenticate.`)}})}J();function je(e){e.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async t=>{t.json&&$(!0);let n=await E();n||(s.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let r=await Nt(n);if(t.json)s.json(r);else{let o=Math.round(r.vm_hours_used/r.vm_hours_limit*100);s.info(`Plan: ${r.plan}`),s.info(`VM hours: ${r.vm_hours_used.toFixed(1)} / ${r.vm_hours_limit}h (${o}%)`),s.info(`Active instances: ${r.active_instances} / ${r.max_concurrent}`),o>80&&s.warn("Running low on hours. Upgrade at https://gibil.dev/pricing")}}catch(r){s.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}import{execSync as Rn}from"child_process";import{McpServer as An}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as On}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as h}from"zod";M();import{execSync as St}from"child_process";import{readFileSync as Nn}from"fs";nt();nt();async function Lt(e){let t=await U(e);if(t.status!=="running")return{status:t.status,exitCode:t.exitCode};let n=await _(t.instance),r="/root/.gibil-jobs",o=`${r}/${e}.exit`,c=`${r}/${e}.log`,l=(await S({instanceName:t.instance,ip:n.ip,command:`test -f ${o} && cat ${o} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING")return{status:"running"};let a=parseInt(l,10),u=await S({instanceName:t.instance,ip:n.ip,command:`cat ${c} 2>/dev/null || echo ''`,timeoutMs:1e4}),m=a===0?"done":"failed",g=new Date,d=Math.round((g.getTime()-new Date(t.startedAt).getTime())/1e3);return t.status=m,t.exitCode=a,t.completedAt=g.toISOString(),await z(t),{status:m,exitCode:a,stdout:u.stdout,durationS:d}}function Ce(e){let t=e.command("job").description("Manage background jobs");t.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&$(!0);let o=await U(n),c=await Lt(n);r.json?s.json({job_id:n,instance:o.instance,command:o.command,status:c.status,exit_code:c.exitCode,started_at:o.startedAt,duration_s:c.durationS,...c.stdout!==void 0?{stdout:c.stdout}:{}}):c.status==="running"?(s.info(`Job ${n} is still running on "${o.instance}"`),s.info(` Command: ${o.command}`),s.info(` Started: ${o.startedAt}`)):(s.info(`Job ${n}: ${c.status} (exit code ${c.exitCode}, ${c.durationS}s)`),c.stdout&&process.stdout.write(c.stdout))}),t.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&$(!0);let r=await ct();if(r.length===0){n.json?s.json([]):s.info("No background jobs.");return}if(n.json)s.json(r.map(o=>({job_id:o.id,instance:o.instance,command:o.command,status:o.status,started_at:o.startedAt,exit_code:o.exitCode})));else for(let o of r){let c=o.status==="running"?"\u27F3 running":o.status==="done"?"\u2713 done":`\u2717 ${o.status}`;s.info(` ${o.id} ${c} ${o.instance} ${o.command}`)}}),t.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&$(!0);let o=await U(n);if(o.status!=="running"){r.json?s.json({job_id:n,status:o.status,message:"Job is not running"}):s.info(`Job ${n} is not running (status: ${o.status})`);return}let c=await _(o.instance);await S({instanceName:o.instance,ip:c.ip,command:`kill -- -${o.pid} 2>/dev/null || kill ${o.pid} 2>/dev/null || true`,timeoutMs:1e4}),o.status="cancelled",o.completedAt=new Date().toISOString(),await z(o),r.json?s.json({job_id:n,status:"cancelled"}):s.info(`Job ${n} cancelled.`)}),t.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&&$(!0);let o=await U(n),c=await _(o.instance),i=`/root/.gibil-jobs/${n}.log`,l=r.follow?`tail -f ${i}`:`cat ${i} 2>/dev/null || echo '(no output yet)'`,a=r.follow?3e5:1e4,u=await S({instanceName:o.instance,ip:c.ip,command:l,stream:!r.json,timeoutMs:a});r.json&&s.json({job_id:n,stdout:u.stdout})})}function Hn(){try{let e=St("git config user.name",{encoding:"utf-8"}).trim(),t=St("git config user.email",{encoding:"utf-8"}).trim();if(!e||!t)return;let n;try{if(St("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let o=St("git config user.signingkey",{encoding:"utf-8"}).trim();if(o)try{n=Nn(o,"utf-8").trim()}catch{(o.startsWith("ssh-")||o.startsWith("key::"))&&(n=o.replace(/^key::/,""))}}}catch{}return{name:e,email:t,signingKey:n}}catch{return}}async function lt(e,t){if(e)return e;if(t)return _(t);let r=(await V()).filter(o=>new Date<new Date(o.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(o=>o.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function B(e,t,n=3e4){return S({instanceName:e.name,ip:e.ip,command:t,stream:!1,timeoutMs:n})}function O(e){return`'${e.replace(/'/g,"'\\''")}'`}async function Ee(e){let t=null;if(e&&(t=await _(e),t.gitIdentity)){let{name:i,email:l,signingKey:a}=t.gitIdentity,u=[`git config --global user.name ${O(i)}`,`git config --global user.email ${O(l)}`];a&&u.push("git config --global gpg.format ssh",`git config --global user.signingkey ${O("key::"+a)}`,"git config --global commit.gpgsign true"),B(t,u.join(" && ")).catch(()=>{})}let n=e?`gibil-${e}`:"gibil",r=new An({name:n,version:"0.4.0"});t||(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. 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:h.string().optional().describe("Server name (auto-generated if omitted)"),repo:h.string().optional().describe("Git repo URL to clone on boot"),ttl:h.number().optional().describe("Auto-destroy after N minutes (default: 60)"),server_type:h.string().optional().describe("Hetzner server type (default: auto-detected)"),location:h.string().optional().describe("Hetzner datacenter (default: auto-detected)"),env:h.record(h.string(),h.string()).optional().describe("Environment variables to set on the server")},async({name:i,repo:l,ttl:a,server_type:u,location:m,env:g})=>{try{let d=i??wt(),f=a??60,w=await A.create(),I=await pt(d),T=await w.createSSHKey(`gibil-${d}`,I.publicKey),y=Hn(),v=g?{env:g}:void 0,Z=ft({repo:l,config:v,ttlMinutes:f,githubToken:process.env.GITHUB_TOKEN,gitIdentity:y}),N=await w.createServer(d,T.id,Z,u,m),H=(await w.waitForReady(N.id)).public_net.ipv4.ip,Jt=new Date,Ne={name:d,serverId:N.id,ip:H,sshKeyId:T.id,keyPath:x.privateKey(d),status:"running",createdAt:Jt.toISOString(),ttlMinutes:f,expiresAt:new Date(Jt.getTime()+f*6e4).toISOString(),repo:l,gitIdentity:y};await W(Ne),await gt(d,H);let It="ready";if(l||v){let He=Date.now(),zt=!1;for(;Date.now()-He<36e4;){try{if((await S({instanceName:d,ip:H,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){zt=!0;break}}catch{}await new Promise(kt=>setTimeout(kt,5e3))}if(!zt){It="timeout";try{It=`timeout \u2014 cloud-init log:
23
+ ${(await S({instanceName:d,ip:H,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:d,ip:H,ttl_minutes:f,status:"running",provisioning:It,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(d){return{content:[{type:"text",text:`Failed to create server: ${d instanceof Error?d.message:String(d)}`}],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:h.string().describe("Name of the server to destroy")},async({name:i})=>{try{let l=await yt(i),a=await A.create();await a.destroyServer(l.serverId).catch(()=>{}),await a.deleteSSHKey(l.sshKeyId).catch(()=>{}),await et(i).catch(()=>{});let{deleteJobsByInstance:u}=await Promise.resolve().then(()=>(nt(),ye));return await u(i).catch(()=>{}),await bt(i),{content:[{type:"text",text:`Server "${i}" destroyed.`}]}}catch(l){return{content:[{type:"text",text:`Failed to destroy: ${l instanceof Error?l.message:String(l)}`}],isError:!0}}}),r.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let i=await V();if(i.length===0)return{content:[{type:"text",text:"No servers running. Use create_server to forge one."}]};let l=i.map(a=>{let u=Math.max(0,Math.floor((new Date(a.expiresAt).getTime()-Date.now())/1e3));return{name:a.name,ip:a.ip,status:a.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:a.repo}});return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),r.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer.",{name:h.string().describe("Server name"),ttl:h.number().describe("New TTL in minutes from now")},async({name:i,ttl:l})=>{try{let a=await _(i),u=await B(a,`pkill -f 'sleep.*shutdown' || true && (sleep ${l*60} && shutdown -h now) &`);return u.exitCode!==0?{content:[{type:"text",text:`Failed to extend TTL: ${u.stderr}`}],isError:!0}:(a.ttlMinutes=l,a.expiresAt=new Date(Date.now()+l*6e4).toISOString(),await W(a),{content:[{type:"text",text:`Server "${i}" TTL extended to ${l} minutes.`}]})}catch(a){return{content:[{type:"text",text:`Failed to extend: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}}));let o=h.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:h.string().describe("Shell command to execute"),working_dir:h.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:h.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:h.boolean().optional().describe("Run in background, return job ID for polling"),server:o},async i=>{let l=await lt(t,i.server),u=`cd ${i.working_dir??"/root/project"} 2>/dev/null || cd /root && ${i.command}`;if(i.background){let d=vt(),f="/root/.gibil-jobs",w=`${f}/${d}.log`,I=`${f}/${d}.exit`,T=`${f}/${d}.pid`,y=`${f}/${d}.sh`,v=["#!/bin/bash",`nohup bash -c '${u.replace(/'/g,"'\\''")}' > ${w} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${T}`,`(wait $BGPID 2>/dev/null; echo $? > ${I}) &`,"echo $BGPID"].join(`
24
+ `),Z=Buffer.from(v).toString("base64"),N=`mkdir -p ${f} && echo '${Z}' | base64 -d > ${y} && chmod +x ${y} && bash ${y}`,Gt=await B(l,N,1e4),H=parseInt(Gt.stdout.trim(),10);return isNaN(H)?{content:[{type:"text",text:"Failed to start background job \u2014 could not capture PID"}],isError:!0}:(await z({id:d,instance:l.name,command:i.command,pid:H,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:d,instance:l.name,status:"running",pid:H,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let m=await B(l,u,i.timeout_ms??12e4);return{content:[{type:"text",text:[m.stdout,m.stderr].filter(Boolean).join(`
25
+ `)||"(no output)"}],isError:m.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:h.string().describe("Job ID returned by vm_bash with background=true")},async i=>{try{let l=await U(i.job_id),a=await Lt(i.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:i.job_id,instance:l.instance,command:l.command,status:a.status,exit_code:a.exitCode,started_at:l.startedAt,duration_s:a.durationS,...a.stdout!==void 0?{stdout:a.stdout}:{}},null,2)}],isError:a.status==="failed"}}catch(l){return{content:[{type:"text",text:`Error: ${l instanceof Error?l.message:String(l)}`}],isError:!0}}}),r.tool("vm_job_list","List all background jobs across all servers.",{},async()=>{let l=(await ct()).map(a=>({job_id:a.id,instance:a.instance,command:a.command,status:a.status,started_at:a.startedAt,exit_code:a.exitCode}));return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}),r.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:h.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:h.number().optional().describe("Start at line N (1-based)"),limit:h.number().optional().describe("Max lines to return"),server:o},async i=>{let l=await lt(t,i.server),a=O(i.path),u=`cat -n ${a}`;i.offset&&i.limit?u=`sed -n '${i.offset},${i.offset+i.limit-1}p' ${a} | cat -n`:i.offset?u=`tail -n +${i.offset} ${a} | cat -n`:i.limit&&(u=`head -n ${i.limit} ${a} | cat -n`);let m=await B(l,u);return m.exitCode!==0?{content:[{type:"text",text:`Error: ${m.stderr}`}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),r.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:h.string().describe("Absolute path on the server"),content:h.string().describe("File content to write"),server:o},async i=>{let l=await lt(t,i.server),a=Buffer.from(i.content).toString("base64"),u=O(i.path),m=`mkdir -p "$(dirname ${u})" && echo '${a}' | base64 -d > ${u}`,g=await B(l,m);return g.exitCode!==0?{content:[{type:"text",text:`Error: ${g.stderr}`}],isError:!0}:{content:[{type:"text",text:`Wrote ${i.path}`}]}}),r.tool("vm_ls","List files and directories on a remote server.",{path:h.string().optional().describe("Directory path (default: /root/project)"),glob:h.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:o},async i=>{let l=await lt(t,i.server),a=i.path??"/root/project",u;i.glob?u=`cd ${O(a)} && find . -path './${i.glob}' -type f 2>/dev/null | sort | head -200`:u=`ls -la ${O(a)}`;let m=await B(l,u);return m.exitCode!==0?{content:[{type:"text",text:`Error: ${m.stderr}`}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),r.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:h.string().describe("Regex pattern to search for"),path:h.string().optional().describe("Directory or file to search (default: /root/project)"),include:h.string().optional().describe("File glob to include (e.g. '*.ts')"),server:o},async i=>{let l=await lt(t,i.server),a=i.path??"/root/project",u=O(i.pattern),m=O(a),g;if(i.include){let w=O(i.include);g=`cd ${m} && (rg -n --glob ${w} ${u} 2>/dev/null || grep -rn --include=${w} ${u} .) | head -100`}else g=`cd ${m} && (rg -n ${u} 2>/dev/null || grep -rn ${u} .) | head -100`;return{content:[{type:"text",text:(await B(l,g)).stdout||"(no matches)"}]}});let c=new On;await r.connect(c)}function Mn(){try{let e=Rn("which gibil",{encoding:"utf-8"}).trim();if(e)return e}catch{}return process.argv[1]??"gibil"}function Pe(e){e.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(t,n)=>{if(n.printConfig){let o={mcpServers:{gibil:{command:Mn(),args:["mcp"]}}};console.log(JSON.stringify(o,null,2)),console.error(""),console.error("Copy this to one of:"),console.error(" ~/.claude/mcp.json (Claude Code CLI)"),console.error(" .claude/mcp.json (project-level)"),console.error(" Claude Code settings (VS Code extension)");return}try{await Ee(t)}catch(r){s.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}J();import{createInterface as Dn}from"readline";import{execSync as Kn}from"child_process";import{existsSync as Ln,readFileSync as Fn,writeFileSync as Gn,mkdirSync as Jn}from"fs";import{join as Ft}from"path";import{homedir as zn}from"os";M();function Un(e){let t=Dn({input:process.stdin,output:process.stderr});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}async function Bn(){let e=!!await At(),t=!!await E();return{hetzner:e,apiKey:t}}function Te(e){e.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").action(async t=>{console.error(Wt);let n=await Bn();if(n.hetzner&&!t.force){s.info(`${R} Already configured.`),n.apiKey?(s.detail("Hetzner",q("connected")),s.detail("Gibil API",q("connected"))):(s.detail("Hetzner",q("connected")),s.detail("Gibil API",p("not configured (optional)"))),s.info(""),s.info(` Run ${b("gibil init --force")} to reconfigure.`),s.info(` Run ${b("gibil create")} to forge a server.`);return}s.info(""),s.info(b("Step 1: Hetzner API Token")),s.info(p(" Your servers run on Hetzner Cloud. You need an API token.")),s.info(p(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),s.info("");let r=await Un(" Hetzner API token: ");r||(s.error("No token provided. Run gibil init again when ready."),process.exit(1));let o=s.spin("Verifying Hetzner token...");try{let g=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();g.error&&(o.fail(`Invalid token: ${g.error.message}`),process.exit(1)),o.succeed("Hetzner token verified")}catch{o.fail("Could not reach Hetzner API. Check your network."),process.exit(1)}await it(r);let c=s.spin("Detecting available server types..."),i="cax11",l="fsn1",a=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let m of a)try{let d=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:m.type,image:"ubuntu-24.04",location:m.location,start_after_create:!1})})).json();if(d.server){await fetch(`https://api.hetzner.cloud/v1/servers/${d.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${r}`}}),i=m.type,l=m.location;break}}catch{}await Ot(i,l),c.succeed(`Default server type: ${i} (${l})`);let u=s.spin("Configuring MCP for Claude Code...");try{let m=Ft(zn(),".claude"),g=Ft(m,".mcp.json");Jn(m,{recursive:!0});let d={};try{d=JSON.parse(Fn(g,"utf-8"))}catch{}d.mcpServers||(d.mcpServers={});let f="gibil";try{f=Kn("which gibil",{encoding:"utf-8"}).trim()||"gibil"}catch{}d.mcpServers.gibil={command:f,args:["mcp"]},Gn(g,JSON.stringify(d,null,2)+`
26
+ `),u.succeed("MCP configured for Claude Code")}catch{u.fail("Could not auto-configure MCP"),s.info(p(" Run gibil mcp --print-config for manual setup"))}s.info(""),s.info(k.initComplete),s.info(""),s.info(p(" Try it now:")),s.info(` ${b("gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10")}`),s.info(` ${b('gibil run demo "npm test"')}`),s.info(` ${b("gibil destroy demo")}`),s.info(""),s.info(p(" Later:")),s.info(` ${b("gibil auth login")} ${p("Add a Gibil API key (optional)")}`),s.info(` ${b("gibil mcp --print-config")} ${p("MCP setup for other editors")}`),s.info("")})}async function Ae(){if(process.env.HETZNER_API_TOKEN)return!1;let e=Ft(x.root,"config.json");return!Ln(e)}try{await import("dotenv/config")}catch{}var Xn=Yn(Vn(import.meta.url)),Oe={version:"0.0.0"};for(let e of["../package.json","../../package.json"])try{Oe=JSON.parse(Wn(Zn(Xn,e),"utf-8"));break}catch{}var j=new qn;j.name("gibil").description("Ephemeral dev compute for humans and AI agents").version(`${Oe.version} ${ut}`,"-v, --version").addHelpText("before",`
24
27
  ${Vt}
25
28
  `).addHelpText("after",`
26
29
  ${p("Docs:")} https://gibil.dev/docs
27
- `);Te(j);me(j);ge(j);we(j);$e(j);xe(j);Ie(j);ke(j);je(j);Ce(j);Ee(j);Ae(j);async function Zn(){let e=process.argv.slice(2);!(e.length===0||e.includes("init")||e.includes("auth")||e.includes("--help")||e.includes("-h")||e.includes("--version")||e.includes("-v")||e.includes("-V")||e.includes("mcp")||e.includes("ssh")||e.includes("run")||e.includes("exec")||e.includes("list")||e.includes("ls"))&&await Oe()&&(s.info(""),s.info(k.setupNeeded),s.info(""),process.exit(1));try{await j.parseAsync(process.argv)}catch(n){n instanceof Error&&s.error(n.message),process.exit(1)}}Zn();
30
+ `);Te(j);me(j);pe(j);be(j);ve(j);$e(j);Se(j);Ie(j);_e(j);je(j);Ce(j);Pe(j);async function Qn(){let e=process.argv.slice(2);!(e.length===0||e.includes("init")||e.includes("auth")||e.includes("--help")||e.includes("-h")||e.includes("--version")||e.includes("-v")||e.includes("-V")||e.includes("mcp")||e.includes("ssh")||e.includes("run")||e.includes("exec")||e.includes("list")||e.includes("ls"))&&await Ae()&&(s.info(""),s.info(k.setupNeeded),s.info(""),process.exit(1));try{await j.parseAsync(process.argv)}catch(n){n instanceof Error&&s.error(n.message),process.exit(1)}}Qn();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gibil",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Ephemeral dev compute for humans and AI agents",
5
5
  "homepage": "https://gibil.dev",
6
6
  "type": "module",