gibil 0.3.2 → 0.4.0

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