@lovek4d/kickflip 0.5.3 → 0.6.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 +69 -37
- package/SKILL.md +19 -13
- package/bin/kickflip.mjs +12 -12
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +156 -83
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -5
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +27 -13
- package/board/public/app-header.js +6 -7
- package/board/public/app-lanes.js +15 -38
- package/board/public/app-needs.js +36 -31
- package/board/public/app-snapshot.js +43 -70
- package/board/public/app-threads.js +38 -46
- package/board/public/app.html +9 -4
- package/board/public/app.js +78 -32
- package/board/public/hub-kit.js +62 -2
- package/board/public/hub.css +2 -0
- package/board/public/new-task.js +8 -6
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +109 -4
- package/board/public/prototype-switcher.js +123 -29
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +64 -24
- package/board/public/sidebar.css +1 -0
- package/board/public/sidebar.js +16 -69
- package/board/public/style.css +108 -57
- package/board/public/ui-cards.js +61 -46
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-header.js +25 -16
- package/board/public/ui-hub.js +12 -1
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +25 -17
- package/board/public/ui-sidebar.js +25 -23
- package/board/public/ui-snapshot.js +43 -52
- package/board/public/ui-threads.js +57 -28
- package/board/public/ui.js +92 -17
- package/board/public/view-hub-settings.js +12 -58
- package/board/public/view-hub.js +2 -1
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +90 -68
- package/board/public/view-settings.js +133 -15
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +19 -19
- package/board/stop.mjs +1 -0
- package/board/store.mjs +27 -18
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +22 -21
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),
|
|
3
|
-
`)})}c(bt,"failure");function rt(t){return t.ending||(Ft(t.child,"SIGTERM"),t.ending=new Promise(e=>{const r=c(()=>{clearTimeout(s),clearInterval(i),e()},"finish");let s=setTimeout(()=>{Ft(t.child,"SIGKILL"),s=setTimeout(r,1e3)},4e3);const i=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{r()}},20)})),t.ending}c(rt,"terminate");async function vt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await rt(t),await t.done}c(vt,"halt");async function q(t,e=!1){await vt(t,e),V(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))}c(q,"stopRun");async function kt(t,e,r,s={}){if(!r)throw new x("This card has no valid worktree.");const i=_t(r),n=re(i);if(n)throw new x(n);if(!O.realpathSync(i).startsWith(O.realpathSync(r)+T.sep))throw new x("Build script must be inside the worktree.");const o=await M(r,"rev-parse","HEAD");if(G)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:h}=N(t),S=wt(h,e),d={...e==="main"?{}:S,...s,sha:o,script:!0,state:"running",...e==="main"?{}:{at:st(),head:o}};delete d.port,delete d.https;const E=Nt(t,e);O.mkdirSync(T.dirname(E),{recursive:!0});const j=O.openSync(E,"w",384);O.fchmodSync(j,384);const $=Vt(i,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});e==="main"?h.main=d:h.cards[e]=d,_(t);const D=B(t,e),l={child:$,board:t,key:e,logFile:E,visited:a(),stopped:!1,port:null,ready:!1};u.set(D,l),P.set(D,o);let F,I;const Q=c(A=>{I||=new Error(A),rt(l)},"fail");$.on("error",A=>{F=A}),$.stdout.on("data",A=>O.writeSync(j,A)),$.stderr.on("data",A=>O.writeSync(j,A));const H=Ct({input:$.stdout,crlfDelay:1/0});return H.on("line",A=>{const tt=/^port (\d{1,5})$/.exec(A);if(!tt||l.port||l.stopped)return;const U=Number(tt[1]);if(U<1||U>65535){Q("Build script printed an invalid port.");return}l.port=U,clearTimeout(l.deadline);const ct=a()+6e4;let lt=!1;const Et=c(async()=>{if(!(lt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){lt=!0;try{if(await f(U)){if(l.stopped||I||$.exitCode!==null||$.signalCode!==null||(l.opening=Gt(l),await l.opening,l.stopped||I||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=a(),h.kind="web",V(t,e,{state:"ready",port:U,https:l.https}),clearInterval(l.poll)}else a()>=ct&&Q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{lt=!1}}},"check");l.poll=setInterval(()=>Et().catch(dt=>Q(dt.message)),250),Et().catch(dt=>Q(dt.message))}),l.deadline=setTimeout(()=>Q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{rt(l)}),l.done=new Promise(A=>$.once("close",(tt,U)=>{clearTimeout(l.deadline),clearInterval(l.poll),H.close(),O.closeSync(j),(async()=>{await rt(l),await Ut(l),l.finished=!0,!l.stopped&&(I||F||tt!==0||l.port&&!l.ready?await bt(l,tt??1,U,I||F||(l.port&&!l.ready?new Error("Build script exited before its port answered."):null)):l.ready?(e==="main"?(delete h.main.state,delete h.main.port,delete h.main.https):delete h.cards[e],_(t)):(h.kind="app",V(t,e,{state:"ready"})))})().catch(ct=>w(`build /${t.route} ${e}: ${ct.message}`)).finally(()=>{u.get(D)===l&&u.delete(D),A()})})),w(`build /${t.route} ${e}: started; log ${E}`),{status:"started"}}c(kt,"launch");async function St(t,e,r){const s=B(t,"main");if(g.has(s))throw new x("A build is already running for main.",409);g.add(s);try{u.has(s)&&await q(u.get(s),!0);const i=T.join(jt.homedir(),".claude","kickflip","try",`${t.route}-main`);return O.mkdirSync(T.dirname(i),{recursive:!0}),Dt(i)?await M(i,"checkout","--detach","--force",r.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",i,r.sha)),await kt(t,"main",i,r)}catch(i){throw i.status!==503&&(N(t).state.main={...r,state:"failed"},Bt(t,"main",i),_(t)),i}finally{g.delete(s)}}c(St,"launchMain");async function Tt(t,{manifest:e=K()}={}){if(await Z(e),nt(e),G)throw new x("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 x("Send only the board route and card key.");const{route:r,key:s}=t;if(s!=="main"&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(d=>d==="."||d==="..")))throw new x("Card key must be <wave>/<id>.");const i=W(e).find(d=>d.route===r);if(!i)throw new x(`/${r} is not a board.`,404);if(s!=="main"&&!i.waves.some(({slug:d,wave:E})=>E.items.some(j=>`${d}/${j.id}`===s)))throw new x("There is no item with this card key.",404);const n=await m();if(n)throw new x(n,409);const o=B(i,s),h=u.get(o);if(g.has(o)||h&&!h.ready&&!h.finished)throw new x(`A build is already running for ${s==="main"?"main":"this card"}.`,409);if(s==="main"){const d=N(i).state.main;if(d?.state==="running")throw new x("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new x("Main has no failed build to retry.",409);const{sha:E,branch:j,slug:$,at:D,script:l}=d;return St(i,X(r,e).repo,{sha:E,branch:j,slug:$,at:D,script:l})}g.add(o);const S=y.delete(o);try{u.has(o)&&await q(u.get(o),!0);const d=await Rt(i,s,e);return await kt(i,s,d)}catch(d){throw b.delete(o),S&&d.status!==503&&V(i,s,{state:void 0,sha:void 0}),d}finally{g.delete(o)}}c(Tt,"start");function xt(t,e,{manifest:r=K()}={}){const s=W(r).find(n=>n.route===t);if(!s)return null;const i=[...u.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}c(xt,"lookup");async function at({manifest:t=K(),routes:e}={}){if(await Z(t),nt(t),G)return R;if(R&&e){if(!Y){const r={routes:new Set,manifest:t};r.done=R.catch(()=>{}).then(()=>(Y=null,at({manifest:r.manifest,routes:[...r.routes]}))),Y=r}Y.manifest=t;for(const r of e)Y.routes.add(r);return Y.done}return R||(R=(async()=>{const r=W(t),s=et(Zt,{}).prs||{};for(const i of[...u.values()])if(!r.some(n=>n.dir===i.board.dir)||i.key!=="main"&&i.ready&&a()-i.visited>=we){await q(i);const{state:n}=N(i.board);i.key==="main"?n.main=null:delete n.cards[i.key],_(i.board)}for(const i of r){if(G)return;if(e&&!e.includes(i.route))continue;const{state:n}=N(i),o=X(i.route,t),h=o.branch||"main",S=o.local?`refs/heads/${h}`:`refs/remotes/origin/${h}`;for(const{slug:d,wave:E}of i.waves)for(const j of E.items){const $=`${d}/${j.id}`,D=ae(j,i.github||E.repo).map(l=>s[le(l)]);if(!(["done","merged","dropped"].includes(String(j.status).toLowerCase())||D.length&&D.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=B(i,$),F=await Rt(i,$,t);if(F){const H=await M(F,"rev-parse","HEAD");let A=0;try{A=Number(await M(F,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(n.cards[$]||={},{head:H,ahead:A,script:Dt(_t(F))}),_(i)}else if(!y.has(l))continue;const I=n.cards[$];if((y.has(l)||i.auto_build&&I.script&&I.ahead>0&&(I.sha||P.get(l))!==I.head&&I.state!=="running")&&!g.has(l))try{await Tt({route:i.route,key:$},{manifest:t})}catch(H){if([409,503].includes(H.status))continue;throw V(i,$,{sha:I.head}),await bt({board:i,key:$,logFile:Bt(i,$,H)},1),H}}catch(l){w(`try /${i.route} ${$}: ${l.message}`)}}try{if(!o.repo||await m())continue;const d=await M(o.repo,"rev-parse",S);let E=!1;try{await M(o.repo,"cat-file","-e",`${d}:.kickflip/build`),E=!0}catch{}if(!E){const I=u.get(B(i,"main"));I&&await q(I),n.main=null,_(i);continue}const j=n.main;if(j?.state==="running"||j?.sha===d&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||xt(i.route,"main",{manifest:t}))))continue;const $=B(i,"main");if(g.has($))continue;const D=await M(o.repo,"log","-1","--format=%cI",d);let l=i.github;if(!l)try{l=Pt(await M(o.repo,"remote","get-url","origin"))}catch{}const F={sha:d,branch:h,slug:l||"",at:D,script:E};await St(i,o.repo,F)}catch(d){w(`try /${i.route} main: ${d.message}`)}}})().finally(()=>{R=null}),R)}c(at,"tick");function Ot(t,e){if(L.has(t.dir))return L.get(t.dir);const r=(async()=>{const s=X(t.route,e),i=s.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},_(t);let o,h;try{o=await $e(s.repo,i)}catch(S){h=S.message}if(o===null?delete n.pull:n.pull={at:st(),...h?{error:h}:{}},_(t),o===null)return null;if(h)return w(`pull /${t.route}: ${h}`),{commits:0,error:h};if(o.commits){w(`pull /${t.route}: ${i} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await v(t,"main-pulled",{branch:i,...o})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:o.commits}})().finally(()=>L.delete(t.dir));return L.set(t.dir,r),r}c(Ot,"pullMain");async function Wt({manifest:t=K(),routes:e}={}){await Z(t);const r=[];for(const s of W(t)){if(G)break;if(e&&!e.includes(s.route))continue;const i=X(s.route,t);!i.local||!i.repo||(await Ot(s,t))?.commits&&r.push(s.route)}return r}c(Wt,"pullMains");async function Kt(t,{manifest:e=K()}={}){if(await Z(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const r=W(e).find(n=>n.route===t.route);if(!r)throw new x(`/${t.route} is not a board.`,404);const s=X(r.route,e);if(!s.local||!s.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const i=await Ot(r,e);if(!i)throw new x("This board\u2019s folder has no origin to pull from.",409);return i.commits&&at({manifest:e,routes:[r.route]}).catch(n=>w(`try /${r.route} main: ${n.message}`)),i}c(Kt,"pull");async function zt(){G=!0;const t=Promise.all([...u.values()].map(e=>e.ready?q(e):vt(e)));await Promise.all([t,R,...L.values()])}return c(zt,"stop"),{start:Tt,tick:at,stop:zt,lookup:xt,initialize:Z,pull:Kt,pullMains:Wt,endUnshared:Ht}}c(ge,"createTryManager");const ht=1e4,mt=c(async(a,...f)=>(await ft("git",["-C",a,...f],{encoding:"utf8",timeout:ht,env:te()})).stdout,"originGit");async function $e(a,f,{run:w=mt}={}){const m=c(async(...p)=>(await w(...p)).trim(),"git"),v=c(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await m(a,"config","--get","remote.origin.url")}catch{return null}try{await m(a,"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`:At(p.stderr,{repo:a}).error)}try{const p=await m(a,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await m(a,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=c((b,C)=>m(a,"merge-base","--is-ancestor",b,C).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||v(`${f} has split from origin`);const g=Number(await m(a,"rev-list","--count",`${p}..${k}`)),y=(await m(a,"worktree","list","--porcelain")).split(`
|
|
1
|
+
var ee=Object.defineProperty;var a=(c,f)=>ee(c,"name",{value:f,configurable:!0});import T from"node:fs";import O from"node:path";import Nt from"node:os";import mt from"node:http";import{execFile as re,spawn as ie}from"node:child_process";import{createInterface as Dt}from"node:readline";import{promisify as se}from"node:util";import{listBoards as U,readManifest as z,readJSON as st,saveJSON as Bt,appendEvent as ne,GITHUB_STATE as oe,REPO_FETCH as Rt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Ft,mainRefOf as ae}from"./board-repo.mjs";import{gitReachFault as Ht,remoteFromInput as ce,remoteGitEnv as le}from"./repos.mjs";import{readiness as de}from"./readiness.mjs";import{tailscaleBin as ue}from"../shared/tailscale.mjs";import{buildScriptError as fe,buildScriptOf as Gt}from"../board/build-script.mjs";import{appendThread as pe,recordedWorktree as he,servedRoundsOpen as me,setItem as we}from"../board/store.mjs";import{BASE_KEY as J,baseWorktreeOf as ye}from"../board/prototype-base.mjs";import{cardPRs as ge,hasPR as $e,prKeyOf as ve}from"../shared/card-prs.mjs";import{hubGate as be}from"../board/on-host.mjs";import{pipeUpgrade as ke,refuse as Se,refuseUpgrade as Te}from"./front-door.mjs";import{shareOf as Oe}from"./share.mjs";class x extends Error{static{a(this,"BuildError")}constructor(f,w=400){super(f),this.status=w}}const wt=se(re),C=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),xe=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,Y={first:8500,last:8599},yt=a(c=>/^\d+$/.test(String(c))&&Number(c)>=Y.first&&Number(c)<=Y.last,"isTryPort"),Ee=30*6e4,at={main:"main",[J]:"prototypes"},F=a(c=>!Object.hasOwn(at,c),"isCard"),Lt=a((c,f)=>O.join(c,Gt(f)),"scriptAt"),gt=a(c=>T.existsSync(c),"exists"),Wt=a((c,f)=>O.join(c.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function $t(c,f,w){const h=Wt(c,f);return T.mkdirSync(O.dirname(h),{recursive:!0}),T.writeFileSync(h,w.message+`
|
|
2
|
+
`,{mode:384}),T.chmodSync(h,384),h}a($t,"failureLog");const _e=a(c=>new Promise(f=>{const w=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},h=>{h.destroy(),f(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>f(!1))}),"probe"),Kt=a((c,f)=>{try{process.kill(-c.pid,f)}catch{}},"signalGroup");async function Ut(c,f,w){const h=await he(c.dir,f),[b,p]=f.split("/"),k=c.waves.find(v=>v.slug===b)?.wave,u=h?.path||k?.items.find(v=>v.id===p)?.worktree;if(typeof u!="string"||!u||u.split(O.sep).includes(".."))return"";let y=h?.repo;const g=(c.github||k?.repo||"").toLowerCase();if(!y&&g){const v=[c.owner?.repo,c.dir,...Object.values(w?.sites||{}).map(j=>j.owner?.repo)];for(const j of new Set(v.filter(M=>typeof M=="string"&&O.isAbsolute(M))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await C(j,"remote","get-url","origin"))?.[1].toLowerCase()===g){y=await C(j,"rev-parse","--show-toplevel");break}}catch{}}if(!h?.path&&!y||h?.path&&!O.isAbsolute(u))return"";try{const v=[O.join(T.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof y=="string"&&O.isAbsolute(y))try{v.push(O.join(T.realpathSync(y),".claude","worktrees"))}catch{}const j=O.resolve(y||"/",u),M=T.realpathSync(j);return v.some(G=>j.startsWith(G+O.sep)&&M.startsWith(G+O.sep))&&T.statSync(M).isDirectory()?M:""}catch{return""}}a(Ut,"installWorktree");function je({now:c=Date.now,answers:f=_e,log:w=console.log,waiting:h=a(async()=>null,"waiting"),event:b=ne,gate:p=be}={}){const k=new Map,u=new Map,y=new Set,g=new Set,v=new Map,j=new Map,M=new Map,G=new Map;let W=!1,H=null,Q=null,nt=null;const ct=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=O.join(t.dir,"data","try.json"),i=st(e,{kind:null,main:null,cards:{}}),s={...yt(i.main_port)?{main_port:i.main_port}:{},kind:i.kind||null,main:i.main||null,cards:{...i.cards},...i.pull?{pull:{...i.pull,running:void 0}}:{}},r={state:s,file:e,written:JSON.stringify(i)};k.set(t.dir,r),s.main_port&&j.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,o])=>o.state==="running");for(const[o]of n)g.add(D(t,o));return s.kind==="web"?(s.cards=Object.fromEntries(n),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),i.prototypes?.state==="failed"&&(s.prototypes=i.prototypes),A(t),r}a(N,"stateOf");function A(t){const e=k.get(t.dir),i=JSON.stringify(e.state);i!==e.written&&(Bt(e.file,e.state),e.written=i)}a(A,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),kt=a((t,e)=>F(e)?t.cards[e]:t[at[e]],"entryOf"),zt=a((t,e,i)=>{F(e)?t.cards[e]=i:t[at[e]]=i},"setEntry"),St=a((t,e)=>{F(e)?delete t.cards[e]:t[at[e]]=null},"dropEntry");function X(t,e,i){const{state:s}=N(t),r=kt(s,e);if(!r)return;const n=Object.hasOwn(i,"state")&&i.state!==r.state;Object.assign(r,i,F(e)&&n?{at:ct()}:{}),A(t)}a(X,"update");function lt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(i=>i.site)];for(const i of e){if(!i?.dir)continue;const s=st(O.join(i.dir,"data","try.json"),{}).main_port;yt(s)&&j.set(i.dir,Number(s))}}a(lt,"reserveMains");function Jt(t){const{board:e,key:i}=t,s=D(e,i),r=i==="main"?j.get(e.dir):v.get(s);if(r)return r;const n=new Set([...j.values(),...v.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return i==="main"?(j.set(e.dir,o),N(e).state.main_port=o,A(e)):v.set(s,o),o}a(Jt,"publicPort");const dt=a(async t=>{const e=ue();if(!e)throw new Error("Tailscale isn't installed on this host");return wt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Tt(t){try{await dt(["serve",`--https=${t}`,"off"])}catch(e){w(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Tt,"removePort");const Ot=a(t=>Oe(st(O.join(t.dir,"board.json"),{})),"sharedWith");async function xt(t,e){const i=await p(t),s=t.headers["tailscale-user-login"];return i!=="other"||!s||!Ot(e).includes(s)?i:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(xt,"relayVerdict");function Yt(){for(const t of u.values()){if(!t.shared?.size)continue;const e=Ot(t.board);for(const i of t.shared)e.includes(i.login)||(t.shared.delete(i),i.res.destroy())}}a(Yt,"endUnshared");async function Vt(t){t.https=Jt(t);const e=t.sockets=new Set,i=a(()=>{t.visited=c()},"visit"),s=a(n=>{e.add(n),n.on("data",i),n.on("close",()=>e.delete(n))},"track"),r=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const m=await xt(n,t.board);if(m!=="owner"&&m!=="shared"){Se(n,o,m);return}if(m==="shared"){const d={login:n.headers["tailscale-user-login"],res:o};r.add(d),o.on("close",()=>r.delete(d))}const S=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:n.headers},d=>{o.writeHead(d.statusCode,d.headers),d.on("data",i),d.pipe(o),d.on("close",()=>{d.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||S.destroy()}),S.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(S)}),t.relay.on("connection",n=>{i(),s(n)}),t.relay.on("upgrade",async(n,o,m)=>{const S=await xt(n,t.board);if(S!=="owner"){Te(o,S==="shared"?"view-only":S);return}s(ke({req:n,socket:o,head:m,port:t.port,path:n.url}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await dt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(Vt,"openPort");async function Qt(t){if(await t.opening?.catch(()=>{}),t.exposed&&await Tt(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||v.delete(D(t.board,t.key))}a(Qt,"closePort");function rt(t=z()){if(nt)return nt;lt(t);for(const e of U(t))N(e);return nt=(async()=>{try{const{stdout:e}=await dt(["serve","status","--json"]),i=JSON.parse(e);for(const s of Object.keys(i?.TCP||{}))yt(s)&&![...u.values()].some(r=>r.https===Number(s))&&await Tt(s)}catch(e){w(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),nt}a(rt,"initialize");async function Et(t,e,i,s){const{board:r,key:n}=t;if(X(r,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;const o=[];let m=!1;try{for await(const d of Dt({input:T.createReadStream(t.logFile),crlfDelay:1/0}))m||=xe.test(d),o.push(d),o.length>5&&o.shift()}catch(d){o.push(`Could not read the build log: ${d.message}`)}const S=t.port||(N(r).state.kind||(r.waves.some(({wave:d})=>d.items.some(_=>_.install?.url))?"app":"web"))==="web"?"Serve":"Build";await pe(r.dir,n,{text:[`${S} failed (exit ${e??1})`,s?.message,i&&`Stopped by ${i}.`,m&&"The login keychain must be unlocked on the Mac.",...o].filter(Boolean).join(`
|
|
3
|
+
`)})}a(Et,"failure");function ot(t){return t.ending||(Kt(t.child,"SIGTERM"),t.ending=new Promise(e=>{const i=a(()=>{clearTimeout(s),clearInterval(r),e()},"finish");let s=setTimeout(()=>{Kt(t.child,"SIGKILL"),s=setTimeout(i,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{i()}},20)})),t.ending}a(ot,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ot(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||M.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,i,s={}){if(!i)throw new x("This card has no valid worktree.");const r=Lt(i,t.build_target),n=fe(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(i)+O.sep))throw new x("Build script must be inside the worktree.");const o=await C(i,"rev-parse","HEAD");if(W)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:m}=N(t),S=kt(m,e),d={...F(e)?S:{},...s,sha:o,script:!0,state:"running",...F(e)?{at:ct(),head:o}:{}};delete d.port,delete d.https;const _=Wt(t,e);T.mkdirSync(O.dirname(_),{recursive:!0});const E=T.openSync(_,"w",384);T.fchmodSync(E,384);const $=ie(r,[e,i],{cwd:i,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(m,e,d),A(t);const B=D(t,e),l={child:$,board:t,key:e,logFile:_,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),M.set(B,o);let R,I;const q=a(P=>{I||=new Error(P),ot(l)},"fail");$.on("error",P=>{R=P}),$.stdout.on("data",P=>T.writeSync(E,P)),$.stderr.on("data",P=>T.writeSync(E,P));const L=Dt({input:$.stdout,crlfDelay:1/0});return L.on("line",P=>{const it=/^port (\d{1,5})$/.exec(P);if(!it||l.port||l.stopped)return;const K=Number(it[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const At=a(async()=>{if(!(pt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){pt=!0;try{if(await f(K)){if(l.stopped||I||$.exitCode!==null||$.signalCode!==null||(l.opening=Vt(l),await l.opening,l.stopped||I||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=c(),m.kind="web",X(t,e,{state:"ready",port:K,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>At().catch(ht=>q(ht.message)),250),At().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{ot(l)}),l.done=new Promise(P=>$.once("close",(it,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(E),(async()=>{if(await ot(l),await Qt(l),l.finished=!0,l.stopped)return;const tt=e===J&&!l.port?new Error('Build script exited without printing "port <n>".'):null;I||R||it!==0||l.port&&!l.ready||tt?await Et(l,it??1,K,I||R||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete m.main.state,delete m.main.port,delete m.main.https):St(m,e),A(t)):(m.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>w(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{u.get(B)===l&&u.delete(B),P()})})),w(`build /${t.route} ${e}: started; log ${_}`),{status:"started"}}a(jt,"launch");async function It(t,e,i){const s=D(t,"main");if(y.has(s))throw new x("A build is already running for main.",409);y.add(s);try{u.has(s)&&await Z(u.get(s),!0);const r=O.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(O.dirname(r),{recursive:!0}),gt(r)?await C(r,"checkout","--detach","--force",i.sha):(await C(e,"worktree","prune"),await C(e,"worktree","add","--detach",r,i.sha)),await jt(t,"main",r,i)}catch(r){throw r.status!==503&&(N(t).state.main={...i,state:"failed"},$t(t,"main",r),A(t)),r}finally{y.delete(s)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await rt(e),lt(e),W)throw new x("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 x("Send only the board route and card key.");const{route:i,key:s}=t;if(F(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(d=>d==="."||d==="..")))throw new x("Card key must be <wave>/<id>.");const r=U(e).find(d=>d.route===i);if(!r)throw new x(`/${i} is not a board.`,404);if(F(s)&&!r.waves.some(({slug:d,wave:_})=>_.items.some(E=>`${d}/${E.id}`===s)))throw new x("There is no item with this card key.",404);const n=await h();if(n)throw new x(n,409);const o=D(r,s),m=u.get(o);if(y.has(o)||m&&!m.ready&&!m.finished)throw new x(`A build is already running for ${s==="main"?"main":s===J?"the prototype site":"this card"}.`,409);if(s==="main"){const d=N(r).state.main;if(d?.state==="running")throw new x("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new x("Main has no failed build to retry.",409);const{sha:_,branch:E,slug:$,at:B,script:l}=d;return It(r,et(i,e).repo,{sha:_,branch:E,slug:$,at:B,script:l,...r.build_target?{target:r.build_target}:{}})}y.add(o);const S=g.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=s===J?Ct(r):await Ut(r,s,e);return await jt(r,s,d)}catch(d){throw v.delete(o),S&&d.status!==503&&X(r,s,{state:void 0,sha:void 0}),d}finally{y.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=ye(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function Xt(t,e){const{state:i}=N(t),s=D(t,J),r=u.get(s);if(!((await me(t.dir)).length>0&&Ct(t))){r&&await Z(r),i.prototypes&&(i.prototypes=null,A(t));return}if(!(r||y.has(s)||i.prototypes?.state==="failed"||await h()))try{await ut({route:t.route,key:J},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw i.prototypes={state:"failed"},$t(t,J,o),A(t),o}}a(Xt,"serveBase");function Mt(t,e,{manifest:i=z()}={}){const s=U(i).find(n=>n.route===t);if(!s)return null;const r=[...u.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Mt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await rt(t),lt(t),W)return H;if(H&&e){if(!Q){const i={routes:new Set,manifest:t};i.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:i.manifest,routes:[...i.routes]}))),Q=i}Q.manifest=t;for(const i of e)Q.routes.add(i);return Q.done}return H||(H=(async()=>{const i=U(t),s=st(oe,{}).prs||{};for(const r of[...u.values()])if(!i.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=Ee){await Z(r);const{state:n}=N(r.board);St(n,r.key),A(r.board)}for(const r of i){if(W)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),m=o.branch||"main",S=ae(o);for(const{slug:d,wave:_}of r.waves)for(const E of _.items){const $=`${d}/${E.id}`,B=ge(E,r.github||_.repo).map(l=>s[ve(l)]);if(!(E.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,$),R=await Ut(r,$,t);if(R){const L=await C(R,"rev-parse","HEAD");let P=0;try{P=Number(await C(R,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(n.cards[$]||={},{head:L,ahead:P,script:gt(Lt(R,r.build_target))}),A(r)}else if(!g.has(l))continue;const I=n.cards[$];if((g.has(l)||r.auto_build&&I.script&&I.ahead>0&&(I.sha||M.get(l))!==I.head&&I.state!=="running")&&!y.has(l))try{await ut({route:r.route,key:$},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,$,{sha:I.head}),await Et({board:r,key:$,logFile:$t(r,$,L)},1),L}}catch(l){w(`try /${r.route} ${$}: ${l.message}`)}}await Xt(r,t).catch(d=>w(`try /${r.route} prototypes: ${d.message}`));try{if(!o.repo||await h())continue;const d=await C(o.repo,"rev-parse",S);let _=!1;try{await C(o.repo,"cat-file","-e",`${d}:${Gt(r.build_target)}`),_=!0}catch{}if(!_){const I=u.get(D(r,"main"));I&&await Z(I),n.main=null,A(r);continue}const E=n.main;if(E?.state==="running"||E?.sha===d&&(E.target||null)===(r.build_target||null)&&(E.state==="failed"||E.state==="ready"&&(n.kind!=="web"||Mt(r.route,"main",{manifest:t}))))continue;const $=D(r,"main");if(y.has($))continue;const B=await C(o.repo,"log","-1","--format=%cI",d);let l=r.github;if(!l)try{l=Ft(await C(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:m,slug:l||"",at:B,script:_,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,R)}catch(d){w(`try /${r.route} main: ${d.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function Pt(t,e){if(G.has(t.dir))return G.get(t.dir);const i=(async()=>{const s=et(t.route,e),r=s.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},A(t);let o,m;try{o=await Ie(s.repo,r)}catch(S){m=S.message}if(o===null?delete n.pull:n.pull={at:ct(),...m?{error:m}:{}},A(t),o===null)return null;if(m)return w(`pull /${t.route}: ${m}`),{commits:0,error:m};if(o.commits){w(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await b(t,"main-pulled",{branch:r,...o})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:o.commits}})().finally(()=>G.delete(t.dir));return G.set(t.dir,i),i}a(Pt,"pullMain");async function Zt({manifest:t=z(),routes:e}={}){await rt(t);const i=[];for(const s of U(t)){if(W)break;if(e&&!e.includes(s.route))continue;const r=et(s.route,t);!r.local||!r.repo||(await Pt(s,t))?.commits&&i.push(s.route)}return i}a(Zt,"pullMains");async function qt(t,{manifest:e=z()}={}){if(await rt(e),W)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const i=U(e).find(n=>n.route===t.route);if(!i)throw new x(`/${t.route} is not a board.`,404);const s=et(i.route,e);if(!s.local||!s.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await Pt(i,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[i.route]}).catch(n=>w(`try /${i.route} main: ${n.message}`)),r}a(qt,"pull");async function te(){W=!0;const t=Promise.all([...u.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...G.values()])}return a(te,"stop"),{start:ut,tick:ft,stop:te,lookup:Mt,initialize:rt,pull:qt,pullMains:Zt,endUnshared:Yt}}a(je,"createTryManager");const vt=1e4,bt=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:vt,env:le()})).stdout,"originGit");async function Ie(c,f,{run:w=bt}={}){const h=a(async(...p)=>(await w(...p)).trim(),"git"),b=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await h(c,"config","--get","remote.origin.url")}catch{return null}try{await h(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){b(p.killed?`git fetch timed out after ${vt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await h(c,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await h(c,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=a((v,j)=>h(c,"merge-base","--is-ancestor",v,j).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||b(`${f} has split from origin`);const y=Number(await h(c,"rev-list","--count",`${p}..${k}`)),g=(await h(c,"worktree","list","--porcelain")).split(`
|
|
4
4
|
|
|
5
|
-
`).map(
|
|
6
|
-
`)).find(
|
|
7
|
-
`).at(-1))}}
|
|
8
|
-
`).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=
|
|
5
|
+
`).map(v=>v.split(`
|
|
6
|
+
`)).find(v=>v.includes(`branch refs/heads/${f}`))?.[0].slice(9);return g?(await h(g,"status","--porcelain","--untracked-files=no")&&b("local changes"),await h(g,"merge","--ff-only","--quiet",k)):await h(c,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:y}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;b(String(p.stderr||p.message).trim().split(`
|
|
7
|
+
`).at(-1))}}a(Ie,"pullBranch");async function Ze({manifest:c=z(),log:f=console.log,routes:w}={}){const h=w?st(Rt,{}):{};for(const b of U(c)){if(w&&!w.includes(b.route))continue;const p=et(b.route,c);if(p.local||!p.repo)continue;const k=p.branch||"main",u=new Date().toISOString(),y=Date.now();try{await bt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),h[b.route]={at:u,ok:!0}}catch(g){f(`fetch /${b.route} after ${Date.now()-y} ms: ${g.message}`);let v=b.github;if(!v&&!b.remote)try{v=Ft(await C(p.repo,"remote","get-url","origin"))}catch{}h[b.route]={at:u,ok:!1,...Ht(g.killed?`git fetch timed out after ${vt/1e3} s`:g.stderr,{github:v||"",remote:b.remote,repo:p.repo})};continue}if(b.remote)try{await Ce(b,p.repo,k,f)}catch(g){f(`pushed branches /${b.route}: ${g.message}`)}}Bt(Rt,h)}a(Ze,"fetchTryBranches");async function Ce(c,f,w,h){const b=c.waves.flatMap(({slug:u,wave:y})=>y.items.filter(g=>g.pushed?.branch&&!$e(g)&&g.lane!=="done").map(g=>({key:`${u}/${g.id}`,branch:g.pushed.branch})));if(!b.length)return;const p=new Map((await bt(f,"ls-remote","--heads","origin")).split(`
|
|
8
|
+
`).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=ce(c.remote)?.host||"the remote";for(const u of b){let y=p.get(u.branch);if(!y)try{y=await C(f,"rev-parse","--verify","--quiet",`refs/heads/${u.branch}^{commit}`)}catch{}let g=!1;if(y)try{await C(f,"merge-base","--is-ancestor",y,`refs/remotes/origin/${w}`),g=!0}catch{}if(!(!g&&p.has(u.branch)))try{await we(c.dir,u.key,{done:!0,force:!0,text:g?`${u.branch} is merged into ${w} on ${k}, so this card is Done.`:`${u.branch} is gone from ${k}, so this card is Done.`})}catch(v){h(`pushed ${u.key}: ${v.message}`)}}}a(Ce,"settlePushed");const V=je({waiting:a(()=>de.waiting("tailscale"),"waiting")}),qe=a((...c)=>V.start(...c),"startBuild"),tr=a((...c)=>V.tick(...c),"tryTick"),er=a((...c)=>V.pull(...c),"pullNow"),rr=a((...c)=>V.pullMains(...c),"pullMains"),ir=a(()=>V.stop(),"stopTries"),sr=a(()=>V.endUnshared(),"endUnsharedTries"),nr=a((...c)=>V.initialize(...c),"initializeTries");export{x as BuildError,Y as TRY_PORTS,je as createTryManager,sr as endUnsharedTries,Ze as fetchTryBranches,nr as initializeTries,Ut as installWorktree,yt as isTryPort,Ie as pullBranch,rr as pullMains,er as pullNow,qe as startBuild,ir as stopTries,tr as tryTick};
|
package/hub/upgrade.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=Object.defineProperty;var t=(e,i)=>d(e,"name",{value:i,configurable:!0});import c from"node:fs";import f from"node:path";const h="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",j=60*6e4,p=t(e=>String(e).split("-")[0].split(".").map(Number),"parts");function y(e,i){const[a,s]=[p(e),p(i)];for(let n=0;n<3;n++)if((a[n]||0)!==(s[n]||0))return(a[n]||0)>(s[n]||0);return!1}t(y,"newer");function _({root:e,fetch:i=globalThis.fetch,run:a,log:s=t(()=>{},"log"),pulled:n=t(()=>{},"pulled")}){const u=c.existsSync(f.join(e,".git"));let l=!1;const g=t(()=>JSON.parse(c.readFileSync(f.join(e,"package.json"),"utf8")).version,"installed");async function k(){if(u)return;let r;try{const o=await i(h,{signal:AbortSignal.timeout(15e3)});if(!o.ok)throw new Error(`npm answered ${o.status}`);r=(await o.json()).version}catch(o){s(`kickflip update check failed: ${o.message}`);return}y(r,g())&&m()}t(k,"check");function m(){l||(l=!0,a().then(({ok:r})=>{r&&n()},r=>s(`kickflip update failed: ${r.message}`)).finally(()=>{l=!1}))}return t(m,"pull"),{check:k}}t(_,"upgrader");export{j as CHECK_MS,h as LATEST_URL,y as newer,_ as upgrader};
|
package/hub/usage.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var N=Object.defineProperty;var
|
|
2
|
-
`);
|
|
3
|
-
`),f.renameSync(
|
|
1
|
+
var N=Object.defineProperty;var c=(e,t)=>N(e,"name",{value:t,configurable:!0});import f from"node:fs";import y from"node:os";import d from"node:path";import{audit as w,hubAudit as S}from"../shared/audit.mjs";import{FOREVER as k}from"./duration.mjs";const g=6,O=180,$={five_hour:5*36e5,seven_day:7*864e5},_=c(e=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return null}},"readJson"),h=c(e=>e==null||e===""||!Number.isFinite(Number(e))?null:Number(e),"num"),b=c(e=>{if(e==null||e==="")return null;const r=typeof e=="number"||/^\d+(\.\d+)?$/.test(String(e))?Number(e)>1e12?Number(e):Number(e)*1e3:Date.parse(String(e));return Number.isFinite(new Date(r).getTime())?new Date(r).toISOString():null},"isoOf"),j=c((e,t)=>({used_percentage:h(e),resets_at:b(t)}),"win");function D(e){let t;try{t=f.openSync(e,"r");const r=f.fstatSync(t).size;for(const s of[256*1024,2*1024*1024]){const n=Math.max(0,r-s),a=Buffer.alloc(r-n);f.readSync(t,a,0,a.length,n);const o=a.toString("utf8").split(`
|
|
2
|
+
`);n>0&&o.shift();for(let u=o.length-1;u>=0;u--)if(o[u].includes("rate_limits"))try{const m=JSON.parse(o[u]),i=m?.payload?.rate_limits??m?.rate_limits;if(i?.primary)return{rl:i}}catch{}if(n===0)break}}catch{}finally{if(t!==void 0)try{f.closeSync(t)}catch{}}return null}c(D,"lastRateLimits");function v(e){const t=[],r=c(s=>{try{return f.readdirSync(s).sort().reverse()}catch{return[]}},"kids");e:for(const s of r(e))for(const n of r(d.join(e,s)))for(const a of r(d.join(e,s,n))){const o=d.join(e,s,n,a);for(const u of r(o))if(/^rollout-.*\.jsonl$/.test(u))try{t.push({file:d.join(o,u),mtime:f.statSync(d.join(o,u)).mtimeMs})}catch{}if(t.length>=g*2)break e}return t.sort((s,n)=>n.mtime-s.mtime).slice(0,g)}c(v,"newestRollouts");function F(e){const t=[];for(const{file:i,mtime:l}of v(e)){const p=D(i);p&&t.push({...p,mtime:l,resets:h(p.rl.primary.resets_at)})}if(!t.length)return null;const r=Math.max(...t.map(i=>i.resets??-1/0)),s=t.filter(i=>i.resets!=null&&Math.abs(i.resets-r)<=O),n=c(i=>s.reduce((l,p)=>(h(p.rl[i]?.used_percent)??-1)>(h(l?.rl[i]?.used_percent)??-1)?p:l,null),"pick");if(!s.length)return null;const a=n("primary"),o=n("secondary"),u=s.reduce((i,l)=>l.mtime>i.mtime?l:i),m=c((i,l)=>i?j(i.rl[l].used_percent,i.rl[l].resets_at):null,"w");return{five_hour:m(a,"primary"),seven_day:m(o,"secondary"),at:new Date(u.mtime).toISOString()}}c(F,"scanCodex");function x(e,t){if(!e||typeof e!="object"||Array.isArray(e))return null;const r=b(e.at),s=c(n=>{const a=e[n];if(!a)return null;const o=j(a.used_percentage,a.resets_at);if(o.used_percentage==null)return null;const u=!r||t-Date.parse(r)>$[n]?"stale":o.resets_at&&t>=Date.parse(o.resets_at)?"reset":"live";return{...o,used_percentage:u==="reset"?0:o.used_percentage,state:u}},"window");return{at:r,five_hour:s("five_hour"),seven_day:s("seven_day")}}c(x,"readingOf");function J({home:e=y.homedir(),now:t=Date.now,cacheMs:r=6e4}={}){const s=d.join(e,".codex","sessions");let n=null,a=-1/0;return({codexShare:o="off"}={})=>{const u=t(),m=x(_(d.join(e,".claude","kickflip","usage.json"))?.claude,u);return o==="off"?{claude:m,codex:null}:(u-a>=r&&(n=F(s),a=u),{claude:m,codex:x(n||{at:null},u)})}}c(J,"usageReader");const I=95,L=c((e,t)=>e==="deaf"&&t?.percent>=I,"contextFull");function W(e,{home:t=y.homedir()}={}){if(typeof e!="string"||!/^[\w-]+$/.test(e))return null;const r=_(d.join(t,".claude","kickflip","context",`${e}.json`));return h(r?.percent)==null||!r.at?null:{percent:h(r.percent),at:r.at}}c(W,"contextReading");async function z({gate:e,home:t=y.homedir(),resume:r}){const s=d.join(t,".claude","kickflip","usage-gate-state.json"),n=_(s);if(e.state==="paused"&&n?.state!=="paused"){const o=e.manual?`The owner paused the hub ${e.resets_at===k?"until they resume it":`until ${e.resets_at}`}.`:`Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage reached the ${e.pause_at}% line.`;w(S(t),{call:"usage-pause",result:"paused",reason:o,inputs:{gate:e,previous:n?.state??null}})}if(n?.state==="paused"&&e.state==="normal"){const o=n.manual?"The owner's pause of the hub ended, so every board was told.":`Claude's ${n.window==="five_hour"?"5-hour":"weekly"} usage is under the line again, so every board was told.`;w(S(t),{call:"usage-resume",result:"resumed",reason:o,inputs:{gate:e,previous:n.manual?"manual":n.window}}),await r(n.manual?"manual":n.window)}f.mkdirSync(d.dirname(s),{recursive:!0});const a=`${s}.${process.pid}.tmp`;f.writeFileSync(a,JSON.stringify(e)+`
|
|
3
|
+
`),f.renameSync(a,s)}c(z,"checkUsagePause");export{I as CONTEXT_FULL,z as checkUsagePause,L as contextFull,W as contextReading,J as usageReader};
|
package/hub/views.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var F=Object.defineProperty;var s=(e,t)=>F(e,"name",{value:t,configurable:!0});import{clock as d,holdLine as S,sendWords as V,snoozeEnd as y}from"./schedule.mjs";import{CHOICES as H,FOREVER as $,durationEnd as W,endOf as M}from"./duration.mjs";import{siteIcon as B}from"./icons.mjs";import{codexStatus as G}from"../shared/codex.mjs";const Z=s(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),O=s((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),p=s(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),d(o,o)},"timeOfDay"),j=s(e=>`checks every ${e.every===1?"minute":`${e.every} min`} while you\u2019re ${e.present?"here":"away"}${e.low?", slower while GitHub\u2019s points are low":""}`,"cadenceLine"),J=s((e,t,n)=>`${j(t)}${e.hold?`; ${S(e.hold,n)}`:""}`,"nowLine"),C=s((e,t)=>e.hold?S(e.hold,t):"pushes go out after each check","deviceLine"),Q=s(e=>`${p(e.quiet.start)} \u2013 ${p(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),X="checks every minute while you're here, every 5 when away",Y=s((e,t)=>e.count?`${e.count} push${e.count===1?"":"es"} queued; next send ${V(e.nextFlushAt,t)}.`:"No pushes queued.","queuedLine");function ee({running:e=!1,nextAt:t,last:n=null},o){if(e)return"Checking GitHub now";const a=`Next check ${d(t,o)}`;if(!n)return a;const u=n.failed.length?"could not reach GitHub for every PR":n.watched?`${O(n.watched,"PR")}, ${n.changes.length?`${n.changes.length} changed`:"no change"}`:"no PRs watched";return`${a} \xB7 last ${d(new Date(n.at),o)}, ${u}`}s(ee,"pollLine");function te(e,t,n){return{title:Z(J(e,t,n)),lines:[ee(t,n),Y(e,n)],count:e.count}}s(te,"pollingNow");function ne(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(r=>!r.ok),a=o.filter(r=>r.state==="broken"),u=t.find(r=>r.link==="events"),l=u?.listen==="asleep"||u?.listen==="waking"?u.listen:u?.ok?"listening":"not listening",c=a.length?`degraded \xB7 ${l}`:o.length?`up \xB7 ${l} \xB7 ${o.length} settling`:`up \xB7 ${l} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:c,lines:o.map(r=>({link:r.link,detail:`${r.state==="broken"?"":"settling: "}${r.detail}`}))}}s(ne,"siteHealth");const R=[{value:"always",label:"Always on",description:s(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:s(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:s(e=>`${e} implements and reviews.`,"description")}],oe=s((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:R.map(({value:o,label:a})=>({value:o,label:a,on:o===e})),description:t.usable?R.find(o=>o.value===e).description(_[n]):t.words,fix:t.fix}),"codexView"),_={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},P=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],se=s(e=>({model:e,builder:_[e],choices:P.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:P.find(t=>t.value===e).description}),"claudeView");function me({now:e,st:t,poll:n,sidebar:o,rows:a,links:u=new Map,code:l={},host:c="",version:r,rules:h=[],remembered:w=new Map,integrations:m=[],codexShare:g="off",claudeModel:A="auto",codex:L=G({installed:!1}),awake:q=null}){const b=new Set(o.sites.filter(i=>i.board).map(i=>i.route)),N=!!(l.running&&l.running===l.onDisk),z=N?` \xB7 runs the code in its files (${l.onDisk})`:"",D=s(i=>({route:i.route,title:i.title,icon:B(i.route,i),href:`/${encodeURIComponent(i.route)}/`,...ne(i,u.get(i.route)),remembered:w.get(i.route)||[],restart:b.has(i.route)&&i.restart===!0}),"site"),U=[["Boards",a.filter(i=>b.has(i.route))],["Pages",a.filter(i=>!b.has(i.route))]].filter(([,i])=>i.length).map(([i,K])=>({label:i,sites:K.map(D)}));return{hub:{tone:o.hub.tone,word:o.hub.word,detail:`${o.hub.detail}${z}`,restart:!N},awake:q&&f(q,e,E),groups:U,integrations:m,usage:o.usage,pause:o.pause,codex:oe(g,L,A),claude:se(A),empty:a.length===0,polling:te(t,n,e),notifications:{on:t.settings.notifications.on,text:o.notifications.text,device:C(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${S(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:h.length?O(h.length,"rule"):"None yet",sites:O(a.length,"site"),host:c,version:r,total:o.total}}s(me,"hubView");const ge="That snooze ends in the past or more than 30 days on. Pick another time.",ye="That keep awake ends in the past or more than 30 days on. Pick another time.",T="until you end it",E="until you turn it off",I="until you resume",f=s((e,t,n)=>e===$?n:`until ${d(e,t)}`,"untilWords"),v=s((e,t)=>H.map(([n,o])=>({value:n,label:o,until:f(W(e,n),e,t)})),"durationChoices"),ie=s((e,t)=>e.on?M(e.snoozed_until,t):null,"snoozedUntil");function $e({now:e,st:t,rules:n=[],linear:o={set:!1},slack:a={set:!1},awake:u={until:null,unsupported:null},pausedUntil:l=null}){const c=t.settings,r=c.notifications,h=ie(r,e),w=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",m=h&&`Snoozed ${f(h,e,T)}`,g=l&&`Paused ${f(l,e,I)}`;return{pause:{on:l?{title:g}:null,summary:l===$?"Paused \xB7 forever":g||"Off",choices:l?null:v(e,I)},snoozed:h?{title:m,summary:h===$?"Snoozed \xB7 forever":m}:null,device:C(t,e),on:{on:r.on,sub:r.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${w} Needs you and the badge still update.`},snooze:r.on&&!h?{choices:v(e,T)}:null,awake:{unsupported:u.unsupported,on:u.until?{title:`Keeping awake ${f(u.until,e,E)}`}:null,choices:u.unsupported||u.until?null:v(e,E),summary:u.unsupported||(u.until?u.until===$?"On \xB7 forever":`On until ${d(u.until,e)}`:"Off")},weekends:{on:r.weekends_off,sub:`Pushes wait from Friday ${p(c.quiet.start)} until Monday ${p(c.quiet.end)}. Urgent ones still go.`},quiet:le(c,e),rules:n,linear:ae(o),slack:re(a)}}s($e,"settingsView");const ue={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:write","reactions:write","users:read"]}}},x={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(ue))}`,label:"a new Slack app"};function re(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${x.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:x,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:x,summary:"No token"}}s(re,"slackWords");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function ae(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}s(ae,"linearWords");function le(e,t){const n=e.quiet,[o,a]=[p(n.start),p(n.end)],u=y(t,"tonight",e),l=[["Snooze tonight",`Until ${d(u,t)}${n.hold?`, then quiet hours hold pushes until ${d(y(u,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${d(y(t,"morning",e),t)}.`],["Snooze monday",`Until ${d(y(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${a}.`:"Off."],["Daily wake",`Every board wakes once at ${a}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:Q(e),hint:X,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${a}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:l.map(([c,r])=>({title:c,sub:r}))}}s(le,"quietView");function ke(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}s(ke,"flushView");export{ye as AWAKE_REFUSED,X as QUIET_HINT,ge as SNOOZE_REFUSED,j as cadenceLine,Z as capital,v as durationChoices,ke as flushView,me as hubView,J as nowLine,O as plural,ee as pollLine,Q as quietSummary,$e as settingsView,ne as siteHealth};
|
package/package.json
CHANGED
package/shared/card-prs.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var i=Object.defineProperty;var t=(e,r)=>i(e,"name",{value:r,configurable:!0});const s=/^(?:([\w.-]+\/[\w.-]+)#)?([1-9]\d*)$/,o=t(e=>Array.isArray(e?.prs)?e.prs:Number.isInteger(e?.pr)?[{number:e.pr}]:[],"storedPRs"),l=t((e,r)=>o(e).map(n=>({repo:n.repo||r,number:n.number,...n.merged_at?{merged_at:n.merged_at}:{}})).filter(n=>n.repo),"cardPRs"),m=t(e=>o(e).length>0,"hasPR"),u=t(({repo:e,number:r})=>`${e}#${r}`,"prKeyOf"),d=t(({repo:e,number:r})=>`https://github.com/${e}/pull/${r}`,"prURL"),x=t((e,r)=>e.map(n=>n.repo&&n.repo!==r?u(n):`#${n.number}`).join(", "),"prNames");function g(e){const r=s.exec(String(e).trim());return r?{...r[1]?{repo:r[1]}:{},number:Number(r[2])}:null}t(g,"parsePRRef");const a=["MERGED","CLOSED"],c=t(e=>e.checks?.state==="fail"||!!e.failing?.length,"failing");function p(e){return e?.state?a.includes(e.state)?5:e.isDraft?1:c(e)?2:e.reviewDecision==="APPROVED"?4:3:0}t(p,"readiness");function R(e){if(!e.length)return-1;const r=e.map(p);return r.every(n=>n===5)?Math.max(0,e.findIndex(n=>n.state==="MERGED")):r.indexOf(Math.min(...r))}t(R,"leastReady");const h=t(e=>e.length>0&&e.every(r=>a.includes(r?.state))&&e.some(r=>r.state==="MERGED"),"allLanded");export{h as allLanded,l as cardPRs,m as hasPR,R as leastReady,g as parsePRRef,u as prKeyOf,x as prNames,d as prURL,o as storedPRs};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var k=Object.defineProperty;var s=(e,t)=>k(e,"name",{value:t,configurable:!0});import i from"node:fs";import c from"node:os";import _ from"node:path";import{execFileSync as M}from"node:child_process";import{audit as d,hubAudit as y}from"./audit.mjs";const f=4,h=3,v=10*6e4,A=Object.freeze({state:"normal",reason:null}),D={1:"normal",2:"warn",4:"critical"},F=s(e=>D[e]??null,"pressureOf");function N(){let e=null;try{e=Number(M("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim())}catch{}return{load:c.loadavg()[0],cores:c.cpus().length,pressure:F(e)}}s(N,"readMachine");function R(e,{load:t,cores:r,pressure:o}){const a=(e==="busy"?h:f)*r;return o==="critical"||t>=a?"busy":"normal"}s(R,"machineState");const b=s((e=c.homedir())=>_.join(e,".claude","kickflip","machine-state.json"),"machineFile"),g=s(e=>{try{return JSON.parse(i.readFileSync(b(e),"utf8"))}catch{return null}},"readFile");function T(e=c.homedir()){const t=g(e);return t?.state==="busy"?{state:"busy",reason:t.reason}:A}s(T,"readMachineState");function z({home:e=c.homedir(),reading:t=N(),now:r=new Date}={}){const o=g(e),a=o?.state??"normal",n=R(a,t),O=n==="busy"?t.pressure==="critical"?"memory pressure critical":"load high":null,l={reading:t,previous:o?.state??null,hold:f,release:h},u=`1-minute load ${t.load.toFixed(1)}`,p=n!==a,S=!o?.logged_at||r.getTime()-Date.parse(o.logged_at)>=v;if(p&&n==="busy"){const E=t.pressure==="critical"?"memory pressure is critical":`${u} is at least ${f}\xD7 its ${t.cores} cores`;d(y(e),{call:"machine-busy",result:n,reason:`This Mac is busy: ${E}, so no new work starts.`,inputs:l},{now:r})}else p?d(y(e),{call:"machine-eased",result:n,reason:`Memory pressure is ${t.pressure??"unread"} and ${u} is under ${h}\xD7 its ${t.cores} cores, so new work starts again.`,inputs:l},{now:r}):S&&d(y(e),{call:"machine-reading",result:n,reason:`Memory pressure is ${t.pressure??"unread"} and ${u} on ${t.cores} cores.`,inputs:l},{now:r});const $={state:n,reason:O,at:r.toISOString(),reading:t,logged_at:p||S?r.toISOString():o.logged_at},m=b(e);i.mkdirSync(_.dirname(m),{recursive:!0});const x=`${m}.${process.pid}.tmp`;return i.writeFileSync(x,JSON.stringify($)+`
|
|
2
|
+
`),i.renameSync(x,m),$}s(z,"checkMachine");export{f as HOLD_LOAD,A as MACHINE_NORMAL,h as RELEASE_LOAD,z as checkMachine,b as machineFile,R as machineState,F as pressureOf,N as readMachine,T as readMachineState};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){const n=new URLSearchParams(location.search);let a=n.get("variant"),e=n.get("state");const r=[];addEventListener("message",t=>{if(!(t.source!==parent||t.data?.kickflip!=="prototype")){a=t.data.take??null,e=t.data.state??null;for(const s of r)s(a,e)}}),globalThis.KickflipPrototype={get take(){return a},get state(){return e},on(t){r.push(t),t(a,e)}}})();
|
package/shared/usage-gate.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var P=Object.defineProperty;var t=(e,s)=>P(e,"name",{value:s,configurable:!0});import W from"node:fs";import f from"node:os";import j from"node:path";import{usageReader as F}from"../hub/usage.mjs";import{readSettingsFile as h,writeSettingsFile as $,readCodexShare as H,readClaudeModel as U}from"../hub/push.mjs";import{FOREVER as C,durationEnd as I,storedEnd as v}from"../hub/duration.mjs";import{codexStatus as G,codexStatusReader as L,effectiveCodexShare as B}from"./codex.mjs";import{MACHINE_NORMAL as z,readMachineState as J}from"./machine-gate.mjs";const K=t(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),l=t(e=>Number(e),"timeOf"),T=["five_hour","seven_day"],b=t((e,s)=>e===C||typeof e=="string"&&Date.parse(e)>l(s)?e:null,"pausedUntil");function V(e,s=new Date){if(e==="off")return null;const o=I(new Date(l(s)),e);if(!o)throw Object.assign(new Error("A pause ends after now and within 30 days: <n>m, <n>h, <n>d, <n>w, forever or a date and time; or off."),{status:400});return v(o)}t(V,"pauseEnd");const q=t(e=>e===C?"until the owner resumes it":`until ${e}`,"pauseUntilWords");function Q({claude:e,codex:s,pauseAt:o=90,keepGoing:n,hubPause:a,boardPause:u,machine:r=z,now:d=Date.now()}={}){const c=K(o)?o:90,x=t(w=>c===100?null:T.find(i=>{const p=w?.[i];return p?.state==="live"&&Number.isFinite(p.used_percentage)&&p.used_percentage>=c&&(!p.resets_at||Date.parse(p.resets_at)>l(d))})||null,"past"),g=t((w,i)=>({window:i,used:i?w[i].used_percentage:null,resets_at:i?w[i].resets_at:null}),"details"),m=x(e),k=g(e,m),R=m&&n?.window===m&&Math.abs(Date.parse(n.until)-Date.parse(k.resets_at))<=3e5&&l(d)<Date.parse(n.until),M=x(s),S={past:!!M,...g(s,M)},_=b(a,d)?"hub":b(u,d)?"board":null;return _?{pause_at:c,state:"paused",manual:_,window:null,used:null,resets_at:b(_==="hub"?a:u,d),codex:S,machine:r}:{pause_at:c,state:m?R?"going":"paused":"normal",manual:null,...k,codex:S,machine:r}}t(Q,"usageGate");const X=t(e=>{try{return JSON.parse(W.readFileSync(j.join(e,"board.json"),"utf8"))||{}}catch{return{}}},"readBoardJson"),Y=t((e,s=Date.now())=>b(X(e).paused_until,s),"boardPausedUntil"),D=new Map;function A({home:e=f.homedir(),now:s=Date.now(),dir:o=null}={}){let n=D.get(e);n||(n={now:l(s)},n.usage=F({home:e,now:t(()=>n.now,"now")}),n.codex=L({home:e,now:t(()=>n.now,"now")}),D.set(e,n)),n.now=l(s);const a=h(e);return Q({...n.usage({codexShare:H(n.codex(),e)}),pauseAt:a.pauseAt,keepGoing:a.keepGoing,hubPause:a.paused_until,boardPause:o?Y(o,s):null,machine:J(e),now:s})}t(A,"readUsageGate");function le(e,{home:s=f.homedir(),now:o=new Date}={}){const n=V(e,o),{paused_until:a,...u}=h(s);return $(n?{...u,paused_until:n}:u,s),n}t(le,"setHubPause");function pe(e,{home:s=f.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});$({...h(s),pauseAt:Number(e)},s)}t(pe,"setPauseAt");function fe(e,{home:s=f.homedir(),now:o=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=h(s);if(e==="on"){const a=A({home:s,now:o});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;$(n,s)}t(fe,"setKeepGoing");const he=t(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 xe({home:e=f.homedir(),now:s=Date.now(),card:o={}}={}){const n=G({home:e}),a={codex:n.reason??"usable",claudeModel:U(e),size:o.size??null,cardModel:o.model??null},u=t(d=>{const{model:c,reason:x}=Z(a.claudeModel,o);return{kind:"claude",model:c,reason:`${d} ${x}`,inputs:a}},"claude");if(!n.usable)return u(n.words);if(a.codexShare=B(h(e).codexShare,n),a.codexShare==="off")return u("Codex share is off.");const r=A({home:e,now:s});return Object.assign(a,{pauseAt:r.pause_at,codexUsage:r.codex}),r.codex.past?u(`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 ${a.codexShare} and Codex is under the ${r.pause_at}% line.`,inputs:a}}t(xe,"builderPick");const E={opus:"Opus",sonnet:"Sonnet"};function Z(e,s){return e!=="auto"?{model:e,reason:`Claude model is ${E[e]}.`}:s.model?{model:s.model,reason:`Claude model is Auto and the card asks for Claude ${E[s.model]}.`}:s.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."}}t(Z,"claudePick");const N={hub:["the hub","Pause hub in Hub settings"],board:["this board","Pause board in Board settings"]},ee=t(e=>e.manual?`Work is paused: the owner paused ${N[e.manual][0]} ${q(e.resets_at)}. ${N[e.manual][1]} resumes it.`:e.state==="paused"?se(e):y(e),"pauseReason"),y=t(e=>`Work is held: this Mac is busy (${e.machine.reason}). It starts again when the Mac eases.`,"busyReason"),me="Held: this Mac is busy. It starts again when the Mac eases.",we=t(e=>e.state==="paused"||e.machine.state==="busy","holdsWork"),be=t(e=>`usage paused ${e.resets_at} ${e.manual?`manual ${e.manual}`:`${e.window} ${e.used}%`}`,"pausedLine"),se=t(e=>`Work is paused: Claude's ${O(e.window)} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"usagePauseReason"),_e=t((e,s)=>!!e.manual||e.machine.state==="busy"||(s==="codex"?e.codex.past:e.state==="paused"),"heldFor"),O=t(e=>e==="five_hour"?"5-hour":"weekly","windowWord"),$e=t((e,s)=>s==="codex"&&!e.manual&&e.codex.past?`Codex is paused: its ${O(e.codex.window)} usage is at ${Math.round(e.codex.used)}%, past the ${e.pause_at}% line. Resumes at ${e.codex.resets_at}.`:s==="codex"&&!e.manual?y(e):ee(e),"pauseReasonFor");export{me as MACHINE_HELD_LINE,Y as boardPausedUntil,xe as builderPick,_e as heldFor,he as heldUsageEvent,we as holdsWork,V as pauseEnd,ee as pauseReason,$e as pauseReasonFor,q as pauseUntilWords,be as pausedLine,A as readUsageGate,le as setHubPause,fe as setKeepGoing,pe as setPauseAt,Q as usageGate,K as validPauseAt};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// The phase bars: one bar per phase, those reached filled, a running one pulsing. An In Progress card draws the five
|
|
2
|
-
// build phases (ui-cards.js) and a starting board's sidebar row its three start steps (ui-sidebar.js)
|
|
3
|
-
// (docs/adr/2026-11-04-1914-a-new-board-shows-its-start-steps-and-clone-progress.md). It needs nothing else, since
|
|
4
|
-
// hub/hub.mjs renders the shell's first sidebar with it in a vm context. style.css draws the bars.
|
|
5
|
-
(function () {
|
|
6
|
-
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
7
|
-
// strip: {reached, running, dim, label, fill, plan}, or null for no bars. reached counts the phases reached, the last of
|
|
8
|
-
// them running when running is set; fill, a percent, fills the next bar that far; plan draws them in the planner blue.
|
|
9
|
-
function phaseBars(strip, phases) {
|
|
10
|
-
if (!strip) return '';
|
|
11
|
-
const { reached, running, dim, label, fill, plan } = strip;
|
|
12
|
-
const bar = (i) => (i === reached - 1 && running ? '<i class="now"></i>' : i < reached ? '<i class="done"></i>'
|
|
13
|
-
: i === reached && fill !== undefined ? `<i class="fill" style="--p:${fill}%"></i>` : '<i class=""></i>');
|
|
14
|
-
return `<span class="phase-bars${dim ? ' dim' : ''}${plan ? ' plan' : ''}" role="img" aria-label="${esc(label)}" title="${esc(label)}">${phases.map((_, i) => bar(i)).join('')}</span>`;
|
|
15
|
-
}
|
|
16
|
-
Object.assign(globalThis.BoardUI ||= {}, { phaseBars });
|
|
17
|
-
})();
|