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