@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.
Files changed (51) hide show
  1. package/CONTEXT.md +32 -16
  2. package/SKILL.md +8 -6
  3. package/bin/doctor.mjs +4 -4
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +8 -8
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/bin/board.mjs +50 -28
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -3
  10. package/board/headless.mjs +4 -3
  11. package/board/merge3.mjs +1 -0
  12. package/board/on-host.mjs +1 -1
  13. package/board/permissions.mjs +2 -2
  14. package/board/public/app-asks.js +6 -89
  15. package/board/public/app-cards.js +19 -10
  16. package/board/public/app-core.js +1 -20
  17. package/board/public/app-reader.js +174 -0
  18. package/board/public/app-threads.js +49 -35
  19. package/board/public/app.html +2 -0
  20. package/board/public/app.js +28 -16
  21. package/board/public/hub.css +1 -0
  22. package/board/public/new-task.js +121 -0
  23. package/board/public/shell.js +4 -0
  24. package/board/public/sidebar.css +1 -0
  25. package/board/public/style.css +91 -3
  26. package/board/public/ui-cards.js +46 -21
  27. package/board/public/ui-enums.js +1 -1
  28. package/board/public/ui-phase-bars.js +4 -4
  29. package/board/public/ui-questions.js +47 -18
  30. package/board/public/ui-sidebar.js +9 -3
  31. package/board/public/ui-threads.js +25 -12
  32. package/board/public/ui.js +42 -12
  33. package/board/public/view-frame.js +1 -0
  34. package/board/public/view-questions.js +45 -16
  35. package/board/public/view-settings.js +38 -19
  36. package/board/server.mjs +20 -18
  37. package/board/store.mjs +15 -13
  38. package/board/worktree.mjs +20 -20
  39. package/hub/boards.mjs +4 -4
  40. package/hub/front-door.mjs +20 -0
  41. package/hub/github.mjs +3 -3
  42. package/hub/hub.mjs +9 -12
  43. package/hub/new-board.mjs +2 -2
  44. package/hub/owner-checks.mjs +1 -0
  45. package/hub/readiness.mjs +2 -2
  46. package/hub/sessions.mjs +2 -2
  47. package/hub/try.mjs +7 -7
  48. package/package.json +1 -1
  49. package/shared/owner-check.mjs +3 -0
  50. package/shared/tailscale.mjs +1 -0
  51. package/shared/usage-gate.mjs +1 -1
package/hub/try.mjs CHANGED
@@ -1,8 +1,8 @@
1
- var qt=Object.defineProperty;var o=(n,f)=>qt(n,"name",{value:f,configurable:!0});import T from"node:fs";import x from"node:path";import Tt from"node:os";import Kt from"node:http";import xt from"node:net";import{execFile as Wt,spawn as Jt}from"node:child_process";import{createInterface as Ot}from"node:readline";import{promisify as zt}from"node:util";import{listBoards as q,readManifest as K,readJSON as rt,saveJSON as It,appendEvent as Ut,GITHUB_STATE as Yt,REPO_FETCH as Ct}from"./boards.mjs";import{boardRepo as Y,githubFromOrigin as Et}from"./board-repo.mjs";import{gitReachFault as jt,remoteFromInput as Qt,remoteGitEnv as Vt}from"./repos.mjs";import{readiness as Xt}from"./readiness.mjs";import{buildScriptError as Zt}from"../board/build-script.mjs";import{appendThread as te,recordedWorktree as ee,setItem as re}from"../board/store.mjs";import{cardPRs as ie,hasPR as se,prKeyOf as ne}from"../shared/card-prs.mjs";class S extends Error{static{o(this,"BuildError")}constructor(f,y=400){super(f),this.status=y}}const dt=zt(Wt),j=o(async(n,...f)=>(await dt("git",["-C",n,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),oe=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,W={first:8500,last:8599},ut=o(n=>/^\d+$/.test(String(n))&&Number(n)>=W.first&&Number(n)<=W.last,"isTryPort"),ae=30*6e4,Mt=o(n=>x.join(n,".kickflip","build"),"scriptAt"),Pt=o(n=>T.existsSync(n),"exists"),_t=o((n,f)=>x.join(n.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function At(n,f,y){const h=_t(n,f);return T.mkdirSync(x.dirname(h),{recursive:!0}),T.writeFileSync(h,y.message+`
2
- `,{mode:384}),T.chmodSync(h,384),h}o(At,"failureLog");const ce=o(n=>new Promise(f=>{const y=Kt.get({host:"127.0.0.1",port:n,path:"/",timeout:1e3},h=>{h.destroy(),f(!0)});y.on("timeout",()=>y.destroy()),y.on("error",()=>f(!1))}),"probe"),Nt=o((n,f)=>{try{process.kill(-n.pid,f)}catch{}},"signalGroup");async function Dt(n,f,y){const h=await ee(n.dir,f),[b,u]=f.split("/"),m=n.waves.find(v=>v.slug===b)?.wave,p=h?.path||m?.items.find(v=>v.id===u)?.worktree;if(typeof p!="string"||!p||p.split(x.sep).includes(".."))return"";let k=h?.repo;const w=(n.github||m?.repo||"").toLowerCase();if(!k&&w){const v=[n.owner?.repo,n.dir,...Object.values(y?.sites||{}).map(P=>P.owner?.repo)];for(const P of new Set(v.filter(C=>typeof C=="string"&&x.isAbsolute(C))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await j(P,"remote","get-url","origin"))?.[1].toLowerCase()===w){k=await j(P,"rev-parse","--show-toplevel");break}}catch{}}if(!h?.path&&!k||h?.path&&!x.isAbsolute(p))return"";try{const v=[x.join(T.realpathSync(Tt.homedir()),".claude","kickflip","worktrees")];if(typeof k=="string"&&x.isAbsolute(k))try{v.push(x.join(T.realpathSync(k),".claude","worktrees"))}catch{}const P=x.resolve(k||"/",p),C=T.realpathSync(P);return v.some(D=>P.startsWith(D+x.sep)&&C.startsWith(D+x.sep))&&T.statSync(C).isDirectory()?C:""}catch{return""}}o(Dt,"installWorktree");function le({now:n=Date.now,answers:f=ce,log:y=console.log,waiting:h=o(async()=>null,"waiting"),event:b=Ut}={}){const u=new Map,m=new Map,p=new Set,k=new Set,w=new Map,v=new Map,P=new Map,C=new Map;let D=!1,L=null,J=null,tt=null;const it=o(()=>new Date(n()).toISOString(),"stamp");function B(t){if(u.has(t.dir))return u.get(t.dir);const e=x.join(t.dir,"data","try.json"),s=rt(e,{kind:null,main:null,cards:{}}),i={...ut(s.main_port)?{main_port:s.main_port}:{},kind:s.kind||null,main:s.main||null,cards:{...s.cards},...s.pull?{pull:{...s.pull,running:void 0}}:{}},r={state:i,file:e,written:JSON.stringify(s)};u.set(t.dir,r),i.main_port&&v.set(t.dir,i.main_port);const a=Object.entries(i.cards).filter(([,l])=>l.state==="running");for(const[l]of a)k.add(R(t,l));return i.kind==="web"?(i.cards=Object.fromEntries(a),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),A(t),r}o(B,"stateOf");function A(t){const e=u.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(It(e.file,e.state),e.written=s)}o(A,"persist");const R=o((t,e)=>`${t.dir}\0${e}`,"runKey"),ht=o((t,e)=>e==="main"?t.main:t.cards[e],"entryOf");function z(t,e,s){const{state:i}=B(t),r=ht(i,e);if(!r)return;const a=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,e!=="main"&&a?{at:it()}:{}),A(t)}o(z,"update");function st(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(s=>s.site)];for(const s of e){if(!s?.dir)continue;const i=rt(x.join(s.dir,"data","try.json"),{}).main_port;ut(i)&&v.set(s.dir,Number(i))}}o(st,"reserveMains");function Bt(t){const{board:e,key:s}=t,i=R(e,s),r=s==="main"?v.get(e.dir):w.get(i);if(r)return r;const a=new Set([...v.values(),...w.values()]);let l=W.first;for(;a.has(l)&&l<=W.last;)l++;if(l>W.last)throw new Error(`No try ports available (${W.first}\u2013${W.last}). Stop a served card and retry.`);return s==="main"?(v.set(e.dir,l),B(e).state.main_port=l,A(e)):w.set(i,l),l}o(Bt,"publicPort");const nt=o(t=>dt("tailscale",t,{encoding:"utf8",timeout:1e4}),"tailscale");async function mt(t){try{await nt(["serve",`--https=${t}`,"off"])}catch(e){y(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}o(mt,"removePort");async function Rt(t){t.https=Bt(t);const e=t.sockets=new Set,s=o(()=>{t.visited=n()},"visit");if(t.relay=xt.createServer(i=>{s();const r=xt.connect(t.port,"127.0.0.1");for(const a of[i,r])e.add(a),a.on("data",s),a.on("error",()=>{i.destroy(),r.destroy()}),a.on("close",()=>{e.delete(a),i.destroy(),r.destroy()});i.pipe(r),r.pipe(i)}),await new Promise((i,r)=>{t.relay.once("error",r),t.relay.listen(0,"127.0.0.1",i)}),!t.stopped){t.exposed=!0;try{await nt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(i){throw new Error(`Couldn't open try port ${t.https}: ${(i.stderr||i.message).trim()}`)}}}o(Rt,"openPort");async function Ft(t){if(await t.opening?.catch(()=>{}),t.exposed&&await mt(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||w.delete(R(t.board,t.key))}o(Ft,"closePort");function V(t=K()){if(tt)return tt;st(t);for(const e of q(t))B(e);return tt=(async()=>{try{const{stdout:e}=await nt(["serve","status","--json"]),s=JSON.parse(e);for(const i of Object.keys(s?.TCP||{}))ut(i)&&![...m.values()].some(r=>r.https===Number(i))&&await mt(i)}catch(e){y(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),tt}o(V,"initialize");async function wt(t,e,s,i){const{board:r,key:a}=t;if(z(r,a,{state:"failed",port:void 0,https:void 0}),a==="main")return;const l=[];let g=!1;try{for await(const d of Ot({input:T.createReadStream(t.logFile),crlfDelay:1/0}))g||=oe.test(d),l.push(d),l.length>5&&l.shift()}catch(d){l.push(`Could not read the build log: ${d.message}`)}const _=t.port||(B(r).state.kind||(r.waves.some(({wave:d})=>d.items.some(O=>O.install?.url))?"app":"web"))==="web"?"Serve":"Build";await te(r.dir,a,{text:[`${_} failed (exit ${e??1})`,i?.message,s&&`Stopped by ${s}.`,g&&"The login keychain must be unlocked on the Mac.",...l].filter(Boolean).join(`
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(v=>v.split(`
6
- `)).find(v=>v.includes(`branch refs/heads/${f}`))?.[0].slice(9);return w?(await h(w,"status","--porcelain","--untracked-files=no")&&b("local changes"),await h(w,"merge","--ff-only","--quiet",m)):await h(n,"update-ref",`refs/heads/${f}`,m,u),{from:u,to:m,commits:k}}catch(u){if(u.message.startsWith("can\u2019t pull: "))throw u;b(String(u.stderr||u.message).trim().split(`
7
- `).at(-1))}}o(de,"pullBranch");async function Ce({manifest:n=K(),log:f=console.log,routes:y}={}){const h=y?rt(Ct,{}):{};for(const b of q(n)){if(y&&!y.includes(b.route))continue;const u=Y(b.route,n);if(u.local||!u.repo)continue;const m=u.branch||"main",p=new Date().toISOString(),k=Date.now();try{await pt(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${m}:refs/remotes/origin/${m}`),h[b.route]={at:p,ok:!0}}catch(w){f(`fetch /${b.route} after ${Date.now()-k} ms: ${w.message}`);let v=b.github;if(!v&&!b.remote)try{v=Et(await j(u.repo,"remote","get-url","origin"))}catch{}h[b.route]={at:p,ok:!1,...jt(w.killed?`git fetch timed out after ${ft/1e3} s`:w.stderr,{github:v||"",remote:b.remote,repo:u.repo})};continue}if(b.remote)try{await ue(b,u.repo,m,f)}catch(w){f(`pushed branches /${b.route}: ${w.message}`)}}It(Ct,h)}o(Ce,"fetchTryBranches");async function ue(n,f,y,h){const b=n.waves.flatMap(({slug:p,wave:k})=>k.items.filter(w=>w.pushed?.branch&&!se(w)&&!["done","merged","dropped"].includes(String(w.status).toLowerCase())).map(w=>({key:`${p}/${w.id}`,branch:w.pushed.branch})));if(!b.length)return;const u=new Map((await pt(f,"ls-remote","--heads","origin")).split(`
8
- `).map(p=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(p)).filter(Boolean).map(p=>[p[2],p[1]])),m=Qt(n.remote)?.host||"the remote";for(const p of b){let k=u.get(p.branch);if(!k)try{k=await j(f,"rev-parse","--verify","--quiet",`refs/heads/${p.branch}^{commit}`)}catch{}let w=!1;if(k)try{await j(f,"merge-base","--is-ancestor",k,`refs/remotes/origin/${y}`),w=!0}catch{}if(!(!w&&u.has(p.branch)))try{await re(n.dir,p.key,{status:w?"merged":"done",force:!0,text:w?`${p.branch} is merged into ${y} on ${m}, so this card is Done.`:`${p.branch} is gone from ${m}, so this card is Done.`})}catch(v){h(`pushed ${p.key}: ${v.message}`)}}}o(ue,"settlePushed");const Q=le({waiting:o(()=>Xt.waiting("tailscale"),"waiting")}),Ee=o((...n)=>Q.start(...n),"startBuild"),je=o((...n)=>Q.tick(...n),"tryTick"),Me=o((...n)=>Q.pull(...n),"pullNow"),Pe=o((...n)=>Q.pullMains(...n),"pullMains"),_e=o(()=>Q.stop(),"stopTries"),Ae=o((...n)=>Q.initialize(...n),"initializeTries");export{S as BuildError,W as TRY_PORTS,le as createTryManager,Ce as fetchTryBranches,Ae as initializeTries,Dt as installWorktree,ut as isTryPort,de as pullBranch,Pe as pullMains,Me as pullNow,Ee as startBuild,_e as stopTries,je as tryTick};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -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};
@@ -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 x,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"),h=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 d=E(s)?s:90,r=o(f=>d===100?null:G.find(i=>{const c=f?.[i];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=d&&(!c.resets_at||Date.parse(c.resets_at)>h(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"),u=r(e),p=l(e,u),C=u&&n?.window===u&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:d,state:u?C?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(v,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(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=h(t);const n=x(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({...x(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=x(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","card-pull","card-plan","overnight-triage","pr-review"].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:u,reason:p}=P(D(e),s);return{kind:"claude",model:u,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const d=O(x(e).codexShare,a);if(d==="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 ${d} 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};
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};