@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/push.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var G=Object.defineProperty;var r=(t,e)=>G(t,"name",{value:e,configurable:!0});import m from"node:crypto";import u from"node:fs";import y from"node:path";import x from"node:os";import{validate as Y,quietOf as C,flushAt as Z,hold as F,notifications as tt,snoozeEnd as et}from"./schedule.mjs";import{storedEnd as nt}from"./duration.mjs";import{presence as K}from"./presence.mjs";import{CODEX_SHARE_VALUES as ot,codexStatus as I,effectiveCodexShare as rt}from"../shared/codex.mjs";const j=y.join(x.homedir(),".claude","kickflip","push"),z=y.join(j,"vapid.json"),O=y.join(j,"subs.json"),h=y.join(j,"queue.json"),D=h+".lock",J=r(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=r(t=>Buffer.from(t).toString("base64url"),"b64u"),$=r(t=>Buffer.from(t,"base64url"),"unb64u"),B=r((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=r((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function L(){try{return JSON.parse(u.readFileSync(z,"utf8"))}catch{}const t=m.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return l(z,t),t}r(L,"vapidKey");function st(){const t=L();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}r(st,"publicKey");function g(t=O){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}r(g,"readSubs");function xt(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),l(O,o),o.length}r(xt,"addSubscription");function bt(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&l(O,n),n.length}r(bt,"removeSubscription");function Et(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):O).map(e=>({host:new URL(e.endpoint).host,agent:e.agent,added:e.added}))}r(Et,"subscriptions");const M=r(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function T(t,e=!1){const n=g();if(t!=="action"||e||!(await K()).atMac)return n;const o=n.filter(i=>!M(i));return o.length?o:n}r(T,"reach");async function jt(){return{atMac:(await K()).atMac,desktops:g().filter(t=>!M(t)).length}}r(jt,"pushRoute");function it(t,e){const n=$(t.keys.p256dh),o=m.createECDH("prime256v1"),i=o.generateKeys(),a=B($(t.keys.auth),o.computeSecret(n)),s=B(a,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),c=m.randomBytes(16),d=B(c,s),k=B(d,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),b=B(d,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),E=m.createCipheriv("aes-128-gcm",k,b),v=Buffer.concat([E.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),E.final(),E.getAuthTag()]),f=Buffer.alloc(4);return f.writeUInt32BE(4096),Buffer.concat([c,f,Buffer.from([i.length]),i,v])}r(it,"encrypt");function at(t){const e=L(),n=P(JSON.stringify({typ:"JWT",alg:"ES256"})),o=P(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=m.sign("sha256",Buffer.from(`${n}.${o}`),{key:m.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${P(i)}, k=${st()}`}r(at,"authorization");async function R(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}r(R,"hubNeeds");const Ot=r(t=>N(g(),t),"sendAll");async function N(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await R()).total}),i=await Promise.all(t.map(async s=>{try{const c=await fetch(s.endpoint,{method:"POST",headers:{authorization:at(s),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:it(s,o),signal:AbortSignal.timeout(15e3)});return{sub:s,status:c.status,error:c.ok?void 0:(await c.text()).slice(0,300)}}catch(c){return{sub:s,status:0,error:String(c.message||c)}}})),a=new Set(i.filter(s=>s.status===404||s.status===410).map(s=>s.sub.endpoint));return a.size&&l(O,g().filter(s=>!a.has(s.endpoint))),i.map(s=>({host:new URL(s.sub.endpoint).host,agent:s.sub.agent,status:s.status,error:s.error,dropped:a.has(s.sub.endpoint)||void 0}))}r(N,"sendTo");const Q=r(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=x.homedir()){try{const e=JSON.parse(u.readFileSync(J(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}r(w,"readSettingsFile");function U(t,e=x.homedir()){const n=J(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
2
- `),u.renameSync(o,n)}r(U,"writeSettingsFile");const Bt=r((t=I(),e=x.homedir())=>rt(w(e).codexShare,t),"readCodexShare");function vt(t,e=I()){if(!ot.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return U({...w(),codexShare:t}),t}r(vt,"setCodexShare");const H=["opus","auto","sonnet"],At=r((t=x.homedir())=>{const e=w(t).claudeModel;return H.includes(e)?e:"auto"},"readClaudeModel");function Dt(t,e=x.homedir()){if(!H.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return U({...w(e),claudeModel:t},e),t}r(Dt,"setClaudeModel");function q(t){return C(w(t))}r(q,"readSettings");function Pt(t){const e=w(t);if(!e.polling&&!e.push)return null;const{push:n,polling:o,...i}=e,{quiet:a}=C(e);return U({...i,quiet:a},t),a}r(Pt,"migrateSettings");function ct(t){const{settings:e,error:n}=Y({notifications:q().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,polling:i,...a}=w();return U({...a,quiet:e.quiet,notifications:e.notifications}),e}r(ct,"saveSettings");async function $t(t,e=new Date){const n=q(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const s=t.snooze==="off"?null:et(e,t.snooze,n);if(s===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=s&&nt(s)}const{notifications:i}=ct({...n,notifications:o}),a=i.on?0:await S(()=>{const{items:s}=p();return s.length&&l(h,{items:[]}),s.length});return{notifications:i,dropped:a}}r($t,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}r(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(D);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(D).mtimeMs>3e4){u.rmdirSync(D);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(D)}catch{}}}r(S,"withQueueLock");function ut(t=new Date){const e=q(),{items:n}=p(),o=F(t,e);return{settings:e,held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?Z(t,e):null}}r(ut,"queueStatus");const V=["action","prompt","fault","reminder"];class ft extends Error{static{r(this,"PushKindError")}}const dt=r(t=>V.includes(t)?null:`a push needs --kind ${V.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Ut(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const a=dt(e);if(a)throw new ft(a);if(!g().length)return{noDevice:!0};const s={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},c=F(i,q());if(c?.why==="off")return{dropped:!0};const d=c!==null;let k=!1;if(n||o&&!d){const f=await N(await T(e,n),s);if(Q(f))return s.tag&&await S(()=>l(h,{items:p().items.filter(A=>A.tag!==s.tag)})),{sent:f};if(n)return{sent:f};k=!0}const b={id:m.randomUUID(),...s,kind:e,queuedAt:i.toISOString()},E=await S(()=>{const f=p(),A=b.tag?f.items.filter(X=>X.tag!==b.tag):f.items,W=A.length<f.items.length;return f.items=[...A,b],l(h,f),{replaced:W}}),v=ut(i);return{queued:{...E,count:v.count,nextFlushAt:v.nextFlushAt,held:d,hold:c,sendFailed:k}}}r(Ut,"deliver");async function qt(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(a=>a.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),l(h,o),i.body):null})}r(qt,"reviseQueued");async function Nt(t,e){const n=new Set(t),o=r(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),a=i.filter(c=>!o(c)),s=i.length-a.length;return s&&l(h,{items:a}),s}):0}r(Nt,"dropQueued");let _=null;function _t(){return _||=(async()=>{const t=await S(()=>{const a=p();return a.items.length&&l(h,{items:[]}),a.items});if(!t.length)return null;const e=[],n=[],o=[],i=await R();for(const a of tt(t,i.sites)){const s=await N(await T(a.kind),{title:a.title,body:a.body,url:a.url,tag:a.tag},i);o.push(...s),(Q(s)?e:n).push(a)}return n.length&&await S(()=>{const a=p(),s=new Set(a.items.map(d=>d.tag).filter(Boolean)),c=t.filter(d=>n.some(k=>k.members.includes(d.id))&&!(d.tag&&s.has(d.tag)));l(h,{items:[...c,...a.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{_=null}),_}r(_t,"flushQueue");export{H as CLAUDE_MODEL_VALUES,V as PUSH_KINDS,ft as PushKindError,xt as addSubscription,Ut as deliver,Nt as dropQueued,_t as flushQueue,Pt as migrateSettings,st as publicKey,dt as pushKindError,jt as pushRoute,ut as queueStatus,At as readClaudeModel,Bt as readCodexShare,q as readSettings,w as readSettingsFile,bt as removeSubscription,qt as reviseQueued,ct as saveSettings,Ot as sendAll,Dt as setClaudeModel,vt as setCodexShare,$t as setNotifications,Et as subscriptions,U as writeSettingsFile};
1
+ var V=Object.defineProperty;var r=(t,e)=>V(t,"name",{value:e,configurable:!0});import h from"node:crypto";import u from"node:fs";import y from"node:path";import k from"node:os";import{validate as X,quietOf as _,flushAt as G,hold as I,notifications as Y,snoozeEnd as Z}from"./schedule.mjs";import{storedEnd as tt}from"./duration.mjs";import{CODEX_SHARE_VALUES as et,codexStatus as K,effectiveCodexShare as nt}from"../shared/codex.mjs";const j=y.join(k.homedir(),".claude","kickflip","push"),L=y.join(j,"vapid.json"),b=y.join(j,"subs.json"),m=y.join(j,"queue.json"),C=m+".lock",z=r(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),F=r(t=>Buffer.from(t).toString("base64url"),"b64u"),D=r(t=>Buffer.from(t,"base64url"),"unb64u"),v=r((t,e)=>h.createHmac("sha256",t).update(e).digest(),"hmac"),f=r((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function J(){try{return JSON.parse(u.readFileSync(L,"utf8"))}catch{}const t=h.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return f(L,t),t}r(J,"vapidKey");function ot(){const t=J();return F(Buffer.concat([Buffer.from([4]),D(t.x),D(t.y)]))}r(ot,"publicKey");function g(t=b){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}r(g,"readSubs");function St(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),f(b,o),{count:o.length,id:N(t.endpoint)}}r(St,"addSubscription");const N=r(t=>h.createHash("sha256").update(t).digest("hex").slice(0,16),"deviceId");function xt(t){const e=g(),n=e.filter(o=>N(o.endpoint)!==t);return n.length!==e.length&&f(b,n),n.length}r(xt,"removeSubscriptionById");function kt(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&f(b,n),n.length}r(kt,"removeSubscription");function rt(t=""){const e=["iPhone","iPad","Android","Windows","Macintosh","Linux"].find(o=>t.includes(o)),n=[["Edg/","Edge"],["OPR/","Opera"],["Firefox/","Firefox"],["FxiOS/","Firefox"],["CriOS/","Chrome"],["Chrome/","Chrome"],["Safari/","Safari"]].find(([o])=>t.includes(o));return{kind:e==="Macintosh"?"Mac":e||"",browser:n?.[1]||""}}r(rt,"deviceNames");function bt(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):b).map(e=>({id:N(e.endpoint),host:new URL(e.endpoint).host,agent:e.agent,added:e.added,...rt(e.agent)}))}r(bt,"subscriptions");function st(t,e){const n=D(t.keys.p256dh),o=h.createECDH("prime256v1"),i=o.generateKeys(),c=v(D(t.keys.auth),o.computeSecret(n)),s=v(c,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),a=h.randomBytes(16),l=v(a,s),x=v(l,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),E=v(l,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),O=h.createCipheriv("aes-128-gcm",x,E),B=Buffer.concat([O.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),O.final(),O.getAuthTag()]),d=Buffer.alloc(4);return d.writeUInt32BE(4096),Buffer.concat([a,d,Buffer.from([i.length]),i,B])}r(st,"encrypt");function it(t){const e=J(),n=F(JSON.stringify({typ:"JWT",alg:"ES256"})),o=F(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=h.sign("sha256",Buffer.from(`${n}.${o}`),{key:h.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${F(i)}, k=${ot()}`}r(it,"authorization");async function M(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}r(M,"hubNeeds");const Et=r(t=>U(g(),t),"sendAll");async function U(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await M()).total}),i=await Promise.all(t.map(async s=>{try{const a=await fetch(s.endpoint,{method:"POST",headers:{authorization:it(s),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:st(s,o),signal:AbortSignal.timeout(15e3)});return{sub:s,status:a.status,error:a.ok?void 0:(await a.text()).slice(0,300)}}catch(a){return{sub:s,status:0,error:String(a.message||a)}}})),c=new Set(i.filter(s=>s.status===404||s.status===410).map(s=>s.sub.endpoint));return c.size&&f(b,g().filter(s=>!c.has(s.endpoint))),i.map(s=>({host:new URL(s.sub.endpoint).host,agent:s.sub.agent,status:s.status,error:s.error,dropped:c.has(s.sub.endpoint)||void 0}))}r(U,"sendTo");const T=r(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=k.homedir()){try{const e=JSON.parse(u.readFileSync(z(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}r(w,"readSettingsFile");function P(t,e=k.homedir()){const n=z(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
2
+ `),u.renameSync(o,n)}r(P,"writeSettingsFile");const Ot=r((t=K(),e=k.homedir())=>nt(w(e).codexShare,t),"readCodexShare");function jt(t,e=K()){if(!et.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return P({...w(),codexShare:t}),t}r(jt,"setCodexShare");const R=["opus","auto","sonnet"],vt=r((t=k.homedir())=>{const e=w(t).claudeModel;return R.includes(e)?e:"auto"},"readClaudeModel");function Bt(t,e=k.homedir()){if(!R.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return P({...w(e),claudeModel:t},e),t}r(Bt,"setClaudeModel");function $(t){return _(w(t))}r($,"readSettings");function At(t){const e=w(t);if(!e.polling&&!e.push)return null;const{push:n,polling:o,...i}=e,{quiet:c}=_(e);return P({...i,quiet:c},t),c}r(At,"migrateSettings");function ct(t){const{settings:e,error:n}=X({notifications:$().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,polling:i,...c}=w();return P({...c,quiet:e.quiet,notifications:e.notifications}),e}r(ct,"saveSettings");async function Ct(t,e=new Date){const n=$(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const s=t.snooze==="off"?null:Z(e,t.snooze,n);if(s===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=s&&tt(s)}const{notifications:i}=ct({...n,notifications:o}),c=i.on?0:await S(()=>{const{items:s}=p();return s.length&&f(m,{items:[]}),s.length});return{notifications:i,dropped:c}}r(Ct,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(m,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}r(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(C);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(C).mtimeMs>3e4){u.rmdirSync(C);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(C)}catch{}}}r(S,"withQueueLock");function at(t=new Date){const e=$(),{items:n}=p(),o=I(t,e);return{settings:e,held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?G(t,e):null}}r(at,"queueStatus");const Q=["action","prompt","fault","reminder"];class ut extends Error{static{r(this,"PushKindError")}}const dt=r(t=>Q.includes(t)?null:`a push needs --kind ${Q.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Ft(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const c=dt(e);if(c)throw new ut(c);if(!g().length)return{noDevice:!0};const s={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},a=I(i,$());if(a?.why==="off")return{dropped:!0};const l=a!==null;let x=!1;if(n||o&&!l){const d=await U(g(),s);if(T(d))return s.tag&&await S(()=>f(m,{items:p().items.filter(A=>A.tag!==s.tag)})),{sent:d};if(n)return{sent:d};x=!0}const E={id:h.randomUUID(),...s,kind:e,queuedAt:i.toISOString()},O=await S(()=>{const d=p(),A=E.tag?d.items.filter(W=>W.tag!==E.tag):d.items,H=A.length<d.items.length;return d.items=[...A,E],f(m,d),{replaced:H}}),B=at(i);return{queued:{...O,count:B.count,nextFlushAt:B.nextFlushAt,held:l,hold:a,sendFailed:x}}}r(Ft,"deliver");async function Dt(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(c=>c.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),f(m,o),i.body):null})}r(Dt,"reviseQueued");async function Pt(t,e){const n=new Set(t),o=r(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),c=i.filter(a=>!o(a)),s=i.length-c.length;return s&&f(m,{items:c}),s}):0}r(Pt,"dropQueued");let q=null;function $t(){return q||=(async()=>{const t=await S(()=>{const c=p();return c.items.length&&f(m,{items:[]}),c.items});if(!t.length)return null;const e=[],n=[],o=[],i=await M();for(const c of Y(t,i.sites)){const s=await U(g(),{title:c.title,body:c.body,url:c.url,tag:c.tag},i);o.push(...s),(T(s)?e:n).push(c)}return n.length&&await S(()=>{const c=p(),s=new Set(c.items.map(l=>l.tag).filter(Boolean)),a=t.filter(l=>n.some(x=>x.members.includes(l.id))&&!(l.tag&&s.has(l.tag)));f(m,{items:[...a,...c.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{q=null}),q}r($t,"flushQueue");export{R as CLAUDE_MODEL_VALUES,Q as PUSH_KINDS,ut as PushKindError,St as addSubscription,Ft as deliver,rt as deviceNames,Pt as dropQueued,$t as flushQueue,At as migrateSettings,ot as publicKey,dt as pushKindError,at as queueStatus,vt as readClaudeModel,Ot as readCodexShare,$ as readSettings,w as readSettingsFile,kt as removeSubscription,xt as removeSubscriptionById,Dt as reviseQueued,ct as saveSettings,Et as sendAll,Bt as setClaudeModel,jt as setCodexShare,Ct as setNotifications,bt as subscriptions,P as writeSettingsFile};
package/hub/readiness.mjs CHANGED
@@ -1,2 +1 @@
1
- var T=Object.defineProperty;var i=(t,n)=>T(t,"name",{value:n,configurable:!0});import f from"node:fs";import m from"node:os";import y from"node:path";import{execFile as N}from"node:child_process";import{githubLogin as P}from"./pr-report.mjs";import{codexStatusReader as w}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{readSettings as C,subscriptions as O}from"./push.mjs";import{tailscaleState as I}from"../bin/install.mjs";import{tailscaleBin as L,tailscaleInstallCommand as F}from"../shared/tailscale.mjs";const x=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],k=i(t=>y.join(t,".claude","kickflip","integrations.json"),"reportFile");function j(t=m.homedir()){try{return JSON.parse(f.readFileSync(k(t),"utf8")).slack||null}catch{return null}}i(j,"readSlackReport");function W(t,{home:n=m.homedir(),now:s=new Date,route:d=null,title:o="Board"}={}){const e=k(n),r={ready:t,at:s.toISOString(),route:d,title:o};f.mkdirSync(y.dirname(e),{recursive:!0});const a=`${e}.${process.pid}.tmp`;return f.writeFileSync(a,JSON.stringify({slack:r},null,2)+`
2
- `,{mode:384}),f.renameSync(a,e),r}i(W,"reportSlack");const D=i((t,n)=>{const s=Math.floor((n-Date.parse(t))/6e4);return s<1?"just now":s<60?`${s} min ago`:s<1440?`${Math.floor(s/60)} h ago`:`${Math.floor(s/1440)} days ago`},"age"),u=i((t,n,s)=>({settings:t,label:`Hub settings \u203A ${n}`,text:s}),"settingsFix");function $(t,{now:n=Date.now(),platform:s=process.platform}={}){const d=i((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:u("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:u("linear","Linear","Add one in")};case"slack":{const r=e?`${e.title}\u2019s session reported ${e.ready?"a":"no"} Slack connector, ${D(e.at,n)}`:"No board session has reported yet";return e?.ready?{ready:!0,detail:r}:{ready:!1,missing:r,fix:{text:"Add Slack to Claude\u2019s connectors. The next board session to start reports it."}}}case"tailscale":{let r,a;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(r=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,a="sudo tailscale set --operator=$USER"):(r="Tailscale / is not serving the hub",a="kickflip doctor --fix"):(r="Tailscale isn't signed in",a="tailscale up"):(r="Tailscale isn't installed on the host",a=F(s)),r?{ready:!1,missing:r,fix:{command:a}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices?{ready:!0,detail:`Pushes on \xB7 ${e.devices} device${e.devices===1?"":"s"}`}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:u("notifications","Notifications","Turn on notifications in")}}},"row");return x.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,r])=>({id:o,name:e,...d(o,t[o]),waits:r}))}i($,"readinessRows");const H=i(t=>(n,s)=>new Promise(d=>{N(n,s,{env:t,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function A({home:t=m.homedir(),env:n=process.env,now:s=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=w({home:t,env:n,now:s,cacheMs:d}),r={github:i(()=>P({env:n}).catch(()=>null),"github"),codex:e,linear:i(()=>R(t),"linear"),slack:i(()=>j(t),"slack"),tailscale:i(()=>I({run:H(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:L({env:n})}),"tailscale"),notifications:i(()=>({on:C(t).notifications.on,devices:O(t).length}),"notifications"),...o},a=new Map;async function g(l,c){if(!["github","tailscale"].includes(l))return r[l]({fresh:c});const p=a.get(l);if(!c&&p&&s()-p.at<d)return p.value;const h=Promise.resolve().then(()=>r[l]());return a.set(l,{at:s(),value:h}),h}i(g,"check");const b=i(async({fresh:l=!1}={})=>$(Object.fromEntries(await Promise.all(x.map(async([c])=>[c,await g(c,l)]))),{now:s()}),"rows");async function S(l){const[c]=$({[l]:await g(l,!1)},{now:s()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return i(S,"waiting"),{rows:b,waiting:S}}i(A,"readinessReader");const M=w(),X=A({probes:{codex:M}});export{M as readCodex,j as readSlackReport,X as readiness,A as readinessReader,$ as readinessRows,W as reportSlack};
1
+ var k=Object.defineProperty;var t=(s,n)=>k(s,"name",{value:n,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as S}from"./pr-report.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as P}from"./linear.mjs";import{slackStatus as T}from"./slack.mjs";import{readSettings as R,subscriptions as C}from"./push.mjs";import{tailscaleState as N}from"../bin/install.mjs";import{tailscaleBin as I,tailscaleInstallCommand as L}from"../shared/tailscale.mjs";const w=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],f=t((s,n,a)=>({settings:s,label:`Hub settings \u203A ${n}`,text:a}),"settingsFix");function x(s,{now:n=Date.now(),platform:a=process.platform}={}){const d=t((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:f("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:f("linear","Linear","Add one in")};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",fix:f("slack","Slack","Paste a new one in")}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`}:{ready:!1,missing:"No Slack token set",fix:f("slack","Slack","Add one in")};case"tailscale":{let i,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(i=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(i="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(i="Tailscale isn't signed in",c="tailscale up"):(i="Tailscale isn't installed on the host",c=L(a)),i?{ready:!1,missing:i,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:i,kind:c,browser:m,added:p})=>({id:i,kind:c,browser:m,added:p}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:f("notifications","Notifications","Turn on notifications in")}}},"row");return w.filter(([o])=>Object.hasOwn(s,o)).map(([o,e,i])=>({id:o,name:e,...d(o,s[o]),waits:i}))}t(x,"readinessRows");const O=t(s=>(n,a)=>new Promise(d=>{$(n,a,{env:s,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function H({home:s=b.homedir(),env:n=process.env,now:a=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=h({home:s,env:n,now:a,cacheMs:d}),i={github:t(()=>S({env:n}).catch(()=>null),"github"),codex:e,linear:t(()=>P(s),"linear"),slack:t(()=>T(s),"slack"),tailscale:t(()=>N({run:O(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:I({env:n})}),"tailscale"),notifications:t(()=>({on:R(s).notifications.on,devices:C(s)}),"notifications"),...o},c=new Map;async function m(l,r){if(!["github","tailscale"].includes(l))return i[l]({fresh:r});const g=c.get(l);if(!r&&g&&a()-g.at<d)return g.value;const u=Promise.resolve().then(()=>i[l]());return c.set(l,{at:a(),value:u}),u}t(m,"check");const p=t(async({fresh:l=!1}={})=>x(Object.fromEntries(await Promise.all(w.map(async([r])=>[r,await m(r,l)]))),{now:a()}),"rows");async function y(l){const[r]=x({[l]:await m(l,!1)},{now:a()});return r.ready||!r.waits?null:r.waits.replace("this",`${r.name}: ${r.missing.replace(/\.$/,"")}`)}return t(y,"waiting"),{rows:p,waiting:y}}t(H,"readinessReader");const A=h(),q=H({probes:{codex:A}});export{A as readCodex,q as readiness,H as readinessReader,x as readinessRows};
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub settings \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function N({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(N,"sendReviews");export{N as sendReviews};
@@ -0,0 +1,2 @@
1
+ var d=Object.defineProperty;var c=(r,e)=>d(r,"name",{value:e,configurable:!0});import n from"node:fs";import o from"node:path";import m from"node:os";const l=c((r=m.homedir())=>o.join(r,".claude","kickflip"),"dirOf"),a=c(r=>o.join(l(r),"secrets.json"),"secretsFile");function f(r,e){n.mkdirSync(o.dirname(r),{recursive:!0});const t=`${r}.${process.pid}.tmp`;n.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `,{mode:384}),n.chmodSync(t,384),n.renameSync(t,r)}c(f,"write");function p(r){const e=a(r);let t={};try{t=JSON.parse(n.readFileSync(e,"utf8"))||{}}catch{}const i=o.join(l(r),"linear.json");if(n.existsSync(i)){let s=null;try{s=JSON.parse(n.readFileSync(i,"utf8"))}catch{}!t.linear&&s&&(t={...t,linear:s},f(e,t)),n.rmSync(i,{force:!0})}return t}c(p,"readAll");const h=c((r,e)=>p(e)[r]||null,"readSecret");function x(r,e){const{[r]:t,...i}=p();f(a(),e?{...i,[r]:e}:i)}c(x,"writeSecret");export{h as readSecret,a as secretsFile,x as writeSecret};
@@ -0,0 +1,2 @@
1
+ var y=Object.defineProperty;var e=(n,i)=>y(n,"name",{value:i,configurable:!0});import d from"node:fs";import h from"node:path";import{execFileSync as b}from"node:child_process";const g=["index.lock","MERGE_HEAD","rebase-merge","rebase-apply","CHERRY_PICK_HEAD","REVERT_HEAD"];function w({root:n,files:i,restart:p,log:s=e(()=>{},"log"),git:c=e((...r)=>b("git",r,{cwd:n,encoding:"utf8"}),"git")}){const r=d.existsSync(h.join(n,".git"));let a=!1;function u(){try{const t=c("rev-parse","HEAD").trim();return a=!1,t}catch(t){return a||s(`self-restart can't read HEAD: ${t.message.split(`
2
+ `)[0]}`),a=!0,null}}e(u,"head");const f=r?u():null;let l=null;function o(t,R){if(l===t)return;l=t,s(`restarting the hub: ${R}`);const m=p();m&&s(`hub restart failed: ${m}`)}e(o,"attempt");const E=e(()=>g.some(t=>d.existsSync(h.join(n,".git",t))),"gitBusy"),A=e(()=>{try{return c("diff","--quiet","HEAD","--",...i),!0}catch{return!1}},"filesMatchHead");function H(){if(!r||E())return;const t=u();t===null||f===null||t===f||!A()||o(t,`HEAD moved to ${t.slice(0,7)} and the hub's files match it`)}e(H,"check");function D(){o(`npm-${Date.now()}`,"kickflip update finished")}return e(D,"restartNow"),{check:H,restartNow:D}}e(w,"restarter");export{w as restarter};
package/hub/sessions.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var ne=Object.defineProperty;var a=(e,s)=>ne(e,"name",{value:s,configurable:!0});import x from"node:fs";import w from"node:path";import j from"node:os";import{spawn as K,spawnSync as oe}from"node:child_process";import{fileURLToPath as ie}from"node:url";import{joinName as re,routeRecord as h,validRoute as ae}from"./routes.mjs";import{withFileLock as ce}from"./locks.mjs";import{RESUME_STATE as D,carryCursor as ue,hubBoard as W,isListening as de,listBoards as L,listenState as le,oldestUnheard as fe,readManifest as I,readJSON as P,saveJSON as J,watchedCounts as pe,watchedLines as me,watcherCursor as he,withManifestLock as we,writeManifest as ge}from"./boards.mjs";import{readUsageGate as be,heldUsageEvent as ke}from"../shared/usage-gate.mjs";import{liveSessions as _,sessionRecords as $e,processStarts as ye,verifiedProcess as Ie}from"./live.mjs";import{deliver as Se}from"./push.mjs";import{WAIT_MS as _e}from"./prompts.mjs";import{nodeBin as H,storedPath as X}from"../bin/install.mjs";import{reposDir as Te}from"../shared/repos.mjs";import{isClone as xe}from"./repos.mjs";import{trustFolder as je}from"./trust.mjs";import{contextFull as Oe,contextReading as ve}from"./usage.mjs";const O=X(w.resolve(w.dirname(ie(import.meta.url)),"..")),Ae=3*6e4,Ee=5*6e4,Ce=15*6e4,q=w.join(j.homedir(),".claude","kickflip","compact-state.json"),De=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ce,"startFailed");function Le(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Le,"startStep");function ps(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=De(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Le(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a(ps,"startingBoards");const Pe=_e/1e3+60,Ue=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${H()}" "${w.join(O,"hub","permission-hook.mjs")}"`,timeout:Pe}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${H()}" "${w.join(O,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ne(e,s){if(!ae(e))throw new m("Choose a board route.","none");return ce(w.join(j.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Xe(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ne,"withSessionLock");function v(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=w.join(j.homedir(),".local","bin","claude");return x.existsSync(e)?e:"claude"}a(v,"claudeBin");const A=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Q(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>x.existsSync(e))||"tmux"}a(Q,"tmuxBin");function U(e){const s=oe(Q(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!A(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(U,"runTmux");const S=a(e=>`kickflip-${e}`,"tmuxName"),N=a((e,s)=>e(["has-session","-t",`=${S(s)}`]).status===0,"hasTmux");async function ms({manifest:e=I(),now:s=new Date,running:t=_(),wakes:r=P(D,{}),tmux:i=U}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),P(q,{})),o=[],l=[];let u=!1;const c=W(e)?.route;for(const p of L(e)){const f=ve(p.owner?.session);if(!Oe(le(p,{now:s.getTime(),running:t,wakes:r}),f)){h(n,p.route)&&(delete n[p.route],u=!0);continue}if(p.route!==c||h(n,p.route)||!N(i,p.route))continue;const d=`=${S(p.route)}:`,$=i(["send-keys","-t",d,"-l","/compact"]),k=$.status===0?i(["send-keys","-t",d,"Enter"]):$,g=k.status===0?null:(k.stderr||k.stdout).trim().slice(0,300)||`tmux exited ${k.status}`;n[p.route]={session:p.owner.session,percent:f.percent,at:s.toISOString(),...g?{error:g}:{}},u=!0,g?l.push(`${p.route}: could not send /compact to tmux session ${S(p.route)} (${g})`):o.push(p.route)}return u&&J(q,n),{compacted:o,failed:l}}a(ms,"compactFullContexts");function B(e,{cwd:s,timeout:t=6e4}){const r=v(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!A(n)));return i.CLAUDE_BG_ISOLATION="none",w.isAbsolute(r)&&(i.PATH=`${w.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",l="",u;try{u=K(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(f){n({status:1,stdout:o,stderr:f.message});return}const c=a(f=>{clearTimeout(p),u.stdout.destroy(),u.stderr.destroy(),n({status:f,stdout:o,stderr:l})},"done"),p=setTimeout(()=>{try{u.kill("SIGKILL")}catch{}c(1)},t);u.stdout.on("data",f=>{o+=f}),u.stderr.on("data",f=>{l+=f}),u.on("error",f=>{l+=f.message,c(1)}),u.on("close",f=>c(f??1)),u.on("exit",f=>setTimeout(()=>c(f??1),300)),u.unref()})}a(B,"runClaude");async function Be(e,{cwd:s,run:t=B,starts:r=ye,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!Ie(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Be,"runSessionStop");async function hs(e,{live:s=_,stop:t=Be}={}){if(!e?.session)return;const r=s();if(!r)throw new m(ee(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||j.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(l=>setTimeout(l,100))}}a(hs,"stopSession");function Me(e){const s=`=${S(e)}`;return Re(Q(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(Me,"attachTmux");function Re(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!A(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const l=K(e,s,{env:t,stdio:"inherit"});l.on("error",u=>{i(),o(new m(u.message,"failed"))}),l.on("close",u=>{i(),n({status:u??1})})})}a(Re,"runForeground");const M=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Ue,...s?["--model",s]:[]],"common"),Fe=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),E=a((e,s)=>`--name ${Fe(re(e,s))}`,"boardFlag"),Ge=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...M({name:s,model:t}),`/kickflip ${E(e,s)}${r?` ${r}`:""}`],"startArgv"),Ke=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${w.join(O,"bin","kickflip.mjs")} join ${E(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),We=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:M({name:t,model:r}),Ke(s,e,t)],"resumeArgv"),Y=a((e,s)=>`You are the hub session. Read \`${w.join(O,"docs","hub-session.md")}\`, then rejoin /${e} (join ${E(e,s)}) and relaunch its watcher.`,"hubPrompt"),z=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...M({name:s,model:r}),t?`${Y(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${E(e,s)} --hub
2
- ${Y(e,s)}`],"hubArgv");function V(e,s,{cwd:t,tmux:r}){const i=v(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(A)])].flatMap(l=>["-u",l]),o=r(["new-session","-d","-s",S(e),"-c",t,"/usr/bin/env",...n,...w.isAbsolute(i)?[`PATH=${w.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(V,"startTmux");async function ws({manifest:e=I(),repo:s,tmux:t=U,attach:r=Me,live:i=_}={}){const n=W(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!N(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");F(o,i);const l=o?.repo||w.resolve(s||process.cwd());if(!x.statSync(l,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${l} is gone; rejoin from its new folder with --repo`:`no folder at ${l}`,"none");V(n.route,z({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:l,tmux:t})}return r(n.route)}a(ws,"hubSession");function R(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(R,"sessionOf");async function Z(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=$e().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(Z,"startedId");async function Je(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Je,"isBackground");const ee=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy Mac. Try again in a moment.`,"unknownLiveness");function F(e,s){if(!e)return;const t=s();if(!t)throw new m(ee(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(F,"refuseLive");async function se(e,{from:s,to:t,mark:r}){await we(async()=>{const i=I(),n=L(i).find(l=>l.route===e);if(n?.owner?.session===s&&(r||t!==s)&&ue(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await ge(i)})}a(se,"moveOwner");async function gs(e,{manifest:s=I(),repo:t,model:r,prompt:i,run:n=B,live:o=_,clones:l=Te()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const u=R(s,e);F(u,o);const c=w.resolve(t||u?.repo||process.cwd());if(!x.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(xe(c,l))try{je(c)}catch(g){throw new m(`Couldn't trust this folder for Claude Code: ${g.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,f=Ge({route:e,name:p,model:r||u?.model,prompt:i}),d=await n(f,{cwd:c});if(d.status!==0){const g=(d.stderr||"").trim()||(d.stdout||"").trim();throw new m(g?`claude --bg failed (${d.status}): ${g.slice(0,300)}`:`claude exited ${d.status} with no output; check \`claude agents\``,"failed")}const{session:$,confirmed:k}=await Z(d.stdout);if(!$)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${d.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:$,confirmed:k,repo:X(c),model:r||u?.model||null,name:p,argv:[v(),...f],output:d.stdout.trim()}}a(gs,"startSession");function He(e,s={}){return Ne(e,({resume:t})=>t(s))}a(He,"resumeSession");async function Xe(e,{manifest:s=I(),run:t=B,tmux:r=U,live:i=_,mark:n=null}={}){if(h(I().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=R(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(F(o,i),h(s.sites,e)?.hub===!0){if(N(r,e))throw new m(`/${e}'s tmux session ${S(e)} is still open; attach with kickflip hub`,"live");const d=V(e,z({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await se(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:d,output:`in tmux session ${S(e)}`}}const l=await Je(o.session,{run:t,cwd:o.repo}),u=We({id:o.session,route:e,name:o.title,model:o.model,background:l}),c=await t(u,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:f}=await Z(c.stdout,o.session);return await se(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:f,repo:o.repo,argv:[v(),...u],output:c.stdout.trim()}}a(Xe,"resumeRecordedSession");function qe(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:l}){if(s)return{action:"none",entry:null};const u=new Date(l).toISOString(),c=e?{...e}:{down_since:u};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||l-Date.parse(c.resumed_at)>=Ee?{action:"push",entry:{...c,pushed_at:u}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([f,d])=>d>(h(e.mark,f)??0)))||o||n&&l-Date.parse(c.down_since)>=Ae?{action:"resume",entry:{...c,resumed_at:u}}:{action:"none",entry:c}}a(qe,"resumeStep");async function bs({manifest:e=I(),now:s=new Date,live:t=_,asked:r=new Set,listening:i=a((u,c)=>de(u,{now:s.getTime(),running:c}),"listening"),resume:n=a((u,c)=>He(u,{manifest:e,live:t,mark:c}),"resume"),push:o=Se,gate:l=be({now:s})}={}){const u=t();if(!u)return{resumed:[],pushed:[],failed:[]};const c=P(D,{}),p=Object.create(null),f={resumed:[],pushed:[],failed:[]};for(const d of L(e)){const $=R(e,d.route),k=!!($&&u.has($.session)),g=i(d,u);let{action:T,entry:b}=qe(h(c,d.route),{listening:g,live:k,recorded:!!$?.repo,now:s.getTime(),asked:r.has(d.route),counts:g?null:pe(d),waiting:!g&&!k&&fe(d)!==null});if(T==="resume"&&l.state==="paused"){const y=h(c,d.route)?.mark||he(d);Object.entries(me(d)).flatMap(([C,te])=>te.slice(h(y,C)||0)).some(C=>!ke(C))||(T="none",delete b.resumed_at)}if(T==="resume")try{await n(d.route,b.mark),f.resumed.push(d.route)}catch(y){y.reason==="unknown"?delete b.resumed_at:b={...b,error:y.message},f.failed.push(`${d.route}: could not resume its session (${y.message})`)}else if(T==="push"){const y=b.error?`The hub couldn't resume it: ${b.error}`:k?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${d.route}, or claude agents.`;try{await o({title:`${d.title}'s session didn't come back`,body:y,url:`/${d.route}/`,tag:`resume-${d.route}`},{kind:"fault",immediate:!0,at:s}),f.pushed.push(d.route)}catch(G){b={...b,pushed_at:void 0},f.failed.push(`${d.route}: could not push that its session didn't come back (${G.message})`)}}b&&(p[d.route]=b)}return JSON.stringify(p)!==JSON.stringify(c)&&J(D,p),f}a(bs,"checkSessions");export{Ue as BG_SETTINGS,Ee as COMEBACK_WAIT,Pe as PROMPT_HOOK_TIMEOUT_S,Ae as RESUME_AFTER,Ce as STARTING_WAIT,m as SessionError,Me as attachTmux,bs as checkSessions,v as claudeBin,ms as compactFullContexts,z as hubArgv,ws as hubSession,A as inherited,We as resumeArgv,Ke as resumePrompt,He as resumeSession,qe as resumeStep,B as runClaude,Be as runSessionStop,U as runTmux,R as sessionOf,Ge as startArgv,De as startFailed,gs as startSession,ps as startingBoards,hs as stopSession,Q as tmuxBin,S as tmuxName,Ne as withSessionLock};
1
+ var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Me(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Me,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Me(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Re=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Re}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),M=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let d=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],d=!0);continue}if(p.route!==c||h(n,p.route)||!M(i,p.route))continue;const u=`=${T(p.route)}:`,y=i(["send-keys","-t",u,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",u,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),d=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return d&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function R(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",d;try{d=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),d.stdout.destroy(),d.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{o+=l}),d.stderr.on("data",l=>{f+=l}),d.on("error",l=>{f+=l.message,c(1)}),d.on("close",l=>c(l??1)),d.on("exit",l=>setTimeout(()=>c(l??1),300)),d.unref()})}a(R,"runClaude");async function Ke(e,{cwd:s,run:t=R,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),o(new m(d.message,"failed"))}),f.on("close",d=>{i(),n({status:d??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${g.join(E,"bin","kickflip.mjs")} join ${L(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
2
+ ${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!M(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy Mac. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=R,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=G(s,e);K(d,o);const c=g.resolve(t||d?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||d?.model,prompt:i}),u=await n(l,{cwd:c});if(u.status!==0){const w=(u.stderr||"").trim()||(u.stdout||"").trim();throw new m(w?`claude --bg failed (${u.status}): ${w.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(u.stdout);if(!y)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${u.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:y,confirmed:b,repo:Q(c),model:r||d?.model||null,name:p,argv:[C(),...l],output:u.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=R,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(M(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),d=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(d,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...d],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:f}){if(s)return{action:"none",entry:null};const d=new Date(f).toISOString(),c=e?{...e}:{down_since:d};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||f-Date.parse(c.resumed_at)>=Ue?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((d,c)=>he(d,{now:s.getTime(),running:c}),"listening"),resume:n=a((d,c)=>ze(d,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gateOf:f=a(d=>Se({now:s,dir:d.dir}),"gateOf")}={}){const d=t();if(!d)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of U(e)){const y=G(e,u.route),b=!!(y&&d.has(y.session)),w=i(u,d);let{action:j,entry:$}=Ze(h(c,u.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(u.route),counts:w?null:be(u),waiting:!w&&!b&&ge(u)!==null});if(j==="resume"&&f(u).state==="paused"){const I=h(c,u.route)?.mark||ke(u);Object.entries($e(u)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,u.route),re={listening:w,live:b,asked:r.has(u.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(u.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(u.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(u.route,$.mark),l.resumed.push(u.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${u.route}, or claude agents.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${u.title}'s session didn't come back`,body:I,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[u.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Re as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,R as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};
package/hub/share.mjs ADDED
@@ -0,0 +1 @@
1
+ var g=Object.defineProperty;var u=(e,t)=>g(e,"name",{value:t,configurable:!0});import y from"node:path";import{isBoardSite as E,readJSON as S,saveJSON as _}from"./boards.mjs";const b=u(e=>typeof e=="string"&&e.length<=200&&/^[^\s@]+@[^\s@]+$/.test(e),"validLogin");function m(e){const t=new Map;for(const n of Array.isArray(e?.share)?e.share:[]){const a=typeof n=="string"?n:n?.login;b(a)&&!t.has(a)&&t.set(a,{login:a,level:n?.level==="full"?"full":"view"})}return[...t.values()]}u(m,"shareEntries");const L=u(e=>m(e).map(t=>t.login),"shareOf");function T(e,t){const n=new Map;for(const[a,s]of Object.entries(t||{})){const r=E(s)&&m(S(y.join(s.dir,"board.json"),{})).find(f=>f.login===e);r&&n.set(a,r.level)}return n}u(T,"sharedRoutes");const v=new Set(["/","/sw.js","/manifest.webmanifest","/favicon.ico"]),x=/^\/_hub\/(?:app\/[\w.-]+|colors\.css|kickflip\.png|hub\.css|icon-(?:180|192|512)\.png|sidebar\.json|stream)$/,R=new Set(["/_hub","/_hub/","/_hub/settings","/_hub/settings/polling"]),j=new RegExp("^/(?:"+["","questions\\.html","notes\\.html","plan\\.html","index\\.html","prototype-[a-z0-9-]+\\.html","[\\w-][\\w.-]*\\.(?:js|mjs|css|png|svg|ico|webp|jpe?g|gif|woff2?)","prototypes/[a-z0-9-]+/[\\w-][\\w.-]*\\.js","uploads/[^/]+","api/(?:board|questions|done|links|working|thread|notes|plan|try/main/log)"].join("|")+")$"),O=new Set(["/_claude/events","/api/upload"]);function U({method:e="GET",pathname:t,routes:n}){const a=e==="GET"||e==="HEAD";if(v.has(t)||x.test(t))return a?"pass":"view-only";if(R.has(t))return a?"home":"other";const s=/^\/([^/]+)(\/.*)?$/.exec(t);if(!s||!n.has(s[1]))return"other";const r=s[2]||"/";return r===`/${s[1]}`||r.startsWith(`/${s[1]}/`)?"view-only":a?j.test(r)?"pass":"view-only":e==="POST"&&n.get(s[1])==="full"&&O.has(r)?"pass":"view-only"}u(U,"shareReach");function F(e){const t=String(e||"").split("/")[1]||"";if(!t||t==="_hub"||t.includes("."))return null;try{return decodeURIComponent(t)}catch{return null}}u(F,"routeOfPath");function B(e,t,n){const a=(e?.sites||[]).filter(r=>t.has(r.route)).map(({count:r,ctx:f,...h})=>h),s=[...t].filter(([,r])=>r==="full").map(([r])=>r);return{code:e?.code??null,total:0,start:a[0]?.route??null,sites:a,starting:[],shared:{by:n||null,full:s}}}u(B,"sharedSidebar");const D=60*6e4,A=20,$=6e4;function H({file:e,now:t=Date.now}={}){const n=S(e,{}),a=new Map,s=new Map,r=new Map;let f=[];const h=u(()=>{try{_(e,n)}catch{}},"save");return{visit(o,l,i){const c=n[o]||={name:null,at:{}};let p=!1;if(l&&c.name!==l&&(c.name=l,p=!0),i){r.set(o,i);const d=new Date(t()).toISOString();c.at[i]=d;const w=`${o} ${i}`;t()-(a.get(w)??-1/0)<$||(a.set(w,t()),p=!0)}p&&h()},watching(o){return s.set(o,(s.get(o)||0)+1),()=>{const l=s.get(o)-1;l>0?s.set(o,l):s.delete(o)}},refuse(o,l,i){f=[{login:o,name:l||null,route:i||null,at:new Date(t()).toISOString()},...f.filter(c=>c.login!==o)].slice(0,A)},people(o,l){return l.map(i=>({login:i,name:n[i]?.name||null,now:s.has(i)&&r.get(i)===o,at:n[i]?.at?.[o]||null}))},refusals(o,l){const i=t()-D;return f.filter(c=>Date.parse(c.at)>=i&&(c.route===o||c.route===null)&&!l.includes(c.login)).map(({login:c,name:p,at:d})=>({login:c,name:p,at:d}))}}}u(H,"shareTracker");export{F as routeOfPath,m as shareEntries,L as shareOf,U as shareReach,H as shareTracker,T as sharedRoutes,B as sharedSidebar,b as validLogin};
package/hub/slack.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var w=Object.defineProperty;var n=(r,t)=>w(r,"name",{value:t,configurable:!0});import o from"node:fs";import s from"node:path";import m from"node:os";import{withFileLock as h}from"./locks.mjs";const i=["self_dm","owner_slack_user","slack_domain","timezone"],a=n(()=>s.join(m.homedir(),".claude","kickflip","slack.json"),"fileOf"),c=n(r=>{try{return JSON.parse(o.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),d=n(r=>Object.fromEntries(i.map(t=>[t,typeof r?.[t]=="string"?r[t]:""])),"identityOf");function f(r){const t=a();o.mkdirSync(s.dirname(t),{recursive:!0});const e=`${t}.${process.pid}.tmp`;return o.writeFileSync(e,JSON.stringify(r,null,2)+`
2
- `,{mode:384}),o.chmodSync(e,384),o.renameSync(e,t),r}n(f,"save");function l(){const r=d(c(a()));if(i.some(e=>r[e]))return r;const t=d(c(s.join(m.homedir(),".claude","slack-bridge","state.json")).config);return i.some(e=>t[e])?f(t):r}n(l,"readSlack");async function $(r){for(const[t,e]of Object.entries(r)){if(!i.includes(t)||typeof e!="string")throw new Error(`Unknown Slack setting ${t}`);if(t==="self_dm"&&e&&!/^D[A-Z0-9]+$/.test(e))throw new Error("self-dm must be a DM channel id");if(t==="owner_slack_user"&&e&&!/^U[A-Z0-9]+$/.test(e))throw new Error("user must be a Slack user id");if(t==="slack_domain"&&e&&!/^[a-z0-9][a-z0-9-]*$/.test(e))throw new Error("domain must be a Slack workspace domain");if(t==="timezone"&&e)try{new Intl.DateTimeFormat("en",{timeZone:e})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return h(a(),()=>f({...l(),...r}))}n($,"setSlack");function z(r=new Date){const t=l().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,e=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:u,value:p})=>[u,p]));return{date:`${e.year}-${e.month}-${e.day}`,morning:!["Sat","Sun"].includes(e.weekday)&&Number(e.hour)>=9}}n(z,"slackDay");export{l as readSlack,$ as setSlack,z as slackDay};
1
+ var b=Object.defineProperty;var n=(r,e)=>b(r,"name",{value:e,configurable:!0});import u from"node:fs";import h from"node:path";import k from"node:os";import{withFileLock as v}from"./locks.mjs";import{readSecret as $,writeSecret as d}from"./secrets.mjs";const m=["self_dm","owner_slack_user","slack_domain","timezone"],p=n(()=>h.join(k.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(m.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function _(r){const e=p();u.mkdirSync(h.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
+ `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub settings \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};
package/hub/stream.mjs CHANGED
@@ -1,19 +1,16 @@
1
- var F=Object.defineProperty;var r=(w,g)=>F(w,"name",{value:g,configurable:!0});import W from"node:http";import A from"node:fs";import B from"node:path";function P({targets:w,watched:g=r(()=>[],"watched"),sidebar:I,retryMs:O=2e3,sidebarMs:j=15e3,pingMs:H=25e3,settleMs:M=300,log:v=r(()=>{},"log")}){const o=new Set,a=new Map,c=new Map,S=new Set;let l=null,y=[],u=new Set,d=null;const h=r((t,e=o)=>{for(const n of e)n.write(t)},"write"),T=r((t,e)=>`event: ${t}
2
- data: ${JSON.stringify(e)}
3
-
4
- `,"event");async function m(t=[]){let e;try{e=await I({fresh:new Set(t)})}catch(i){v(`sidebar for the stream failed: ${i.message}`);return}const n=JSON.stringify(e);n!==l&&(l=n,h(`event: sidebar
5
- data: ${n}
1
+ var W=Object.defineProperty;var r=(p,m)=>W(p,"name",{value:m,configurable:!0});import A from"node:http";import B from"node:fs";import G from"node:path";function R({targets:p,watched:m=r(()=>[],"watched"),sidebar:z,retryMs:I=2e3,sidebarMs:M=15e3,pingMs:T=25e3,settleMs:j=300,log:g=r(()=>{},"log")}){const s=new Map,a=new Map,i=new Map,v=new Set;let l=null,y=null,S=[],u=new Set,d=null;const H=r(t=>{for(const e of s.keys())e.write(t)},"write"),$=r((t,e,n)=>{const o=s.get(t);t.write(`event: sidebar
2
+ data: ${o?JSON.stringify(o.view(e)):n}
6
3
 
7
- `))}r(m,"refreshSidebar");function $(t){t&&u.add(t),!d&&(d=setTimeout(()=>{d=null;const e=[...u];u=new Set,m(e)},M))}r($,"sidebarSoon");function b(t){h(T("change",{route:t})),$(t)}r(b,"changed");function _(t,e){const n={port:e,req:null};a.set(t,n);const i=r(()=>{a.get(t)===n&&a.delete(t),n.req?.destroy()},"drop");n.req=W.get({host:"127.0.0.1",port:e,path:"/api/stream",headers:{accept:"text/event-stream"}},s=>{if(s.statusCode!==200){s.resume(),i();return}S.has(t)&&b(t),S.add(t),s.setEncoding("utf8");let f="";s.on("data",C=>{f+=C.replace(/\r\n/g,`
8
- `);for(let p=f.indexOf(`
4
+ `)},"sidebarTo"),J=r((t,e)=>`event: ${t}
5
+ data: ${JSON.stringify(e)}
9
6
 
10
- `);p>=0;p=f.indexOf(`
7
+ `,"event");async function h(t=[]){let e;try{e=await z({fresh:new Set(t)})}catch(o){g(`sidebar for the stream failed: ${o.message}`);return}const n=JSON.stringify(e);if(n!==l){l=n,y=e;for(const o of s.keys())$(o,e,n)}}r(h,"refreshSidebar");function q(t){t&&u.add(t),!d&&(d=setTimeout(()=>{d=null;const e=[...u];u=new Set,h(e)},j))}r(q,"sidebarSoon");function b(t){const e=J("change",{route:t});for(const[n,o]of s)(!o||o.routes.has(t))&&n.write(e);q(t)}r(b,"changed");function N(t,e){const n={port:e,req:null};a.set(t,n);const o=r(()=>{a.get(t)===n&&a.delete(t),n.req?.destroy()},"drop");n.req=A.get({host:"127.0.0.1",port:e,path:"/api/stream",headers:{accept:"text/event-stream"}},c=>{if(c.statusCode!==200){c.resume(),o();return}v.has(t)&&b(t),v.add(t),c.setEncoding("utf8");let f="";c.on("data",E=>{f+=E.replace(/\r\n/g,`
8
+ `);for(let w=f.indexOf(`
11
9
 
12
- `)){const E=f.slice(0,p);f=f.slice(p+2),/^event: ?change$/m.test(E)&&b(t)}}),s.on("close",i),s.on("error",i)}),n.req.on("error",i)}r(_,"connect");function q(){let t;try{t=new Map(w().map(e=>[e.route,e.port]))}catch(e){v(`stream targets failed: ${e.message}`);return}for(const[e,n]of a)t.get(e)!==n.port&&(a.delete(e),n.req?.destroy());for(const[e,n]of t)a.has(e)||_(e,n)}r(q,"reconcile");function x(){let t;try{t=new Set(g().map(e=>e.dir))}catch(e){v(`stream watched failed: ${e.message}`);return}for(const[e,n]of c)t.has(e)||(n.close(),c.delete(e));for(const e of t)if(!c.has(e))try{const n=A.watch(B.join(e,"data"),(i,s)=>{s==="watcher.json"&&$()});n.on("error",()=>{n.close(),c.delete(e)}),c.set(e,n)}catch{}}r(x,"watchWatchers");function k(){q(),x(),y=[setInterval(()=>{q(),x()},O),setInterval(()=>m(),j),setInterval(()=>h(`: ping
10
+ `);w>=0;w=f.indexOf(`
13
11
 
14
- `),H)]}r(k,"startHolding");function z(){for(const t of y)clearInterval(t);y=[],clearTimeout(d),d=null,u=new Set;for(const t of a.values())t.req?.destroy();a.clear();for(const t of c.values())t.close();c.clear(),S.clear(),l=null}r(z,"stopHolding");function J(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
12
+ `)){const F=f.slice(0,w);f=f.slice(w+2),/^event: ?change$/m.test(F)&&b(t)}}),c.on("close",o),c.on("error",o)}),n.req.on("error",o)}r(N,"connect");function x(){let t;try{t=new Map(p().map(e=>[e.route,e.port]))}catch(e){g(`stream targets failed: ${e.message}`);return}for(const[e,n]of a)t.get(e)!==n.port&&(a.delete(e),n.req?.destroy());for(const[e,n]of t)a.has(e)||N(e,n)}r(x,"reconcile");function k(){let t;try{t=new Set(m().map(e=>e.dir))}catch(e){g(`stream watched failed: ${e.message}`);return}for(const[e,n]of i)t.has(e)||(n.close(),i.delete(e));for(const e of t)if(!i.has(e))try{const n=B.watch(G.join(e,"data"),(o,c)=>{c==="watcher.json"&&q()});n.on("error",()=>{n.close(),i.delete(e)}),i.set(e,n)}catch{}}r(k,"watchWatchers");function _(){x(),k(),S=[setInterval(()=>{x(),k()},I),setInterval(()=>h(),M),setInterval(()=>H(`: ping
15
13
 
16
- `),o.add(e),o.size===1&&k(),l?h(`event: sidebar
17
- data: ${l}
14
+ `),T)]}r(_,"startHolding");function O(){for(const t of S)clearInterval(t);S=[],clearTimeout(d),d=null,u=new Set;for(const t of a.values())t.req?.destroy();a.clear();for(const t of i.values())t.close();i.clear(),v.clear(),l=null,y=null}r(O,"stopHolding");function C(t,e,n=null){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
18
15
 
19
- `,[e]):m(),t.on("close",()=>{o.delete(e),o.size||z()})}r(J,"handle");function N(){for(const t of o)t.end();o.clear(),z()}return r(N,"close"),{handle:J,close:N,changed:r(t=>{o.size&&b(t)},"changed"),refresh:r(()=>{o.size&&m()},"refresh")}}r(P,"hubStream");export{P as hubStream};
16
+ `),s.set(e,n),s.size===1&&_(),l?$(e,y,l):h(),t.on("close",()=>{s.delete(e),s.size||O()})}r(C,"handle");function D(){for(const t of s.keys())t.end();s.clear(),O()}return r(D,"close"),{handle:C,close:D,changed:r(t=>{s.size&&b(t)},"changed"),refresh:r(()=>{s.size&&h()},"refresh")}}r(R,"hubStream");export{R as hubStream};
package/hub/try.mjs CHANGED
@@ -1,8 +1,8 @@
1
- var Wt=Object.defineProperty;var a=(n,f)=>Wt(n,"name",{value:f,configurable:!0});import x from"node:fs";import O from"node:path";import Ot from"node:os";import ut from"node:http";import{execFile as Jt,spawn as zt}from"node:child_process";import{createInterface as Ct}from"node:readline";import{promisify as Ut}from"node:util";import{listBoards as W,readManifest as J,readJSON as rt,saveJSON as Et,appendEvent as Yt,GITHUB_STATE as qt,REPO_FETCH as It}from"./boards.mjs";import{boardRepo as Q,githubFromOrigin as jt}from"./board-repo.mjs";import{gitReachFault as Mt,remoteFromInput as Qt,remoteGitEnv as Vt}from"./repos.mjs";import{readiness as Xt}from"./readiness.mjs";import{tailscaleBin as Zt}from"../shared/tailscale.mjs";import{buildScriptError as te}from"../board/build-script.mjs";import{appendThread as ee,recordedWorktree as ie,setItem as re}from"../board/store.mjs";import{cardPRs as se,hasPR as ne,prKeyOf as oe}from"../shared/card-prs.mjs";import{hubGate as ae}from"../board/on-host.mjs";import{pipeUpgrade as ce,refuse as le,refuseUpgrade as de}from"./front-door.mjs";class T extends Error{static{a(this,"BuildError")}constructor(f,w=400){super(f),this.status=w}}const ft=Ut(Jt),M=a(async(n,...f)=>(await ft("git",["-C",n,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),ue=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,z={first:8500,last:8599},pt=a(n=>/^\d+$/.test(String(n))&&Number(n)>=z.first&&Number(n)<=z.last,"isTryPort"),fe=30*6e4,Pt=a(n=>O.join(n,".kickflip","build"),"scriptAt"),_t=a(n=>x.existsSync(n),"exists"),At=a((n,f)=>O.join(n.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function Nt(n,f,w){const m=At(n,f);return x.mkdirSync(O.dirname(m),{recursive:!0}),x.writeFileSync(m,w.message+`
2
- `,{mode:384}),x.chmodSync(m,384),m}a(Nt,"failureLog");const pe=a(n=>new Promise(f=>{const w=ut.get({host:"127.0.0.1",port:n,path:"/",timeout:1e3},m=>{m.destroy(),f(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>f(!1))}),"probe"),Dt=a((n,f)=>{try{process.kill(-n.pid,f)}catch{}},"signalGroup");async function Bt(n,f,w){const m=await ie(n.dir,f),[v,p]=f.split("/"),k=n.waves.find(b=>b.slug===v)?.wave,d=m?.path||k?.items.find(b=>b.id===p)?.worktree;if(typeof d!="string"||!d||d.split(O.sep).includes(".."))return"";let g=m?.repo;const y=(n.github||k?.repo||"").toLowerCase();if(!g&&y){const b=[n.owner?.repo,n.dir,...Object.values(w?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(b.filter(P=>typeof P=="string"&&O.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(I,"remote","get-url","origin"))?.[1].toLowerCase()===y){g=await M(I,"rev-parse","--show-toplevel");break}}catch{}}if(!m?.path&&!g||m?.path&&!O.isAbsolute(d))return"";try{const b=[O.join(x.realpathSync(Ot.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&O.isAbsolute(g))try{b.push(O.join(x.realpathSync(g),".claude","worktrees"))}catch{}const I=O.resolve(g||"/",d),P=x.realpathSync(I);return b.some(L=>I.startsWith(L+O.sep)&&P.startsWith(L+O.sep))&&x.statSync(P).isDirectory()?P:""}catch{return""}}a(Bt,"installWorktree");function he({now:n=Date.now,answers:f=pe,log:w=console.log,waiting:m=a(async()=>null,"waiting"),event:v=Yt,gate:p=ae}={}){const k=new Map,d=new Map,g=new Set,y=new Set,b=new Map,I=new Map,P=new Map,L=new Map;let G=!1,R=null,U=null,et=null;const st=a(()=>new Date(n()).toISOString(),"stamp");function D(t){if(k.has(t.dir))return k.get(t.dir);const i=O.join(t.dir,"data","try.json"),r=rt(i,{kind:null,main:null,cards:{}}),s={...pt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},e={state:s,file:i,written:JSON.stringify(r)};k.set(t.dir,e),s.main_port&&I.set(t.dir,s.main_port);const o=Object.entries(s.cards).filter(([,c])=>c.state==="running");for(const[c]of o)y.add(B(t,c));return s.kind==="web"?(s.cards=Object.fromEntries(o),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),A(t),e}a(D,"stateOf");function A(t){const i=k.get(t.dir),r=JSON.stringify(i.state);r!==i.written&&(Et(i.file,i.state),i.written=r)}a(A,"persist");const B=a((t,i)=>`${t.dir}\0${i}`,"runKey"),wt=a((t,i)=>i==="main"?t.main:t.cards[i],"entryOf");function Y(t,i,r){const{state:s}=D(t),e=wt(s,i);if(!e)return;const o=Object.hasOwn(r,"state")&&r.state!==e.state;Object.assign(e,r,i!=="main"&&o?{at:st()}:{}),A(t)}a(Y,"update");function nt(t){const i=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of i){if(!r?.dir)continue;const s=rt(O.join(r.dir,"data","try.json"),{}).main_port;pt(s)&&I.set(r.dir,Number(s))}}a(nt,"reserveMains");function Ft(t){const{board:i,key:r}=t,s=B(i,r),e=r==="main"?I.get(i.dir):b.get(s);if(e)return e;const o=new Set([...I.values(),...b.values()]);let c=z.first;for(;o.has(c)&&c<=z.last;)c++;if(c>z.last)throw new Error(`No try ports available (${z.first}\u2013${z.last}). Stop a served card and retry.`);return r==="main"?(I.set(i.dir,c),D(i).state.main_port=c,A(i)):b.set(s,c),c}a(Ft,"publicPort");const ot=a(async t=>{const i=Zt();if(!i)throw new Error("Tailscale isn't installed on this host");return ft(i,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function yt(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(i){w(`try port ${t}: could not close port: ${(i.stderr||i.message).trim()}`)}}a(yt,"removePort");async function Rt(t){t.https=Ft(t);const i=t.sockets=new Set,r=a(()=>{t.visited=n()},"visit"),s=a(e=>{i.add(e),e.on("data",r),e.on("close",()=>i.delete(e))},"track");if(t.relay=ut.createServer(async(e,o)=>{const c=await p(e);if(c!=="owner"){le(e,o,c);return}const h=ut.request({host:"127.0.0.1",port:t.port,method:e.method,path:e.url,headers:e.headers},S=>{o.writeHead(S.statusCode,S.headers),S.on("data",r),S.pipe(o),S.on("close",()=>{S.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||h.destroy()}),h.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),e.pipe(h)}),t.relay.on("connection",e=>{r(),s(e)}),t.relay.on("upgrade",async(e,o,c)=>{const h=await p(e);if(h!=="owner"){de(o,h);return}s(ce({req:e,socket:o,head:c,port:t.port,path:e.url}))}),await new Promise((e,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",e)}),!t.stopped){t.exposed=!0;try{await ot(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(e){throw new Error(`Couldn't open try port ${t.https}: ${(e.stderr||e.message).trim()}`)}}}a(Rt,"openPort");async function Lt(t){if(await t.opening?.catch(()=>{}),t.exposed&&await yt(t.https),t.relay){for(const i of t.sockets)i.destroy();await new Promise(i=>t.relay.close(i))}t.keepPort||b.delete(B(t.board,t.key))}a(Lt,"closePort");function X(t=J()){if(et)return et;nt(t);for(const i of W(t))D(i);return et=(async()=>{try{const{stdout:i}=await ot(["serve","status","--json"]),r=JSON.parse(i);for(const s of Object.keys(r?.TCP||{}))pt(s)&&![...d.values()].some(e=>e.https===Number(s))&&await yt(s)}catch(i){w(`try startup cleanup: ${(i.stderr||i.message).trim()}`)}})(),et}a(X,"initialize");async function gt(t,i,r,s){const{board:e,key:o}=t;if(Y(e,o,{state:"failed",port:void 0,https:void 0}),o==="main")return;const c=[];let h=!1;try{for await(const u of Ct({input:x.createReadStream(t.logFile),crlfDelay:1/0}))h||=ue.test(u),c.push(u),c.length>5&&c.shift()}catch(u){c.push(`Could not read the build log: ${u.message}`)}const S=t.port||(D(e).state.kind||(e.waves.some(({wave:u})=>u.items.some(C=>C.install?.url))?"app":"web"))==="web"?"Serve":"Build";await ee(e.dir,o,{text:[`${S} failed (exit ${i??1})`,s?.message,r&&`Stopped by ${r}.`,h&&"The login keychain must be unlocked on the Mac.",...c].filter(Boolean).join(`
3
- `)})}a(gt,"failure");function it(t){return t.ending||(Dt(t.child,"SIGTERM"),t.ending=new Promise(i=>{const r=a(()=>{clearTimeout(s),clearInterval(e),i()},"finish");let s=setTimeout(()=>{Dt(t.child,"SIGKILL"),s=setTimeout(r,1e3)},4e3);const e=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{r()}},20)})),t.ending}a(it,"terminate");async function $t(t,i=!1){t.keepPort=i,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await it(t),await t.done}a($t,"halt");async function Z(t,i=!1){await $t(t,i),Y(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(B(t.board,t.key))}a(Z,"stopRun");async function bt(t,i,r,s={}){if(!r)throw new T("This card has no valid worktree.");const e=Pt(r),o=te(e);if(o)throw new T(o);if(!x.realpathSync(e).startsWith(x.realpathSync(r)+O.sep))throw new T("Build script must be inside the worktree.");const c=await M(r,"rev-parse","HEAD");if(G)throw new T("The hub is stopping. Try again after it restarts.",503);const{state:h}=D(t),S=wt(h,i),u={...i==="main"?{}:S,...s,sha:c,script:!0,state:"running",...i==="main"?{}:{at:st(),head:c}};delete u.port,delete u.https;const C=At(t,i);x.mkdirSync(O.dirname(C),{recursive:!0});const E=x.openSync(C,"w",384);x.fchmodSync(E,384);const $=zt(e,[i,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});i==="main"?h.main=u:h.cards[i]=u,A(t);const N=B(t,i),l={child:$,board:t,key:i,logFile:C,visited:n(),stopped:!1,port:null,ready:!1};d.set(N,l),P.set(N,c);let F,j;const q=a(_=>{j||=new Error(_),it(l)},"fail");$.on("error",_=>{F=_}),$.stdout.on("data",_=>x.writeSync(E,_)),$.stderr.on("data",_=>x.writeSync(E,_));const H=Ct({input:$.stdout,crlfDelay:1/0});return H.on("line",_=>{const tt=/^port (\d{1,5})$/.exec(_);if(!tt||l.port||l.stopped)return;const K=Number(tt[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const ct=n()+6e4;let lt=!1;const xt=a(async()=>{if(!(lt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){lt=!0;try{if(await f(K)){if(l.stopped||j||$.exitCode!==null||$.signalCode!==null||(l.opening=Rt(l),await l.opening,l.stopped||j||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=n(),h.kind="web",Y(t,i,{state:"ready",port:K,https:l.https}),clearInterval(l.poll)}else n()>=ct&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{lt=!1}}},"check");l.poll=setInterval(()=>xt().catch(dt=>q(dt.message)),250),xt().catch(dt=>q(dt.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{it(l)}),l.done=new Promise(_=>$.once("close",(tt,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),H.close(),x.closeSync(E),(async()=>{await it(l),await Lt(l),l.finished=!0,!l.stopped&&(j||F||tt!==0||l.port&&!l.ready?await gt(l,tt??1,K,j||F||(l.port&&!l.ready?new Error("Build script exited before its port answered."):null)):l.ready?(i==="main"?(delete h.main.state,delete h.main.port,delete h.main.https):delete h.cards[i],A(t)):(h.kind="app",Y(t,i,{state:"ready"})))})().catch(ct=>w(`build /${t.route} ${i}: ${ct.message}`)).finally(()=>{d.get(N)===l&&d.delete(N),_()})})),w(`build /${t.route} ${i}: started; log ${C}`),{status:"started"}}a(bt,"launch");async function vt(t,i,r){const s=B(t,"main");if(g.has(s))throw new T("A build is already running for main.",409);g.add(s);try{d.has(s)&&await Z(d.get(s),!0);const e=O.join(Ot.homedir(),".claude","kickflip","try",`${t.route}-main`);return x.mkdirSync(O.dirname(e),{recursive:!0}),_t(e)?await M(e,"checkout","--detach","--force",r.sha):(await M(i,"worktree","prune"),await M(i,"worktree","add","--detach",e,r.sha)),await bt(t,"main",e,r)}catch(e){throw e.status!==503&&(D(t).state.main={...r,state:"failed"},Nt(t,"main",e),A(t)),e}finally{g.delete(s)}}a(vt,"launchMain");async function kt(t,{manifest:i=J()}={}){if(await X(i),nt(i),G)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(u=>!["route","key"].includes(u))||typeof t.route!="string"||typeof t.key!="string")throw new T("Send only the board route and card key.");const{route:r,key:s}=t;if(s!=="main"&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(u=>u==="."||u==="..")))throw new T("Card key must be <wave>/<id>.");const e=W(i).find(u=>u.route===r);if(!e)throw new T(`/${r} is not a board.`,404);if(s!=="main"&&!e.waves.some(({slug:u,wave:C})=>C.items.some(E=>`${u}/${E.id}`===s)))throw new T("There is no item with this card key.",404);const o=await m();if(o)throw new T(o,409);const c=B(e,s),h=d.get(c);if(g.has(c)||h&&!h.ready&&!h.finished)throw new T(`A build is already running for ${s==="main"?"main":"this card"}.`,409);if(s==="main"){const u=D(e).state.main;if(u?.state==="running")throw new T("A build is already running for main.",409);if(u?.state!=="failed"||!u.sha)throw new T("Main has no failed build to retry.",409);const{sha:C,branch:E,slug:$,at:N,script:l}=u;return vt(e,Q(r,i).repo,{sha:C,branch:E,slug:$,at:N,script:l})}g.add(c);const S=y.delete(c);try{d.has(c)&&await Z(d.get(c),!0);const u=await Bt(e,s,i);return await bt(e,s,u)}catch(u){throw b.delete(c),S&&u.status!==503&&Y(e,s,{state:void 0,sha:void 0}),u}finally{g.delete(c)}}a(kt,"start");function St(t,i,{manifest:r=J()}={}){const s=W(r).find(o=>o.route===t);if(!s)return null;const e=[...d.values()].find(o=>o.board.dir===s.dir&&o.key.split("/").at(-1).toLowerCase()===i.toLowerCase()&&o.ready&&!o.stopped&&o.child.exitCode===null&&o.child.signalCode===null);return e?{key:e.key,port:e.port}:null}a(St,"lookup");async function at({manifest:t=J(),routes:i}={}){if(await X(t),nt(t),G)return R;if(R&&i){if(!U){const r={routes:new Set,manifest:t};r.done=R.catch(()=>{}).then(()=>(U=null,at({manifest:r.manifest,routes:[...r.routes]}))),U=r}U.manifest=t;for(const r of i)U.routes.add(r);return U.done}return R||(R=(async()=>{const r=W(t),s=rt(qt,{}).prs||{};for(const e of[...d.values()])if(!r.some(o=>o.dir===e.board.dir)||e.key!=="main"&&e.ready&&n()-e.visited>=fe){await Z(e);const{state:o}=D(e.board);e.key==="main"?o.main=null:delete o.cards[e.key],A(e.board)}for(const e of r){if(G)return;if(i&&!i.includes(e.route))continue;const{state:o}=D(e),c=Q(e.route,t),h=c.branch||"main",S=c.local?`refs/heads/${h}`:`refs/remotes/origin/${h}`;for(const{slug:u,wave:C}of e.waves)for(const E of C.items){const $=`${u}/${E.id}`,N=se(E,e.github||C.repo).map(l=>s[oe(l)]);if(!(["done","merged","dropped"].includes(String(E.status).toLowerCase())||N.length&&N.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=B(e,$),F=await Bt(e,$,t);if(F){const H=await M(F,"rev-parse","HEAD");let _=0;try{_=Number(await M(F,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(o.cards[$]||={},{head:H,ahead:_,script:_t(Pt(F))}),A(e)}else if(!y.has(l))continue;const j=o.cards[$];if((y.has(l)||e.auto_build&&j.script&&j.ahead>0&&(j.sha||P.get(l))!==j.head&&j.state!=="running")&&!g.has(l))try{await kt({route:e.route,key:$},{manifest:t})}catch(H){if([409,503].includes(H.status))continue;throw Y(e,$,{sha:j.head}),await gt({board:e,key:$,logFile:Nt(e,$,H)},1),H}}catch(l){w(`try /${e.route} ${$}: ${l.message}`)}}try{if(!c.repo||await m())continue;const u=await M(c.repo,"rev-parse",S);let C=!1;try{await M(c.repo,"cat-file","-e",`${u}:.kickflip/build`),C=!0}catch{}if(!C){const j=d.get(B(e,"main"));j&&await Z(j),o.main=null,A(e);continue}const E=o.main;if(E?.state==="running"||E?.sha===u&&(E.state==="failed"||E.state==="ready"&&(o.kind!=="web"||St(e.route,"main",{manifest:t}))))continue;const $=B(e,"main");if(g.has($))continue;const N=await M(c.repo,"log","-1","--format=%cI",u);let l=e.github;if(!l)try{l=jt(await M(c.repo,"remote","get-url","origin"))}catch{}const F={sha:u,branch:h,slug:l||"",at:N,script:C};await vt(e,c.repo,F)}catch(u){w(`try /${e.route} main: ${u.message}`)}}})().finally(()=>{R=null}),R)}a(at,"tick");function Tt(t,i){if(L.has(t.dir))return L.get(t.dir);const r=(async()=>{const s=Q(t.route,i),e=s.branch||"main",{state:o}=D(t);o.pull={...o.pull,running:!0},A(t);let c,h;try{c=await me(s.repo,e)}catch(S){h=S.message}if(c===null?delete o.pull:o.pull={at:st(),...h?{error:h}:{}},A(t),c===null)return null;if(h)return w(`pull /${t.route}: ${h}`),{commits:0,error:h};if(c.commits){w(`pull /${t.route}: ${e} fast-forwarded ${c.commits} commit${c.commits===1?"":"s"} to ${c.to.slice(0,7)}`);try{await v(t,"main-pulled",{branch:e,...c})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:c.commits}})().finally(()=>L.delete(t.dir));return L.set(t.dir,r),r}a(Tt,"pullMain");async function Ht({manifest:t=J(),routes:i}={}){await X(t);const r=[];for(const s of W(t)){if(G)break;if(i&&!i.includes(s.route))continue;const e=Q(s.route,t);!e.local||!e.repo||(await Tt(s,t))?.commits&&r.push(s.route)}return r}a(Ht,"pullMains");async function Gt(t,{manifest:i=J()}={}){if(await X(i),G)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(o=>o!=="route")||typeof t.route!="string")throw new T("Send only the board route.");const r=W(i).find(o=>o.route===t.route);if(!r)throw new T(`/${t.route} is not a board.`,404);const s=Q(r.route,i);if(!s.local||!s.repo)throw new T("This board fetches its branch on every poll; only a local board pulls.",409);const e=await Tt(r,i);if(!e)throw new T("This board\u2019s folder has no origin to pull from.",409);return e.commits&&at({manifest:i,routes:[r.route]}).catch(o=>w(`try /${r.route} main: ${o.message}`)),e}a(Gt,"pull");async function Kt(){G=!0;const t=Promise.all([...d.values()].map(i=>i.ready?Z(i):$t(i)));await Promise.all([t,R,...L.values()])}return a(Kt,"stop"),{start:kt,tick:at,stop:Kt,lookup:St,initialize:X,pull:Gt,pullMains:Ht}}a(he,"createTryManager");const ht=1e4,mt=a(async(n,...f)=>(await ft("git",["-C",n,...f],{encoding:"utf8",timeout:ht,env:Vt()})).stdout,"originGit");async function me(n,f,{run:w=mt}={}){const m=a(async(...p)=>(await w(...p)).trim(),"git"),v=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await m(n,"config","--get","remote.origin.url")}catch{return null}try{await m(n,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){v(p.killed?`git fetch timed out after ${ht/1e3} s`:Mt(p.stderr,{repo:n}).error)}try{const p=await m(n,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await m(n,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),d=a((b,I)=>m(n,"merge-base","--is-ancestor",b,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await d(k,p))return{from:p,to:p,commits:0};await d(p,k)||v(`${f} has split from origin`);const g=Number(await m(n,"rev-list","--count",`${p}..${k}`)),y=(await m(n,"worktree","list","--porcelain")).split(`
1
+ var ee=Object.defineProperty;var a=(c,f)=>ee(c,"name",{value:f,configurable:!0});import T from"node:fs";import O from"node:path";import Nt from"node:os";import mt from"node:http";import{execFile as re,spawn as ie}from"node:child_process";import{createInterface as Dt}from"node:readline";import{promisify as se}from"node:util";import{listBoards as U,readManifest as z,readJSON as st,saveJSON as Bt,appendEvent as ne,GITHUB_STATE as oe,REPO_FETCH as Rt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Ft,mainRefOf as ae}from"./board-repo.mjs";import{gitReachFault as Ht,remoteFromInput as ce,remoteGitEnv as le}from"./repos.mjs";import{readiness as de}from"./readiness.mjs";import{tailscaleBin as ue}from"../shared/tailscale.mjs";import{buildScriptError as fe,buildScriptOf as Gt}from"../board/build-script.mjs";import{appendThread as pe,recordedWorktree as he,servedRoundsOpen as me,setItem as we}from"../board/store.mjs";import{BASE_KEY as J,baseWorktreeOf as ye}from"../board/prototype-base.mjs";import{cardPRs as ge,hasPR as $e,prKeyOf as ve}from"../shared/card-prs.mjs";import{hubGate as be}from"../board/on-host.mjs";import{pipeUpgrade as ke,refuse as Se,refuseUpgrade as Te}from"./front-door.mjs";import{shareOf as Oe}from"./share.mjs";class x extends Error{static{a(this,"BuildError")}constructor(f,w=400){super(f),this.status=w}}const wt=se(re),C=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),xe=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,Y={first:8500,last:8599},yt=a(c=>/^\d+$/.test(String(c))&&Number(c)>=Y.first&&Number(c)<=Y.last,"isTryPort"),Ee=30*6e4,at={main:"main",[J]:"prototypes"},F=a(c=>!Object.hasOwn(at,c),"isCard"),Lt=a((c,f)=>O.join(c,Gt(f)),"scriptAt"),gt=a(c=>T.existsSync(c),"exists"),Wt=a((c,f)=>O.join(c.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function $t(c,f,w){const h=Wt(c,f);return T.mkdirSync(O.dirname(h),{recursive:!0}),T.writeFileSync(h,w.message+`
2
+ `,{mode:384}),T.chmodSync(h,384),h}a($t,"failureLog");const _e=a(c=>new Promise(f=>{const w=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},h=>{h.destroy(),f(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>f(!1))}),"probe"),Kt=a((c,f)=>{try{process.kill(-c.pid,f)}catch{}},"signalGroup");async function Ut(c,f,w){const h=await he(c.dir,f),[b,p]=f.split("/"),k=c.waves.find(v=>v.slug===b)?.wave,u=h?.path||k?.items.find(v=>v.id===p)?.worktree;if(typeof u!="string"||!u||u.split(O.sep).includes(".."))return"";let y=h?.repo;const g=(c.github||k?.repo||"").toLowerCase();if(!y&&g){const v=[c.owner?.repo,c.dir,...Object.values(w?.sites||{}).map(j=>j.owner?.repo)];for(const j of new Set(v.filter(M=>typeof M=="string"&&O.isAbsolute(M))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await C(j,"remote","get-url","origin"))?.[1].toLowerCase()===g){y=await C(j,"rev-parse","--show-toplevel");break}}catch{}}if(!h?.path&&!y||h?.path&&!O.isAbsolute(u))return"";try{const v=[O.join(T.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof y=="string"&&O.isAbsolute(y))try{v.push(O.join(T.realpathSync(y),".claude","worktrees"))}catch{}const j=O.resolve(y||"/",u),M=T.realpathSync(j);return v.some(G=>j.startsWith(G+O.sep)&&M.startsWith(G+O.sep))&&T.statSync(M).isDirectory()?M:""}catch{return""}}a(Ut,"installWorktree");function je({now:c=Date.now,answers:f=_e,log:w=console.log,waiting:h=a(async()=>null,"waiting"),event:b=ne,gate:p=be}={}){const k=new Map,u=new Map,y=new Set,g=new Set,v=new Map,j=new Map,M=new Map,G=new Map;let W=!1,H=null,Q=null,nt=null;const ct=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=O.join(t.dir,"data","try.json"),i=st(e,{kind:null,main:null,cards:{}}),s={...yt(i.main_port)?{main_port:i.main_port}:{},kind:i.kind||null,main:i.main||null,cards:{...i.cards},...i.pull?{pull:{...i.pull,running:void 0}}:{}},r={state:s,file:e,written:JSON.stringify(i)};k.set(t.dir,r),s.main_port&&j.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,o])=>o.state==="running");for(const[o]of n)g.add(D(t,o));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),i.prototypes?.state==="failed"&&(s.prototypes=i.prototypes),A(t),r}a(N,"stateOf");function A(t){const e=k.get(t.dir),i=JSON.stringify(e.state);i!==e.written&&(Bt(e.file,e.state),e.written=i)}a(A,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),kt=a((t,e)=>F(e)?t.cards[e]:t[at[e]],"entryOf"),zt=a((t,e,i)=>{F(e)?t.cards[e]=i:t[at[e]]=i},"setEntry"),St=a((t,e)=>{F(e)?delete t.cards[e]:t[at[e]]=null},"dropEntry");function X(t,e,i){const{state:s}=N(t),r=kt(s,e);if(!r)return;const n=Object.hasOwn(i,"state")&&i.state!==r.state;Object.assign(r,i,F(e)&&n?{at:ct()}:{}),A(t)}a(X,"update");function lt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(i=>i.site)];for(const i of e){if(!i?.dir)continue;const s=st(O.join(i.dir,"data","try.json"),{}).main_port;yt(s)&&j.set(i.dir,Number(s))}}a(lt,"reserveMains");function Jt(t){const{board:e,key:i}=t,s=D(e,i),r=i==="main"?j.get(e.dir):v.get(s);if(r)return r;const n=new Set([...j.values(),...v.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return i==="main"?(j.set(e.dir,o),N(e).state.main_port=o,A(e)):v.set(s,o),o}a(Jt,"publicPort");const dt=a(async t=>{const e=ue();if(!e)throw new Error("Tailscale isn't installed on this host");return wt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Tt(t){try{await dt(["serve",`--https=${t}`,"off"])}catch(e){w(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Tt,"removePort");const Ot=a(t=>Oe(st(O.join(t.dir,"board.json"),{})),"sharedWith");async function xt(t,e){const i=await p(t),s=t.headers["tailscale-user-login"];return i!=="other"||!s||!Ot(e).includes(s)?i:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(xt,"relayVerdict");function Yt(){for(const t of u.values()){if(!t.shared?.size)continue;const e=Ot(t.board);for(const i of t.shared)e.includes(i.login)||(t.shared.delete(i),i.res.destroy())}}a(Yt,"endUnshared");async function Vt(t){t.https=Jt(t);const e=t.sockets=new Set,i=a(()=>{t.visited=c()},"visit"),s=a(n=>{e.add(n),n.on("data",i),n.on("close",()=>e.delete(n))},"track"),r=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const m=await xt(n,t.board);if(m!=="owner"&&m!=="shared"){Se(n,o,m);return}if(m==="shared"){const d={login:n.headers["tailscale-user-login"],res:o};r.add(d),o.on("close",()=>r.delete(d))}const S=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:n.headers},d=>{o.writeHead(d.statusCode,d.headers),d.on("data",i),d.pipe(o),d.on("close",()=>{d.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||S.destroy()}),S.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(S)}),t.relay.on("connection",n=>{i(),s(n)}),t.relay.on("upgrade",async(n,o,m)=>{const S=await xt(n,t.board);if(S!=="owner"){Te(o,S==="shared"?"view-only":S);return}s(ke({req:n,socket:o,head:m,port:t.port,path:n.url}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await dt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(Vt,"openPort");async function Qt(t){if(await t.opening?.catch(()=>{}),t.exposed&&await Tt(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||v.delete(D(t.board,t.key))}a(Qt,"closePort");function rt(t=z()){if(nt)return nt;lt(t);for(const e of U(t))N(e);return nt=(async()=>{try{const{stdout:e}=await dt(["serve","status","--json"]),i=JSON.parse(e);for(const s of Object.keys(i?.TCP||{}))yt(s)&&![...u.values()].some(r=>r.https===Number(s))&&await Tt(s)}catch(e){w(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),nt}a(rt,"initialize");async function Et(t,e,i,s){const{board:r,key:n}=t;if(X(r,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;const o=[];let m=!1;try{for await(const d of Dt({input:T.createReadStream(t.logFile),crlfDelay:1/0}))m||=xe.test(d),o.push(d),o.length>5&&o.shift()}catch(d){o.push(`Could not read the build log: ${d.message}`)}const S=t.port||(N(r).state.kind||(r.waves.some(({wave:d})=>d.items.some(_=>_.install?.url))?"app":"web"))==="web"?"Serve":"Build";await pe(r.dir,n,{text:[`${S} failed (exit ${e??1})`,s?.message,i&&`Stopped by ${i}.`,m&&"The login keychain must be unlocked on the Mac.",...o].filter(Boolean).join(`
3
+ `)})}a(Et,"failure");function ot(t){return t.ending||(Kt(t.child,"SIGTERM"),t.ending=new Promise(e=>{const i=a(()=>{clearTimeout(s),clearInterval(r),e()},"finish");let s=setTimeout(()=>{Kt(t.child,"SIGKILL"),s=setTimeout(i,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{i()}},20)})),t.ending}a(ot,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ot(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||M.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,i,s={}){if(!i)throw new x("This card has no valid worktree.");const r=Lt(i,t.build_target),n=fe(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(i)+O.sep))throw new x("Build script must be inside the worktree.");const o=await C(i,"rev-parse","HEAD");if(W)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:m}=N(t),S=kt(m,e),d={...F(e)?S:{},...s,sha:o,script:!0,state:"running",...F(e)?{at:ct(),head:o}:{}};delete d.port,delete d.https;const _=Wt(t,e);T.mkdirSync(O.dirname(_),{recursive:!0});const E=T.openSync(_,"w",384);T.fchmodSync(E,384);const $=ie(r,[e,i],{cwd:i,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(m,e,d),A(t);const B=D(t,e),l={child:$,board:t,key:e,logFile:_,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),M.set(B,o);let R,I;const q=a(P=>{I||=new Error(P),ot(l)},"fail");$.on("error",P=>{R=P}),$.stdout.on("data",P=>T.writeSync(E,P)),$.stderr.on("data",P=>T.writeSync(E,P));const L=Dt({input:$.stdout,crlfDelay:1/0});return L.on("line",P=>{const it=/^port (\d{1,5})$/.exec(P);if(!it||l.port||l.stopped)return;const K=Number(it[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const At=a(async()=>{if(!(pt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){pt=!0;try{if(await f(K)){if(l.stopped||I||$.exitCode!==null||$.signalCode!==null||(l.opening=Vt(l),await l.opening,l.stopped||I||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=c(),m.kind="web",X(t,e,{state:"ready",port:K,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>At().catch(ht=>q(ht.message)),250),At().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{ot(l)}),l.done=new Promise(P=>$.once("close",(it,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(E),(async()=>{if(await ot(l),await Qt(l),l.finished=!0,l.stopped)return;const tt=e===J&&!l.port?new Error('Build script exited without printing "port <n>".'):null;I||R||it!==0||l.port&&!l.ready||tt?await Et(l,it??1,K,I||R||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete m.main.state,delete m.main.port,delete m.main.https):St(m,e),A(t)):(m.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>w(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{u.get(B)===l&&u.delete(B),P()})})),w(`build /${t.route} ${e}: started; log ${_}`),{status:"started"}}a(jt,"launch");async function It(t,e,i){const s=D(t,"main");if(y.has(s))throw new x("A build is already running for main.",409);y.add(s);try{u.has(s)&&await Z(u.get(s),!0);const r=O.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(O.dirname(r),{recursive:!0}),gt(r)?await C(r,"checkout","--detach","--force",i.sha):(await C(e,"worktree","prune"),await C(e,"worktree","add","--detach",r,i.sha)),await jt(t,"main",r,i)}catch(r){throw r.status!==503&&(N(t).state.main={...i,state:"failed"},$t(t,"main",r),A(t)),r}finally{y.delete(s)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await rt(e),lt(e),W)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new x("Send only the board route and card key.");const{route:i,key:s}=t;if(F(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(d=>d==="."||d==="..")))throw new x("Card key must be <wave>/<id>.");const r=U(e).find(d=>d.route===i);if(!r)throw new x(`/${i} is not a board.`,404);if(F(s)&&!r.waves.some(({slug:d,wave:_})=>_.items.some(E=>`${d}/${E.id}`===s)))throw new x("There is no item with this card key.",404);const n=await h();if(n)throw new x(n,409);const o=D(r,s),m=u.get(o);if(y.has(o)||m&&!m.ready&&!m.finished)throw new x(`A build is already running for ${s==="main"?"main":s===J?"the prototype site":"this card"}.`,409);if(s==="main"){const d=N(r).state.main;if(d?.state==="running")throw new x("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new x("Main has no failed build to retry.",409);const{sha:_,branch:E,slug:$,at:B,script:l}=d;return It(r,et(i,e).repo,{sha:_,branch:E,slug:$,at:B,script:l,...r.build_target?{target:r.build_target}:{}})}y.add(o);const S=g.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=s===J?Ct(r):await Ut(r,s,e);return await jt(r,s,d)}catch(d){throw v.delete(o),S&&d.status!==503&&X(r,s,{state:void 0,sha:void 0}),d}finally{y.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=ye(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function Xt(t,e){const{state:i}=N(t),s=D(t,J),r=u.get(s);if(!((await me(t.dir)).length>0&&Ct(t))){r&&await Z(r),i.prototypes&&(i.prototypes=null,A(t));return}if(!(r||y.has(s)||i.prototypes?.state==="failed"||await h()))try{await ut({route:t.route,key:J},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw i.prototypes={state:"failed"},$t(t,J,o),A(t),o}}a(Xt,"serveBase");function Mt(t,e,{manifest:i=z()}={}){const s=U(i).find(n=>n.route===t);if(!s)return null;const r=[...u.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Mt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await rt(t),lt(t),W)return H;if(H&&e){if(!Q){const i={routes:new Set,manifest:t};i.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:i.manifest,routes:[...i.routes]}))),Q=i}Q.manifest=t;for(const i of e)Q.routes.add(i);return Q.done}return H||(H=(async()=>{const i=U(t),s=st(oe,{}).prs||{};for(const r of[...u.values()])if(!i.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=Ee){await Z(r);const{state:n}=N(r.board);St(n,r.key),A(r.board)}for(const r of i){if(W)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),m=o.branch||"main",S=ae(o);for(const{slug:d,wave:_}of r.waves)for(const E of _.items){const $=`${d}/${E.id}`,B=ge(E,r.github||_.repo).map(l=>s[ve(l)]);if(!(E.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,$),R=await Ut(r,$,t);if(R){const L=await C(R,"rev-parse","HEAD");let P=0;try{P=Number(await C(R,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(n.cards[$]||={},{head:L,ahead:P,script:gt(Lt(R,r.build_target))}),A(r)}else if(!g.has(l))continue;const I=n.cards[$];if((g.has(l)||r.auto_build&&I.script&&I.ahead>0&&(I.sha||M.get(l))!==I.head&&I.state!=="running")&&!y.has(l))try{await ut({route:r.route,key:$},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,$,{sha:I.head}),await Et({board:r,key:$,logFile:$t(r,$,L)},1),L}}catch(l){w(`try /${r.route} ${$}: ${l.message}`)}}await Xt(r,t).catch(d=>w(`try /${r.route} prototypes: ${d.message}`));try{if(!o.repo||await h())continue;const d=await C(o.repo,"rev-parse",S);let _=!1;try{await C(o.repo,"cat-file","-e",`${d}:${Gt(r.build_target)}`),_=!0}catch{}if(!_){const I=u.get(D(r,"main"));I&&await Z(I),n.main=null,A(r);continue}const E=n.main;if(E?.state==="running"||E?.sha===d&&(E.target||null)===(r.build_target||null)&&(E.state==="failed"||E.state==="ready"&&(n.kind!=="web"||Mt(r.route,"main",{manifest:t}))))continue;const $=D(r,"main");if(y.has($))continue;const B=await C(o.repo,"log","-1","--format=%cI",d);let l=r.github;if(!l)try{l=Ft(await C(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:m,slug:l||"",at:B,script:_,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,R)}catch(d){w(`try /${r.route} main: ${d.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function Pt(t,e){if(G.has(t.dir))return G.get(t.dir);const i=(async()=>{const s=et(t.route,e),r=s.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},A(t);let o,m;try{o=await Ie(s.repo,r)}catch(S){m=S.message}if(o===null?delete n.pull:n.pull={at:ct(),...m?{error:m}:{}},A(t),o===null)return null;if(m)return w(`pull /${t.route}: ${m}`),{commits:0,error:m};if(o.commits){w(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await b(t,"main-pulled",{branch:r,...o})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:o.commits}})().finally(()=>G.delete(t.dir));return G.set(t.dir,i),i}a(Pt,"pullMain");async function Zt({manifest:t=z(),routes:e}={}){await rt(t);const i=[];for(const s of U(t)){if(W)break;if(e&&!e.includes(s.route))continue;const r=et(s.route,t);!r.local||!r.repo||(await Pt(s,t))?.commits&&i.push(s.route)}return i}a(Zt,"pullMains");async function qt(t,{manifest:e=z()}={}){if(await rt(e),W)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const i=U(e).find(n=>n.route===t.route);if(!i)throw new x(`/${t.route} is not a board.`,404);const s=et(i.route,e);if(!s.local||!s.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await Pt(i,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[i.route]}).catch(n=>w(`try /${i.route} main: ${n.message}`)),r}a(qt,"pull");async function te(){W=!0;const t=Promise.all([...u.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...G.values()])}return a(te,"stop"),{start:ut,tick:ft,stop:te,lookup:Mt,initialize:rt,pull:qt,pullMains:Zt,endUnshared:Yt}}a(je,"createTryManager");const vt=1e4,bt=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:vt,env:le()})).stdout,"originGit");async function Ie(c,f,{run:w=bt}={}){const h=a(async(...p)=>(await w(...p)).trim(),"git"),b=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await h(c,"config","--get","remote.origin.url")}catch{return null}try{await h(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){b(p.killed?`git fetch timed out after ${vt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await h(c,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await h(c,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=a((v,j)=>h(c,"merge-base","--is-ancestor",v,j).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||b(`${f} has split from origin`);const y=Number(await h(c,"rev-list","--count",`${p}..${k}`)),g=(await h(c,"worktree","list","--porcelain")).split(`
4
4
 
5
- `).map(b=>b.split(`
6
- `)).find(b=>b.includes(`branch refs/heads/${f}`))?.[0].slice(9);return y?(await m(y,"status","--porcelain","--untracked-files=no")&&v("local changes"),await m(y,"merge","--ff-only","--quiet",k)):await m(n,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:g}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;v(String(p.stderr||p.message).trim().split(`
7
- `).at(-1))}}a(me,"pullBranch");async function Ne({manifest:n=J(),log:f=console.log,routes:w}={}){const m=w?rt(It,{}):{};for(const v of W(n)){if(w&&!w.includes(v.route))continue;const p=Q(v.route,n);if(p.local||!p.repo)continue;const k=p.branch||"main",d=new Date().toISOString(),g=Date.now();try{await mt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),m[v.route]={at:d,ok:!0}}catch(y){f(`fetch /${v.route} after ${Date.now()-g} ms: ${y.message}`);let b=v.github;if(!b&&!v.remote)try{b=jt(await M(p.repo,"remote","get-url","origin"))}catch{}m[v.route]={at:d,ok:!1,...Mt(y.killed?`git fetch timed out after ${ht/1e3} s`:y.stderr,{github:b||"",remote:v.remote,repo:p.repo})};continue}if(v.remote)try{await we(v,p.repo,k,f)}catch(y){f(`pushed branches /${v.route}: ${y.message}`)}}Et(It,m)}a(Ne,"fetchTryBranches");async function we(n,f,w,m){const v=n.waves.flatMap(({slug:d,wave:g})=>g.items.filter(y=>y.pushed?.branch&&!ne(y)&&!["done","merged","dropped"].includes(String(y.status).toLowerCase())).map(y=>({key:`${d}/${y.id}`,branch:y.pushed.branch})));if(!v.length)return;const p=new Map((await mt(f,"ls-remote","--heads","origin")).split(`
8
- `).map(d=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(d)).filter(Boolean).map(d=>[d[2],d[1]])),k=Qt(n.remote)?.host||"the remote";for(const d of v){let g=p.get(d.branch);if(!g)try{g=await M(f,"rev-parse","--verify","--quiet",`refs/heads/${d.branch}^{commit}`)}catch{}let y=!1;if(g)try{await M(f,"merge-base","--is-ancestor",g,`refs/remotes/origin/${w}`),y=!0}catch{}if(!(!y&&p.has(d.branch)))try{await re(n.dir,d.key,{status:y?"merged":"done",force:!0,text:y?`${d.branch} is merged into ${w} on ${k}, so this card is Done.`:`${d.branch} is gone from ${k}, so this card is Done.`})}catch(b){m(`pushed ${d.key}: ${b.message}`)}}}a(we,"settlePushed");const V=he({waiting:a(()=>Xt.waiting("tailscale"),"waiting")}),De=a((...n)=>V.start(...n),"startBuild"),Be=a((...n)=>V.tick(...n),"tryTick"),Fe=a((...n)=>V.pull(...n),"pullNow"),Re=a((...n)=>V.pullMains(...n),"pullMains"),Le=a(()=>V.stop(),"stopTries"),He=a((...n)=>V.initialize(...n),"initializeTries");export{T as BuildError,z as TRY_PORTS,he as createTryManager,Ne as fetchTryBranches,He as initializeTries,Bt as installWorktree,pt as isTryPort,me as pullBranch,Re as pullMains,Fe as pullNow,De as startBuild,Le as stopTries,Be as tryTick};
5
+ `).map(v=>v.split(`
6
+ `)).find(v=>v.includes(`branch refs/heads/${f}`))?.[0].slice(9);return g?(await h(g,"status","--porcelain","--untracked-files=no")&&b("local changes"),await h(g,"merge","--ff-only","--quiet",k)):await h(c,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:y}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;b(String(p.stderr||p.message).trim().split(`
7
+ `).at(-1))}}a(Ie,"pullBranch");async function Ze({manifest:c=z(),log:f=console.log,routes:w}={}){const h=w?st(Rt,{}):{};for(const b of U(c)){if(w&&!w.includes(b.route))continue;const p=et(b.route,c);if(p.local||!p.repo)continue;const k=p.branch||"main",u=new Date().toISOString(),y=Date.now();try{await bt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),h[b.route]={at:u,ok:!0}}catch(g){f(`fetch /${b.route} after ${Date.now()-y} ms: ${g.message}`);let v=b.github;if(!v&&!b.remote)try{v=Ft(await C(p.repo,"remote","get-url","origin"))}catch{}h[b.route]={at:u,ok:!1,...Ht(g.killed?`git fetch timed out after ${vt/1e3} s`:g.stderr,{github:v||"",remote:b.remote,repo:p.repo})};continue}if(b.remote)try{await Ce(b,p.repo,k,f)}catch(g){f(`pushed branches /${b.route}: ${g.message}`)}}Bt(Rt,h)}a(Ze,"fetchTryBranches");async function Ce(c,f,w,h){const b=c.waves.flatMap(({slug:u,wave:y})=>y.items.filter(g=>g.pushed?.branch&&!$e(g)&&g.lane!=="done").map(g=>({key:`${u}/${g.id}`,branch:g.pushed.branch})));if(!b.length)return;const p=new Map((await bt(f,"ls-remote","--heads","origin")).split(`
8
+ `).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=ce(c.remote)?.host||"the remote";for(const u of b){let y=p.get(u.branch);if(!y)try{y=await C(f,"rev-parse","--verify","--quiet",`refs/heads/${u.branch}^{commit}`)}catch{}let g=!1;if(y)try{await C(f,"merge-base","--is-ancestor",y,`refs/remotes/origin/${w}`),g=!0}catch{}if(!(!g&&p.has(u.branch)))try{await we(c.dir,u.key,{done:!0,force:!0,text:g?`${u.branch} is merged into ${w} on ${k}, so this card is Done.`:`${u.branch} is gone from ${k}, so this card is Done.`})}catch(v){h(`pushed ${u.key}: ${v.message}`)}}}a(Ce,"settlePushed");const V=je({waiting:a(()=>de.waiting("tailscale"),"waiting")}),qe=a((...c)=>V.start(...c),"startBuild"),tr=a((...c)=>V.tick(...c),"tryTick"),er=a((...c)=>V.pull(...c),"pullNow"),rr=a((...c)=>V.pullMains(...c),"pullMains"),ir=a(()=>V.stop(),"stopTries"),sr=a(()=>V.endUnshared(),"endUnsharedTries"),nr=a((...c)=>V.initialize(...c),"initializeTries");export{x as BuildError,Y as TRY_PORTS,je as createTryManager,sr as endUnsharedTries,Ze as fetchTryBranches,nr as initializeTries,Ut as installWorktree,yt as isTryPort,Ie as pullBranch,rr as pullMains,er as pullNow,qe as startBuild,ir as stopTries,tr as tryTick};
package/hub/upgrade.mjs CHANGED
@@ -1 +1 @@
1
- var E=Object.defineProperty;var t=(r,s)=>E(r,"name",{value:s,configurable:!0});import h from"node:fs";import d from"node:path";const _="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",C=60*6e4,v=t(r=>String(r).split("-")[0].split(".").map(Number),"parts");function F(r,s){const[l,u]=[v(r),v(s)];for(let n=0;n<3;n++)if((l[n]||0)!==(u[n]||0))return(l[n]||0)>(u[n]||0);return!1}t(F,"newer");function L({root:r,fetch:s=globalThis.fetch,builders:l,run:u,log:n=t(()=>{},"log"),changed:c=t(()=>{},"changed")}){const k=h.existsSync(d.join(r,".git"));let i=null,f=!1,a=!1,p=0,o=null;const y=t(()=>JSON.parse(h.readFileSync(d.join(r,"package.json"),"utf8")).version,"installed");async function S(){if(!k){try{const e=await s(_,{signal:AbortSignal.timeout(15e3)});if(!e.ok)throw new Error(`npm answered ${e.status}`);i=(await e.json()).version}catch(e){n(`kickflip update check failed: ${e.message}`)}c()}}t(S,"check");function g(){return k||!i||!F(i,y())?null:a?{state:"updating",version:i}:f?{state:"waiting",version:i,builders:p}:o?{state:"failed",version:i,error:o}:{state:"available",version:i}}t(g,"state");let m=Promise.resolve();const w=t(()=>m=m.catch(()=>{}).then(b),"tick");async function b(){if(!(!f||a)){if(p=(await l()).length,p){c();return}f=!1,a=!0,o=null,c(),u().then(({ok:e,error:x})=>{e||(o=x)},e=>{o=e.message}).finally(()=>{a=!1,c()})}}t(b,"tickOnce");async function j(){if(!g())throw Object.assign(new Error("kickflip is up to date."),{status:409});return a||(f=!0),await w(),g()}return t(j,"request"),{check:S,state:g,request:j,tick:w}}t(L,"upgrader");export{C as CHECK_MS,_ as LATEST_URL,F as newer,L as upgrader};
1
+ var d=Object.defineProperty;var t=(e,i)=>d(e,"name",{value:i,configurable:!0});import c from"node:fs";import f from"node:path";const h="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",j=60*6e4,p=t(e=>String(e).split("-")[0].split(".").map(Number),"parts");function y(e,i){const[a,s]=[p(e),p(i)];for(let n=0;n<3;n++)if((a[n]||0)!==(s[n]||0))return(a[n]||0)>(s[n]||0);return!1}t(y,"newer");function _({root:e,fetch:i=globalThis.fetch,run:a,log:s=t(()=>{},"log"),pulled:n=t(()=>{},"pulled")}){const u=c.existsSync(f.join(e,".git"));let l=!1;const g=t(()=>JSON.parse(c.readFileSync(f.join(e,"package.json"),"utf8")).version,"installed");async function k(){if(u)return;let r;try{const o=await i(h,{signal:AbortSignal.timeout(15e3)});if(!o.ok)throw new Error(`npm answered ${o.status}`);r=(await o.json()).version}catch(o){s(`kickflip update check failed: ${o.message}`);return}y(r,g())&&m()}t(k,"check");function m(){l||(l=!0,a().then(({ok:r})=>{r&&n()},r=>s(`kickflip update failed: ${r.message}`)).finally(()=>{l=!1}))}return t(m,"pull"),{check:k}}t(_,"upgrader");export{j as CHECK_MS,h as LATEST_URL,y as newer,_ as upgrader};
package/hub/usage.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var x=Object.defineProperty;var a=(t,e)=>x(t,"name",{value:e,configurable:!0});import f from"node:fs";import _ from"node:os";import m from"node:path";const S=6,N=180,b={five_hour:5*36e5,seven_day:7*864e5},h=a(t=>{try{return JSON.parse(f.readFileSync(t,"utf8"))}catch{return null}},"readJson"),y=a(t=>t==null||t===""||!Number.isFinite(Number(t))?null:Number(t),"num"),g=a(t=>{if(t==null||t==="")return null;const n=typeof t=="number"||/^\d+(\.\d+)?$/.test(String(t))?Number(t)>1e12?Number(t):Number(t)*1e3:Date.parse(String(t));return Number.isFinite(new Date(n).getTime())?new Date(n).toISOString():null},"isoOf"),w=a((t,e)=>({used_percentage:y(t),resets_at:g(e)}),"win");function O(t){let e;try{e=f.openSync(t,"r");const n=f.fstatSync(e).size;for(const r of[256*1024,2*1024*1024]){const s=Math.max(0,n-r),u=Buffer.alloc(n-s);f.readSync(e,u,0,u.length,s);const c=u.toString("utf8").split(`
2
- `);s>0&&c.shift();for(let i=c.length-1;i>=0;i--)if(c[i].includes("rate_limits"))try{const d=JSON.parse(c[i]),o=d?.payload?.rate_limits??d?.rate_limits;if(o?.primary)return{rl:o}}catch{}if(s===0)break}}catch{}finally{if(e!==void 0)try{f.closeSync(e)}catch{}}return null}a(O,"lastRateLimits");function k(t){const e=[],n=a(r=>{try{return f.readdirSync(r).sort().reverse()}catch{return[]}},"kids");t:for(const r of n(t))for(const s of n(m.join(t,r)))for(const u of n(m.join(t,r,s))){const c=m.join(t,r,s,u);for(const i of n(c))if(/^rollout-.*\.jsonl$/.test(i))try{e.push({file:m.join(c,i),mtime:f.statSync(m.join(c,i)).mtimeMs})}catch{}if(e.length>=S*2)break t}return e.sort((r,s)=>s.mtime-r.mtime).slice(0,S)}a(k,"newestRollouts");function D(t){const e=[];for(const{file:o,mtime:l}of k(t)){const p=O(o);p&&e.push({...p,mtime:l,resets:y(p.rl.primary.resets_at)})}if(!e.length)return null;const n=Math.max(...e.map(o=>o.resets??-1/0)),r=e.filter(o=>o.resets!=null&&Math.abs(o.resets-n)<=N),s=a(o=>r.reduce((l,p)=>(y(p.rl[o]?.used_percent)??-1)>(y(l?.rl[o]?.used_percent)??-1)?p:l,null),"pick");if(!r.length)return null;const u=s("primary"),c=s("secondary"),i=r.reduce((o,l)=>l.mtime>o.mtime?l:o),d=a((o,l)=>o?w(o.rl[l].used_percent,o.rl[l].resets_at):null,"w");return{five_hour:d(u,"primary"),seven_day:d(c,"secondary"),at:new Date(i.mtime).toISOString()}}a(D,"scanCodex");function j(t,e){if(!t||typeof t!="object"||Array.isArray(t))return null;const n=g(t.at),r=a(s=>{const u=t[s];if(!u)return null;const c=w(u.used_percentage,u.resets_at);if(c.used_percentage==null)return null;const i=!n||e-Date.parse(n)>b[s]?"stale":c.resets_at&&e>=Date.parse(c.resets_at)?"reset":"live";return{...c,used_percentage:i==="reset"?0:c.used_percentage,state:i}},"window");return{at:n,five_hour:r("five_hour"),seven_day:r("seven_day")}}a(j,"readingOf");function J({home:t=_.homedir(),now:e=Date.now,cacheMs:n=6e4}={}){const r=m.join(t,".codex","sessions");let s=null,u=-1/0;return({codexShare:c="off"}={})=>{const i=e(),d=j(h(m.join(t,".claude","kickflip","usage.json"))?.claude,i);return c==="off"?{claude:d,codex:null}:(i-u>=n&&(s=D(r),u=i),{claude:d,codex:j(s||{at:null},i)})}}a(J,"usageReader");const F=95,L=a((t,e)=>t==="deaf"&&e?.percent>=F,"contextFull");function R(t,{home:e=_.homedir()}={}){if(typeof t!="string"||!/^[\w-]+$/.test(t))return null;const n=h(m.join(e,".claude","kickflip","context",`${t}.json`));return y(n?.percent)==null||!n.at?null:{percent:y(n.percent),at:n.at}}a(R,"contextReading");async function W({gate:t,home:e=_.homedir(),resume:n}){const r=m.join(e,".claude","kickflip","usage-gate-state.json"),s=h(r);s?.state==="paused"&&t.state==="normal"&&await n(s.window),f.mkdirSync(m.dirname(r),{recursive:!0});const u=`${r}.${process.pid}.tmp`;f.writeFileSync(u,JSON.stringify(t)+`
3
- `),f.renameSync(u,r)}a(W,"checkUsagePause");export{F as CONTEXT_FULL,W as checkUsagePause,L as contextFull,R as contextReading,J as usageReader};
1
+ var N=Object.defineProperty;var c=(e,t)=>N(e,"name",{value:t,configurable:!0});import f from"node:fs";import y from"node:os";import d from"node:path";import{audit as w,hubAudit as S}from"../shared/audit.mjs";import{FOREVER as k}from"./duration.mjs";const g=6,O=180,$={five_hour:5*36e5,seven_day:7*864e5},_=c(e=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return null}},"readJson"),h=c(e=>e==null||e===""||!Number.isFinite(Number(e))?null:Number(e),"num"),b=c(e=>{if(e==null||e==="")return null;const r=typeof e=="number"||/^\d+(\.\d+)?$/.test(String(e))?Number(e)>1e12?Number(e):Number(e)*1e3:Date.parse(String(e));return Number.isFinite(new Date(r).getTime())?new Date(r).toISOString():null},"isoOf"),j=c((e,t)=>({used_percentage:h(e),resets_at:b(t)}),"win");function D(e){let t;try{t=f.openSync(e,"r");const r=f.fstatSync(t).size;for(const s of[256*1024,2*1024*1024]){const n=Math.max(0,r-s),a=Buffer.alloc(r-n);f.readSync(t,a,0,a.length,n);const o=a.toString("utf8").split(`
2
+ `);n>0&&o.shift();for(let u=o.length-1;u>=0;u--)if(o[u].includes("rate_limits"))try{const m=JSON.parse(o[u]),i=m?.payload?.rate_limits??m?.rate_limits;if(i?.primary)return{rl:i}}catch{}if(n===0)break}}catch{}finally{if(t!==void 0)try{f.closeSync(t)}catch{}}return null}c(D,"lastRateLimits");function v(e){const t=[],r=c(s=>{try{return f.readdirSync(s).sort().reverse()}catch{return[]}},"kids");e:for(const s of r(e))for(const n of r(d.join(e,s)))for(const a of r(d.join(e,s,n))){const o=d.join(e,s,n,a);for(const u of r(o))if(/^rollout-.*\.jsonl$/.test(u))try{t.push({file:d.join(o,u),mtime:f.statSync(d.join(o,u)).mtimeMs})}catch{}if(t.length>=g*2)break e}return t.sort((s,n)=>n.mtime-s.mtime).slice(0,g)}c(v,"newestRollouts");function F(e){const t=[];for(const{file:i,mtime:l}of v(e)){const p=D(i);p&&t.push({...p,mtime:l,resets:h(p.rl.primary.resets_at)})}if(!t.length)return null;const r=Math.max(...t.map(i=>i.resets??-1/0)),s=t.filter(i=>i.resets!=null&&Math.abs(i.resets-r)<=O),n=c(i=>s.reduce((l,p)=>(h(p.rl[i]?.used_percent)??-1)>(h(l?.rl[i]?.used_percent)??-1)?p:l,null),"pick");if(!s.length)return null;const a=n("primary"),o=n("secondary"),u=s.reduce((i,l)=>l.mtime>i.mtime?l:i),m=c((i,l)=>i?j(i.rl[l].used_percent,i.rl[l].resets_at):null,"w");return{five_hour:m(a,"primary"),seven_day:m(o,"secondary"),at:new Date(u.mtime).toISOString()}}c(F,"scanCodex");function x(e,t){if(!e||typeof e!="object"||Array.isArray(e))return null;const r=b(e.at),s=c(n=>{const a=e[n];if(!a)return null;const o=j(a.used_percentage,a.resets_at);if(o.used_percentage==null)return null;const u=!r||t-Date.parse(r)>$[n]?"stale":o.resets_at&&t>=Date.parse(o.resets_at)?"reset":"live";return{...o,used_percentage:u==="reset"?0:o.used_percentage,state:u}},"window");return{at:r,five_hour:s("five_hour"),seven_day:s("seven_day")}}c(x,"readingOf");function J({home:e=y.homedir(),now:t=Date.now,cacheMs:r=6e4}={}){const s=d.join(e,".codex","sessions");let n=null,a=-1/0;return({codexShare:o="off"}={})=>{const u=t(),m=x(_(d.join(e,".claude","kickflip","usage.json"))?.claude,u);return o==="off"?{claude:m,codex:null}:(u-a>=r&&(n=F(s),a=u),{claude:m,codex:x(n||{at:null},u)})}}c(J,"usageReader");const I=95,L=c((e,t)=>e==="deaf"&&t?.percent>=I,"contextFull");function W(e,{home:t=y.homedir()}={}){if(typeof e!="string"||!/^[\w-]+$/.test(e))return null;const r=_(d.join(t,".claude","kickflip","context",`${e}.json`));return h(r?.percent)==null||!r.at?null:{percent:h(r.percent),at:r.at}}c(W,"contextReading");async function z({gate:e,home:t=y.homedir(),resume:r}){const s=d.join(t,".claude","kickflip","usage-gate-state.json"),n=_(s);if(e.state==="paused"&&n?.state!=="paused"){const o=e.manual?`The owner paused the hub ${e.resets_at===k?"until they resume it":`until ${e.resets_at}`}.`:`Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage reached the ${e.pause_at}% line.`;w(S(t),{call:"usage-pause",result:"paused",reason:o,inputs:{gate:e,previous:n?.state??null}})}if(n?.state==="paused"&&e.state==="normal"){const o=n.manual?"The owner's pause of the hub ended, so every board was told.":`Claude's ${n.window==="five_hour"?"5-hour":"weekly"} usage is under the line again, so every board was told.`;w(S(t),{call:"usage-resume",result:"resumed",reason:o,inputs:{gate:e,previous:n.manual?"manual":n.window}}),await r(n.manual?"manual":n.window)}f.mkdirSync(d.dirname(s),{recursive:!0});const a=`${s}.${process.pid}.tmp`;f.writeFileSync(a,JSON.stringify(e)+`
3
+ `),f.renameSync(a,s)}c(z,"checkUsagePause");export{I as CONTEXT_FULL,z as checkUsagePause,L as contextFull,W as contextReading,J as usageReader};