@lovek4d/kickflip 0.5.0 → 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.
Files changed (110) hide show
  1. package/CONTEXT.md +105 -68
  2. package/README.md +1 -1
  3. package/SKILL.md +27 -21
  4. package/bin/kickflip.mjs +27 -18
  5. package/bin/wait-events.mjs +2 -2
  6. package/board/agents.mjs +2 -0
  7. package/board/arch-review.mjs +4 -4
  8. package/board/bin/board.mjs +199 -79
  9. package/board/build-script.mjs +2 -1
  10. package/board/domain.mjs +9 -7
  11. package/board/finish.mjs +4 -4
  12. package/board/headless.mjs +9 -4
  13. package/board/on-host.mjs +1 -1
  14. package/board/onboarding.mjs +11 -0
  15. package/board/prototype-base.mjs +1 -0
  16. package/board/prototype-page.mjs +1 -0
  17. package/board/public/app-arch.js +1 -1
  18. package/board/public/app-asks.js +6 -6
  19. package/board/public/app-banner.js +7 -13
  20. package/board/public/app-cards.js +75 -37
  21. package/board/public/app-core.js +26 -8
  22. package/board/public/app-header.js +8 -8
  23. package/board/public/app-lanes.js +67 -58
  24. package/board/public/app-needs.js +140 -10
  25. package/board/public/app-reader.js +1 -1
  26. package/board/public/app-snapshot.js +46 -72
  27. package/board/public/app-threads.js +78 -75
  28. package/board/public/app.html +9 -4
  29. package/board/public/app.js +114 -38
  30. package/board/public/attach.js +2 -2
  31. package/board/public/dialogs.js +9 -39
  32. package/board/public/hub-kit.js +66 -4
  33. package/board/public/hub.css +2 -0
  34. package/board/public/linear-key.js +46 -35
  35. package/board/public/new-task.js +20 -39
  36. package/board/public/progress-review.html +46 -0
  37. package/board/public/progress-review.js +158 -0
  38. package/board/public/prototype-kit.js +137 -7
  39. package/board/public/prototype-switcher.js +152 -123
  40. package/board/public/prototype.html +57 -3
  41. package/board/public/router.js +7 -6
  42. package/board/public/shell.js +68 -26
  43. package/board/public/sidebar.css +1 -0
  44. package/board/public/sidebar.js +16 -30
  45. package/board/public/style.css +205 -83
  46. package/board/public/ui-cards.js +91 -65
  47. package/board/public/ui-enums.js +1 -1
  48. package/board/public/ui-header.js +27 -18
  49. package/board/public/ui-hub.js +12 -1
  50. package/board/public/ui-links.js +7 -7
  51. package/board/public/ui-order.js +12 -10
  52. package/board/public/ui-page-header.js +19 -7
  53. package/board/public/ui-patch.js +7 -33
  54. package/board/public/ui-progress-bar.js +28 -0
  55. package/board/public/ui-questions.js +47 -52
  56. package/board/public/ui-sidebar.js +34 -26
  57. package/board/public/ui-snapshot.js +43 -52
  58. package/board/public/ui-threads.js +59 -30
  59. package/board/public/ui.js +125 -18
  60. package/board/public/view-hub-settings.js +22 -67
  61. package/board/public/view-hub.js +19 -4
  62. package/board/public/view-notes.js +27 -7
  63. package/board/public/view-questions.js +205 -240
  64. package/board/public/view-settings.js +313 -76
  65. package/board/render-check.mjs +10 -0
  66. package/board/review.mjs +1 -1
  67. package/board/server.mjs +19 -20
  68. package/board/stop.mjs +1 -0
  69. package/board/store.mjs +29 -15
  70. package/board/sync.mjs +2 -0
  71. package/board/tickets.mjs +3 -3
  72. package/board/transcript.mjs +2 -0
  73. package/board/worktree.mjs +25 -20
  74. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  75. package/hooks-plugin/hooks/hooks.json +9 -0
  76. package/hooks-plugin/hooks/usage-gate.mjs +20 -3
  77. package/hub/board-repo.mjs +1 -1
  78. package/hub/boards.mjs +4 -4
  79. package/hub/front-door.mjs +5 -5
  80. package/hub/github.mjs +3 -3
  81. package/hub/hub.mjs +11 -11
  82. package/hub/incoming.mjs +2 -2
  83. package/hub/linear.mjs +1 -2
  84. package/hub/live.mjs +2 -2
  85. package/hub/locks.mjs +1 -1
  86. package/hub/owner-checks.mjs +1 -1
  87. package/hub/pr-report.mjs +2 -2
  88. package/hub/presence.mjs +1 -1
  89. package/hub/push.mjs +2 -2
  90. package/hub/readiness.mjs +1 -2
  91. package/hub/review-send.mjs +1 -0
  92. package/hub/secrets.mjs +2 -0
  93. package/hub/self-restart.mjs +2 -0
  94. package/hub/sessions.mjs +2 -2
  95. package/hub/share.mjs +1 -0
  96. package/hub/slack.mjs +2 -2
  97. package/hub/stream.mjs +10 -13
  98. package/hub/try.mjs +7 -7
  99. package/hub/upgrade.mjs +1 -1
  100. package/hub/usage.mjs +3 -3
  101. package/hub/views.mjs +1 -1
  102. package/package.json +2 -1
  103. package/shared/audit.mjs +4 -0
  104. package/shared/card-prs.mjs +1 -1
  105. package/shared/fake-gh.mjs +17 -4
  106. package/shared/machine-gate.mjs +2 -0
  107. package/shared/prototype-frame.js +1 -0
  108. package/shared/service.mjs +4 -4
  109. package/shared/usage-gate.mjs +1 -1
  110. package/board/public/ui-phase-bars.js +0 -17
package/hub/github.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var ne=Object.defineProperty;var k=(e,s)=>ne(e,"name",{value:s,configurable:!0});import j from"node:fs";import z from"node:path";import"node:os";import{savedClient as re}from"./linear.mjs";import{checksPassed as H,fetchPRs as ae,markReady as oe,reportOf as ie}from"./pr-report.mjs";import{wakesDue as ce,localDay as de}from"./schedule.mjs";import{GITHUB_STATE as K,WAKE_STATE as W,WAVES as _,listBoards as U,appendEvent as V,prKey as b,readJSON as F,saveJSON as Q,reviewerLogins as fe}from"./boards.mjs";import{appendThread as X,setItem as le,mergedLine as ue,mergeCard as ge}from"../board/store.mjs";import{allLanded as Y,leastReady as me}from"../shared/card-prs.mjs";import{runOwnerChecks as he}from"./owner-checks.mjs";const B=["ci","review","comments","merged","mergeable","draft"],P=k(e=>e&&e!=="UNKNOWN"?e:null,"known");function pe(e){const r=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:r?String(r.id):null,eventAt:r?.at||null}}k(pe,"summarize");function we(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),B.filter(a=>r.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
1
+ var le=Object.defineProperty;var S=(e,s)=>le(e,"name",{value:s,configurable:!0});import x from"node:fs";import U from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as V,fetchPRs as ue,markReady as me,reportOf as ge}from"./pr-report.mjs";import{wakesDue as pe,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as F,WAKE_STATE as Q,WAVES as X,listBoards as Y,appendEvent as Z,prKey as M,readJSON as ee,saveJSON as te,reviewerLogins as we}from"./boards.mjs";import{appendThread as H,setItem as se,mergedLine as ye,mergeCard as ve}from"../board/store.mjs";import{allLanded as re,cardPRs as ke,leastReady as $e,prNames as Se}from"../shared/card-prs.mjs";const ne=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as T,hubAudit as ae}from"../shared/audit.mjs";const _=["ci","review","comments","merged","mergeable","draft","linked"],G=S(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(e){const n=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:n?String(n.id):null,eventAt:n?.at||null,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,commits:Array.isArray(e.commits)?e.commits:[]}}S(Ee,"summarize");function Re(e,s){const n=new Set;if(!e)return s.state==="MERGED"&&n.add("merged"),s.eventId&&n.add("comments"),_.filter(a=>n.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
2
2
  `)!==s.failing.join(`
3
- `))&&r.add("ci"),e.reviewDecision!==s.reviewDecision&&r.add("review"),s.eventId&&s.eventId!==e.eventId&&r.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&r.add("merged");for(const a of["mergeable","mergeStateStatus"])P(e[a])&&P(s[a])&&e[a]!==s[a]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),B.filter(a=>r.has(a))}k(we,"changesBetween");function Z(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!P(s[n])&&P(e?.[n])&&(r[n]=e[n]);return r}k(Z,"carry");function ye(e,s){const r=z.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;j.mkdirSync(z.dirname(r),{recursive:!0}),j.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
- `),j.renameSync(n,r)}k(ye,"writeChecked");async function Je({every:e,manifest:s}){const r=new Date().toISOString(),n=F(K,{prs:{}});n.prs||={},n.acknowledged||={},n.threaded||={};const a=U(s),i=[],c=[...new Map(a.flatMap(t=>t.prs).map(t=>[b(t),{repo:t.repo,pr:t.pr}])).values()];let g=null;if(c.length)try{g=await ae(c)}catch(t){i.push(t.message)}const w=new Map(c.map((t,u)=>[b(t),g?g.results[u]:null])),f=g?.login??null,d=k((t,u)=>JSON.stringify([b(t),u]),"fetchKey"),m=new Map,E=new Map,y=new Map;for(const t of a){t.me=t.githubLogin||f;for(const u of t.prs){const p=b(u);m.set(p,u);const O=y.get(p)||new Set;if(O.add(t.me),y.set(p,O),!t.me)continue;const C=d(u,t.me),o=E.get(C);(!o||u.since<o.since)&&E.set(C,{...u,me:t.me})}}const l={},$={},h=new Map,D=Object.fromEntries([...m.keys()].map(t=>[t,[]])),S=new Set;for(const[t,u]of E){const p=w.get(b(u));if(!p?.view){l[t]=null,p?.error&&!S.has(p.error)&&(S.add(p.error),i.push(p.error));continue}$[t]=ie(p,u);const O=pe($[t]);l[t]=O,h.has(b(u))||h.set(b(u),O)}const M=[],A=[],L=new Set,ee=new Map,q=new Set,te=new Set,se=new Set;for(const t of a){const u=t.prs.map(o=>{const v=b(o),R=JSON.stringify([t.route,o.wave,o.id]),N=n.acknowledged[v]?.[R]||n.prs[v],I=l[d(o,t.me)],J=I?we(N,I):[];return J.length&&(D[v]=B.filter(x=>D[v].includes(x)||J.includes(x))),{p:o,key:v,recipient:R,previous:N,next:I,changes:J}});for(const o of u){const v=$[d(o.p,t.me)],R=u.filter(N=>N.p.wave===o.p.wave&&N.p.id===o.p.id);v&&await ke(t,o,v,R,{state:n,merged:ee,failed:i})&&q.add(t.route),t.skipDraft&&await Se(t,o,w.get(o.key)?.view,te,i),t.autoMerge&&await $e(t,o,w.get(o.key)?.view,se,i)}const p=u.filter(o=>o.changes.length);let O=u.every(o=>o.next),C=!0;if(p.length)try{M.push({route:t.route,id:await V(t,"github-change",{prs:p.map(({p:o,changes:v})=>({wave:o.wave,id:o.id,pr:o.pr,changes:v}))})})}catch(o){i.push(`${t.route}: could not add the github-change event (${o.message})`);for(const{key:v}of p)L.add(v);O=C=!1}for(const{key:o,recipient:v,previous:R,next:N,changes:I}of u)N&&(C||!I.length)&&(n.acknowledged[o]||={},n.acknowledged[o][v]=Z(R,N));if(O)try{ye(t.dir,t.prs.length?e:null),A.push(t.route)}catch(o){i.push(`${t.route}: could not write checked.json (${o.message})`)}}const G=[];for(const[t,u]of m){const p=h.get(t);!p||L.has(t)||(D[t].length&&G.push({repo:u.repo,pr:u.pr,changes:D[t]}),n.prs[t]={...Z(n.prs[t],p),at:r},y.get(t).size===1&&delete n.acknowledged[t])}for(const t of Object.keys(n.acknowledged))m.has(t)||delete n.acknowledged[t];for(const t of Object.keys(n.threaded))m.has(t)||delete n.threaded[t];await he(a,n,i),n.lastCheck={at:r,watched:m.size,failed:i.length,changed:G.length,boards:A},Q(K,n);const T={moved:[],failed:[]};try{await De(a,l,d,T)}catch(t){T.failed.push(`linear: ${t.message}`)}return{at:r,watched:m.size,failed:i,changes:G,events:M,checked:A,merged:[...q],linear:T,rate:g?.rate??null}}k(Je,"checkGitHub");function ve(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}k(ve,"lineText");async function ke(e,{p:s,key:r,recipient:n},a,i,{state:c,merged:g,failed:w}){const f=`${s.wave}/${s.id}`,d=new Set(c.threaded[r]?.[n]||[]),m=e.waves.find(l=>l.slug===s.wave)?.wave,E={...e.reviewerLogins,...fe(m?.reviewers)};for(const l of a.events){const $=`${l.kind}:${l.id}`;if(!["user","reviewer"].includes(l.who)||l.at<=s.since||d.has($))continue;const h=ve(l);try{h&&await X(e.dir,f,{text:h,via:"github",who:l.who==="user"?"love":E[String(l.login).toLowerCase()]||l.login,link:l.url||`https://github.com/${s.repo}/pull/${s.pr}`}),d.add($)}catch(D){w.push(`${e.route}: could not write ${f}'s GitHub line (${D.message})`)}}if(d.size&&((c.threaded[r]||={})[n]=[...d]),a.state!=="MERGED"||!Y(i.map(l=>l.next)))return!1;const y=JSON.stringify([e.kind==="waves"?_:e.dir,s.wave,s.id]);if(!g.has(y)){const l=e.github||e.waves.find(h=>h.slug===s.wave)?.wave.repo,$=ue(i.filter(h=>h.next.state==="MERGED").map(h=>({repo:h.p.repo,number:h.p.pr})),l);try{await le(e.dir,f,{status:"merged",force:!0,text:$}),g.set(y,!0)}catch(h){w.push(`${e.route}: could not record ${f} merged (${h.message})`),g.set(y,!1)}}return g.get(y)}k(ke,"recordOnCard");async function Se(e,{p:s,key:r},n,a,i){if(!(a.has(r)||n?.state!=="OPEN"||!n.isDraft||!H(n.statusCheckRollup))){a.add(r);try{await oe({repo:s.repo,pr:s.pr})}catch(c){i.push(`${e.route}: could not mark ${r} ready for review (${c.message})`);return}try{await X(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(c){i.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${c.message})`)}}}k(Se,"readyGreenDraft");async function $e(e,{p:s},r,n,a){const i=`${s.wave}/${s.id}`,c=JSON.stringify([e.dir,i]);if(!(n.has(c)||r?.state!=="OPEN"||r.isDraft||r.reviewDecision!=="APPROVED"||r.mergeStateStatus!=="CLEAN"||!H(r.statusCheckRollup))){n.add(c);try{await ge(e.dir,i)}catch(g){g.message.startsWith("Not merged:")||a.push(`${e.route}: could not merge ${i} (${g.message})`)}}}k($e,"mergeApproved");async function De(e,s,r,n){const a=re();if(!a)return;const i=new Map;for(const c of e){const g=Map.groupBy(c.prs,f=>JSON.stringify([f.wave,f.id])),w=c.kind==="waves"?_:c.dir;for(const{slug:f,wave:d}of c.waves)for(const m of d.items){if(m.status==="dropped")continue;const E=new Set(m.linear||[]);c.tickets?.source==="linear"&&m.ticket&&E.add(m.ticket);const y=(g.get(JSON.stringify([f,m.id]))||[]).map(S=>s[r(S,c.me)]||null),l=y[me(y)],$=["merged","done"].includes(m.status)||Y(y),h={merged:$,ready:l?.state==="OPEN"&&!l.isDraft,hasPR:y.length>0&&($||y.some(S=>S?.state==="OPEN")),failed:y.some(S=>!S)},D=JSON.stringify([w,f,m.id]);for(const S of E){i.has(S)||i.set(S,{items:new Map,scoped:!1,created:!1});const M=i.get(S),A=M.items.get(D);M.items.set(D,{...h,failed:h.failed||A?.failed}),M.scoped||=y.some(Boolean),M.created||=m.ticket===S&&!!m.ticket_created}}}for(const[c,g]of i){const w=[...g.items.values()];if(!g.scoped||w.some(d=>d.failed))continue;const f=w.every(d=>d.merged)?"done":w.every(d=>d.merged||d.ready)?"review":w.some(d=>d.hasPR)?"started":null;if(f)try{const d=await a.advanceIssue(c,f,{owner:a.saved.viewer.id,created:g.created});d&&n.moved.push({id:c,state:d})}catch(d){n.failed.push(`linear ${c}: ${d.message}`)}}}k(De,"advanceLinear");async function je({now:e=new Date,settings:s,manifest:r}){const n=F(W,{});n.sent||={};const a=ce(e,s,n.sent),i=[],c=[],g=[];if(!a.length)return{due:a,events:i,failed:c};const w=U(r);for(const f of w)try{i.push({route:f.route,id:await V(f,"check-github",{reason:"morning"})})}catch(d){c.push(`${f.route}: could not add the morning check-github event (${d.message})`),g.push(f.route)}if(i.length||!w.length){for(const f of a)n.sent[f]=de(e);n.last={at:e.toISOString(),due:a,routes:i.map(f=>f.route),failed:g},Q(W,n)}return{due:a,events:i,failed:c}}k(je,"sendWakes");export{B as CHANGE_KINDS,Z as carry,we as changesBetween,Je as checkGitHub,je as sendWakes,pe as summarize};
3
+ `))&&n.add("ci"),e.reviewDecision!==s.reviewDecision&&n.add("review"),s.eventId&&s.eventId!==e.eventId&&n.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&n.add("merged");for(const a of["mergeable","mergeStateStatus"])G(e[a])&&G(s[a])&&e[a]!==s[a]&&n.add("mergeable");return e.isDraft!==s.isDraft&&n.add("draft"),_.filter(a=>n.has(a))}S(Re,"changesBetween");function oe(e,s){const n={...s};for(const r of["mergeable","mergeStateStatus"])!G(s[r])&&G(e?.[r])&&(n[r]=e[r]);return n}S(oe,"carry");function Oe(e,s){const n=U.join(e,"data","checked.json"),r=`${n}.${process.pid}.tmp`;x.mkdirSync(U.dirname(n),{recursive:!0}),x.writeFileSync(r,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
+ `),x.renameSync(r,n)}S(Oe,"writeChecked");async function Ue({every:e,manifest:s}){const n=new Date().toISOString(),r=ee(F,{prs:{}});r.prs||={},r.acknowledged||={},r.threaded||={};const a=Y(s),i=[],c=[...new Map(a.flatMap(t=>t.prs).map(t=>[M(t),{repo:t.repo,pr:t.pr}])).values()],f=[...new Set(a.flatMap(Ne))];let o=null;if(c.length||f.length)try{o=await ue(c,f)}catch(t){i.push(t.message)}const d=new Map(a.map(t=>[t.route,[]]));if(o)for(const t of a)d.set(t.route,await Pe(t,o.open,i));const p=new Map(c.map((t,g)=>[M(t),o?o.results[g]:null])),h=o?.login??null,m=S((t,g)=>JSON.stringify([M(t),g]),"fetchKey"),y=new Map,O=new Map,R=new Map;for(const t of a){t.me=t.githubLogin||h;for(const g of t.prs){const v=M(g);y.set(v,g);const N=R.get(v)||new Set;if(N.add(t.me),R.set(v,N),!t.me)continue;const C=m(g,t.me),l=O.get(C);(!l||g.since<l.since)&&O.set(C,{...g,me:t.me})}}const u={},E={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),k=new Set;for(const[t,g]of O){const v=p.get(M(g));if(!v?.view){u[t]=null,v?.error&&!k.has(v.error)&&(k.add(v.error),i.push(v.error));continue}E[t]=ge(v,g);const N=Ee(E[t]);u[t]=N,w.has(M(g))||w.set(M(g),N)}const q=[],B=[],z=new Set,ie=new Map,K=new Set,ce=new Set,de=new Set;for(const t of a){const g=t.prs.map(l=>{const $=M(l),P=JSON.stringify([t.route,l.wave,l.id]),A=r.acknowledged[$]?.[P]||r.prs[$],I=u[m(l,t.me)],j=I?Re(A,I):[];return j.length&&(D[$]=_.filter(W=>D[$].includes(W)||j.includes(W))),{p:l,key:$,recipient:P,previous:A,next:I,changes:j}});for(const l of g){const $=E[m(l.p,t.me)],P=g.filter(A=>A.p.wave===l.p.wave&&A.p.id===l.p.id);$&&await Me(t,l,$,P,{state:r,merged:ie,failed:i})&&K.add(t.route),t.skipDraft&&await be(t,l,p.get(l.key)?.view,ce,i),t.autoMerge&&await Ce(t,l,p.get(l.key)?.view,de,i)}const v=g.filter(l=>l.changes.length);let N=g.every(l=>l.next),C=!0;if(v.length||d.get(t.route).length)try{const l=[...v.map(({p:$,changes:P})=>({wave:$.wave,id:$.id,pr:$.pr,changes:P})),...d.get(t.route)];q.push({route:t.route,id:await Z(t,"github-change",{prs:l})})}catch(l){i.push(`${t.route}: could not add the github-change event (${l.message})`);for(const{key:$}of v)z.add($);N=C=!1}for(const{key:l,recipient:$,previous:P,next:A,changes:I}of g)A&&(C||!I.length)&&(r.acknowledged[l]||={},r.acknowledged[l][$]=oe(P,A));if(N)try{Oe(t.dir,t.prs.length?e:null),B.push(t.route)}catch(l){i.push(`${t.route}: could not write checked.json (${l.message})`)}}const J=[];for(const[t,g]of y){const v=w.get(t);!v||z.has(t)||(D[t].length&&J.push({repo:g.repo,pr:g.pr,changes:D[t]}),r.prs[t]={...oe(r.prs[t],v),at:n},R.get(t).size===1&&delete r.acknowledged[t])}for(const t of Object.keys(r.acknowledged))y.has(t)||delete r.acknowledged[t];for(const t of Object.keys(r.threaded))y.has(t)||delete r.threaded[t];await De(a,r,i),r.lastCheck={at:n,watched:y.size,failed:i.length,changed:J.length,boards:B},te(F,r);const L={moved:[],failed:[]};try{await Ie(a,u,m,L)}catch(t){L.failed.push(`linear: ${t.message}`)}return{at:n,watched:y.size,failed:i,changes:J,events:q,checked:B,merged:[...K],linear:L,rate:o?.rate??null}}S(Ue,"checkGitHub");const Ne=S(e=>e.github?[e.github]:[...new Set(e.waves.map(s=>s.wave.repo).filter(Boolean))],"repoList");async function Pe(e,s,n){const r=[];for(const{slug:a,wave:i}of e.waves){const c=e.github||i.repo,f=new Map((s[c]||[]).filter(o=>!o.isCrossRepository).map(o=>[o.headRefName,o.number]));for(const o of i.items){if(!f.has(o.id)||o.lane==="done"||ke(o,c).some(h=>h.repo===c))continue;const d=`${a}/${o.id}`,p=f.get(o.id);try{await se(e.dir,d,{pr:p}),await H(e.dir,d,{text:`Found PR #${p} on branch ${o.id}.`}),r.push({wave:a,id:o.id,pr:p,changes:["linked"]})}catch(h){n.push(`${e.route}: could not link PR #${p} to ${d} (${h.message})`)}}}return r}S(Pe,"linkHandPRs");function Ae(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}S(Ae,"lineText");async function Me(e,{p:s,key:n,recipient:r},a,i,{state:c,merged:f,failed:o}){const d=`${s.wave}/${s.id}`,p=new Set(c.threaded[n]?.[r]||[]),h=e.waves.find(u=>u.slug===s.wave)?.wave,m={...e.reviewerLogins,...we(h?.reviewers)};for(const u of a.events){const E=`${u.kind}:${u.id}`;if(!["user","reviewer"].includes(u.who)||u.at<=s.since||p.has(E))continue;const w=Ae(u);try{w&&await H(e.dir,d,{text:w,via:"github",who:u.who==="user"?"love":m[String(u.login).toLowerCase()]||u.login,link:u.url||`https://github.com/${s.repo}/pull/${s.pr}`}),p.add(E)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}p.size&&((c.threaded[n]||={})[r]=[...p]);const y=i.map(u=>u.next);if(!ne.includes(a.state)||!y.every(u=>ne.includes(u?.state)))return!1;const O=re(y),R=JSON.stringify([e.kind==="waves"?X:e.dir,s.wave,s.id]);if(!f.has(R)){const u=e.github||e.waves.find(k=>k.slug===s.wave)?.wave.repo,E=i.filter(k=>k.next.state==="MERGED").map(k=>({repo:k.p.repo,number:k.p.pr})),w=i.map(k=>({repo:k.p.repo,number:k.p.pr})),D=O?ye(E,u):`Closed: ${Se(w,u)} closed unmerged.`;try{await se(e.dir,d,{merged:E,force:!0,text:D}),f.set(R,O)}catch(k){o.push(`${e.route}: could not record ${d} ${O?"merged":"closed"} (${k.message})`),f.set(R,!1)}}return f.get(R)}S(Me,"recordOnCard");async function be(e,{p:s,key:n},r,a,i){if(a.has(n)||r?.state!=="OPEN"||!r.isDraft||!V(r.statusCheckRollup))return;a.add(n);const c={call:"ready-draft",key:`${s.wave}/${s.id}`,reason:`The board skips the draft and PR #${s.pr}'s CI passed.`,inputs:{repo:s.repo,pr:s.pr,isDraft:r.isDraft,checks:"passed"}};try{await me({repo:s.repo,pr:s.pr})}catch(f){b(T(e.dir),{...c,result:"failed",reason:`${c.reason} gh failed: ${f.message}`},{once:!0}),i.push(`${e.route}: could not mark ${n} ready for review (${f.message})`);return}b(T(e.dir),{...c,result:"marked ready"});try{await H(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(f){i.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${f.message})`)}}S(be,"readyGreenDraft");async function Ce(e,{p:s},n,r,a){const i=`${s.wave}/${s.id}`,c=JSON.stringify([e.dir,i]);if(r.has(c)||n?.state!=="OPEN"||n.isDraft||n.reviewDecision!=="APPROVED"||n.mergeStateStatus!=="CLEAN"||!V(n.statusCheckRollup))return;r.add(c);const f={call:"auto-merge",key:i,reason:`Auto-merge is on and PR #${s.pr} is approved, green and clean.`,inputs:{repo:s.repo,pr:s.pr,reviewDecision:n.reviewDecision,mergeStateStatus:n.mergeStateStatus,checks:"passed"}};try{await ve(e.dir,i),b(T(e.dir),{...f,result:"merged"})}catch(o){b(T(e.dir),{...f,result:"not merged",reason:`${f.reason} ${o.message}`},{once:!0}),o.message.startsWith("Not merged:")||a.push(`${e.route}: could not merge ${i} (${o.message})`)}}S(Ce,"mergeApproved");async function Ie(e,s,n,r){const a=fe();if(!a)return;const i=new Map;for(const c of e){const f=Map.groupBy(c.prs,d=>JSON.stringify([d.wave,d.id])),o=c.kind==="waves"?X:c.dir;for(const{slug:d,wave:p}of c.waves)for(const h of p.items){const m=new Set(h.linear||[]);c.tickets?.source==="linear"&&h.ticket&&m.add(h.ticket);const y=(f.get(JSON.stringify([d,h.id]))||[]).map(w=>s[n(w,c.me)]||null),O=y[$e(y)],R=h.lane==="done"||re(y),u={merged:R,ready:O?.state==="OPEN"&&!O.isDraft,hasPR:y.length>0&&(R||y.some(w=>w?.state==="OPEN")),failed:y.some(w=>!w)},E=JSON.stringify([o,d,h.id]);for(const w of m){i.has(w)||i.set(w,{items:new Map,scoped:!1,created:!1,cards:[]});const D=i.get(w),k=D.items.get(E);D.items.set(E,{...u,failed:u.failed||k?.failed}),D.scoped||=y.some(Boolean),D.created||=h.ticket===w&&!!h.ticket_created,D.cards.push(`${c.route}:${d}/${h.id}`)}}}for(const[c,f]of i){const o=[...f.items.values()];if(!f.scoped||o.some(m=>m.failed))continue;const d=o.every(m=>m.merged)?"done":o.every(m=>m.merged||m.ready)?"review":o.some(m=>m.hasPR)?"started":null;if(!d)continue;const p={done:"Every card on it has merged.",review:"Every card on it has merged or has a PR ready for review.",started:"A card on it has a PR."}[d],h={call:"linear-move",key:c,reason:p,inputs:{target:d,cards:f.cards,items:o}};try{const m=await a.advanceIssue(c,d,{owner:a.saved.viewer.id,created:f.created});m&&(r.moved.push({id:c,state:m}),b(ae(),{...h,result:`moved to ${m}`}))}catch(m){b(ae(),{...h,result:"failed",reason:`${p} ${m.message}`},{once:!0}),r.failed.push(`linear ${c}: ${m.message}`)}}}S(Ie,"advanceLinear");async function Ve({now:e=new Date,settings:s,manifest:n}){const r=ee(Q,{});r.sent||={};const a=pe(e,s,r.sent),i=[],c=[],f=[];if(!a.length)return{due:a,events:i,failed:c};const o=Y(n);for(const d of o)try{i.push({route:d.route,id:await Z(d,"check-github",{reason:"morning"})})}catch(p){c.push(`${d.route}: could not add the morning check-github event (${p.message})`),f.push(d.route)}if(i.length||!o.length){for(const d of a)r.sent[d]=he(e);r.last={at:e.toISOString(),due:a,routes:i.map(d=>d.route),failed:f},te(Q,r)}return{due:a,events:i,failed:c}}S(Ve,"sendWakes");export{_ as CHANGE_KINDS,oe as carry,Re as changesBetween,Ue as checkGitHub,Ve as sendWakes,Ee as summarize};
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Rt=Object.defineProperty;var l=(e,t)=>Rt(e,"name",{value:t,configurable:!0});import Re from"node:http";import Et from"node:net";import{spawn as Ee,execFileSync as Dt}from"node:child_process";import w from"node:fs";import $ from"node:path";import De from"node:os";import{fileURLToPath as Lt}from"node:url";import Le from"node:vm";import Ct from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as At,addSubscription as Ht,removeSubscription as It,sendAll as Mt,flushQueue as Ce,queueStatus as Ae,saveSettings as Bt,readSettings as V,migrateSettings as Ft,setNotifications as Ut,readCodexShare as He,setCodexShare as Nt,readClaudeModel as Gt,setClaudeModel as zt,deliver as Wt,pushRoute as Jt}from"./push.mjs";import{ownerTracker as Kt}from"./presence.mjs";import{POLL_MS as Ie,answerOf as Vt,expired as Zt,pendingFor as Me,promptRecord as Qt,pushOf as Xt,updatePrompts as M,shown as Yt,pruneRemembered as fe,rememberRules as qt,rememberedAnswer as en,rememberedFor as Be}from"./prompts.mjs";import{hold as tn,heldUntil as nn,holdLine as an,editQuiet as sn,checkEvery as Fe,nextCheckAt as rn,LOW_POINTS as on}from"./schedule.mjs";import{capital as cn,hubView as un,settingsView as ln,flushView as dn,SNOOZE_REFUSED as hn,AWAKE_REFUSED as fn}from"./views.mjs";import{caffeinate as pn,setKeepAwake as mn,awakeUntil as Ue,awakeUnsupported as gn}from"./awake.mjs";import{checkGitHub as wn,sendWakes as bn}from"./github.mjs";import{checkIncoming as yn}from"./incoming.mjs";import{listBoards as R,isListening as Sn,isBoardSite as Ne,listenState as Ge,RESUME_STATE as kn,sendToBoards as ze,checkListening as $n,recordHubCode as _n,runningHubCode as xn,boardCode as We,recordLinkFaults as vn,linksOf as Tn,hubLink as On,hubCodeState as jn,readJSON as Pn}from"./boards.mjs";import{liveSessions as Je}from"./live.mjs";import{RuleError as Rn,applyRules as En,changed as Ke,readHubRules as pe,writeHubRules as Dn}from"./rules.mjs";import{siteIcon as Ln}from"./icons.mjs";import{readNeeds as Cn,sidebarSites as An,startRoute as Hn,hubHealth as In}from"./home.mjs";import{checkSessions as Mn,compactFullContexts as Bn,startingBoards as Fn}from"./sessions.mjs";import{repoService as Un,repoLookupRoute as Nn}from"./repos.mjs";import{boardRepo as Ve,changeBoardRepo as Gn,migrateBoardRepos as zn,migrateHubBoard as Wn}from"./board-repo.mjs";import{requestBoard as Jn,retryBoard as Kn,dismissBoard as Vn,clonePercent as Zn,settleStarts as Qn}from"./new-board.mjs";import{boardClosure as Xn,closeBoard as Yn,closedBoards as qn,reopenBoard as ea}from"./close-board.mjs";import{restartBoard as ta,restartable as na}from"./restart-board.mjs";import{crossSite as aa,isJSON as me}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as ge,appFile as sa,colorsVersion as Ze,destOf as ra,fileVersion as oa,pageOf as Qe,shellHtml as ia}from"./shell.mjs";import{hubStream as ca}from"./stream.mjs";import{usageReader as ua,checkUsagePause as la,contextReading as da}from"./usage.mjs";import{readUsageGate as Xe,setPauseAt as ha,setKeepGoing as fa}from"../shared/usage-gate.mjs";import{readiness as pa,readCodex as Z}from"./readiness.mjs";import{BuildError as ma,startBuild as ga,stopTries as wa,tryTick as we,initializeTries as ba,fetchTryBranches as Ye,pullNow as ya,pullMains as Sa}from"./try.mjs";import{serveFiles as ka}from"./files.mjs";import{upgrader as $a,CHECK_MS as _a}from"./upgrade.mjs";import{fallBackFromLinear as xa,openBoardBuilders as qe}from"../board/store.mjs";import{recordRefusal as va}from"../board/permissions.mjs";import{hubGate as et}from"../board/on-host.mjs";import{pipeUpgrade as Ta,refuse as Oa,refuseUpgrade as ja}from"./front-door.mjs";import{restartRunner as Pa}from"../board/worktree.mjs";import{hubService as Ra}from"../shared/service.mjs";import{cloneRoot as tt}from"../shared/repos.mjs";import{LinearError as be,linearStatus as Q,removeLinearKey as Ea,saveLinearKey as Da,savedClient as nt}from"./linear.mjs";const X=$.dirname(Lt(import.meta.url)),La=$.join(X,"..","shared","colors.css"),c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),Y=$.join(De.homedir(),".claude","kickflip"),at=$.join(Y,"manifest.json"),q=$.join(Y,"logs"),st=$.join(Y,"run"),ye=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Y,q,st])w.mkdirSync(e,{recursive:!0});try{const e=_n();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await zn();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await Wn();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const L=Un(),Ca=ua(),ee=pn({log:c});ee.apply();let te={version:1,sites:{}};function g(){try{te=JSON.parse(w.readFileSync(at,"utf8")),te.sites||={}}catch(e){e.code==="ENOENT"?te={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return te}l(g,"loadManifest");const O=new Map,C=new Set;let ne=!1;const rt=5e3,ae=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Se=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),B=l(e=>Se(e)?"cmd":e?.port?"port":"files","servesOf"),F=new Map;function se(e){return $.join(st,e+".pid")}l(se,"pidFile");function Aa(e,t){try{const n=Number(w.readFileSync(se(e),"utf8")),r=n?Dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{w.unlinkSync(se(e))}catch{}}l(Aa,"killStale");function Ha(e,t){const n=O.get(e)||{restarts:0},r=w.openSync($.join(q,e+".log"),"a"),[a,...d]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};w.writeSync(r,`
2
+ var Ft=Object.defineProperty;var l=(e,t)=>Ft(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Gt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as zt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Vt,removeSubscriptionById as Zt,sendAll as Qt,flushQueue as Ge,queueStatus as ze,saveSettings as Xt,readSettings as ee,migrateSettings as Yt,setNotifications as qt,readCodexShare as We,setCodexShare as en,readClaudeModel as tn,setClaudeModel as nn,deliver as sn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as Je,answerOf as rn,expired as on,pendingFor as Ke,promptRecord as cn,pushOf as ln,updatePrompts as G,shown as un,pruneRemembered as ye,rememberRules as dn,rememberedAnswer as hn,rememberedFor as Ve}from"./prompts.mjs";import{hold as fn,heldUntil as pn,holdLine as mn,editQuiet as gn,checkEvery as Ze,nextCheckAt as wn,LOW_POINTS as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n,AWAKE_REFUSED as xn}from"./views.mjs";import{caffeinate as Tn,setKeepAwake as vn,awakeUntil as Qe,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as Se,listenState as Xe,RESUME_STATE as Ln,sendToBoards as Ye,checkListening as Cn,recordHubCode as An,hubCode as In,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Hn,linksOf as Un,hubLink as Bn,hubCodeState as Fn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Nn,applyRules as Gn,changed as nt,readHubRules as ke,writeHubRules as zn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Kn,startRoute as Vn,hubHealth as Zn}from"./home.mjs";import{checkSessions as Qn,compactFullContexts as Xn,startingBoards as Yn}from"./sessions.mjs";import{repoService as qn,repoLookupRoute as es}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ts,migrateBoardRepos as ns,migrateHubBoard as ss}from"./board-repo.mjs";import{requestBoard as as,retryBoard as rs,dismissBoard as os,clonePercent as is,settleStarts as cs}from"./new-board.mjs";import{boardClosure as ls,closeBoard as us,closedBoards as ds,reopenBoard as hs}from"./close-board.mjs";import{restartBoard as fs,restartable as ps}from"./restart-board.mjs";import{crossSite as ms,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as gs,colorsVersion as at,destOf as ws,fileVersion as bs,pageOf as rt,shellHtml as ys}from"./shell.mjs";import{hubStream as Ss}from"./stream.mjs";import{usageReader as ks,checkUsagePause as $s,contextReading as _s}from"./usage.mjs";import{readUsageGate as te,setPauseAt as xs,setKeepGoing as Ts,setHubPause as vs,boardPausedUntil as Os}from"../shared/usage-gate.mjs";import{checkMachine as js}from"../shared/machine-gate.mjs";import{readiness as Ps,readCodex as ne}from"./readiness.mjs";import{BuildError as Rs,startBuild as Es,stopTries as Ds,endUnsharedTries as Ls,tryTick as xe,initializeTries as Cs,fetchTryBranches as ot,pullNow as As,pullMains as Is}from"./try.mjs";import{serveFiles as Ms}from"./files.mjs";import{upgrader as Hs,CHECK_MS as Us}from"./upgrade.mjs";import{restarter as Bs}from"./self-restart.mjs";import{fallBackFromLinear as Fs,openBoardBuilders as Ns}from"../board/store.mjs";import{recordRefusal as Gs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as zs,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as it,shareReach as Vs,routeOfPath as Zs,sharedSidebar as Qs,shareTracker as Xs}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as Ys}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as qs,saveLinearKey as ea,savedClient as lt}from"./linear.mjs";import{SlackError as ta,slackStatus as na,removeSlackToken as sa,saveSlackToken as aa}from"./slack.mjs";import{sendReviews as ra}from"./review-send.mjs";const z=$.dirname(zt(import.meta.url)),oa=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),W=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(W,"manifest.json"),ae=$.join(W,"logs"),ht=$.join(W,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[W,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=An();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ns();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await ss();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=qn(),ia=ks(),re=Tn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function ca(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?Gt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(ca,"killStale");function la(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=Ne(t)?We(t):null;let i;try{i=Ee(a,d,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(k){w.writeSync(r,`hub could not start it: ${k.message}
5
- `),w.closeSync(r),Object.assign(n,{child:null,key:ae(t),state:"crashed",exitCode:null,nextStart:Date.now()+ot(n.restarts++)}),O.set(e,n);return}w.closeSync(r),w.writeFileSync(se(e),String(i.pid)),Object.assign(n,{child:i,key:ae(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),O.set(e,n),c(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const S=l((k,y)=>{if(C.delete(i),n.child!==i)return;try{w.unlinkSync(se(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:k??y,nextStart:Date.now()+ot(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${k??y})`)},"ended");i.on("error",k=>{c(`${e} spawn error: ${k.message}`),i.pid||S(k.code,null)}),i.on("exit",S)}l(Ha,"start");const ot=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function re(e){const t=O.get(e);if(!t||!t.child){O.delete(e);return}t.stopping=!0;const n=t.child;oe(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&oe(n,"SIGKILL")},rt)}l(re,"stop");const oe=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function ke(e){return new Promise(t=>{const n=Et.connect({port:e,host:"127.0.0.1"}),r=l(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}l(ke,"probe");let it=!0;const A=new Set;let $e=!1;const U=new Set,Ia=3e4;async function Ma(e){const t=Date.now()+Ia;for(;;){const n=j(g().sites,e),r=O.get(e);if(n&&r?.child&&r.up&&r.key===ae(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}l(Ma,"serverAnswers");async function ct(){if(ne)return;const{sites:e}=g();for(const t of[...O.keys()])A.has(t)||(j(e,t)?Se(j(e,t))||(c(`${t} no longer has a command; stopping it`),re(t),O.delete(t)):(c(`${t} left the manifest; stopping it`),re(t),O.delete(t)));for(const t of[...F.keys()])B(j(e,t))!=="port"&&F.delete(t);for(const[t,n]of Object.entries(e))B(n)==="port"&&F.set(t,await ke(n.port));for(const[t,n]of Object.entries(e)){if(!Se(n))continue;it&&Aa(t,n);const r=O.get(t);if(r&&r.child&&r.key!==ae(n)){c(`${t} entry changed; restarting it`),re(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}const a=r?.child&&r.code&&!r.stopping&&!U.has(t)&&!A.has(t)?We(n):null;if(a&&a!==r.code){if(!(await qe(n.dir)).length){c(`${t} runs older code than its files; restarting it`),re(t),Object.assign(r,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}r.waitedOn!==a&&c(`${t} runs older code than its files; restarts once no builder runs`),r.waitedOn=a}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&Ha(t,n)}it=!1;for(const[t,n]of O){const r=j(e,t);r&&(n.up=n.child?await ke(r.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(ct,"reconcile");const Ba=setInterval(()=>ct().catch(e=>c("reconcile failed:",e.message)),2e3);ct().catch(e=>c("reconcile failed:",e.message));try{Ft()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const ut=l(async()=>{try{for(const e of await xa())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await ut();const Fa=6e4,lt=60*6e4,b={running:null,startedAt:0,firstAt:Date.now()+Fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0},H=Kt(),dt=l(()=>b.startedAt?rn({last:new Date(b.startedAt),...H.state(),remaining:b.remaining}):new Date(b.firstAt),"nextPollAt");function ht({changed:e,back:t}){e&&_.refresh(),t&&b.startedAt&&!b.running&&_e().catch(n=>c("poll failed:",n.message))}l(ht,"presenceChanged");function _e({flush:e=!0}={}){if(b.running)return b.running;const t=new Date;return b.startedAt=t.getTime(),b.running=(async()=>{const n=V();let r;try{r=await wn({every:Fe({present:!1,remaining:b.remaining}),manifest:g()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}b.remaining=r.rate?.remaining??null,r.merged.length&&(await Ye({manifest:g(),log:c,routes:r.merged}),c(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),we({manifest:g(),routes:r.merged}).catch(a=>c("merge refresh failed:",a.message))),await Ye({manifest:g(),log:c});try{const a=await Sa({manifest:g()});a.length&&we({manifest:g(),routes:a}).catch(d=>c("pull refresh failed:",d.message))}catch(a){c("pull failed:",a.message)}try{r.failed.push(...(await yn({manifest:g()})).failed)}catch(a){r.failed.push(`incoming PRs: ${a.message}`)}r.failed.length&&Date.now()-b.errorLoggedAt>lt&&(b.errorLoggedAt=Date.now(),c(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&c(`github check: ${r.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${r.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:d}of r.linear.moved)c(`linear: ${a} \u2192 ${d}`);r.linear.failed.length&&Date.now()-b.linearErrorLoggedAt>lt&&(b.linearErrorLoggedAt=Date.now(),c(`linear check: ${r.linear.failed.length} step${r.linear.failed.length===1?"":"s"} failed, first: ${r.linear.failed[0]} (logged at most hourly)`));try{r.deaf=await $n({manifest:g()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&c(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{vn(g())}catch(a){c("could not record link faults:",a.message)}if(r.flushed=null,r.held=nn(t,n)!==null,e&&!r.held)try{const a=await Ce();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},c(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){c("push queue flush failed:",a.message)}return b.last=r,r})().finally(()=>{b.running=null}),b.running}l(_e,"runPoll");let ft=null;function Ua(){ft||=bn({settings:V(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{ft=null})}l(Ua,"runWakes");const xe=new Set,D={running:null};function pt(){if(D.running||A.size||U.size)return;const e=new Set(xe);xe.clear(),D.running=Mn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Bn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{D.running=null})}l(pt,"runSessionCheck"),await ba(g());const Na=Date.now(),N=new Map;function G(e){e?(ce.delete(e),_.changed(e)):_.refresh()}l(G,"promptChanged");async function mt(){const{gone:e,rulesChanged:t}=await M(n=>{const r=Zt(n,{now:Date.now(),aliveSince:Na}),a=fe(n,ve());for(const d of r)delete n.prompts[d.id];return{gone:r,rulesChanged:a}});for(const n of e)G(n.route);t&&_.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(mt,"prunePrompts");const ve=l(()=>({running:Je(),boards:R(g())}),"promptContext");async function gt(e){const t=R(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await va(Ve(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(gt,"noteRefusal");let z=null,wt=null;async function Te(){return z?wt??=z.catch(()=>{}).then(()=>(wt=null,Te())):(z=(async()=>la({gate:Xe(),resume:l(async e=>{const t=await ze(R(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{z=null}),z)}l(Te,"checkUsageGate"),setInterval(()=>{we({manifest:g()}).catch(e=>c("try tick failed:",e.message)),W.tick().catch(e=>c("kickflip update tick failed:",e.message)),Te().catch(e=>c("usage gate failed:",e.message)),H.check().then(ht,e=>c("presence check failed:",e.message)).finally(()=>{!b.running&&Date.now()>=dt().getTime()&&_e().catch(e=>c("poll failed:",e.message))}),Qn({listening:l(e=>Sn(e)&&ie().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&_.refresh()},e=>c("settling starts failed:",e.message)),Ua(),ee.apply(),mt().catch(e=>c("prompt expiry failed:",e.message)).finally(pt)},15e3);function bt(){if(ne)return;ne=!0,clearInterval(Ba),ee.stop(),c("hub stopping; stopping sites");for(const n of O.values())n.child&&(n.stopping=!0);for(const n of C)oe(n,"SIGTERM");let e=!1;wa().finally(()=>{e=!0});const t=Date.now()+rt;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)c(`pid ${n.pid} ignored SIGTERM; killing it`),oe(n,"SIGKILL");process.exit(0)}},50)}l(bt,"shutdown"),process.on("SIGTERM",bt),process.on("SIGINT",bt);const yt=new Map;function St(e,t={n:3e3}){let n=0,r;try{r=w.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of r){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const d=$.join(e,a.name);if(a.isDirectory())n=Math.max(n,St(d,t));else try{n=Math.max(n,w.statSync(d).mtimeMs)}catch{}}return n}l(St,"newestMtime");function Ga(e,t){const n=yt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=St(t.dir);return yt.set(e,{at:r,checked:Date.now()}),r}l(Ga,"changedAt");const kt=l(e=>{try{return typeof e=="string"&&w.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ie(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=O.get(t)||{},a=B(n),d=a==="files"?kt(n.dir)?"up":"down":a==="port"?F.has(t)?F.get(t)?"up":"down":"starting":r.child?r.up?"up":"starting":r.state==="crashed"?"down":r.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:a,state:d,exitCode:r.exitCode??null,owner:n.owner||null,restart:na(n),changedAt:Ga(t,n)||null,created:n.created||null,log:$.join(q,t+".log")}})}l(ie,"status");const I=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),ce=new Map,$t=2e4,za=1500;async function Wa(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(za)});return t.ok?Cn(await t.json()):null}catch{return null}}l(Wa,"fetchNeeds");async function _t(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const d=ce.get(a.route);(t===!0||t?.has?.(a.route)||!d||Date.now()-d.at>=$t)&&ce.set(a.route,{at:Date.now(),needs:Wa(a.port)})}const r=await Promise.all(n.map(a=>ce.get(a.route).needs));return new Map(n.map((a,d)=>[a.route,r[d]]))}l(_t,"waitingCounts");const xt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Me(null).length,"badgeTotal"),Oe=new Map;function Ja(e){const t=new Map,n=new Map,r={running:Je(),wakes:Pn(kn,{})};for(const a of e)try{const d=Ge(a,r);n.set(a.route,d);let h=Oe.get(a.route);(!h||h.listen!==d||Date.now()-h.at>=$t)&&(h={at:Date.now(),listen:d,links:Tn(a,{listen:d})},Oe.set(a.route,h)),t.set(a.route,h.links)}catch(d){c(`links of ${a.route} failed:`,d.message),t.set(a.route,[])}return{links:t,listens:n}}l(Ja,"boardLinks");async function ue({fresh:e=!1}={}){const t=new Date,n=ie(),r=await _t(n,{fresh:e}),a=g(),d=R(a),{links:h,listens:i}=Ja(d),S=jn(),k=new Map(d.map(u=>[u.route,da(u.owner?.session)]).filter(([,u])=>u)),y=Fn(a,t.getTime(),Zn,u=>i.get(u)==="listening"&&n.some(f=>f.route===u&&f.state==="up")),m=new Set(y.map(u=>u.route)),p=An(n,r,new Set(d.map(u=>u.route)),i,k).filter(u=>!m.has(u.route)),v=V(),s=tn(t,v);return{data:{code:S.running??null,total:xt(r),start:Hn(p),sites:p,hub:In(p,h,On(S)),home:w.realpathSync(De.homedir()),starting:y,usage:Ca({codexShare:He(Z())}),pause:Xe(),upgrade:W.state(),notifications:s?s.why==="off"?{state:"off",text:"Off"}:{state:"held",text:cn(an(s,t))}:{state:"on",text:"On"},away:!H.state().present},links:h,rows:n,code:S}}l(ue,"sidebarState");function vt(e,t,n,r,a){let d=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};d.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(d=Ct.gzipSync(d),h["content-encoding"]="gzip"),h["content-length"]=d.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:d)}l(vt,"sendText");async function Ka(e){const{data:t}=await ue(),n=Le.createContext({});for(const r of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Le.runInContext(w.readFileSync($.join(ge,r),"utf8"),n);return ia({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:le(180)})}l(Ka,"shellPage");const W=$a({root:tt(),builders:l(async()=>(await Promise.all(R(g()).map(e=>qe(e.dir)))).flat(),"builders"),run:Va,log:c,changed:l(()=>_.refresh(),"changed")});function Va(){const e=$.join(q,"kickflip-update.log");w.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=w.openSync(e,"a");return c("running kickflip update"),new Promise(n=>{const r=Ee(process.execPath,[$.join(tt(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});w.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const d=w.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${a}: ${d}`),n(a===0?{ok:!0}:{ok:!1,error:d.replace(/^kickflip: /,"")})})})}l(Va,"runUpdate"),W.check(),setInterval(()=>W.check(),_a);const _=ca({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Ne(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>R(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await ue({fresh:e})).data,"sidebar"),log:c});function le(e){let t="0";try{t=Math.floor(w.statSync($.join(X,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(le,"iconUrl");const Za=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:le(192),sizes:"192x192",type:"image/png"},{src:le(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const r of e)if(n+=r,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function x(e,t){if(!me(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(x,"readJSONBody");const Tt=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
- <title>${I(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${le(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ze()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Qa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Xa=l(()=>({running:!!b.running,nextAt:dt(),last:b.last,present:H.state().present,every:Fe({present:H.state().present,remaining:b.remaining}),low:b.remaining!==null&&b.remaining<on}),"pollState");async function de(e){await mt();const t=new Date,{data:n,links:r,rows:a,code:d}=await ue();return{...un({now:t,st:Ae(t),poll:Xa(),sidebar:n,rows:a,links:r,rules:pe(),integrations:await pa.rows(),code:d,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:He(Z()),claudeModel:Gt(),codex:Z(),awake:Ue(t),remembered:new Map(R(g()).map(h=>[h.route,Be(h.route)]))}),prompts:Me(null).map(Yt),remembered:Be(null)}}l(de,"hubData");const J=l(async()=>{const e=new Date,t={until:Ue(e),unsupported:gn()};return{...ln({now:e,st:Ae(e),rules:pe(),route:await Jt(),linear:Q(),awake:t}),closed:qn()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ya(e,t,n){const r=l((s,o,u,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(u)},"send"),a=l((s,o)=>{const u=xn();return r(s,"application/json",JSON.stringify(o),u?{"x-hub-code":u}:{})},"sendJSON"),d=l(({route:s,started:o})=>(o.then(({error:u})=>{u&&c(`new board /${s} didn't start: ${u}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Qa(e),i=n.pathname,S=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(S&&e.method==="GET")try{return a(200,await Xn(S[1]))}catch(s){return a(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let s;try{const o=await x(e);if(s=o.route,i==="/_hub/reopen-board"){const T=await ea(s);return _.refresh(),a(200,T)}if(A.has(s))return a(409,{error:"That board is already closing."});A.add(s);const u=l(()=>A.delete(s),"release");t.once("finish",u),t.once("close",u),await D.running;const f=await Yn(s,o.name);return _.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(i==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await x(e)).route,U.has(s))return a(409,{error:"That board is already restarting."});U.add(s),await D.running;const o=await ta(s,{serverUp:Ma,portUp:ke,onStarting:l(u=>c(`restart /${s}: ${u}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&c(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{U.delete(s),_.refresh()}}if(i==="/_hub/restart-hub"&&e.method==="POST")try{return await x(e),$e?a(409,{error:"The hub is already restarting."}):($e=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=Pa("hub");s&&($e=!1,c(`hub restart failed: ${s}`))}),a(200,{restarting:!0,command:Ra().restartCommand}))}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await x(e),a(200,{upgrade:await W.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/presence"&&e.method==="POST")return await x(e),ht(H.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await x(e),o=R(g()).find(u=>u.route===s);return o?(xe.add(s),await D.running,pt(),await D.running,Oe.clear(),_.changed(s),a(200,{route:s,listen:Ge(o)})):a(404,{error:"That board is not open."})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/repo"&&e.method==="GET")return Nn(n,{repos:L,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Ve(k[1]):await Gn(k[1],await x(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,s)}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await x(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return d(await Jn(s,{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(y&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return d(await Kn(y[1],{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const s=await Vn(m[1]);return _.refresh(),a(200,s)}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}if((i==="/_hub/build"||i==="/_hub/pull")&&e.method==="POST"){if(ne)return a(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${i==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let u;try{u=await x(e)}catch(f){return a(f.status||400,{error:i==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return i==="/_hub/pull"?a(200,await ya(u,{manifest:g()})):a(202,await ga(u,{manifest:g(),log:c}))}catch(f){if(f instanceof ma)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",w.readFileSync($.join(X,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Za());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",w.readFileSync($.join(X,`icon-${p[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/colors.css"){const s=n.searchParams.get("v")===Ze()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",w.readFileSync(La),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",w.readFileSync($.join(ge,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return r(200,"text/css; charset=utf-8",w.readFileSync($.join(ge,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await ue({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const s=sa(i.slice(10));if(!s){t.writeHead(404).end();return}const o=$.extname(s),u={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===oa(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,u,w.readFileSync(s),{"cache-control":f}):vt(e,t,200,{"content-type":u,"cache-control":f},w.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await de(e));if(i==="/_hub/api/settings")return a(200,await J());if(i==="/_hub/push/key")return a(200,{key:At()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ht(await x(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:It((await x(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Mt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:s.filter(o=>o.status>=200&&o.status<300).length,results:s})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("set"),u=o==="on"?{on:s.get("value")==="on"}:o==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!u)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let T;try{T=await Ut(u)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:hn}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await J()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{mn(s.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):r(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:fn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return ee.apply(),h?a(200,await J()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Nt(s.get("value"),Z({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{zt(s.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):r(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?ha(s.get("value")):fa(s.get("value")),await Te()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(Ea(),c("linear: key removed"),await ut()):c(`linear: key ${(await Da(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof be))throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await J()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=Q();if(!s.set||s.refused)return a(200,s);const o=nt();try{const u=await o.teams();return a(200,{...Q(),teams:u.map(({key:f,name:T})=>({key:f,name:T})),projects:u.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(u){if(!(u instanceof be))throw u;return a(200,{...Q(),error:u.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=nt();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await x(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const u=typeof o.name=="string"?o.name.trim():"";if(!u||u.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await s.createProject(o.team,u))}catch(f){if(!(f instanceof be))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(u=>s.has(u)).map(u=>[u,s.get(u).trim()]));s.get("set")==="hold"&&(o.hold=s.get("value")||"off");try{Bt(sn(V(),o))}catch(u){if(u.status!==400)throw u;return h?a(400,{error:u.message}):r(400,"text/plain; charset=utf-8",u.message)}return _.refresh(),h?a(200,await J()):P(t,"/_hub/#quiet")}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await x(e,64*1024),o=En(pe(),s.rules,s.base??[]),Ke(o.changes)&&Dn(o.rules)}catch(f){if(!(f instanceof Rn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Ke(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const u=await ze(R(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${u.events.map(f=>f.route).join(", ")||"no board"}${u.failed.length?`; ${u.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...u})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ce();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,dn(s)):P(t,"/_hub/")}if(i==="/_hub/poll"&&e.method==="POST"){const s=await x(e);return a(200,await _e({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:u}=await M(f=>{const T=ve(),K=T.boards.find(Pe=>Pe.owner?.session&&Pe.owner.session===s.session_id)||null;fe(f,T)&&_.refresh();const je=en(f,s);if(je)return{answer:je};const he=Qt(s,{board:K,live:T.running?.get(s.session_id)});return f.prompts[he.id]=he,{rec:he}});return o?a(200,{state:"answered",answer:o}):(G(u.route),await gt(s),c(`prompt ${u.id}: ${u.route?`/${u.route}`:u.cwd} ${u.tool}${u.action?` (${u.action})`:""}`),Wt(Xt(u),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${u.id}: push failed: ${f.message}`)),a(200,{id:u.id,deadline:u.deadline}))}if(i==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await gt(s)})}const v=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return v?qa(e,t,{id:v[1],op:v[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ya,"hubRoute");async function qa(e,t,{id:n,op:r,url:a,sendJSON:d}){if(r==="wait"&&e.method==="GET"){const h=l(()=>M(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=l(m=>(G(m.route),d(200,{state:"answered",answer:m.answer})),"take"),S=await h();if(!S)return d(404,{state:"gone"});if(S.answer)return i(S);const k=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Ie,Ie));if(await new Promise(m=>{const p=N.get(n)||new Set;N.set(n,p);const v=l(()=>{clearTimeout(s),p.delete(v),!p.size&&N.get(n)===p&&N.delete(n),m()},"done"),s=setTimeout(v,k);p.add(v),t.on("close",v)}),t.writableEnded||t.destroyed)return;const y=await h();return y?y.answer?i(y):d(200,{state:"pending"}):d(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await x(e),i=await M(y=>{const m=j(y.prompts,n);if(!m)return{status:404,error:"That prompt has expired."};if(m.answer)return{status:409,error:"That prompt was already answered."};const{answer:p,error:v}=Vt(m,h);if(v)return{status:400,error:v};m.answer=p;const s=ve();return fe(y,s),qt(y,m,s),{rec:m,answer:p}});if(i.error)return d(i.status,{error:i.error});const{rec:S,answer:k}=i;c(`prompt ${n}: ${k.choice}`);for(const y of[...N.get(n)||[]])y();return G(S.route),d(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await M(i=>{const S=j(i.prompts,n);return delete i.prompts[n],S});return h&&(G(h.route),c(`prompt ${n}: its hook gave up`)),d(200,{ok:!0})}return!1}l(qa,"promptRoute");const es=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function Ot(e,t){const n=O.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=B(t),[d,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="files"?["down","down \xB7 its folder is missing"]:n.why==="code"?["wait","restarting \xB7 its code changed"]:n.child?["wait","starting \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${r}`],i=a==="cmd"?"The hub runs every site and restarts one that stops.":a==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${Tt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=Se(t)?qe(t):null;let h;try{h=Be(i,a,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
+ `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(la,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ua=3e4;async function da(e){const t=Date.now()+ua;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(da,"serverAnswers");async function gt(){if(ie)return;wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...J.keys()])U(R(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ca(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Ns(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&la(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=Bs({root:ve(),files:In().files,restart:l(()=>ct("hub"),"restart"),log:c}),ha=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Yt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const fa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=an(),yt=l(()=>y.startedAt?wn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(St,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Pn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Is({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ra({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Cn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Hn(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=pn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let kt=null;function pa(){kt||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{kt=null})}l(pa,"runWakes");const Ce=new Set,A={running:null};function $t(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Qn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Xn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l($t,"runSessionCheck"),await Cs(g());const ma=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=on(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(_t,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function xt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Gs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(xt,"noteRefusal");let Q=null,Tt=null;async function he(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,he())):(Q=(async()=>$s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{js()}catch(e){c("machine check failed:",e.message)}F.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=yt().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),cs({listening:l(e=>Dn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),re.apply(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function vt(){if(ie)return;ie=!0,clearInterval(ha),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ds().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,jt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(jt,"newestMtime");function ga(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=jt(t.dir);return Ot.set(e,{at:s,checked:Date.now()}),s}l(ga,"changedAt");const Pt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Pt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ps(n),changedAt:ga(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Rt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ba,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Rt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Et,"waitingCounts");const Dt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Ln,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Rt)&&(p={at:Date.now(),listen:a,links:Un(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(ya,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Et(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=ya(a),u=Fn(),_=new Map(a.map(o=>[o.route,_s(o.owner?.session)]).filter(([,o])=>o)),k=Yn(i,t.getTime(),is,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>Os(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Kn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=fn(t,O);return{data:{code:u.running??null,version:ut,total:Dt(s),start:Vn(w),sites:w,hub:Zn(w,p,Bn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:ia({codexShare:We(ne())}),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Lt(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Lt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return ys({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(Sa,"shellPage");const Ct=Hs({root:ve(),run:ka,log:c,pulled:l(()=>wt.restartNow(),"pulled")});function ka(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[$.join(ve(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const a=b.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l(ka,"runUpdate"),Ct.check(),setInterval(()=>Ct.check(),Us);const x=Ss({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const $a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function v(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(v,"readJSONBody");const At=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),xa=l(()=>({running:!!y.running,nextAt:yt(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<bn}),"pollState");async function ge(e){await _t();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...Sn({now:t,st:ze(t),poll:xa(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Ps.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(ne()),claudeModel:tn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(un),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:On()},n=te({now:e}),s=n.manual==="hub"?jn(n.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:se(),slack:na(),awake:t,pausedUntil:s}),closed:ds()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ta(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Mn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=_a(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await ls(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await hs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await us(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,K.has(r))return a(409,{error:"That board is already restarting."});K.add(r),await A.running;const o=await fs(r,{serverUp:da,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{K.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ys().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),St(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,$t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return es(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ts(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await as(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await rs(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await os(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await As(d,{manifest:g()})):a(202,await Es(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Rs)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(oa),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Ks(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=gs(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===bs(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Lt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Kt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Vt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Qt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await qt(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:_n}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:xn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vs(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{en(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{nn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?xs(r.get("value")):Ts(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(qs(),c("linear: key removed"),await bt()):c(`linear: key ${(await ea(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(sa(),c("slack: token removed")):c(`slack: token ${(await aa(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ta))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Xt(gn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=Gn(ke(),r.rules,r.base??[]),nt(o.changes)&&zn(o.rules)}catch(f){if(!(f instanceof Nn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,$n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=hn(f,r);if(q)return{answer:q};const be=cn(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await xt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),sn(ln(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await xt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ta,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Je,Je));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=rn(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),dn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(va,"promptRoute");const Oa=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(It,"downWhy");function Mt(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(ja(t,n))}l(Mt,"sendDown");function ja(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=It(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${I(t.title||e)} isn't answering</h1>
12
- <p class="lede">${I(i)}</p>
11
+ <h1>${N(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${N(u)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${es({icon:Ln(e,t),title:I(t.title||e),sub:`<span class="st ${d}">${I(h)}</span>`})}</div>
14
+ <div class="group">${Oa({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(Ot,"downPage");const jt=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Pt=Re.createServer((e,t)=>{et(e).then(n=>n==="owner"?ts(e,t):Oa(e,t,n))});function ts(e,t){const n=jt(e);if(e.method!=="GET"&&e.method!=="HEAD"&&aa(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const r=new Set(Object.keys(g().sites));if(Qe({method:e.method,pathname:n.pathname,search:n.search,dest:ra(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Ka(m).then(p=>vt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},p)).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ye,pid:process.pid},sites:ie()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=ie();_t(m,{fresh:n.searchParams.get("fresh")==="1"}).then(p=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:xt(p),sites:Object.fromEntries(m.map(v=>[v.route,p.get(v.route)??null]))}))}).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){Ya(e,t,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=n.pathname.split("/")[1],d=j(g().sites,a);if(!d){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${Tt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${I(a)}</h1></main></body></html>`);return}if(n.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+n.search}).end();return}const h="/"+a,i=e.url.slice(h.length)||"/",S=Qe({pathname:n.pathname,dest:"document",sites:r})==="shell";if(B(d)==="files"){if(!kt(d.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d));return}ka(e,t,{dir:d.dir,rest:i,prefix:h,headers:S?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},y=Re.request({host:"127.0.0.1",port:d.port,method:e.method,path:i,headers:k},m=>{const p={...m.headers};S&&(p.vary=[p.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof p.location=="string"&&p.location.startsWith("/")&&!p.location.startsWith("//")&&!p.location.startsWith(h+"/")&&p.location!==h&&(p.location=h+p.location),t.writeHead(m.statusCode,p),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),y.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d))}),e.pipe(y)}l(ts,"serve"),Pt.on("upgrade",async(e,t,n)=>{const r=await et(e);if(r!=="owner"){ja(t,r);return}const a=jt(e).pathname.split("/")[1],d=j(g().sites,a);if(!d?.port){t.destroy();return}const h=`/${a}`;Ta({req:e,socket:t,head:n,port:d.port,path:e.url.slice(h.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${h}`]})}),Pt.listen(ye,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${ye}/ (manifest ${at})`));
16
+ </main></body></html>`}l(ja,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Xs({file:$.join(W,"share-seen.json")}),we=new Set;function Ht(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Ls()}l(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Ut(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Vs({method:e.method,pathname:i,routes:a}):"other",h=Zs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Qs(_,a,u?.name||u?.login),"sidebar")}}}l(Ut,"access");const Bt=Ue.createServer((e,t)=>{Ut(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Pa(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Pa(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ms(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:ws(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);Sa(m,n).then(w=>Lt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Et(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Dt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Ta(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Pt(p.dir)){Mt(t,a,p);return}Ms(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ht),S.on("error",()=>{if(t.headersSent){t.destroy();return}Mt(t,a,p)}),e.pipe(S)}l(Pa,"serve"),Bt.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ut(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;zs({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Bt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
package/hub/incoming.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var N=Object.defineProperty;var c=(n,r)=>N(n,"name",{value:r,configurable:!0});import g from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as C}from"./pr-report.mjs";import{cardPRs as E}from"../shared/card-prs.mjs";const k=c((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(t=>t.pr));return n.filter(t=>!t.draft&&!k(t.author,i)&&!s.has(t.number)).map(t=>{const u=new Set(t.files),f=[...new Set(r.filter(o=>o.files.some(m=>u.has(m))).map(o=>o.key))],e=t.requested.some(o=>k(o,i));return f.length||e?{number:t.number,title:t.title,author:t.author,url:t.url,head:t.head,touches:f,asked:e}:null}).filter(Boolean)}c(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;g.mkdirSync(S.dirname(i),{recursive:!0}),g.writeFileSync(s,JSON.stringify(r,null,2)+`
2
- `),g.renameSync(s,i)}c(M,"writeIncoming");async function _({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=C}){const s=F(y,{}),t=[],u=[];let f;for(const e of v(n).filter(o=>o.auto_review&&o.github))try{const o=e.githubLogin||(f??=await i()),m=await r.openPRs(e.github),$=new Map(m.map(l=>[l.number,l])),d=[];for(const{slug:l,wave:I}of e.waves)for(const b of I.items.filter(a=>a?.status!=="dropped"))for(const{number:a}of E(b,e.github).filter(h=>h.repo===e.github)){const h=`${e.github}#${a}`;let p=$.get(a)?.files||s[h];if(!p){const w=await r.prFiles(e.github,a);p=w.files,w.state==="MERGED"&&(s[h]=p)}d.push({key:`${l}/${b.id}`,pr:a,files:p})}M(e.dir,{at:new Date().toISOString(),repo:e.github,prs:J({open:m,cards:d,me:o})}),t.push(e.route)}catch(o){u.push(`${e.route}: ${o.message}`)}return L(y,s),{checked:t,failed:u}}c(_,"checkIncoming");export{_ as checkIncoming,J as incomingFor};
1
+ var N=Object.defineProperty;var u=(n,r)=>N(n,"name",{value:r,configurable:!0});import d from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as P}from"./pr-report.mjs";import{cardPRs as C,storedPRs as E}from"../shared/card-prs.mjs";const k=u((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(e=>e.pr));return n.filter(e=>!e.draft&&!k(e.author,i)&&!s.has(e.number)).map(e=>{const f=new Set(e.files),m=[...new Set(r.filter(o=>o.files.some(l=>f.has(l))).map(o=>o.key))],t=e.requested.some(o=>k(o,i));return m.length||t?{number:e.number,title:e.title,author:e.author,url:e.url,head:e.head,touches:m,asked:t}:null}).filter(Boolean)}u(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;d.mkdirSync(S.dirname(i),{recursive:!0}),d.writeFileSync(s,JSON.stringify(r,null,2)+`
2
+ `),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=P}){const s=F(y,{}),e=[],f=[];let m;for(const t of v(n).filter(o=>o.auto_review&&o.github))try{const o=t.githubLogin||(m??=await i()),l=await r.openPRs(t.github),$=new Map(l.map(h=>[h.number,h])),p=[];for(const{slug:h,wave:I}of t.waves)for(const b of I.items.filter(a=>a&&(a.lane!=="done"||E(a).some(c=>c.merged_at))))for(const{number:a}of C(b,t.github).filter(c=>c.repo===t.github)){const c=`${t.github}#${a}`;let g=$.get(a)?.files||s[c];if(!g){const w=await r.prFiles(t.github,a);g=w.files,w.state==="MERGED"&&(s[c]=g)}p.push({key:`${h}/${b.id}`,pr:a,files:g})}M(t.dir,{at:new Date().toISOString(),repo:t.github,prs:J({open:l,cards:p,me:o})}),e.push(t.route)}catch(o){f.push(`${t.route}: ${o.message}`)}return L(y,s),{checked:e,failed:f}}u(q,"checkIncoming");export{q as checkIncoming,J as incomingFor};
package/hub/linear.mjs CHANGED
@@ -1,2 +1 @@
1
- var j=Object.defineProperty;var a=(s,e)=>j(s,"name",{value:e,configurable:!0});import f from"node:fs";import v from"node:path";import _ from"node:os";class p extends Error{static{a(this,"LinearError")}constructor(e,r=502){super(e),this.status=r}}const k=a((s=_.homedir())=>v.join(s,".claude","kickflip","linear.json"),"keyFile"),C=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function g(s){try{const e=JSON.parse(f.readFileSync(k(s),"utf8"));return typeof e?.key=="string"&&e.key?e:null}catch{return null}}a(g,"readLinearKey");function $(s){const e=g(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a($,"linearStatus");const E=a(s=>{const e=$(s);return e.set&&!e.refused},"linearWorks"),P="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function I(s){const e=k();f.mkdirSync(v.dirname(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;f.writeFileSync(r,JSON.stringify(s,null,2)+`
2
- `,{mode:384}),f.chmodSync(r,384),f.renameSync(r,e)}a(I,"writeKeyFile");function L(s,{fetch:e=globalThis.fetch,checked:r=a(()=>{},"checked")}={}){async function u(n,t={}){let i;try{i=await e(C(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new p(`Could not reach Linear: ${c.message}`)}const o=await i.json().catch(()=>null);if(i.status===401||o?.errors?.some(c=>c.extensions?.type==="authentication error"))throw r(!1),new p("Linear refused the key.",400);if(o?.errors?.length){const c=o.errors[0];throw Object.assign(new p(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!i.ok||!o?.data)throw new p(`Linear answered ${i.status}.`);return r(!0),o.data}a(u,"call");const l="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await u(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${l} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new p(`Linear has no team ${n}.`,400)})()}a(d,"team");const m=a((n,t)=>n.projects.nodes.find(i=>i.id===t)||n.projects.nodes.find(i=>i.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new p(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await u("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await u("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const i=await u("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:i.team.projects.nodes.map(o=>({id:o.id,name:o.name}))}}))},async createProject(n,t){const i=await d(n),o=await u("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[i.id]}});if(!o.projectCreate.success)throw new p("Linear did not create the project.");return{...o.projectCreate.project,team:i.key}},async createIssue({team:n,project:t,title:i,description:o}){const c=await d(n),y={teamId:c.id,projectId:m(c,t).id,title:i,...o?{description:o}:{}},h=await u("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:y});if(!h.issueCreate.success)throw new p("Linear did not create the issue.");return h.issueCreate.issue},async issue(n){try{const t=await u("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:i,created:o}={}){const c=await w.issue(n);if(!c||c.assignee!==i&&!o||x(c.state)>={started:1,review:2,done:3}[t])return null;const y=S((await d(c.team)).states.nodes,t);if(!y)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:y.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return y.name},async moveIssue(n,t){const i=await w.issue(n);if(!i)throw new p(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(i.state.type))return null;const o=S((await d(i.team)).states.nodes,t);if(!o||o.id===i.state.id)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:o.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return o.name}};return w}a(L,"linearClient");function x(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(x,"stageOf");function S(s,e){const r=[...s].sort((d,m)=>d.position-m.position),u=a(d=>r.filter(m=>m.type===d),"of"),l=a(d=>/review/i.test(d.name),"review");return e==="started"?u("started").find(d=>/^in progress$/i.test(d.name))||u("started").find(d=>!l(d))||null:e==="review"?u("started").find(l)||null:e==="done"&&(u("completed").find(d=>/^done$/i.test(d.name))||u("completed")[0])||null}a(S,"stateFor");async function N(s,{fetch:e}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new p("Paste a Linear personal API key.",400);const u=new Date().toISOString();let l=!1;const d=await L(r,{fetch:e,checked:a(m=>{l=!m},"checked")}).viewer().catch(m=>{if(!l)throw m;return null});return I(d?{key:r,...d,saved_at:u}:{key:r,saved_at:u,refused_at:u}),$()}a(N,"saveLinearKey");function F(s,e){const r=g();if(r?.key!==s||!!r.refused_at==!e)return;const{refused_at:u,...l}=r;I(e?l:{...l,refused_at:new Date().toISOString()})}a(F,"markChecked");function U(){return f.rmSync(k(),{force:!0}),$()}a(U,"removeLinearKey");function z({fetch:s}={}){const e=g();return e?{...L(e.key,{fetch:s,checked:a(r=>F(e.key,r),"checked")}),saved:e}:null}a(z,"savedClient");export{P as LINEAR_NEEDS_KEY,p as LinearError,L as linearClient,$ as linearStatus,E as linearWorks,g as readLinearKey,U as removeLinearKey,N as saveLinearKey,z as savedClient,x as stageOf,S as stateFor};
1
+ var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
package/hub/live.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function y(t=l){let e=[];try{e=c.readdirSync(t).filter(n=>n.endsWith(".json"))}catch{}return e.flatMap(n=>{try{const r=JSON.parse(c.readFileSync(f.join(t,n),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(y,"sessionRecords");const N=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"running");function d(t){try{return h("ps",t,{encoding:"utf8",timeout:3e3,env:{...process.env,TZ:"UTC"}})}catch(e){return typeof e.status=="number"&&!e.signal?e.stdout||"":null}}s(d,"ps");function m(t){const e=new Map,n=t.filter(N);if(!n.length)return e;const r=d(["-o","pid=,lstart=","-p",n.join(",")]);if(r===null)return null;for(const o of r.split(`
2
- `)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function O(t,e=m){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const n=u(t.procStart);if(!Number.isFinite(n))return!1;const r=e([t.pid])?.get(t.pid);return Number.isFinite(r)&&Math.abs(n-r)<=a}s(O,"verifiedProcess");function b({dir:t=l,starts:e=m,session:n=null}={}){const r=y(t).filter(i=>!n||i.sessionId===n),o=e([...new Set(r.map(i=>i.pid))]);return o&&new Map(x(r,o).map(i=>[i.sessionId,i]))}s(b,"liveSessions");function I(t){const e=d(["-o","ppid=","-p",String(t)]);return e===null?void 0:Number(e.trim())||null}s(I,"parentOf");function R(t,e,{ppid:n=process.ppid,parent:r=I,live:o=b}={}){if(n!==e[0])return!0;const i=r(e[0]);if(i===null||i!==void 0&&e[1]!==void 0&&i!==e[1])return!0;const p=o({session:t});return p!==null&&!p.has(t)}s(R,"sessionGone");export{l as SESSIONS_DIR,x as liveOf,b as liveSessions,I as parentOf,m as processStarts,d as ps,R as sessionGone,y as sessionRecords,O as verifiedProcess};
1
+ var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function N(t=l){let e=[];try{e=c.readdirSync(t).filter(n=>n.endsWith(".json"))}catch{}return e.flatMap(n=>{try{const r=JSON.parse(c.readFileSync(f.join(t,n),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(N,"sessionRecords");const b=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"running");function d(t){const e=Number(process.env.KICKFLIP_PS_TIMEOUT_MS)||3e3;try{return h("ps",t,{encoding:"utf8",timeout:e,env:{...process.env,TZ:"UTC"}})}catch(n){return typeof n.status=="number"&&!n.signal?n.stdout||"":null}}s(d,"ps");function m(t){const e=new Map,n=t.filter(b);if(!n.length)return e;const r=d(["-o","pid=,lstart=","-p",n.join(",")]);if(r===null)return null;for(const o of r.split(`
2
+ `)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function _(t,e=m){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const n=u(t.procStart);if(!Number.isFinite(n))return!1;const r=e([t.pid])?.get(t.pid);return Number.isFinite(r)&&Math.abs(n-r)<=a}s(_,"verifiedProcess");function y({dir:t=l,starts:e=m,session:n=null}={}){const r=N(t).filter(i=>!n||i.sessionId===n),o=e([...new Set(r.map(i=>i.pid))]);return o&&new Map(x(r,o).map(i=>[i.sessionId,i]))}s(y,"liveSessions");function I(t){const e=d(["-o","ppid=","-p",String(t)]);return e===null?void 0:Number(e.trim())||null}s(I,"parentOf");function j(t,e,{ppid:n=process.ppid,parent:r=I,live:o=y}={}){if(n!==e[0])return!0;const i=r(e[0]);if(i===null||i!==void 0&&e[1]!==void 0&&i!==e[1])return!0;const p=o({session:t});return p!==null&&!p.has(t)}s(j,"sessionGone");export{l as SESSIONS_DIR,x as liveOf,y as liveSessions,I as parentOf,m as processStarts,d as ps,j as sessionGone,N as sessionRecords,_ as verifiedProcess};
package/hub/locks.mjs CHANGED
@@ -1 +1 @@
1
- var d=Object.defineProperty;var a=(r,c)=>d(r,"name",{value:c,configurable:!0});import t from"node:fs";import o from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:s=1e4,heartbeat:m=!1}={}){const e=`${r}.lock`;t.mkdirSync(o.dirname(r),{recursive:!0});const y=Date.now()+s;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(Date.now()>y)throw Object.assign(new Error(`${o.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=m?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}a(u,"withFileLock");export{u as withFileLock};
1
+ var f=Object.defineProperty;var o=(r,c)=>f(r,"name",{value:c,configurable:!0});import t from"node:fs";import a from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:m=1e4,heartbeat:s=!1}={}){const e=`${r}.lock`;t.mkdirSync(a.dirname(r),{recursive:!0});const y=performance.now()+m;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(performance.now()>y)throw Object.assign(new Error(`${a.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=s?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}o(u,"withFileLock");export{u as withFileLock};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var w=(r,s)=>g(r,"name",{value:s,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,s,u){const m=s.ownerChecks||{},i={},d=new Set;for(const e of r)for(const{slug:f,wave:p}of e.waves)for(const n of p.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||!["done","merged"].includes(n.status))continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,f,n.id,c.check]);if(d.has(o))continue;d.add(o);const t=`${f}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(m[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,t,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${t}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,t,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${t} on its passing check (${k.message})`)}}s.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
1
+ var C=Object.defineProperty;var w=(r,t)=>C(r,"name",{value:t,configurable:!0});import{WAVES as g}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,t,u){const p=t.ownerChecks||{},i={},f=new Set;for(const e of r)for(const{slug:d,wave:m}of e.waves)for(const n of m.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||n.lane!=="done")continue;const o=JSON.stringify([e.kind==="waves"?g:e.dir,d,n.id,c.check]);if(f.has(o))continue;f.add(o);const s=`${d}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(p[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,s,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${s}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,s,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${s} on its passing check (${k.message})`)}}t.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
package/hub/pr-report.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var y=Object.defineProperty;var u=(t,n)=>y(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const _="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",f="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=u(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function d(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,f].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(l,i,m)=>{if(l)return o(new Error(g(m||l.message)));try{s(JSON.parse(i))}catch(a){o(new Error(g(`unreadable output (${a.message})`)))}})})}u(d,"gh");const R="\u{1F916} ",b="<!-- claude-bridge -->";async function x({repo:t,pr:n,replyTo:s,words:o,login:l}){let i=process.env;if(l){const a=await new Promise(r=>h("gh",["auth","token","-u",l],{env:{...i,PATH:[i.PATH,f].filter(Boolean).join(":")}},(c,e)=>r(c?"":e.trim())));if(!a)throw new Error(`gh has no ${l} account; run gh auth login`);i={...i,GH_TOKEN:a}}const m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await d(["api",m,"-f",`body=${R}${o}
1
+ var _=Object.defineProperty;var i=(t,n)=>_(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const E="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",g="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",w=i(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function p(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(r,u,m)=>{if(r)return o(new Error(w(m||r.message)));try{s(JSON.parse(u))}catch(l){o(new Error(w(`unreadable output (${l.message})`)))}})})}i(p,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(t){const n=process.env;if(!t)return n;const s=await new Promise(o=>h("gh",["auth","token","-u",t],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")}},(r,u)=>o(r?"":u.trim())));if(!s)throw new Error(`gh has no ${t} account; run gh auth login`);return{...n,GH_TOKEN:s}}i(y,"envFor");async function O({repo:t,pr:n,replyTo:s,words:o,login:r}){const u=await y(r),m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await p(["api",m,"-f",`body=${v}${o}
2
2
 
3
- ${b}`],i)).html_url}catch(a){throw new Error(`${t}#${n}: ${a.message}`)}}u(x,"postToPR");function C({repo:t,pr:n}){return new Promise((s,o)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,f].filter(Boolean).join(":")},timeout:9e4},(l,i,m)=>l?o(new Error(g(m||l.message))):s())})}u(C,"markReady");const D=u(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function A({repo:t,pr:n,since:s}){let o;try{o=await d(["pr","view",String(n),"--repo",t,"--json",_])}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}const l=u(r=>d(["api","--paginate","--slurp",`repos/${t}/${r}`]).then(c=>c.flat()).catch(()=>[]),"list"),[i,m,a]=await Promise.all([l(`issues/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/reviews`)]);return{view:o,issueComments:i,reviewComments:m,reviews:a}}u(A,"fetchPR");const w="author { __typename login }",v=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${w} createdAt body url } } reviews(last: 50) { nodes { databaseId ${w} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${w} createdAt body url path line replyTo { databaseId } } } } }`;function S(t,n=process.env){return new Promise((s,o)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,f].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(l,i,m)=>{let a=null;try{a=JSON.parse(i)}catch{}if(a?.data)return s(a);o(new Error(`GitHub query: ${g(l?m||l.message:"unreadable output")}`))})})}u(S,"graphql");const $=u(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user");async function T(t){const n=t.map(({repo:i,pr:m},a)=>{const[r,c]=i.split("/");return`pr${a}: repository(owner: ${JSON.stringify(r)}, name: ${JSON.stringify(c)}) { pullRequest(number: ${Number(m)}) { ${v} } }`}),{data:s,errors:o=[]}=await S(`query { viewer { login } rateLimit { remaining resetAt } ${n.join(" ")} }`),l=t.map(({repo:i,pr:m},a)=>{const r=s[`pr${a}`]?.pullRequest;if(!r)return{error:`${i}#${m}: ${o.find(e=>e.path?.[0]===`pr${a}`)?.message||"GitHub did not answer for it"}`};const c=u(e=>({id:e.databaseId??null,user:$(e.author),created_at:e.createdAt??null,body:e.body??null,html_url:e.url??null}),"comment");return{view:{state:r.state,isDraft:r.isDraft,reviewDecision:r.reviewDecision,mergeable:r.mergeable,mergeStateStatus:r.mergeStateStatus,headRefOid:r.headRefOid,statusCheckRollup:r.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[]},issueComments:(r.comments?.nodes??[]).map(c),reviewComments:(r.reviewThreads?.nodes??[]).flatMap(e=>e.comments?.nodes??[]).map(e=>({...c(e),path:e.path??null,line:e.line??null,in_reply_to_id:e.replyTo?.databaseId??null})),reviews:(r.reviews?.nodes??[]).map(e=>({id:e.databaseId??null,user:$(e.author),submitted_at:e.submittedAt??null,body:e.body??null,state:e.state??null,html_url:e.url??null}))}});return{login:s.viewer?.login??null,rate:s.rateLimit??null,results:l}}u(T,"fetchPRs");function O({view:t,issueComments:n,reviewComments:s,reviews:o},{pr:l,since:i,me:m}){const a=u((e,p)=>({kind:p,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),r=[...n.map(e=>({...a(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...a(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...o.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>i).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,p)=>e.at<p.at?-1:e.at>p.at?1:0),c=t.statusCheckRollup??[];return{pr:Number(l),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:c.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:c.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:r}}u(O,"reportOf");async function I(t){let n;try{n=await d(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(o=>o.path),requested:(s.reviewRequests||[]).map(o=>o.login).filter(Boolean)}))}u(I,"openPRs");async function H(t,n){let s;try{s=await d(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}return{state:s.state??null,files:(s.files||[]).map(o=>o.path)}}u(H,"prFiles");async function L({env:t}={}){const n=await d(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}u(L,"githubLogin");export{b as CLAUDE_MARK,R as CLAUDE_PREFIX,_ as PR_VIEW_FIELDS,D as checksPassed,A as fetchPR,T as fetchPRs,L as githubLogin,C as markReady,I as openPRs,x as postToPR,H as prFiles,O as reportOf};
3
+ ${b}`],u)).html_url}catch(l){throw new Error(`${t}#${n}: ${l.message}`)}}i(O,"postToPR");async function A({repo:t,pr:n,head:s,body:o,login:r}){const u=await y(r);try{return(await p(["api",`repos/${t}/pulls/${n}/reviews`,"-f",`body=${o}`,"-f","event=COMMENT","-f",`commit_id=${s}`],u)).html_url}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}}i(A,"postReview");async function T(t){return(await p(["api","--paginate","--slurp",`repos/${t}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}i(T,"collaborators");async function I(t){try{return(await p(["api",`users/${encodeURIComponent(t)}`])).login}catch{return null}}i(I,"githubUser");async function H({repo:t,pr:n,since:s}){try{return(await p(["api","--paginate","--slurp",`repos/${t}/pulls/${n}/reviews`])).flat().filter(r=>Date.parse(r.submitted_at)>Date.parse(s)).map(r=>({login:r.user?.login??null,at:r.submitted_at}))}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}}i(H,"reviewsSince");function L({repo:t,pr:n}){return new Promise((s,o)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,g].filter(Boolean).join(":")},timeout:9e4},(r,u,m)=>r?o(new Error(w(m||r.message))):s())})}i(L,"markReady");const N=i(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function q({repo:t,pr:n,since:s}){let o;try{o=await p(["pr","view",String(n),"--repo",t,"--json",E])}catch(c){throw new Error(`${t}#${n}: ${c.message}`)}const r=i(c=>p(["api","--paginate","--slurp",`repos/${t}/${c}`]).then(d=>d.flat()).catch(()=>[]),"list"),[u,m,l]=await Promise.all([r(`issues/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/reviews`)]);return{view:o,issueComments:u,reviewComments:m,reviews:l}}i(q,"fetchPR");const $="author { __typename login }",S=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles latestReviews(first: 50) { nodes { state } } recent: commits(last: 20) { nodes { commit { oid messageHeadline } } } commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;function P(t,n=process.env){return new Promise((s,o)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(r,u,m)=>{let l=null;try{l=JSON.parse(u)}catch{}if(l?.data)return s(l);o(new Error(`GitHub query: ${w(r?m||r.message:"unreadable output")}`))})})}i(P,"graphql");const R=i(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user"),C="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function U(t,n=[]){const s=t.map(({repo:l,pr:c},d)=>{const[e,f]=l.split("/");return`pr${d}: repository(owner: ${JSON.stringify(e)}, name: ${JSON.stringify(f)}) { pullRequest(number: ${Number(c)}) { ${S} } }`});n.forEach((l,c)=>{const[d,e]=l.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(d)}, name: ${JSON.stringify(e)}) { ${C} }`)});const{data:o,errors:r=[]}=await P(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),u=Object.fromEntries(n.flatMap((l,c)=>o[`open${c}`]?.pullRequests?[[l,o[`open${c}`].pullRequests.nodes]]:[])),m=t.map(({repo:l,pr:c},d)=>{const e=o[`pr${d}`]?.pullRequest;if(!e)return{error:`${l}#${c}: ${r.find(a=>a.path?.[0]===`pr${d}`)?.message||"GitHub did not answer for it"}`};const f=i(a=>({id:a.databaseId??null,user:R(a.author),created_at:a.createdAt??null,body:a.body??null,html_url:a.url??null}),"comment");return{view:{state:e.state,isDraft:e.isDraft,reviewDecision:e.reviewDecision,mergeable:e.mergeable,mergeStateStatus:e.mergeStateStatus,headRefOid:e.headRefOid,statusCheckRollup:e.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,changedFiles:e.changedFiles??null,approved:(e.latestReviews?.nodes??[]).some(a=>a.state==="APPROVED"),commits:(e.recent?.nodes??[]).map(a=>({oid:a.commit.oid.slice(0,10),subject:a.commit.messageHeadline}))},issueComments:(e.comments?.nodes??[]).map(f),reviewComments:(e.reviewThreads?.nodes??[]).flatMap(a=>a.comments?.nodes??[]).map(a=>({...f(a),path:a.path??null,line:a.line??null,in_reply_to_id:a.replyTo?.databaseId??null})),reviews:(e.reviews?.nodes??[]).map(a=>({id:a.databaseId??null,user:R(a.author),submitted_at:a.submittedAt??null,body:a.body??null,state:a.state??null,html_url:a.url??null}))}});return{login:o.viewer?.login??null,rate:o.rateLimit??null,results:m,open:u}}i(U,"fetchPRs");function k({view:t,issueComments:n,reviewComments:s,reviews:o},{pr:r,since:u,me:m}){const l=i((e,f)=>({kind:f,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),c=[...n.map(e=>({...l(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...l(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...o.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>u).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,f)=>e.at<f.at?-1:e.at>f.at?1:0),d=t.statusCheckRollup??[];return{pr:Number(r),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:d.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:d.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:c,title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,files:t.changedFiles??null,approved:!!t.approved,commits:t.commits??[]}}i(k,"reportOf");async function F(t){let n;try{n=await p(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(o=>o.path),requested:(s.reviewRequests||[]).map(o=>o.login).filter(Boolean)}))}i(F,"openPRs");async function B(t,n){let s;try{s=await p(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}return{state:s.state??null,files:(s.files||[]).map(o=>o.path)}}i(B,"prFiles");async function j({env:t}={}){const n=await p(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}i(j,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,E as PR_VIEW_FIELDS,N as checksPassed,T as collaborators,q as fetchPR,U as fetchPRs,j as githubLogin,I as githubUser,L as markReady,F as openPRs,A as postReview,O as postToPR,B as prFiles,k as reportOf,H as reviewsSince};
package/hub/presence.mjs CHANGED
@@ -1 +1 @@
1
- var f=Object.defineProperty;var o=(r,t)=>f(r,"name",{value:t,configurable:!0});import{execFile as p}from"node:child_process";const d=3*6e4,m=o(r=>new Promise((t,n)=>{p("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(e,a)=>e?n(e):t(a))}),"ioreg");async function I(r=m){try{const[t,n]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),e=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(t),a=[...n.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!a.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...a));return{atMac:!e&&s<d,locked:e,idleMs:s}}catch(t){return{atMac:!1,error:String(t.message||t)}}}o(I,"readPresence");function g({run:r=m,ttl:t=5e3,clock:n=Date.now}={}){let e=null;return()=>((!e||n()-e.at>=t)&&(e={at:n(),reading:I(r)}),e.reading)}o(g,"presenceReader");const M=g();function w({read:r=M,clock:t=Date.now}={}){let n=-1/0,e=!1,a=null;const s=o(i=>{const l=t(),c=i||l-n<d,u={present:c,changed:c!==e,back:c&&!e};return u.back&&(a=l),e=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return n=t(),s(!0)},state:o(()=>({present:e,backAt:a}),"state")}}o(w,"ownerTracker");export{d as IDLE_LIMIT_MS,w as ownerTracker,M as presence,g as presenceReader,I as readPresence};
1
+ var m=Object.defineProperty;var a=(r,e)=>m(r,"name",{value:e,configurable:!0});import{execFile as f}from"node:child_process";const d=3*6e4,I=a(r=>new Promise((e,o)=>{f("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function p(r=I){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...n));return{atMac:!t&&s<d,locked:t,idleMs:s}}catch(e){return{atMac:!1,error:String(e.message||e)}}}a(p,"readPresence");function h({read:r=p,clock:e=Date.now}={}){let o=-1/0,t=!1,n=null;const s=a(i=>{const l=e(),c=i||l-o<d,u={present:c,changed:c!==t,back:c&&!t};return u.back&&(n=l),t=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return o=e(),s(!0)},state:a(()=>({present:t,backAt:n}),"state")}}a(h,"ownerTracker");export{d as IDLE_LIMIT_MS,h as ownerTracker,p as readPresence};