gibil 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +88 -126
  2. package/dist/cli/index.js +42 -29
  3. package/package.json +7 -1
package/README.md CHANGED
@@ -4,186 +4,148 @@
4
4
 
5
5
  <h1 align="center">Gibil</h1>
6
6
 
7
- <p align="center"><strong>The box. Forged in seconds. Gone when done.</strong></p>
7
+ <p align="center"><strong>Your own machine, on demand.</strong></p>
8
8
 
9
9
  <p align="center">
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" />
10
+ <img src="https://img.shields.io/badge/version-0.4.0-blue" alt="Version 0.4.0" />
11
+ <img src="https://img.shields.io/badge/tests-364%20passing-brightgreen" alt="Tests: 364 passing" />
12
12
  <img src="https://img.shields.io/badge/typescript-%3E5.0-3178C6" alt="TypeScript" />
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-339933" alt="Node >= 20" />
14
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
- 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.
18
+ A fresh Linux box with root, Docker, SSH, and your repo cloned —<br/>
19
+ forged in one command, gone when you're done.
20
20
  </p>
21
21
 
22
22
  ---
23
23
 
24
- ## The Problem
24
+ ## What it is
25
25
 
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.
26
+ You need a server. Maybe to try a branch without messing up your local. Maybe to let an AI agent run a test suite somewhere safe. Maybe just a clean Linux machine for an afternoon.
27
27
 
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.
28
+ `gibil create` gives you one in under 90 seconds. SSH in, `docker run` whatever, `apt install` whatever it's a real Ubuntu VM, not a sandbox. You set how long it lives, and when the timer ends it deletes itself. No forgotten instances, no surprise bills, no manual cleanup.
29
29
 
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.
30
+ That's the whole pitch: **a fresh server, in one command, that disappears when you're done.**
31
31
 
32
- ## 30-Second Demo
33
-
34
- ```bash
35
- npm install -g gibil
36
- gibil init # enter your Hetzner API token
37
- gibil branch feat/payments --run "pnpm test" # spins up a server, checks out the branch, runs tests
38
- gibil ssh feat-payments # SSH in to debug
39
- gibil destroy feat-payments # gone, no trace
40
- ```
32
+ > Pick your cloud. **[Hetzner](https://www.hetzner.com/cloud/)** for cheap EU/US baseline (~$0.008/hr) or **<a href="https://www.vultr.com/?ref=9900033-9J" rel="sponsored nofollow">Vultr</a>** for APAC density (Tokyo, Seoul, Singapore, Sydney, Mumbai) — new Vultr accounts get **$300 in free credits** through this link. Same commands, same flow.
33
+ >
34
+ > <sub>The Vultr link is a referral — Gibil gets a kickback that helps fund development.</sub>
41
35
 
42
36
  ## Install
43
37
 
44
38
  ```bash
45
39
  npm install -g gibil
40
+ gibil init # picks your provider, paste your token, you're done
46
41
  ```
47
42
 
48
- ## Forge. Run. Burn.
43
+ `gibil init` walks you through choosing a provider on first run. You only need a token from the cloud you pick — Hetzner if you want the cheapest EU/US machine, Vultr if you need an APAC region. Run `gibil providers` any time to see what's available with concrete specs.
49
44
 
50
45
  ```bash
51
- # Forge a server with your repo cloned and ready
52
- gibil create --name my-app --repo github.com/you/project --ttl 30m
46
+ gibil providers # list providers, regions, sizes (small/medium/large)
47
+ gibil init --add vultr # add a second provider later
48
+ ```
53
49
 
54
- # Let your agent work on it
55
- gibil run my-app "pnpm install && pnpm test"
50
+ ## Use it with your AI agent
56
51
 
57
- # Or give any AI agent direct access via MCP
58
- gibil mcp my-app
52
+ **The lazy way** — paste this prompt into Claude Code, Cursor, Copilot, Gemini, or any coding agent. It will install gibil, configure it, and start using it for you:
59
53
 
60
- # Burn it when done
61
- gibil destroy my-app
62
54
  ```
55
+ Use Gibil whenever a task needs a real Linux machine — heavy tests, builds, sandboxed
56
+ agent work, branch previews, anything risky or noisy for my laptop.
57
+
58
+ If gibil is not installed yet:
59
+ 1. npm install -g gibil
60
+ 2. Ask me which provider to use (hetzner for EU/US, vultr for APAC)
61
+ 3. Ask for the matching token: HETZNER_API_TOKEN or VULTR_API_KEY
62
+ 4. export <THAT_TOKEN>=<value>
63
+
64
+ To use it:
65
+ gibil create --name <task> --repo <url> --ttl 30m --json
66
+ gibil create --name <task> --provider vultr --location nrt --json # Tokyo
67
+ gibil run <task> "<command>" --json
68
+ gibil ssh <task> # interactive shell when needed
69
+ gibil destroy <task> --json # or just let the TTL auto-delete it
70
+
71
+ Always pass --json. Keep TTLs short (15m–1h). VMs auto-delete on TTL expiry,
72
+ so there's no cleanup risk if you forget.
73
+ ```
74
+
75
+ **For Claude Code users** — `gibil init` also wires up the MCP server automatically (`vm_bash`, `vm_read`, `vm_write`, `vm_grep`). Your agent gets first-class VM tools, not just a CLI to call.
63
76
 
64
- ## Why Gibil?
65
-
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`.
68
- - **Run your agent remotely** — `--agent claude` installs Claude Code on the server. Also supports `aider` and `codex`. Direct filesystem access, no MCP latency.
69
- - **SSH when it breaks** — `gibil ssh <name>` drops you into a real terminal. Debug live, not from CI logs.
70
- - **Preview a branch** — `--port 3000` tunnels the app to localhost. Open your browser, see the branch running live.
71
- - **Parallel branches** — `gibil branch feat/A feat/B feat/C` boots three servers in parallel. Zero interference.
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.
75
-
76
- ## Commands
77
-
78
- | Command | Description |
79
- | ------------------------- | ----------------------------------------------------------- |
80
- | `gibil init` | Set up gibil — Hetzner token, MCP config, agent skill |
81
- | `gibil create` | Forge an ephemeral server |
82
- | `gibil branch <branch>` | Spin up a branch on a clean server (auto-detects repo) |
83
- | `gibil checkout <branch>` | Alias for `gibil branch` |
84
- | `gibil ssh <name>` | SSH into a running server |
85
- | `gibil run <name> <cmd>` | Execute a command remotely (`--background` for async) |
86
- | `gibil job <cmd>` | Manage background jobs (status, list, cancel, logs) |
87
- | `gibil exec <name>` | Upload and run a local script |
88
- | `gibil mcp [name]` | Start MCP server for AI agents (`--print-config` for setup) |
89
- | `gibil list` | List all active servers |
90
- | `gibil extend <name>` | Extend a server's TTL |
91
- | `gibil destroy [name]` | Burn down a server |
92
- | `gibil auth` | Manage authentication |
93
- | `gibil usage` | View usage and plan limits |
94
-
95
- ## Two Ways to Use Agents
96
-
97
- ### Option A: Agent on your laptop (MCP)
98
-
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.
77
+ **For long-term use across sessions** — install the agent skill once and it's permanent:
100
78
 
101
79
  ```bash
102
- gibil create --name my-app --repo github.com/you/project
103
- gibil mcp # Your agent gets vm_bash, vm_read, vm_write tools
80
+ npx skills add https://github.com/AlexikM/gibil-skills --skill gibil
104
81
  ```
105
82
 
106
- | Pros | Cons |
107
- | ----------------------------------- | ------------------------------------------ |
108
- | See everything the agent does | Every file/command is a network round-trip |
109
- | Agent uses your local config | Your laptop stays busy (fans, battery) |
110
- | Works with any MCP-compatible agent | Long commands block with no streaming |
83
+ Works with [40+ agents](https://agentskills.io). Now they all know about gibil natively.
111
84
 
112
- ### Option B: Agent on the server (--agent)
113
-
114
- The agent runs directly on the server. Your laptop is just a terminal. Good for heavy work, long sessions, or when you want your laptop free.
85
+ ## The 30-second tour
115
86
 
116
87
  ```bash
117
- gibil branch feat/payments --agent claude
118
- gibil ssh feat-payments
88
+ # Forge a server with your repo cloned and ready
89
+ gibil create --name my-app --repo github.com/you/project --size medium --ttl 1h
119
90
 
120
- # Set your API key in the session (stays in memory, never written to disk)
121
- export ANTHROPIC_API_KEY=sk-ant-...
122
- claude # direct filesystem access, full Docker, zero latency
91
+ # Use SSH in, run a command, or both
92
+ gibil ssh my-app
93
+ gibil run my-app "pnpm install && pnpm test"
123
94
 
124
- # Also supports aider and codex:
125
- gibil branch feat/payments --agent aider # then: export ANTHROPIC_API_KEY=...
126
- gibil branch feat/payments --agent codex # then: export OPENAI_API_KEY=...
95
+ # Burn when you're done
96
+ gibil destroy my-app
127
97
  ```
128
98
 
129
- | Pros | Cons |
130
- | ---------------------------------- | ------------------------------ |
131
- | Direct filesystem — no MCP latency | Need to SSH in to interact |
132
- | Full Docker, real Linux tools | Set API key manually after SSH |
133
- | Your laptop is free | Need tmux to persist sessions |
134
- | Works with claude, aider, codex | |
135
-
136
- > **Security note:** Don't pass API keys via `--env` — they end up on disk in `/etc/environment`. Instead, SSH in and `export` the key. It stays in memory and vanishes when the server is destroyed.
137
-
138
- **Rule of thumb:** Quick checks → MCP (Option A). Heavy work or "let it run" → remote agent (Option B).
139
-
140
- ## Preview a Branch
141
-
142
- Run a dev server on a gibil server and open it in your browser:
99
+ That's the whole thing.
143
100
 
144
101
  ```bash
145
- gibil branch feat/payments --run "pnpm dev" --port 3000
146
- # Server created, branch checked out, dev server started
147
- # → Local: http://localhost:3000 ← open in your browser
148
- # Tunnel running in background
102
+ # Need APAC? Pick the right provider and region
103
+ gibil create --name tokyo-test --provider vultr --location nrt --size small --ttl 30m
104
+
105
+ # See everything available providers, regions, sizes with real specs
106
+ gibil providers
149
107
  ```
150
108
 
151
- Or with an interactive SSH session and port forwarding:
109
+ ## Why you'd want one
152
110
 
153
- ```bash
154
- gibil ssh feat-payments --port 3000 --port 8080
155
- # Forwarding localhost:3000 feat-payments:3000
156
- # Forwarding localhost:8080 feat-payments:8080
157
- # Tunnel active while SSH session is open
158
- ```
111
+ - **Try a branch without leaving yours.** `gibil branch feat/payments` checks the branch out on a fresh machine. Your local stays on `main`. No stash, no juggling.
112
+ - **Preview it in your browser.** Add `--port 3000` and the remote dev server tunnels to `localhost:3000` over SSH. Encrypted, no public ports exposed.
113
+ - **Keep your laptop quiet.** Heavy tests, big builds, Docker-in-Docker — runs on the server, not your fans.
114
+ - **Let an AI agent work remotely.** Pass `--agent claude` and Claude Code runs on the server itself, with full root and Docker. Your laptop is just a window.
115
+ - **Run several at once.** `gibil branch feat/A feat/B feat/C` boots three machines in parallel. They don't see each other.
116
+ - **Break it freely.** It's disposable. `rm -rf /` if you want — `gibil destroy` and create another in 90s.
159
117
 
160
- Both approaches tunnel traffic through SSH — encrypted, no public ports exposed, works behind any firewall.
118
+ ## Common commands
161
119
 
162
- ## Agent Workflow (JSON)
120
+ | Command | What it does |
121
+ | ---------------------------------- | ------------------------------------------------------------------------- |
122
+ | `gibil create --name X` | Forge a fresh server (`--provider`, `--size`, `--location` to customize) |
123
+ | `gibil branch <branch>` | Same, but checks out a branch from your repo |
124
+ | `gibil ssh <name>` | SSH in (with `--port` to tunnel a dev server) |
125
+ | `gibil run <name> "<cmd>"` | Run a command remotely |
126
+ | `gibil list` | See what's running, with provider per row |
127
+ | `gibil providers` | List providers, regions, and sizes with real specs |
128
+ | `gibil extend <name> 1h` | Give it more time |
129
+ | `gibil destroy <name>` | Burn it down |
163
130
 
164
- Every command supports `--json` for programmatic use:
131
+ Run `gibil --help` for the full list.
165
132
 
166
- ```bash
167
- gibil create --name task --repo https://github.com/user/repo --json --ttl 30m
168
- gibil run task "cd /root/project && pnpm install && pnpm test" --json
169
- gibil destroy task --json
170
- ```
133
+ ## Pricing
134
+
135
+ Free during alpha. Bring your own Hetzner or Vultr token and you pay the cloud directly — typically a fraction of a cent per agent run. Managed plans coming soon.
171
136
 
172
- ## Integrations
137
+ ## Going deeper
173
138
 
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/).
139
+ The README stops here on purpose the docs go all the way.
180
140
 
181
- ## Links
141
+ - **[Quick Start](https://gibil.dev/docs/quickstart)** — first server in 5 minutes
142
+ - **[Recipes](https://gibil.dev/docs/recipes/code-test-loop)** — code-test loops, parallel sharding, PR review, bug repro
143
+ - **[Run an AI agent on the box](https://gibil.dev/docs/guides/claude-code)** — Claude Code, MCP, agent setup
144
+ - **[Reference](https://gibil.dev/docs/reference/cli-flags)** — every flag, JSON schemas, `.gibil.yml`
145
+ - **[How it works](https://gibil.dev/docs/concepts/how-it-works)** — what happens between `create` and SSH-ready
146
+ - **[Blog](https://gibil.dev/blog)** — design notes and updates
182
147
 
183
- - [Documentation](https://gibil.dev/docs)
184
- - [Quick Start](https://gibil.dev/docs/quickstart)
185
- - [Recipes](https://gibil.dev/docs/recipes/code-test-loop)
186
- - [Blog](https://gibil.dev/blog)
148
+ Also: the [VS Code extension](vscode-extension/), the [Sandcastle integration](integrations/sandcastle/), and the [agent skill](https://github.com/AlexikM/gibil-skills) for Claude Code, Cursor, and 40+ others.
187
149
 
188
150
  ## License
189
151
 
package/dist/cli/index.js CHANGED
@@ -1,33 +1,46 @@
1
1
  #!/usr/bin/env node
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
- ${f(" ~~~~~~~~")}
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}
2
+ var Or=Object.defineProperty;var D=(t,e)=>()=>(t&&(e=t(t=0)),e);var ie=(t,e)=>{for(var n in e)Or(t,n,{get:e[n],enumerable:!0})};import Se from"picocolors";function et(t,e){let n=Math.max(t.length+4,...e.map(c=>kt(c).length+4)),r=`${m("\u256D")}${m("\u2500".repeat(n))}${m("\u256E")}`,i=`${m("\u2570")}${m("\u2500".repeat(n))}${m("\u256F")}`,s=`${m("\u2502")} ${H} ${h(t)}${" ".repeat(n-kt(t).length-4)}${m("\u2502")}`,a=`${m("\u251C")}${m("\u2500".repeat(n))}${m("\u2524")}`,l=e.map(c=>{let u=n-kt(c).length-2;return`${m("\u2502")} ${c}${" ".repeat(Math.max(0,u))}${m("\u2502")}`});return[r,s,a,...l,i].join(`
3
+ `)}function kt(t){return t.replace(/\x1b\[[0-9;]*m/g,"")}var se,Hr,ee,Mr,m,h,H,N,Le,rn,Qe,on,sn,nn,De,C,R=D(()=>{"use strict";se=t=>Se.red(t),Hr=t=>Se.yellow(t),ee=t=>Se.green(t),Mr=t=>Se.red(t),m=t=>Se.dim(t),h=t=>Se.bold(t),H="\u{1F98E}",N=ee("\u2713"),Le=Mr("\u2716"),rn=Hr("\u26A0"),Qe="\u{12248}",on=`
4
+ ${se(" /\\")}
5
+ ${se(" / \\")}
6
+ ${se(" / \u{1F525} \\")}
7
+ ${se(" / \\")}
8
+ ${m(" ~~~~~~~~")}
9
+ ${h(" g i b i l")} ${m(Qe)}
10
+ `,sn=`${H} ${h("gibil")} ${m(Qe)}`,nn=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],De=class{timer=null;frame=0;text;constructor(e){this.text=e}start(){return process.stderr.isTTY?(this.timer=setInterval(()=>{let e=se(nn[this.frame%nn.length]);process.stderr.write(`\r ${e} ${this.text}`),this.frame++},80),this):(process.stderr.write(` ${this.text}
11
11
  `),this)}update(e){this.text=e,process.stderr.isTTY||process.stderr.write(` ${e}
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
- ${E.destroyAll}`):r.info(`
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+=`
12
+ `)}succeed(e){this.stop(),process.stderr.write(`\r ${N} ${e??this.text}
13
+ `)}fail(e){this.stop(),process.stderr.write(`\r ${Le} ${e??this.text}
14
+ `)}stop(){this.timer&&(clearInterval(this.timer),this.timer=null,process.stderr.isTTY&&process.stderr.write("\r\x1B[K"))}};C={welcome:`${H} Your first fire. Welcome to Gibil.`,noInstances:`${H} No fires burning. Gibil sleeps.`,destroyAll:`${H} All fires extinguished. Gibil moves on.`,destroySingle:t=>`${H} "${t}" \u2014 fire out.`,authSuccess:`${H} Logged in. The forge is yours.`,authLogout:`${H} Logged out. The forge cools.`,createReady:(t,e)=>`${H} "${t}" forged ${m(`(${e}s)`)}`,fleetReady:(t,e)=>`${H} Fleet forged \u2014 ${t}/${e} fires lit.`,ttlWarning:(t,e)=>`${H} ${t} \u2014 flame is low (${e}m remaining)`,initComplete:`${H} The forge is ready. Run ${h("gibil create")} to light your first fire.`,setupNeeded:`${H} No forge configured. Run ${h("gibil init")} to get started.`}});function I(t){It=t}function ae(t){return It&&t!=="error"?!1:an[t]>=an[Kr]}var Kr,It,an,o,E=D(()=>{"use strict";R();Kr="info",It=!1,an={debug:0,info:1,warn:2,error:3,silent:4};o={debug(t,...e){ae("debug")&&console.debug(`${m("[debug]")} ${t}`,...e)},info(t,...e){ae("info")&&console.log(t,...e)},warn(t,...e){ae("warn")&&console.warn(`${rn} ${t}`,...e)},error(t,...e){ae("error")&&console.error(`${Le} ${t}`,...e)},success(t){ae("info")&&console.log(`${N} ${t}`)},step(t){ae("info")&&console.log(` ${m("\u203A")} ${t}`)},flame(t){ae("info")&&console.log(t)},detail(t,e){ae("info")&&console.log(` ${m(t+":")} ${e}`)},spin(t){return It?new De(t):new De(t).start()},json(t){console.log(JSON.stringify(t,null,2))}}});var cn={};ie(cn,{HETZNER_META:()=>tt,PROVIDER_CATALOG:()=>Et,VULTR_META:()=>nt});var tt,nt,Et,ze=D(()=>{"use strict";tt={name:"hetzner",label:"Hetzner Cloud",defaultRegion:"fsn1",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:40,nativeType:"cax11"},{name:"medium",vcpu:4,ramGb:8,diskGb:80,nativeType:"cax21"},{name:"large",vcpu:8,ramGb:16,diskGb:160,nativeType:"cax31"}]},nt={name:"vultr",label:"Vultr",defaultRegion:"nrt",sizes:[{name:"small",vcpu:2,ramGb:4,diskGb:80,nativeType:"vc2-2c-4gb"},{name:"medium",vcpu:4,ramGb:8,diskGb:160,nativeType:"vc2-4c-8gb"},{name:"large",vcpu:6,ramGb:16,diskGb:320,nativeType:"vc2-6c-16gb"}]},Et={hetzner:tt,vultr:nt}});import{homedir as Dr}from"os";import{join as Z,resolve as Lr}from"path";import{existsSync as zr}from"fs";function W(){return process.env.GIBIL_HOME??Z(Dr(),".gibil")}function rt(){let t=process.argv[1];if(t){let e=Lr(t);if(zr(e))return{command:process.execPath,args:[e,"mcp"]}}return{command:"gibil",args:["mcp"]}}var P,L=D(()=>{"use strict";P={get root(){return W()},get instances(){return Z(W(),"instances")},get keys(){return Z(W(),"keys")},get jobs(){return Z(W(),"jobs")},get knownHostsDir(){return Z(W(),"known_hosts")},instanceFile:t=>Z(W(),"instances",`${t}.json`),keyDir:t=>Z(W(),"keys",t),privateKey:t=>Z(W(),"keys",t,"id_ed25519"),publicKey:t=>Z(W(),"keys",t,"id_ed25519.pub"),knownHostsFile:t=>Z(W(),"known_hosts",t)}});var Ue={};ie(Ue,{clearApiKey:()=>Ct,fetchUsage:()=>Rt,getApiKey:()=>M,getApiUrl:()=>Vr,getApiUrlFromConfig:()=>ot,getDefaultAgent:()=>Fe,getDefaultProvider:()=>At,getHetznerToken:()=>Yr,getProviderToken:()=>_e,getServerDefaults:()=>Wr,saveApiKey:()=>Tt,saveDefaultAgent:()=>it,saveHetznerToken:()=>jt,saveProviderToken:()=>Ge,saveServerDefaults:()=>Nt,setDefaultProvider:()=>Pe,trackUsage:()=>te,verifyApiKey:()=>ce});import{readFile as Gr,writeFile as Fr,mkdir as Ur}from"fs/promises";import{existsSync as Br}from"fs";import{join as Jr}from"path";function ln(){return Jr(P.root,"config.json")}async function z(){let t=ln();if(!Br(t))return{};let e=await Gr(t,"utf-8"),n=JSON.parse(e);return n.hetzner_token&&!n.providers?.hetzner?.token&&(n.providers={...n.providers??{},hetzner:{token:n.hetzner_token,default_server_type:n.default_server_type,default_location:n.default_location,...n.providers?.hetzner??{}}}),n}async function xe(t){await Ur(P.root,{recursive:!0,mode:448});let e={...t};e.providers?.hetzner&&(delete e.hetzner_token,delete e.default_server_type,delete e.default_location),await Fr(ln(),JSON.stringify(e,null,2),{mode:384})}async function Tt(t){let e=await z();e.api_key=t,await xe(e)}async function M(){return process.env.GIBIL_API_KEY?process.env.GIBIL_API_KEY:(await z()).api_key??null}async function Ct(){let t=await z();delete t.api_key,await xe(t)}function Vr(){return process.env.GIBIL_API_URL??un}async function ot(){return process.env.GIBIL_API_URL?process.env.GIBIL_API_URL:(await z()).api_url??un}async function _e(t){let e=process.env[qr[t]];return e||((await z()).providers?.[t]?.token??null)}async function Ge(t,e){let n=await z();n.providers||(n.providers={}),n.providers[t]={...n.providers[t]??{},token:e},await xe(n)}async function At(){return(await z()).default_provider??"hetzner"}async function Pe(t){let e=await z();e.default_provider=t,await xe(e)}async function jt(t){await Ge("hetzner",t)}async function Yr(){return _e("hetzner")}async function Nt(t,e){let n=await z();n.providers||(n.providers={}),n.providers.hetzner={...n.providers.hetzner??{},default_server_type:t,default_location:e},await xe(n)}async function Wr(){let t=await z(),e=t.providers?.hetzner;return{serverType:e?.default_server_type??t.default_server_type??"cax11",location:e?.default_location??t.default_location??"fsn1"}}async function it(t){let e=await z();t?e.default_agent=t:delete e.default_agent,await xe(e)}async function Fe(){return(await z()).default_agent??null}async function ce(t){let e=await ot(),n=await fetch(`${e}/auth-verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t})});if(n.status===401)throw new Error("Invalid API key. Get one at https://gibil.dev");if(!n.ok){let r=await n.text();throw new Error(`API error (${n.status}): ${r}`)}return await n.json()}async function te(t,e,n,r){let i=await ot(),s=await fetch(`${i}/usage-track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,instance_name:n,server_type:r})});if(s.status===429)throw new Error("Usage limit reached. Please try again later or contact support.");if(!s.ok){let a=await s.text();throw new Error(`Usage tracking failed (${s.status}): ${a}`)}}async function Rt(t){let e=await ot(),n=await fetch(`${e}/usage-get`,{headers:{Authorization:`Bearer ${t}`}});if(!n.ok){let r=await n.text();throw new Error(`Failed to fetch usage (${n.status}): ${r}`)}return await n.json()}var un,qr,G=D(()=>{"use strict";L();un="https://zopdxjruwktjyjunitrv.supabase.co/functions/v1";qr={hetzner:"HETZNER_API_TOKEN",vultr:"VULTR_API_KEY"}});var dn={};ie(dn,{HetznerProvider:()=>Ht});function Ot(t){return{id:t.id,name:t.name,status:t.status,ipv4:t.public_net.ipv4.ip,ipv6:t.public_net.ipv6.ip,serverType:t.server_type?.name??"unknown",location:t.datacenter?.location?.name??"unknown",labels:t.labels,created:t.created}}function Xr(t){return{id:t.id,name:t.name,fingerprint:t.fingerprint??""}}var Zr,Ht,fn=D(()=>{"use strict";E();ze();Zr="https://api.hetzner.cloud/v1";Ht=class t{token;constructor(e){this.token=e}static async create(e){let{getHetznerToken:n}=await Promise.resolve().then(()=>(G(),Ue)),r=e??await n();if(!r)throw new Error("HETZNER_API_TOKEN is required. Run 'gibil init' or set it in your environment.");return new t(r)}async request(e,n,r){let i=`${Zr}${n}`;o.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),l;try{l=JSON.parse(a).error?.message??a}catch{l=a}let c="";throw s.status===401||s.status===403?c=`
15
+ Your Hetzner token may be invalid or expired. Run: gibil init --force`:s.status===409&&l.includes("name")?c=`
16
+ A server with this name already exists. Try a different --name or run: gibil destroy <name>`:s.status===422&&(l.includes("location")||l.includes("server_type"))?c=`
17
+ This server type may not be available in your region. Run: gibil init --force`:s.status===429&&(c=`
18
+ Rate limited by Hetzner. Wait a moment and retry your command.`),new Error(`Hetzner API error (${s.status}): ${l}${c}`)}return s.status===204?{}:await s.json()}async createServer(e,n,r,i,s){if(!i||!s){let{getServerDefaults:d}=await Promise.resolve().then(()=>(G(),Ue)),f=await d();i=i??f.serverType,s=s??f.location}if(i.startsWith("cax")&&!["fsn1","nbg1"].includes(s))throw new Error(`ARM server type "${i}" is not available in "${s}". Use --location fsn1 or --location nbg1, or switch to an x86 type (cpx11, cpx21, etc.).`);let c=typeof n=="string"?parseInt(n,10):n,u={name:e,server_type:i,image:"ubuntu-24.04",ssh_keys:[c],labels:{gibil:"true","gibil-name":e},location:s};o.debug(`createServer payload: ${JSON.stringify({name:e,server_type:i,image:"ubuntu-24.04",location:s})}`),r&&(u.user_data=r);try{let d=await this.request("POST","/servers",u);return Ot(d.server)}catch(d){let f=`(server_type=${i}, location=${s}). Try a different --server-type or --location.`;throw d instanceof Error?new Error(`${d.message} ${f}`):d}}async destroyServer(e){await this.request("DELETE",`/servers/${e}`)}async getServer(e){let n=await this.request("GET",`/servers/${e}`);return Ot(n.server)}async listServers(e="gibil=true"){return(await this.request("GET",`/servers?label_selector=${encodeURIComponent(e)}&per_page=50`)).servers.map(Ot)}async waitForReady(e,n=12e4){let r=Date.now(),i=3e3;for(;Date.now()-r<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;o.debug(`Server ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Server ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let r=await this.request("POST","/ssh_keys",{name:e,public_key:n});return Xr(r.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}sizes(){return tt.sizes}}});var pn={};ie(pn,{buildAffiliateNudgeLine:()=>to,buildAffiliateProviderNudge:()=>Mt,getAffiliateProgram:()=>st,getAffiliateProgramNames:()=>eo});function Qr(){let t=process.env.GIBIL_NO_REFERRAL?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}function st(t){return Qr()?null:mn.programs[t]??null}function eo(){return Object.keys(mn.programs)}function to(t){return t?`
24
19
 
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}
20
+ No ${t.label} account? Get ${t.credit} free credits \u2192 ${t.ref_url}
21
+ ${t.disclosure}`:""}function Mt(t,e){if(e)return null;let n=st(t);return n?`New here? ${n.credit} free credits \u2192 ${n.ref_url}`:null}var mn,at=D(()=>{"use strict";mn={version:1,programs:{vultr:{label:"Vultr",ref_url:"https://www.vultr.com/?ref=9900033-9J",credit:"$300",console_url:"https://console.vultr.com/user/apiaccess/",disclosure:"Referral link \u2014 Gibil gets a kickback that helps fund development."}}}});var gn={};ie(gn,{VultrProvider:()=>Dt});function Kt(t){let e=t.status;t.status==="active"&&t.power_status==="running"?e="running":t.status==="pending"&&(e="initializing");let n={};for(let r of t.tags??[])n[r]="true";return{id:t.id,name:t.label,status:e,ipv4:t.main_ip,ipv6:t.v6_main_ip,serverType:t.plan,location:t.region,labels:n,created:t.date_created}}function oo(t){return{id:t.id,name:t.name,fingerprint:""}}var no,ro,Dt,hn=D(()=>{"use strict";E();ze();no="https://api.vultr.com/v2",ro=2284;Dt=class t{apiKey;constructor(e){this.apiKey=e}static async create(e){let n=e;if(!n){let{getProviderToken:r}=await Promise.resolve().then(()=>(G(),Ue));n=await r("vultr")??void 0}if(!n){let{getAffiliateProgram:r,buildAffiliateNudgeLine:i}=await Promise.resolve().then(()=>(at(),pn)),s=i(r("vultr"));throw new Error(`VULTR_API_KEY is required. Run 'gibil init --provider vultr' or set it in your environment.${s}`)}return new t(n)}async request(e,n,r){let i=`${no}${n}`;o.debug(`${e} ${i}`);let s=await fetch(i,{method:e,headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0,signal:AbortSignal.timeout(3e4)});if(!s.ok){let a=await s.text(),l;try{l=JSON.parse(a).error??a}catch{l=a}let c="";throw s.status===401||s.status===403?c=`
22
+ Your Vultr API key may be invalid. Re-run: gibil init --provider vultr --token <new-key>`:s.status===429&&(c=`
23
+ Rate limited by Vultr. Wait a moment and retry.`),new Error(`Vultr API error (${s.status}): ${l}${c}`)}return s.status===204?{}:await s.json()}async createServer(e,n,r,i,s){let a=i??"vc2-2c-4gb",l=s??"nrt",c={region:l,plan:a,os_id:ro,label:e,tags:["gibil",`gibil-${e}`],sshkey_id:[String(n)]};r&&(c.user_data=Buffer.from(r,"utf-8").toString("base64"));try{let u=await this.request("POST","/instances",c);return Kt(u.instance)}catch(u){let d=`(plan=${a}, region=${l}). Try a different --server-type or --location.`;throw u instanceof Error?new Error(`${u.message} ${d}`):u}}async destroyServer(e){await this.request("DELETE",`/instances/${e}`)}async getServer(e){let n=await this.request("GET",`/instances/${e}`);return Kt(n.instance)}async listServers(e="gibil=true"){let n=e.split("=")[0]||"gibil";return((await this.request("GET",`/instances?tag=${encodeURIComponent(n)}&per_page=50`)).instances??[]).map(Kt)}async waitForReady(e,n=12e4){let r=Date.now(),i=3e3;for(;Date.now()-r<n;){let s=await this.getServer(e);if(s.status==="running"&&s.ipv4!=="0.0.0.0")return s;o.debug(`Vultr instance ${e} status: ${s.status}, waiting...`),await new Promise(a=>setTimeout(a,i))}throw new Error(`Vultr instance ${e} did not become ready within ${n/1e3}s`)}async createSSHKey(e,n){let r=await this.request("POST","/ssh-keys",{name:e,ssh_key:n});return oo(r.ssh_key)}async deleteSSHKey(e){await this.request("DELETE",`/ssh-keys/${e}`)}sizes(){return nt.sizes}}});var yn={};ie(yn,{ProviderRegistry:()=>ct,providerRegistry:()=>F});var ct,F,ke=D(()=>{"use strict";ct=class{factories=new Map;register(e,n){this.factories.set(e,n)}async get(e){let n=this.factories.get(e);if(!n)throw new Error(`Provider "${e}" is not registered. Registered: ${this.listRegistered().join(", ")||"(none)"}`);return n()}async forInstance(e){let n=e.provider??"hetzner";return this.get(n)}listRegistered(){return[...this.factories.keys()]}},F=new ct;F.register("hetzner",async()=>{let{HetznerProvider:t}=await Promise.resolve().then(()=>(fn(),dn));return t.create()});F.register("vultr",async()=>{let{VultrProvider:t}=await Promise.resolve().then(()=>(hn(),gn));return t.create()})});import{readFile as fo,writeFile as mo,mkdir as bn,rm as Sn,readdir as po,rename as go}from"fs/promises";import{existsSync as $n}from"fs";import{join as Lt}from"path";async function Gt(t,e,n){let r=`${t}.tmp`;await mo(r,e,n);try{await go(r,t)}catch(i){throw await Sn(r,{force:!0}).catch(s=>{console.warn(`Warning: failed to clean up temp file ${r}: ${s}`)}),i}}var zt,le,pe,Ft,ut,A,ge,X,O=D(()=>{"use strict";L();zt=class{instancesDir;keysDir;constructor(e){let n=e??P.root;this.instancesDir=Lt(n,"instances"),this.keysDir=Lt(n,"keys")}async ensureDirectories(){await bn(this.instancesDir,{recursive:!0,mode:448}),await bn(this.keysDir,{recursive:!0,mode:448})}instanceFile(e){return Lt(this.instancesDir,`${e}.json`)}async save(e){await this.ensureDirectories(),await Gt(this.instanceFile(e.name),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.instanceFile(e);if(!$n(n))return null;let r=await fo(n,"utf-8");return JSON.parse(r)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Instance "${e}" not found. Run "gibil list" to see active instances.`);return n}async loadActiveOrThrow(e){let n=await this.loadOrThrow(e);if(new Date>new Date(n.expiresAt))throw new Error(`Instance "${e}" has expired (TTL was ${n.ttlMinutes}m). Run "gibil destroy ${e}" to clean up.`);return n}async delete(e){let n=this.instanceFile(e);$n(n)&&await Sn(n)}async list(){await this.ensureDirectories();let e=await po(this.instancesDir),n=[];for(let r of e){if(!r.endsWith(".json"))continue;let i=r.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}},le=new zt,pe=t=>le.save(t),Ft=t=>le.load(t),ut=t=>le.loadOrThrow(t),A=t=>le.loadActiveOrThrow(t),ge=t=>le.delete(t),X=()=>le.list()});var Je={};ie(Je,{GIBIL_SIZE_TARGETS:()=>No,SIZE_NAMES:()=>On,isSizeName:()=>Ro,resolveSize:()=>Oo});function Ro(t){return On.includes(t)}function Oo(t,e){let n=t.sizes().find(r=>r.name===e);if(!n){let r=t.sizes().map(i=>i.name).join(", ")||"(none)";throw new Error(`Size "${e}" is not available on this provider. Available: ${r}`)}return n.nativeType}var On,No,Ve=D(()=>{"use strict";On=["small","medium","large"],No={small:{vcpu:2,ramGb:4},medium:{vcpu:4,ramGb:8},large:{vcpu:8,ramGb:16}}});var Un={};ie(Un,{JobStore:()=>vt,deleteJob:()=>Vo,deleteJobsByInstance:()=>Ye,listJobs:()=>je,listJobsByInstance:()=>qo,loadJob:()=>Jo,loadJobOrThrow:()=>ue,saveJob:()=>V});import{readFile as Fo,mkdir as zn,rm as Uo,readdir as Bo}from"fs/promises";import{existsSync as Gn}from"fs";import{join as Fn}from"path";var vt,ve,V,Jo,ue,Vo,je,qo,Ye,we=D(()=>{"use strict";L();O();vt=class{jobsDir;constructor(e){let n=e??P.root;this.jobsDir=Fn(n,"jobs")}jobFile(e){if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error(`Invalid job ID: "${e}"`);return Fn(this.jobsDir,`${e}.json`)}async save(e){await zn(this.jobsDir,{recursive:!0,mode:448}),await Gt(this.jobFile(e.id),JSON.stringify(e,null,2),{mode:384})}async load(e){let n=this.jobFile(e);if(!Gn(n))return null;let r=await Fo(n,"utf-8");return JSON.parse(r)}async loadOrThrow(e){let n=await this.load(e);if(!n)throw new Error(`Job "${e}" not found. Run "gibil job list" to see active jobs.`);return n}async delete(e){let n=this.jobFile(e);Gn(n)&&await Uo(n)}async list(){await zn(this.jobsDir,{recursive:!0,mode:448});let e=await Bo(this.jobsDir),n=[];for(let r of e){if(!r.endsWith(".json"))continue;let i=r.replace(".json",""),s=await this.load(i);s&&n.push(s)}return n}async listByInstance(e){return(await this.list()).filter(r=>r.instance===e)}async deleteByInstance(e){let n=await this.listByInstance(e);for(let r of n)await this.delete(r.id)}},ve=new vt,V=t=>ve.save(t),Jo=t=>ve.load(t),ue=t=>ve.loadOrThrow(t),Vo=t=>ve.delete(t),je=()=>ve.list(),qo=t=>ve.listByInstance(t),Ye=t=>ve.deleteByInstance(t)});import{Command as Ui}from"commander";import{readFileSync as Bi}from"fs";import{fileURLToPath as Ji}from"url";import{dirname as Vi,join as qi}from"path";ke();L();import{mkdir as io,rm as vn,readFile as so,chmod as ao}from"fs/promises";import{existsSync as wn}from"fs";import{execFile as co}from"child_process";import{promisify as lo}from"util";var uo=lo(co);async function lt(t){let e=P.keyDir(t);wn(e)&&await vn(e,{recursive:!0}),await io(e,{recursive:!0});let n=P.privateKey(t),r=P.publicKey(t);await uo("ssh-keygen",["-t","ed25519","-f",n,"-N","","-C",`gibil-${t}`]),await ao(n,384);let i=await so(r,"utf-8");return{privateKeyPath:n,publicKeyPath:r,publicKey:i.trim()}}async function ne(t){let e=P.keyDir(t);wn(e)&&await vn(e,{recursive:!0})}L();E();R();O();import{Client as Pn}from"ssh2";import{readFile as kn}from"fs/promises";import{createHash as ho,timingSafeEqual as yo}from"crypto";function Ut(t){return`SHA256:${ho("sha256").update(t).digest("base64").replace(/=+$/,"")}`}function vo(t){if(t.length<4)throw new Error("Host key buffer too short");let e=t.readUInt32BE(0);if(e===0||4+e>t.length)throw new Error("Host key buffer length prefix is invalid");return t.subarray(4,4+e).toString("ascii")}function xn(t,e,n){let r=vo(n),i=n.toString("base64");return`${e===22?t:`[${t}]:${e}`} ${r} ${i}
24
+ `}function wo(t){return t.toString("base64")}function dt(t){return Buffer.from(t,"base64")}function bo(t,e){if(t.length!==e.length)return!1;let n=Buffer.from(t,"utf8"),r=Buffer.from(e,"utf8");return n.length!==r.length?!1:yo(n,r)}function Bt(t,e){let n=wo(e),r=Ut(e);return t?bo(t,n)?{outcome:"verified",fingerprint:r,presentedBase64:n}:{outcome:"mismatch",fingerprint:r,presentedBase64:n}:{outcome:"pinned",fingerprint:r,presentedBase64:n}}async function _n(t,e,n){let r=await t.load(e);if(!r)throw new Error(`Instance "${e}" not found \u2014 cannot persist host key.`);await t.save({...r,hostPublicKey:n})}var In=(t,e)=>_n(le,t,e);async function k(t){let{instanceName:e,ip:n,command:r,stream:i=!1,timeoutMs:s=3e4,port:a=22}=t,l=await kn(P.privateKey(e),"utf-8"),c=(await Ft(e))?.hostPublicKey,u=null;return new Promise((d,f)=>{let p=new Pn,g="",y="",v=null,S=!1;p.on("ready",()=>{o.debug(`SSH connected to ${n}`),(u?.outcome==="pinned"?In(e,u.presentedBase64).catch($=>{o.debug(`Failed to persist host key for ${e}: ${$}`)}):Promise.resolve()).finally(()=>{p.exec(r,($,b)=>{if($)return p.end(),f($);v=setTimeout(()=>{S||(S=!0,p.destroy(),f(new Error(`Command timed out after ${s/1e3}s on ${n}`)))},s),b.on("data",_=>{let T=_.toString();g+=T,i&&process.stdout.write(T)}),b.stderr.on("data",_=>{let T=_.toString();y+=T,i&&process.stderr.write(T)}),b.on("close",_=>{v&&clearTimeout(v),!S&&(S=!0,p.end(),d({stdout:g,stderr:y,exitCode:_??0}))})})})}).on("error",w=>{if(v&&clearTimeout(v),S)return;if(S=!0,u?.outcome==="mismatch"){let b=c?Ut(dt(c)):"(unknown)";return f(new Error(`SSH host key for ${n} does not match the pinned fingerprint for "${e}".
25
+ Pinned: ${b}
26
+ Presented: ${u.fingerprint}
27
+ This usually means a man-in-the-middle attempt, or the instance was rebuilt outside gibil. If the rebuild was expected, run: gibil destroy ${e} && gibil create --name ${e} ...`))}let $="";w.code==="ECONNREFUSED"?$=" (instance may have been destroyed or is still booting)":w.code==="EHOSTUNREACH"?$=" (IP unreachable \u2014 instance may not be running)":w.code==="ETIMEDOUT"&&($=" (connection timed out \u2014 check if instance is running with 'gibil list')"),f(new Error(`SSH connection to ${n} failed: ${w.message}${$}`))}).connect({host:n,port:a,username:"root",privateKey:l,readyTimeout:s,hostVerifier:w=>(u=Bt(c,w),u.outcome!=="mismatch"),agent:process.env.SSH_AUTH_SOCK,agentForward:!!process.env.SSH_AUTH_SOCK})})}function En(t){let{instanceName:e,ip:n,filePath:r,timeoutMs:i=3e4}=t,s=null,a=!1;return(async()=>{try{let l=await kn(P.privateKey(e),"utf-8"),c=(await Ft(e))?.hostPublicKey,u=null;s=new Pn,await new Promise((d,f)=>{s.on("ready",()=>{(u?.outcome==="pinned"?In(e,u.presentedBase64).catch(()=>{}):Promise.resolve()).finally(()=>{s.exec(`tail -f ${r} 2>/dev/null`,(g,y)=>{if(g)return s.end(),f(g);y.on("data",v=>{a||process.stdout.write(m(v.toString()))}),y.stderr.on("data",v=>{a||process.stderr.write(m(v.toString()))}),y.on("close",()=>{s.end(),d()})})})}).on("error",p=>{a||o.debug(`Verbose log tail failed: ${p.message}`),f(p)}).connect({host:n,port:22,username:"root",privateKey:l,readyTimeout:i,hostVerifier:p=>(u=Bt(c,p),u.outcome!=="mismatch")})})}catch{}})(),{abort(){if(a=!0,s)try{s.end()}catch{}}}}async function ft(t,e,n=12e4){let r=Date.now(),i=5e3;for(;Date.now()-r<n;)try{await k({instanceName:t,ip:e,command:"echo ready",timeoutMs:1e4});return}catch{o.debug(`SSH not ready on ${e}, retrying...`),await new Promise(s=>setTimeout(s,i))}throw new Error(`SSH did not become available on ${e} within ${n/1e3}s`)}function he(t){let{repo:e,config:n,ttlMinutes:r,githubToken:i,gitIdentity:s}=t,a=["#!/bin/bash","set -euo pipefail","","# \u2500\u2500 Gibil cloud-init \u2500\u2500","export HOME=/root","export DEBIAN_FRONTEND=noninteractive"];i&&a.push(`export GITHUB_TOKEN=${U(i)}`),a.push("","# Base packages","apt-get update -qq","apt-get install -y -qq git curl wget build-essential unzip > /dev/null 2>&1","","# Install GitHub CLI","if ! type gh > /dev/null 2>&1; then"," curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null"," chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg"," ARCH=$(dpkg --print-architecture)",' echo "deb [arch=${ARCH} signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list'," apt-get update -qq && apt-get install -y -qq gh > /dev/null 2>&1","fi","");let l=n?.image??"node:20";if(a.push(...$o(l)),t.agent){let c=_o(t.agent);c&&(a.push(`# Install ${t.agent} + tmux`),t.agent==="aider"&&a.push("apt-get install -y -qq python3-pip > /dev/null 2>&1"),a.push(`${c} > /dev/null 2>&1`,"apt-get install -y -qq tmux > /dev/null 2>&1",""))}if(n?.services&&n.services.length>0){a.push(...So()),a.push("");for(let c of n.services)a.push(...xo(c))}if(n?.env){a.push("# Environment variables");for(let[c,u]of Object.entries(n.env))a.push(`export ${c}=${U(u)}`),a.push(`echo ${U(`${c}=${u}`)} >> /etc/environment`);a.push("")}if(a.push("# Configure git"),s?(a.push(`git config --global user.email ${U(s.email)}`),a.push(`git config --global user.name ${U(s.name)}`),s.signingKey&&(a.push("git config --global gpg.format ssh"),a.push(`git config --global user.signingkey ${U("key::"+s.signingKey)}`),a.push("git config --global commit.gpgsign true"),a.push("git config --global tag.gpgsign true"),a.push("mkdir -p /root/.ssh"),a.push(`echo ${U(s.email+" "+s.signingKey)} > /root/.ssh/allowed_signers`),a.push("git config --global gpg.ssh.allowedSignersFile /root/.ssh/allowed_signers"))):(a.push("git config --global user.email 'gibil@bot.dev'"),a.push("git config --global user.name 'Gibil Bot'")),a.push(""),e){let c=e.match(/github\.com\/([^/]+\/[^/.]+)/);if(a.push("# Clone repository"),a.push("cd /root"),c){let u=c[1];a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(` CLONE_URL="https://x-access-token:\${GITHUB_TOKEN}@github.com/${u}.git"`),a.push("else"),a.push(` CLONE_URL='https://github.com/${u}.git'`),a.push("fi"),a.push('timeout 300 git clone "$CLONE_URL" /root/project || { echo "Git clone failed or timed out"; exit 1; }')}else a.push(`timeout 300 git clone ${U(e)} /root/project || { echo "Git clone failed or timed out"; exit 1; }`);a.push("cd /root/project"),a.push(""),a.push('if [ -n "${GITHUB_TOKEN:-}" ]; then'),a.push(' echo "${GITHUB_TOKEN}" | gh auth login --with-token 2>/dev/null || true'),c&&a.push(` git -C /root/project remote set-url origin "https://x-access-token:\${GITHUB_TOKEN}@github.com/${c[1]}.git"`),a.push("fi"),a.push("")}if(r&&r>0&&(a.push("# Auto-destroy after TTL"),a.push(`echo "shutdown -h now" | at now + ${r} minutes 2>/dev/null || true`),a.push(`(sleep ${r*60} && shutdown -h now) &`),a.push("")),a.push("# Clean up cloud-init secrets"),a.push("rm -f /var/lib/cloud/instance/user-data.txt"),a.push(""),a.push("# Signal that infrastructure is ready"),a.push("touch /root/.gibil-ready"),a.push('echo "Gibil infrastructure ready"'),a.push(""),e&&n?.tasks&&n.tasks.length>0){a.push("# Run project tasks"),a.push("cd /root/project");for(let c of n.tasks)a.push(`echo '\u25B6 Running task: '${U(c.name)}`),a.push(`if ! ${c.command}; then`),a.push(` echo '\u2717 Task failed: '${U(c.name)}`),a.push(" touch /root/.gibil-tasks-failed"),a.push("fi");a.push(""),a.push("# Signal tasks complete"),a.push("if [ ! -f /root/.gibil-tasks-failed ]; then"),a.push(" touch /root/.gibil-tasks-done"),a.push(' echo "Gibil tasks complete"'),a.push("else"),a.push(' echo "Gibil tasks finished with errors"'),a.push("fi")}return a.join(`
28
+ `)}function $o(t){let e=[];if(t.startsWith("node:")){let n=t.split(":")[1]??"20";e.push("# Install Node.js"),e.push(`curl -fsSL https://deb.nodesource.com/setup_${n}.x | bash - > /dev/null 2>&1`),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("")}else if(t.startsWith("python:")){let n=t.split(":")[1]??"3.12";e.push("# Install Python"),e.push(`apt-get install -y -qq python${n} python3-pip python3-venv > /dev/null 2>&1`),e.push("")}else if(t.startsWith("go:")){let n=t.split(":")[1]??"1.22";e.push("# Install Go"),e.push('GO_ARCH=$(uname -m | sed "s/x86_64/amd64/" | sed "s/aarch64/arm64/")'),e.push(`wget -q https://go.dev/dl/go${n}.linux-\${GO_ARCH}.tar.gz -O /tmp/go.tar.gz`),e.push("tar -C /usr/local -xzf /tmp/go.tar.gz"),e.push("export PATH=$PATH:/usr/local/go/bin"),e.push('echo "export PATH=\\$PATH:/usr/local/go/bin" >> /root/.bashrc'),e.push("")}else e.push("# Install Node.js (default)"),e.push("curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1"),e.push("apt-get install -y -qq nodejs > /dev/null 2>&1"),e.push("npm install -g pnpm@latest > /dev/null 2>&1"),e.push("");return e}function So(){return["# Install Docker","curl -fsSL https://get.docker.com | sh > /dev/null 2>&1","systemctl enable docker --now"]}function xo(t){let e=[];e.push(`# Start service: ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`);let r=`docker run -d --name ${t.name.replace(/[^a-zA-Z0-9_-]/g,"")}`;if(t.port&&(r+=` -p ${t.port}:${t.port}`),t.env)for(let[i,s]of Object.entries(t.env))r+=` -e ${i}=${U(s)}`;return r+=` ${U(t.image)}`,e.push(r),e.push(""),e}var Tn={claude:"npm install -g @anthropic-ai/claude-code",aider:"pip install --break-system-packages aider-chat",codex:"npm install -g @openai/codex"},Ie={claude:["ANTHROPIC_API_KEY"],aider:["ANTHROPIC_API_KEY","OPENAI_API_KEY"],codex:["OPENAI_API_KEY"]},Q=Object.keys(Tn);function _o(t){return Tn[t]??null}function U(t){return`'${t.replace(/'/g,"'\\''")}'`}import{readFile as Po}from"fs/promises";import{existsSync as Cn,statSync as ko}from"fs";import{join as Io}from"path";import{parse as jn}from"yaml";async function Ee(t){let e=t.match(/github\.com\/([^/]+)\/([^/.]+)/);if(!e)return null;let[,n,r]=e,i=`https://raw.githubusercontent.com/${n}/${r}/HEAD/.gibil.yml`;try{let s={};process.env.GITHUB_TOKEN&&(s.Authorization=`token ${process.env.GITHUB_TOKEN}`);let a=await fetch(i,{signal:AbortSignal.timeout(1e4),headers:s});if(!a.ok)return null;let l=await a.text();return To(l)}catch{return null}}var Eo=".gibil.yml";async function Te(t){let e;if(Cn(t)&&ko(t).isFile()?e=t:e=Io(t,Eo),!Cn(e))return null;let n=await Po(e,"utf-8"),r=jn(n);return Nn(r)}function To(t){let e=jn(t);return Nn(e)}function Nn(t){if(!t||typeof t!="object")throw new Error("Invalid .gibil.yml: must be a YAML object");let e=t,n={};return typeof e.name=="string"&&(n.name=e.name),typeof e.image=="string"&&(n.image=e.image),typeof e.server_type=="string"&&(n.server_type=e.server_type),typeof e.location=="string"&&(n.location=e.location),Array.isArray(e.services)&&(n.services=e.services.map(r=>{let i=r;if(typeof i.name!="string"||typeof i.image!="string")throw new Error("Each service must have a 'name' and 'image' field");return{name:i.name,image:i.image,port:typeof i.port=="number"?i.port:void 0,env:An(i.env,`service "${i.name}"`)}})),Array.isArray(e.tasks)&&(n.tasks=e.tasks.map(r=>{let i=r;if(typeof i.name!="string"||typeof i.command!="string")throw new Error("Each task must have a 'name' and 'command' field");return{name:i.name,command:i.command}})),e.env!==void 0&&(n.env=An(e.env,"top-level")),n}function An(t,e){if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new Error(`env in ${e} must be a key-value object`);let n={};for(let[r,i]of Object.entries(t))if(typeof i=="string")n[r]=i;else if(typeof i=="number"||typeof i=="boolean")n[r]=String(i);else throw new Error(`env.${r} in ${e} must be a string, number, or boolean \u2014 got ${typeof i}`);return Object.keys(n).length>0?n:void 0}O();import{randomBytes as Co}from"crypto";function ye(t=6){return Co(Math.ceil(t/2)).toString("hex").slice(0,t)}function Be(){return`gibil-${ye()}`}function Rn(){return`fleet-${ye(8)}`}function mt(){return`j-${ye(8)}`}L();E();var Ao=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$/;function pt(t){if(!Ao.test(t))throw new Error(`Invalid instance name "${t}". Names must be 1-63 chars, start with alphanumeric, and contain only [a-zA-Z0-9_-].`);return t}function gt(t,e){let n=parseInt(t,10);if(isNaN(n)||n<=0)throw new Error(`${e} must be a positive integer, got "${t}"`);return n}var J=525600,jo={m:1,h:60,d:1440,w:10080,mo:43200,y:525600};function Ce(t){let e=t.trim().toLowerCase();if(/^\d+$/.test(e)){let l=parseInt(e,10);if(l<=0)throw new Error(`TTL must be positive, got "${t}"`);if(l>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got ${l} minutes.`);return l}let n=e.match(/^(\d+)(mo|[mhdwy])$/);if(!n)throw new Error(`Invalid TTL "${t}". Use a number (minutes) or a duration: 2h, 7d, 1w, 1mo, 3mo, 6mo, 1y`);let r=parseInt(n[1],10),i=n[2],s=jo[i],a=r*s;if(a<=0)throw new Error(`TTL must be positive, got "${t}"`);if(a>J)throw new Error(`TTL cannot exceed 1 year (${J} minutes). Got "${t}" = ${a} minutes.`);return a}G();R();import{execSync as ht}from"child_process";import{readFileSync as Ho}from"fs";var Jt="key::";function Mo(){try{let t=ht("git config user.name",{encoding:"utf-8"}).trim(),e=ht("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(ht("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=ht("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=Ho(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith(Jt))&&(n=i.startsWith(Jt)?i.slice(Jt.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function yt(t,e,n){o.step("Generating SSH keys...");let r=await lt(e),i,s;try{o.step("Uploading SSH key..."),i=await t.createSSHKey(`gibil-${e}-${ye(4)}`,r.publicKey),n.repo&&n.repo.includes("github.com")&&!process.env.GITHUB_TOKEN&&o.debug("No GITHUB_TOKEN set \u2014 private repos will fail to clone. Set GITHUB_TOKEN to enable private repo access.");let a=Mo(),l=he({repo:n.repo,config:n.config??void 0,ttlMinutes:n.ttlMinutes,githubToken:process.env.GITHUB_TOKEN,gitIdentity:a,agent:n.agent}),c=o.spin(`Creating server on ${n.providerName??"hetzner"}...`),u=await t.createServer(e,i.id,l,n.serverType??n.config?.server_type,n.location??n.config?.location);s=u.id,c.succeed("Server created");let d=o.spin("VM booting..."),p=(await t.waitForReady(u.id)).ipv4;d.succeed(`VM running at ${p}`);let g=new Date,y={name:e,serverId:u.id,ip:p,sshKeyId:i.id,keyPath:P.privateKey(e),status:"running",createdAt:g.toISOString(),ttlMinutes:n.ttlMinutes,expiresAt:new Date(g.getTime()+n.ttlMinutes*6e4).toISOString(),repo:n.repo,fleetId:n.fleetId,gitIdentity:a,provider:n.providerName??"hetzner"};await pe(y);let v=o.spin("Waiting for SSH...");if(await ft(e,p),v.succeed("SSH ready"),n.repo||n.config){let S=o.spin("Provisioning (runtime, repo, deps)..."),w;n.verbose&&!n.json&&(w=En({instanceName:e,ip:p,filePath:"/var/log/cloud-init-output.log"}));let $=36e4,b=5e3,_=Date.now(),T=!1;for(;Date.now()-_<$;){try{if((await k({instanceName:e,ip:p,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){T=!0;break}}catch{}await new Promise(oe=>setTimeout(oe,b))}if(w?.abort(),T)S.succeed("Provisioning complete");else{S.fail("Provisioning may have failed");try{let oe=await k({instanceName:e,ip:p,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'No cloud-init log found'",timeoutMs:1e4});o.info(oe.stdout)}catch{o.warn("Could not read cloud-init log.")}}}return y}catch(a){throw o.error(`Failed to create instance "${e}", cleaning up...`),s&&await t.destroyServer(s).catch(l=>o.warn(`Could not destroy server ${s}: ${l instanceof Error?l.message:String(l)}`)),i&&await t.deleteSSHKey(i.id).catch(l=>o.warn(`Could not delete SSH key ${i.id}: ${l instanceof Error?l.message:String(l)}`)),await ne(e).catch(l=>o.warn(`Could not clean up local SSH keys: ${l instanceof Error?l.message:String(l)}`)),a}}function Hn(t){let e=Math.max(0,Math.floor((new Date(t.expiresAt).getTime()-Date.now())/1e3));return{name:t.name,ip:t.ip,ssh:`gibil ssh ${t.name}`,status:t.status,ttl_remaining:e,created_at:t.createdAt,fleet_id:t.fleetId}}function Mn(t){t.command("create").description("Spin up one or more ephemeral dev machines").option("-n, --name <name>","Instance name").option("-f, --fleet <count>","Number of instances to create in parallel").option("-r, --repo <git-url>","Git repository to clone on startup").option("--json","Output instance info as JSON").option("--ttl <duration>","Auto-destroy timer (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)","60").option("-c, --config <path>","Path to .gibil.yml config").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("-q, --quiet","Suppress non-essential output").option("-e, --env <KEY=VALUE...>","Environment variables to set on the server").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async e=>{e.json&&I(!0);let n=Ce(e.ttl??"60"),r=gt(e.fleet??"1","Fleet count");if(r>20)throw new Error("Fleet size cannot exceed 20. Contact support for higher limits.");if(e.name&&pt(e.name),!e.agent){let d=await Fe();d&&(e.agent=d)}if(e.agent&&!Q.includes(e.agent))throw new Error(`Unknown agent "${e.agent}". Supported: ${Q.join(", ")}`);let i={};if(e.env)for(let d of e.env){let f=d.indexOf("=");if(f<=0)throw new Error(`Invalid --env format: "${d}". Use KEY=VALUE.`);i[d.slice(0,f)]=d.slice(f+1)}i.GITHUB_TOKEN&&!process.env.GITHUB_TOKEN&&(process.env.GITHUB_TOKEN=i.GITHUB_TOKEN);let s=null;if(e.config?s=await Te(e.config):e.repo?s=await Ee(e.repo)??await Te(process.cwd()):s=await Te(process.cwd()),Object.keys(i).length>0&&(s||(s={}),s.env={...s.env,...i}),e.dryRun){let d=e.name??Be(),f=e.provider??"hetzner",p=e.serverType??s?.server_type;if(!p&&e.size){let{isSizeName:$}=await Promise.resolve().then(()=>(Ve(),Je));if(!$(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);let{PROVIDER_CATALOG:b}=await Promise.resolve().then(()=>(ze(),cn));p=b[f]?.sizes.find(T=>T.name===e.size)?.nativeType}p||(p=f==="vultr"?"vc2-2c-4gb":"cax11");let g=p,y=e.location??s?.location??(f==="vultr"?"nrt":"nbg1"),v=s?.image??"node:20",S=he({repo:e.repo,config:s??void 0,ttlMinutes:n,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:e.agent}),w={name:d,serverType:g,location:y,image:v,ttlMinutes:n,repo:e.repo,agent:e.agent,cloudInitScript:S};e.json?o.json(w):(o.info(""),o.info(h("Dry run \u2014 no server will be created")),o.info(""),o.info(` ${m("Name:")} ${d}`),o.info(` ${m("Server type:")} ${g}`),o.info(` ${m("Location:")} ${y}`),o.info(` ${m("Image:")} ${v}`),o.info(` ${m("TTL:")} ${n} minutes`),e.repo&&o.info(` ${m("Repo:")} ${e.repo}`),e.agent&&o.info(` ${m("Agent:")} ${e.agent}`),o.info(""),o.info("Cloud-init script:"),o.info("\u2500".repeat(17)),o.info(S));return}let a=await M();if(a){o.info("Verifying API key...");let d=await ce(a);o.info(` Authenticated as ${d.user.email} (${d.user.plan})`)}if(e.agent&&!Ie[e.agent]?.some(f=>s?.env?.[f]||i[f])){let f=Ie[e.agent]?.join(" or ")??"";o.warn(`${e.agent} needs ${f}. SSH in and export it (recommended) or pass with --env.`)}let l=e.provider??"hetzner",c=await F.get(l),u=e.serverType;if(!u&&e.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ve(),Je));if(!d(e.size))throw new Error(`Unknown size "${e.size}". Valid sizes: small, medium, large.`);u=f(c,e.size)}if(r===1){let d=e.name??Be(),f=Date.now(),p=o.spin(`Forging "${d}"...`),g=await yt(c,d,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,agent:e.agent,verbose:e.verbose}),y=((Date.now()-f)/1e3).toFixed(1);p.succeed(C.createReady(d,y)),a&&await te(a,"create",g.name,e.serverType).catch(v=>o.debug(`Usage tracking failed: ${v instanceof Error?v.message:String(v)}`)),e.json?o.json(Hn(g)):(o.info(""),o.info(et("Server ready",[`${m("Name:")} ${h(g.name)}`,`${m("IP:")} ${g.ip}`,`${m("TTL:")} ${n} minutes`,`${m("SSH:")} ${h(`gibil ssh ${g.name}`)}`])),o.info(""),o.info(m(" Try:")),o.info(` ${h(`gibil run ${g.name} "<your test command>"`)}`),o.info(` ${h(`gibil ssh ${g.name}`)}`),o.info(` ${h(`gibil destroy ${g.name}`)}`),o.info(""))}else{let d=Rn(),f=e.name??"gibil",p=Date.now(),g=o.spin(`Forging fleet "${d}" \u2014 ${r} servers...`),y=Array.from({length:r},(b,_)=>`${f}-${_+1}-${d.slice(6)}`),v=await Promise.allSettled(y.map(b=>yt(c,b,{repo:e.repo,ttlMinutes:n,config:s,providerName:l,serverType:u,location:e.location,fleetId:d,agent:e.agent,verbose:e.verbose}))),S=[],w=[];for(let b=0;b<v.length;b++){let _=v[b];_.status==="fulfilled"?S.push(_.value):w.push(`${y[b]}: ${_.reason instanceof Error?_.reason.message:String(_.reason)}`)}let $=((Date.now()-p)/1e3).toFixed(1);if(g.succeed(C.fleetReady(S.length,r)+` ${m(`(${$}s)`)}`),a&&await Promise.all(S.map(b=>te(a,"create",b.name,e.serverType).catch(_=>o.debug(`Usage tracking failed for ${b.name}: ${_ instanceof Error?_.message:String(_)}`)))),e.json)o.json({fleet_id:d,instances:S.map(Hn),errors:w});else{o.info("");for(let b of S)o.info(` ${N} ${h(b.name)} ${m("\u2192")} ${b.ip}`);for(let b of w)o.info(` ${Le} ${b}`);o.info("")}}})}O();import{spawn as Wo}from"child_process";import{spawn as zo}from"child_process";import{existsSync as Go}from"fs";L();import{writeFile as Ko,mkdir as Do,rm as Lo}from"fs/promises";var Kn={dir:P.knownHostsDir,file:P.knownHostsFile};async function Ae(t,e=Kn){if(!t.hostPublicKey)return null;await Do(e.dir,{recursive:!0,mode:448});let n=e.file(t.name),r=dt(t.hostPublicKey),i=xn(t.ip,22,r);return await Ko(n,i,{mode:384}),n}async function Dn(t,e=Kn){await Lo(e.file(t),{force:!0})}E();R();function qe(t){if(t.includes(":")){let e=t.split(":");if(e.length!==3)throw new Error(`Invalid port mapping "${t}". Use PORT or LOCAL:HOST:REMOTE.`);let[n,,r]=e;Vt(n,t),Vt(r,t)}else Vt(t,t)}function Vt(t,e){let n=parseInt(t,10);if(isNaN(n)||n<1||n>65535||String(n)!==t)throw new Error(`Invalid port number in "${e}". Must be 1-65535.`)}async function Ln(t,e){if(!Go(t.keyPath))throw new Error(`SSH key not found: ${t.keyPath}. The instance may have been destroyed.`);let n=await Ae(t);n||o.warn(m(` No pinned host key for ${t.name} (instance pre-dates v0.4.x). Tunnels are insecure on this hop; running any \`gibil run ${t.name}\` will pin it.`));let r=[];for(let i of e){qe(i);let s=i.includes(":")?i:`${i}:localhost:${i}`,a=i.includes(":")?i.split(":")[0]:i;r.push(a);let l=["-f","-N","-L",s,"-i",t.keyPath,"-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];n?l.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${n}`):l.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null"),l.push(`root@${t.ip}`),zo("ssh",l,{stdio:"ignore",detached:!0}).unref()}return r}E();R();E();O();we();var Yo=["ECONNREFUSED","EHOSTUNREACH","ETIMEDOUT"];function be(t){if(!(t instanceof Error))return!1;let e=t.message;return Yo.some(n=>e.includes(n))}async function q(t){try{let{providerRegistry:e}=await Promise.resolve().then(()=>(ke(),yn));return await(await e.forInstance(t)).getServer(t.serverId),"still_exists"}catch(e){return e instanceof Error&&e.message.includes("(404)")?(await ne(t.name),await Ye(t.name),await ge(t.name),o.warn(`Instance "${t.name}" no longer exists on the provider \u2014 cleaned up local metadata`),"cleaned"):"api_error"}}function Zo(t){let e=["-A","-i",t.keyPath,"-o","LogLevel=ERROR"];if(t.knownHostsPath?e.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${t.knownHostsPath}`):e.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null"),t.ports&&t.ports.length>0)for(let n of t.ports){qe(n);let r=n.includes(":")?n:`${n}:localhost:${n}`;e.push("-L",r)}return e.push(`root@${t.ip}`),e}function Bn(t){t.command("ssh <name>").description("SSH into a running ephemeral machine").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000 --port 5432)").action(async(e,n)=>{let r=await A(e),i=await Ae(r);i||o.warn(m(` No pinned host key for ${e} (instance pre-dates v0.4.x). Connection is insecure on this hop; running any \`gibil run ${e}\` will pin it.`));let s=Zo({ip:r.ip,keyPath:r.keyPath,knownHostsPath:i??void 0,ports:n.port});if(n.port&&n.port.length>0){o.info("");for(let l of n.port){let c=l.includes(":")?l.split(":")[0]:l;o.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${l}`)}o.info(""),o.info(m(" Tunnel active while SSH session is open. Ctrl+C to stop.")),o.info("")}Wo("ssh",s,{stdio:"inherit"}).on("exit",l=>{let c=()=>process.exit(l??0);l===255?q(r).then(c,c):c()})})}O();we();E();function Jn(t){t.command("run <name> <command...>").description("Execute a command on a running instance").option("--json","Output result as JSON").option("--timeout <seconds>","Command timeout in seconds (default: 30)").option("-b, --background","Run in background, return job ID immediately").action(async(e,n,r)=>{r.json&&I(!0);let i=await A(e),s=n.join(" "),a=r.timeout?gt(r.timeout,"Timeout")*1e3:3e4;if(r.background){let c=mt(),u="/root/.gibil-jobs",d=`${u}/${c}.log`,f=`${u}/${c}.exit`,p=`${u}/${c}.pid`,g=`${u}/${c}.sh`,y=["#!/bin/bash",`nohup bash -c '${s.replace(/'/g,"'\\''")}' > ${d} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${p}`,`(wait $BGPID 2>/dev/null; echo $? > ${f}) &`,"echo $BGPID"].join(`
29
+ `),v=Buffer.from(y).toString("base64"),S=`mkdir -p ${u} && echo '${v}' | base64 -d > ${g} && chmod +x ${g} && bash ${g}`,w;try{w=await k({instanceName:e,ip:i.ip,command:S,timeoutMs:1e4})}catch(b){throw be(b)&&await q(i)==="cleaned"&&process.exit(1),b}let $=parseInt(w.stdout.trim(),10);isNaN($)&&(o.error("Failed to start background job \u2014 could not capture PID"),process.exit(1)),await V({id:c,instance:e,command:s,pid:$,status:"running",startedAt:new Date().toISOString()}),r.json?o.json({job_id:c,instance:e,status:"running",pid:$}):(o.info(`Background job started: ${c} (PID ${$})`),o.info(` Poll: gibil job ${c}`));return}o.info(`Running on "${e}" (${i.ip}): ${s}`);let l;try{l=await k({instanceName:e,ip:i.ip,command:s,stream:!r.json,timeoutMs:a})}catch(c){throw be(c)&&await q(i)==="cleaned"&&process.exit(1),c}r.json?o.json({instance:e,command:s,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&o.error(`Command exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}ke();import{createInterface as di}from"readline";O();we();E();G();R();L();import{createHash as Vn}from"crypto";import{readFile as Wn,writeFile as Xo,mkdir as Qo}from"fs/promises";import{existsSync as wt,readFileSync as ei}from"fs";import{hostname as qn,userInfo as ti,platform as ni,arch as ri}from"os";import{join as Oe,dirname as Zn}from"path";import{fork as oi}from"child_process";import{fileURLToPath as Xn}from"url";function Qn(){return Oe(P.root,"device_id")}function ii(){return Oe(P.root,"config.json")}var si=process.env.GIBIL_TELEMETRY_URL??"https://zopdxjruwktjyjunitrv.supabase.co/functions/v1/telemetry-ingest",ai="tk_alpha_09a93302e0f3e73417a9e9dbfc500a61",We=null,Ne=null;function ci(){try{let t=ti(),e=`${qn()}:${t.username}:${t.homedir}`;return Vn("sha256").update(e).digest("hex").slice(0,16)}catch{return Vn("sha256").update(`${qn()}:${Date.now()}`).digest("hex").slice(0,16)}}async function er(){if(We)return We;let t=Qn();if(wt(t)){let n=(await Wn(t,"utf-8")).trim();if(n.length>0)return We=n,We}let e=ci();return await Qo(P.root,{recursive:!0,mode:448}),await Xo(t,e,{mode:384}),We=e,e}async function qt(){if(Ne!==null)return Ne;let t=process.env.GIBIL_TELEMETRY;if(t!==void 0)return Ne=!["0","false","off","no"].includes(t.toLowerCase()),Ne;try{let e=ii();if(wt(e)&&JSON.parse(await Wn(e,"utf-8")).telemetry===!1)return Ne=!1,!1}catch{}return Ne=!0,!0}async function tr(){return wt(Qn())?!1:(await er(),!0)}var Re=null;function li(){if(Re)return Re;let t=Zn(Xn(import.meta.url));for(let e of["../package.json","../../package.json"])try{return Re=JSON.parse(ei(Oe(t,e),"utf-8")).version??"0.0.0",Re}catch{}return Re="0.0.0",Re}async function Ze(t){if(!await qt())return;let e=await er();if(!e)return;let n={...t,device_id:e,cli_version:li(),timestamp:new Date().toISOString(),os:ni(),arch:ri(),node_version:process.version},r=Zn(Xn(import.meta.url)),s=[Oe(r,"telemetry-send.js"),Oe(r,"..","utils","telemetry-send.js"),Oe(r,"telemetry-send.ts")].find(a=>wt(a));if(s)try{oi(s,{detached:!0,stdio:"ignore",...s.endsWith(".ts")?{execArgv:["--import","tsx"]}:{},env:{...process.env,TELEMETRY_PAYLOAD:JSON.stringify(n),TELEMETRY_ENDPOINT:si,TELEMETRY_INGEST_KEY:ai}}).unref()}catch{}}var Yn=new Map,ui=5e3;async function nr(t){let e=Date.now(),n=Yn.get(t)??0;e-n<ui||(Yn.set(t,e),await Ze({event:"mcp_tool",tool:t}))}function rr(t){return t.slice(3).filter(e=>e.startsWith("-")).map(e=>e.replace(/=.*$/,""))}async function fi(t){let e=t.length;o.info(""),o.info(h(`This will destroy ${e} running server${e===1?"":"s"}:`)),o.info("");for(let i of t){let s=Math.round((Date.now()-new Date(i.createdAt).getTime())/6e4),a=s<60?`${s}m old`:`${Math.floor(s/60)}h ${s%60}m old`;o.info(` ${m("\u2022")} ${h(i.name)} ${m("\u2192")} ${i.ip} ${m(`(${a})`)}`)}o.info(""),o.info(m("Type 'yes' to confirm, anything else to cancel:"));let n=di({input:process.stdin,output:process.stdout}),r=await new Promise(i=>n.question("> ",i));return n.close(),r.trim().toLowerCase()==="yes"}async function or(t){let e=await ut(t),n=await F.forInstance(e);o.info(`Destroying instance "${t}" (server ${e.serverId})...`);try{await n.destroyServer(e.serverId)}catch(s){o.warn(`Could not delete server ${e.serverId}: ${s instanceof Error?s.message:String(s)}`)}try{await n.deleteSSHKey(e.sshKeyId)}catch(s){o.warn(`Could not delete SSH key ${e.sshKeyId}: ${s instanceof Error?s.message:String(s)}`)}await ne(t),await Ye(t),await Dn(t),await ge(t);let r=await M();r&&await te(r,"destroy",t).catch(s=>o.warn(`Usage tracking failed (billing may be inaccurate): ${s instanceof Error?s.message:String(s)}`));let i=Math.round((Date.now()-new Date(e.createdAt).getTime())/6e4);await Ze({event:"lifecycle",duration_minutes:i}),o.info(` ${N} ${C.destroySingle(t)}`)}function ir(t){t.command("destroy [name]").description("Destroy a running ephemeral machine").option("-a, --all","Destroy all gibil instances").option("--json","Output result as JSON").option("-y, --yes","Skip the confirmation prompt for --all (required in non-interactive contexts)").action(async(e,n)=>{if(n.json&&I(!0),n.all){let r=await X();if(r.length===0){n.json?o.json({destroyed:[],failed:[]}):o.info(C.noInstances);return}if(!n.yes&&(n.json&&(o.json({error:"confirmation required",message:`--all would destroy ${r.length} server(s). Re-run with --yes to confirm.`,instances:r.map(u=>({name:u.name,ip:u.ip}))}),process.exit(1)),process.stdin.isTTY||(o.error("stdin is not a TTY. Re-run with --yes to confirm in non-interactive contexts."),process.exit(1)),!await fi(r))){o.info(""),o.info("Cancelled. No servers were destroyed.");return}o.info(`Destroying ${r.length} instance(s)...`);let i=3,s=[];for(let c=0;c<r.length;c+=i){let u=r.slice(c,c+i),d=await Promise.allSettled(u.map(f=>or(f.name)));s.push(...d)}let a=[],l=[];for(let c=0;c<s.length;c++)if(s[c].status==="fulfilled")a.push(r[c].name);else{let u=s[c].reason;l.push(`${r[c].name}: ${u instanceof Error?u.message:String(u)}`)}n.json?o.json({destroyed:a,failed:l}):l.length===0?o.info(`
30
+ ${C.destroyAll}`):o.info(`
31
+ ${a.length} destroyed, ${l.length} failed`)}else e||(o.error('Specify an instance name or use --all. Run "gibil list" to see instances.'),process.exit(1)),await or(e),n.json&&o.json({destroyed:[e]})})}O();E();R();function sr(t){t.command("list").alias("ls").description("List all active gibil instances").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await X();if(n.length===0){e.json?o.json({instances:[]}):o.info(C.noInstances);return}let r=n.map(i=>{let s=Math.max(0,Math.floor((new Date(i.expiresAt).getTime()-Date.now())/1e3));return{name:i.name,ip:i.ip,ssh:`gibil ssh ${i.name}`,status:i.status,ttl_remaining:s,created_at:i.createdAt,fleet_id:i.fleetId,provider:i.provider??"hetzner"}});if(e.json){o.json({instances:r});return}o.info(m(`${"NAME".padEnd(28)} ${"PROVIDER".padEnd(10)} ${"IP".padEnd(18)} ${"STATUS".padEnd(11)} ${"TTL".padEnd(10)} ${"AGE".padEnd(10)}`)),o.info(m("\u2500".repeat(90)));for(let i of r){let s=ar(i.ttl_remaining),a=mi(i.created_at),l=i.name.padEnd(28),c=i.provider.padEnd(10),u=i.status.padEnd(11),d=s.padEnd(10),f=a.padEnd(10),p=i.status==="running"?ee(u):se(u),g=i.ttl_remaining<=300?se(d):d;o.info(`${h(l)} ${m(c)} ${i.ip.padEnd(18)} ${p} ${g} ${m(f)}`)}o.info(`
32
+ ${m(`${r.length} server(s)`)}`)})}function ar(t){if(t<=0)return"expired";let e=Math.floor(t/60),n=Math.floor(e/60),r=Math.floor(n/24);if(r>=1){let i=n%24;return i>0?`${r}d ${i}h`:`${r}d`}return n>=1?`${n}h ${e%60}m`:`${e}m ${t%60}s`}function mi(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);return ar(n)}O();E();function cr(t){t.command("extend <name>").description("Extend the TTL of a running instance").requiredOption("--ttl <duration>","New TTL from now (e.g. 60, 2h, 7d, 1mo, 3mo, 1y)").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let r=await A(e),i=Ce(n.ttl);try{await k({instanceName:e,ip:r.ip,command:["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${i} minutes 2>/dev/null || true`,`(sleep ${i*60} && shutdown -h now) &`].join(" && ")})}catch(a){throw be(a)&&await q(r)==="cleaned"&&process.exit(1),a}let s=new Date(Date.now()+i*6e4).toISOString();r.ttlMinutes=i,r.expiresAt=s,await pe(r),n.json?o.json({name:r.name,ttl_minutes:i,expires_at:s}):o.info(`\u2713 Extended "${e}" TTL to ${i} minutes (expires ${s})`)})}import{readFile as pi}from"fs/promises";import{randomBytes as gi}from"crypto";O();E();function lr(t){t.command("exec <name>").description("Upload and run a local script on an instance").requiredOption("-s, --script <path>","Path to local script").option("--json","Output result as JSON").action(async(e,n)=>{n.json&&I(!0);let r=await A(e),i=await pi(n.script,"utf-8");o.info(`Uploading and running script "${n.script}" on "${e}"...`);let s=Buffer.from(i).toString("base64"),a=`/tmp/gibil-script-${gi(4).toString("hex")}.sh`,l;try{l=await k({instanceName:e,ip:r.ip,command:`echo '${s}' | base64 -d > ${a} && chmod +x ${a} && ${a}; EXIT=$?; rm -f ${a}; exit $EXIT`,stream:!n.json})}catch(c){throw be(c)&&await q(r)==="cleaned"&&process.exit(1),c}n.json?o.json({instance:e,script:n.script,stdout:l.stdout,stderr:l.stderr,exit_code:l.exitCode}):l.exitCode!==0&&o.error(`Script exited with code ${l.exitCode}`),process.exit(l.exitCode??1)})}G();E();R();import{createInterface as hi}from"readline";function ur(t){let e=hi({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,r=>{e.close(),n(r.trim())})})}function dr(t){let e=t.command("auth").description("Manage authentication");e.command("login").description("Log in with your Gibil API key").option("--key <api-key>","API key (or enter interactively)").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=n.key??process.env.GIBIL_API_KEY;r||(r=await ur("Enter your API key: ")),r||(o.error("No API key provided."),process.exit(1)),r.startsWith("pk_")||(o.error('Invalid key format. API keys start with "pk_".'),process.exit(1)),o.info("Verifying API key...");try{let i=await ce(r);await Tt(r),n.json?o.json({authenticated:!0,email:i.user.email,plan:i.user.plan}):(o.info(C.authSuccess),o.detail("Email",i.user.email),o.detail("Plan","alpha (free)"))}catch(i){o.error(i instanceof Error?i.message:String(i)),process.exit(1)}}),e.command("setup").description("Configure Hetzner API token (stored in ~/.gibil/config.json)").option("--token <token>","Hetzner API token (or enter interactively)").action(async n=>{let r=n.token;r||(r=await ur("Enter your Hetzner API token: ")),r||(o.error("No token provided."),process.exit(1));try{let s=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${r}`}})).json();s.error&&(o.error(`Invalid token: ${s.error.message}`),process.exit(1))}catch(i){o.error(`Could not verify token: ${i instanceof Error?i.message:"Check your network."}`),process.exit(1)}await jt(r),o.success("Hetzner token saved to ~/.gibil/config.json")}),e.command("logout").description("Clear stored API key").action(async()=>{await Ct(),o.info(C.authLogout)}),e.command("status").description("Show current authentication status").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=await M();if(!r){n.json?o.json({authenticated:!1}):o.info(`Not logged in. Run ${h("gibil auth login")} to authenticate.`);return}try{let i=await ce(r);n.json?o.json({authenticated:!0,email:i.user.email,plan:i.user.plan,limits:i.limits}):(o.success(`Authenticated as ${i.user.email}`),o.detail("Plan",i.user.plan),o.detail("Concurrent servers",String(i.limits.max_concurrent)),o.detail("Hours remaining",String(i.limits.remaining_hours)))}catch{n.json?o.json({authenticated:!1,error:"Key verification failed"}):o.error(`Stored API key is invalid. Run ${h("gibil auth login")} to re-authenticate.`)}})}G();E();function fr(t){t.command("usage").description("Show current month's usage and plan limits").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await M();n||(o.error('Not logged in. Run "gibil auth login" first.'),process.exit(1));try{let r=await Rt(n);e.json?o.json(r):(o.info("Plan: alpha (free)"),o.info(`VM hours used: ${r.vm_hours_used.toFixed(1)}h`),o.info(`Active instances: ${r.active_instances}`))}catch(r){o.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}import{McpServer as yi}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as vi}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as x}from"zod";ke();O();L();import{execSync as bt}from"child_process";import{readFileSync as wi}from"fs";we();O();we();E();async function Yt(t){let e=await ue(t);if(e.status!=="running")return{status:e.status,exitCode:e.exitCode};let n=await A(e.instance),r="/root/.gibil-jobs",i=`${r}/${t}.exit`,s=`${r}/${t}.log`,l=(await k({instanceName:e.instance,ip:n.ip,command:`test -f ${i} && cat ${i} || echo RUNNING`,timeoutMs:1e4})).stdout.trim();if(l==="RUNNING"){try{if((await k({instanceName:e.instance,ip:n.ip,command:`kill -0 ${e.pid} 2>/dev/null && echo "alive" || echo "dead"`,timeoutMs:1e4})).stdout.trim()==="dead"){let y=new Date;e.status="orphaned",e.completedAt=y.toISOString(),await V(e);let v=Math.round((y.getTime()-new Date(e.startedAt).getTime())/1e3),S;try{S=(await k({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4})).stdout}catch{}return{status:"orphaned",durationS:v,stdout:S}}}catch{}return{status:"running"}}let c=parseInt(l,10),u=await k({instanceName:e.instance,ip:n.ip,command:`cat ${s} 2>/dev/null || echo ''`,timeoutMs:1e4}),d=c===0?"done":"failed",f=new Date,p=Math.round((f.getTime()-new Date(e.startedAt).getTime())/1e3);return e.status=d,e.exitCode=c,e.completedAt=f.toISOString(),await V(e),{status:d,exitCode:c,stdout:u.stdout,durationS:p}}function mr(t){let e=t.command("job").description("Manage background jobs");e.command("status <id>").description("Check status of a background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n),s=await Yt(n);r.json?o.json({job_id:n,instance:i.instance,command:i.command,status:s.status,exit_code:s.exitCode,started_at:i.startedAt,duration_s:s.durationS,...s.stdout!==void 0?{stdout:s.stdout}:{}}):s.status==="running"?(o.info(`Job ${n} is still running on "${i.instance}"`),o.info(` Command: ${i.command}`),o.info(` Started: ${i.startedAt}`)):s.status==="orphaned"?(o.warn(`Job ${n} is orphaned \u2014 process died without writing exit code`),o.info(` Instance: ${i.instance}`),o.info(` Command: ${i.command}`),s.durationS!==void 0&&o.info(` Duration: ${s.durationS}s`),s.stdout&&(o.info(" Output:"),process.stdout.write(s.stdout))):(o.info(`Job ${n}: ${s.status} (exit code ${s.exitCode}, ${s.durationS}s)`),s.stdout&&process.stdout.write(s.stdout))}),e.command("list").description("List all background jobs").option("--json","Output result as JSON").action(async n=>{n.json&&I(!0);let r=await je(),i=await X(),s=new Set(i.map(a=>a.name));for(let a of r)a.status==="running"&&!s.has(a.instance)&&(a.status="orphaned",a.completedAt=new Date().toISOString(),await V(a));if(r.length===0){n.json?o.json([]):o.info("No background jobs.");return}if(n.json)o.json(r.map(a=>({job_id:a.id,instance:a.instance,command:a.command,status:a.status,started_at:a.startedAt,exit_code:a.exitCode})));else for(let a of r){let l=a.status==="running"?"\u27F3 running":a.status==="done"?"\u2713 done":a.status==="orphaned"?"\u26A0 orphaned":`\u2717 ${a.status}`;o.info(` ${a.id} ${l} ${a.instance} ${a.command}`)}}),e.command("cancel <id>").description("Cancel a running background job").option("--json","Output result as JSON").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n);if(i.status!=="running"){r.json?o.json({job_id:n,status:i.status,message:"Job is not running"}):o.info(`Job ${n} is not running (status: ${i.status})`);return}let s=await A(i.instance);await k({instanceName:i.instance,ip:s.ip,command:`kill -- -${i.pid} 2>/dev/null || kill ${i.pid} 2>/dev/null || true`,timeoutMs:1e4}),i.status="cancelled",i.completedAt=new Date().toISOString(),await V(i),r.json?o.json({job_id:n,status:"cancelled"}):o.info(`Job ${n} cancelled.`)}),e.command("logs <id>").description("Fetch output of a background job").option("--json","Output result as JSON").option("-f, --follow","Follow log output (tail -f)").action(async(n,r)=>{r.json&&I(!0);let i=await ue(n),s=await A(i.instance),a=`/root/.gibil-jobs/${n}.log`,l=r.follow?`tail -f ${a}`:`cat ${a} 2>/dev/null || echo '(no output yet)'`,c=r.follow?3e5:1e4,u=await k({instanceName:i.instance,ip:s.ip,command:l,stream:!r.json,timeoutMs:c});r.json&&o.json({job_id:n,stdout:u.stdout})})}function K(t,e,n){let r=`[${t}] ${e}`;return n&&(r+=`
33
+
34
+ Suggestion: ${n}`),r}function Xe(t){let e=t instanceof Error?t.message:String(t);return/no active server|not found|no servers|does not exist/i.test(e)?"instance_not_found":/timeout|timed out|ETIMEDOUT/i.test(e)?"timeout":/ECONNREFUSED|EHOSTUNREACH|SSH connection|ssh2|handshake/i.test(e)?"ssh_connection":/hetzner|api|token|401|403|409|422|429|quota/i.test(e)?"provider_error":/invalid|must be|validation/i.test(e)?"validation":"provider_error"}var $t="key::";function bi(){try{let t=bt("git config user.name",{encoding:"utf-8"}).trim(),e=bt("git config user.email",{encoding:"utf-8"}).trim();if(!t||!e)return;let n;try{if(bt("git config gpg.format",{encoding:"utf-8"}).trim()==="ssh"){let i=bt("git config user.signingkey",{encoding:"utf-8"}).trim();if(i)try{n=wi(i,"utf-8").trim()}catch{(i.startsWith("ssh-")||i.startsWith($t))&&(n=i.startsWith($t)?i.slice($t.length):i)}}}catch{}return{name:t,email:e,signingKey:n}}catch{return}}async function He(t,e){if(t)return t;if(e)return A(e);let r=(await X()).filter(i=>new Date<new Date(i.expiresAt));if(r.length===0)throw new Error("No active servers. Use create_server first.");if(r.length===1)return r[0];throw new Error(`Multiple servers running: ${r.map(i=>i.name).join(", ")}. Pass the "server" parameter to specify which one.`)}function re(t,e,n=3e4){return k({instanceName:t.name,ip:t.ip,command:e,stream:!1,timeoutMs:n})}function B(t){return`'${t.replace(/'/g,"'\\''")}'`}function $i(t){let e=t.trim(),n=e.split(",");if(n.length<5)throw new Error(`Unexpected vm_stats output (expected 5 comma-separated sections, got ${n.length}): ${e}`);let r=parseInt(n[0],10);if(isNaN(r))throw new Error(`Failed to parse CPU cores: ${n[0]}`);let i=n[1].trim().split(/\s+/),s=parseFloat(i[0]),a=parseFloat(i[1]),l=parseFloat(i[2]);if(isNaN(s)||isNaN(a)||isNaN(l))throw new Error(`Failed to parse load averages: ${n[1]}`);let c=n[2].trim().split(/\s+/),u=parseInt(c[0],10),d=parseInt(c[1],10),f=parseInt(c[2],10);if(isNaN(u)||isNaN(d)||isNaN(f))throw new Error(`Failed to parse memory: ${n[2]}`);let p=n[3].trim().split(/\s+/),g=$=>Math.round(parseFloat($.replace(/G$/i,""))),y=g(p[0]),v=g(p[1]),S=g(p[2]);if(isNaN(y)||isNaN(v)||isNaN(S))throw new Error(`Failed to parse disk: ${n[3]}`);let w=parseInt(n[4].trim(),10);if(isNaN(w))throw new Error(`Failed to parse uptime: ${n[4]}`);return{cpu:{cores:r,load_1m:s,load_5m:a,load_15m:l},memory:{total_mb:u,used_mb:d,available_mb:f},disk:{total_gb:y,used_gb:v,available_gb:S},uptime_seconds:w}}async function pr(t){let e=null;if(t&&(e=await A(t),e.gitIdentity)){let{name:l,email:c,signingKey:u}=e.gitIdentity,d=[`git config --global user.name ${B(l)}`,`git config --global user.email ${B(c)}`];u&&d.push("git config --global gpg.format ssh",`git config --global user.signingkey ${B($t+u)}`,"git config --global commit.gpgsign true"),re(e,d.join(" && ")).catch(()=>{})}let n=t?`gibil-${t}`:"gibil",r=new yi({name:n,version:"0.4.0"}),i=r.tool.bind(r);r.tool=((...l)=>{let c=l[0],u=l.length-1;if(typeof l[u]=="function"){let d=l[u];l[u]=async(...f)=>(nr(c).catch(()=>{}),d(...f))}return i(...l)}),e||(r.tool("create_server","Forge a new ephemeral server with a full Linux environment (Ubuntu 24.04, Node.js 20, pnpm). Clones the repo to /root/project and waits until fully provisioned. Picks the configured default provider unless 'provider' is set (hetzner, vultr). After creation, use vm_bash to run commands, vm_read/vm_write for files, vm_grep to search code. Destroy with destroy_server when done.",{name:x.string().optional().describe("Server name (auto-generated if omitted)"),repo:x.string().optional().describe("Git repo URL to clone on boot"),ttl:x.number().optional().describe("Auto-destroy after N minutes (default: 60, max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)"),provider:x.enum(["hetzner","vultr"]).optional().describe("Cloud provider (default: configured default \u2014 hetzner if unset). vultr gives APAC regions."),size:x.enum(["small","medium","large"]).optional().describe("Gibil size: small (2 vCPU/4 GB), medium (4 vCPU/8 GB), large (8 vCPU/16 GB). Resolved per provider \u2014 see gibil providers."),server_type:x.string().optional().describe("Provider-native SKU (overrides size). Hetzner: cax11/cax21/cax31 (ARM, fsn1/nbg1) or cpx21/cpx31 (x86). Vultr: vc2-2c-4gb/vc2-4c-8gb."),location:x.string().optional().describe("Provider region. Hetzner: fsn1/nbg1/ash. Vultr: nrt/sgp/syd/icn/bom."),env:x.record(x.string(),x.string()).optional().describe("Environment variables to set on the server")},async({name:l,repo:c,ttl:u,provider:d,size:f,server_type:p,location:g,env:y})=>{let v=null,S=null,w=null,$=null;try{w=l??Be(),l&&pt(l);let{getDefaultProvider:b}=await Promise.resolve().then(()=>(G(),Ue)),_=d??await b();$=await F.get(_);let T=p;if(!T&&f){let{resolveSize:en}=await Promise.resolve().then(()=>(Ve(),Je));T=en($,f)}let oe=await lt(w),fe=await $.createSSHKey(`gibil-${w}-${ye(4)}`,oe.publicKey);v=fe;let Xt=bi(),me=c?await Ee(c):null;y&&Object.keys(y).length>0&&(me||(me={}),me.env={...me.env,...y});let Ar=(me?.services?.length??0)>0,$e=u??(Ar?120:60);if($e<1||$e>J)return{content:[{type:"text",text:K("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let jr=he({repo:c,config:me??void 0,ttlMinutes:$e,githubToken:process.env.GITHUB_TOKEN,gitIdentity:Xt}),xt=await $.createServer(w,fe.id,jr,T,g);S=xt.id;let Ke=(await $.waitForReady(xt.id)).ipv4,Qt=new Date,Nr={name:w,serverId:xt.id,ip:Ke,sshKeyId:fe.id,keyPath:P.privateKey(w),status:"running",createdAt:Qt.toISOString(),ttlMinutes:$e,expiresAt:new Date(Qt.getTime()+$e*6e4).toISOString(),repo:c,gitIdentity:Xt,provider:_};await pe(Nr),await ft(w,Ke);let _t="ready";if(c||me){let Rr=Date.now(),tn=!1;for(;Date.now()-Rr<36e4;){try{if((await k({instanceName:w,ip:Ke,command:"test -f /root/.gibil-ready && echo ready || echo waiting",timeoutMs:1e4})).stdout.trim()==="ready"){tn=!0;break}}catch{}await new Promise(Pt=>setTimeout(Pt,5e3))}if(!tn){_t="timeout";try{_t=`timeout \u2014 cloud-init log:
35
+ ${(await k({instanceName:w,ip:Ke,command:"tail -20 /var/log/cloud-init-output.log 2>/dev/null || echo 'no log'",timeoutMs:1e4})).stdout}`}catch{}}}return{content:[{type:"text",text:JSON.stringify({name:w,ip:Ke,ttl_minutes:$e,status:"running",provisioning:_t,working_directory:c?"/root/project":"/root",hint:c?'Server ready. Run commands with vm_bash, e.g.: vm_bash({ command: "pnpm test" })':"Server ready. Clone a repo or run commands with vm_bash."},null,2)}]}}catch(b){$&&S&&await $.destroyServer(S).catch(()=>{}),$&&v&&await $.deleteSSHKey(v.id).catch(()=>{}),w&&(await ne(w).catch(()=>{}),await ge(w).catch(()=>{}));let _=b instanceof Error?b.message:String(b),T=Xe(b);return{content:[{type:"text",text:K(T,`Failed to create server: ${_}`,T==="provider_error"?"Check your HETZNER_API_TOKEN and plan limits":"Verify parameters and try again")}],isError:!0}}}),r.tool("destroy_server","Burn a server. Deletes the Hetzner VM, SSH keys, and local metadata. Always destroy servers when done to avoid costs. Works on expired instances too.",{name:x.string().describe("Name of the server to destroy")},async({name:l})=>{try{let c=await ut(l),u=await F.forInstance(c);await u.destroyServer(c.serverId).catch(()=>{}),await u.deleteSSHKey(c.sshKeyId).catch(()=>{}),await ne(l).catch(()=>{});let{deleteJobsByInstance:d}=await Promise.resolve().then(()=>(we(),Un));return await d(l).catch(()=>{}),await ge(l),{content:[{type:"text",text:`Server "${l}" destroyed.`}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,`Failed to destroy server "${l}": ${u}`,d==="instance_not_found"?"Check server name with list_servers":"Check your HETZNER_API_TOKEN")}],isError:!0}}}),r.tool("list_servers","List all active gibil servers with their names, IPs, and remaining TTL.",{},async()=>{let l=await X();if(l.length===0)return{content:[{type:"text",text:"No servers running. Use create_server to forge one."}]};let c=l.map(u=>{let d=Math.max(0,Math.floor((new Date(u.expiresAt).getTime()-Date.now())/1e3));return{name:u.name,ip:u.ip,status:u.status,ttl_remaining_seconds:d,ttl_warning:d<300?"Less than 5 minutes left \u2014 extend with extend_server or finish up":void 0,repo:u.repo}});return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),r.tool("extend_server","Extend a server's TTL. Resets the auto-destroy timer. Supports long-lived durations up to 1 year.",{name:x.string().describe("Server name"),ttl:x.number().describe("New TTL in minutes from now (max 525600 = 1 year). Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 129600 (3mo), 259200 (6mo), 525600 (1y)")},async({name:l,ttl:c})=>{try{if(c<1||c>J)return{content:[{type:"text",text:K("validation",`TTL must be between 1 and ${J} minutes (1 year)`,`Pass a ttl value between 1 and ${J}. Common values: 1440 (1d), 10080 (7d), 43200 (1mo), 525600 (1y)`)}],isError:!0};let u=Math.floor(c),d=await A(l),f=await re(d,["pkill -f 'sleep.*shutdown' || true",`for j in $(atq 2>/dev/null | awk '{print $1}'); do atrm "$j" 2>/dev/null; done; true`,`echo "shutdown -h now" | at now + ${u} minutes 2>/dev/null || true`,`(sleep ${u*60} && shutdown -h now) &`].join(" && "));return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to extend TTL: ${f.stderr}`,"The remote command failed \u2014 check instance status with list_servers")}],isError:!0}:(d.ttlMinutes=c,d.expiresAt=new Date(Date.now()+c*6e4).toISOString(),await pe(d),{content:[{type:"text",text:`Server "${l}" TTL extended to ${c} minutes.`}]})}catch(u){let d=u instanceof Error?u.message:String(u),f=Xe(u);return{content:[{type:"text",text:K(f,`Failed to extend server "${l}": ${d}`,f==="instance_not_found"?"Check server name with list_servers":"Instance may be unreachable \u2014 wait and retry")}],isError:!0}}}));let s=x.string().optional().describe("Server name (auto-selects if only one is running)");r.tool("vm_bash","Run a shell command on a remote server. Default working directory is /root/project. Use for: installing deps, running tests, git operations, builds. For commands over 2 minutes, set background=true to get a job_id you can poll with vm_job_status.",{command:x.string().describe("Shell command to execute"),working_dir:x.string().optional().describe("Working directory (default: /root/project)"),timeout_ms:x.number().optional().describe("Timeout in ms (default: 120000). Increase for long builds or test suites."),background:x.boolean().optional().describe("Run in background, return job ID for polling"),server:s},async l=>{let c=await He(e,l.server),u=l.working_dir??"/root/project",d=`cd ${B(u)} 2>/dev/null || cd /root && ${l.command}`;if(l.background){let g=mt(),y="/root/.gibil-jobs",v=`${y}/${g}.log`,S=`${y}/${g}.exit`,w=`${y}/${g}.pid`,$=`${y}/${g}.sh`,b=["#!/bin/bash",`nohup bash -c '${d.replace(/'/g,"'\\''")}' > ${v} 2>&1 &`,"BGPID=$!",`echo $BGPID > ${w}`,`(wait $BGPID 2>/dev/null; echo $? > ${S}) &`,"echo $BGPID"].join(`
36
+ `),_=Buffer.from(b).toString("base64"),T=`mkdir -p ${y} && echo '${_}' | base64 -d > ${$} && chmod +x ${$} && bash ${$}`,oe=await re(c,T,1e4),fe=parseInt(oe.stdout.trim(),10);return isNaN(fe)?{content:[{type:"text",text:K("command_failed","Failed to start background job \u2014 could not capture PID","Check that the server is accessible and the command is valid")}],isError:!0}:(await V({id:g,instance:c.name,command:l.command,pid:fe,status:"running",startedAt:new Date().toISOString()}),{content:[{type:"text",text:JSON.stringify({job_id:g,instance:c.name,status:"running",pid:fe,hint:"Poll with vm_job_status({ job_id }) to check completion."},null,2)}]})}let f=await re(c,d,l.timeout_ms??12e4);return{content:[{type:"text",text:[f.stdout,f.stderr].filter(Boolean).join(`
37
+ `)||"(no output)"}],isError:f.exitCode!==0}}),r.tool("vm_job_status","Check the status of a background job started with vm_bash(background=true). Returns status, exit code, and output when done.",{job_id:x.string().describe("Job ID returned by vm_bash with background=true")},async l=>{try{let c=await ue(l.job_id),u=await Yt(l.job_id);return{content:[{type:"text",text:JSON.stringify({job_id:l.job_id,instance:c.instance,command:c.command,status:u.status,exit_code:u.exitCode,started_at:c.startedAt,duration_s:u.durationS,...u.stdout!==void 0?{stdout:u.stdout}:{}},null,2)}],isError:u.status==="failed"||u.status==="orphaned"}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,u,"Check job_id is correct \u2014 use vm_job_list to see all jobs")}],isError:!0}}}),r.tool("vm_job_list","List all background jobs across all servers. Read-only \u2014 does not modify job state. Use vm_sweep_orphans to mark dead jobs.",{},async()=>{let c=(await je()).map(u=>({job_id:u.id,instance:u.instance,command:u.command,status:u.status,started_at:u.startedAt,exit_code:u.exitCode}));return{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}),r.tool("vm_sweep_orphans","Mark running jobs as orphaned if their server no longer exists. Use after destroy_server to clean up lingering job records.",{},async()=>{let l=await je(),c=await X(),u=new Set(c.map(f=>f.name)),d=[];for(let f of l)f.status==="running"&&!u.has(f.instance)&&(f.status="orphaned",f.completedAt=new Date().toISOString(),await V(f),d.push(f.id));return{content:[{type:"text",text:JSON.stringify({swept_count:d.length,swept_job_ids:d},null,2)}]}}),r.tool("vm_read","Read a file from a remote server. Returns the file contents with line numbers.",{path:x.string().describe("Absolute path on the server (e.g. /root/project/src/app.ts)"),offset:x.number().int().min(1).max(999999).optional().describe("Start at line N (1-based)"),limit:x.number().int().min(1).max(999999).optional().describe("Max lines to return"),server:s},async l=>{let c=await He(e,l.server),u=B(l.path),d=`cat -n ${u}`;l.offset&&l.limit?d=`awk 'NR>=${l.offset} && NR<=${l.offset+l.limit-1} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.offset?d=`awk 'NR>=${l.offset} {printf "%6d\\t%s\\n", NR, $0}' ${u}`:l.limit&&(d=`head -n ${l.limit} ${u} | cat -n`);let f=await re(c,d);return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to read ${l.path}: ${f.stderr}`,"Check the file path exists on the server \u2014 use vm_ls to browse")}],isError:!0}:{content:[{type:"text",text:f.stdout}]}}),r.tool("vm_write","Write content to a file on a remote server. Creates parent directories if needed. Overwrites existing files.",{path:x.string().describe("Absolute path on the server"),content:x.string().describe("File content to write"),server:s},async l=>{let c=await He(e,l.server),u=Buffer.from(l.content).toString("base64"),d=B(l.path),f=`mkdir -p "$(dirname ${d})" && echo '${u}' | base64 -d > ${d}`,p=await re(c,f);return p.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to write ${l.path}: ${p.stderr}`,"Check the path is valid and the disk is not full")}],isError:!0}:{content:[{type:"text",text:`Wrote ${l.path}`}]}}),r.tool("vm_ls","List files and directories on a remote server.",{path:x.string().optional().describe("Directory path (default: /root/project)"),glob:x.string().optional().describe("Glob pattern to filter (e.g. '**/*.ts')"),server:s},async l=>{let c=await He(e,l.server),u=l.path??"/root/project",d;l.glob?d=`cd ${B(u)} && find . -path ${B("./"+l.glob)} -type f 2>/dev/null | sort | head -200`:d=`ls -la ${B(u)}`;let f=await re(c,d);return f.exitCode!==0?{content:[{type:"text",text:K("command_failed",`Failed to list ${u}: ${f.stderr}`,"Check the directory path exists on the server")}],isError:!0}:{content:[{type:"text",text:f.stdout}]}}),r.tool("vm_grep","Search for a pattern in files on a remote server. Uses ripgrep if available, falls back to grep.",{pattern:x.string().describe("Regex pattern to search for"),path:x.string().optional().describe("Directory or file to search (default: /root/project)"),include:x.string().optional().describe("File glob to include (e.g. '*.ts')"),server:s},async l=>{let c=await He(e,l.server),u=l.path??"/root/project",d=B(l.pattern),f=B(u),p;if(l.include){let v=B(l.include);p=`cd ${f} && (rg -n --glob ${v} ${d} 2>/dev/null || grep -rn --include=${v} ${d} .) | head -100`}else p=`cd ${f} && (rg -n ${d} 2>/dev/null || grep -rn ${d} .) | head -100`;return{content:[{type:"text",text:(await re(c,p)).stdout||"(no matches)"}]}}),r.tool("vm_stats","Get server resource usage \u2014 CPU cores, load average, memory, disk, and uptime. Returns structured data for monitoring.",{server:s},async l=>{try{let c=await He(e,l.server),d=await re(c,`echo "$(nproc),$(cat /proc/loadavg),$(free -m | awk '/Mem:/{print $2,$3,$7}'),$(df -BG / | awk 'NR==2{print $2,$3,$4}'),$(awk '{print int($1)}' /proc/uptime)"`);if(d.exitCode!==0)return{content:[{type:"text",text:K("command_failed",`Failed to collect stats: ${d.stderr}`,"Check the server is accessible with vm_bash")}],isError:!0};let f=$i(d.stdout);return{content:[{type:"text",text:JSON.stringify(f,null,2)}]}}catch(c){let u=c instanceof Error?c.message:String(c),d=Xe(c);return{content:[{type:"text",text:K(d,`Failed to get stats: ${u}`,"Check the server is accessible with vm_bash")}],isError:!0}}});let a=new vi;await r.connect(a)}E();L();function gr(t){t.command("mcp [name]").description("Start an MCP server (used by Claude Code, Cursor, and other agents)").option("--print-config","Print the MCP JSON config (with resolved binary path) and exit").action(async(e,n)=>{if(n.printConfig){let r={mcpServers:{gibil:rt()}};console.log(JSON.stringify(r,null,2)),console.error(""),console.error("Add or merge the mcpServers entry into one of:"),console.error(" ~/.claude.json (Claude Code, user-level \u2014 usually already has other mcpServers; merge in)"),console.error(" .mcp.json (repo root, shared with your team \u2014 create if it doesn't exist)");return}try{await pr(e)}catch(r){o.error(r instanceof Error?r.message:String(r)),process.exit(1)}})}G();E();L();R();import{createInterface as Ii}from"readline";import{existsSync as Ei,readFileSync as Ti,writeFileSync as Ci}from"fs";import{join as br}from"path";import{homedir as Ai}from"os";at();E();var hr="https://1.1.1.1/cdn-cgi/trace";function Si(){let t=process.env.GIBIL_SKIP_IP_DETECTION?.trim().toLowerCase();return!(!t||t==="0"||t==="false"||t==="no"||t==="off")}async function yr(t){if(Si())return o.debug("Public IP detection skipped (GIBIL_SKIP_IP_DETECTION set)"),null;o.debug(`Detecting public IP via ${hr}`);let e=new AbortController,n=setTimeout(()=>e.abort(),t.timeoutMs);try{let r=await fetch(hr,{signal:e.signal});if(!r.ok)return null;let i=await r.text();return xi(i)}catch{return null}finally{clearTimeout(n)}}function xi(t){for(let e of t.split(`
38
+ `))if(e.startsWith("ip=")){let n=e.slice(3).trim();return n.length>0?n:null}return null}var _i="https://console.vultr.com/user/apiaccess/",Pi="https://whatismyip.com";function vr(t){let e=t.program?.console_url??_i;if(t.hasAccount)return[{heading:"Vultr API Key",body:["Get your personal access token from the API access page."],link:e,gate:!1}];let n=[];return t.program&&n.push({heading:"New to Vultr?",body:[`Sign up with this link to get ${t.program.credit} in free credits \u2014 enough for weeks of Gibil usage on small VMs.`],link:t.program.ref_url,disclosure:t.program.disclosure,gate:!0}),n.push({heading:"Step 1 of 3 \u2014 Open the API page",body:["Sign in to Vultr, then open the API access page below.","You can also reach it via Account \u2192 API in the top-right."],link:e,gate:!0}),n.push({heading:"Step 2 of 3 \u2014 Whitelist your IP",body:ki(t.detectedIp),gate:!0}),n.push({heading:"Step 3 of 3 \u2014 Copy your API key",body:['Scroll to the top of the page. Your "Personal Access Token" is the long string at the top.',"Click the eye icon to reveal it, then copy."],gate:!0}),n}function ki(t){let e=['Vultr blocks API requests until you whitelist an IP. Scroll to "Access Control" and either:'];return t?e.push(` \u2022 Add your current IP \u2014 we detected: ${t}`):e.push(` \u2022 Add your current IP \u2014 find it at ${Pi}`),e.push(" \u2022 Or add 0.0.0.0/0 (any IP \u2014 fine for personal use, less secure)"),e.push('Click "Update Access Control" to save.'),e}function Me(t){let e=Ii({input:process.stdin,output:process.stderr});return new Promise(n=>{e.question(t,r=>{e.close(),n(r.trim())})})}var de=["hetzner","vultr"];async function ji(){let t=[];for(let n of de)await _e(n)&&t.push(n);let e=!!await M();return{providers:t,apiKey:e}}function wr(t){return t==="hetzner"?"Hetzner":"Vultr"}async function Ni(){o.info(""),o.info(h("Which provider would you like to set up?")),o.info(m(" hetzner Cheapest EU/US baseline (default)")),o.info(m(" vultr Strongest APAC coverage (Tokyo, Seoul, Singapore, Sydney, Mumbai)")),o.info("");let t=(await Me(" Provider [hetzner]: ")).toLowerCase();return t==="vultr"?"vultr":(t===""||t==="hetzner"||o.warn(`Unknown provider "${t}". Defaulting to hetzner.`),"hetzner")}async function Wt(t,e){let n=e;if(!n&&(t==="vultr"?n=await Ri():(o.info(""),o.info(h("Hetzner API Token")),o.info(m(" Get one at: https://console.hetzner.cloud \u2192 API Tokens")),o.info(""),n=await Me(" Hetzner API token: ")),!n)){let i=t==="hetzner"?"Hetzner API token":"Vultr API key";o.error(`No ${i.toLowerCase()} provided.`),process.exit(1)}let r=o.spin(`Verifying ${t} token...`);try{await $r(t,n),r.succeed(`${t} token verified`)}catch(i){r.fail(i instanceof Error?i.message:String(i)),process.exit(1)}return await Ge(t,n),n}async function Ri(){o.info(""),o.info(h("Vultr API Key")),o.info("");let t=(await Me(" Do you have a Vultr account? [Y/n]: ")).toLowerCase().trim(),e=t!=="n"&&t!=="no",n=null;e||(n=await yr({timeoutMs:2e3}));let r=st("vultr"),i=vr({hasAccount:e,detectedIp:n,program:r});for(let s of i)await Oi(s);return o.info(""),Me(" Vultr API key: ")}async function Oi(t){o.info(""),t.heading&&o.info(h(t.heading));for(let e of t.body)o.info(` ${e}`);t.link&&o.info(m(` \u2192 ${t.link}`)),t.disclosure&&o.info(m(` ${t.disclosure}`)),t.gate&&await Me(m(" (press Enter when done) "))}async function $r(t,e){if(t==="hetzner"){let r=await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${e}`}})).json();if(r.error)throw new Error(`Invalid Hetzner token: ${r.error.message}`)}else if(t==="vultr"){let n=await fetch("https://api.vultr.com/v2/account",{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let r=await n.text(),i=`Invalid Vultr API key (${n.status}): ${r}`;throw n.status===401||n.status===403?new Error(`${i}
39
+
40
+ Most likely cause: your IP isn't whitelisted under "Access Control".
41
+ Open https://console.vultr.com/user/apiaccess/ \u2192 Access Control \u2192 add your IP.`):new Error(i)}}}function Sr(t){t.command("init").description("Set up gibil \u2014 configure your forge in 60 seconds").option("--force","Reconfigure even if already set up").option("--provider <name>",`Configure a specific provider non-interactively (${de.join(", ")})`).option("--token <value>","API token for --provider; required when --provider is set").option("--set-default","Mark the configured provider as the default after setup").option("--add <name>",`Add a second provider interactively (${de.join(", ")})`).action(async e=>{if(e.provider){de.includes(e.provider)||(o.error(`Unknown provider "${e.provider}". Supported: ${de.join(", ")}.`),process.exit(1));let c=e.provider;e.token||(o.error("--token is required when --provider is set."),process.exit(1));let u=o.spin(`Verifying ${c} token...`);try{await $r(c,e.token)}catch(d){u.fail(d instanceof Error?d.message:String(d)),process.exit(1)}u.succeed(`${c} token verified`),await Ge(c,e.token),e.setDefault&&(await Pe(c),o.info(`${N} ${c} is now the default provider.`)),o.info(`${N} ${c} configured. Try: gibil create --provider ${c}`);return}if(e.add){de.includes(e.add)||(o.error(`Unknown provider "${e.add}". Supported: ${de.join(", ")}.`),process.exit(1));let c=e.add;await Wt(c,e.token),e.setDefault&&(await Pe(c),o.info(`${N} ${c} is now the default provider.`)),o.info(`${N} ${c} added. Try: gibil create --provider ${c}`);return}console.error(on);let n=await ji();if(n.providers.length>0&&!e.force){o.info(`${N} Already configured.`);for(let u of n.providers)o.detail(wr(u),ee("connected"));o.detail("Gibil API",n.apiKey?ee("connected"):m("not configured (optional)")),o.info(""),o.info(` Run ${h("gibil init --force")} to reconfigure.`);let c=de.filter(u=>!n.providers.includes(u));c.length>0&&o.info(` Run ${h(`gibil init --add ${c[0]}`)} to add ${wr(c[0])}.`),o.info(` Run ${h("gibil create")} to forge a server.`);return}let r=await Ni(),i;if(r==="vultr"?(await Wt("vultr"),await Pe("vultr")):(i=await Wt("hetzner"),await Pe("hetzner")),r==="hetzner"&&i){let c=o.spin("Detecting available server types..."),u="cax11",d="fsn1",f=[{type:"cax11",location:"fsn1"},{type:"cpx11",location:"fsn1"}];for(let p of f)try{let y=await(await fetch("https://api.hetzner.cloud/v1/servers",{method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json"},body:JSON.stringify({name:"gibil-probe",server_type:p.type,image:"ubuntu-24.04",location:p.location,start_after_create:!1})})).json();if(y.server){await fetch(`https://api.hetzner.cloud/v1/servers/${y.server.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${i}`}}),u=p.type,d=p.location;break}}catch{}await Nt(u,d),c.succeed(`Default server type: ${u} (${d})`)}let s=o.spin("Configuring MCP for Claude Code...");try{let c=br(Ai(),".claude.json"),u={};try{u=JSON.parse(Ti(c,"utf-8"))}catch{}u.mcpServers||(u.mcpServers={}),u.mcpServers.gibil=rt(),Ci(c,JSON.stringify(u,null,2)+`
42
+ `),s.succeed("MCP configured for Claude Code (~/.claude.json)")}catch{s.fail("Could not auto-configure MCP"),o.info(m(" Run gibil mcp --print-config for manual setup"))}o.info(""),o.info(h("Default coding agent (optional)")),o.info(m(` Install a coding agent on every server. Options: ${Q.join(", ")}`)),o.info(m(" Press Enter to skip \u2014 you can always use --agent later.")),o.info("");let l=(await Me(" Default agent [none]: ")).toLowerCase().trim();l&&Q.includes(l)?(await it(l),o.info(` ${N} Default agent: ${ee(l)}`)):l?o.info(m(` Unknown agent "${l}", skipping. Use --agent with: ${Q.join(", ")}`)):(await it(null),o.info(m(" No default agent. Use --agent claude (or aider, codex) when creating servers."))),o.info(""),o.info(C.initComplete),o.info(""),o.info(m(" Try it now:")),o.info(` ${h('gibil branch feat/my-feature --run "pnpm test"')}`),o.info(` ${h("gibil ssh feat-my-feature")}`),o.info(` ${h("gibil destroy feat-my-feature")}`),o.info(""),o.info(m(" Or with full control:")),o.info(` ${h("gibil create --name demo --repo https://github.com/lukeed/clsx --ttl 10")}`),o.info(` ${h('gibil run demo "npm test"')}`),o.info(` ${h("gibil destroy demo")}`),o.info(""),o.info(m(" Later:")),o.info(` ${h("gibil auth login")} ${m("Add a Gibil API key (optional)")}`),o.info(` ${h("gibil mcp --print-config")} ${m("MCP setup for other editors")}`),o.info("")})}async function xr(){if(process.env.HETZNER_API_TOKEN||process.env.VULTR_API_KEY)return!1;let t=br(P.root,"config.json");return!Ei(t)}ke();import{execSync as Hi}from"child_process";import{existsSync as Y}from"fs";E();R();G();function Mi(){try{let t=Hi("git remote get-url origin",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();if(!t)throw new Error("empty");return t}catch{throw new Error("Not in a git repo, or no remote configured. Use --repo to specify.")}}function Ki(t){if(!t||!t.trim())throw new Error("Branch name cannot be empty.");if(/[;&|$`(){}<>!;"'\s\\]/.test(t))throw new Error(`Invalid branch name "${t}". Contains shell-unsafe characters.`)}function Zt(t){return t.replace(/\//g,"-").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase().slice(0,40)}function Di(){return Y("pnpm-lock.yaml")?"pnpm install":Y("bun.lockb")||Y("bun.lock")?"bun install":Y("yarn.lock")?"yarn install":Y("package-lock.json")?"npm install":Y("Cargo.lock")?"cargo build":Y("go.sum")?"go mod download":Y("uv.lock")?"uv sync":Y("poetry.lock")?"poetry install":Y("requirements.txt")?"pip install -r requirements.txt":Y("Gemfile.lock")?"bundle install":null}async function _r(t,e,n){let r=Zt(e),i=Date.now(),s=o.spin(`Forging "${r}" for branch ${h(e)}...`),a=await yt(t,r,{repo:n.repo,ttlMinutes:n.ttlMinutes,config:n.config,providerName:n.providerName,serverType:n.serverType,location:n.location,agent:n.agent,verbose:n.verbose}),l=o.spin(`Checking out ${h(e)}...`);if((await k({instanceName:r,ip:a.ip,command:"cd /root/project && git rev-parse --abbrev-ref HEAD",timeoutMs:1e4})).stdout.trim()===e)l.succeed(`Already on ${e}`);else{let d=await k({instanceName:r,ip:a.ip,command:`cd /root/project && git fetch origin '${e.replace(/'/g,"'\\''")}' && git checkout '${e.replace(/'/g,"'\\''")}'`,timeoutMs:6e4});d.exitCode!==0?(l.fail(`Failed to checkout ${e}`),d.stderr&&o.info(m(d.stderr.trim()))):l.succeed(`Checked out ${e}`)}if(!(!n.noTasks&&n.config?.tasks&&n.config.tasks.length>0)){let d=Di();if(d){let f=o.spin(`Installing deps (${d})...`),p=await k({instanceName:r,ip:a.ip,command:`cd /root/project && ${d}`,timeoutMs:3e5});p.exitCode!==0?(f.fail("Dep install failed"),p.stderr&&o.info(m(p.stderr.trim().slice(-500)))):f.succeed("Deps installed")}}if(n.run)if(n.port&&n.port.length>0)o.info(`Starting: ${h(n.run)} (background)`),await k({instanceName:r,ip:a.ip,command:`cd /root/project && nohup ${n.run} > /tmp/gibil-run.log 2>&1 &`,timeoutMs:3e4}),await new Promise(d=>setTimeout(d,3e3));else{o.info(""),o.info(`Running: ${h(n.run)}`);let d=await k({instanceName:r,ip:a.ip,command:`cd /root/project && ${n.run}`,stream:!n.json,timeoutMs:3e5});n.json&&o.info(d.stdout),d.exitCode!==0&&o.info(m(`Exit code: ${d.exitCode}`))}if(n.port&&n.port.length>0){let d=await Ln(a,n.port);o.info("");for(let f of d)o.info(` ${h(`http://localhost:${f}`)} \u2192 ${r}:${f}`);o.info(""),o.info(m(" Tunnel running in background. Kill with: lsof -ti :PORT | xargs kill"))}let u=((Date.now()-i)/1e3).toFixed(1);return s.succeed(C.createReady(r,u)),n.json?console.log(JSON.stringify({name:r,branch:e,ip:a.ip,ttl_minutes:n.ttlMinutes,ssh:`gibil ssh ${r}`})):(o.info(""),o.info(et(`${e}`,[`Server: ${r}`,`Branch: ${e}`,`IP: ${a.ip}`,`TTL: ${n.ttlMinutes} minutes`,"",`SSH: gibil ssh ${r}`,`Test: gibil run ${r} "pnpm test"`,`Done: gibil destroy ${r}`]))),a}function Pr(t){t.command("branch <branches...>").description("Spin up a branch on a clean Linux server").option("-r, --repo <git-url>","Git repo URL (auto-detected from cwd)").option("--run <command>","Run a command after checkout").option("--ttl <duration>","Auto-destroy timer (e.g. 30, 2h, 7d, 1mo, 3mo, 1y)","30").option("--json","Output as JSON").option("--no-tasks","Skip .gibil.yml tasks").option("--provider <name>","Cloud provider to use (hetzner, vultr)").option("--size <name>","Gibil size: small (2/4), medium (4/8), large (8/16)").option("--server-type <type>","Provider-native server type (overrides --size)").option("--location <loc>","Provider region (e.g. fsn1, nrt)").option("--agent <name>","Install a coding agent (claude, aider, codex)").option("-p, --port <ports...>","Forward local port(s) to server (e.g. --port 3000)").option("-V, --verbose","Stream cloud-init logs during provisioning").option("--dry-run","Print config summary and cloud-init script without deploying").action(async(e,n)=>{n.json&&I(!0);for(let d of e)Ki(d);let r=Ce(n.ttl),i=n.repo??Mi(),s=null;if(s=await Ee(i)??await Te(process.cwd()),!n.agent){let d=await Fe();d&&(n.agent=d)}if(n.agent){if(!Q.includes(n.agent))throw new Error(`Unknown agent "${n.agent}". Supported: ${Q.join(", ")}`);if(!Ie[n.agent]?.some(f=>s?.env?.[f])){let f=Ie[n.agent]?.join(" or ")??"";o.warn(`${n.agent} needs ${f}. SSH in and export it (recommended) or pass with --env.`)}}if(n.dryRun){for(let d of e){let f=Zt(d),p=n.serverType??s?.server_type??"cax11",g=n.location??s?.location??"nbg1",y=s?.image??"node:20",v=he({repo:i,config:s??void 0,ttlMinutes:r,githubToken:process.env.GITHUB_TOKEN,gitIdentity:void 0,agent:n.agent}),S={name:f,serverType:p,location:g,image:y,ttlMinutes:r,repo:i,agent:n.agent,cloudInitScript:v};n.json?o.json(S):(o.info(""),o.info(h("Dry run \u2014 no server will be created")),o.info(""),o.info(` ${m("Name:")} ${f}`),o.info(` ${m("Branch:")} ${d}`),o.info(` ${m("Server type:")} ${p}`),o.info(` ${m("Location:")} ${g}`),o.info(` ${m("Image:")} ${y}`),o.info(` ${m("TTL:")} ${r} minutes`),o.info(` ${m("Repo:")} ${i}`),n.agent&&o.info(` ${m("Agent:")} ${n.agent}`),o.info(""),o.info("Cloud-init script:"),o.info("\u2500".repeat(17)),o.info(v))}return}let a=await M();if(a){let d=await ce(a);o.info(`Authenticated as ${d.user.email} (${d.user.plan})`)}let l=n.provider??"hetzner",c=await F.get(l),u=n.serverType;if(!u&&n.size){let{isSizeName:d,resolveSize:f}=await Promise.resolve().then(()=>(Ve(),Je));if(!d(n.size))throw new Error(`Unknown size "${n.size}". Valid sizes: small, medium, large.`);u=f(c,n.size)}if(e.length===1){let d=await _r(c,e[0],{repo:i,ttlMinutes:r,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:l,serverType:u,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose});a&&await te(a,"create",d.name).catch(()=>{})}else{o.info(`Forging ${h(String(e.length))} branches in parallel...`),o.info("");let d=await Promise.allSettled(e.map(g=>_r(c,g,{repo:i,ttlMinutes:r,config:s,run:n.run,json:n.json,noTasks:n.noTasks,providerName:l,serverType:u,location:n.location,agent:n.agent,port:n.port,verbose:n.verbose}))),f=d.filter(g=>g.status==="fulfilled"),p=d.filter(g=>g.status==="rejected");if(!n.json){if(o.info(""),o.info(`${f.length}/${e.length} branches ready.`),p.length>0)for(let g=0;g<d.length;g++){let y=d[g];y.status==="rejected"&&o.error(` ${e[g]}: ${y.reason instanceof Error?y.reason.message:String(y.reason)}`)}o.info(""),o.info(m(`Destroy all: gibil destroy ${e.map(Zt).join(" ")}`))}if(a)for(let g of d)g.status==="fulfilled"&&await te(a,"create",g.value.name).catch(()=>{});p.length>0&&process.exit(1)}})}function kr(){let t=process.argv.indexOf("checkout");t>=2&&t===2&&(process.argv[t]="branch")}O();import{spawn as Li}from"child_process";E();R();function Ir(t){if(t.includes(":")){let e=t.split(":");if(e.length===2)return{local:e[0],host:"localhost",remote:e[1]};if(e.length===3)return{local:e[0],host:e[1],remote:e[2]};throw new Error(`Invalid port spec "${t}". Use PORT, LOCAL:REMOTE, or LOCAL:HOST:REMOTE.`)}return{local:t,host:"localhost",remote:t}}function zi(t){let{local:e,host:n,remote:r}=Ir(t);return qe(`${e}:${n}:${r}`),`${e}:${n}:${r}`}function Er(t){t.command("forward <name> <ports...>").description("Forward local ports to a running ephemeral machine via SSH").action(async(e,n)=>{let r=await A(e),i=n.map(c=>({spec:c,mapping:zi(c),...Ir(c)})),s=await Ae(r);s||o.warn(m(` No pinned host key for ${e} (instance pre-dates v0.4.x). Tunnel is insecure on this hop; running any \`gibil run ${e}\` will pin it.`));let a=["-N","-i",r.keyPath,"-o","LogLevel=ERROR","-o","ExitOnForwardFailure=yes"];s?a.push("-o","StrictHostKeyChecking=yes","-o",`UserKnownHostsFile=${s}`):a.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null");for(let{mapping:c}of i)a.push("-L",c);a.push(`root@${r.ip}`),o.info("");for(let{local:c,host:u,remote:d}of i)o.info(` Forwarding ${h(`localhost:${c}`)} \u2192 ${e}:${u}:${d}`);o.info(""),o.info(m(" Tunnel active. Press Ctrl+C to stop.")),o.info(""),Li("ssh",a,{stdio:"inherit"}).on("exit",c=>{let u=()=>process.exit(c??0);c===255?(o.warn(" SSH connection failed \u2014 checking if server still exists..."),q(r).then(u,u)):(o.info(" Tunnel closed."),u())})})}ze();G();E();R();at();async function Gi(){let t=await At(),e=await Promise.all(Object.values(Et).map(async n=>{let r=await _e(n.name);return{name:n.name,label:n.label,defaultRegion:n.defaultRegion,configured:r!==null,sizes:n.sizes}}));return{default:t,providers:e}}function St(t,e){return t.length>=e?t:t+" ".repeat(e-t.length)}function Fi(t){for(let e of t.providers){let n=e.name===t.default,r=e.configured?ee("configured"):m("not configured"),i=n?h(" (default)"):"";if(o.info(""),o.info(`${h(e.label)}${i} ${m("\xB7")} region ${e.defaultRegion} ${m("\xB7")} ${r}`),!e.configured){o.info(m(` Run: gibil init --add ${e.name}`));let s=Mt(e.name,e.configured);s&&o.info(m(` ${s}`))}for(let s of e.sizes){let a=St(s.name,8),l=St(`${s.vcpu} vCPU`,7),c=St(`${s.ramGb} GB`,6),u=St(`${s.diskGb} GB SSD`,11);o.info(` ${a} ${l} ${c} ${u} ${m("\u2192")} ${s.nativeType}`)}}o.info("")}function Tr(t){t.command("providers").description("List supported providers, regions, and sizes").option("--json","Output as JSON").action(async e=>{e.json&&I(!0);let n=await Gi();e.json?o.json(n):Fi(n)})}E();R();try{await import("dotenv/config")}catch{}var Yi=Vi(Ji(import.meta.url)),Cr={version:"0.0.0"};for(let t of["../package.json","../../package.json"])try{Cr=JSON.parse(Bi(qi(Yi,t),"utf-8"));break}catch{}var j=new Ui;j.name("gibil").description("Your own machine, on demand. Forge, use, burn.").version(`${Cr.version} ${Qe}`,"-v, --version").addHelpText("before",`
43
+ ${sn}
31
44
  `).addHelpText("after",`
32
- ${f("Docs:")} https://gibil.dev/docs
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();
45
+ ${m("Docs:")} https://gibil.dev/docs
46
+ `);Sr(j);Mn(j);Bn(j);Jn(j);ir(j);sr(j);cr(j);lr(j);dr(j);fr(j);mr(j);gr(j);Pr(j);Er(j);Tr(j);async function Wi(){kr();let t=process.argv.slice(2);!(t.length===0||t.includes("init")||t.includes("auth")||t.includes("--help")||t.includes("-h")||t.includes("--version")||t.includes("-v")||t.includes("-V")||t.includes("mcp")||t.includes("ssh")||t.includes("run")||t.includes("exec")||t.includes("list")||t.includes("ls")||t.includes("forward")||t.includes("providers"))&&await xr()&&(o.info(""),o.info(C.setupNeeded),o.info(""),process.exit(1)),await tr()&&await qt()&&o.info(m("gibil collects anonymous usage stats to improve the CLI. To disable: GIBIL_TELEMETRY=0"));let r=t[0]??"help",i=rr(process.argv),s=Date.now(),a=0;try{await j.parseAsync(process.argv)}catch(c){a=1,c instanceof Error&&o.error(c.message)}["auth","config","--help","-h","--version","-v","help"].includes(r)||await Ze({event:"command",command:r,flags:i,exit_code:a,duration_ms:Date.now()-s}),a!==0&&process.exit(1)}Wi();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gibil",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Your own machine, on demand. Forge, use, burn.",
5
5
  "homepage": "https://gibil.dev",
6
6
  "type": "module",
@@ -18,6 +18,10 @@
18
18
  "test": "vitest run",
19
19
  "test:watch": "vitest",
20
20
  "lint": "tsc --noEmit",
21
+ "smoke": "scripts/smoke.sh",
22
+ "smoke:hetzner": "scripts/smoke.sh --provider hetzner",
23
+ "smoke:vultr": "scripts/smoke.sh --provider vultr",
24
+ "smoke:all": "scripts/smoke.sh --provider both",
21
25
  "prepublishOnly": "pnpm build"
22
26
  },
23
27
  "keywords": [
@@ -30,6 +34,8 @@
30
34
  "remote-server",
31
35
  "ssh",
32
36
  "hetzner",
37
+ "vultr",
38
+ "multi-cloud",
33
39
  "claude-code",
34
40
  "dev-environment"
35
41
  ],