@r5n/hydra 0.4.0 → 0.5.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 (2) hide show
  1. package/dist/cli.js +1 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -46,7 +46,7 @@ ${d.default.gray(fn)} ${n}
46
46
  `);let R=0,v=0;b(),s=setInterval(()=>{if(e&&f===a)return;$(),a=f;let Y=d.default.magenta(o[R]);if(e)process.stdout.write(`${Y} ${f}...`);else if(n==="timer")process.stdout.write(`${Y} ${f} ${_(c)}`);else{let D=".".repeat(Math.floor(v)).slice(0,3);process.stdout.write(`${Y} ${f}${D}`)}R=R+1<o.length?R+1:0,v=v<o.length?v+0.125:0},r)},B=(E="",R=0)=>{t=!1,clearInterval(s),$();let v=R===0?d.default.green(rt):R===1?d.default.red(Pt):d.default.red(Mt);f=L(E??f),n==="timer"?process.stdout.write(`${v} ${f} ${_(c)}
47
47
  `):process.stdout.write(`${v} ${f}
48
48
  `),g(),i()};return{start:y,stop:B,message:(E="")=>{f=L(E??f)}}},It=async(n,o)=>{let r={},e=Object.keys(n);for(let i of e){let s=n[i],t=await s({results:r})?.catch((f)=>{throw f});if(typeof o?.onCancel=="function"&&U(t)){r[i]="canceled",o.onCancel({results:r});continue}r[i]=t}return r};var dr=!process.env.NO_COLOR&&(process.env.FORCE_COLOR||process.stdout.isTTY),gr=["blue","cyan","gray","green","magenta","red","white","yellow"],hr=["bold","dim","italic","underline","reset"],zt={bold:"\x1B[1m",dim:"\x1B[2m",italic:"\x1B[3m",reset:"\x1B[0m",underline:"\x1B[4m"},br={"\x1B[30m":"\x1B[90m","\x1B[31m":"\x1B[91m","\x1B[32m":"\x1B[92m","\x1B[33m":"\x1B[93m","\x1B[34m":"\x1B[94m","\x1B[35m":"\x1B[95m","\x1B[36m":"\x1B[96m","\x1B[37m":"\x1B[97m"};function st(n){return(o)=>dr?`${n}${o}${zt.reset}`:o}function Kt({bright:n=!1,color:o}){let r=Bun.color(o,"ansi");if(!r)return"";return n?br[r]||r:r}var yr=gr.reduce((n,o)=>{return n[o]=st(Kt({color:o})),n[`${o}Bright`]=st(Kt({bright:!0,color:o})),n},{}),wr=hr.reduce((n,o)=>{return n[o]=st(zt[o]),n},{}),w={...yr,...wr};var x=({prefix:n="",level:o=process.env.DEBUG?0:1}={})=>{function r(...f){if(o<=0){let a=f.join(" ");console.log(An({level:w.dim("DEBUG"),message:a,prefix:n}))}}function e(...f){if(o<=1){let a=f.join(" ");console.info(An({level:w.cyan("INFO "),message:a,prefix:n}))}}function i(...f){if(o<=2){let a=f.join(" ");console.warn(An({level:w.yellow("WARN "),message:a,prefix:n}))}}function s(...f){if(o<=3){let a=f.join(" ");console.error(An({level:w.red("ERROR"),message:a,prefix:n}))}}function t(f){return x({level:o,prefix:`${n}:${f}`})}return{child:t,debug:r,error:s,info:e,warn:i}},ft=x();function An({prefix:n,level:o,message:r}){let e=new Date().toISOString().split("T")[1]?.split(".")[0]||"",i=n?`[${n}] `:"";return`${w.dim(e||"")} ${o} ${i}${r}`}var p={arrow:w.yellow,bold:w.bold,boolean:w.cyan,bullet:w.yellow,busy:w.yellow,command:w.green,dim:w.dim,error:w.red,flag:w.yellow,info:w.cyan,number:w.magenta,offline:w.red,online:w.green,separator:w.dim,string:w.green,success:w.green,value:w.cyan,warning:w.yellow};var Ai=x({prefix:"cli-core"}).child("error");var j=new Uint32Array(65536),xr=(n,o)=>{let r=n.length,e=o.length,i=1<<r-1,s=-1,t=0,f=r,a=r;while(a--)j[n.charCodeAt(a)]|=1<<a;for(a=0;a<e;a++){let c=j[o.charCodeAt(a)],m=c|t;if(c|=(c&s)+s^s,t|=~(c|s),s&=c,t&i)f++;if(s&i)f--;t=t<<1|1,s=s<<1|~(m|t),t&=m}a=r;while(a--)j[n.charCodeAt(a)]=0;return f},$r=(n,o)=>{let r=o.length,e=n.length,i=[],s=[],t=Math.ceil(r/32),f=Math.ceil(e/32);for(let g=0;g<t;g++)s[g]=-1,i[g]=0;let a=0;for(;a<f-1;a++){let g=0,$=-1,L=a*32,_=Math.min(32,e)+L;for(let y=L;y<_;y++)j[n.charCodeAt(y)]|=1<<y;for(let y=0;y<r;y++){let B=j[o.charCodeAt(y)],E=s[y/32|0]>>>y&1,R=i[y/32|0]>>>y&1,v=B|g,Y=((B|R)&$)+$^$|B|R,D=g|~(Y|$),xn=$&Y;if(D>>>31^E)s[y/32|0]^=1<<y;if(xn>>>31^R)i[y/32|0]^=1<<y;D=D<<1|E,xn=xn<<1|R,$=xn|~(v|D),g=D&v}for(let y=L;y<_;y++)j[n.charCodeAt(y)]=0}let c=0,m=-1,l=a*32,u=Math.min(32,e-l)+l;for(let g=l;g<u;g++)j[n.charCodeAt(g)]|=1<<g;let b=e;for(let g=0;g<r;g++){let $=j[o.charCodeAt(g)],L=s[g/32|0]>>>g&1,_=i[g/32|0]>>>g&1,y=$|c,B=(($|_)&m)+m^m|$|_,E=c|~(B|m),R=m&B;if(b+=E>>>e-1&1,b-=R>>>e-1&1,E>>>31^L)s[g/32|0]^=1<<g;if(R>>>31^_)i[g/32|0]^=1<<g;E=E<<1|L,R=R<<1|_,m=R|~(y|E),c=E&y}for(let g=l;g<u;g++)j[n.charCodeAt(g)]=0;return b},kt=(n,o)=>{if(n.length<o.length){let r=o;o=n,n=r}if(o.length===0)return n.length;if(n.length<=32)return xr(n,o);return $r(n,o)};function Jt(n,o,r=3){return o.map((s)=>({distance:kt(n.toLowerCase(),s.toLowerCase()),option:s})).filter(({distance:s})=>s<=Math.max(3,n.length/2)).sort((s,t)=>s.distance-t.distance).slice(0,r).map(({option:s})=>s)}function Yt(n,o){return o[n.toLowerCase()]||null}var jt=x({prefix:"cli-core"}).child("graceful");async function an(n,o={}){let{fallback:r,logLevel:e="warn",message:i="Operation failed gracefully"}=o;try{return await n()}catch(s){let t=s instanceof Error?s.message:String(s);if(jt[e](`${i}: ${t}`),r)try{await r()}catch(f){jt.error(`Fallback also failed: ${f instanceof Error?f.message:String(f)}`)}return}}var Er={backoffMultiplier:2,initialDelay:1000,maxAttempts:3,maxDelay:30000,onRetry:(n,o)=>{ft.warn(`Retry attempt ${n}: ${o.message}`)},retryableErrors:["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENOTFOUND","rate limit","429","503","502","504","npm ERR!","EPUBLISHCONFLICT"]};async function Z(n,o={}){let r={...Er,...o},e=new Error("Operation failed");for(let i=1;i<=r.maxAttempts;i++)try{return await n()}catch(s){if(e=s instanceof Error?s:new Error(String(s)),!r.retryableErrors.some((a)=>e.message.toLowerCase().includes(a.toLowerCase()))||i===r.maxAttempts)throw e;r.onRetry(i,e);let f=Math.min(r.initialDelay*r.backoffMultiplier**(i-1),r.maxDelay);await new Promise((a)=>setTimeout(a,f))}throw e}var k=(n)=>{return n.map=(o)=>k((r)=>o(n(r))),n.or=(o)=>k((r)=>{try{return n(r)}catch(e){return o(r)}}),n},Xn=()=>"bad banditype"();var Zt=(n)=>k((o)=>n.includes(o)?o:Xn()),_n=(n)=>k((o)=>typeof o===typeof n?o:Xn()),gn=()=>_n(""),Qt=()=>_n(0),Vt=()=>_n(!0);var Ft=()=>_n();var Dt=(n)=>k((o)=>o instanceof n?o:Xn()),at=(n)=>Dt(Object).map((o)=>{let r={};for(let e in o){let i=n(o[e]);i!==void 0&&(r[e]=i)}return r}),cn=(n)=>Dt(Object).map((o)=>{let r={};for(let e in n){let i=n[e](o[e]);i!==void 0&&(r[e]=i)}return r});function Or(n,o){return k((r)=>{try{return n(r)}catch{try{return o(r)}catch{throw new Error("No matching schema in union")}}})}function nn(n,o){return(r)=>{if(r===void 0)return o;return n(r)}}function no(){return k((n)=>{let o=Qt()(n);if(!Number.isInteger(o)||o<=0)throw new Error("Must be a positive integer");return o})}function to(){return k((n)=>{let o=gn()(n);try{return new URL(o),o}catch{throw new Error("Must be a valid URL")}})}function oo(n,o){return k((r)=>{let e=gn()(r);if(!n.test(e))throw new Error(o||`Must match pattern ${n}`);return e})}function hn(n){return{parse(o){try{return n(o)}catch(r){throw new Error(r instanceof Error?r.message:"Validation failed")}},safeParse(o){try{return{data:n(o),success:!0}}catch(r){return{error:r instanceof Error?r:new Error("Validation failed"),success:!1}}},validator:n}}function tn(){return Or(Ft(),gn())}function on(n){return nn(gn(),n)}function mn(n){return nn(Vt(),n)}function Un(n,o){return nn(Zt(n),o)}import{existsSync as V}from"fs";import{copyFile as Ar,mkdir as ln,readdir as mt}from"fs/promises";import{join as T,resolve as Xr}from"path";import{parseArgs as _r}from"util";var{$:pn}=globalThis.Bun;import{join as so}from"path";var{$:io}=globalThis.Bun;import{tmpdir as Rr}from"os";var ro={MACHINES_DIR:"machines",TEMP_DIR:Rr()};var Sr=cn({directory:on("./gh-runners"),labels:tn(),name:on("runner"),numberOfMachines:nn(no(),1),os:Un(["osx","linux","windows"],"osx"),overwrite:mn(!1),run:mn(!1),runnerGroup:tn(),url:tn()}),Tr=hn(Sr),eo=hn(cn({defaultProfile:tn(),profiles:at(Tr.validator)}));var Lr=["hydra.json",".hydra.json","hydra.toml",".hydra.toml"];async function Nr(n=process.cwd()){for(let o of Lr){let r=so(n,o);if(await Bun.file(r).exists())return r}return null}async function A(n){let o=n?.configPath||await Nr();if(!o)return null;let r=Bun.file(o);if(!await r.exists())return null;try{let e=await r.text(),i=o.endsWith(".toml")?await Gr(e):JSON.parse(e);return eo.parse(i)}catch(e){if(e instanceof Error)throw new Error(`Invalid config file: ${e.message}`);throw e}}async function X(n,o){if(!n)return null;let r=o||n.defaultProfile;if(!r)return null;return n.profiles[r]||null}async function Gr(n){let o=so(ro.TEMP_DIR,`hydra-config-${Date.now()}.toml`);await Bun.write(o,n);try{let r=await import(o);return await io`rm -f ${o}`.quiet(),r.default}catch(r){throw await io`rm -f ${o}`.quiet(),new Error(`Failed to parse TOML configuration: ${r}`)}}var Ur=x({prefix:"hydra"}).child("backup");async function qr(){let{values:n}=_r({allowPositionals:!0,args:Bun.argv,options:{compress:{default:!0,type:"boolean"},config:{short:"c",type:"string"},directory:{default:`${process.cwd()}/gh-runners`,short:"d",type:"string"},list:{default:!1,short:"l",type:"boolean"},output:{short:"o",type:"string"},profile:{short:"p",type:"string"},restore:{short:"r",type:"string"}},strict:!0}),o=await A({configPath:n.config}),r=await X(o,n.profile);return{compress:n.compress,directory:n.directory||r?.directory||`${process.cwd()}/gh-runners`,list:n.list,output:n.output,restore:n.restore}}async function fo(){try{let n=await qr();if(n.list){await vr(n.output||T(n.directory,"backups"));return}if(n.restore){await Hr(n);return}await Cr(n)}catch(n){Ur.error("Backup operation failed:",n),process.exit(1)}}async function Cr(n){let o=Xr(n.directory),r=T(o,"machines");if(!V(r)){h.error(`No runners found in ${n.directory}`);return}let e=W();e.start("Creating backup...");try{let i=await mt(r);if(i.length===0){e.stop("No runners to backup");return}let s=`backup-${new Date().toISOString().replace(/[:.]/g,"-")}`,t=n.output||T(o,"backups"),f=T(t,s);await ln(f,{recursive:!0});let a=0,c=[];for(let l of i){let u=T(r,l),b=T(f,"runners",l);await ln(b,{recursive:!0});let g=[".runner",".credentials",".credentials_rsaparams",".path",".env","config.sh"];for(let $ of g){let L=T(u,$);if(V(L)){let _=T(b,$);await Ar(L,_);let y=await Bun.file(L).stat();a+=y.size,c.push(`runners/${l}/${$}`)}}}let m={configFiles:c,date:new Date,id:s,runnerCount:i.length,size:a};if(await Bun.write(T(f,"metadata.json"),JSON.stringify(m,null,2)),n.compress){e.message("Compressing backup..."),await pn`cd ${t} && tar -czf ${s}.tar.gz ${s}`.quiet(),await pn`rm -rf ${f}`.quiet();let l=await Bun.file(T(t,`${s}.tar.gz`)).stat();e.stop(`Backup created: ${s}.tar.gz (${ct(l.size)})`)}else e.stop(`Backup created: ${s} (${ct(a)})`);h.success(`Backed up ${i.length} runner(s)`)}catch(i){throw e.stop("Backup failed"),i}}async function vr(n){if(!V(n)){h.info("No backups found");return}let o=W();o.start("Scanning backups...");try{let r=await mt(n),e=[];for(let i of r){let s=T(n,i);if(i.endsWith(".tar.gz")){let t=i.replace(".tar.gz",""),f=await Bun.file(s).stat();e.push({configFiles:[],date:new Date(f.mtime),id:t,runnerCount:0,size:f.size})}else{let t=T(s,"metadata.json");if(V(t)){let f=await Bun.file(t).json();e.push(f)}}}if(o.stop("Found backups"),e.length===0){h.info("No backups found");return}console.log(),console.log(p.bold("\uD83D\uDCE6 Available Backups")),console.log("\u2500".repeat(80));for(let i of e.sort((s,t)=>new Date(t.date).getTime()-new Date(s.date).getTime())){let s=new Date(i.date).toLocaleString(),t=ct(i.size),f=i.runnerCount>0?`${i.runnerCount} runners`:"compressed";console.log(`${p.value(i.id)}`),console.log(` Date: ${s}`),console.log(` Size: ${t}`),console.log(` Content: ${f}`),console.log()}}catch(r){throw o.stop("Failed to list backups"),r}}async function Hr(n){let o=n.output||T(n.directory,"backups"),r=T(o,n.restore),e=`${r}.tar.gz`,i=V(e),s=V(r);if(!i&&!s){h.error(`Backup not found: ${n.restore}`);return}if(!await C({initialValue:!1,message:"This will overwrite existing runner configurations. Continue?"})){h.info("Restore cancelled");return}let f=W();f.start("Restoring backup...");try{let a=r;if(i){f.message("Extracting backup...");let g=T(o,`temp-${Date.now()}`);await ln(g,{recursive:!0}),await pn`cd ${g} && tar -xzf ${e}`.quiet(),a=T(g,n.restore)}let c=T(a,"metadata.json");if(!V(c))throw new Error("Invalid backup: metadata.json not found");await Bun.file(c).json();let m=T(a,"runners"),l=T(n.directory,"machines");await ln(l,{recursive:!0});let u=await mt(m),b=0;for(let g of u){let $=T(m,g),L=T(l,g);if(V(L)){let _=await Gn({message:`Runner ${g} already exists. What should we do?`,options:[{label:"Skip",value:"skip"},{label:"Overwrite",value:"overwrite"},{label:"Rename",value:"rename"}]});if(_==="skip")continue;else if(_==="rename"){let y=await q({message:"Enter new runner name:",placeholder:`${g}-restored`});if(!y)continue;let B=T(l,y);await ln(B,{recursive:!0}),await pn`cp -r ${$}/* ${B}/`.quiet(),b++;continue}}await ln(L,{recursive:!0}),await pn`cp -r ${$}/* ${L}/`.quiet(),b++}if(i){let g=a.replace(`/${n.restore}`,"");await pn`rm -rf ${g}`.quiet()}f.stop("Restore completed"),h.success(`Restored ${b} runner(s) from backup ${n.restore}`)}catch(a){throw f.stop("Restore failed"),a}}function ct(n){if(n<1024)return`${n}B`;if(n<1048576)return`${(n/1024).toFixed(1)}KB`;if(n<1073741824)return`${(n/1048576).toFixed(1)}MB`;return`${(n/1073741824).toFixed(2)}GB`}import{parseArgs as Kr}from"util";var{$:I}=globalThis.Bun;function bn(){let n=import.meta.path,o=n.substring(0,n.lastIndexOf("/"));return o.substring(0,o.lastIndexOf("/"))}var Pr=cn({directory:on(`${process.cwd()}/gh-runners`),labels:tn(),name:on("runner"),numberOfMachines:nn(oo(/^\d+$/,"Number of machines must be a positive integer"),"1"),os:Un(["osx","linux","windows"],"osx"),overwrite:mn(!1),run:mn(!1),token:on(process.env.GITHUB_PERSONAL_ACCESS_TOKEN??""),url:to()}),Os=hn(Pr);var rn=x({prefix:"WorkerManager"}),P=new Set,ao=!1;function Mr(){if(ao)return;ao=!0;let n=()=>{if(P.size>0)rn.info("Cleaning up workers...");co()};process.on("exit",n),process.on("SIGINT",n),process.on("SIGTERM",n),process.on("uncaughtException",(o)=>{rn.error("Uncaught exception:",o),n(),process.exit(1)})}function Br(n){P.add(n),rn.debug(`Worker added. Total workers: ${P.size}`),n.addEventListener("exit",()=>{P.delete(n),rn.debug(`Worker exited. Remaining workers: ${P.size}`)})}function Wr(n){if(P.has(n))n.terminate(),P.delete(n),rn.debug(`Worker removed. Remaining workers: ${P.size}`)}function co(){if(P.size>0){rn.info(`Terminating ${P.size} workers...`);for(let n of P)try{n.terminate()}catch(o){rn.error("Failed to terminate worker:",o)}P.clear()}}function Ir(){return P.size}Mr();var qn={add:Br,remove:Wr,get size(){return Ir()},terminateAll:co};var M=x({prefix:"createMachines"});async function Cn(){let n=await kr(),{overwrite:o,directory:r,numberOfMachines:e,os:i,run:s,name:t,labels:f,runnerGroup:a}=n,{token:c,url:m}=n;if(!m)throw new Error("URL (-u) parameter is required");if(!c){let b=zr(m),g=await C({message:`No token provided. Would you like to open GitHub to get a runner token?
49
- ${b}`});if(U(g)||!g)throw new Error("Token is required to create runners");M.info("Opening GitHub runner settings..."),await I`open ${b}`.quiet();let $=await q({message:"Enter the runner registration token:",placeholder:"AXXXXXXXXXXXXXXXXXXXXXXXXX",validate:(L)=>{if(!L||L.trim().length===0)return"Token is required";return}});if(U($))throw new Error("Token is required to create runners");c=$.trim()}if(M.info(`Creating ${e} runners in ${r}`),M.debug(`OS: ${i}, Name: ${t}, Labels: ${f||"none"}`),await Bun.file(r).exists())if(o)M.warn(`Directory ${r} exists, removing...`),await I`rm -rf ${r}`;else throw new Error(`Directory ${r} already exists`);let u=await Jr({directory:r,os:i});if(await Yr({directory:r,numberOfMachines:e,overwrite:o,runnerBase:u}),!s){M.info("Runners created but not started");return}jr({directory:r,labels:f,name:t,numberOfMachines:e,runnerGroup:a,token:c,url:m})}function zr(n){try{let o=new URL(n),r=o.pathname.split("/").filter(Boolean);if(o.hostname==="github.com"||o.hostname==="www.github.com"){if(r[0]==="orgs"&&r[1])return`https://github.com/organizations/${r[1]}/settings/actions/runners/new`;else if(r.length>=2)return`https://github.com/${r[0]}/${r[1]}/settings/actions/runners/new`}else if(r[0]==="orgs"&&r[1])return`${o.origin}/organizations/${r[1]}/settings/actions/runners/new`;else if(r.length>=2)return`${o.origin}/${r[0]}/${r[1]}/settings/actions/runners/new`;return n}catch{return n}}async function kr(){let{values:n}=Kr({allowPositionals:!0,args:Bun.argv,options:{config:{short:"c",type:"string"},directory:{default:`${process.cwd()}/gh-runners`,short:"d",type:"string"},labels:{short:"l",type:"string"},name:{default:"runner",short:"n",type:"string"},numberOfMachines:{default:"1",short:"m",type:"string"},os:{default:"osx",short:"s",type:"string"},overwrite:{default:!1,short:"f",type:"boolean"},profile:{short:"p",type:"string"},run:{default:!1,short:"r",type:"boolean"},runnerGroup:{short:"g",type:"string"},token:{short:"t",type:"string"},url:{short:"u",type:"string"}},strict:!0}),o=await A({configPath:n.config}),r=await X(o,n.profile),e={directory:n.directory,labels:n.labels,name:n.name,numberOfMachines:n.numberOfMachines,os:n.os,overwrite:n.overwrite,run:n.run,runnerGroup:n.runnerGroup,token:n.token,url:n.url};if(r)return{directory:e.directory??r.directory??`${process.cwd()}/gh-runners`,labels:e.labels??r.labels,name:e.name??r.name??"runner",numberOfMachines:e.numberOfMachines??String(r.numberOfMachines)??"1",os:e.os??r.os??"osx",overwrite:e.overwrite??r.overwrite??!1,run:e.run??r.run??!1,runnerGroup:e.runnerGroup??r.runnerGroup,token:e.token,url:e.url??r.url};return{directory:e.directory,labels:e.labels,name:e.name,numberOfMachines:e.numberOfMachines,os:e.os,overwrite:e.overwrite,run:e.run,runnerGroup:e.runnerGroup,token:e.token,url:e.url}}async function Jr({directory:n,os:o}){let r=M.child("download");r.info(`Fetching latest runner version for ${o}...`);let e=await Z(async()=>(await I`curl -Ls -o /dev/null -w "%{url_effective}" https://github.com/actions/runner/releases/latest`).text(),{onRetry:(c,m)=>{r.warn(`Retrying GitHub API call (attempt ${c}): ${m.message}`)}}),s=(await I`basename ${e}`).text().trim().replace("v",""),t=`actions-runner-${o}-x64-${s}.tar.gz`,f=`https://github.com/actions/runner/releases/download/v${s}/${t}`;if(r.debug(`Runner version: ${s}`),r.debug(`Download URL: ${f}`),await I`mkdir -p ${n}`,await I`mkdir -p ${n}/machines`,!await Bun.file(`${n}/${t}`).exists())r.info("Downloading runner image..."),await Z(async()=>await I`curl -o ${n}/${t} -L ${f}`,{maxAttempts:5,onRetry:(c,m)=>{r.warn(`Retrying runner download (attempt ${c}): ${m.message}`)}}),r.info("Download complete");else r.info("Runner image already exists, skipping download");return t}async function Yr({overwrite:n,numberOfMachines:o,directory:r,runnerBase:e}){let i=M.child("extract"),s=Number(o),t=(()=>{try{let m=Bun.spawnSync(["nproc"]);return Number.parseInt(m.stdout.toString().trim(),10)||4}catch{try{let m=Bun.spawnSync(["sysctl","-n","hw.ncpu"]);return Number.parseInt(m.stdout.toString().trim(),10)||4}catch{return 4}}})(),f=Math.min(Math.max(2,Math.floor(t/2)),s,8);i.info(`Extracting ${s} runners with concurrency of ${f}`);let a=Array.from({length:s},(m,l)=>l+1).map((m)=>({execute:async()=>{let l=`${r}/machines/${m}`;if(await Bun.file(l).exists())if(n)i.debug(`Machine ${m} already exists, removing...`),await I`rm -rf ${l}`;else{i.debug(`Machine ${m} already exists, skipping...`);return}await I`mkdir -p ${l}`,await I`tar xzf ${e} -C ${l.replace(`${r}/`,"")}`.cwd(r);let b=`${bn()}/src/runner`;await I`cp -R ${b}/hooks ${l}`,await I`cp -R ${b}/.runner-env ${l}/.runner-env`,i.debug(`Machine ${m} extracted successfully`)},runnerIndex:m})),c=[];for(let m=0;m<a.length;m+=f){let l=a.slice(m,m+f),u=await Promise.allSettled(l.map((b)=>b.execute()));for(let[b,g]of u.entries())if(g.status==="rejected")throw i.error(`Failed to extract runner ${l[b]?.runnerIndex}: ${g.reason}`),g.reason;c.push(...u)}i.info(`Successfully extracted ${s} runners`)}function jr({directory:n,numberOfMachines:o,token:r,url:e,name:i,labels:s,runnerGroup:t}){M.info(`Starting ${o} runners...`);for(let f=1;f<=Number(o);f++){let a=`${bn()}/src/runner-worker.ts`,c=`${i}-${f}`,m=new Worker(a,{name:c,ref:!0});m.addEventListener("open",()=>{M.info(`Worker ${c} started`),m.postMessage({index:f,labels:s,machineCwd:`${n}/machines/${f}`,name:i,runnerGroup:t,token:r,url:e})}),m.addEventListener("message",({data:l})=>{let u=l.type==="error"?"error":"info";M[u](`${c}: ${l.message}`)}),m.addEventListener("error",(l)=>{M.error(`Worker ${c} error:`,l)}),m.addEventListener("exit",(l)=>{M.info(`Worker ${c} exited with code ${l}`)}),qn.add(m)}M.info("All workers started. Press Ctrl+C to stop all machines")}import{existsSync as yn}from"fs";import{readdir as mo}from"fs/promises";import{join as F,resolve as Zr}from"path";import{parseArgs as Qr}from"util";var{$:lt}=globalThis.Bun;var Vr=x({prefix:"hydra"}).child("health");async function Fr(){let{values:n}=Qr({allowPositionals:!0,args:Bun.argv,options:{config:{short:"c",type:"string"},directory:{default:`${process.cwd()}/gh-runners`,short:"d",type:"string"},fix:{default:!1,short:"f",type:"boolean"},json:{default:!1,short:"j",type:"boolean"},profile:{short:"p",type:"string"},runner:{short:"r",type:"string"},token:{short:"t",type:"string"},url:{short:"u",type:"string"}},strict:!0}),o=await A({configPath:n.config}),r=await X(o,n.profile);return{directory:n.directory||r?.directory||`${process.cwd()}/gh-runners`,fix:n.fix,json:n.json,runner:n.runner,token:n.token,url:n.url||r?.url}}async function lo(){try{let n=await Fr(),o=Zr(n.directory),r=F(o,"machines");if(!yn(r)){h.error(`No runners found in ${n.directory}`);return}let e=W();e.start("Performing health checks...");let i=await mo(r),s=[];for(let t of i){if(n.runner&&t!==n.runner)continue;let f=F(r,t),a=await Dr(t,f,n);s.push(a)}if(e.stop("Health checks completed"),n.json){console.log(JSON.stringify(s,null,2));return}if(re(s),n.fix)await ee(s,n)}catch(n){Vr.error("Failed to perform health checks:",n),process.exit(1)}}async function Dr(n,o,r){let e=[],i={configuration:!1,connectivity:!1,disk:{percentage:0,total:0,used:0},lastActivity:null,process:!1};try{let t=F(o,".runner");if(yn(t)){let f=await Bun.file(t).text(),a=await ne(f.trim());if(i.process=a,!a)e.push("Runner process is not running")}else e.push("No PID file found")}catch{e.push("Could not check process status")}try{let t=await te(o);if(i.disk=t,t.percentage>90)e.push(`Low disk space: ${t.percentage.toFixed(1)}% used`)}catch{e.push("Could not check disk space")}try{let t=F(o,"_diag");if(yn(t)){let f=await mo(t),a=null;for(let c of f)if(c.endsWith(".log")){let m=await Bun.file(F(t,c)).stat();if(!a||m.mtime>a)a=new Date(m.mtime)}if(i.lastActivity=a,a){let c=(Date.now()-a.getTime())/3600000;if(c>24)e.push(`No activity for ${Math.floor(c)} hours`)}}}catch{e.push("Could not check last activity")}try{let t=F(o,".runner"),f=F(o,".credentials");if(i.configuration=yn(t)&&yn(f),!i.configuration)e.push("Missing configuration files")}catch{e.push("Could not check configuration")}if(r.token&&r.url)try{let t=await oe(r.url,r.token);if(i.connectivity=t,!t)e.push("Cannot connect to GitHub API")}catch{e.push("Failed to check GitHub connectivity")}let s="healthy";if(e.length===0)s="healthy";else if(e.some((t)=>t.includes("not running")||t.includes("Missing configuration")||t.includes("Cannot connect")))s="critical";else if(e.length>0)s="warning";return{checks:i,issues:e,runner:n,status:s}}async function ne(n){try{return await lt`kill -0 ${n}`.quiet(),!0}catch{return!1}}async function te(n){let r=(await lt`df -k ${n} | tail -1`.text()).trim().split(/\s+/),e=parseInt(r[1]||"0",10)*1024,i=parseInt(r[2]||"0",10)*1024;return{percentage:parseInt(r[4]?.replace("%","")||"0",10),total:e,used:i}}async function oe(n,o){return Z(async()=>{let r=n.match(/github\.com[:/]([^/]+)(?:\/([^/.]+))?/);if(!r)return!1;let[,e,i]=r,s=i?`https://api.github.com/repos/${e}/${i}`:`https://api.github.com/orgs/${e}`;return(await fetch(s,{headers:{Accept:"application/vnd.github.v3+json",Authorization:`Bearer ${o}`}})).ok},{maxAttempts:3})}function re(n){console.log(),console.log(p.bold("\uD83C\uDFE5 Runner Health Status")),console.log("\u2500".repeat(80));let o={critical:"\u274C",healthy:"\u2705",unknown:"\u2753",warning:"\u26A0\uFE0F"},r={critical:p.error,healthy:p.success,unknown:p.dim,warning:p.warning};for(let i of n){let s=o[i.status],t=r[i.status];if(console.log(`${s} ${p.bold(`Runner ${i.runner}`)}: ${t(i.status.toUpperCase())}`),console.log(` Process: ${i.checks.process?"\u2705 Running":"\u274C Not running"}`),console.log(` Config: ${i.checks.configuration?"\u2705 Valid":"\u274C Missing"}`),i.checks.disk.total>0){let f=i.checks.disk.percentage>90?p.error:p.success;console.log(` Disk: ${f(`${i.checks.disk.percentage.toFixed(1)}% used`)}`)}if(i.checks.lastActivity){let a=(Date.now()-i.checks.lastActivity.getTime())/3600000>24?p.warning:p.success;console.log(` Last Activity: ${a(ie(i.checks.lastActivity))}`)}if(i.checks.connectivity!==null)console.log(` GitHub API: ${i.checks.connectivity?"\u2705 Connected":"\u274C Disconnected"}`);if(i.issues.length>0){console.log(" Issues:");for(let f of i.issues)console.log(` - ${p.dim(f)}`)}console.log()}let e={critical:n.filter((i)=>i.status==="critical").length,healthy:n.filter((i)=>i.status==="healthy").length,total:n.length,warning:n.filter((i)=>i.status==="warning").length};console.log("\u2500".repeat(80)),console.log(`Summary: Total: ${e.total} | ${p.success(`Healthy: ${e.healthy}`)} | ${p.warning(`Warning: ${e.warning}`)} | ${p.error(`Critical: ${e.critical}`)}`)}async function ee(n,o){let r=n.filter((i)=>i.status==="critical");if(r.length===0){h.info("No critical issues found that require fixing");return}if(console.log(),!await C({initialValue:!1,message:`Found ${r.length} runners with critical issues. Attempt to fix?`}))return;for(let i of r){if(console.log(),h.info(`Attempting to fix runner ${i.runner}...`),!i.checks.process&&i.checks.configuration){h.info("Attempting to restart runner...");try{let s=F(o.directory,"machines",i.runner);await lt`cd ${s} && ./run.sh`.quiet(),h.success(`Runner ${i.runner} restarted successfully`)}catch(s){h.error(`Failed to restart runner ${i.runner}`)}}if(!i.checks.configuration)h.warning(`Runner ${i.runner} needs to be reconfigured. Run 'hydra create' to set up again.`)}}function ie(n){let r=new Date().getTime()-n.getTime(),e=Math.floor(r/3600000);if(e<1)return`${Math.floor(r/60000)}m ago`;if(e<24)return`${e}h ago`;return`${Math.floor(e/24)}d ago`}var po="0.4.0";var uo={backup:`
49
+ ${b}`});if(U(g)||!g)throw new Error("Token is required to create runners");M.info("Opening GitHub runner settings..."),await I`open ${b}`.quiet();let $=await q({message:"Enter the runner registration token:",placeholder:"AXXXXXXXXXXXXXXXXXXXXXXXXX",validate:(L)=>{if(!L||L.trim().length===0)return"Token is required";return}});if(U($))throw new Error("Token is required to create runners");c=$.trim()}if(M.info(`Creating ${e} runners in ${r}`),M.debug(`OS: ${i}, Name: ${t}, Labels: ${f||"none"}`),await Bun.file(r).exists())if(o)M.warn(`Directory ${r} exists, removing...`),await I`rm -rf ${r}`;else throw new Error(`Directory ${r} already exists`);let u=await Jr({directory:r,os:i});if(await Yr({directory:r,numberOfMachines:e,overwrite:o,runnerBase:u}),!s){M.info("Runners created but not started");return}jr({directory:r,labels:f,name:t,numberOfMachines:e,runnerGroup:a,token:c,url:m})}function zr(n){try{let o=new URL(n),r=o.pathname.split("/").filter(Boolean);if(o.hostname==="github.com"||o.hostname==="www.github.com"){if(r[0]==="orgs"&&r[1])return`https://github.com/organizations/${r[1]}/settings/actions/runners/new`;else if(r.length>=2)return`https://github.com/${r[0]}/${r[1]}/settings/actions/runners/new`}else if(r[0]==="orgs"&&r[1])return`${o.origin}/organizations/${r[1]}/settings/actions/runners/new`;else if(r.length>=2)return`${o.origin}/${r[0]}/${r[1]}/settings/actions/runners/new`;return n}catch{return n}}async function kr(){let{values:n}=Kr({allowPositionals:!0,args:Bun.argv,options:{config:{short:"c",type:"string"},directory:{default:`${process.cwd()}/gh-runners`,short:"d",type:"string"},labels:{short:"l",type:"string"},name:{default:"runner",short:"n",type:"string"},numberOfMachines:{default:"1",short:"m",type:"string"},os:{default:"osx",short:"s",type:"string"},overwrite:{default:!1,short:"f",type:"boolean"},profile:{short:"p",type:"string"},run:{default:!1,short:"r",type:"boolean"},runnerGroup:{short:"g",type:"string"},token:{short:"t",type:"string"},url:{short:"u",type:"string"}},strict:!0}),o=await A({configPath:n.config}),r=await X(o,n.profile),e={directory:n.directory,labels:n.labels,name:n.name,numberOfMachines:n.numberOfMachines,os:n.os,overwrite:n.overwrite,run:n.run,runnerGroup:n.runnerGroup,token:n.token,url:n.url};if(r)return{directory:e.directory??r.directory??`${process.cwd()}/gh-runners`,labels:e.labels??r.labels,name:e.name??r.name??"runner",numberOfMachines:e.numberOfMachines??String(r.numberOfMachines)??"1",os:e.os??r.os??"osx",overwrite:e.overwrite??r.overwrite??!1,run:e.run??r.run??!1,runnerGroup:e.runnerGroup??r.runnerGroup,token:e.token,url:e.url??r.url};return{directory:e.directory,labels:e.labels,name:e.name,numberOfMachines:e.numberOfMachines,os:e.os,overwrite:e.overwrite,run:e.run,runnerGroup:e.runnerGroup,token:e.token,url:e.url}}async function Jr({directory:n,os:o}){let r=M.child("download");r.info(`Fetching latest runner version for ${o}...`);let e=await Z(async()=>(await I`curl -Ls -o /dev/null -w "%{url_effective}" https://github.com/actions/runner/releases/latest`).text(),{onRetry:(c,m)=>{r.warn(`Retrying GitHub API call (attempt ${c}): ${m.message}`)}}),s=(await I`basename ${e}`).text().trim().replace("v",""),t=`actions-runner-${o}-x64-${s}.tar.gz`,f=`https://github.com/actions/runner/releases/download/v${s}/${t}`;if(r.debug(`Runner version: ${s}`),r.debug(`Download URL: ${f}`),await I`mkdir -p ${n}`,await I`mkdir -p ${n}/machines`,!await Bun.file(`${n}/${t}`).exists())r.info("Downloading runner image..."),await Z(async()=>await I`curl -o ${n}/${t} -L ${f}`,{maxAttempts:5,onRetry:(c,m)=>{r.warn(`Retrying runner download (attempt ${c}): ${m.message}`)}}),r.info("Download complete");else r.info("Runner image already exists, skipping download");return t}async function Yr({overwrite:n,numberOfMachines:o,directory:r,runnerBase:e}){let i=M.child("extract"),s=Number(o),t=(()=>{try{let m=Bun.spawnSync(["nproc"]);return Number.parseInt(m.stdout.toString().trim(),10)||4}catch{try{let m=Bun.spawnSync(["sysctl","-n","hw.ncpu"]);return Number.parseInt(m.stdout.toString().trim(),10)||4}catch{return 4}}})(),f=Math.min(Math.max(2,Math.floor(t/2)),s,8);i.info(`Extracting ${s} runners with concurrency of ${f}`);let a=Array.from({length:s},(m,l)=>l+1).map((m)=>({execute:async()=>{let l=`${r}/machines/${m}`;if(await Bun.file(l).exists())if(n)i.debug(`Machine ${m} already exists, removing...`),await I`rm -rf ${l}`;else{i.debug(`Machine ${m} already exists, skipping...`);return}await I`mkdir -p ${l}`,await I`tar xzf ${e} -C ${l.replace(`${r}/`,"")}`.cwd(r);let b=`${bn()}/src/runner`;await I`cp -R ${b}/hooks ${l}`,await I`cp -R ${b}/.runner-env ${l}/.runner-env`,i.debug(`Machine ${m} extracted successfully`)},runnerIndex:m})),c=[];for(let m=0;m<a.length;m+=f){let l=a.slice(m,m+f),u=await Promise.allSettled(l.map((b)=>b.execute()));for(let[b,g]of u.entries())if(g.status==="rejected")throw i.error(`Failed to extract runner ${l[b]?.runnerIndex}: ${g.reason}`),g.reason;c.push(...u)}i.info(`Successfully extracted ${s} runners`)}function jr({directory:n,numberOfMachines:o,token:r,url:e,name:i,labels:s,runnerGroup:t}){M.info(`Starting ${o} runners...`);for(let f=1;f<=Number(o);f++){let a=`${bn()}/src/runner-worker.ts`,c=`${i}-${f}`,m=new Worker(a,{name:c,ref:!0});m.addEventListener("open",()=>{M.info(`Worker ${c} started`),m.postMessage({index:f,labels:s,machineCwd:`${n}/machines/${f}`,name:i,runnerGroup:t,token:r,url:e})}),m.addEventListener("message",({data:l})=>{let u=l.type==="error"?"error":"info";M[u](`${c}: ${l.message}`)}),m.addEventListener("error",(l)=>{M.error(`Worker ${c} error:`,l)}),m.addEventListener("exit",(l)=>{M.info(`Worker ${c} exited with code ${l}`)}),qn.add(m)}M.info("All workers started. Press Ctrl+C to stop all machines")}import{existsSync as yn}from"fs";import{readdir as mo}from"fs/promises";import{join as F,resolve as Zr}from"path";import{parseArgs as Qr}from"util";var{$:lt}=globalThis.Bun;var Vr=x({prefix:"hydra"}).child("health");async function Fr(){let{values:n}=Qr({allowPositionals:!0,args:Bun.argv,options:{config:{short:"c",type:"string"},directory:{default:`${process.cwd()}/gh-runners`,short:"d",type:"string"},fix:{default:!1,short:"f",type:"boolean"},json:{default:!1,short:"j",type:"boolean"},profile:{short:"p",type:"string"},runner:{short:"r",type:"string"},token:{short:"t",type:"string"},url:{short:"u",type:"string"}},strict:!0}),o=await A({configPath:n.config}),r=await X(o,n.profile);return{directory:n.directory||r?.directory||`${process.cwd()}/gh-runners`,fix:n.fix,json:n.json,runner:n.runner,token:n.token,url:n.url||r?.url}}async function lo(){try{let n=await Fr(),o=Zr(n.directory),r=F(o,"machines");if(!yn(r)){h.error(`No runners found in ${n.directory}`);return}let e=W();e.start("Performing health checks...");let i=await mo(r),s=[];for(let t of i){if(n.runner&&t!==n.runner)continue;let f=F(r,t),a=await Dr(t,f,n);s.push(a)}if(e.stop("Health checks completed"),n.json){console.log(JSON.stringify(s,null,2));return}if(re(s),n.fix)await ee(s,n)}catch(n){Vr.error("Failed to perform health checks:",n),process.exit(1)}}async function Dr(n,o,r){let e=[],i={configuration:!1,connectivity:!1,disk:{percentage:0,total:0,used:0},lastActivity:null,process:!1};try{let t=F(o,".runner");if(yn(t)){let f=await Bun.file(t).text(),a=await ne(f.trim());if(i.process=a,!a)e.push("Runner process is not running")}else e.push("No PID file found")}catch{e.push("Could not check process status")}try{let t=await te(o);if(i.disk=t,t.percentage>90)e.push(`Low disk space: ${t.percentage.toFixed(1)}% used`)}catch{e.push("Could not check disk space")}try{let t=F(o,"_diag");if(yn(t)){let f=await mo(t),a=null;for(let c of f)if(c.endsWith(".log")){let m=await Bun.file(F(t,c)).stat();if(!a||m.mtime>a)a=new Date(m.mtime)}if(i.lastActivity=a,a){let c=(Date.now()-a.getTime())/3600000;if(c>24)e.push(`No activity for ${Math.floor(c)} hours`)}}}catch{e.push("Could not check last activity")}try{let t=F(o,".runner"),f=F(o,".credentials");if(i.configuration=yn(t)&&yn(f),!i.configuration)e.push("Missing configuration files")}catch{e.push("Could not check configuration")}if(r.token&&r.url)try{let t=await oe(r.url,r.token);if(i.connectivity=t,!t)e.push("Cannot connect to GitHub API")}catch{e.push("Failed to check GitHub connectivity")}let s="healthy";if(e.length===0)s="healthy";else if(e.some((t)=>t.includes("not running")||t.includes("Missing configuration")||t.includes("Cannot connect")))s="critical";else if(e.length>0)s="warning";return{checks:i,issues:e,runner:n,status:s}}async function ne(n){try{return await lt`kill -0 ${n}`.quiet(),!0}catch{return!1}}async function te(n){let r=(await lt`df -k ${n} | tail -1`.text()).trim().split(/\s+/),e=parseInt(r[1]||"0",10)*1024,i=parseInt(r[2]||"0",10)*1024;return{percentage:parseInt(r[4]?.replace("%","")||"0",10),total:e,used:i}}async function oe(n,o){return Z(async()=>{let r=n.match(/github\.com[:/]([^/]+)(?:\/([^/.]+))?/);if(!r)return!1;let[,e,i]=r,s=i?`https://api.github.com/repos/${e}/${i}`:`https://api.github.com/orgs/${e}`;return(await fetch(s,{headers:{Accept:"application/vnd.github.v3+json",Authorization:`Bearer ${o}`}})).ok},{maxAttempts:3})}function re(n){console.log(),console.log(p.bold("\uD83C\uDFE5 Runner Health Status")),console.log("\u2500".repeat(80));let o={critical:"\u274C",healthy:"\u2705",unknown:"\u2753",warning:"\u26A0\uFE0F"},r={critical:p.error,healthy:p.success,unknown:p.dim,warning:p.warning};for(let i of n){let s=o[i.status],t=r[i.status];if(console.log(`${s} ${p.bold(`Runner ${i.runner}`)}: ${t(i.status.toUpperCase())}`),console.log(` Process: ${i.checks.process?"\u2705 Running":"\u274C Not running"}`),console.log(` Config: ${i.checks.configuration?"\u2705 Valid":"\u274C Missing"}`),i.checks.disk.total>0){let f=i.checks.disk.percentage>90?p.error:p.success;console.log(` Disk: ${f(`${i.checks.disk.percentage.toFixed(1)}% used`)}`)}if(i.checks.lastActivity){let a=(Date.now()-i.checks.lastActivity.getTime())/3600000>24?p.warning:p.success;console.log(` Last Activity: ${a(ie(i.checks.lastActivity))}`)}if(i.checks.connectivity!==null)console.log(` GitHub API: ${i.checks.connectivity?"\u2705 Connected":"\u274C Disconnected"}`);if(i.issues.length>0){console.log(" Issues:");for(let f of i.issues)console.log(` - ${p.dim(f)}`)}console.log()}let e={critical:n.filter((i)=>i.status==="critical").length,healthy:n.filter((i)=>i.status==="healthy").length,total:n.length,warning:n.filter((i)=>i.status==="warning").length};console.log("\u2500".repeat(80)),console.log(`Summary: Total: ${e.total} | ${p.success(`Healthy: ${e.healthy}`)} | ${p.warning(`Warning: ${e.warning}`)} | ${p.error(`Critical: ${e.critical}`)}`)}async function ee(n,o){let r=n.filter((i)=>i.status==="critical");if(r.length===0){h.info("No critical issues found that require fixing");return}if(console.log(),!await C({initialValue:!1,message:`Found ${r.length} runners with critical issues. Attempt to fix?`}))return;for(let i of r){if(console.log(),h.info(`Attempting to fix runner ${i.runner}...`),!i.checks.process&&i.checks.configuration){h.info("Attempting to restart runner...");try{let s=F(o.directory,"machines",i.runner);await lt`cd ${s} && ./run.sh`.quiet(),h.success(`Runner ${i.runner} restarted successfully`)}catch(s){h.error(`Failed to restart runner ${i.runner}`)}}if(!i.checks.configuration)h.warning(`Runner ${i.runner} needs to be reconfigured. Run 'hydra create' to set up again.`)}}function ie(n){let r=new Date().getTime()-n.getTime(),e=Math.floor(r/3600000);if(e<1)return`${Math.floor(r/60000)}m ago`;if(e<24)return`${e}h ago`;return`${Math.floor(e/24)}d ago`}var po="0.5.0";var uo={backup:`
50
50
  hydra backup - Backup and restore runner configurations
51
51
 
52
52
  USAGE
package/package.json CHANGED
@@ -39,5 +39,5 @@
39
39
  },
40
40
  "type": "module",
41
41
  "types": "src/cli.ts",
42
- "version": "0.4.0"
42
+ "version": "0.5.0"
43
43
  }