@lovek4d/kickflip 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.
- package/CONTEXT.md +32 -16
- package/SKILL.md +8 -6
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +8 -8
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +50 -28
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -3
- package/board/headless.mjs +4 -3
- package/board/merge3.mjs +1 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -2
- package/board/public/app-asks.js +6 -89
- package/board/public/app-cards.js +19 -10
- package/board/public/app-core.js +1 -20
- package/board/public/app-reader.js +174 -0
- package/board/public/app-threads.js +49 -35
- package/board/public/app.html +2 -0
- package/board/public/app.js +28 -16
- package/board/public/hub.css +1 -0
- package/board/public/new-task.js +121 -0
- package/board/public/shell.js +4 -0
- package/board/public/sidebar.css +1 -0
- package/board/public/style.css +91 -3
- package/board/public/ui-cards.js +46 -21
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-phase-bars.js +4 -4
- package/board/public/ui-questions.js +47 -18
- package/board/public/ui-sidebar.js +9 -3
- package/board/public/ui-threads.js +25 -12
- package/board/public/ui.js +42 -12
- package/board/public/view-frame.js +1 -0
- package/board/public/view-questions.js +45 -16
- package/board/public/view-settings.js +38 -19
- package/board/server.mjs +20 -18
- package/board/store.mjs +15 -13
- package/board/worktree.mjs +20 -20
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +20 -0
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +9 -12
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -0
- package/hub/readiness.mjs +2 -2
- package/hub/sessions.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/package.json +1 -1
- package/shared/owner-check.mjs +3 -0
- package/shared/tailscale.mjs +1 -0
- package/shared/usage-gate.mjs +1 -1
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),
|
|
3
|
-
`)})}o(wt,"failure");function et(t){return t.ending||(Nt(t.child,"SIGTERM"),t.ending=new Promise(e=>{const s=o(()=>{clearTimeout(i),clearInterval(r),e()},"finish"),i=setTimeout(()=>{Nt(t.child,"SIGKILL"),s()},4e3),r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{s()}},20)})),t.ending}o(et,"terminate");async function yt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await et(t),await t.done}o(yt,"halt");async function X(t,e=!1){await yt(t,e),z(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(R(t.board,t.key))}o(X,"stopRun");async function gt(t,e,s,i={}){if(!s)throw new S("This card has no valid worktree.");const r=Mt(s),a=Zt(r);if(a)throw new S(a);if(!T.realpathSync(r).startsWith(T.realpathSync(s)+x.sep))throw new S("Build script must be inside the worktree.");const l=await j(s,"rev-parse","HEAD");if(D)throw new S("The hub is stopping. Try again after it restarts.",503);const{state:g}=B(t),_=ht(g,e),d={...e==="main"?{}:_,...i,sha:l,script:!0,state:"running",...e==="main"?{}:{at:it(),head:l}};delete d.port,delete d.https;const O=_t(t,e);T.mkdirSync(x.dirname(O),{recursive:!0});const I=T.openSync(O,"w",384);T.fchmodSync(I,384);const $=Jt(r,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route}});e==="main"?g.main=d:g.cards[e]=d,A(t);const N=R(t,e),c={child:$,board:t,key:e,logFile:O,visited:n(),stopped:!1,port:null,ready:!1};m.set(N,c),P.set(N,l);let F,E;const U=o(M=>{E||=new Error(M),et(c)},"fail");$.on("error",M=>{F=M}),$.stdout.on("data",M=>T.writeSync(I,M)),$.stderr.on("data",M=>T.writeSync(I,M));const G=Ot({input:$.stdout,crlfDelay:1/0});return G.on("line",M=>{const Z=/^port (\d{1,5})$/.exec(M);if(!Z||c.port||c.stopped)return;const H=Number(Z[1]);if(H<1||H>65535){U("Build script printed an invalid port.");return}c.port=H,clearTimeout(c.deadline);const at=n()+6e4;let ct=!1;const St=o(async()=>{if(!(ct||c.stopped||c.ready||$.exitCode!==null||$.signalCode!==null)){ct=!0;try{if(await f(H)){if(c.stopped||E||$.exitCode!==null||$.signalCode!==null||(c.opening=Rt(c),await c.opening,c.stopped||E||$.exitCode!==null||$.signalCode!==null))return;c.ready=!0,c.visited=n(),g.kind="web",z(t,e,{state:"ready",port:H,https:c.https}),clearInterval(c.poll)}else n()>=at&&U("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{ct=!1}}},"check");c.poll=setInterval(()=>St().catch(lt=>U(lt.message)),250),St().catch(lt=>U(lt.message))}),c.deadline=setTimeout(()=>U("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{et(c)}),c.done=new Promise(M=>$.once("close",(Z,H)=>{clearTimeout(c.deadline),clearInterval(c.poll),G.close(),T.closeSync(I),(async()=>{await et(c),await Ft(c),c.finished=!0,!c.stopped&&(E||F||Z!==0||c.port&&!c.ready?await wt(c,Z??1,H,E||F||(c.port&&!c.ready?new Error("Build script exited before its port answered."):null)):c.ready?(e==="main"?(delete g.main.state,delete g.main.port,delete g.main.https):delete g.cards[e],A(t)):(g.kind="app",z(t,e,{state:"ready"})))})().catch(at=>y(`build /${t.route} ${e}: ${at.message}`)).finally(()=>{m.get(N)===c&&m.delete(N),M()})})),y(`build /${t.route} ${e}: started; log ${O}`),{status:"started"}}o(gt,"launch");async function $t(t,e,s){const i=R(t,"main");if(p.has(i))throw new S("A build is already running for main.",409);p.add(i);try{m.has(i)&&await X(m.get(i),!0);const r=x.join(Tt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(x.dirname(r),{recursive:!0}),Pt(r)?await j(r,"checkout","--detach","--force",s.sha):(await j(e,"worktree","prune"),await j(e,"worktree","add","--detach",r,s.sha)),await gt(t,"main",r,s)}catch(r){throw r.status!==503&&(B(t).state.main={...s,state:"failed"},At(t,"main",r),A(t)),r}finally{p.delete(i)}}o($t,"launchMain");async function vt(t,{manifest:e=K()}={}){if(await V(e),st(e),D)throw new S("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new S("Send only the board route and card key.");const{route:s,key:i}=t;if(i!=="main"&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(d=>d==="."||d==="..")))throw new S("Card key must be <wave>/<id>.");const r=q(e).find(d=>d.route===s);if(!r)throw new S(`/${s} is not a board.`,404);if(i!=="main"&&!r.waves.some(({slug:d,wave:O})=>O.items.some(I=>`${d}/${I.id}`===i)))throw new S("There is no item with this card key.",404);const a=await h();if(a)throw new S(a,409);const l=R(r,i),g=m.get(l);if(p.has(l)||g&&!g.ready&&!g.finished)throw new S(`A build is already running for ${i==="main"?"main":"this card"}.`,409);if(i==="main"){const d=B(r).state.main;if(d?.state==="running")throw new S("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new S("Main has no failed build to retry.",409);const{sha:O,branch:I,slug:$,at:N,script:c}=d;return $t(r,Y(s,e).repo,{sha:O,branch:I,slug:$,at:N,script:c})}p.add(l);const _=k.delete(l);try{m.has(l)&&await X(m.get(l),!0);const d=await Dt(r,i,e);return await gt(r,i,d)}catch(d){throw w.delete(l),_&&d.status!==503&&z(r,i,{state:void 0,sha:void 0}),d}finally{p.delete(l)}}o(vt,"start");function bt(t,e,{manifest:s=K()}={}){const i=q(s).find(a=>a.route===t);if(!i)return null;const r=[...m.values()].find(a=>a.board.dir===i.dir&&a.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&a.ready&&!a.stopped&&a.child.exitCode===null&&a.child.signalCode===null);return r?{key:r.key,port:r.port}:null}o(bt,"lookup");async function ot({manifest:t=K(),routes:e}={}){if(await V(t),st(t),D)return L;if(L&&e){if(!J){const s={routes:new Set,manifest:t};s.done=L.catch(()=>{}).then(()=>(J=null,ot({manifest:s.manifest,routes:[...s.routes]}))),J=s}J.manifest=t;for(const s of e)J.routes.add(s);return J.done}return L||(L=(async()=>{const s=q(t),i=rt(Yt,{}).prs||{};for(const r of[...m.values()])if(!s.some(a=>a.dir===r.board.dir)||r.key!=="main"&&r.ready&&n()-r.visited>=ae){await X(r);const{state:a}=B(r.board);r.key==="main"?a.main=null:delete a.cards[r.key],A(r.board)}for(const r of s){if(D)return;if(e&&!e.includes(r.route))continue;const{state:a}=B(r),l=Y(r.route,t),g=l.branch||"main",_=l.local?`refs/heads/${g}`:`refs/remotes/origin/${g}`;for(const{slug:d,wave:O}of r.waves)for(const I of O.items){const $=`${d}/${I.id}`,N=ie(I,r.github||O.repo).map(c=>i[ne(c)]);if(!(["done","merged","dropped"].includes(String(I.status).toLowerCase())||N.length&&N.every(c=>["MERGED","CLOSED"].includes(c?.state))))try{const c=R(r,$),F=await Dt(r,$,t);if(F){const G=await j(F,"rev-parse","HEAD");let M=0;try{M=Number(await j(F,"rev-list","--count",`${_}..HEAD`))}catch{}Object.assign(a.cards[$]||={},{head:G,ahead:M,script:Pt(Mt(F))}),A(r)}else if(!k.has(c))continue;const E=a.cards[$];if((k.has(c)||r.auto_build&&E.script&&E.ahead>0&&(E.sha||P.get(c))!==E.head&&E.state!=="running")&&!p.has(c))try{await vt({route:r.route,key:$},{manifest:t})}catch(G){if([409,503].includes(G.status))continue;throw z(r,$,{sha:E.head}),await wt({board:r,key:$,logFile:At(r,$,G)},1),G}}catch(c){y(`try /${r.route} ${$}: ${c.message}`)}}try{if(!l.repo||await h())continue;const d=await j(l.repo,"rev-parse",_);let O=!1;try{await j(l.repo,"cat-file","-e",`${d}:.kickflip/build`),O=!0}catch{}if(!O){const E=m.get(R(r,"main"));E&&await X(E),a.main=null,A(r);continue}const I=a.main;if(I?.state==="running"||I?.sha===d&&(I.state==="failed"||I.state==="ready"&&(a.kind!=="web"||bt(r.route,"main",{manifest:t}))))continue;const $=R(r,"main");if(p.has($))continue;const N=await j(l.repo,"log","-1","--format=%cI",d);let c=r.github;if(!c)try{c=Et(await j(l.repo,"remote","get-url","origin"))}catch{}const F={sha:d,branch:g,slug:c||"",at:N,script:O};await $t(r,l.repo,F)}catch(d){y(`try /${r.route} main: ${d.message}`)}}})().finally(()=>{L=null}),L)}o(ot,"tick");function kt(t,e){if(C.has(t.dir))return C.get(t.dir);const s=(async()=>{const i=Y(t.route,e),r=i.branch||"main",{state:a}=B(t);a.pull={...a.pull,running:!0},A(t);let l,g;try{l=await de(i.repo,r)}catch(_){g=_.message}if(l===null?delete a.pull:a.pull={at:it(),...g?{error:g}:{}},A(t),l===null)return null;if(g)return y(`pull /${t.route}: ${g}`),{commits:0,error:g};if(l.commits){y(`pull /${t.route}: ${r} fast-forwarded ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await b(t,"main-pulled",{branch:r,...l})}catch(_){y(`pull /${t.route}: could not send main-pulled (${_.message})`)}}return{commits:l.commits}})().finally(()=>C.delete(t.dir));return C.set(t.dir,s),s}o(kt,"pullMain");async function Lt({manifest:t=K(),routes:e}={}){await V(t);const s=[];for(const i of q(t)){if(D)break;if(e&&!e.includes(i.route))continue;const r=Y(i.route,t);!r.local||!r.repo||(await kt(i,t))?.commits&&s.push(i.route)}return s}o(Lt,"pullMains");async function Gt(t,{manifest:e=K()}={}){if(await V(e),D)throw new S("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(a=>a!=="route")||typeof t.route!="string")throw new S("Send only the board route.");const s=q(e).find(a=>a.route===t.route);if(!s)throw new S(`/${t.route} is not a board.`,404);const i=Y(s.route,e);if(!i.local||!i.repo)throw new S("This board fetches its branch on every poll; only a local board pulls.",409);const r=await kt(s,e);if(!r)throw new S("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ot({manifest:e,routes:[s.route]}).catch(a=>y(`try /${s.route} main: ${a.message}`)),r}o(Gt,"pull");async function Ht(){D=!0;const t=Promise.all([...m.values()].map(e=>e.ready?X(e):yt(e)));await Promise.all([t,L,...C.values()])}return o(Ht,"stop"),{start:vt,tick:ot,stop:Ht,lookup:bt,initialize:V,pull:Gt,pullMains:Lt}}o(le,"createTryManager");const ft=1e4,pt=o(async(n,...f)=>(await dt("git",["-C",n,...f],{encoding:"utf8",timeout:ft,env:Vt()})).stdout,"originGit");async function de(n,f,{run:y=pt}={}){const h=o(async(...u)=>(await y(...u)).trim(),"git"),b=o(u=>{throw new Error(`can\u2019t pull: ${u}`)},"refuse");try{await h(n,"config","--get","remote.origin.url")}catch{return null}try{await h(n,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(u){b(u.killed?`git fetch timed out after ${ft/1e3} s`:jt(u.stderr,{repo:n}).error)}try{const u=await h(n,"rev-parse","--verify",`refs/heads/${f}^{commit}`),m=await h(n,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),p=o((v,P)=>h(n,"merge-base","--is-ancestor",v,P).then(()=>!0,C=>{if(C.code===1)return!1;throw C}),"isAncestor");if(u===m||await p(m,u))return{from:u,to:u,commits:0};await p(u,m)||b(`${f} has split from origin`);const k=Number(await h(n,"rev-list","--count",`${u}..${m}`)),w=(await h(n,"worktree","list","--porcelain")).split(`
|
|
1
|
+
var Wt=Object.defineProperty;var a=(n,f)=>Wt(n,"name",{value:f,configurable:!0});import x from"node:fs";import O from"node:path";import Ot from"node:os";import ut from"node:http";import{execFile as Jt,spawn as zt}from"node:child_process";import{createInterface as Ct}from"node:readline";import{promisify as Ut}from"node:util";import{listBoards as W,readManifest as J,readJSON as rt,saveJSON as Et,appendEvent as Yt,GITHUB_STATE as qt,REPO_FETCH as It}from"./boards.mjs";import{boardRepo as Q,githubFromOrigin as jt}from"./board-repo.mjs";import{gitReachFault as Mt,remoteFromInput as Qt,remoteGitEnv as Vt}from"./repos.mjs";import{readiness as Xt}from"./readiness.mjs";import{tailscaleBin as Zt}from"../shared/tailscale.mjs";import{buildScriptError as te}from"../board/build-script.mjs";import{appendThread as ee,recordedWorktree as ie,setItem as re}from"../board/store.mjs";import{cardPRs as se,hasPR as ne,prKeyOf as oe}from"../shared/card-prs.mjs";import{hubGate as ae}from"../board/on-host.mjs";import{pipeUpgrade as ce,refuse as le,refuseUpgrade as de}from"./front-door.mjs";class T extends Error{static{a(this,"BuildError")}constructor(f,w=400){super(f),this.status=w}}const ft=Ut(Jt),M=a(async(n,...f)=>(await ft("git",["-C",n,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),ue=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,z={first:8500,last:8599},pt=a(n=>/^\d+$/.test(String(n))&&Number(n)>=z.first&&Number(n)<=z.last,"isTryPort"),fe=30*6e4,Pt=a(n=>O.join(n,".kickflip","build"),"scriptAt"),_t=a(n=>x.existsSync(n),"exists"),At=a((n,f)=>O.join(n.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function Nt(n,f,w){const m=At(n,f);return x.mkdirSync(O.dirname(m),{recursive:!0}),x.writeFileSync(m,w.message+`
|
|
2
|
+
`,{mode:384}),x.chmodSync(m,384),m}a(Nt,"failureLog");const pe=a(n=>new Promise(f=>{const w=ut.get({host:"127.0.0.1",port:n,path:"/",timeout:1e3},m=>{m.destroy(),f(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>f(!1))}),"probe"),Dt=a((n,f)=>{try{process.kill(-n.pid,f)}catch{}},"signalGroup");async function Bt(n,f,w){const m=await ie(n.dir,f),[v,p]=f.split("/"),k=n.waves.find(b=>b.slug===v)?.wave,d=m?.path||k?.items.find(b=>b.id===p)?.worktree;if(typeof d!="string"||!d||d.split(O.sep).includes(".."))return"";let g=m?.repo;const y=(n.github||k?.repo||"").toLowerCase();if(!g&&y){const b=[n.owner?.repo,n.dir,...Object.values(w?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(b.filter(P=>typeof P=="string"&&O.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(I,"remote","get-url","origin"))?.[1].toLowerCase()===y){g=await M(I,"rev-parse","--show-toplevel");break}}catch{}}if(!m?.path&&!g||m?.path&&!O.isAbsolute(d))return"";try{const b=[O.join(x.realpathSync(Ot.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&O.isAbsolute(g))try{b.push(O.join(x.realpathSync(g),".claude","worktrees"))}catch{}const I=O.resolve(g||"/",d),P=x.realpathSync(I);return b.some(L=>I.startsWith(L+O.sep)&&P.startsWith(L+O.sep))&&x.statSync(P).isDirectory()?P:""}catch{return""}}a(Bt,"installWorktree");function he({now:n=Date.now,answers:f=pe,log:w=console.log,waiting:m=a(async()=>null,"waiting"),event:v=Yt,gate:p=ae}={}){const k=new Map,d=new Map,g=new Set,y=new Set,b=new Map,I=new Map,P=new Map,L=new Map;let G=!1,R=null,U=null,et=null;const st=a(()=>new Date(n()).toISOString(),"stamp");function D(t){if(k.has(t.dir))return k.get(t.dir);const i=O.join(t.dir,"data","try.json"),r=rt(i,{kind:null,main:null,cards:{}}),s={...pt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},e={state:s,file:i,written:JSON.stringify(r)};k.set(t.dir,e),s.main_port&&I.set(t.dir,s.main_port);const o=Object.entries(s.cards).filter(([,c])=>c.state==="running");for(const[c]of o)y.add(B(t,c));return s.kind==="web"?(s.cards=Object.fromEntries(o),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),A(t),e}a(D,"stateOf");function A(t){const i=k.get(t.dir),r=JSON.stringify(i.state);r!==i.written&&(Et(i.file,i.state),i.written=r)}a(A,"persist");const B=a((t,i)=>`${t.dir}\0${i}`,"runKey"),wt=a((t,i)=>i==="main"?t.main:t.cards[i],"entryOf");function Y(t,i,r){const{state:s}=D(t),e=wt(s,i);if(!e)return;const o=Object.hasOwn(r,"state")&&r.state!==e.state;Object.assign(e,r,i!=="main"&&o?{at:st()}:{}),A(t)}a(Y,"update");function nt(t){const i=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of i){if(!r?.dir)continue;const s=rt(O.join(r.dir,"data","try.json"),{}).main_port;pt(s)&&I.set(r.dir,Number(s))}}a(nt,"reserveMains");function Ft(t){const{board:i,key:r}=t,s=B(i,r),e=r==="main"?I.get(i.dir):b.get(s);if(e)return e;const o=new Set([...I.values(),...b.values()]);let c=z.first;for(;o.has(c)&&c<=z.last;)c++;if(c>z.last)throw new Error(`No try ports available (${z.first}\u2013${z.last}). Stop a served card and retry.`);return r==="main"?(I.set(i.dir,c),D(i).state.main_port=c,A(i)):b.set(s,c),c}a(Ft,"publicPort");const ot=a(async t=>{const i=Zt();if(!i)throw new Error("Tailscale isn't installed on this host");return ft(i,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function yt(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(i){w(`try port ${t}: could not close port: ${(i.stderr||i.message).trim()}`)}}a(yt,"removePort");async function Rt(t){t.https=Ft(t);const i=t.sockets=new Set,r=a(()=>{t.visited=n()},"visit"),s=a(e=>{i.add(e),e.on("data",r),e.on("close",()=>i.delete(e))},"track");if(t.relay=ut.createServer(async(e,o)=>{const c=await p(e);if(c!=="owner"){le(e,o,c);return}const h=ut.request({host:"127.0.0.1",port:t.port,method:e.method,path:e.url,headers:e.headers},S=>{o.writeHead(S.statusCode,S.headers),S.on("data",r),S.pipe(o),S.on("close",()=>{S.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||h.destroy()}),h.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),e.pipe(h)}),t.relay.on("connection",e=>{r(),s(e)}),t.relay.on("upgrade",async(e,o,c)=>{const h=await p(e);if(h!=="owner"){de(o,h);return}s(ce({req:e,socket:o,head:c,port:t.port,path:e.url}))}),await new Promise((e,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",e)}),!t.stopped){t.exposed=!0;try{await ot(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(e){throw new Error(`Couldn't open try port ${t.https}: ${(e.stderr||e.message).trim()}`)}}}a(Rt,"openPort");async function Lt(t){if(await t.opening?.catch(()=>{}),t.exposed&&await yt(t.https),t.relay){for(const i of t.sockets)i.destroy();await new Promise(i=>t.relay.close(i))}t.keepPort||b.delete(B(t.board,t.key))}a(Lt,"closePort");function X(t=J()){if(et)return et;nt(t);for(const i of W(t))D(i);return et=(async()=>{try{const{stdout:i}=await ot(["serve","status","--json"]),r=JSON.parse(i);for(const s of Object.keys(r?.TCP||{}))pt(s)&&![...d.values()].some(e=>e.https===Number(s))&&await yt(s)}catch(i){w(`try startup cleanup: ${(i.stderr||i.message).trim()}`)}})(),et}a(X,"initialize");async function gt(t,i,r,s){const{board:e,key:o}=t;if(Y(e,o,{state:"failed",port:void 0,https:void 0}),o==="main")return;const c=[];let h=!1;try{for await(const u of Ct({input:x.createReadStream(t.logFile),crlfDelay:1/0}))h||=ue.test(u),c.push(u),c.length>5&&c.shift()}catch(u){c.push(`Could not read the build log: ${u.message}`)}const S=t.port||(D(e).state.kind||(e.waves.some(({wave:u})=>u.items.some(C=>C.install?.url))?"app":"web"))==="web"?"Serve":"Build";await ee(e.dir,o,{text:[`${S} failed (exit ${i??1})`,s?.message,r&&`Stopped by ${r}.`,h&&"The login keychain must be unlocked on the Mac.",...c].filter(Boolean).join(`
|
|
3
|
+
`)})}a(gt,"failure");function it(t){return t.ending||(Dt(t.child,"SIGTERM"),t.ending=new Promise(i=>{const r=a(()=>{clearTimeout(s),clearInterval(e),i()},"finish");let s=setTimeout(()=>{Dt(t.child,"SIGKILL"),s=setTimeout(r,1e3)},4e3);const e=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{r()}},20)})),t.ending}a(it,"terminate");async function $t(t,i=!1){t.keepPort=i,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await it(t),await t.done}a($t,"halt");async function Z(t,i=!1){await $t(t,i),Y(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(B(t.board,t.key))}a(Z,"stopRun");async function bt(t,i,r,s={}){if(!r)throw new T("This card has no valid worktree.");const e=Pt(r),o=te(e);if(o)throw new T(o);if(!x.realpathSync(e).startsWith(x.realpathSync(r)+O.sep))throw new T("Build script must be inside the worktree.");const c=await M(r,"rev-parse","HEAD");if(G)throw new T("The hub is stopping. Try again after it restarts.",503);const{state:h}=D(t),S=wt(h,i),u={...i==="main"?{}:S,...s,sha:c,script:!0,state:"running",...i==="main"?{}:{at:st(),head:c}};delete u.port,delete u.https;const C=At(t,i);x.mkdirSync(O.dirname(C),{recursive:!0});const E=x.openSync(C,"w",384);x.fchmodSync(E,384);const $=zt(e,[i,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});i==="main"?h.main=u:h.cards[i]=u,A(t);const N=B(t,i),l={child:$,board:t,key:i,logFile:C,visited:n(),stopped:!1,port:null,ready:!1};d.set(N,l),P.set(N,c);let F,j;const q=a(_=>{j||=new Error(_),it(l)},"fail");$.on("error",_=>{F=_}),$.stdout.on("data",_=>x.writeSync(E,_)),$.stderr.on("data",_=>x.writeSync(E,_));const H=Ct({input:$.stdout,crlfDelay:1/0});return H.on("line",_=>{const tt=/^port (\d{1,5})$/.exec(_);if(!tt||l.port||l.stopped)return;const K=Number(tt[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const ct=n()+6e4;let lt=!1;const xt=a(async()=>{if(!(lt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){lt=!0;try{if(await f(K)){if(l.stopped||j||$.exitCode!==null||$.signalCode!==null||(l.opening=Rt(l),await l.opening,l.stopped||j||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=n(),h.kind="web",Y(t,i,{state:"ready",port:K,https:l.https}),clearInterval(l.poll)}else n()>=ct&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{lt=!1}}},"check");l.poll=setInterval(()=>xt().catch(dt=>q(dt.message)),250),xt().catch(dt=>q(dt.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{it(l)}),l.done=new Promise(_=>$.once("close",(tt,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),H.close(),x.closeSync(E),(async()=>{await it(l),await Lt(l),l.finished=!0,!l.stopped&&(j||F||tt!==0||l.port&&!l.ready?await gt(l,tt??1,K,j||F||(l.port&&!l.ready?new Error("Build script exited before its port answered."):null)):l.ready?(i==="main"?(delete h.main.state,delete h.main.port,delete h.main.https):delete h.cards[i],A(t)):(h.kind="app",Y(t,i,{state:"ready"})))})().catch(ct=>w(`build /${t.route} ${i}: ${ct.message}`)).finally(()=>{d.get(N)===l&&d.delete(N),_()})})),w(`build /${t.route} ${i}: started; log ${C}`),{status:"started"}}a(bt,"launch");async function vt(t,i,r){const s=B(t,"main");if(g.has(s))throw new T("A build is already running for main.",409);g.add(s);try{d.has(s)&&await Z(d.get(s),!0);const e=O.join(Ot.homedir(),".claude","kickflip","try",`${t.route}-main`);return x.mkdirSync(O.dirname(e),{recursive:!0}),_t(e)?await M(e,"checkout","--detach","--force",r.sha):(await M(i,"worktree","prune"),await M(i,"worktree","add","--detach",e,r.sha)),await bt(t,"main",e,r)}catch(e){throw e.status!==503&&(D(t).state.main={...r,state:"failed"},Nt(t,"main",e),A(t)),e}finally{g.delete(s)}}a(vt,"launchMain");async function kt(t,{manifest:i=J()}={}){if(await X(i),nt(i),G)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(u=>!["route","key"].includes(u))||typeof t.route!="string"||typeof t.key!="string")throw new T("Send only the board route and card key.");const{route:r,key:s}=t;if(s!=="main"&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(u=>u==="."||u==="..")))throw new T("Card key must be <wave>/<id>.");const e=W(i).find(u=>u.route===r);if(!e)throw new T(`/${r} is not a board.`,404);if(s!=="main"&&!e.waves.some(({slug:u,wave:C})=>C.items.some(E=>`${u}/${E.id}`===s)))throw new T("There is no item with this card key.",404);const o=await m();if(o)throw new T(o,409);const c=B(e,s),h=d.get(c);if(g.has(c)||h&&!h.ready&&!h.finished)throw new T(`A build is already running for ${s==="main"?"main":"this card"}.`,409);if(s==="main"){const u=D(e).state.main;if(u?.state==="running")throw new T("A build is already running for main.",409);if(u?.state!=="failed"||!u.sha)throw new T("Main has no failed build to retry.",409);const{sha:C,branch:E,slug:$,at:N,script:l}=u;return vt(e,Q(r,i).repo,{sha:C,branch:E,slug:$,at:N,script:l})}g.add(c);const S=y.delete(c);try{d.has(c)&&await Z(d.get(c),!0);const u=await Bt(e,s,i);return await bt(e,s,u)}catch(u){throw b.delete(c),S&&u.status!==503&&Y(e,s,{state:void 0,sha:void 0}),u}finally{g.delete(c)}}a(kt,"start");function St(t,i,{manifest:r=J()}={}){const s=W(r).find(o=>o.route===t);if(!s)return null;const e=[...d.values()].find(o=>o.board.dir===s.dir&&o.key.split("/").at(-1).toLowerCase()===i.toLowerCase()&&o.ready&&!o.stopped&&o.child.exitCode===null&&o.child.signalCode===null);return e?{key:e.key,port:e.port}:null}a(St,"lookup");async function at({manifest:t=J(),routes:i}={}){if(await X(t),nt(t),G)return R;if(R&&i){if(!U){const r={routes:new Set,manifest:t};r.done=R.catch(()=>{}).then(()=>(U=null,at({manifest:r.manifest,routes:[...r.routes]}))),U=r}U.manifest=t;for(const r of i)U.routes.add(r);return U.done}return R||(R=(async()=>{const r=W(t),s=rt(qt,{}).prs||{};for(const e of[...d.values()])if(!r.some(o=>o.dir===e.board.dir)||e.key!=="main"&&e.ready&&n()-e.visited>=fe){await Z(e);const{state:o}=D(e.board);e.key==="main"?o.main=null:delete o.cards[e.key],A(e.board)}for(const e of r){if(G)return;if(i&&!i.includes(e.route))continue;const{state:o}=D(e),c=Q(e.route,t),h=c.branch||"main",S=c.local?`refs/heads/${h}`:`refs/remotes/origin/${h}`;for(const{slug:u,wave:C}of e.waves)for(const E of C.items){const $=`${u}/${E.id}`,N=se(E,e.github||C.repo).map(l=>s[oe(l)]);if(!(["done","merged","dropped"].includes(String(E.status).toLowerCase())||N.length&&N.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=B(e,$),F=await Bt(e,$,t);if(F){const H=await M(F,"rev-parse","HEAD");let _=0;try{_=Number(await M(F,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(o.cards[$]||={},{head:H,ahead:_,script:_t(Pt(F))}),A(e)}else if(!y.has(l))continue;const j=o.cards[$];if((y.has(l)||e.auto_build&&j.script&&j.ahead>0&&(j.sha||P.get(l))!==j.head&&j.state!=="running")&&!g.has(l))try{await kt({route:e.route,key:$},{manifest:t})}catch(H){if([409,503].includes(H.status))continue;throw Y(e,$,{sha:j.head}),await gt({board:e,key:$,logFile:Nt(e,$,H)},1),H}}catch(l){w(`try /${e.route} ${$}: ${l.message}`)}}try{if(!c.repo||await m())continue;const u=await M(c.repo,"rev-parse",S);let C=!1;try{await M(c.repo,"cat-file","-e",`${u}:.kickflip/build`),C=!0}catch{}if(!C){const j=d.get(B(e,"main"));j&&await Z(j),o.main=null,A(e);continue}const E=o.main;if(E?.state==="running"||E?.sha===u&&(E.state==="failed"||E.state==="ready"&&(o.kind!=="web"||St(e.route,"main",{manifest:t}))))continue;const $=B(e,"main");if(g.has($))continue;const N=await M(c.repo,"log","-1","--format=%cI",u);let l=e.github;if(!l)try{l=jt(await M(c.repo,"remote","get-url","origin"))}catch{}const F={sha:u,branch:h,slug:l||"",at:N,script:C};await vt(e,c.repo,F)}catch(u){w(`try /${e.route} main: ${u.message}`)}}})().finally(()=>{R=null}),R)}a(at,"tick");function Tt(t,i){if(L.has(t.dir))return L.get(t.dir);const r=(async()=>{const s=Q(t.route,i),e=s.branch||"main",{state:o}=D(t);o.pull={...o.pull,running:!0},A(t);let c,h;try{c=await me(s.repo,e)}catch(S){h=S.message}if(c===null?delete o.pull:o.pull={at:st(),...h?{error:h}:{}},A(t),c===null)return null;if(h)return w(`pull /${t.route}: ${h}`),{commits:0,error:h};if(c.commits){w(`pull /${t.route}: ${e} fast-forwarded ${c.commits} commit${c.commits===1?"":"s"} to ${c.to.slice(0,7)}`);try{await v(t,"main-pulled",{branch:e,...c})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:c.commits}})().finally(()=>L.delete(t.dir));return L.set(t.dir,r),r}a(Tt,"pullMain");async function Ht({manifest:t=J(),routes:i}={}){await X(t);const r=[];for(const s of W(t)){if(G)break;if(i&&!i.includes(s.route))continue;const e=Q(s.route,t);!e.local||!e.repo||(await Tt(s,t))?.commits&&r.push(s.route)}return r}a(Ht,"pullMains");async function Gt(t,{manifest:i=J()}={}){if(await X(i),G)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(o=>o!=="route")||typeof t.route!="string")throw new T("Send only the board route.");const r=W(i).find(o=>o.route===t.route);if(!r)throw new T(`/${t.route} is not a board.`,404);const s=Q(r.route,i);if(!s.local||!s.repo)throw new T("This board fetches its branch on every poll; only a local board pulls.",409);const e=await Tt(r,i);if(!e)throw new T("This board\u2019s folder has no origin to pull from.",409);return e.commits&&at({manifest:i,routes:[r.route]}).catch(o=>w(`try /${r.route} main: ${o.message}`)),e}a(Gt,"pull");async function Kt(){G=!0;const t=Promise.all([...d.values()].map(i=>i.ready?Z(i):$t(i)));await Promise.all([t,R,...L.values()])}return a(Kt,"stop"),{start:kt,tick:at,stop:Kt,lookup:St,initialize:X,pull:Gt,pullMains:Ht}}a(he,"createTryManager");const ht=1e4,mt=a(async(n,...f)=>(await ft("git",["-C",n,...f],{encoding:"utf8",timeout:ht,env:Vt()})).stdout,"originGit");async function me(n,f,{run:w=mt}={}){const m=a(async(...p)=>(await w(...p)).trim(),"git"),v=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await m(n,"config","--get","remote.origin.url")}catch{return null}try{await m(n,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){v(p.killed?`git fetch timed out after ${ht/1e3} s`:Mt(p.stderr,{repo:n}).error)}try{const p=await m(n,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await m(n,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),d=a((b,I)=>m(n,"merge-base","--is-ancestor",b,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await d(k,p))return{from:p,to:p,commits:0};await d(p,k)||v(`${f} has split from origin`);const g=Number(await m(n,"rev-list","--count",`${p}..${k}`)),y=(await m(n,"worktree","list","--porcelain")).split(`
|
|
4
4
|
|
|
5
|
-
`).map(
|
|
6
|
-
`)).find(
|
|
7
|
-
`).at(-1))}}
|
|
8
|
-
`).map(
|
|
5
|
+
`).map(b=>b.split(`
|
|
6
|
+
`)).find(b=>b.includes(`branch refs/heads/${f}`))?.[0].slice(9);return y?(await m(y,"status","--porcelain","--untracked-files=no")&&v("local changes"),await m(y,"merge","--ff-only","--quiet",k)):await m(n,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:g}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;v(String(p.stderr||p.message).trim().split(`
|
|
7
|
+
`).at(-1))}}a(me,"pullBranch");async function Ne({manifest:n=J(),log:f=console.log,routes:w}={}){const m=w?rt(It,{}):{};for(const v of W(n)){if(w&&!w.includes(v.route))continue;const p=Q(v.route,n);if(p.local||!p.repo)continue;const k=p.branch||"main",d=new Date().toISOString(),g=Date.now();try{await mt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),m[v.route]={at:d,ok:!0}}catch(y){f(`fetch /${v.route} after ${Date.now()-g} ms: ${y.message}`);let b=v.github;if(!b&&!v.remote)try{b=jt(await M(p.repo,"remote","get-url","origin"))}catch{}m[v.route]={at:d,ok:!1,...Mt(y.killed?`git fetch timed out after ${ht/1e3} s`:y.stderr,{github:b||"",remote:v.remote,repo:p.repo})};continue}if(v.remote)try{await we(v,p.repo,k,f)}catch(y){f(`pushed branches /${v.route}: ${y.message}`)}}Et(It,m)}a(Ne,"fetchTryBranches");async function we(n,f,w,m){const v=n.waves.flatMap(({slug:d,wave:g})=>g.items.filter(y=>y.pushed?.branch&&!ne(y)&&!["done","merged","dropped"].includes(String(y.status).toLowerCase())).map(y=>({key:`${d}/${y.id}`,branch:y.pushed.branch})));if(!v.length)return;const p=new Map((await mt(f,"ls-remote","--heads","origin")).split(`
|
|
8
|
+
`).map(d=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(d)).filter(Boolean).map(d=>[d[2],d[1]])),k=Qt(n.remote)?.host||"the remote";for(const d of v){let g=p.get(d.branch);if(!g)try{g=await M(f,"rev-parse","--verify","--quiet",`refs/heads/${d.branch}^{commit}`)}catch{}let y=!1;if(g)try{await M(f,"merge-base","--is-ancestor",g,`refs/remotes/origin/${w}`),y=!0}catch{}if(!(!y&&p.has(d.branch)))try{await re(n.dir,d.key,{status:y?"merged":"done",force:!0,text:y?`${d.branch} is merged into ${w} on ${k}, so this card is Done.`:`${d.branch} is gone from ${k}, so this card is Done.`})}catch(b){m(`pushed ${d.key}: ${b.message}`)}}}a(we,"settlePushed");const V=he({waiting:a(()=>Xt.waiting("tailscale"),"waiting")}),De=a((...n)=>V.start(...n),"startBuild"),Be=a((...n)=>V.tick(...n),"tryTick"),Fe=a((...n)=>V.pull(...n),"pullNow"),Re=a((...n)=>V.pullMains(...n),"pullMains"),Le=a(()=>V.stop(),"stopTries"),He=a((...n)=>V.initialize(...n),"initializeTries");export{T as BuildError,z as TRY_PORTS,he as createTryManager,Ne as fetchTryBranches,He as initializeTries,Bt as installWorktree,pt as isTryPort,me as pullBranch,Re as pullMains,Fe as pullNow,De as startBuild,Le as stopTries,Be as tryTick};
|
package/package.json
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var h=Object.defineProperty;var r=(t,e)=>h(t,"name",{value:e,configurable:!0});import{execFile as m}from"node:child_process";const k=/^[\w.-]+\/[\w.-]+$/,A=/^(@[\w.-]+\/)?[\w.][\w.-]*$/,S=/^[\w.-]+(\/[\w.-]+)*$/,b=/^[\w-]+(\.[\w-]+)*$/,u=r(t=>{throw new Error(t)},"bad"),d=r(t=>k.test(t||"")?t:u(`"${t??""}" isn't a GitHub repo such as owner/name`),"repoOf"),p=r((t,e)=>t&&!t.startsWith("-")?t:u(`${e} is missing`),"plain"),l={tag:{usage:"tag <owner/repo> <pattern>",args:r(([t,e,...s])=>s.length?u("tag takes a repo and one pattern"):[d(t),p(e,"the tag pattern")],"args"),words:r(([t,e])=>`a ${e} tag on ${t}`,"words"),async run([t,e],{exec:s}){const n=await s("git",["ls-remote","--tags",`https://github.com/${t}.git`]),a=j(e),i=[...new Set(n.split(`
|
|
2
|
+
`).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(g).at(-1);return i?{passed:!0,found:`tag ${i} is on ${t}`}:{passed:!1}}},npm:{usage:"npm <package> <version-or-range>",args:r(([t,e,...s])=>s.length?u("npm takes a package and one version or range"):[A.test(t||"")?t:u(`"${t??""}" isn't an npm package name`),p(e,"the version or range")],"args"),words:r(([t,e])=>`${t}@${e} on npm`,"words"),async run([t,e],{exec:s}){const n=(await s("npm",["view",`${t}@${e}`,"version","--json"]).catch(i=>{if(/No match found for version/.test(`${i.stdout??""}${i.stderr??""}`))return"";throw i})).trim(),o=(n?[JSON.parse(n)].flat():[]).sort(g).at(-1);return o?{passed:!0,found:`${t}@${o} is on npm`}:{passed:!1}}},run:{usage:"run <owner/repo> <workflow>",args:r(([t,...e])=>[d(t),p(e.join(" "),"the workflow")],"args"),words:r(([t,e])=>`a passing ${e} run on ${t}`,"words"),async run([t,e],{exec:s,since:n}){const a=await s("gh",["run","list","--repo",t,"--workflow",e,"--status","success","--limit","20","--json","createdAt,url"]),o=JSON.parse(a||"[]").filter(i=>String(i.createdAt)>=n).sort((i,c)=>String(i.createdAt).localeCompare(String(c.createdAt))).at(-1);return o?{passed:!0,found:`${e} passed on ${t} (${o.url})`}:{passed:!1}}},gh:{usage:"gh <owner/repo> <api-path> <field>=<value>",args([t,e,s,...n]){n.length&&u("gh takes a repo, an API path and one field=value"),(!S.test(e||"")||e.split("/").includes(".."))&&u(`"${e??""}" isn't an API path under the repo, such as actions/permissions`);const[a,...o]=String(s??"").split("=");return(!b.test(a)||!o.length)&&u(`"${s??""}" isn't a field=value, such as enabled=true`),[d(t),e,s]},words:r(([t,e,s])=>`${e} reads ${s} on ${t}`,"words"),async run([t,e,s],{exec:n}){const[a,...o]=s.split("="),i=o.join("="),c=a.split(".").reduce((f,w)=>f?.[w],JSON.parse(await n("gh",["api",`repos/${t}/${e}`])));return c!==void 0&&String(c)===i?{passed:!0,found:`${e} reads ${s} on ${t}`}:{passed:!1}}},url:{usage:"url <http(s) address> [<status> | <text>]",args([t,...e]){let s;try{s=new URL(t)}catch{u(`"${t??""}" isn't a web address`)}return["http:","https:"].includes(s.protocol)||u("url takes an http or https address"),e.length?[t,e.join(" ")]:[t]},words:r(([t,e])=>e===void 0||/^\d{3}$/.test(e)?`${t} answers ${e??200}`:`${t} shows "${e}"`,"words"),async run([t,e],{fetchImpl:s}){const n=e!==void 0&&/^\d{3}$/.test(e)?Number(e):200,a=await s(t,{redirect:"follow",signal:AbortSignal.timeout(2e4)}),o=await a.text();return a.status!==n?{passed:!1}:n===200&&e!==void 0&&!/^\d{3}$/.test(e)?o.includes(e)?{passed:!0,found:`${t} shows "${e}"`}:{passed:!1}:{passed:!0,found:`${t} answered ${n}`}}}},$=Object.values(l).map(t=>t.usage);function E(t){const[e,...s]=String(t??"").trim().split(/\s+/).filter(Boolean);e||u(`--check takes "<name> <args>": ${$.join(", ")}`);const n=Object.hasOwn(l,e)?l[e]:u(`--check has no check "${e}"; it takes ${$.join(", ")}`);try{return{name:e,args:n.args(s)}}catch(a){return u(`--check ${n.usage}: ${a.message}`)}}r(E,"parseCheck");const C=r(({name:t,args:e})=>[t,...e].join(" "),"checkText"),I=r(({name:t,args:e})=>l[t].words(e),"checkWords");function N({name:t,args:e},{since:s,exec:n=v,fetchImpl:a=fetch}={}){return l[t].run(e,{since:s,exec:n,fetchImpl:a})}r(N,"runCheck");function v(t,e){return new Promise((s,n)=>{m(t,e,{timeout:6e4,maxBuffer:16*1024*1024,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(a,o,i)=>{if(!a)return s(o);const c=String(i||"").trim().split(`
|
|
3
|
+
`).find(Boolean)?.replace(/^npm error /,"");n(Object.assign(new Error(`${t} ${e[0]} failed: ${c||a.message}`),{stdout:o,stderr:i}))})})}r(v,"execArgv");function j(t){return new RegExp(`^${t.split("").map(e=>e==="*"?".*":e==="?"?".":e.replace(/[.+^${}()|[\]\\]/g,"\\$&")).join("")}$`)}r(j,"globOf");function g(t,e){return t.localeCompare(e,"en",{numeric:!0})}r(g,"byVersion");export{$ as CHECK_USAGE,C as checkText,I as checkWords,E as parseCheck,N as runCheck};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var s=(t,l)=>p(t,"name",{value:l,configurable:!0});import n from"node:fs";import f from"node:os";import a from"node:path";const m=a.join("Tailscale.app","Contents","MacOS","Tailscale"),r=s(t=>{try{return n.statSync(t).isFile()&&(n.accessSync(t,n.constants.X_OK),!0)}catch{return!1}},"isExecutable");function j({env:t=process.env,platform:l=process.platform,apps:e=["/Applications",a.join(f.homedir(),"Applications")]}={}){for(const i of(t.PATH||"").split(a.delimiter).filter(Boolean)){const o=a.join(i,"tailscale");if(r(o))return o}return l!=="darwin"?null:e.map(i=>a.join(i,m)).find(r)??null}s(j,"tailscaleBin");const c=s((t=process.platform)=>t==="darwin"?"brew install --cask tailscale-app":"curl -fsSL https://tailscale.com/install.sh | sh","tailscaleInstallCommand"),x=s((t=process.platform)=>t==="darwin"?`install Tailscale from https://tailscale.com/download, or ${c(t)}`:c(t),"tailscaleInstallHint");export{j as tailscaleBin,c as tailscaleInstallCommand,x as tailscaleInstallHint};
|
package/shared/usage-gate.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as
|
|
1
|
+
var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as h,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as M,effectiveCodexShare as O}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),w=o(e=>Number(e),"timeOf"),G=["five_hour","seven_day"];function v({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const u=E(s)?s:90,r=o(f=>u===100?null:G.find(i=>{const c=f?.[i];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=u&&(!c.resets_at||Date.parse(c.resets_at)>w(a))})||null,"past"),l=o((f,i)=>({window:i,used:i?f[i].used_percentage:null,resets_at:i?f[i].resets_at:null}),"details"),d=r(e),p=l(e,d),C=d&&n?.window===d&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&w(a)<Date.parse(n.until),x=r(t);return{pause_at:u,state:d?C?"going":"paused":"normal",...p,codex:{past:!!x,...l(t,x)}}}o(v,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:w(t)},s.usage=$({home:e,now:o(()=>s.now,"now")}),s.codex=M({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=w(t);const n=h(e);return v({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(k,"readUsageGate");function U(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...h(t),pauseAt:Number(e)},t)}o(U,"setPauseAt");function W(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=h(t);if(e==="on"){const a=k({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(W,"setKeepGoing");const z=o(e=>{try{return["github-change","check-github","card-unblocked","overnight-triage","pr-review","owner-check-broken"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function B({home:e=g.homedir(),now:t=Date.now(),card:s={}}={}){const n=o(l=>{const{model:d,reason:p}=P(D(e),s);return{kind:"claude",model:d,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const u=O(h(e).codexShare,a);if(u==="off")return n("Codex share is off.");const r=k({home:e,now:t});return r.codex.past?n(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${u} and Codex is under the ${r.pause_at}% line.`}}o(B,"builderPick");const b={opus:"Opus",sonnet:"Sonnet"};function P(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${b[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${b[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(P,"claudePick");const H=o(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{B as builderPick,z as heldUsageEvent,H as pauseReason,k as readUsageGate,W as setKeepGoing,U as setPauseAt,v as usageGate,E as validPauseAt};
|