gibil 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/dist/cli/index.js +26 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,30 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Gibil</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><strong>
|
|
7
|
+
<p align="center"><strong>The box. 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.3.
|
|
11
|
-
<img src="https://img.shields.io/badge/tests-
|
|
10
|
+
<img src="https://img.shields.io/badge/version-0.3.3-blue" alt="Version 0.3.3" />
|
|
11
|
+
<img src="https://img.shields.io/badge/tests-326%20passing-brightgreen" alt="Tests: 326 passing" />
|
|
12
12
|
<img src="https://img.shields.io/badge/typescript-%3E5.0-3178C6" alt="TypeScript" />
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D20-339933" alt="Node >= 20" />
|
|
14
14
|
<img src="https://img.shields.io/badge/license-proprietary-red" alt="License: Proprietary" />
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Your own machine, on demand. Root, Docker, SSH, your repo — ready in one command.<br/>
|
|
19
|
+
Forge for 30 minutes, 30 days, or anywhere in between. You set the timer.
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
## The Problem
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Your code needs a machine somewhere — to run a branch, run an agent, run a test suite, run a long-lived dev session. The usual options force a tradeoff: container sandboxes strip out half of the OS, microvm sandboxes cap at 8 vCPU and 24-hour sessions, raw cloud VMs take a half-hour of setup, and persistent VPSes leak state and bill you when idle.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**Gibil is just the box.** A fresh server with root, Docker, SSH, your repo cloned — forged in seconds, kept alive for as long as you set the TTL, gone when you're done. Today: a real Ubuntu Linux box on Hetzner Cloud at ~$0.008/hr ([list price](https://www.hetzner.com/cloud/)). On the roadmap: more clouds, more shapes — the [CloudProvider interface](src/providers/) is built for swap.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
You're deep in a feature on `main` and Slack pings: "can you check why tests fail on `feat/payments`?" `gibil branch feat/payments` gives the branch its own box. Your local stays on `main`. Flow intact.
|
|
31
31
|
|
|
32
32
|
## 30-Second Demo
|
|
33
33
|
|
|
@@ -49,12 +49,12 @@ npm install -g gibil
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
# Forge a server with your repo cloned and ready
|
|
52
|
-
gibil create --name my-app --repo github.com/you/project --ttl
|
|
52
|
+
gibil create --name my-app --repo github.com/you/project --ttl 30m
|
|
53
53
|
|
|
54
54
|
# Let your agent work on it
|
|
55
55
|
gibil run my-app "pnpm install && pnpm test"
|
|
56
56
|
|
|
57
|
-
# Or give
|
|
57
|
+
# Or give any AI agent direct access via MCP
|
|
58
58
|
gibil mcp my-app
|
|
59
59
|
|
|
60
60
|
# Burn it when done
|
|
@@ -63,14 +63,15 @@ gibil destroy my-app
|
|
|
63
63
|
|
|
64
64
|
## Why Gibil?
|
|
65
65
|
|
|
66
|
-
- **
|
|
66
|
+
- **A real machine, not a stripped sandbox** — own kernel, full root, Docker-in-Docker, real systemd. Today's image is Ubuntu on Hetzner; the architecture targets any cloud, any shape.
|
|
67
|
+
- **Don't switch branches** — `gibil branch feat/X` gives the branch its own machine. Your local stays on `main`.
|
|
67
68
|
- **Run your agent remotely** — `--agent claude` installs Claude Code on the server. Also supports `aider` and `codex`. Direct filesystem access, no MCP latency.
|
|
68
69
|
- **SSH when it breaks** — `gibil ssh <name>` drops you into a real terminal. Debug live, not from CI logs.
|
|
69
70
|
- **Preview a branch** — `--port 3000` tunnels the app to localhost. Open your browser, see the branch running live.
|
|
70
71
|
- **Parallel branches** — `gibil branch feat/A feat/B feat/C` boots three servers in parallel. Zero interference.
|
|
71
|
-
- **MCP built in** — `gibil mcp` gives
|
|
72
|
-
- **Ephemeral by
|
|
73
|
-
-
|
|
72
|
+
- **MCP built in** — `gibil mcp` gives any MCP-compatible agent direct access to a remote server.
|
|
73
|
+
- **Ephemeral by default, persistent by choice** — set a TTL from `15m` to `30d`, or extend it later. The box lives as long as the work does.
|
|
74
|
+
- **BYOC** — bring your own cloud. Your code stays in your account, not ours. Today: Hetzner. Multi-cloud on the roadmap.
|
|
74
75
|
|
|
75
76
|
## Commands
|
|
76
77
|
|
|
@@ -95,11 +96,11 @@ gibil destroy my-app
|
|
|
95
96
|
|
|
96
97
|
### Option A: Agent on your laptop (MCP)
|
|
97
98
|
|
|
98
|
-
Your agent runs locally and reaches into the server via MCP tools. Good for quick tasks where you want to watch the agent work.
|
|
99
|
+
Your agent runs locally and reaches into the server via MCP tools. Good for quick tasks where you want to watch the agent work. Works with any MCP-compatible agent.
|
|
99
100
|
|
|
100
101
|
```bash
|
|
101
102
|
gibil create --name my-app --repo github.com/you/project
|
|
102
|
-
gibil mcp #
|
|
103
|
+
gibil mcp # Your agent gets vm_bash, vm_read, vm_write tools
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
| Pros | Cons |
|
|
@@ -163,18 +164,19 @@ Both approaches tunnel traffic through SSH — encrypted, no public ports expose
|
|
|
163
164
|
Every command supports `--json` for programmatic use:
|
|
164
165
|
|
|
165
166
|
```bash
|
|
166
|
-
gibil create --name task --repo https://github.com/user/repo --json --ttl
|
|
167
|
+
gibil create --name task --repo https://github.com/user/repo --json --ttl 30m
|
|
167
168
|
gibil run task "cd /root/project && pnpm install && pnpm test" --json
|
|
168
169
|
gibil destroy task --json
|
|
169
170
|
```
|
|
170
171
|
|
|
171
|
-
##
|
|
172
|
+
## Integrations
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
- **Agent skill** — works with Claude Code, Cursor, Copilot, Gemini CLI, and [40+ other agents](https://agentskills.io):
|
|
175
|
+
```bash
|
|
176
|
+
npx skills add https://github.com/AlexikM/gibil-skills --skill gibil
|
|
177
|
+
```
|
|
178
|
+
- **VS Code extension** — sidebar with live TTL countdowns, SSH terminal, run / extend / destroy actions. See [vscode-extension/](vscode-extension/).
|
|
179
|
+
- **Sandcastle provider** — run Sandcastle agent loops on disposable gibil VMs instead of local Docker. See [integrations/sandcastle/](integrations/sandcastle/).
|
|
178
180
|
|
|
179
181
|
## Links
|
|
180
182
|
|
package/dist/cli/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`)}function
|
|
4
|
-
${
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
${
|
|
2
|
+
var Fn=Object.defineProperty;var ae=(t,e)=>()=>(t&&(e=t(t=0)),e);var lt=(t,e)=>{for(var n in e)Fn(t,n,{get:e[n],enumerable:!0})};import he from"picocolors";function Ge(t,e){let n=Math.max(t.length+4,...e.map(c=>ut(c).length+4)),o=`${f("\u256D")}${f("\u2500".repeat(n))}${f("\u256E")}`,i=`${f("\u2570")}${f("\u2500".repeat(n))}${f("\u256F")}`,a=`${f("\u2502")} ${P} ${h(t)}${" ".repeat(n-ut(t).length-4)}${f("\u2502")}`,s=`${f("\u251C")}${f("\u2500".repeat(n))}${f("\u2524")}`,l=e.map(c=>{let u=n-ut(c).length-2;return`${f("\u2502")} ${c}${" ".repeat(Math.max(0,u))}${f("\u2502")}`});return[o,a,s,...l,i].join(`
|
|
3
|
+
`)}function ut(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var ee,Gn,te,Un,f,h,P,B,je,jt,Fe,Pt,Nt,Ct,Ce,E,D=ae(()=>{"use strict";ee=t=>he.red(t),Gn=t=>he.yellow(t),te=t=>he.green(t),Un=t=>he.red(t),f=t=>he.dim(t),h=t=>he.bold(t),P="\u{1F98E}",B=te("\u2713"),je=Un("\u2716"),jt=Gn("\u26A0"),Fe="\u{12248}",Pt=`
|
|
4
|
+
${ee(" /\\")}
|
|
5
|
+
${ee(" / \\")}
|
|
6
|
+
${ee(" / \u{1F525} \\")}
|
|
7
|
+
${ee(" / \\")}
|
|
8
8
|
${f(" ~~~~~~~~")}
|
|
9
|
-
${
|
|
10
|
-
`,
|
|
9
|
+
${h(" g i b i l")} ${f(Fe)}
|
|
10
|
+
`,Nt=`${P} ${h("gibil")} ${f(Fe)}`,Ct=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],Ce=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=ee(Ct[this.frame%Ct.length]);process.stderr.write(`\r ${e} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
|
|
11
11
|
`),this)}update(e){this.text=e,process.stderr.isTTY||process.stderr.write(` ${e}
|
|
12
|
-
`)}succeed(e){this.stop(),process.stderr.write(`\r ${
|
|
13
|
-
`)}fail(e){this.stop(),process.stderr.write(`\r ${
|
|
14
|
-
`)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};E={welcome:`${
|
|
15
|
-
Your Hetzner token may be invalid or expired. Run: gibil init --force`:a.status===409&&
|
|
16
|
-
A server with this name already exists. Try a different --name or run: gibil destroy <name>`:a.status===422&&(
|
|
17
|
-
This server type may not be available in your region. Run: gibil init --force`:a.status===429&&(
|
|
18
|
-
Rate limited by Hetzner. Wait a moment and retry your command.`),new Error(`Hetzner API error (${a.status}): ${c}${l}`)}return a.status===204?{}:await a.json()}async createServer(e,n,o,i,a){if(!i||!a){let{getServerDefaults:d}=await Promise.resolve().then(()=>(q(),ze)),m=await d();i=i??m.serverType,a=a??m.location}if(i.startsWith("cax")&&!["fsn1","nbg1"].includes(a))throw new Error(`ARM server type "${i}" is not available in "${a}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let l=typeof n=="string"?parseInt(n,10):n,u={name:e,server_type:i,image:"ubuntu-24.04",ssh_keys:[l],labels:{gibil:"true","gibil-name":e},location:a};r.debug(`createServer payload: ${JSON.stringify({name:e,server_type:i,image:"ubuntu-24.04",location:a})}`),o&&(u.user_data=o);try{let d=await this.request("POST","/servers",u);return ht(d.server)}catch(d){let m=`(server_type=${i}, location=${a}). Try a different --server-type or --location.`;throw d instanceof Error?new Error(`${d.message} ${m}`):d}}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){let n=await this.request("GET",`/servers/${e}`);return ht(n.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(ht)}async waitForReady(e,n=12e4){let o=Date.now(),i=3e3;for(;Date.now()-o<n;){let a=await this.getServer(e);if(a.status==="running"&&a.ipv4!=="0.0.0.0")return a;r.debug(`Server ${e} status: ${a.status}, waiting...`),await new Promise(s=>setTimeout(s,i))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let o=await this.request("POST","/ssh_keys",{name:e,public_key:n});return eo(o.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}}});import{readFile as yo,writeFile as vo,mkdir as zt,rm as qt,readdir as wo,rename as bo}from"fs/promises";import{existsSync as Bt}from"fs";import{join as yt}from"path";async function wt(t,e,n){let o=`${t}.tmp`;await vo(o,e,n);try{await bo(o,t)}catch(i){throw await qt(o,{force:!0}).catch(a=>{console.warn(`Warning: failed to clean up temp file ${o}: ${a}`)}),i}}var vt,Ne,ce,We,C,le,Y,A=se(()=>{"use strict";F();vt=class{instancesDir;keysDir;constructor(e){let n=e??_.root;this.instancesDir=yt(n,"instances"),this.keysDir=yt(n,"keys")}async ensureDirectories(){await zt(this.instancesDir,{recursive:!0,mode:448}),await zt(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return yt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await wt(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!Bt(n))return null;let o=await yo(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);Bt(n)&&await qt(n)}async list(){await this.ensureDirectories();let e=await wo(this.instancesDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),a=await this.load(i);a&&n.push(a)}return n}},Ne=new vt,ce=t=>Ne.save(t),We=t=>Ne.loadOrThrow(t),C=t=>Ne.loadActiveOrThrow(t),le=t=>Ne.delete(t),Y=()=>Ne.list()});var tn={};at(tn,{JobStore:()=>Qe,deleteJob:()=>Po,deleteJobsByInstance:()=>He,listJobs:()=>be,listJobsByInstance:()=>No,loadJob:()=>jo,loadJobOrThrow:()=>ie,saveJob:()=>G});import{readFile as Eo,mkdir as Xt,rm as Co,readdir as To}from"fs/promises";import{existsSync as Qt}from"fs";import{join as en}from"path";var Qe,de,G,jo,ie,Po,be,No,He,me=se(()=>{"use strict";F();A();Qe=class{jobsDir;constructor(e){let n=e??_.root;this.jobsDir=en(n,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return en(this.jobsDir,`${e}.json`)}async save(e){await Xt(this.jobsDir,{recursive:!0,mode:448}),await wt(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.jobFile(e);if(!Qt(n))return null;let o=await Eo(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(e){let n=this.jobFile(e);Qt(n)&&await Co(n)}async list(){await Xt(this.jobsDir,{recursive:!0,mode:448});let e=await To(this.jobsDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),a=await this.load(i);a&&n.push(a)}return n}async listByInstance(e){return(await this.list()).filter(o=>o.instance===e)}async deleteByInstance(e){let n=await this.listByInstance(e);for(let o of n)await this.delete(o.id)}},de=new Qe,G=t=>de.save(t),jo=t=>de.load(t),ie=t=>de.loadOrThrow(t),Po=t=>de.delete(t),be=()=>de.list(),No=t=>de.listByInstance(t),He=t=>de.deleteByInstance(t)});import{Command as gr}from"commander";import{readFileSync as hr}from"fs";import{fileURLToPath as yr}from"url";import{dirname as vr,join as wr}from"path";he();F();import{mkdir as to,rm as Rt,readFile as no,chmod as oo}from"fs/promises";import{existsSync as Ht}from"fs";import{execFile as ro}from"child_process";import{promisify as io}from"util";var so=io(ro);async function Be(t){let e=_.keyDir(t);Ht(e)&&await Rt(e,{recursive:!0}),await to(e,{recursive:!0});let n=_.privateKey(t),o=_.publicKey(t);await so("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await oo(n,384);let i=await no(o,"utf-8");return{privateKeyPath:n,publicKeyPath:o,publicKey:i.trim()}}async function Z(t){let e=_.keyDir(t);Ht(e)&&await Rt(e,{recursive:!0})}F();k();H();import{Client as Mt}from"ssh2";import{readFile as Dt}from"fs/promises";async function x(t){let{instanceName:e,ip:n,command:o,stream:i=!1,timeoutMs:a=3e4}=t,s=await Dt(_.privateKey(e),"utf-8");return new Promise((c,l)=>{let u=new Mt,d="",m="",p=null,g=!1;u.on("ready",()=>{r.debug(`SSH connected to ${n}`),u.exec(o,(h,v)=>{if(h)return u.end(),l(h);p=setTimeout(()=>{g||(g=!0,u.destroy(),l(new Error(`Command timed out after ${a/1e3}s on ${n}`)))},a),v.on("data",$=>{let w=$.toString();d+=w,i&&process.stdout.write(w)}),v.stderr.on("data",$=>{let w=$.toString();m+=w,i&&process.stderr.write(w)}),v.on("close",$=>{p&&clearTimeout(p),!g&&(g=!0,u.end(),c({stdout:d,stderr:m,exitCode:$??0}))})})}).on("error",h=>{if(p&&clearTimeout(p),g)return;g=!0;let v="";h.code==="ECONNREFUSED"?v=" (instance may have been destroyed or is still booting)":h.code==="EHOSTUNREACH"?v=" (IP unreachable \u2014 instance may not be running)":h.code==="ETIMEDOUT"&&(v=" (connection timed out \u2014 check if instance is running with 'gibil list')"),l(new Error(`SSH connection to ${n} failed: ${h.message}${v}`))}).connect({host:n,port:22,username:"root",privateKey:s,readyTimeout:a,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}function Kt(t){let{instanceName:e,ip:n,filePath:o,timeoutMs:i=3e4}=t,a=null,s=!1;return(async()=>{try{let c=await Dt(_.privateKey(e),"utf-8");a=new Mt,await new Promise((l,u)=>{a.on("ready",()=>{a.exec(`tail -f ${o} 2>/dev/null`,(d,m)=>{if(d)return a.end(),u(d);m.on("data",p=>{s||process.stdout.write(f(p.toString()))}),m.stderr.on("data",p=>{s||process.stderr.write(f(p.toString()))}),m.on("close",()=>{a.end(),l()})})}).on("error",d=>{s||r.debug(`Verbose log tail failed: ${d.message}`),u(d)}).connect({host:n,port:22,username:"root",privateKey:c,readyTimeout:i,hostVerifier:()=>!0})})}catch{}})(),{abort(){if(s=!0,a)try{a.end()}catch{}}}}async function qe(t,e,n=12e4){let o=Date.now(),i=5e3;for(;Date.now()-o<n;)try{await x({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{r.debug(`SSH not ready on ${e}, retrying...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function ae(t){let{repo:e,config:n,ttlMinutes:o,githubToken:i,gitIdentity:a}=t,s=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];i&&s.push(`export GITHUB_TOKEN=${M(i)}`),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(...ao(c)),t.agent){let l=uo(t.agent);l&&(s.push(`# Install ${t.agent} + tmux`),t.agent==="aider"&&s.push("apt-get install -y -qq python3-pip > /dev/null 2>&1"),s.push(`${l} > /dev/null 2>&1`,"apt-get install -y -qq tmux > /dev/null 2>&1",""))}if(n?.services&&n.services.length>0){s.push(...co()),s.push("");for(let l of n.services)s.push(...lo(l))}if(n?.env){s.push("# Environment variables");for(let[l,u]of Object.entries(n.env))s.push(`export ${l}=${M(u)}`),s.push(`echo ${M(`${l}=${u}`)} >> /etc/environment`);s.push("")}if(s.push("# Configure git"),a?(s.push(`git config --global user.email ${M(a.email)}`),s.push(`git config --global user.name ${M(a.name)}`),a.signingKey&&(s.push("git config --global gpg.format ssh"),s.push(`git config --global user.signingkey ${M("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 ${M(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\/([^/]+\/[^/.]+)/);if(s.push("# Clone repository"),s.push("cd /root"),l){let u=l[1];s.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),s.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),s.push("else"),s.push(` CLONE_URL='https://github.com/${u}.git'`),s.push("fi"),s.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else s.push(`timeout 300 git clone ${M(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(o&&o>0&&(s.push("# Auto-destroy after TTL"),s.push(`echo "shutdown -h now" | at now + ${o} minutes 2>/dev/null || true`),s.push(`(sleep ${o*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: '${M(l.name)}`),s.push(`if ! ${l.command}; then`),s.push(` echo '\u2717 Task failed: '${M(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(`
|
|
19
|
-
`)}function ao(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 co(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function lo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let o=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(o+=` -p ${t.port}:${t.port}`),t.env)for(let[i,a]of Object.entries(t.env))o+=` -e ${i}=${M(a)}`;return o+=` ${M(t.image)}`,e.push(o),e.push(""),e}var Lt={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},ye={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},W=Object.keys(Lt);function uo(t){return Lt[t]??null}function M(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as mo}from"fs/promises";import{existsSync as Ft,statSync as fo}from"fs";import{join as po}from"path";import{parse as Ut}from"yaml";async function ve(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,n,o]=e,i=`https://raw.githubusercontent.com/${n}/${o}/HEAD/.gibil.yml`;try{let a={};process.env.GITHUB_TOKEN&&(a.Authorization=`token ${process.env.GITHUB_TOKEN}`);let s=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:a});if(!s.ok)return null;let c=await s.text();return ho(c)}catch{return null}}var go=".gibil.yml";async function we(t){let e;if(Ft(t)&&fo(t).isFile()?e=t:e=po(t,go),!Ft(e))return null;let n=await mo(e,"utf-8"),o=Ut(n);return Jt(o)}function ho(t){let e=Ut(t);return Jt(e)}function Jt(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:i.name,image:i.image,port:typeof i.port=="number"?i.port:void 0,env:Gt(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:i.name,command:i.command}})),e.env!==void 0&&(n.env=Gt(e.env,"top-level")),n}function Gt(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[o,i]of Object.entries(t))if(typeof i=="string")n[o]=i;else if(typeof i=="number"||typeof i=="boolean")n[o]=String(i);else throw new Error(`env.${o} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(n).length>0?n:void 0}A();import{randomBytes as $o}from"crypto";function ue(t=6){return $o(Math.ceil(t/2)).toString("hex").slice(0,t)}function Ae(){return`gibil-${ue()}`}function Wt(){return`fleet-${ue(8)}`}function Ye(){return`j-${ue(8)}`}F();k();var xo=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function Ve(t){if(!xo.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 Oe(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}q();H();import{execSync as Ze}from"child_process";import{readFileSync as So}from"fs";var bt="key::";function _o(){try{let t=Ze("git config user.name",{encoding:"utf-8"}).trim(),e=Ze("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(Ze("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=Ze("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=So(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(bt))&&(n=i.startsWith(bt)?i.slice(bt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function Xe(t,e,n){r.step("Generating SSH keys...");let o=await Be(e),i,a;try{r.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${ue(4)}`,o.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&r.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let s=_o(),c=ae({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:s,agent:n.agent}),l=r.spin("Creating server on Hetzner..."),u=await t.createServer(e,i.id,c,n.serverType??n.config?.server_type,n.location??n.config?.location);a=u.id,l.succeed("Server created");let d=r.spin("VM booting..."),p=(await t.waitForReady(u.id)).ipv4;d.succeed(`VM running at ${p}`);let g=new Date,h={name:e,serverId:u.id,ip:p,sshKeyId:i.id,keyPath:_.privateKey(e),status:"running",createdAt:g.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(g.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:s};await ce(h);let v=r.spin("Waiting for SSH...");if(await qe(e,p),v.succeed("SSH ready"),n.repo||n.config){let $=r.spin("Provisioning (runtime, repo, deps)..."),w;n.verbose&&!n.json&&(w=Kt({instanceName:e,ip:p,filePath:"/var/log/cloud-init-output.log"}));let b=36e4,O=5e3,R=Date.now(),pe=!1;for(;Date.now()-R<b;){try{if((await x({instanceName:e,ip:p,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){pe=!0;break}}catch{}await new Promise(L=>setTimeout(L,O))}if(w?.abort(),pe)$.succeed("Provisioning complete");else{$.fail("Provisioning may have failed");try{let L=await x({instanceName:e,ip:p,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});r.info(L.stdout)}catch{r.warn("Could not read cloud-init log.")}}}return h}catch(s){throw r.error(`Failed to create instance "${e}", cleaning up...`),a&&await t.destroyServer(a).catch(c=>r.warn(`Could not destroy Hetzner server ${a}: ${c instanceof Error?c.message:String(c)}`)),i&&await t.deleteSSHKey(i.id).catch(c=>r.warn(`Could not delete Hetzner SSH key ${i.id}: ${c instanceof Error?c.message:String(c)}`)),await Z(e).catch(c=>r.warn(`Could not clean up local SSH keys: ${c instanceof Error?c.message:String(c)}`)),s}}function Yt(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`ssh -i ${t.keyPath} -o StrictHostKeyChecking=no root@${t.ip}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}function Vt(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").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async e=>{e.json&&I(!0);let n=Oe(e.ttl??"60","TTL");if(n>10080)throw new Error("TTL cannot exceed 7 days (10080 minutes).");let o=Oe(e.fleet??"1","Fleet count");if(o>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&Ve(e.name),!e.agent){let l=await Pe();l&&(e.agent=l)}if(e.agent&&!W.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${W.join(", ")}`);let i={};if(e.env)for(let l of e.env){let u=l.indexOf("=");if(u<=0)throw new Error(`Invalid --env format: "${l}". Use KEY=VALUE.`);i[l.slice(0,u)]=l.slice(u+1)}i.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=i.GITHUB_TOKEN);let a=null;if(e.config?a=await we(e.config):e.repo?a=await ve(e.repo)??await we(process.cwd()):a=await we(process.cwd()),Object.keys(i).length>0&&(a||(a={}),a.env={...a.env,...i}),e.dryRun){let l=e.name??Ae(),u=e.serverType??a?.server_type??"cx22",d=e.location??a?.location??"nbg1",m=a?.image??"node:20",p=ae({repo:e.repo,config:a??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),g={name:l,serverType:u,location:d,image:m,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:p};e.json?r.json(g):(r.info(""),r.info(y("Dry run \u2014 no server will be created")),r.info(""),r.info(` ${f("Name:")} ${l}`),r.info(` ${f("Server type:")} ${u}`),r.info(` ${f("Location:")} ${d}`),r.info(` ${f("Image:")} ${m}`),r.info(` ${f("TTL:")} ${n} minutes`),e.repo&&r.info(` ${f("Repo:")} ${e.repo}`),e.agent&&r.info(` ${f("Agent:")} ${e.agent}`),r.info(""),r.info("Cloud-init script:"),r.info("\u2500".repeat(17)),r.info(p));return}let s=await P();if(s){r.info("Verifying API key...");let l=await re(s);r.info(` Authenticated as ${l.user.email} (${l.user.plan})`)}if(e.agent&&!ye[e.agent]?.some(u=>a?.env?.[u]||i[u])){let u=ye[e.agent]?.join(" or ")??"";r.warn(`${e.agent} needs ${u}. SSH in and export it (recommended) or pass with --env.`)}let c=await N.create();if(o===1){let l=e.name??Ae(),u=Date.now(),d=r.spin(`Forging "${l}"...`),m=await Xe(c,l,{repo:e.repo,ttlMinutes:n,config:a,serverType:e.serverType,location:e.location,agent:e.agent,verbose:e.verbose}),p=((Date.now()-u)/1e3).toFixed(1);d.succeed(E.createReady(l,p)),s&&await V(s,"create",m.name,e.serverType).catch(g=>r.debug(`Usage tracking failed: ${g instanceof Error?g.message:String(g)}`)),e.json?r.json(Yt(m)):(r.info(""),r.info(Fe("Server ready",[`${f("Name:")} ${y(m.name)}`,`${f("IP:")} ${m.ip}`,`${f("TTL:")} ${n} minutes`,`${f("SSH:")} ${y(`gibil ssh ${m.name}`)}`])),r.info(""),r.info(f(" Try:")),r.info(` ${y(`gibil run ${m.name} "<your test command>"`)}`),r.info(` ${y(`gibil ssh ${m.name}`)}`),r.info(` ${y(`gibil destroy ${m.name}`)}`),r.info(""))}else{let l=Wt(),u=e.name??"gibil",d=Date.now(),m=r.spin(`Forging fleet "${l}" \u2014 ${o} servers...`),p=Array.from({length:o},(w,b)=>`${u}-${b+1}-${l.slice(6)}`),g=await Promise.allSettled(p.map(w=>Xe(c,w,{repo:e.repo,ttlMinutes:n,config:a,serverType:e.serverType,location:e.location,fleetId:l,agent:e.agent,verbose:e.verbose}))),h=[],v=[];for(let w=0;w<g.length;w++){let b=g[w];b.status==="fulfilled"?h.push(b.value):v.push(`${p[w]}: ${b.reason instanceof Error?b.reason.message:String(b.reason)}`)}let $=((Date.now()-d)/1e3).toFixed(1);if(m.succeed(E.fleetReady(h.length,o)+` ${f(`(${$}s)`)}`),s&&await Promise.all(h.map(w=>V(s,"create",w.name,e.serverType).catch(b=>r.debug(`Usage tracking failed for ${w.name}: ${b instanceof Error?b.message:String(b)}`)))),e.json)r.json({fleet_id:l,instances:h.map(Yt),errors:v});else{r.info("");for(let w of h)r.info(` ${z} ${y(w.name)} ${f("\u2192")} ${w.ip}`);for(let w of v)r.info(` ${Ce} ${w}`);r.info("")}}})}A();import{spawn as Oo}from"child_process";import{spawn as Io}from"child_process";import{existsSync as ko}from"fs";function Re(t){if(t.includes(":")){let e=t.split(":");if(e.length!==3)throw new Error(`Invalid port mapping "${t}". Use PORT or LOCAL:HOST:REMOTE.`);let[n,,o]=e;$t(n,t),$t(o,t)}else $t(t,t)}function $t(t,e){let n=parseInt(t,10);if(isNaN(n)||n<1||n>65535||String(n)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}function Zt(t,e){if(!ko(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=[];for(let o of e){Re(o);let i=o.includes(":")?o:`${o}:localhost:${o}`,a=o.includes(":")?o.split(":")[0]:o;n.push(a),Io("ssh",["-f","-N","-L",i,"-i",t.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes",`root@${t.ip}`],{stdio:"ignore",detached:!0}).unref()}return n}k();H();k();A();me();var Ao=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function fe(t){if(!(t instanceof Error))return!1;let e=t.message;return Ao.some(n=>e.includes(n))}async function U(t){try{let{HetznerProvider:e}=await Promise.resolve().then(()=>(he(),Ot)),{getHetznerToken:n}=await Promise.resolve().then(()=>(q(),ze)),o=await n();return o?(await(await e.create(o)).getServer(t.serverId),"still_exists"):"api_error"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await Z(t.name),await He(t.name),await le(t.name),r.warn(`Instance "${t.name}" no longer exists on Hetzner \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function nn(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000 --port 5432)").action(async(e,n)=>{let o=await C(e),i=["-A","-i",o.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];if(n.port&&n.port.length>0){for(let s of n.port){Re(s);let c=s.includes(":")?s:`${s}:localhost:${s}`;i.push("-L",c)}r.info("");for(let s of n.port){let c=s.includes(":")?s.split(":")[0]:s;r.info(` Forwarding ${y(`localhost:${c}`)} \u2192 ${e}:${s}`)}r.info(""),r.info(f(" Tunnel active while SSH session is open. Ctrl+C to stop.")),r.info("")}i.push(`root@${o.ip}`),Oo("ssh",i,{stdio:"inherit"}).on("exit",s=>{let c=()=>process.exit(s??0);s===255?U(o).then(c,c):c()})})}A();me();k();function on(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").option("--timeout <seconds>","Command timeout in seconds (default: 30)").option("-b, --background","Run in background, return job ID immediately").action(async(e,n,o)=>{o.json&&I(!0);let i=await C(e),a=n.join(" "),s=o.timeout?Oe(o.timeout,"Timeout")*1e3:3e4;if(o.background){let l=Ye(),u="/root/.gibil-jobs",d=`${u}/${l}.log`,m=`${u}/${l}.exit`,p=`${u}/${l}.pid`,g=`${u}/${l}.sh`,h=["#!/bin/bash",`nohup bash -c '${a.replace(/'/g,"'\\''")}' > ${d} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${p}`,`(wait $BGPID 2>/dev/null; echo $? > ${m}) &`,"echo $BGPID"].join(`
|
|
20
|
-
`),v=Buffer.from(
|
|
12
|
+
`)}succeed(e){this.stop(),process.stderr.write(`\r ${B} ${e??this.text}
|
|
13
|
+
`)}fail(e){this.stop(),process.stderr.write(`\r ${je} ${e??this.text}
|
|
14
|
+
`)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};E={welcome:`${P} Your first fire. Welcome to Gibil.`,noInstances:`${P} No fires burning. Gibil sleeps.`,destroyAll:`${P} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${P} "${t}" \u2014 fire out.`,authSuccess:`${P} Logged in. The forge is yours.`,authLogout:`${P} Logged out. The forge cools.`,createReady:(t,e)=>`${P} "${t}" forged ${f(`(${e}s)`)}`,fleetReady:(t,e)=>`${P} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${P} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${P} The forge is ready. Run ${h("gibil create")} to light your first fire.`,setupNeeded:`${P} No forge configured. Run ${h("gibil init")} to get started.`}});function I(t){dt=t}function ne(t){return dt&&t!=="error"?!1:At[t]>=At[Jn]}var Jn,dt,At,r,k=ae(()=>{"use strict";D();Jn="info",dt=!1,At={debug:0,info:1,warn:2,error:3,silent:4};r={debug(t,...e){ne("debug")&&console.debug(`${f("[debug]")} ${t}`,...e)},info(t,...e){ne("info")&&console.log(t,...e)},warn(t,...e){ne("warn")&&console.warn(`${jt} ${t}`,...e)},error(t,...e){ne("error")&&console.error(`${je} ${t}`,...e)},success(t){ne("info")&&console.log(`${B} ${t}`)},step(t){ne("info")&&console.log(` ${f("\u203A")} ${t}`)},flame(t){ne("info")&&console.log(t)},detail(t,e){ne("info")&&console.log(` ${f(t+":")} ${e}`)},spin(t){return dt?new Ce(t):new Ce(t).start()},json(t){console.log(JSON.stringify(t,null,2))}}});import{homedir as zn}from"os";import{join as re,resolve as Bn}from"path";import{existsSync as qn}from"fs";function Ue(){let t=process.argv[1];if(t){let e=Bn(t);if(qn(e))return{command:process.execPath,args:[e,"mcp"]}}return{command:"gibil",args:["mcp"]}}var oe,_,F=ae(()=>{"use strict";oe=re(zn(),".gibil"),_={root:oe,instances:re(oe,"instances"),keys:re(oe,"keys"),jobs:re(oe,"jobs"),instanceFile:t=>re(oe,"instances",`${t}.json`),keyDir:t=>re(oe,"keys",t),privateKey:t=>re(oe,"keys",t,"id_ed25519"),publicKey:t=>re(oe,"keys",t,"id_ed25519.pub")}});var Be={};lt(Be,{clearApiKey:()=>pt,fetchUsage:()=>yt,getApiKey:()=>N,getApiUrl:()=>Qn,getApiUrlFromConfig:()=>Je,getDefaultAgent:()=>Ae,getHetznerToken:()=>gt,getServerDefaults:()=>eo,saveApiKey:()=>ft,saveDefaultAgent:()=>ze,saveHetznerToken:()=>Ne,saveServerDefaults:()=>ht,trackUsage:()=>Z,verifyApiKey:()=>ie});import{readFile as Yn,writeFile as Wn,mkdir as Vn}from"fs/promises";import{existsSync as Zn}from"fs";import{join as Xn}from"path";async function q(){if(!Zn(mt))return{};let t=await Yn(mt,"utf-8");return JSON.parse(t)}async function Pe(t){await Vn(_.root,{recursive:!0,mode:448}),await Wn(mt,JSON.stringify(t,null,2),{mode:384})}async function ft(t){let e=await q();e.api_key=t,await Pe(e)}async function N(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await q()).api_key??null}async function pt(){let t=await q();delete t.api_key,await Pe(t)}function Qn(){return process.env.GIBIL_API_URL??Rt}async function Je(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await q()).api_url??Rt}async function Ne(t){let e=await q();e.hetzner_token=t,await Pe(e)}async function gt(){return process.env.HETZNER_API_TOKEN?process.env.HETZNER_API_TOKEN:(await q()).hetzner_token??null}async function ht(t,e){let n=await q();n.default_server_type=t,n.default_location=e,await Pe(n)}async function eo(){let t=await q();return{serverType:t.default_server_type??"cax11",location:t.default_location??"fsn1"}}async function ze(t){let e=await q();t?e.default_agent=t:delete e.default_agent,await Pe(e)}async function Ae(){return(await q()).default_agent??null}async function ie(t){let e=await Je(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let o=await n.text();throw new Error(`API error (${n.status}): ${o}`)}return await n.json()}async function Z(t,e,n,o){let i=await Je(),a=await fetch(`${i}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:o})});if(a.status===429)throw new Error("Usage limit reached. Please try again later or contact support.");if(!a.ok){let s=await a.text();throw new Error(`Usage tracking failed (${a.status}): ${s}`)}}async function yt(t){let e=await Je(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let o=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${o}`)}return await n.json()}var mt,Rt,Y=ae(()=>{"use strict";F();mt=Xn(_.root,"config.json"),Rt="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1"});var Ot={};lt(Ot,{HetznerProvider:()=>A});function vt(t){return{id:t.id,name:t.name,status:t.status,ipv4:t.public_net.ipv4.ip,ipv6:t.public_net.ipv6.ip,serverType:t.server_type?.name??"unknown",location:t.datacenter?.location?.name??"unknown",labels:t.labels,created:t.created}}function no(t){return{id:t.id,name:t.name,fingerprint:t.fingerprint??""}}var to,A,ye=ae(()=>{"use strict";k();to="https://api.hetzner.cloud/v1";A=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(Y(),Be)),o=e??await n();if(!o)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(o)}async request(e,n,o){let i=`${to}${n}`;r.debug(`${e} ${i}`);let a=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:o?JSON.stringify(o):void 0,signal:AbortSignal.timeout(3e4)});if(!a.ok){let s=await a.text(),l;try{l=JSON.parse(s).error?.message??s}catch{l=s}let c="";throw a.status===401||a.status===403?c=`
|
|
15
|
+
Your Hetzner token may be invalid or expired. Run: gibil init --force`:a.status===409&&l.includes("name")?c=`
|
|
16
|
+
A server with this name already exists. Try a different --name or run: gibil destroy <name>`:a.status===422&&(l.includes("location")||l.includes("server_type"))?c=`
|
|
17
|
+
This server type may not be available in your region. Run: gibil init --force`:a.status===429&&(c=`
|
|
18
|
+
Rate limited by Hetzner. Wait a moment and retry your command.`),new Error(`Hetzner API error (${a.status}): ${l}${c}`)}return a.status===204?{}:await a.json()}async createServer(e,n,o,i,a){if(!i||!a){let{getServerDefaults:d}=await Promise.resolve().then(()=>(Y(),Be)),m=await d();i=i??m.serverType,a=a??m.location}if(i.startsWith("cax")&&!["fsn1","nbg1"].includes(a))throw new Error(`ARM server type "${i}" is not available in "${a}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let c=typeof n=="string"?parseInt(n,10):n,u={name:e,server_type:i,image:"ubuntu-24.04",ssh_keys:[c],labels:{gibil:"true","gibil-name":e},location:a};r.debug(`createServer payload: ${JSON.stringify({name:e,server_type:i,image:"ubuntu-24.04",location:a})}`),o&&(u.user_data=o);try{let d=await this.request("POST","/servers",u);return vt(d.server)}catch(d){let m=`(server_type=${i}, location=${a}). Try a different --server-type or --location.`;throw d instanceof Error?new Error(`${d.message} ${m}`):d}}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){let n=await this.request("GET",`/servers/${e}`);return vt(n.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(vt)}async waitForReady(e,n=12e4){let o=Date.now(),i=3e3;for(;Date.now()-o<n;){let a=await this.getServer(e);if(a.status==="running"&&a.ipv4!=="0.0.0.0")return a;r.debug(`Server ${e} status: ${a.status}, waiting...`),await new Promise(s=>setTimeout(s,i))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let o=await this.request("POST","/ssh_keys",{name:e,public_key:n});return no(o.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}}});import{readFile as wo,writeFile as bo,mkdir as Bt,rm as Yt,readdir as $o,rename as xo}from"fs/promises";import{existsSync as qt}from"fs";import{join as wt}from"path";async function $t(t,e,n){let o=`${t}.tmp`;await bo(o,e,n);try{await xo(o,t)}catch(i){throw await Yt(o,{force:!0}).catch(a=>{console.warn(`Warning: failed to clean up temp file ${o}: ${a}`)}),i}}var bt,Re,le,We,T,ue,V,R=ae(()=>{"use strict";F();bt=class{instancesDir;keysDir;constructor(e){let n=e??_.root;this.instancesDir=wt(n,"instances"),this.keysDir=wt(n,"keys")}async ensureDirectories(){await Bt(this.instancesDir,{recursive:!0,mode:448}),await Bt(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return wt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await $t(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!qt(n))return null;let o=await wo(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);qt(n)&&await Yt(n)}async list(){await this.ensureDirectories();let e=await $o(this.instancesDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),a=await this.load(i);a&&n.push(a)}return n}},Re=new bt,le=t=>Re.save(t),We=t=>Re.loadOrThrow(t),T=t=>Re.loadActiveOrThrow(t),ue=t=>Re.delete(t),V=()=>Re.list()});var nn={};lt(nn,{JobStore:()=>tt,deleteJob:()=>Ro,deleteJobsByInstance:()=>He,listJobs:()=>xe,listJobsByInstance:()=>Oo,loadJob:()=>Ao,loadJobOrThrow:()=>se,saveJob:()=>U});import{readFile as jo,mkdir as Qt,rm as Po,readdir as No}from"fs/promises";import{existsSync as en}from"fs";import{join as tn}from"path";var tt,me,U,Ao,se,Ro,xe,Oo,He,fe=ae(()=>{"use strict";F();R();tt=class{jobsDir;constructor(e){let n=e??_.root;this.jobsDir=tn(n,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return tn(this.jobsDir,`${e}.json`)}async save(e){await Qt(this.jobsDir,{recursive:!0,mode:448}),await $t(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.jobFile(e);if(!en(n))return null;let o=await jo(n,"utf-8");return JSON.parse(o)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(e){let n=this.jobFile(e);en(n)&&await Po(n)}async list(){await Qt(this.jobsDir,{recursive:!0,mode:448});let e=await No(this.jobsDir),n=[];for(let o of e){if(!o.endsWith(".json"))continue;let i=o.replace(".json",""),a=await this.load(i);a&&n.push(a)}return n}async listByInstance(e){return(await this.list()).filter(o=>o.instance===e)}async deleteByInstance(e){let n=await this.listByInstance(e);for(let o of n)await this.delete(o.id)}},me=new tt,U=t=>me.save(t),Ao=t=>me.load(t),se=t=>me.loadOrThrow(t),Ro=t=>me.delete(t),xe=()=>me.list(),Oo=t=>me.listByInstance(t),He=t=>me.deleteByInstance(t)});import{Command as wr}from"commander";import{readFileSync as br}from"fs";import{fileURLToPath as $r}from"url";import{dirname as xr,join as Sr}from"path";ye();F();import{mkdir as oo,rm as Mt,readFile as ro,chmod as io}from"fs/promises";import{existsSync as Ht}from"fs";import{execFile as so}from"child_process";import{promisify as ao}from"util";var co=ao(so);async function qe(t){let e=_.keyDir(t);Ht(e)&&await Mt(e,{recursive:!0}),await oo(e,{recursive:!0});let n=_.privateKey(t),o=_.publicKey(t);await co("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await io(n,384);let i=await ro(o,"utf-8");return{privateKeyPath:n,publicKeyPath:o,publicKey:i.trim()}}async function X(t){let e=_.keyDir(t);Ht(e)&&await Mt(e,{recursive:!0})}F();k();D();import{Client as Dt}from"ssh2";import{readFile as Lt}from"fs/promises";async function x(t){let{instanceName:e,ip:n,command:o,stream:i=!1,timeoutMs:a=3e4}=t,s=await Lt(_.privateKey(e),"utf-8");return new Promise((l,c)=>{let u=new Dt,d="",m="",p=null,g=!1;u.on("ready",()=>{r.debug(`SSH connected to ${n}`),u.exec(o,(y,v)=>{if(y)return u.end(),c(y);p=setTimeout(()=>{g||(g=!0,u.destroy(),c(new Error(`Command timed out after ${a/1e3}s on ${n}`)))},a),v.on("data",$=>{let w=$.toString();d+=w,i&&process.stdout.write(w)}),v.stderr.on("data",$=>{let w=$.toString();m+=w,i&&process.stderr.write(w)}),v.on("close",$=>{p&&clearTimeout(p),!g&&(g=!0,u.end(),l({stdout:d,stderr:m,exitCode:$??0}))})})}).on("error",y=>{if(p&&clearTimeout(p),g)return;g=!0;let v="";y.code==="ECONNREFUSED"?v=" (instance may have been destroyed or is still booting)":y.code==="EHOSTUNREACH"?v=" (IP unreachable \u2014 instance may not be running)":y.code==="ETIMEDOUT"&&(v=" (connection timed out \u2014 check if instance is running with 'gibil list')"),c(new Error(`SSH connection to ${n} failed: ${y.message}${v}`))}).connect({host:n,port:22,username:"root",privateKey:s,readyTimeout:a,hostVerifier:()=>!0,agent:process.env.SSH_AUTH_SOCK,agentForward:!0})})}function Kt(t){let{instanceName:e,ip:n,filePath:o,timeoutMs:i=3e4}=t,a=null,s=!1;return(async()=>{try{let l=await Lt(_.privateKey(e),"utf-8");a=new Dt,await new Promise((c,u)=>{a.on("ready",()=>{a.exec(`tail -f ${o} 2>/dev/null`,(d,m)=>{if(d)return a.end(),u(d);m.on("data",p=>{s||process.stdout.write(f(p.toString()))}),m.stderr.on("data",p=>{s||process.stderr.write(f(p.toString()))}),m.on("close",()=>{a.end(),c()})})}).on("error",d=>{s||r.debug(`Verbose log tail failed: ${d.message}`),u(d)}).connect({host:n,port:22,username:"root",privateKey:l,readyTimeout:i,hostVerifier:()=>!0})})}catch{}})(),{abort(){if(s=!0,a)try{a.end()}catch{}}}}async function Ye(t,e,n=12e4){let o=Date.now(),i=5e3;for(;Date.now()-o<n;)try{await x({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{r.debug(`SSH not ready on ${e}, retrying...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function ce(t){let{repo:e,config:n,ttlMinutes:o,githubToken:i,gitIdentity:a}=t,s=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];i&&s.push(`export GITHUB_TOKEN=${L(i)}`),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 l=n?.image??"node:20";if(s.push(...lo(l)),t.agent){let c=fo(t.agent);c&&(s.push(`# Install ${t.agent} + tmux`),t.agent==="aider"&&s.push("apt-get install -y -qq python3-pip > /dev/null 2>&1"),s.push(`${c} > /dev/null 2>&1`,"apt-get install -y -qq tmux > /dev/null 2>&1",""))}if(n?.services&&n.services.length>0){s.push(...uo()),s.push("");for(let c of n.services)s.push(...mo(c))}if(n?.env){s.push("# Environment variables");for(let[c,u]of Object.entries(n.env))s.push(`export ${c}=${L(u)}`),s.push(`echo ${L(`${c}=${u}`)} >> /etc/environment`);s.push("")}if(s.push("# Configure git"),a?(s.push(`git config --global user.email ${L(a.email)}`),s.push(`git config --global user.name ${L(a.name)}`),a.signingKey&&(s.push("git config --global gpg.format ssh"),s.push(`git config --global user.signingkey ${L("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 ${L(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 c=e.match(/github\.com\/([^/]+\/[^/.]+)/);if(s.push("# Clone repository"),s.push("cd /root"),c){let u=c[1];s.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),s.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),s.push("else"),s.push(` CLONE_URL='https://github.com/${u}.git'`),s.push("fi"),s.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else s.push(`timeout 300 git clone ${L(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'),c&&s.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${c[1]}.git"`),s.push("fi"),s.push("")}if(o&&o>0&&(s.push("# Auto-destroy after TTL"),s.push(`echo "shutdown -h now" | at now + ${o} minutes 2>/dev/null || true`),s.push(`(sleep ${o*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 c of n.tasks)s.push(`echo '\u25B6 Running task: '${L(c.name)}`),s.push(`if ! ${c.command}; then`),s.push(` echo '\u2717 Task failed: '${L(c.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(`
|
|
19
|
+
`)}function lo(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 uo(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function mo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let o=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(o+=` -p ${t.port}:${t.port}`),t.env)for(let[i,a]of Object.entries(t.env))o+=` -e ${i}=${L(a)}`;return o+=` ${L(t.image)}`,e.push(o),e.push(""),e}var Ft={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},ve={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},W=Object.keys(Ft);function fo(t){return Ft[t]??null}function L(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as po}from"fs/promises";import{existsSync as Gt,statSync as go}from"fs";import{join as ho}from"path";import{parse as Jt}from"yaml";async function we(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,n,o]=e,i=`https://raw.githubusercontent.com/${n}/${o}/HEAD/.gibil.yml`;try{let a={};process.env.GITHUB_TOKEN&&(a.Authorization=`token ${process.env.GITHUB_TOKEN}`);let s=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:a});if(!s.ok)return null;let l=await s.text();return vo(l)}catch{return null}}var yo=".gibil.yml";async function be(t){let e;if(Gt(t)&&go(t).isFile()?e=t:e=ho(t,yo),!Gt(e))return null;let n=await po(e,"utf-8"),o=Jt(n);return zt(o)}function vo(t){let e=Jt(t);return zt(e)}function zt(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:i.name,image:i.image,port:typeof i.port=="number"?i.port:void 0,env:Ut(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(o=>{let i=o;if(typeof i.name!="string"||typeof i.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:i.name,command:i.command}})),e.env!==void 0&&(n.env=Ut(e.env,"top-level")),n}function Ut(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[o,i]of Object.entries(t))if(typeof i=="string")n[o]=i;else if(typeof i=="number"||typeof i=="boolean")n[o]=String(i);else throw new Error(`env.${o} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(n).length>0?n:void 0}R();import{randomBytes as So}from"crypto";function de(t=6){return So(Math.ceil(t/2)).toString("hex").slice(0,t)}function Oe(){return`gibil-${de()}`}function Wt(){return`fleet-${de(8)}`}function Ve(){return`j-${de(8)}`}F();k();var _o=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function Ze(t){if(!_o.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 Xe(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}var G=525600,Io={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function $e(t){let e=t.trim().toLowerCase();if(/^\d+$/.test(e)){let l=parseInt(e,10);if(l<=0)throw new Error(`TTL must be positive, got "${t}"`);if(l>G)throw new Error(`TTL cannot exceed 1 year (${G} minutes). Got ${l} minutes.`);return l}let n=e.match(/^(\d+)(mo|[mhdwy])$/);if(!n)throw new Error(`Invalid TTL "${t}". Use a number (minutes) or a duration: 2h, 7d, 1w, 1mo, 3mo, 6mo, 1y`);let o=parseInt(n[1],10),i=n[2],a=Io[i],s=o*a;if(s<=0)throw new Error(`TTL must be positive, got "${t}"`);if(s>G)throw new Error(`TTL cannot exceed 1 year (${G} minutes). Got "${t}" = ${s} minutes.`);return s}Y();D();import{execSync as Qe}from"child_process";import{readFileSync as ko}from"fs";var xt="key::";function Eo(){try{let t=Qe("git config user.name",{encoding:"utf-8"}).trim(),e=Qe("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(Qe("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=Qe("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=ko(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(xt))&&(n=i.startsWith(xt)?i.slice(xt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function et(t,e,n){r.step("Generating SSH keys...");let o=await qe(e),i,a;try{r.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${de(4)}`,o.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&r.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let s=Eo(),l=ce({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:s,agent:n.agent}),c=r.spin("Creating server on Hetzner..."),u=await t.createServer(e,i.id,l,n.serverType??n.config?.server_type,n.location??n.config?.location);a=u.id,c.succeed("Server created");let d=r.spin("VM booting..."),p=(await t.waitForReady(u.id)).ipv4;d.succeed(`VM running at ${p}`);let g=new Date,y={name:e,serverId:u.id,ip:p,sshKeyId:i.id,keyPath:_.privateKey(e),status:"running",createdAt:g.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(g.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:s};await le(y);let v=r.spin("Waiting for SSH...");if(await Ye(e,p),v.succeed("SSH ready"),n.repo||n.config){let $=r.spin("Provisioning (runtime, repo, deps)..."),w;n.verbose&&!n.json&&(w=Kt({instanceName:e,ip:p,filePath:"/var/log/cloud-init-output.log"}));let b=36e4,M=5e3,H=Date.now(),ge=!1;for(;Date.now()-H<b;){try{if((await x({instanceName:e,ip:p,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){ge=!0;break}}catch{}await new Promise(j=>setTimeout(j,M))}if(w?.abort(),ge)$.succeed("Provisioning complete");else{$.fail("Provisioning may have failed");try{let j=await x({instanceName:e,ip:p,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});r.info(j.stdout)}catch{r.warn("Could not read cloud-init log.")}}}return y}catch(s){throw r.error(`Failed to create instance "${e}", cleaning up...`),a&&await t.destroyServer(a).catch(l=>r.warn(`Could not destroy Hetzner server ${a}: ${l instanceof Error?l.message:String(l)}`)),i&&await t.deleteSSHKey(i.id).catch(l=>r.warn(`Could not delete Hetzner SSH key ${i.id}: ${l instanceof Error?l.message:String(l)}`)),await X(e).catch(l=>r.warn(`Could not clean up local SSH keys: ${l instanceof Error?l.message:String(l)}`)),s}}function Vt(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`ssh -i ${t.keyPath} -o StrictHostKeyChecking=no root@${t.ip}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}function Zt(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <duration>","Auto-destroy timer (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)","60").option("-c, --config <path>","Path to .gibil.yml config").option("--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").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async e=>{e.json&&I(!0);let n=$e(e.ttl??"60"),o=Xe(e.fleet??"1","Fleet count");if(o>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&Ze(e.name),!e.agent){let c=await Ae();c&&(e.agent=c)}if(e.agent&&!W.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${W.join(", ")}`);let i={};if(e.env)for(let c of e.env){let u=c.indexOf("=");if(u<=0)throw new Error(`Invalid --env format: "${c}". Use KEY=VALUE.`);i[c.slice(0,u)]=c.slice(u+1)}i.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=i.GITHUB_TOKEN);let a=null;if(e.config?a=await be(e.config):e.repo?a=await we(e.repo)??await be(process.cwd()):a=await be(process.cwd()),Object.keys(i).length>0&&(a||(a={}),a.env={...a.env,...i}),e.dryRun){let c=e.name??Oe(),u=e.serverType??a?.server_type??"cx22",d=e.location??a?.location??"nbg1",m=a?.image??"node:20",p=ce({repo:e.repo,config:a??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),g={name:c,serverType:u,location:d,image:m,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:p};e.json?r.json(g):(r.info(""),r.info(h("Dry run \u2014 no server will be created")),r.info(""),r.info(` ${f("Name:")} ${c}`),r.info(` ${f("Server type:")} ${u}`),r.info(` ${f("Location:")} ${d}`),r.info(` ${f("Image:")} ${m}`),r.info(` ${f("TTL:")} ${n} minutes`),e.repo&&r.info(` ${f("Repo:")} ${e.repo}`),e.agent&&r.info(` ${f("Agent:")} ${e.agent}`),r.info(""),r.info("Cloud-init script:"),r.info("\u2500".repeat(17)),r.info(p));return}let s=await N();if(s){r.info("Verifying API key...");let c=await ie(s);r.info(` Authenticated as ${c.user.email} (${c.user.plan})`)}if(e.agent&&!ve[e.agent]?.some(u=>a?.env?.[u]||i[u])){let u=ve[e.agent]?.join(" or ")??"";r.warn(`${e.agent} needs ${u}. SSH in and export it (recommended) or pass with --env.`)}let l=await A.create();if(o===1){let c=e.name??Oe(),u=Date.now(),d=r.spin(`Forging "${c}"...`),m=await et(l,c,{repo:e.repo,ttlMinutes:n,config:a,serverType:e.serverType,location:e.location,agent:e.agent,verbose:e.verbose}),p=((Date.now()-u)/1e3).toFixed(1);d.succeed(E.createReady(c,p)),s&&await Z(s,"create",m.name,e.serverType).catch(g=>r.debug(`Usage tracking failed: ${g instanceof Error?g.message:String(g)}`)),e.json?r.json(Vt(m)):(r.info(""),r.info(Ge("Server ready",[`${f("Name:")} ${h(m.name)}`,`${f("IP:")} ${m.ip}`,`${f("TTL:")} ${n} minutes`,`${f("SSH:")} ${h(`gibil ssh ${m.name}`)}`])),r.info(""),r.info(f(" Try:")),r.info(` ${h(`gibil run ${m.name} "<your test command>"`)}`),r.info(` ${h(`gibil ssh ${m.name}`)}`),r.info(` ${h(`gibil destroy ${m.name}`)}`),r.info(""))}else{let c=Wt(),u=e.name??"gibil",d=Date.now(),m=r.spin(`Forging fleet "${c}" \u2014 ${o} servers...`),p=Array.from({length:o},(w,b)=>`${u}-${b+1}-${c.slice(6)}`),g=await Promise.allSettled(p.map(w=>et(l,w,{repo:e.repo,ttlMinutes:n,config:a,serverType:e.serverType,location:e.location,fleetId:c,agent:e.agent,verbose:e.verbose}))),y=[],v=[];for(let w=0;w<g.length;w++){let b=g[w];b.status==="fulfilled"?y.push(b.value):v.push(`${p[w]}: ${b.reason instanceof Error?b.reason.message:String(b.reason)}`)}let $=((Date.now()-d)/1e3).toFixed(1);if(m.succeed(E.fleetReady(y.length,o)+` ${f(`(${$}s)`)}`),s&&await Promise.all(y.map(w=>Z(s,"create",w.name,e.serverType).catch(b=>r.debug(`Usage tracking failed for ${w.name}: ${b instanceof Error?b.message:String(b)}`)))),e.json)r.json({fleet_id:c,instances:y.map(Vt),errors:v});else{r.info("");for(let w of y)r.info(` ${B} ${h(w.name)} ${f("\u2192")} ${w.ip}`);for(let w of v)r.info(` ${je} ${w}`);r.info("")}}})}R();import{spawn as Ho}from"child_process";import{spawn as To}from"child_process";import{existsSync as Co}from"fs";function Me(t){if(t.includes(":")){let e=t.split(":");if(e.length!==3)throw new Error(`Invalid port mapping "${t}". Use PORT or LOCAL:HOST:REMOTE.`);let[n,,o]=e;St(n,t),St(o,t)}else St(t,t)}function St(t,e){let n=parseInt(t,10);if(isNaN(n)||n<1||n>65535||String(n)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}function Xt(t,e){if(!Co(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=[];for(let o of e){Me(o);let i=o.includes(":")?o:`${o}:localhost:${o}`,a=o.includes(":")?o.split(":")[0]:o;n.push(a),To("ssh",["-f","-N","-L",i,"-i",t.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes",`root@${t.ip}`],{stdio:"ignore",detached:!0}).unref()}return n}k();D();k();R();fe();var Mo=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function pe(t){if(!(t instanceof Error))return!1;let e=t.message;return Mo.some(n=>e.includes(n))}async function J(t){try{let{HetznerProvider:e}=await Promise.resolve().then(()=>(ye(),Ot)),{getHetznerToken:n}=await Promise.resolve().then(()=>(Y(),Be)),o=await n();return o?(await(await e.create(o)).getServer(t.serverId),"still_exists"):"api_error"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await X(t.name),await He(t.name),await ue(t.name),r.warn(`Instance "${t.name}" no longer exists on Hetzner \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function on(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000 --port 5432)").action(async(e,n)=>{let o=await T(e),i=["-A","-i",o.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];if(n.port&&n.port.length>0){for(let s of n.port){Me(s);let l=s.includes(":")?s:`${s}:localhost:${s}`;i.push("-L",l)}r.info("");for(let s of n.port){let l=s.includes(":")?s.split(":")[0]:s;r.info(` Forwarding ${h(`localhost:${l}`)} \u2192 ${e}:${s}`)}r.info(""),r.info(f(" Tunnel active while SSH session is open. Ctrl+C to stop.")),r.info("")}i.push(`root@${o.ip}`),Ho("ssh",i,{stdio:"inherit"}).on("exit",s=>{let l=()=>process.exit(s??0);s===255?J(o).then(l,l):l()})})}R();fe();k();function rn(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").option("--timeout <seconds>","Command timeout in seconds (default: 30)").option("-b, --background","Run in background, return job ID immediately").action(async(e,n,o)=>{o.json&&I(!0);let i=await T(e),a=n.join(" "),s=o.timeout?Xe(o.timeout,"Timeout")*1e3:3e4;if(o.background){let c=Ve(),u="/root/.gibil-jobs",d=`${u}/${c}.log`,m=`${u}/${c}.exit`,p=`${u}/${c}.pid`,g=`${u}/${c}.sh`,y=["#!/bin/bash",`nohup bash -c '${a.replace(/'/g,"'\\''")}' > ${d} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${p}`,`(wait $BGPID 2>/dev/null; echo $? > ${m}) &`,"echo $BGPID"].join(`
|
|
20
|
+
`),v=Buffer.from(y).toString("base64"),$=`mkdir -p ${u} && echo '${v}' | base64 -d > ${g} && chmod +x ${g} && bash ${g}`,w;try{w=await x({instanceName:e,ip:i.ip,command:$,timeoutMs:1e4})}catch(M){throw pe(M)&&await J(i)==="cleaned"&&process.exit(1),M}let b=parseInt(w.stdout.trim(),10);isNaN(b)&&(r.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await U({id:c,instance:e,command:a,pid:b,status:"running",startedAt:new Date().toISOString()}),o.json?r.json({job_id:c,instance:e,status:"running",pid:b}):(r.info(`Background job started: ${c} (PID ${b})`),r.info(` Poll: gibil job ${c}`));return}r.info(`Running on "${e}" (${i.ip}): ${a}`);let l;try{l=await x({instanceName:e,ip:i.ip,command:a,stream:!o.json,timeoutMs:s})}catch(c){throw pe(c)&&await J(i)==="cleaned"&&process.exit(1),c}o.json?r.json({instance:e,command:a,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&r.error(`Command exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}ye();import{createInterface as Vo}from"readline";R();fe();k();Y();D();F();import{createHash as sn}from"crypto";import{readFile as un,writeFile as Do,mkdir as Lo}from"fs/promises";import{existsSync as ot,readFileSync as Ko}from"fs";import{hostname as an,userInfo as Fo,platform as Go,arch as Uo}from"os";import{join as Ie,dirname as dn}from"path";import{fork as Jo}from"child_process";import{fileURLToPath as mn}from"url";var nt=Ie(_.root,"device_id"),cn=Ie(_.root,"config.json"),zo=process.env.GIBIL_TELEMETRY_URL??"https://zopdxjruwktjyjunitrv.supabase.co/functions/v1/telemetry-ingest",Bo="tk_alpha_09a93302e0f3e73417a9e9dbfc500a61",De=null,Se=null;function qo(){try{let t=Fo(),e=`${an()}:${t.username}:${t.homedir}`;return sn("sha256").update(e).digest("hex").slice(0,16)}catch{return sn("sha256").update(`${an()}:${Date.now()}`).digest("hex").slice(0,16)}}async function fn(){if(De)return De;if(ot(nt)){let e=(await un(nt,"utf-8")).trim();if(e.length>0)return De=e,De}let t=qo();return await Lo(_.root,{recursive:!0,mode:448}),await Do(nt,t,{mode:384}),De=t,t}async function _t(){if(Se!==null)return Se;let t=process.env.GIBIL_TELEMETRY;if(t!==void 0)return Se=!["0","false","off","no"].includes(t.toLowerCase()),Se;try{if(ot(cn)&&JSON.parse(await un(cn,"utf-8")).telemetry===!1)return Se=!1,!1}catch{}return Se=!0,!0}async function pn(){return ot(nt)?!1:(await fn(),!0)}var _e=null;function Yo(){if(_e)return _e;let t=dn(mn(import.meta.url));for(let e of["../package.json","../../package.json"])try{return _e=JSON.parse(Ko(Ie(t,e),"utf-8")).version??"0.0.0",_e}catch{}return _e="0.0.0",_e}async function Le(t){if(!await _t())return;let e=await fn();if(!e)return;let n={...t,device_id:e,cli_version:Yo(),timestamp:new Date().toISOString(),os:Go(),arch:Uo(),node_version:process.version},o=dn(mn(import.meta.url)),a=[Ie(o,"telemetry-send.js"),Ie(o,"..","utils","telemetry-send.js"),Ie(o,"telemetry-send.ts")].find(s=>ot(s));if(a)try{Jo(a,{detached:!0,stdio:"ignore",...a.endsWith(".ts")?{execArgv:["--import","tsx"]}:{},env:{...process.env,TELEMETRY_PAYLOAD:JSON.stringify(n),TELEMETRY_ENDPOINT:zo,TELEMETRY_INGEST_KEY:Bo}}).unref()}catch{}}var ln=new Map,Wo=5e3;async function gn(t){let e=Date.now(),n=ln.get(t)??0;e-n<Wo||(ln.set(t,e),await Le({event:"mcp_tool",tool:t}))}function hn(t){return t.slice(3).filter(e=>e.startsWith("-")).map(e=>e.replace(/=.*$/,""))}async function Zo(t){let e=t.length;r.info(""),r.info(h(`This will destroy ${e} running server${e===1?"":"s"}:`)),r.info("");for(let i of t){let a=Math.round((Date.now()-new Date(i.createdAt).getTime())/6e4),s=a<60?`${a}m old`:`${Math.floor(a/60)}h ${a%60}m old`;r.info(` ${f("\u2022")} ${h(i.name)} ${f("\u2192")} ${i.ip} ${f(`(${s})`)}`)}r.info(""),r.info(f("Type 'yes' to confirm, anything else to cancel:"));let n=Vo({input:process.stdin,output:process.stdout}),o=await new Promise(i=>n.question("> ",i));return n.close(),o.trim().toLowerCase()==="yes"}async function yn(t,e){let n=await We(e);r.info(`Destroying instance "${e}" (server ${n.serverId})...`);try{await t.destroyServer(n.serverId)}catch(a){r.warn(`Could not delete server ${n.serverId}: ${a instanceof Error?a.message:String(a)}`)}try{await t.deleteSSHKey(n.sshKeyId)}catch(a){r.warn(`Could not delete SSH key ${n.sshKeyId}: ${a instanceof Error?a.message:String(a)}`)}await X(e),await He(e),await ue(e);let o=await N();o&&await Z(o,"destroy",e).catch(a=>r.warn(`Usage tracking failed (billing may be inaccurate): ${a instanceof Error?a.message:String(a)}`));let i=Math.round((Date.now()-new Date(n.createdAt).getTime())/6e4);await Le({event:"lifecycle",duration_minutes:i}),r.info(` ${B} ${E.destroySingle(e)}`)}function vn(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").option("-y, --yes","Skip the confirmation prompt for --all (required in non-interactive contexts)").action(async(e,n)=>{if(n.json&&I(!0),n.all){let o=await V();if(o.length===0){n.json?r.json({destroyed:[],failed:[]}):r.info(E.noInstances);return}if(!n.yes&&(n.json&&(r.json({error:"confirmation required",message:`--all would destroy ${o.length} server(s). Re-run with --yes to confirm.`,instances:o.map(d=>({name:d.name,ip:d.ip}))}),process.exit(1)),process.stdin.isTTY||(r.error("stdin is not a TTY. Re-run with --yes to confirm in non-interactive contexts."),process.exit(1)),!await Zo(o))){r.info(""),r.info("Cancelled. No servers were destroyed.");return}let i=await A.create();r.info(`Destroying ${o.length} instance(s)...`);let a=3,s=[];for(let u=0;u<o.length;u+=a){let d=o.slice(u,u+a),m=await Promise.allSettled(d.map(p=>yn(i,p.name)));s.push(...m)}let l=[],c=[];for(let u=0;u<s.length;u++)if(s[u].status==="fulfilled")l.push(o[u].name);else{let d=s[u].reason;c.push(`${o[u].name}: ${d instanceof Error?d.message:String(d)}`)}n.json?r.json({destroyed:l,failed:c}):c.length===0?r.info(`
|
|
21
21
|
${E.destroyAll}`):r.info(`
|
|
22
|
-
${
|
|
23
|
-
${f(`${o.length} server(s)`)}`)})}function
|
|
22
|
+
${l.length} destroyed, ${c.length} failed`)}else{e||(r.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1));let o=await A.create();await yn(o,e),n.json&&r.json({destroyed:[e]})}})}R();k();D();function wn(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await V();if(n.length===0){e.json?r.json({instances:[]}):r.info(E.noInstances);return}let o=n.map(i=>{let a=Math.max(0,Math.floor((new Date(i.expiresAt).getTime()-Date.now())/1e3));return{name:i.name,ip:i.ip,ssh:`ssh -i ${i.keyPath} -o StrictHostKeyChecking=no root@${i.ip}`,status:i.status,ttl_remaining:a,created_at:i.createdAt,fleet_id:i.fleetId}});if(e.json){r.json({instances:o});return}r.info(f(`${"NAME".padEnd(30)} ${"IP".padEnd(18)} ${"STATUS".padEnd(12)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),r.info(f("\u2500".repeat(80)));for(let i of o){let a=bn(i.ttl_remaining),s=Xo(i.created_at),l=i.name.padEnd(30),c=i.status.padEnd(12),u=a.padEnd(10),d=s.padEnd(10),m=i.status==="running"?te(c):ee(c),p=i.ttl_remaining<=300?ee(u):u;r.info(`${h(l)} ${i.ip.padEnd(18)} ${m} ${p} ${f(d)}`)}r.info(`
|
|
23
|
+
${f(`${o.length} server(s)`)}`)})}function bn(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=Math.floor(e/60),o=Math.floor(n/24);if(o>=1){let i=n%24;return i>0?`${o}d ${i}h`:`${o}d`}return n>=1?`${n}h ${e%60}m`:`${e}m ${t%60}s`}function Xo(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return bn(n)}R();k();function $n(t){t.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <duration>","New TTL from now (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let o=await T(e),i=$e(n.ttl);try{await x({instanceName:e,ip:o.ip,command:["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`,`(sleep ${i*60} && shutdown -h now) &`].join(" && ")})}catch(s){throw pe(s)&&await J(o)==="cleaned"&&process.exit(1),s}let a=new Date(Date.now()+i*6e4).toISOString();o.ttlMinutes=i,o.expiresAt=a,await le(o),n.json?r.json({name:o.name,ttl_minutes:i,expires_at:a}):r.info(`\u2713 Extended "${e}" TTL to ${i} minutes (expires ${a})`)})}import{readFile as Qo}from"fs/promises";import{randomBytes as er}from"crypto";R();k();function xn(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let o=await T(e),i=await Qo(n.script,"utf-8");r.info(`Uploading and running script "${n.script}" on "${e}"...`);let a=Buffer.from(i).toString("base64"),s=`/tmp/gibil-script-${er(4).toString("hex")}.sh`,l;try{l=await x({instanceName:e,ip:o.ip,command:`echo '${a}' | base64 -d > ${s} && chmod +x ${s} && ${s}; EXIT=$?; rm -f ${s}; exit $EXIT`,stream:!n.json})}catch(c){throw pe(c)&&await J(o)==="cleaned"&&process.exit(1),c}n.json?r.json({instance:e,script:n.script,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&r.error(`Script exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}Y();k();D();import{createInterface as tr}from"readline";function Sn(t){let e=tr({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,o=>{e.close(),n(o.trim())})})}function _n(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=n.key??process.env.GIBIL_API_KEY;o||(o=await Sn("Enter your API key: ")),o||(r.error("No API key provided."),process.exit(1)),o.startsWith("pk_")||(r.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),r.info("Verifying API key...");try{let i=await ie(o);await ft(o),n.json?r.json({authenticated:!0,email:i.user.email,plan:i.user.plan}):(r.info(E.authSuccess),r.detail("Email",i.user.email),r.detail("Plan","alpha (free)"))}catch(i){r.error(i instanceof Error?i.message:String(i)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let o=n.token;o||(o=await Sn("Enter your Hetzner API token: ")),o||(r.error("No token provided."),process.exit(1));try{let a=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${o}`}})).json();a.error&&(r.error(`Invalid token: ${a.error.message}`),process.exit(1))}catch(i){r.error(`Could not verify token: ${i instanceof Error?i.message:"Check your network."}`),process.exit(1)}await Ne(o),r.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await pt(),r.info(E.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=await N();if(!o){n.json?r.json({authenticated:!1}):r.info(`Not logged in. Run ${h("gibil auth login")} to authenticate.`);return}try{let i=await ie(o);n.json?r.json({authenticated:!0,email:i.user.email,plan:i.user.plan,limits:i.limits}):(r.success(`Authenticated as ${i.user.email}`),r.detail("Plan",i.user.plan),r.detail("Concurrent servers",String(i.limits.max_concurrent)),r.detail("Hours remaining",String(i.limits.remaining_hours)))}catch{n.json?r.json({authenticated:!1,error:"Key verification failed"}):r.error(`Stored API key is invalid. Run ${h("gibil auth login")} to re-authenticate.`)}})}Y();k();function In(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await N();n||(r.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let o=await yt(n);e.json?r.json(o):(r.info("Plan: alpha (free)"),r.info(`VM hours used: ${o.vm_hours_used.toFixed(1)}h`),r.info(`Active instances: ${o.active_instances}`))}catch(o){r.error(o instanceof Error?o.message:String(o)),process.exit(1)}})}import{McpServer as nr}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as or}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as S}from"zod";ye();R();F();import{execSync as rt}from"child_process";import{readFileSync as rr}from"fs";fe();R();fe();k();async function It(t){let e=await se(t);if(e.status!=="running")return{status:e.status,exitCode:e.exitCode};let n=await T(e.instance),o="/root/.gibil-jobs",i=`${o}/${t}.exit`,a=`${o}/${t}.log`,l=(await x({instanceName:e.instance,ip:n.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING"){try{if((await x({instanceName:e.instance,ip:n.ip,command:`kill -0 ${e.pid} 2>/dev/null && echo "alive" || echo "dead"`,timeoutMs:1e4})).stdout.trim()==="dead"){let y=new Date;e.status="orphaned",e.completedAt=y.toISOString(),await U(e);let v=Math.round((y.getTime()-new Date(e.startedAt).getTime())/1e3),$;try{$=(await x({instanceName:e.instance,ip:n.ip,command:`cat ${a} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:v,stdout:$}}}catch{}return{status:"running"}}let c=parseInt(l,10),u=await x({instanceName:e.instance,ip:n.ip,command:`cat ${a} 2>/dev/null || echo ''`,timeoutMs:1e4}),d=c===0?"done":"failed",m=new Date,p=Math.round((m.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=d,e.exitCode=c,e.completedAt=m.toISOString(),await U(e),{status:d,exitCode:c,stdout:u.stdout,durationS:p}}function kn(t){let e=t.command("job").description("Manage background jobs");e.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,o)=>{o.json&&I(!0);let i=await se(n),a=await It(n);o.json?r.json({job_id:n,instance:i.instance,command:i.command,status:a.status,exit_code:a.exitCode,started_at:i.startedAt,duration_s:a.durationS,...a.stdout!==void 0?{stdout:a.stdout}:{}}):a.status==="running"?(r.info(`Job ${n} is still running on "${i.instance}"`),r.info(` Command: ${i.command}`),r.info(` Started: ${i.startedAt}`)):a.status==="orphaned"?(r.warn(`Job ${n} is orphaned \u2014 process died without writing exit code`),r.info(` Instance: ${i.instance}`),r.info(` Command: ${i.command}`),a.durationS!==void 0&&r.info(` Duration: ${a.durationS}s`),a.stdout&&(r.info(" Output:"),process.stdout.write(a.stdout))):(r.info(`Job ${n}: ${a.status} (exit code ${a.exitCode}, ${a.durationS}s)`),a.stdout&&process.stdout.write(a.stdout))}),e.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let o=await xe(),i=await V(),a=new Set(i.map(s=>s.name));for(let s of o)s.status==="running"&&!a.has(s.instance)&&(s.status="orphaned",s.completedAt=new Date().toISOString(),await U(s));if(o.length===0){n.json?r.json([]):r.info("No background jobs.");return}if(n.json)r.json(o.map(s=>({job_id:s.id,instance:s.instance,command:s.command,status:s.status,started_at:s.startedAt,exit_code:s.exitCode})));else for(let s of o){let l=s.status==="running"?"\u27F3 running":s.status==="done"?"\u2713 done":s.status==="orphaned"?"\u26A0 orphaned":`\u2717 ${s.status}`;r.info(` ${s.id} ${l} ${s.instance} ${s.command}`)}}),e.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,o)=>{o.json&&I(!0);let i=await se(n);if(i.status!=="running"){o.json?r.json({job_id:n,status:i.status,message:"Job is not running"}):r.info(`Job ${n} is not running (status: ${i.status})`);return}let a=await T(i.instance);await x({instanceName:i.instance,ip:a.ip,command:`kill -- -${i.pid} 2>/dev/null || kill ${i.pid} 2>/dev/null || true`,timeoutMs:1e4}),i.status="cancelled",i.completedAt=new Date().toISOString(),await U(i),o.json?r.json({job_id:n,status:"cancelled"}):r.info(`Job ${n} cancelled.`)}),e.command("logs <id>").description("Fetch output of a background job").option("--json","Output result as JSON").option("-f, --follow","Follow log output (tail -f)").action(async(n,o)=>{o.json&&I(!0);let i=await se(n),a=await T(i.instance),s=`/root/.gibil-jobs/${n}.log`,l=o.follow?`tail -f ${s}`:`cat ${s} 2>/dev/null || echo '(no output yet)'`,c=o.follow?3e5:1e4,u=await x({instanceName:i.instance,ip:a.ip,command:l,stream:!o.json,timeoutMs:c});o.json&&r.json({job_id:n,stdout:u.stdout})})}function O(t,e,n){let o=`[${t}] ${e}`;return n&&(o+=`
|
|
24
24
|
|
|
25
|
-
Suggestion: ${n}`),o}function Ke(t){let e=t instanceof Error?t.message:String(t);return/no active server|not found|no servers|does not exist/i.test(e)?"instance_not_found":/timeout|timed out|ETIMEDOUT/i.test(e)?"timeout":/ECONNREFUSED|EHOSTUNREACH|SSH connection|ssh2|handshake/i.test(e)?"ssh_connection":/hetzner|api|token|401|403|409|422|429|quota/i.test(e)?"provider_error":/invalid|must be|validation/i.test(e)?"validation":"provider_error"}var
|
|
26
|
-
${(await x({instanceName:v,ip:
|
|
27
|
-
`),
|
|
28
|
-
`)||"(no output)"}],isError:m.exitCode!==0}}),o.tool("vm_job_status","Check the status of a background job started with vm_bash(background=true). Returns status, exit code, and output when done.",{job_id:S.string().describe("Job ID returned by vm_bash with background=true")},async
|
|
29
|
-
`),u.succeed("MCP configured for Claude Code")}catch{u.fail("Could not auto-configure MCP"),r.info(f(" Run gibil mcp --print-config for manual setup"))}r.info(""),r.info(
|
|
30
|
-
${
|
|
25
|
+
Suggestion: ${n}`),o}function Ke(t){let e=t instanceof Error?t.message:String(t);return/no active server|not found|no servers|does not exist/i.test(e)?"instance_not_found":/timeout|timed out|ETIMEDOUT/i.test(e)?"timeout":/ECONNREFUSED|EHOSTUNREACH|SSH connection|ssh2|handshake/i.test(e)?"ssh_connection":/hetzner|api|token|401|403|409|422|429|quota/i.test(e)?"provider_error":/invalid|must be|validation/i.test(e)?"validation":"provider_error"}var it="key::";function ir(){try{let t=rt("git config user.name",{encoding:"utf-8"}).trim(),e=rt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(rt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=rt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=rr(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(it))&&(n=i.startsWith(it)?i.slice(it.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function ke(t,e){if(t)return t;if(e)return T(e);let o=(await V()).filter(i=>new Date<new Date(i.expiresAt));if(o.length===0)throw new Error("No active servers. Use create_server first.");if(o.length===1)return o[0];throw new Error(`Multiple servers running: ${o.map(i=>i.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function Q(t,e,n=3e4){return x({instanceName:t.name,ip:t.ip,command:e,stream:!1,timeoutMs:n})}function K(t){return`'${t.replace(/'/g,"'\\''")}'`}function sr(t){let e=t.trim(),n=e.split(",");if(n.length<5)throw new Error(`Unexpected vm_stats output (expected 5 comma-separated sections, got ${n.length}): ${e}`);let o=parseInt(n[0],10);if(isNaN(o))throw new Error(`Failed to parse CPU cores: ${n[0]}`);let i=n[1].trim().split(/\s+/),a=parseFloat(i[0]),s=parseFloat(i[1]),l=parseFloat(i[2]);if(isNaN(a)||isNaN(s)||isNaN(l))throw new Error(`Failed to parse load averages: ${n[1]}`);let c=n[2].trim().split(/\s+/),u=parseInt(c[0],10),d=parseInt(c[1],10),m=parseInt(c[2],10);if(isNaN(u)||isNaN(d)||isNaN(m))throw new Error(`Failed to parse memory: ${n[2]}`);let p=n[3].trim().split(/\s+/),g=b=>Math.round(parseFloat(b.replace(/G$/i,""))),y=g(p[0]),v=g(p[1]),$=g(p[2]);if(isNaN(y)||isNaN(v)||isNaN($))throw new Error(`Failed to parse disk: ${n[3]}`);let w=parseInt(n[4].trim(),10);if(isNaN(w))throw new Error(`Failed to parse uptime: ${n[4]}`);return{cpu:{cores:o,load_1m:a,load_5m:s,load_15m:l},memory:{total_mb:u,used_mb:d,available_mb:m},disk:{total_gb:y,used_gb:v,available_gb:$},uptime_seconds:w}}async function En(t){let e=null;if(t&&(e=await T(t),e.gitIdentity)){let{name:l,email:c,signingKey:u}=e.gitIdentity,d=[`git config --global user.name ${K(l)}`,`git config --global user.email ${K(c)}`];u&&d.push("git config --global gpg.format ssh",`git config --global user.signingkey ${K(it+u)}`,"git config --global commit.gpgsign true"),Q(e,d.join(" && ")).catch(()=>{})}let n=t?`gibil-${t}`:"gibil",o=new nr({name:n,version:"0.4.0"}),i=o.tool.bind(o);o.tool=((...l)=>{let c=l[0],u=l.length-1;if(typeof l[u]=="function"){let d=l[u];l[u]=async(...m)=>(gn(c).catch(()=>{}),d(...m))}return i(...l)}),e||(o.tool("create_server","Forge a new ephemeral server with a full Linux environment (Ubuntu 24.04, Node.js 20, pnpm). Clones the repo to /root/project and waits until fully provisioned. After creation, use vm_bash to run commands, vm_read/vm_write for files, vm_grep to search code. Destroy with destroy_server when done.",{name:S.string().optional().describe("Server name (auto-generated if omitted)"),repo:S.string().optional().describe("Git repo URL to clone on boot"),ttl:S.number().optional().describe("Auto-destroy after N minutes (default: 60, max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)"),server_type:S.string().optional().describe("Hetzner server type (default: auto-detected)"),location:S.string().optional().describe("Hetzner datacenter (default: auto-detected)"),env:S.record(S.string(),S.string()).optional().describe("Environment variables to set on the server")},async({name:l,repo:c,ttl:u,server_type:d,location:m,env:p})=>{let g=null,y=null,v=null,$=null;try{v=l??Oe(),l&&Ze(l),$=await A.create();let w=await qe(v),b=await $.createSSHKey(`gibil-${v}-${de(4)}`,w.publicKey);g=b;let M=ir(),H=c?await we(c):null;p&&Object.keys(p).length>0&&(H||(H={}),H.env={...H.env,...p});let ge=(H?.services?.length??0)>0,j=u??(ge?120:60);if(j<1||j>G)return{content:[{type:"text",text:O("validation",`TTL must be between 1 and ${G} minutes (1 year)`,`Pass a ttl value between 1 and ${G}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let Ee=ce({repo:c,config:H??void 0,ttlMinutes:j,githubToken:process.env.GITHUB_TOKEN,gitIdentity:M}),st=await $.createServer(v,b.id,Ee,d,m);y=st.id;let Te=(await $.waitForReady(st.id)).ipv4,Et=new Date,Ln={name:v,serverId:st.id,ip:Te,sshKeyId:b.id,keyPath:_.privateKey(v),status:"running",createdAt:Et.toISOString(),ttlMinutes:j,expiresAt:new Date(Et.getTime()+j*6e4).toISOString(),repo:c,gitIdentity:M};await le(Ln),await Ye(v,Te);let at="ready";if(c||H){let Kn=Date.now(),Tt=!1;for(;Date.now()-Kn<36e4;){try{if((await x({instanceName:v,ip:Te,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){Tt=!0;break}}catch{}await new Promise(ct=>setTimeout(ct,5e3))}if(!Tt){at="timeout";try{at=`timeout \u2014 cloud-init log:
|
|
26
|
+
${(await x({instanceName:v,ip:Te,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:v,ip:Te,ttl_minutes:j,status:"running",provisioning:at,working_directory:c?"/root/project":"/root",hint:c?'Server ready. Run commands with vm_bash, e.g.: vm_bash({ command: "pnpm test" })':"Server ready. Clone a repo or run commands with vm_bash."},null,2)}]}}catch(w){$&&y&&await $.destroyServer(y).catch(()=>{}),$&&g&&await $.deleteSSHKey(g.id).catch(()=>{}),v&&(await X(v).catch(()=>{}),await ue(v).catch(()=>{}));let b=w instanceof Error?w.message:String(w),M=Ke(w);return{content:[{type:"text",text:O(M,`Failed to create server: ${b}`,M==="provider_error"?"Check your HETZNER_API_TOKEN and plan limits":"Verify parameters and try again")}],isError:!0}}}),o.tool("destroy_server","Burn a server. Deletes the Hetzner VM, SSH keys, and local metadata. Always destroy servers when done to avoid costs. Works on expired instances too.",{name:S.string().describe("Name of the server to destroy")},async({name:l})=>{try{let c=await We(l),u=await A.create();await u.destroyServer(c.serverId).catch(()=>{}),await u.deleteSSHKey(c.sshKeyId).catch(()=>{}),await X(l).catch(()=>{});let{deleteJobsByInstance:d}=await Promise.resolve().then(()=>(fe(),nn));return await d(l).catch(()=>{}),await ue(l),{content:[{type:"text",text:`Server "${l}" destroyed.`}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Ke(c);return{content:[{type:"text",text:O(d,`Failed to destroy server "${l}": ${u}`,d==="instance_not_found"?"Check server name with list_servers":"Check your HETZNER_API_TOKEN")}],isError:!0}}}),o.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let l=await V();if(l.length===0)return{content:[{type:"text",text:"No servers running. Use create_server to forge one."}]};let c=l.map(u=>{let d=Math.max(0,Math.floor((new Date(u.expiresAt).getTime()-Date.now())/1e3));return{name:u.name,ip:u.ip,status:u.status,ttl_remaining_seconds:d,ttl_warning:d<300?"Less than 5 minutes left \u2014 extend with extend_server or finish up":void 0,repo:u.repo}});return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),o.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer. Supports long-lived durations up to 1 year.",{name:S.string().describe("Server name"),ttl:S.number().describe("New TTL in minutes from now (max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 129600 (3mo), 259200 (6mo), 525600 (1y)")},async({name:l,ttl:c})=>{try{if(c<1||c>G)return{content:[{type:"text",text:O("validation",`TTL must be between 1 and ${G} minutes (1 year)`,`Pass a ttl value between 1 and ${G}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let u=Math.floor(c),d=await T(l),m=await Q(d,["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${u} minutes 2>/dev/null || true`,`(sleep ${u*60} && shutdown -h now) &`].join(" && "));return m.exitCode!==0?{content:[{type:"text",text:O("command_failed",`Failed to extend TTL: ${m.stderr}`,"The remote command failed \u2014 check instance status with list_servers")}],isError:!0}:(d.ttlMinutes=c,d.expiresAt=new Date(Date.now()+c*6e4).toISOString(),await le(d),{content:[{type:"text",text:`Server "${l}" TTL extended to ${c} minutes.`}]})}catch(u){let d=u instanceof Error?u.message:String(u),m=Ke(u);return{content:[{type:"text",text:O(m,`Failed to extend server "${l}": ${d}`,m==="instance_not_found"?"Check server name with list_servers":"Instance may be unreachable \u2014 wait and retry")}],isError:!0}}}));let a=S.string().optional().describe("Server name (auto-selects if only one is running)");o.tool("vm_bash","Run a shell command on a remote server. Default working directory is /root/project. Use for: installing deps, running tests, git operations, builds. For commands over 2 minutes, set background=true to get a job_id you can poll with vm_job_status.",{command:S.string().describe("Shell command to execute"),working_dir:S.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:S.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:S.boolean().optional().describe("Run in background, return job ID for polling"),server:a},async l=>{let c=await ke(e,l.server),u=l.working_dir??"/root/project",d=`cd ${K(u)} 2>/dev/null || cd /root && ${l.command}`;if(l.background){let g=Ve(),y="/root/.gibil-jobs",v=`${y}/${g}.log`,$=`${y}/${g}.exit`,w=`${y}/${g}.pid`,b=`${y}/${g}.sh`,M=["#!/bin/bash",`nohup bash -c '${d.replace(/'/g,"'\\''")}' > ${v} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${w}`,`(wait $BGPID 2>/dev/null; echo $? > ${$}) &`,"echo $BGPID"].join(`
|
|
27
|
+
`),H=Buffer.from(M).toString("base64"),ge=`mkdir -p ${y} && echo '${H}' | base64 -d > ${b} && chmod +x ${b} && bash ${b}`,j=await Q(c,ge,1e4),Ee=parseInt(j.stdout.trim(),10);return isNaN(Ee)?{content:[{type:"text",text:O("command_failed","Failed to start background job \u2014 could not capture PID","Check that the server is accessible and the command is valid")}],isError:!0}:(await U({id:g,instance:c.name,command:l.command,pid:Ee,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:g,instance:c.name,status:"running",pid:Ee,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let m=await Q(c,d,l.timeout_ms??12e4);return{content:[{type:"text",text:[m.stdout,m.stderr].filter(Boolean).join(`
|
|
28
|
+
`)||"(no output)"}],isError:m.exitCode!==0}}),o.tool("vm_job_status","Check the status of a background job started with vm_bash(background=true). Returns status, exit code, and output when done.",{job_id:S.string().describe("Job ID returned by vm_bash with background=true")},async l=>{try{let c=await se(l.job_id),u=await It(l.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:l.job_id,instance:c.instance,command:c.command,status:u.status,exit_code:u.exitCode,started_at:c.startedAt,duration_s:u.durationS,...u.stdout!==void 0?{stdout:u.stdout}:{}},null,2)}],isError:u.status==="failed"||u.status==="orphaned"}}catch(c){let u=c instanceof Error?c.message:String(c),d=Ke(c);return{content:[{type:"text",text:O(d,u,"Check job_id is correct \u2014 use vm_job_list to see all jobs")}],isError:!0}}}),o.tool("vm_job_list","List all background jobs across all servers. Read-only \u2014 does not modify job state. Use vm_sweep_orphans to mark dead jobs.",{},async()=>{let c=(await xe()).map(u=>({job_id:u.id,instance:u.instance,command:u.command,status:u.status,started_at:u.startedAt,exit_code:u.exitCode}));return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),o.tool("vm_sweep_orphans","Mark running jobs as orphaned if their server no longer exists. Use after destroy_server to clean up lingering job records.",{},async()=>{let l=await xe(),c=await V(),u=new Set(c.map(m=>m.name)),d=[];for(let m of l)m.status==="running"&&!u.has(m.instance)&&(m.status="orphaned",m.completedAt=new Date().toISOString(),await U(m),d.push(m.id));return{content:[{type:"text",text:JSON.stringify({swept_count:d.length,swept_job_ids:d},null,2)}]}}),o.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:S.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:S.number().int().min(1).max(999999).optional().describe("Start at line N (1-based)"),limit:S.number().int().min(1).max(999999).optional().describe("Max lines to return"),server:a},async l=>{let c=await ke(e,l.server),u=K(l.path),d=`cat -n ${u}`;l.offset&&l.limit?d=`awk 'NR>=${l.offset} && NR<=${l.offset+l.limit-1} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.offset?d=`awk 'NR>=${l.offset} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.limit&&(d=`head -n ${l.limit} ${u} | cat -n`);let m=await Q(c,d);return m.exitCode!==0?{content:[{type:"text",text:O("command_failed",`Failed to read ${l.path}: ${m.stderr}`,"Check the file path exists on the server \u2014 use vm_ls to browse")}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),o.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:S.string().describe("Absolute path on the server"),content:S.string().describe("File content to write"),server:a},async l=>{let c=await ke(e,l.server),u=Buffer.from(l.content).toString("base64"),d=K(l.path),m=`mkdir -p "$(dirname ${d})" && echo '${u}' | base64 -d > ${d}`,p=await Q(c,m);return p.exitCode!==0?{content:[{type:"text",text:O("command_failed",`Failed to write ${l.path}: ${p.stderr}`,"Check the path is valid and the disk is not full")}],isError:!0}:{content:[{type:"text",text:`Wrote ${l.path}`}]}}),o.tool("vm_ls","List files and directories on a remote server.",{path:S.string().optional().describe("Directory path (default: /root/project)"),glob:S.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:a},async l=>{let c=await ke(e,l.server),u=l.path??"/root/project",d;l.glob?d=`cd ${K(u)} && find . -path ${K("./"+l.glob)} -type f 2>/dev/null | sort | head -200`:d=`ls -la ${K(u)}`;let m=await Q(c,d);return m.exitCode!==0?{content:[{type:"text",text:O("command_failed",`Failed to list ${u}: ${m.stderr}`,"Check the directory path exists on the server")}],isError:!0}:{content:[{type:"text",text:m.stdout}]}}),o.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:S.string().describe("Regex pattern to search for"),path:S.string().optional().describe("Directory or file to search (default: /root/project)"),include:S.string().optional().describe("File glob to include (e.g. '*.ts')"),server:a},async l=>{let c=await ke(e,l.server),u=l.path??"/root/project",d=K(l.pattern),m=K(u),p;if(l.include){let v=K(l.include);p=`cd ${m} && (rg -n --glob ${v} ${d} 2>/dev/null || grep -rn --include=${v} ${d} .) | head -100`}else p=`cd ${m} && (rg -n ${d} 2>/dev/null || grep -rn ${d} .) | head -100`;return{content:[{type:"text",text:(await Q(c,p)).stdout||"(no matches)"}]}}),o.tool("vm_stats","Get server resource usage \u2014 CPU cores, load average, memory, disk, and uptime. Returns structured data for monitoring.",{server:a},async l=>{try{let c=await ke(e,l.server),d=await Q(c,`echo "$(nproc),$(cat /proc/loadavg),$(free -m | awk '/Mem:/{print $2,$3,$7}'),$(df -BG / | awk 'NR==2{print $2,$3,$4}'),$(awk '{print int($1)}' /proc/uptime)"`);if(d.exitCode!==0)return{content:[{type:"text",text:O("command_failed",`Failed to collect stats: ${d.stderr}`,"Check the server is accessible with vm_bash")}],isError:!0};let m=sr(d.stdout);return{content:[{type:"text",text:JSON.stringify(m,null,2)}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Ke(c);return{content:[{type:"text",text:O(d,`Failed to get stats: ${u}`,"Check the server is accessible with vm_bash")}],isError:!0}}});let s=new or;await o.connect(s)}k();F();function Tn(t){t.command("mcp [name]").description("Start an MCP server (used by Claude Code, Cursor, and other agents)").option("--print-config","Print the MCP JSON config (with resolved binary path) and exit").action(async(e,n)=>{if(n.printConfig){let o={mcpServers:{gibil:Ue()}};console.log(JSON.stringify(o,null,2)),console.error(""),console.error("Add or merge the mcpServers entry into one of:"),console.error(" ~/.claude.json (Claude Code, user-level \u2014 usually already has other mcpServers; merge in)"),console.error(" .mcp.json (repo root, shared with your team \u2014 create if it doesn't exist)");return}try{await En(e)}catch(o){r.error(o instanceof Error?o.message:String(o)),process.exit(1)}})}Y();k();F();D();import{createInterface as ar}from"readline";import{existsSync as cr,readFileSync as lr,writeFileSync as ur}from"fs";import{join as jn}from"path";import{homedir as dr}from"os";function Cn(t){let e=ar({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,o=>{e.close(),n(o.trim())})})}async function mr(){let t=!!await gt(),e=!!await N();return{hetzner:t,apiKey:e}}function Pn(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(Pt);let n=await mr();if(n.hetzner&&!e.force){r.info(`${B} Already configured.`),n.apiKey?(r.detail("Hetzner",te("connected")),r.detail("Gibil API",te("connected"))):(r.detail("Hetzner",te("connected")),r.detail("Gibil API",f("not configured (optional)"))),r.info(""),r.info(` Run ${h("gibil init --force")} to reconfigure.`),r.info(` Run ${h("gibil create")} to forge a server.`);return}r.info(""),r.info(h("Step 1: Hetzner API Token")),r.info(f(" Your servers run on Hetzner Cloud. You need an API token.")),r.info(f(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),r.info("");let o=await Cn(" Hetzner API token: ");o||(r.error("No token provided. Run gibil init again when ready."),process.exit(1));let i=r.spin("Verifying Hetzner token...");try{let g=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${o}`}})).json();g.error&&(i.fail(`Invalid token: ${g.error.message}`),process.exit(1)),i.succeed("Hetzner token verified")}catch{i.fail("Could not reach Hetzner API. Check your network."),process.exit(1)}await Ne(o);let a=r.spin("Detecting available server types..."),s="cax11",l="fsn1",c=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let p of c)try{let y=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:p.type,image:"ubuntu-24.04",location:p.location,start_after_create:!1})})).json();if(y.server){await fetch(`https://api.hetzner.cloud/v1/servers/${y.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${o}`}}),s=p.type,l=p.location;break}}catch{}await ht(s,l),a.succeed(`Default server type: ${s} (${l})`);let u=r.spin("Configuring MCP for Claude Code...");try{let p=jn(dr(),".claude.json"),g={};try{g=JSON.parse(lr(p,"utf-8"))}catch{}g.mcpServers||(g.mcpServers={}),g.mcpServers.gibil=Ue(),ur(p,JSON.stringify(g,null,2)+`
|
|
29
|
+
`),u.succeed("MCP configured for Claude Code (~/.claude.json)")}catch{u.fail("Could not auto-configure MCP"),r.info(f(" Run gibil mcp --print-config for manual setup"))}r.info(""),r.info(h("Default coding agent (optional)")),r.info(f(` Install a coding agent on every server. Options: ${W.join(", ")}`)),r.info(f(" Press Enter to skip \u2014 you can always use --agent later.")),r.info("");let m=(await Cn(" Default agent [none]: ")).toLowerCase().trim();m&&W.includes(m)?(await ze(m),r.info(` ${B} Default agent: ${te(m)}`)):m?r.info(f(` Unknown agent "${m}", skipping. Use --agent with: ${W.join(", ")}`)):(await ze(null),r.info(f(" No default agent. Use --agent claude (or aider, codex) when creating servers."))),r.info(""),r.info(E.initComplete),r.info(""),r.info(f(" Try it now:")),r.info(` ${h('gibil branch feat/my-feature --run "pnpm test"')}`),r.info(` ${h("gibil ssh feat-my-feature")}`),r.info(` ${h("gibil destroy feat-my-feature")}`),r.info(""),r.info(f(" Or with full control:")),r.info(` ${h("gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10")}`),r.info(` ${h('gibil run demo "npm test"')}`),r.info(` ${h("gibil destroy demo")}`),r.info(""),r.info(f(" Later:")),r.info(` ${h("gibil auth login")} ${f("Add a Gibil API key (optional)")}`),r.info(` ${h("gibil mcp --print-config")} ${f("MCP setup for other editors")}`),r.info("")})}async function Nn(){if(process.env.HETZNER_API_TOKEN)return!1;let t=jn(_.root,"config.json");return!cr(t)}ye();import{execSync as fr}from"child_process";import{existsSync as z}from"fs";k();D();Y();function pr(){try{let t=fr("git remote get-url origin",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();if(!t)throw new Error("empty");return t}catch{throw new Error("Not in a git repo, or no remote configured. Use --repo to specify.")}}function gr(t){if(!t||!t.trim())throw new Error("Branch name cannot be empty.");if(/[;&|$`(){}<>!;"'\s\\]/.test(t))throw new Error(`Invalid branch name "${t}". Contains shell-unsafe characters.`)}function kt(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function hr(){return z("pnpm-lock.yaml")?"pnpm install":z("bun.lockb")||z("bun.lock")?"bun install":z("yarn.lock")?"yarn install":z("package-lock.json")?"npm install":z("Cargo.lock")?"cargo build":z("go.sum")?"go mod download":z("uv.lock")?"uv sync":z("poetry.lock")?"poetry install":z("requirements.txt")?"pip install -r requirements.txt":z("Gemfile.lock")?"bundle install":null}async function An(t,e,n){let o=kt(e),i=Date.now(),a=r.spin(`Forging "${o}" for branch ${h(e)}...`),s=await et(t,o,{repo:n.repo,ttlMinutes:n.ttlMinutes,config:n.config,serverType:n.serverType,location:n.location,agent:n.agent,verbose:n.verbose}),l=r.spin(`Checking out ${h(e)}...`);if((await x({instanceName:o,ip:s.ip,command:"cd /root/project && git rev-parse --abbrev-ref HEAD",timeoutMs:1e4})).stdout.trim()===e)l.succeed(`Already on ${e}`);else{let d=await x({instanceName:o,ip:s.ip,command:`cd /root/project && git fetch origin '${e.replace(/'/g,"'\\''")}' && git checkout '${e.replace(/'/g,"'\\''")}'`,timeoutMs:6e4});d.exitCode!==0?(l.fail(`Failed to checkout ${e}`),d.stderr&&r.info(f(d.stderr.trim()))):l.succeed(`Checked out ${e}`)}if(!(!n.noTasks&&n.config?.tasks&&n.config.tasks.length>0)){let d=hr();if(d){let m=r.spin(`Installing deps (${d})...`),p=await x({instanceName:o,ip:s.ip,command:`cd /root/project && ${d}`,timeoutMs:3e5});p.exitCode!==0?(m.fail("Dep install failed"),p.stderr&&r.info(f(p.stderr.trim().slice(-500)))):m.succeed("Deps installed")}}if(n.run)if(n.port&&n.port.length>0)r.info(`Starting: ${h(n.run)} (background)`),await x({instanceName:o,ip:s.ip,command:`cd /root/project && nohup ${n.run} > /tmp/gibil-run.log 2>&1 &`,timeoutMs:3e4}),await new Promise(d=>setTimeout(d,3e3));else{r.info(""),r.info(`Running: ${h(n.run)}`);let d=await x({instanceName:o,ip:s.ip,command:`cd /root/project && ${n.run}`,stream:!n.json,timeoutMs:3e5});n.json&&r.info(d.stdout),d.exitCode!==0&&r.info(f(`Exit code: ${d.exitCode}`))}if(n.port&&n.port.length>0){let d=Xt(s,n.port);r.info("");for(let m of d)r.info(` ${h(`http://localhost:${m}`)} \u2192 ${o}:${m}`);r.info(""),r.info(f(" Tunnel running in background. Kill with: lsof -ti :PORT | xargs kill"))}let u=((Date.now()-i)/1e3).toFixed(1);return a.succeed(E.createReady(o,u)),n.json?console.log(JSON.stringify({name:o,branch:e,ip:s.ip,ttl_minutes:n.ttlMinutes,ssh:`gibil ssh ${o}`})):(r.info(""),r.info(Ge(`${e}`,[`Server: ${o}`,`Branch: ${e}`,`IP: ${s.ip}`,`TTL: ${n.ttlMinutes} minutes`,"",`SSH: gibil ssh ${o}`,`Test: gibil run ${o} "pnpm test"`,`Done: gibil destroy ${o}`]))),s}function Rn(t){t.command("branch <branches...>").description("Spin up a branch on a clean Linux server").option("-r, --repo <git-url>","Git repo URL (auto-detected from cwd)").option("--run <command>","Run a command after checkout").option("--ttl <duration>","Auto-destroy timer (e.g. 30, 2h, 7d, 1mo, 3mo, 1y)","30").option("--json","Output as JSON").option("--no-tasks","Skip .gibil.yml tasks").option("--server-type <type>","Hetzner server type").option("--location <loc>","Hetzner location").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async(e,n)=>{n.json&&I(!0);for(let c of e)gr(c);let o=$e(n.ttl),i=n.repo??pr(),a=null;if(a=await we(i)??await be(process.cwd()),!n.agent){let c=await Ae();c&&(n.agent=c)}if(n.agent){if(!W.includes(n.agent))throw new Error(`Unknown agent "${n.agent}". Supported: ${W.join(", ")}`);if(!ve[n.agent]?.some(u=>a?.env?.[u])){let u=ve[n.agent]?.join(" or ")??"";r.warn(`${n.agent} needs ${u}. SSH in and export it (recommended) or pass with --env.`)}}if(n.dryRun){for(let c of e){let u=kt(c),d=n.serverType??a?.server_type??"cx22",m=n.location??a?.location??"nbg1",p=a?.image??"node:20",g=ce({repo:i,config:a??void 0,ttlMinutes:o,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:n.agent}),y={name:u,serverType:d,location:m,image:p,ttlMinutes:o,repo:i,agent:n.agent,cloudInitScript:g};n.json?r.json(y):(r.info(""),r.info(h("Dry run \u2014 no server will be created")),r.info(""),r.info(` ${f("Name:")} ${u}`),r.info(` ${f("Branch:")} ${c}`),r.info(` ${f("Server type:")} ${d}`),r.info(` ${f("Location:")} ${m}`),r.info(` ${f("Image:")} ${p}`),r.info(` ${f("TTL:")} ${o} minutes`),r.info(` ${f("Repo:")} ${i}`),n.agent&&r.info(` ${f("Agent:")} ${n.agent}`),r.info(""),r.info("Cloud-init script:"),r.info("\u2500".repeat(17)),r.info(g))}return}let s=await N();if(s){let c=await ie(s);r.info(`Authenticated as ${c.user.email} (${c.user.plan})`)}let l=await A.create();if(e.length===1){let c=await An(l,e[0],{repo:i,ttlMinutes:o,config:a,run:n.run,json:n.json,noTasks:n.noTasks,serverType:n.serverType,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose});s&&await Z(s,"create",c.name).catch(()=>{})}else{r.info(`Forging ${h(String(e.length))} branches in parallel...`),r.info("");let c=await Promise.allSettled(e.map(m=>An(l,m,{repo:i,ttlMinutes:o,config:a,run:n.run,json:n.json,noTasks:n.noTasks,serverType:n.serverType,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose}))),u=c.filter(m=>m.status==="fulfilled"),d=c.filter(m=>m.status==="rejected");if(!n.json){if(r.info(""),r.info(`${u.length}/${e.length} branches ready.`),d.length>0)for(let m=0;m<c.length;m++){let p=c[m];p.status==="rejected"&&r.error(` ${e[m]}: ${p.reason instanceof Error?p.reason.message:String(p.reason)}`)}r.info(""),r.info(f(`Destroy all: gibil destroy ${e.map(kt).join(" ")}`))}if(s)for(let m of c)m.status==="fulfilled"&&await Z(s,"create",m.value.name).catch(()=>{});d.length>0&&process.exit(1)}})}function On(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}R();import{spawn as yr}from"child_process";k();D();function Mn(t){if(t.includes(":")){let e=t.split(":");if(e.length===2)return{local:e[0],host:"localhost",remote:e[1]};if(e.length===3)return{local:e[0],host:e[1],remote:e[2]};throw new Error(`Invalid port spec "${t}". Use PORT, LOCAL:REMOTE, or LOCAL:HOST:REMOTE.`)}return{local:t,host:"localhost",remote:t}}function vr(t){let{local:e,host:n,remote:o}=Mn(t);return Me(`${e}:${n}:${o}`),`${e}:${n}:${o}`}function Hn(t){t.command("forward <name> <ports...>").description("Forward local ports to a running ephemeral machine via SSH").action(async(e,n)=>{let o=await T(e),i=n.map(l=>({spec:l,mapping:vr(l),...Mn(l)})),a=["-N","-i",o.keyPath,"-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];for(let{mapping:l}of i)a.push("-L",l);a.push(`root@${o.ip}`),r.info("");for(let{local:l,host:c,remote:u}of i)r.info(` Forwarding ${h(`localhost:${l}`)} \u2192 ${e}:${c}:${u}`);r.info(""),r.info(f(" Tunnel active. Press Ctrl+C to stop.")),r.info(""),yr("ssh",a,{stdio:"inherit"}).on("exit",l=>{let c=()=>process.exit(l??0);l===255?(r.warn(" SSH connection failed \u2014 checking if server still exists..."),J(o).then(c,c)):(r.info(" Tunnel closed."),c())})})}k();D();try{await import("dotenv/config")}catch{}var _r=xr($r(import.meta.url)),Dn={version:"0.0.0"};for(let t of["../package.json","../../package.json"])try{Dn=JSON.parse(br(Sr(_r,t),"utf-8"));break}catch{}var C=new wr;C.name("gibil").description("Your own machine, on demand. Forge, use, burn.").version(`${Dn.version} ${Fe}`,"-v, --version").addHelpText("before",`
|
|
30
|
+
${Nt}
|
|
31
31
|
`).addHelpText("after",`
|
|
32
32
|
${f("Docs:")} https://gibil.dev/docs
|
|
33
|
-
`);
|
|
33
|
+
`);Pn(C);Zt(C);on(C);rn(C);vn(C);wn(C);$n(C);xn(C);_n(C);In(C);kn(C);Tn(C);Rn(C);Hn(C);async function Ir(){On();let t=process.argv.slice(2);!(t.length===0||t.includes("init")||t.includes("auth")||t.includes("--help")||t.includes("-h")||t.includes("--version")||t.includes("-v")||t.includes("-V")||t.includes("mcp")||t.includes("ssh")||t.includes("run")||t.includes("exec")||t.includes("list")||t.includes("ls")||t.includes("forward"))&&await Nn()&&(r.info(""),r.info(E.setupNeeded),r.info(""),process.exit(1)),await pn()&&await _t()&&r.info(f("gibil collects anonymous usage stats to improve the CLI. To disable: GIBIL_TELEMETRY=0"));let o=t[0]??"help",i=hn(process.argv),a=Date.now(),s=0;try{await C.parseAsync(process.argv)}catch(c){s=1,c instanceof Error&&r.error(c.message)}["auth","config","--help","-h","--version","-v","help"].includes(o)||await Le({event:"command",command:o,flags:i,exit_code:s,duration_ms:Date.now()-a}),s!==0&&process.exit(1)}Ir();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gibil",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Your own machine, on demand. Forge, use, burn.",
|
|
5
5
|
"homepage": "https://gibil.dev",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|