gibil 0.1.2 → 0.1.5

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 +15 -0
  2. package/dist/index.js +22 -5
  3. package/package.json +14 -5
package/README.md CHANGED
@@ -82,6 +82,21 @@ gibil run task "cd /root/project && pnpm install && pnpm test" --json
82
82
  gibil destroy task --json
83
83
  ```
84
84
 
85
+ ## Agent Skill
86
+
87
+ Teach your AI agent how to use gibil. Works with Claude Code, Cursor, Copilot, Gemini CLI, and [40+ other agents](https://agentskills.io).
88
+
89
+ ```bash
90
+ npx skills add https://github.com/AlexikM/gibil-skills --skill gibil
91
+ ```
92
+
93
+ ## Links
94
+
95
+ - [Documentation](https://gibil.dev/docs)
96
+ - [Quick Start](https://gibil.dev/docs/quickstart)
97
+ - [Recipes](https://gibil.dev/docs/recipes/code-test-loop)
98
+ - [Blog](https://gibil.dev/blog)
99
+
85
100
  ## License
86
101
 
87
102
  Proprietary. See [LICENSE](LICENSE) for details.
package/dist/index.js CHANGED
@@ -1,6 +1,23 @@
1
1
  #!/usr/bin/env node
2
- var Me=Object.defineProperty;var X=(t,e)=>()=>(t&&(e=t(t=0)),e);var Le=(t,e)=>{for(var n in e)Me(t,n,{get:e[n],enumerable:!0})};import{homedir as Ge}from"os";import{join as I}from"path";var k,f,C=X(()=>{"use strict";k=I(Ge(),".gibil"),f={root:k,instances:I(k,"instances"),keys:I(k,"keys"),instanceFile:t=>I(k,"instances",`${t}.json`),keyDir:t=>I(k,"keys",t),privateKey:t=>I(k,"keys",t,"id_ed25519"),publicKey:t=>I(k,"keys",t,"id_ed25519.pub")}});var ne={};Le(ne,{clearApiKey:()=>q,fetchUsage:()=>J,getApiKey:()=>x,getApiUrl:()=>Be,getApiUrlFromConfig:()=>M,getHetznerToken:()=>Je,saveApiKey:()=>F,saveHetznerToken:()=>B,trackUsage:()=>P,verifyApiKey:()=>j});import{readFile as ze,writeFile as De,mkdir as Ue}from"fs/promises";import{existsSync as Fe}from"fs";import{join as qe}from"path";async function _(){if(!Fe(D))return{};let t=await ze(D,"utf-8");return JSON.parse(t)}async function U(t){await Ue(f.root,{recursive:!0,mode:448}),await De(D,JSON.stringify(t,null,2),{mode:384})}async function F(t){let e=await _();e.api_key=t,await U(e)}async function x(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await _()).api_key??null}async function q(){let t=await _();delete t.api_key,await U(t)}function Be(){return process.env.GIBIL_API_URL??te}async function M(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await _()).api_url??te}async function B(t){let e=await _();e.hetzner_token=t,await U(e)}async function Je(){return process.env.HETZNER_API_TOKEN?process.env.HETZNER_API_TOKEN:(await _()).hetzner_token??null}async function j(t){let e=await M(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let i=await n.text();throw new Error(`API error (${n.status}): ${i}`)}return await n.json()}async function P(t,e,n,i){let r=await M(),a=await fetch(`${r}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:i})});if(a.status===429)throw new Error("Plan limit reached. Upgrade at https://gibil.dev/pricing");if(!a.ok){let o=await a.text();throw new Error(`Usage tracking failed (${a.status}): ${o}`)}}async function J(t){let e=await M(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let i=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${i}`)}return await n.json()}var D,te,A=X(()=>{"use strict";C();D=qe(f.root,"config.json"),te="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1"});import{Command as Et}from"commander";var Re="info",ee=!1,Q={debug:0,info:1,warn:2,error:3,silent:4};function m(t){ee=t}function K(t){return ee&&t!=="error"?!1:Q[t]>=Q[Re]}var s={debug(t,...e){K("debug")&&console.debug(`[debug] ${t}`,...e)},info(t,...e){K("info")&&console.log(t,...e)},warn(t,...e){K("warn")&&console.warn(`\u26A0 ${t}`,...e)},error(t,...e){K("error")&&console.error(`\u2716 ${t}`,...e)},json(t){console.log(JSON.stringify(t,null,2))}};var Ve="https://api.hetzner.cloud/v1",E=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(A(),ne)),i=e??await n();if(!i)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil auth setup' or set it in your environment.");return new t(i)}async request(e,n,i){let r=`${Ve}${n}`;s.debug(`${e} ${r}`);let a=await fetch(r,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:i?JSON.stringify(i):void 0});if(!a.ok){let o=await a.text(),c;try{c=JSON.parse(o).error?.message??o}catch{c=o}throw new Error(`Hetzner API error (${a.status}): ${c}`)}return a.status===204?{}:await a.json()}async createServer(e,n,i,r="cax11",a="fsn1"){let o={name:e,server_type:r,image:"ubuntu-24.04",ssh_keys:[n],labels:{gibil:"true","gibil-name":e},location:a};return i&&(o.user_data=i),(await this.request("POST","/servers",o)).server}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){return(await this.request("GET",`/servers/${e}`)).server}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers}async waitForReady(e,n=12e4){let i=Date.now(),r=3e3;for(;Date.now()-i<n;){let a=await this.getServer(e);if(a.status==="running"&&a.public_net.ipv4.ip!=="0.0.0.0")return a;s.debug(`Server ${e} status: ${a.status}, waiting...`),await new Promise(o=>setTimeout(o,r))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){return(await this.request("POST","/ssh_keys",{name:e,public_key:n})).ssh_key}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}};C();import{mkdir as We,rm as re,readFile as Ze,chmod as Ye}from"fs/promises";import{existsSync as ie}from"fs";import{execFile as Xe}from"child_process";import{promisify as Qe}from"util";var et=Qe(Xe);async function oe(t){let e=f.keyDir(t);ie(e)&&await re(e,{recursive:!0}),await We(e,{recursive:!0});let n=f.privateKey(t),i=f.publicKey(t);await et("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await Ye(n,384);let r=await Ze(i,"utf-8");return{privateKeyPath:n,publicKeyPath:i,publicKey:r.trim()}}async function L(t){let e=f.keyDir(t);ie(e)&&await re(e,{recursive:!0})}C();import{Client as tt}from"ssh2";import{readFile as nt}from"fs/promises";async function S(t){let{instanceName:e,ip:n,command:i,stream:r=!1,timeoutMs:a=3e4}=t,o=await nt(f.privateKey(e),"utf-8");return new Promise((c,l)=>{let u=new tt,$="",y="";u.on("ready",()=>{s.debug(`SSH connected to ${n}`),u.exec(i,(d,p)=>{if(d)return u.end(),l(d);p.on("data",g=>{let O=g.toString();$+=O,r&&process.stdout.write(O)}),p.stderr.on("data",g=>{let O=g.toString();y+=O,r&&process.stderr.write(O)}),p.on("close",g=>{u.end(),c({stdout:$,stderr:y,exitCode:g??0})})})}).on("error",d=>{let p="";d.code==="ECONNREFUSED"?p=" (instance may have been destroyed or is still booting)":d.code==="EHOSTUNREACH"?p=" (IP unreachable \u2014 instance may not be running)":d.code==="ETIMEDOUT"&&(p=" (connection timed out \u2014 check if instance is running with 'gibil list')"),l(new Error(`SSH connection to ${n} failed: ${d.message}${p}`))}).connect({host:n,port:22,username:"root",privateKey:o,readyTimeout:a,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}async function se(t,e,n=12e4){let i=Date.now(),r=5e3;for(;Date.now()-i<n;)try{await S({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{s.debug(`SSH not ready on ${e}, retrying...`),await new Promise(a=>setTimeout(a,r))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function ae(t){let{repo:e,config:n,ttlMinutes:i,githubToken:r,gitIdentity:a}=t,o=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];r&&o.push(`export GITHUB_TOKEN=${w(r)}`),o.push("","# Base packages","apt-get update -qq","apt-get install -y -qq git curl wget build-essential unzip > /dev/null 2>&1","","# Install GitHub CLI","if ! type gh > /dev/null 2>&1; then"," curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null"," chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg"," ARCH=$(dpkg --print-architecture)",' echo "deb [arch=${ARCH} signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list'," apt-get update -qq && apt-get install -y -qq gh > /dev/null 2>&1","fi","");let c=n?.image??"node:20";if(o.push(...rt(c)),n?.services&&n.services.length>0){o.push(...it()),o.push("");for(let l of n.services)o.push(...ot(l))}if(n?.env){o.push("# Environment variables");for(let[l,u]of Object.entries(n.env))o.push(`export ${l}=${w(u)}`),o.push(`echo 'export ${l}=${w(u)}' >> /root/.bashrc`);o.push("")}if(o.push("# Configure git"),a?(o.push(`git config --global user.email ${w(a.email)}`),o.push(`git config --global user.name ${w(a.name)}`),a.signingKey&&(o.push("git config --global gpg.format ssh"),o.push(`git config --global user.signingkey ${w("key::"+a.signingKey)}`),o.push("git config --global commit.gpgsign true"),o.push("git config --global tag.gpgsign true"),o.push("mkdir -p /root/.ssh"),o.push(`echo ${w(a.email+" "+a.signingKey)} > /root/.ssh/allowed_signers`),o.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(o.push("git config --global user.email 'gibil@bot.dev'"),o.push("git config --global user.name 'Gibil Bot'")),o.push(""),e){let l=e.match(/github\.com\/([^/]+\/[^/.]+)/);o.push("# Clone repository"),o.push("cd /root"),l?(o.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),o.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${l[1]}.git"`),o.push("else"),o.push(` CLONE_URL=${w(e)}`),o.push("fi"),o.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')):o.push(`timeout 300 git clone ${w(e)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`),o.push("cd /root/project"),o.push(""),o.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),o.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),l&&o.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${l[1]}.git"`),o.push("fi"),o.push("")}if(i&&i>0&&(o.push("# Auto-destroy after TTL"),o.push(`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`),o.push(`(sleep ${i*60} && shutdown -h now) &`),o.push("")),o.push("# Clean up cloud-init secrets"),o.push("rm -f /var/lib/cloud/instance/user-data.txt"),o.push(""),o.push("# Signal that infrastructure is ready"),o.push("touch /root/.gibil-ready"),o.push('echo "Gibil infrastructure ready"'),o.push(""),e&&n?.tasks&&n.tasks.length>0){o.push("# Run project tasks"),o.push("cd /root/project");for(let l of n.tasks)o.push(`echo '\u25B6 Running task: '${w(l.name)}`),o.push(`if ! ${l.command}; then`),o.push(` echo '\u2717 Task failed: '${w(l.name)}`),o.push(" touch /root/.gibil-tasks-failed"),o.push("fi");o.push(""),o.push("# Signal tasks complete"),o.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),o.push(" touch /root/.gibil-tasks-done"),o.push(' echo "Gibil tasks complete"'),o.push("else"),o.push(' echo "Gibil tasks finished with errors"'),o.push("fi")}return o.join(`
3
- `)}function rt(t){let e=[];if(t.startsWith("node:")){let n=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("")}else if(t.startsWith("python:")){let n=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let n=t.split(":")[1]??"1.22";e.push("# Install Go"),e.push(`wget -q https://go.dev/dl/go${n}.linux-amd64.tar.gz -O /tmp/go.tar.gz`),e.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),e.push("export PATH=$PATH:/usr/local/go/bin"),e.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),e.push("")}else e.push("# Install Node.js (default)"),e.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("");return e}function it(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function ot(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let i=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(i+=` -p ${t.port}:${t.port}`),t.env)for(let[r,a]of Object.entries(t.env))i+=` -e ${r}=${w(a)}`;return i+=` ${t.image}`,e.push(i),e.push(""),e}function w(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as st}from"fs/promises";import{existsSync as ce,statSync as at}from"fs";import{join as ct}from"path";import{parse as ue}from"yaml";var lt=".gibil.yml";async function V(t){let e;if(ce(t)&&at(t).isFile()?e=t:e=ct(t,lt),!ce(e))return null;let n=await st(e,"utf-8"),i=ue(n);return ge(i)}function pe(t){let e=ue(t);return ge(e)}function ge(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(i=>{let r=i;if(typeof r.name!="string"||typeof r.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:r.name,image:r.image,port:typeof r.port=="number"?r.port:void 0,env:le(r.env,`service "${r.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(i=>{let r=i;if(typeof r.name!="string"||typeof r.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:r.name,command:r.command}})),e.env!==void 0&&(n.env=le(e.env,"top-level")),n}function le(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[i,r]of Object.entries(t))if(typeof r=="string")n[i]=r;else if(typeof r=="number"||typeof r=="boolean")n[i]=String(r);else throw new Error(`env.${i} in ${e} must be a string, number, or boolean \u2014 got ${typeof r}`);return Object.keys(n).length>0?n:void 0}C();import{readFile as ut,writeFile as pt,mkdir as me,rm as gt,readdir as mt}from"fs/promises";import{existsSync as de}from"fs";import{join as W}from"path";var Z=class{instancesDir;keysDir;constructor(e){let n=e??f.root;this.instancesDir=W(n,"instances"),this.keysDir=W(n,"keys")}async ensureDirectories(){await me(this.instancesDir,{recursive:!0,mode:448}),await me(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return W(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await pt(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!de(n))return null;let i=await ut(n,"utf-8");return JSON.parse(i)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);de(n)&&await gt(n)}async list(){await this.ensureDirectories();let e=await mt(this.instancesDir),n=[];for(let i of e){if(!i.endsWith(".json"))continue;let r=i.replace(".json",""),a=await this.load(r);a&&n.push(a)}return n}},H=new Z;var R=t=>H.save(t);var fe=t=>H.loadOrThrow(t),b=t=>H.loadActiveOrThrow(t),he=t=>H.delete(t),G=()=>H.list();import{randomBytes as dt}from"crypto";function ye(t=6){return dt(Math.ceil(t/2)).toString("hex").slice(0,t)}function we(){return`gibil-${ye()}`}function ve(){return`fleet-${ye(8)}`}C();var ft=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function be(t){if(!ft.test(t))throw new Error(`Invalid instance name "${t}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return t}function Y(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}A();import{execSync as z}from"child_process";import{readFileSync as ht}from"fs";function yt(){try{let t=z("git config user.name",{encoding:"utf-8"}).trim(),e=z("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(z("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let r=z("git config user.signingkey",{encoding:"utf-8"}).trim();if(r)try{n=ht(r,"utf-8").trim()}catch{(r.startsWith("ssh-")||r.startsWith("key::"))&&(n=r.replace(/^key::/,""))}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function $e(t,e,n){s.info(` Generating SSH keys for "${e}"...`);let i=await oe(e),r;try{s.info(" Uploading SSH key to Hetzner..."),r=await t.createSSHKey(`gibil-${e}`,i.publicKey);let a=yt(),o=ae({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a});s.info(" Creating server...");let c=await t.createServer(e,r.id,o,n.config?.server_type??n.serverType,n.config?.location??n.location);s.info(" Waiting for server to be ready...");let u=(await t.waitForReady(c.id)).public_net.ipv4.ip,$=new Date,y={name:e,serverId:c.id,ip:u,sshKeyId:r.id,keyPath:f.privateKey(e),status:"running",createdAt:$.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date($.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:a};return await R(y),s.info(` Waiting for SSH on ${u}...`),await se(e,u),s.info(` \u2713 Instance "${e}" is ready at ${u}`),y}catch(a){if(s.error(`Failed to create instance "${e}", cleaning up...`),await L(e).catch(o=>s.warn(`Could not clean up SSH keys: ${o instanceof Error?o.message:String(o)}`)),r){let o=r.id;await t.deleteSSHKey(o).catch(c=>s.warn(`Could not delete Hetzner SSH key ${o}: ${c instanceof Error?c.message:String(c)}`))}throw a}}function xe(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`ssh -i ${t.keyPath} -o StrictHostKeyChecking=no root@${t.ip}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}async function wt(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return s.debug(`Cannot fetch config from non-GitHub repo: ${t}`),null;let[,n,i]=e,r=`https://raw.githubusercontent.com/${n}/${i}/HEAD/.gibil.yml`;s.debug(`Fetching config from ${r}`);try{let a=await fetch(r,{signal:AbortSignal.timeout(1e4)});if(!a.ok)return s.debug(`No .gibil.yml found in repo (${a.status})`),null;let o=await a.text();return pe(o)}catch{return s.debug("Failed to fetch repo config, continuing without it"),null}}function Se(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <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").action(async e=>{e.json&&m(!0);let n=Y(e.ttl??"60","TTL");if(n>10080)throw new Error("TTL cannot exceed 7 days (10080 minutes).");let i=Y(e.fleet??"1","Fleet count");if(i>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");e.name&&be(e.name);let r=null;e.config?r=await V(e.config):e.repo?r=await wt(e.repo):r=await V(process.cwd());let a=await x();if(a){s.info("Verifying API key...");let c=await j(a);s.info(` Authenticated as ${c.user.email} (${c.user.plan})`)}let o=await E.create();if(i===1){let c=e.name??we();s.info(`Creating instance "${c}"...`);let l=await $e(o,c,{repo:e.repo,ttlMinutes:n,config:r,serverType:e.serverType,location:e.location});a&&await P(a,"create",l.name,e.serverType).catch(u=>s.debug(`Usage tracking failed: ${u instanceof Error?u.message:String(u)}`)),e.json?s.json(xe(l)):(s.info(""),s.info("Instance ready:"),s.info(` Name: ${l.name}`),s.info(` IP: ${l.ip}`),s.info(` SSH: ssh -i ${l.keyPath} -o StrictHostKeyChecking=no root@${l.ip}`),s.info(` TTL: ${n} minutes`),s.info(""),s.info(`Quick connect: gibil ssh ${l.name}`))}else{let c=ve(),l=e.name??"gibil";s.info(`Creating fleet "${c}" with ${i} instances...`);let u=Array.from({length:i},(p,g)=>`${l}-${g+1}-${c.slice(6)}`),$=await Promise.allSettled(u.map(p=>$e(o,p,{repo:e.repo,ttlMinutes:n,config:r,serverType:e.serverType,location:e.location,fleetId:c}))),y=[],d=[];for(let p=0;p<$.length;p++){let g=$[p];g.status==="fulfilled"?y.push(g.value):d.push(`${u[p]}: ${g.reason instanceof Error?g.reason.message:String(g.reason)}`)}if(a&&await Promise.all(y.map(p=>P(a,"create",p.name,e.serverType).catch(g=>s.debug(`Usage tracking failed for ${p.name}: ${g instanceof Error?g.message:String(g)}`)))),e.json)s.json({fleet_id:c,instances:y.map(xe),errors:d});else{s.info(""),s.info(`Fleet "${c}": ${y.length}/${i} instances created`);for(let p of y)s.info(` \u2713 ${p.name} \u2192 ${p.ip}`);for(let p of d)s.error(` \u2717 ${p}`)}}})}import{spawn as vt}from"child_process";function ke(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").action(async e=>{let n=await b(e),i=["-A","-i",n.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR",`root@${n.ip}`];vt("ssh",i,{stdio:"inherit"}).on("exit",a=>{process.exit(a??0)})})}function Ie(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").action(async(e,n,i)=>{i.json&&m(!0);let r=await b(e),a=n.join(" ");s.info(`Running on "${e}" (${r.ip}): ${a}`);let o=await S({instanceName:e,ip:r.ip,command:a,stream:!i.json});i.json?s.json({instance:e,command:a,stdout:o.stdout,stderr:o.stderr,exit_code:o.exitCode}):o.exitCode!==0&&s.error(`Command exited with code ${o.exitCode}`),process.exit(o.exitCode??1)})}A();async function Ee(t,e){let n=await fe(e);s.info(`Destroying instance "${e}" (server ${n.serverId})...`);try{await t.destroyServer(n.serverId)}catch(r){s.warn(`Could not delete server ${n.serverId}: ${r instanceof Error?r.message:String(r)}`)}try{await t.deleteSSHKey(n.sshKeyId)}catch(r){s.warn(`Could not delete SSH key ${n.sshKeyId}: ${r instanceof Error?r.message:String(r)}`)}await L(e),await he(e);let i=await x();i&&await P(i,"destroy",e).catch(r=>s.warn(`Usage tracking failed (billing may be inaccurate): ${r instanceof Error?r.message:String(r)}`)),s.info(` \u2713 Instance "${e}" destroyed`)}function Ce(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").action(async(e,n)=>{if(n.json&&m(!0),n.all){let i=await G();if(i.length===0){n.json?s.json({destroyed:[],failed:[]}):s.info("No instances to destroy.");return}let r=await E.create();s.info(`Destroying ${i.length} instance(s)...`);let a=await Promise.allSettled(i.map(l=>Ee(r,l.name))),o=[],c=[];for(let l=0;l<a.length;l++)if(a[l].status==="fulfilled")o.push(i[l].name);else{let u=a[l].reason;c.push(`${i[l].name}: ${u instanceof Error?u.message:String(u)}`)}n.json?s.json({destroyed:o,failed:c}):s.info(`
4
- ${o.length} destroyed, ${c.length} failed`)}else{e||(s.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1));let i=await E.create();await Ee(i,e),n.json&&s.json({destroyed:[e]})}})}function _e(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&m(!0);let n=await G();if(n.length===0){e.json?s.json({instances:[]}):s.info("No active instances.");return}let i=n.map(r=>{let a=Math.max(0,Math.floor((new Date(r.expiresAt).getTime()-Date.now())/1e3));return{name:r.name,ip:r.ip,ssh:`ssh -i ${r.keyPath} -o StrictHostKeyChecking=no root@${r.ip}`,status:r.status,ttl_remaining:a,created_at:r.createdAt,fleet_id:r.fleetId}});if(e.json){s.json({instances:i});return}s.info(`${"NAME".padEnd(30)} ${"IP".padEnd(18)} ${"STATUS".padEnd(12)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`),s.info("\u2500".repeat(80));for(let r of i){let a=je(r.ttl_remaining),o=bt(r.created_at);s.info(`${r.name.padEnd(30)} ${r.ip.padEnd(18)} ${r.status.padEnd(12)} ${a.padEnd(10)} ${o.padEnd(10)}`)}s.info(`
5
- ${i.length} instance(s)`)})}function je(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=t%60;return e>=60?`${Math.floor(e/60)}h ${e%60}m`:`${e}m ${n}s`}function bt(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return je(n)}function Pe(t){t.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(e,n)=>{n.json&&m(!0);let i=await b(e),r=parseInt(n.ttl,10);(isNaN(r)||r<=0)&&(s.error("TTL must be a positive number of minutes"),process.exit(1)),await S({instanceName:e,ip:i.ip,command:["pkill -f 'sleep.*shutdown' || true",`(sleep ${r*60} && shutdown -h now) &`].join(" && ")});let a=new Date(Date.now()+r*6e4).toISOString();i.ttlMinutes=r,i.expiresAt=a,await R(i),n.json?s.json({name:i.name,ttl_minutes:r,expires_at:a}):s.info(`\u2713 Extended "${e}" TTL to ${r} minutes (expires ${a})`)})}import{readFile as $t}from"fs/promises";import{randomBytes as xt}from"crypto";function Ae(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&m(!0);let i=await b(e),r=await $t(n.script,"utf-8");s.info(`Uploading and running script "${n.script}" on "${e}"...`);let a=Buffer.from(r).toString("base64"),o=`/tmp/gibil-script-${xt(4).toString("hex")}.sh`,c=await S({instanceName:e,ip:i.ip,command:`echo '${a}' | base64 -d > ${o} && chmod +x ${o} && ${o}; EXIT=$?; rm -f ${o}; exit $EXIT`,stream:!n.json});n.json?s.json({instance:e,script:n.script,stdout:c.stdout,stderr:c.stderr,exit_code:c.exitCode}):c.exitCode!==0&&s.error(`Script exited with code ${c.exitCode}`),process.exit(c.exitCode??1)})}A();import{createInterface as St}from"readline";function Te(t){let e=St({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,i=>{e.close(),n(i.trim())})})}function Oe(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&m(!0);let i=n.key??process.env.GIBIL_API_KEY;i||(i=await Te("Enter your API key: ")),i||(s.error("No API key provided."),process.exit(1)),i.startsWith("pk_")||(s.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),s.info("Verifying API key...");try{let r=await j(i);await F(i),n.json?s.json({authenticated:!0,email:r.user.email,plan:r.user.plan}):(s.info(`\u2713 Logged in as ${r.user.email}`),s.info(` Plan: ${r.user.plan}`),s.info(` Limits: ${r.limits.max_concurrent} concurrent VMs, ${r.limits.remaining_hours}h remaining`))}catch(r){s.error(r instanceof Error?r.message:String(r)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let i=n.token;i||(i=await Te("Enter your Hetzner API token: ")),i||(s.error("No token provided."),process.exit(1));try{let a=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${i}`}})).json();a.error&&(s.error(`Invalid token: ${a.error.message}`),process.exit(1))}catch{s.error("Could not verify token with Hetzner API."),process.exit(1)}await B(i),s.info("\u2713 Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await q(),s.info("\u2713 Logged out. API key removed from ~/.gibil/config.json")}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&m(!0);let i=await x();if(!i){n.json?s.json({authenticated:!1}):s.info('Not logged in. Run "gibil auth login" to authenticate.');return}try{let r=await j(i);n.json?s.json({authenticated:!0,email:r.user.email,plan:r.user.plan,limits:r.limits}):(s.info(`\u2713 Authenticated as ${r.user.email}`),s.info(` Plan: ${r.user.plan}`),s.info(` Concurrent VMs: ${r.limits.max_concurrent}`),s.info(` Hours remaining: ${r.limits.remaining_hours}`))}catch{n.json?s.json({authenticated:!1,error:"Key verification failed"}):s.error('Stored API key is invalid. Run "gibil auth login" to re-authenticate.')}})}A();function He(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&m(!0);let n=await x();n||(s.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let i=await J(n);if(e.json)s.json(i);else{let r=Math.round(i.vm_hours_used/i.vm_hours_limit*100);s.info(`Plan: ${i.plan}`),s.info(`VM hours: ${i.vm_hours_used.toFixed(1)} / ${i.vm_hours_limit}h (${r}%)`),s.info(`Active instances: ${i.active_instances} / ${i.max_concurrent}`),r>80&&s.warn("Running low on hours. Upgrade at https://gibil.dev/pricing")}}catch(i){s.error(i instanceof Error?i.message:String(i)),process.exit(1)}})}import{McpServer as kt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as It}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as h}from"zod";var N;function T(t,e=3e4){return S({instanceName:N.name,ip:N.ip,command:t,stream:!1,timeoutMs:e})}async function Ne(t){if(N=await b(t),N.gitIdentity){let{name:i,email:r,signingKey:a}=N.gitIdentity,o=[`git config --global user.name '${i.replace(/'/g,"'\\''")}'`,`git config --global user.email '${r.replace(/'/g,"'\\''")}'`];a&&o.push("git config --global gpg.format ssh",`git config --global user.signingkey 'key::${a.replace(/'/g,"'\\''")}'`,"git config --global commit.gpgsign true"),T(o.join(" && ")).catch(()=>{})}let e=new kt({name:`gibil-${t}`,version:"0.1.0"});e.tool("vm_bash","Run a shell command on the remote VM. Use for: builds, tests, git, package managers, any shell operation.",{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: 30000)")},async({command:i,working_dir:r,timeout_ms:a})=>{let c=await T(`cd ${r??"/root/project"} 2>/dev/null || cd /root && ${i}`,a??3e4);return{content:[{type:"text",text:[c.stdout,c.stderr].filter(Boolean).join(`
6
- `)||"(no output)"}],isError:c.exitCode!==0}}),e.tool("vm_read","Read a file from the remote VM. Returns the file contents.",{path:h.string().describe("Absolute path on the VM (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")},async({path:i,offset:r,limit:a})=>{let o=`cat -n '${i.replace(/'/g,"'\\''")}'`;r&&a?o=`sed -n '${r},${r+a-1}p' '${i.replace(/'/g,"'\\''")}' | cat -n`:r?o=`tail -n +${r} '${i.replace(/'/g,"'\\''")}' | cat -n`:a&&(o=`head -n ${a} '${i.replace(/'/g,"'\\''")}' | cat -n`);let c=await T(o);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:c.stdout}]}}),e.tool("vm_write","Write content to a file on the remote VM. Creates parent directories if needed. Overwrites existing files.",{path:h.string().describe("Absolute path on the VM"),content:h.string().describe("File content to write")},async({path:i,content:r})=>{let a=Buffer.from(r).toString("base64"),o=`mkdir -p "$(dirname '${i.replace(/'/g,"'\\''")}')" && echo '${a}' | base64 -d > '${i.replace(/'/g,"'\\''")}'`,c=await T(o);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:`Wrote ${i}`}]}}),e.tool("vm_ls","List files and directories on the remote VM.",{path:h.string().optional().describe("Directory path (default: /root/project)"),glob:h.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')")},async({path:i,glob:r})=>{let a=i??"/root/project",o;r?o=`cd '${a.replace(/'/g,"'\\''")}' && find . -path './${r}' -type f 2>/dev/null | sort | head -200`:o=`ls -la '${a.replace(/'/g,"'\\''")}'`;let c=await T(o);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:c.stdout}]}}),e.tool("vm_grep","Search for a pattern in files on the remote VM. 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 in (default: /root/project)"),include:h.string().optional().describe("File glob to include (e.g. '*.ts')")},async({pattern:i,path:r,include:a})=>{let o=r??"/root/project",c=i.replace(/'/g,"'\\''"),l=o.replace(/'/g,"'\\''"),u;if(a){let d=a.replace(/'/g,"'\\''");u=`cd '${l}' && (rg -n --glob '${d}' '${c}' 2>/dev/null || grep -rn --include='${d}' '${c}' .) | head -100`}else u=`cd '${l}' && (rg -n '${c}' 2>/dev/null || grep -rn '${c}' .) | head -100`;return{content:[{type:"text",text:(await T(u)).stdout||"(no matches)"}]}});let n=new It;await e.connect(n)}function Ke(t){t.command("mcp <name>").description("Start an MCP server for a running instance (used by Claude Code)").action(async e=>{try{await Ne(e)}catch(n){s.error(n instanceof Error?n.message:String(n)),process.exit(1)}})}try{await import("dotenv/config")}catch{}var v=new Et;v.name("gibil").description("Ephemeral dev compute for humans and AI agents").version("0.1.0");Se(v);ke(v);Ie(v);Ce(v);_e(v);Pe(v);Ae(v);Oe(v);He(v);Ke(v);async function Ct(){try{await v.parseAsync(process.argv)}catch(t){t instanceof Error&&s.error(t.message),process.exit(1)}}Ct();
2
+ var st=Object.defineProperty;var fe=(t,e)=>()=>(t&&(e=t(t=0)),e);var at=(t,e)=>{for(var n in e)st(t,n,{get:e[n],enumerable:!0})};import{homedir as pt}from"os";import{join as K}from"path";var R,y,M=fe(()=>{"use strict";R=K(pt(),".gibil"),y={root:R,instances:K(R,"instances"),keys:K(R,"keys"),instanceFile:t=>K(R,"instances",`${t}.json`),keyDir:t=>K(R,"keys",t),privateKey:t=>K(R,"keys",t,"id_ed25519"),publicKey:t=>K(R,"keys",t,"id_ed25519.pub")}});var ke={};at(ke,{clearApiKey:()=>ae,fetchUsage:()=>le,getApiKey:()=>C,getApiUrl:()=>yt,getApiUrlFromConfig:()=>X,getHetznerToken:()=>ce,saveApiKey:()=>J,saveHetznerToken:()=>V,trackUsage:()=>F,verifyApiKey:()=>H});import{readFile as gt,writeFile as dt,mkdir as mt}from"fs/promises";import{existsSync as ft}from"fs";import{join as ht}from"path";async function D(){if(!ft(oe))return{};let t=await gt(oe,"utf-8");return JSON.parse(t)}async function se(t){await mt(y.root,{recursive:!0,mode:448}),await dt(oe,JSON.stringify(t,null,2),{mode:384})}async function J(t){let e=await D();e.api_key=t,await se(e)}async function C(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await D()).api_key??null}async function ae(){let t=await D();delete t.api_key,await se(t)}function yt(){return process.env.GIBIL_API_URL??xe}async function X(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await D()).api_url??xe}async function V(t){let e=await D();e.hetzner_token=t,await se(e)}async function ce(){return process.env.HETZNER_API_TOKEN?process.env.HETZNER_API_TOKEN:(await D()).hetzner_token??null}async function H(t){let e=await X(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let i=await n.text();throw new Error(`API error (${n.status}): ${i}`)}return await n.json()}async function F(t,e,n,i){let r=await X(),a=await fetch(`${r}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:i})});if(a.status===429)throw new Error("Plan limit reached. Upgrade at https://gibil.dev/pricing");if(!a.ok){let s=await a.text();throw new Error(`Usage tracking failed (${a.status}): ${s}`)}}async function le(t){let e=await X(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let i=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${i}`)}return await n.json()}var oe,xe,L=fe(()=>{"use strict";M();oe=ht(y.root,"config.json"),xe="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1"});import{Command as tn}from"commander";import G from"picocolors";var T=t=>G.red(t),ct=t=>G.yellow(t),N=t=>G.green(t),lt=t=>G.red(t),u=t=>G.dim(t),g=t=>G.bold(t),b="\u{1F98E}";var _=N("\u2713"),q=lt("\u2716"),ye=ct("\u26A0"),Z="\u{12248}",we=`
3
+ ${T(" /\\")}
4
+ ${T(" / \\")}
5
+ ${T(" / \u{1F525} \\")}
6
+ ${T(" / \\")}
7
+ ${u(" ~~~~~~~~")}
8
+ ${g(" g i b i l")} ${u(Z)}
9
+ `,$e=`${b} ${g("gibil")} ${u(Z)}`,he=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],B=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=T(he[this.frame%he.length]);process.stderr.write(`\r ${e} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
10
+ `),this)}update(e){this.text=e,process.stderr.isTTY||process.stderr.write(` ${e}
11
+ `)}succeed(e){this.stop(),process.stderr.write(`\r ${_} ${e??this.text}
12
+ `)}fail(e){this.stop(),process.stderr.write(`\r ${q} ${e??this.text}
13
+ `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};function ve(t,e){let n=Math.max(t.length+4,...e.map(l=>re(l).length+4)),i=`${u("\u256D")}${u("\u2500".repeat(n))}${u("\u256E")}`,r=`${u("\u2570")}${u("\u2500".repeat(n))}${u("\u256F")}`,a=`${u("\u2502")} ${b} ${g(t)}${" ".repeat(n-re(t).length-4)}${u("\u2502")}`,s=`${u("\u251C")}${u("\u2500".repeat(n))}${u("\u2524")}`,c=e.map(l=>{let p=n-re(l).length-2;return`${u("\u2502")} ${l}${" ".repeat(Math.max(0,p))}${u("\u2502")}`});return[i,a,s,...c,r].join(`
14
+ `)}function re(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var h={welcome:`${b} Your first fire. Welcome to Gibil.`,noInstances:`${b} No fires burning. Gibil sleeps.`,destroyAll:`${b} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${b} "${t}" \u2014 fire out.`,authSuccess:`${b} Logged in. The forge is yours.`,authLogout:`${b} Logged out. The forge cools.`,createReady:(t,e)=>`${b} "${t}" forged ${u(`(${e}s)`)}`,fleetReady:(t,e)=>`${b} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${b} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${b} The forge is ready. Run ${g("gibil create")} to light your first fire.`,setupNeeded:`${b} No forge configured. Run ${g("gibil init")} to get started.`};var ut="info",ie=!1,be={debug:0,info:1,warn:2,error:3,silent:4};function m(t){ie=t}function j(t){return ie&&t!=="error"?!1:be[t]>=be[ut]}var o={debug(t,...e){j("debug")&&console.debug(`${u("[debug]")} ${t}`,...e)},info(t,...e){j("info")&&console.log(t,...e)},warn(t,...e){j("warn")&&console.warn(`${ye} ${t}`,...e)},error(t,...e){j("error")&&console.error(`${q} ${t}`,...e)},success(t){j("info")&&console.log(`${_} ${t}`)},step(t){j("info")&&console.log(` ${u("\u203A")} ${t}`)},flame(t){j("info")&&console.log(t)},detail(t,e){j("info")&&console.log(` ${u(t+":")} ${e}`)},spin(t){return ie?new B(t):new B(t).start()},json(t){console.log(JSON.stringify(t,null,2))}};var wt="https://api.hetzner.cloud/v1",z=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(L(),ke)),i=e??await n();if(!i)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(i)}async request(e,n,i){let r=`${wt}${n}`;o.debug(`${e} ${r}`);let a=await fetch(r,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:i?JSON.stringify(i):void 0});if(!a.ok){let s=await a.text(),c;try{c=JSON.parse(s).error?.message??s}catch{c=s}throw new Error(`Hetzner API error (${a.status}): ${c}`)}return a.status===204?{}:await a.json()}async createServer(e,n,i,r="cax11",a="fsn1"){let s={name:e,server_type:r,image:"ubuntu-24.04",ssh_keys:[n],labels:{gibil:"true","gibil-name":e},location:a};return i&&(s.user_data=i),(await this.request("POST","/servers",s)).server}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){return(await this.request("GET",`/servers/${e}`)).server}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers}async waitForReady(e,n=12e4){let i=Date.now(),r=3e3;for(;Date.now()-i<n;){let a=await this.getServer(e);if(a.status==="running"&&a.public_net.ipv4.ip!=="0.0.0.0")return a;o.debug(`Server ${e} status: ${a.status}, waiting...`),await new Promise(s=>setTimeout(s,r))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){return(await this.request("POST","/ssh_keys",{name:e,public_key:n})).ssh_key}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}};M();import{mkdir as $t,rm as Se,readFile as vt,chmod as bt}from"fs/promises";import{existsSync as Ie}from"fs";import{execFile as xt}from"child_process";import{promisify as kt}from"util";var St=kt(xt);async function Ce(t){let e=y.keyDir(t);Ie(e)&&await Se(e,{recursive:!0}),await $t(e,{recursive:!0});let n=y.privateKey(t),i=y.publicKey(t);await St("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await bt(n,384);let r=await vt(i,"utf-8");return{privateKeyPath:n,publicKeyPath:i,publicKey:r.trim()}}async function Q(t){let e=y.keyDir(t);Ie(e)&&await Se(e,{recursive:!0})}M();import{Client as It}from"ssh2";import{readFile as Ct}from"fs/promises";async function P(t){let{instanceName:e,ip:n,command:i,stream:r=!1,timeoutMs:a=3e4}=t,s=await Ct(y.privateKey(e),"utf-8");return new Promise((c,l)=>{let p=new It,f="",v="";p.on("ready",()=>{o.debug(`SSH connected to ${n}`),p.exec(i,(d,$)=>{if(d)return p.end(),l(d);$.on("data",A=>{let O=A.toString();f+=O,r&&process.stdout.write(O)}),$.stderr.on("data",A=>{let O=A.toString();v+=O,r&&process.stderr.write(O)}),$.on("close",A=>{p.end(),c({stdout:f,stderr:v,exitCode:A??0})})})}).on("error",d=>{let $="";d.code==="ECONNREFUSED"?$=" (instance may have been destroyed or is still booting)":d.code==="EHOSTUNREACH"?$=" (IP unreachable \u2014 instance may not be running)":d.code==="ETIMEDOUT"&&($=" (connection timed out \u2014 check if instance is running with 'gibil list')"),l(new Error(`SSH connection to ${n} failed: ${d.message}${$}`))}).connect({host:n,port:22,username:"root",privateKey:s,readyTimeout:a,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}async function Ee(t,e,n=12e4){let i=Date.now(),r=5e3;for(;Date.now()-i<n;)try{await P({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{o.debug(`SSH not ready on ${e}, retrying...`),await new Promise(a=>setTimeout(a,r))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function Ae(t){let{repo:e,config:n,ttlMinutes:i,githubToken:r,gitIdentity:a}=t,s=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];r&&s.push(`export GITHUB_TOKEN=${S(r)}`),s.push("","# Base packages","apt-get update -qq","apt-get install -y -qq git curl wget build-essential unzip > /dev/null 2>&1","","# Install GitHub CLI","if ! type gh > /dev/null 2>&1; then"," curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null"," chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg"," ARCH=$(dpkg --print-architecture)",' echo "deb [arch=${ARCH} signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list'," apt-get update -qq && apt-get install -y -qq gh > /dev/null 2>&1","fi","");let c=n?.image??"node:20";if(s.push(...Et(c)),n?.services&&n.services.length>0){s.push(...At()),s.push("");for(let l of n.services)s.push(..._t(l))}if(n?.env){s.push("# Environment variables");for(let[l,p]of Object.entries(n.env))s.push(`export ${l}=${S(p)}`),s.push(`echo 'export ${l}=${S(p)}' >> /root/.bashrc`);s.push("")}if(s.push("# Configure git"),a?(s.push(`git config --global user.email ${S(a.email)}`),s.push(`git config --global user.name ${S(a.name)}`),a.signingKey&&(s.push("git config --global gpg.format ssh"),s.push(`git config --global user.signingkey ${S("key::"+a.signingKey)}`),s.push("git config --global commit.gpgsign true"),s.push("git config --global tag.gpgsign true"),s.push("mkdir -p /root/.ssh"),s.push(`echo ${S(a.email+" "+a.signingKey)} > /root/.ssh/allowed_signers`),s.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(s.push("git config --global user.email 'gibil@bot.dev'"),s.push("git config --global user.name 'Gibil Bot'")),s.push(""),e){let l=e.match(/github\.com\/([^/]+\/[^/.]+)/);s.push("# Clone repository"),s.push("cd /root"),l?(s.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),s.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${l[1]}.git"`),s.push("else"),s.push(` CLONE_URL=${S(e)}`),s.push("fi"),s.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')):s.push(`timeout 300 git clone ${S(e)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`),s.push("cd /root/project"),s.push(""),s.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),s.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),l&&s.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${l[1]}.git"`),s.push("fi"),s.push("")}if(i&&i>0&&(s.push("# Auto-destroy after TTL"),s.push(`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`),s.push(`(sleep ${i*60} && shutdown -h now) &`),s.push("")),s.push("# Clean up cloud-init secrets"),s.push("rm -f /var/lib/cloud/instance/user-data.txt"),s.push(""),s.push("# Signal that infrastructure is ready"),s.push("touch /root/.gibil-ready"),s.push('echo "Gibil infrastructure ready"'),s.push(""),e&&n?.tasks&&n.tasks.length>0){s.push("# Run project tasks"),s.push("cd /root/project");for(let l of n.tasks)s.push(`echo '\u25B6 Running task: '${S(l.name)}`),s.push(`if ! ${l.command}; then`),s.push(` echo '\u2717 Task failed: '${S(l.name)}`),s.push(" touch /root/.gibil-tasks-failed"),s.push("fi");s.push(""),s.push("# Signal tasks complete"),s.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),s.push(" touch /root/.gibil-tasks-done"),s.push(' echo "Gibil tasks complete"'),s.push("else"),s.push(' echo "Gibil tasks finished with errors"'),s.push("fi")}return s.join(`
15
+ `)}function Et(t){let e=[];if(t.startsWith("node:")){let n=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("")}else if(t.startsWith("python:")){let n=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let n=t.split(":")[1]??"1.22";e.push("# Install Go"),e.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),e.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),e.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),e.push("export PATH=$PATH:/usr/local/go/bin"),e.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),e.push("")}else e.push("# Install Node.js (default)"),e.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("");return e}function At(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function _t(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let i=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(i+=` -p ${t.port}:${t.port}`),t.env)for(let[r,a]of Object.entries(t.env))i+=` -e ${r}=${S(a)}`;return i+=` ${t.image}`,e.push(i),e.push(""),e}function S(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as Pt}from"fs/promises";import{existsSync as _e,statSync as Tt}from"fs";import{join as jt}from"path";import{parse as Te}from"yaml";var Ht=".gibil.yml";async function ue(t){let e;if(_e(t)&&Tt(t).isFile()?e=t:e=jt(t,Ht),!_e(e))return null;let n=await Pt(e,"utf-8"),i=Te(n);return He(i)}function je(t){let e=Te(t);return He(e)}function He(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(i=>{let r=i;if(typeof r.name!="string"||typeof r.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:r.name,image:r.image,port:typeof r.port=="number"?r.port:void 0,env:Pe(r.env,`service "${r.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(i=>{let r=i;if(typeof r.name!="string"||typeof r.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:r.name,command:r.command}})),e.env!==void 0&&(n.env=Pe(e.env,"top-level")),n}function Pe(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[i,r]of Object.entries(t))if(typeof r=="string")n[i]=r;else if(typeof r=="number"||typeof r=="boolean")n[i]=String(r);else throw new Error(`env.${i} in ${e} must be a string, number, or boolean \u2014 got ${typeof r}`);return Object.keys(n).length>0?n:void 0}M();import{readFile as Ot,writeFile as Nt,mkdir as Oe,rm as Rt,readdir as Kt}from"fs/promises";import{existsSync as Ne}from"fs";import{join as pe}from"path";var ge=class{instancesDir;keysDir;constructor(e){let n=e??y.root;this.instancesDir=pe(n,"instances"),this.keysDir=pe(n,"keys")}async ensureDirectories(){await Oe(this.instancesDir,{recursive:!0,mode:448}),await Oe(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return pe(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await Nt(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!Ne(n))return null;let i=await Ot(n,"utf-8");return JSON.parse(i)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);Ne(n)&&await Rt(n)}async list(){await this.ensureDirectories();let e=await Kt(this.instancesDir),n=[];for(let i of e){if(!i.endsWith(".json"))continue;let r=i.replace(".json",""),a=await this.load(r);a&&n.push(a)}return n}},W=new ge;var ee=t=>W.save(t);var Re=t=>W.loadOrThrow(t),E=t=>W.loadActiveOrThrow(t),Ke=t=>W.delete(t),te=()=>W.list();import{randomBytes as Mt}from"crypto";function Me(t=6){return Mt(Math.ceil(t/2)).toString("hex").slice(0,t)}function Le(){return`gibil-${Me()}`}function ze(){return`fleet-${Me(8)}`}M();var Lt=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function Ge(t){if(!Lt.test(t))throw new Error(`Invalid instance name "${t}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return t}function de(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}L();import{execSync as ne}from"child_process";import{readFileSync as zt}from"fs";function Gt(){try{let t=ne("git config user.name",{encoding:"utf-8"}).trim(),e=ne("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(ne("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let r=ne("git config user.signingkey",{encoding:"utf-8"}).trim();if(r)try{n=zt(r,"utf-8").trim()}catch{(r.startsWith("ssh-")||r.startsWith("key::"))&&(n=r.replace(/^key::/,""))}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function De(t,e,n){o.step("Generating SSH keys...");let i=await Ce(e),r;try{o.step("Uploading SSH key..."),r=await t.createSSHKey(`gibil-${e}`,i.publicKey);let a=Gt(),s=Ae({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a});o.step("Creating server...");let c=await t.createServer(e,r.id,s,n.config?.server_type??n.serverType,n.config?.location??n.location);o.step("Waiting for server...");let p=(await t.waitForReady(c.id)).public_net.ipv4.ip,f=new Date,v={name:e,serverId:c.id,ip:p,sshKeyId:r.id,keyPath:y.privateKey(e),status:"running",createdAt:f.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(f.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:a};return await ee(v),o.step(`Waiting for SSH on ${p}...`),await Ee(e,p),v}catch(a){if(o.error(`Failed to create instance "${e}", cleaning up...`),await Q(e).catch(s=>o.warn(`Could not clean up SSH keys: ${s instanceof Error?s.message:String(s)}`)),r){let s=r.id;await t.deleteSSHKey(s).catch(c=>o.warn(`Could not delete Hetzner SSH key ${s}: ${c instanceof Error?c.message:String(c)}`))}throw a}}function Fe(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`ssh -i ${t.keyPath} -o StrictHostKeyChecking=no root@${t.ip}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}async function Dt(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return o.debug(`Cannot fetch config from non-GitHub repo: ${t}`),null;let[,n,i]=e,r=`https://raw.githubusercontent.com/${n}/${i}/HEAD/.gibil.yml`;o.debug(`Fetching config from ${r}`);try{let a=await fetch(r,{signal:AbortSignal.timeout(1e4)});if(!a.ok)return o.debug(`No .gibil.yml found in repo (${a.status})`),null;let s=await a.text();return je(s)}catch{return o.debug("Failed to fetch repo config, continuing without it"),null}}function Ue(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <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").action(async e=>{e.json&&m(!0);let n=de(e.ttl??"60","TTL");if(n>10080)throw new Error("TTL cannot exceed 7 days (10080 minutes).");let i=de(e.fleet??"1","Fleet count");if(i>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");e.name&&Ge(e.name);let r=null;e.config?r=await ue(e.config):e.repo?r=await Dt(e.repo):r=await ue(process.cwd());let a=await C();if(a){o.info("Verifying API key...");let c=await H(a);o.info(` Authenticated as ${c.user.email} (${c.user.plan})`)}let s=await z.create();if(i===1){let c=e.name??Le(),l=Date.now(),p=o.spin(`Forging "${c}"...`),f=await De(s,c,{repo:e.repo,ttlMinutes:n,config:r,serverType:e.serverType,location:e.location}),v=((Date.now()-l)/1e3).toFixed(1);p.succeed(h.createReady(c,v)),a&&await F(a,"create",f.name,e.serverType).catch(d=>o.debug(`Usage tracking failed: ${d instanceof Error?d.message:String(d)}`)),e.json?o.json(Fe(f)):(o.info(""),o.info(ve("Server ready",[`${u("Name:")} ${g(f.name)}`,`${u("IP:")} ${f.ip}`,`${u("TTL:")} ${n} minutes`,`${u("SSH:")} ${g(`gibil ssh ${f.name}`)}`])),o.info(""))}else{let c=ze(),l=e.name??"gibil",p=Date.now(),f=o.spin(`Forging fleet "${c}" \u2014 ${i} servers...`),v=Array.from({length:i},(w,I)=>`${l}-${I+1}-${c.slice(6)}`),d=await Promise.allSettled(v.map(w=>De(s,w,{repo:e.repo,ttlMinutes:n,config:r,serverType:e.serverType,location:e.location,fleetId:c}))),$=[],A=[];for(let w=0;w<d.length;w++){let I=d[w];I.status==="fulfilled"?$.push(I.value):A.push(`${v[w]}: ${I.reason instanceof Error?I.reason.message:String(I.reason)}`)}let O=((Date.now()-p)/1e3).toFixed(1);if(f.succeed(h.fleetReady($.length,i)+` ${u(`(${O}s)`)}`),a&&await Promise.all($.map(w=>F(a,"create",w.name,e.serverType).catch(I=>o.debug(`Usage tracking failed for ${w.name}: ${I instanceof Error?I.message:String(I)}`)))),e.json)o.json({fleet_id:c,instances:$.map(Fe),errors:A});else{o.info("");for(let w of $)o.info(` ${_} ${g(w.name)} ${u("\u2192")} ${w.ip}`);for(let w of A)o.info(` ${q} ${w}`);o.info("")}}})}import{spawn as Ft}from"child_process";function Be(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").action(async e=>{let n=await E(e),i=["-A","-i",n.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR",`root@${n.ip}`];Ft("ssh",i,{stdio:"inherit"}).on("exit",a=>{process.exit(a??0)})})}function qe(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").action(async(e,n,i)=>{i.json&&m(!0);let r=await E(e),a=n.join(" ");o.info(`Running on "${e}" (${r.ip}): ${a}`);let s=await P({instanceName:e,ip:r.ip,command:a,stream:!i.json});i.json?o.json({instance:e,command:a,stdout:s.stdout,stderr:s.stderr,exit_code:s.exitCode}):s.exitCode!==0&&o.error(`Command exited with code ${s.exitCode}`),process.exit(s.exitCode??1)})}L();async function Je(t,e){let n=await Re(e);o.info(`Destroying instance "${e}" (server ${n.serverId})...`);try{await t.destroyServer(n.serverId)}catch(r){o.warn(`Could not delete server ${n.serverId}: ${r instanceof Error?r.message:String(r)}`)}try{await t.deleteSSHKey(n.sshKeyId)}catch(r){o.warn(`Could not delete SSH key ${n.sshKeyId}: ${r instanceof Error?r.message:String(r)}`)}await Q(e),await Ke(e);let i=await C();i&&await F(i,"destroy",e).catch(r=>o.warn(`Usage tracking failed (billing may be inaccurate): ${r instanceof Error?r.message:String(r)}`)),o.info(` ${_} ${h.destroySingle(e)}`)}function Ve(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").action(async(e,n)=>{if(n.json&&m(!0),n.all){let i=await te();if(i.length===0){n.json?o.json({destroyed:[],failed:[]}):o.info(h.noInstances);return}let r=await z.create();o.info(`Destroying ${i.length} instance(s)...`);let a=await Promise.allSettled(i.map(l=>Je(r,l.name))),s=[],c=[];for(let l=0;l<a.length;l++)if(a[l].status==="fulfilled")s.push(i[l].name);else{let p=a[l].reason;c.push(`${i[l].name}: ${p instanceof Error?p.message:String(p)}`)}n.json?o.json({destroyed:s,failed:c}):c.length===0?o.info(`
16
+ ${h.destroyAll}`):o.info(`
17
+ ${s.length} destroyed, ${c.length} failed`)}else{e||(o.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1));let i=await z.create();await Je(i,e),n.json&&o.json({destroyed:[e]})}})}function We(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&m(!0);let n=await te();if(n.length===0){e.json?o.json({instances:[]}):o.info(h.noInstances);return}let i=n.map(r=>{let a=Math.max(0,Math.floor((new Date(r.expiresAt).getTime()-Date.now())/1e3));return{name:r.name,ip:r.ip,ssh:`ssh -i ${r.keyPath} -o StrictHostKeyChecking=no root@${r.ip}`,status:r.status,ttl_remaining:a,created_at:r.createdAt,fleet_id:r.fleetId}});if(e.json){o.json({instances:i});return}o.info(u(`${"NAME".padEnd(30)} ${"IP".padEnd(18)} ${"STATUS".padEnd(12)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),o.info(u("\u2500".repeat(80)));for(let r of i){let a=Ye(r.ttl_remaining),s=Ut(r.created_at),c=r.name.padEnd(30),l=r.status.padEnd(12),p=a.padEnd(10),f=s.padEnd(10),v=r.status==="running"?N(l):T(l),d=r.ttl_remaining<=300?T(p):p;o.info(`${g(c)} ${r.ip.padEnd(18)} ${v} ${d} ${u(f)}`)}o.info(`
18
+ ${u(`${i.length} server(s)`)}`)})}function Ye(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=t%60;return e>=60?`${Math.floor(e/60)}h ${e%60}m`:`${e}m ${n}s`}function Ut(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return Ye(n)}function Ze(t){t.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(e,n)=>{n.json&&m(!0);let i=await E(e),r=parseInt(n.ttl,10);(isNaN(r)||r<=0)&&(o.error("TTL must be a positive number of minutes"),process.exit(1)),await P({instanceName:e,ip:i.ip,command:["pkill -f 'sleep.*shutdown' || true",`(sleep ${r*60} && shutdown -h now) &`].join(" && ")});let a=new Date(Date.now()+r*6e4).toISOString();i.ttlMinutes=r,i.expiresAt=a,await ee(i),n.json?o.json({name:i.name,ttl_minutes:r,expires_at:a}):o.info(`\u2713 Extended "${e}" TTL to ${r} minutes (expires ${a})`)})}import{readFile as Bt}from"fs/promises";import{randomBytes as qt}from"crypto";function Xe(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&m(!0);let i=await E(e),r=await Bt(n.script,"utf-8");o.info(`Uploading and running script "${n.script}" on "${e}"...`);let a=Buffer.from(r).toString("base64"),s=`/tmp/gibil-script-${qt(4).toString("hex")}.sh`,c=await P({instanceName:e,ip:i.ip,command:`echo '${a}' | base64 -d > ${s} && chmod +x ${s} && ${s}; EXIT=$?; rm -f ${s}; exit $EXIT`,stream:!n.json});n.json?o.json({instance:e,script:n.script,stdout:c.stdout,stderr:c.stderr,exit_code:c.exitCode}):c.exitCode!==0&&o.error(`Script exited with code ${c.exitCode}`),process.exit(c.exitCode??1)})}L();import{createInterface as Jt}from"readline";function Qe(t){let e=Jt({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,i=>{e.close(),n(i.trim())})})}function et(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&m(!0);let i=n.key??process.env.GIBIL_API_KEY;i||(i=await Qe("Enter your API key: ")),i||(o.error("No API key provided."),process.exit(1)),i.startsWith("pk_")||(o.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),o.info("Verifying API key...");try{let r=await H(i);await J(i),n.json?o.json({authenticated:!0,email:r.user.email,plan:r.user.plan}):(o.info(h.authSuccess),o.detail("Email",r.user.email),o.detail("Plan",r.user.plan),o.detail("Limits",`${r.limits.max_concurrent} concurrent servers, ${r.limits.remaining_hours}h remaining`))}catch(r){o.error(r instanceof Error?r.message:String(r)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let i=n.token;i||(i=await Qe("Enter your Hetzner API token: ")),i||(o.error("No token provided."),process.exit(1));try{let a=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${i}`}})).json();a.error&&(o.error(`Invalid token: ${a.error.message}`),process.exit(1))}catch{o.error("Could not verify token with Hetzner API."),process.exit(1)}await V(i),o.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await ae(),o.info(h.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&m(!0);let i=await C();if(!i){n.json?o.json({authenticated:!1}):o.info(`Not logged in. Run ${g("gibil auth login")} to authenticate.`);return}try{let r=await H(i);n.json?o.json({authenticated:!0,email:r.user.email,plan:r.user.plan,limits:r.limits}):(o.success(`Authenticated as ${r.user.email}`),o.detail("Plan",r.user.plan),o.detail("Concurrent servers",String(r.limits.max_concurrent)),o.detail("Hours remaining",String(r.limits.remaining_hours)))}catch{n.json?o.json({authenticated:!1,error:"Key verification failed"}):o.error(`Stored API key is invalid. Run ${g("gibil auth login")} to re-authenticate.`)}})}L();function tt(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&m(!0);let n=await C();n||(o.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let i=await le(n);if(e.json)o.json(i);else{let r=Math.round(i.vm_hours_used/i.vm_hours_limit*100);o.info(`Plan: ${i.plan}`),o.info(`VM hours: ${i.vm_hours_used.toFixed(1)} / ${i.vm_hours_limit}h (${r}%)`),o.info(`Active instances: ${i.active_instances} / ${i.max_concurrent}`),r>80&&o.warn("Running low on hours. Upgrade at https://gibil.dev/pricing")}}catch(i){o.error(i instanceof Error?i.message:String(i)),process.exit(1)}})}import{McpServer as Vt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Wt}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as x}from"zod";var Y;function U(t,e=3e4){return P({instanceName:Y.name,ip:Y.ip,command:t,stream:!1,timeoutMs:e})}async function nt(t){if(Y=await E(t),Y.gitIdentity){let{name:i,email:r,signingKey:a}=Y.gitIdentity,s=[`git config --global user.name '${i.replace(/'/g,"'\\''")}'`,`git config --global user.email '${r.replace(/'/g,"'\\''")}'`];a&&s.push("git config --global gpg.format ssh",`git config --global user.signingkey 'key::${a.replace(/'/g,"'\\''")}'`,"git config --global commit.gpgsign true"),U(s.join(" && ")).catch(()=>{})}let e=new Vt({name:`gibil-${t}`,version:"0.1.0"});e.tool("vm_bash","Run a shell command on the remote VM. Use for: builds, tests, git, package managers, any shell operation.",{command:x.string().describe("Shell command to execute"),working_dir:x.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:x.number().optional().describe("Timeout in ms (default: 30000)")},async({command:i,working_dir:r,timeout_ms:a})=>{let c=await U(`cd ${r??"/root/project"} 2>/dev/null || cd /root && ${i}`,a??3e4);return{content:[{type:"text",text:[c.stdout,c.stderr].filter(Boolean).join(`
19
+ `)||"(no output)"}],isError:c.exitCode!==0}}),e.tool("vm_read","Read a file from the remote VM. Returns the file contents.",{path:x.string().describe("Absolute path on the VM (e.g. /root/project/src/app.ts)"),offset:x.number().optional().describe("Start at line N (1-based)"),limit:x.number().optional().describe("Max lines to return")},async({path:i,offset:r,limit:a})=>{let s=`cat -n '${i.replace(/'/g,"'\\''")}'`;r&&a?s=`sed -n '${r},${r+a-1}p' '${i.replace(/'/g,"'\\''")}' | cat -n`:r?s=`tail -n +${r} '${i.replace(/'/g,"'\\''")}' | cat -n`:a&&(s=`head -n ${a} '${i.replace(/'/g,"'\\''")}' | cat -n`);let c=await U(s);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:c.stdout}]}}),e.tool("vm_write","Write content to a file on the remote VM. Creates parent directories if needed. Overwrites existing files.",{path:x.string().describe("Absolute path on the VM"),content:x.string().describe("File content to write")},async({path:i,content:r})=>{let a=Buffer.from(r).toString("base64"),s=`mkdir -p "$(dirname '${i.replace(/'/g,"'\\''")}')" && echo '${a}' | base64 -d > '${i.replace(/'/g,"'\\''")}'`,c=await U(s);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:`Wrote ${i}`}]}}),e.tool("vm_ls","List files and directories on the remote VM.",{path:x.string().optional().describe("Directory path (default: /root/project)"),glob:x.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')")},async({path:i,glob:r})=>{let a=i??"/root/project",s;r?s=`cd '${a.replace(/'/g,"'\\''")}' && find . -path './${r}' -type f 2>/dev/null | sort | head -200`:s=`ls -la '${a.replace(/'/g,"'\\''")}'`;let c=await U(s);return c.exitCode!==0?{content:[{type:"text",text:`Error: ${c.stderr}`}],isError:!0}:{content:[{type:"text",text:c.stdout}]}}),e.tool("vm_grep","Search for a pattern in files on the remote VM. Uses ripgrep if available, falls back to grep.",{pattern:x.string().describe("Regex pattern to search for"),path:x.string().optional().describe("Directory or file to search in (default: /root/project)"),include:x.string().optional().describe("File glob to include (e.g. '*.ts')")},async({pattern:i,path:r,include:a})=>{let s=r??"/root/project",c=i.replace(/'/g,"'\\''"),l=s.replace(/'/g,"'\\''"),p;if(a){let d=a.replace(/'/g,"'\\''");p=`cd '${l}' && (rg -n --glob '${d}' '${c}' 2>/dev/null || grep -rn --include='${d}' '${c}' .) | head -100`}else p=`cd '${l}' && (rg -n '${c}' 2>/dev/null || grep -rn '${c}' .) | head -100`;return{content:[{type:"text",text:(await U(p)).stdout||"(no matches)"}]}});let n=new Wt;await e.connect(n)}function rt(t){t.command("mcp <name>").description("Start an MCP server for a running instance (used by Claude Code)").action(async e=>{try{await nt(e)}catch(n){o.error(n instanceof Error?n.message:String(n)),process.exit(1)}})}L();import{createInterface as Yt}from"readline";import{execSync as Zt}from"child_process";import{existsSync as Xt}from"fs";import{join as Qt}from"path";M();function me(t){let e=Yt({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,i=>{e.close(),n(i.trim())})})}async function en(){let t=!!await ce(),e=!!await C();return{hetzner:t,apiKey:e}}function it(t){t.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").action(async e=>{console.error(we);let n=await en();if(n.hetzner&&!e.force){o.info(`${_} Already configured.`),n.apiKey?(o.detail("Hetzner",N("connected")),o.detail("Gibil API",N("connected"))):(o.detail("Hetzner",N("connected")),o.detail("Gibil API",u("not configured (optional)"))),o.info(""),o.info(` Run ${g("gibil init --force")} to reconfigure.`),o.info(` Run ${g("gibil create")} to forge a server.`);return}o.info(""),o.info(g("Step 1: Hetzner API Token")),o.info(u(" Your servers run on Hetzner Cloud. You need an API token.")),o.info(u(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),o.info("");let i=await me(" Hetzner API token: ");i||(o.error("No token provided. Run gibil init again when ready."),process.exit(1));let r=o.spin("Verifying Hetzner token...");try{let l=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${i}`}})).json();l.error&&(r.fail(`Invalid token: ${l.error.message}`),process.exit(1)),r.succeed("Hetzner token verified")}catch{r.fail("Could not reach Hetzner API. Check your network."),process.exit(1)}await V(i),o.info(""),o.info(g("Step 2: Gibil API Key")+u(" (optional)")),o.info(u(" For usage tracking and plan limits.")),o.info(u(" Skip this if you just want to use your own Hetzner token.")),o.info("");let a=await me(" Gibil API key (or press Enter to skip): ");if(a)if(!a.startsWith("pk_"))o.warn('API keys start with "pk_". Skipping.');else{let c=o.spin("Verifying API key...");try{let l=await H(a);await J(a),c.succeed(`Logged in as ${l.user.email} (${l.user.plan})`)}catch(l){c.fail(`Could not verify key: ${l instanceof Error?l.message:String(l)}`),o.info(u(" Continuing without Gibil API. You can add it later with: gibil auth login"))}}else o.info(u(" Skipped. You can add it later with: gibil auth login"));if(o.info(""),o.info(h.initComplete),o.info(""),o.info(u(" Quick start:")),o.info(` ${g("gibil create")} ${u("Forge a server")}`),o.info(` ${g("gibil create --repo github.com/you/project")} ${u("Clone a repo on boot")}`),o.info(` ${g("gibil ssh <name>")} ${u("Connect to it")}`),o.info(` ${g("gibil destroy <name>")} ${u("Burn it down")}`),o.info(""),(await me(" Install the gibil agent skill? (Y/n): ")).toLowerCase()!=="n"){let c=o.spin("Installing gibil skill...");try{Zt("npx -y skills add https://github.com/AlexikM/gibil-skills --skill gibil -y -g",{stdio:"pipe",timeout:3e4}),c.succeed("Gibil skill installed for your AI agents")}catch{c.fail("Could not install skill automatically"),o.info(u(" Install manually: npx skills add https://github.com/AlexikM/gibil-skills --skill gibil"))}}else o.info(u(" Skipped. Install later: npx skills add https://github.com/AlexikM/gibil-skills --skill gibil"))})}async function ot(){if(process.env.HETZNER_API_TOKEN)return!1;let t=Qt(y.root,"config.json");return!Xt(t)}try{await import("dotenv/config")}catch{}var k=new tn;k.name("gibil").description("Ephemeral dev compute for humans and AI agents").version(`0.1.2 ${Z}`,"-v, --version").addHelpText("before",`
20
+ ${$e}
21
+ `).addHelpText("after",`
22
+ ${u("Docs:")} https://gibil.dev/docs
23
+ `);it(k);Ue(k);Be(k);qe(k);Ve(k);We(k);Ze(k);Xe(k);et(k);tt(k);rt(k);async function nn(){let t=process.argv.slice(2);!(t.length===0||t.includes("init")||t.includes("auth")||t.includes("--help")||t.includes("-h")||t.includes("--version")||t.includes("-v")||t.includes("-V")||t.includes("mcp")||t.includes("ssh")||t.includes("run")||t.includes("exec")||t.includes("list")||t.includes("ls"))&&await ot()&&(o.info(""),o.info(h.setupNeeded),o.info(""),process.exit(1));try{await k.parseAsync(process.argv)}catch(n){n instanceof Error&&o.error(n.message),process.exit(1)}}nn();
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "gibil",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Ephemeral dev compute for humans and AI agents",
5
+ "homepage": "https://gibil.dev",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "gibil": "./dist/index.js"
8
9
  },
9
10
  "files": [
10
- "dist"
11
+ "dist",
12
+ "LICENSE"
11
13
  ],
12
14
  "scripts": {
13
15
  "build": "tsup",
@@ -23,19 +25,26 @@
23
25
  "ephemeral",
24
26
  "compute",
25
27
  "cli",
26
- "ai-agent"
28
+ "ai-agent",
29
+ "mcp",
30
+ "remote-server",
31
+ "ssh",
32
+ "hetzner",
33
+ "claude-code",
34
+ "dev-environment"
27
35
  ],
28
- "author": "Alex Mouradian",
36
+ "author": "AlexikM",
29
37
  "license": "SEE LICENSE IN LICENSE",
30
38
  "dependencies": {
31
39
  "@modelcontextprotocol/sdk": "^1.27.1",
32
40
  "commander": "^14.0.3",
33
- "dotenv": "^17.3.1",
41
+ "picocolors": "^1.1.1",
34
42
  "ssh2": "^1.17.0",
35
43
  "yaml": "^2.8.2",
36
44
  "zod": "^4.3.6"
37
45
  },
38
46
  "devDependencies": {
47
+ "dotenv": "^17.3.1",
39
48
  "@types/node": "^25.5.0",
40
49
  "@types/ssh2": "^1.15.5",
41
50
  "tsup": "^8.5.1",