@lovek4d/kickflip 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Bt=Object.defineProperty;var u=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Be from"node:http";import Ft from"node:net";import{spawn as Fe,execFileSync as Ge}from"node:child_process";import b from"node:fs";import k from"node:path";import ze from"node:os";import{fileURLToPath as Gt}from"node:url";import Je from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ke,queueStatus as We,saveSettings as Qt,readSettings as ee,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Ve,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ze,answerOf as an,expired as rn,pendingFor as Qe,promptRecord as on,pushOf as cn,updatePrompts as F,shown as ln,pruneRemembered as Se,rememberRules as un,rememberedAnswer as dn,rememberedFor as Xe}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as Sn,flushView as kn,SNOOZE_REFUSED as $n}from"./views.mjs";import{caffeinate as _n,setKeepAwake as xn,awakeOn as vn,awakeUnsupported as Tn}from"./awake.mjs";import{endOf as On}from"./duration.mjs";import{nodeBin as jn}from"../bin/install.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as ke,listenState as Ye,RESUME_STATE as Cn,sendToBoards as qe,checkListening as Ln,recordHubCode as An,hubCode as In,runningHubCode as Hn,boardCode as et,recordLinkFaults as Mn,linksOf as Nn,hubLink as Un,hubCodeState as Bn,readJSON as tt,migrateCellarNode as Fn}from"./boards.mjs";import{liveSessions as nt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as st,readHubRules as $e,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn,holdNeeds as Xn}from"./home.mjs";import{checkSessions as Yn,compactFullContexts as qn,startingBoards as es}from"./sessions.mjs";import{repoService as ts,repoLookupRoute as ns}from"./repos.mjs";import{boardRepo as at,changeBoardRepo as ss,migrateBoardRepos as as,migrateHubBoard as rs}from"./board-repo.mjs";import{requestBoard as os,retryBoard as is,dismissBoard as cs,clonePercent as ls,settleStarts as us}from"./new-board.mjs";import{boardClosure as ds,closeBoard as hs,closedBoards as fs,reopenBoard as ps}from"./close-board.mjs";import{restartBoard as ms,restartable as gs}from"./restart-board.mjs";import{crossSite as ws,isJSON as _e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as xe,appFile as bs,colorsVersion as rt,destOf as ys,fileVersion as Ss,pageOf as ot,shellHtml as ks}from"./shell.mjs";import{hubStream as $s}from"./stream.mjs";import{usageReader as _s,checkUsagePause as xs,contextReading as vs}from"./usage.mjs";import{readUsageGate as te,setPauseAt as Ts,setKeepGoing as Os,setHubPause as js,boardPausedUntil as Ps}from"../shared/usage-gate.mjs";import{checkMachine as Rs}from"../shared/machine-gate.mjs";import{readiness as Es,readCodex as ne}from"./readiness.mjs";import{BuildError as Ds,startBuild as Cs,stopTries as Ls,endUnsharedTries as As,tryTick as ve,initializeTries as Is,fetchTryBranches as it,pullNow as Hs,pullMains as Ms}from"./try.mjs";import{serveFiles as Ns}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Fs}from"./self-restart.mjs";import{fallBackFromLinear as Gs,migrateWaveNames as zs,openBoardBuilders as Js}from"../board/store.mjs";import{recordRefusal as Ks}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ws,refuse as Vs,refuseUpgrade as Zs}from"./front-door.mjs";import{shareEntries as Qs,sharedRoutes as ct,shareReach as Xs,routeOfPath as Ys,sharedSidebar as qs,shareTracker as ea}from"./share.mjs";import{restartRunner as lt}from"../board/worktree.mjs";import{hubService as ta}from"../shared/service.mjs";import{cloneRoot as se}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ae,removeLinearKey as na,saveLinearKey as sa,savedClient as ut}from"./linear.mjs";import{SlackError as aa,slackStatus as ra,removeSlackToken as oa,saveSlackToken as ia}from"./slack.mjs";import{sendReviews as ca}from"./review-send.mjs";const G=k.dirname(Gt(import.meta.url)),la=k.join(G,"..","shared","colors.css"),dt=JSON.parse(b.readFileSync(k.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=k.join(ze.homedir(),".claude","kickflip"),ht=k.join(z,"manifest.json"),re=k.join(z,"logs"),ft=k.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,re,ft])b.mkdirSync(e,{recursive:!0});try{const e=An();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await as();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await rs();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await zs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}try{const{routes:e,loginItem:t}=await Fn();e.length&&c(`manifest: ${e.map(n=>`/${n}`).join(", ")} spawn node by Homebrew's link`),t&&c("hub login item file: spawns node by Homebrew's link")}catch(e){c("Cellar node migration failed:",e.message)}const H=ts(),ua=_s(),oe=_n({log:c});oe.apply();let ie={version:1,sites:{}};function g(){try{ie=JSON.parse(b.readFileSync(ht,"utf8")),ie.sites||={}}catch(e){e.code==="ENOENT"?ie={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return ie}u(g,"loadManifest");const j=new Map,M=new Set;let ce=!1;const pt=5e3,le=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),N=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function ue(e){return k.join(ft,e+".pid")}u(ue,"pidFile");function da(e,t){try{const n=Number(b.readFileSync(ue(e),"utf8")),s=n?Ge("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ue(e))}catch{}}u(da,"killStale");function ha(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(re,e+".log"),"a"),[o,...r]=t.cmd,f={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
2
+ var Ut=Object.defineProperty;var u=(e,t)=>Ut(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ft from"node:net";import{spawn as Fe,execFileSync as Ge}from"node:child_process";import b from"node:fs";import S from"node:path";import ze from"node:os";import{fileURLToPath as Gt}from"node:url";import Je from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ke,queueStatus as We,saveSettings as Qt,readSettings as te,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Ve,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as Ze,answerOf as sn,expired as rn,pendingFor as Qe,promptRecord as on,pushOf as cn,updatePrompts as F,shown as ln,pruneRemembered as ke,rememberRules as un,rememberedAnswer as dn,rememberedFor as Xe}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as kn,flushView as Sn,SNOOZE_REFUSED as $n}from"./views.mjs";import{caffeinate as _n,setKeepAwake as xn,awakeOn as vn,awakeUnsupported as Tn}from"./awake.mjs";import{endOf as On}from"./duration.mjs";import{nodeBin as jn}from"../bin/install.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as Se,listenState as Ye,RESUME_STATE as Ln,sendToBoards as qe,checkListening as Cn,recordHubCode as In,hubCode as An,runningHubCode as Hn,boardCode as et,recordLinkFaults as Mn,linksOf as Nn,hubLink as Bn,hubCodeState as Un,readJSON as tt,migrateCellarNode as Fn}from"./boards.mjs";import{liveSessions as nt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as at,readHubRules as $e,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn,holdNeeds as Xn}from"./home.mjs";import{checkSessions as Yn,compactFullContexts as qn,startingBoards as ea}from"./sessions.mjs";import{repoService as ta,repoLookupRoute as na}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as aa,migrateBoardRepos as sa,migrateHubBoard as ra}from"./board-repo.mjs";import{requestBoard as oa,retryBoard as ia,dismissBoard as ca,clonePercent as la,settleStarts as ua}from"./new-board.mjs";import{boardClosure as da,closeBoard as ha,closedBoards as fa,reopenBoard as pa}from"./close-board.mjs";import{restartBoard as ma,restartable as ga}from"./restart-board.mjs";import{crossSite as wa,isJSON as _e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as xe,appFile as ba,colorsVersion as rt,destOf as ya,fileVersion as ka,pageOf as ot,shellHtml as Sa}from"./shell.mjs";import{hubStream as $a}from"./stream.mjs";import{usageReader as _a,checkUsagePause as xa,contextReading as va}from"./usage.mjs";import{readUsageGate as ne,setPauseAt as Ta,setKeepGoing as Oa,setHubPause as ja,boardPausedUntil as Pa}from"../shared/usage-gate.mjs";import{checkMachine as Ra}from"../shared/machine-gate.mjs";import{readiness as Ea,readCodex as ae}from"./readiness.mjs";import{BuildError as Da,startBuild as La,killBuild as Ca,stopTries as Ia,endUnsharedTries as Aa,tryTick as ve,initializeTries as Ha,fetchTryBranches as it,pullNow as Ma,pullMains as Na}from"./try.mjs";import{serveFiles as Ba}from"./files.mjs";import{upgrader as Ua}from"./upgrade.mjs";import{restarter as Fa}from"./self-restart.mjs";import{fallBackFromLinear as Ga,migrateWaveNames as za,openBoardBuilders as Ja}from"../board/store.mjs";import{recordRefusal as Ka}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Wa,refuse as Va,refuseUpgrade as Za}from"./front-door.mjs";import{shareEntries as Qa,sharedRoutes as ct,shareReach as Xa,routeOfPath as Ya,sharedSidebar as qa,shareTracker as es}from"./share.mjs";import{restartRunner as lt}from"../board/worktree.mjs";import{hubService as ts}from"../shared/service.mjs";import{cloneRoot as se}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as re,removeLinearKey as ns,saveLinearKey as as,savedClient as ut}from"./linear.mjs";import{SlackError as ss,slackStatus as rs,removeSlackToken as os,saveSlackToken as is}from"./slack.mjs";import{sendReviews as cs}from"./review-send.mjs";const G=S.dirname(Gt(import.meta.url)),ls=S.join(G,"..","shared","colors.css"),dt=JSON.parse(b.readFileSync(S.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=S.join(ze.homedir(),".claude","kickflip"),ht=S.join(z,"manifest.json"),oe=S.join(z,"logs"),ft=S.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,oe,ft])b.mkdirSync(e,{recursive:!0});try{const e=In();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await sa();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await ra();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await za();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}try{const{routes:e,loginItem:t}=await Fn();e.length&&c(`manifest: ${e.map(n=>`/${n}`).join(", ")} spawn node by Homebrew's link`),t&&c("hub login item file: spawns node by Homebrew's link")}catch(e){c("Cellar node migration failed:",e.message)}const H=ta(),us=_a(),ie=_n({log:c});ie.apply();let ce={version:1,sites:{}};function g(){try{ce=JSON.parse(b.readFileSync(ht,"utf8")),ce.sites||={}}catch(e){e.code==="ENOENT"?ce={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return ce}u(g,"loadManifest");const j=new Map,M=new Set;let le=!1;const pt=5e3,ue=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),N=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function de(e){return S.join(ft,e+".pid")}u(de,"pidFile");function ds(e,t){try{const n=Number(b.readFileSync(de(e),"utf8")),a=n?Ge("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";a&&a.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(de(e))}catch{}}u(ds,"killStale");function hs(e,t){const n=j.get(e)||{restarts:0},a=b.openSync(S.join(oe,e+".log"),"a"),[o,...r]=t.cmd,f={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(a,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=ke(t)?et(t):null;let h;try{h=Fe(o,r,{cwd:t.cwd||t.dir,env:f,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
- `),b.closeSync(s),Object.assign(n,{child:null,key:le(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ue(e),String(h.pid)),Object.assign(n,{child:h,key:le(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,S)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(ue(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(ha,"start");const mt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function de(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;he(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&he(n,"SIGKILL")},pt)}u(de,"stop");const he=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let gt=!0;const U=new Set;let Ee=!1;const K=new Set,fa=3e4;async function pa(e){const t=Date.now()+fa;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===le(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(pa,"serverAnswers");async function wt(){if(ce)return;bt&&yt.check();const{sites:e}=g();for(const t of[...j.keys()])U.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),de(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),de(t),j.delete(t)));for(const t of[...J.keys()])N(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))N(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;gt&&da(t,n);const s=j.get(t);if(s&&s.child&&s.key!==le(n)){c(`${t} entry changed; restarting it`),de(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=bt&&s?.child&&s.code&&!s.stopping&&!K.has(t)&&!U.has(t)?et(n):null;if(o&&o!==s.code){if(!(await Js(n.dir)).length){c(`${t} runs older code than its files; restarting it`),de(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ha(t,n)}gt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(wt,"reconcile");const bt=process.env.KICKFLIP_CODE_RESTARTS!=="off",yt=Fs({root:se(),files:In().files,restart:u(()=>lt("hub"),"restart"),log:c}),ma=setInterval(()=>wt().catch(e=>c("reconcile failed:",e.message)),2e3);wt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const St=u(async()=>{try{for(const e of await Gs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await St();const ga=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ga,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},fe=sn(),wa=u(()=>$.startedAt?wn({last:new Date($.startedAt),...fe.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Ce().catch(n=>c("poll failed:",n.message))}u(kt,"presenceChanged");function Ce({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=ee();let s;try{s=await Pn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await it({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await it({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Ms({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ca({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Ln({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Mn(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Ke();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Ce,"runPoll");let $t=null;function ba(){$t||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(ba,"runWakes");const Le=new Set,A={running:null};function _t(){if(A.running||U.size||K.size)return;const e=new Set(Le);Le.clear(),A.running=Yn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>qn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}u(_t,"runSessionCheck"),await Is(g());const ya=Date.now(),W=new Map;function V(e){e?(Q.delete(e),x.changed(e)):x.refresh()}u(V,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await F(n=>{const s=rn(n,{now:Date.now(),aliveSince:ya}),o=Se(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)V(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ae=u(()=>({running:nt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ks(at(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Z=null,Tt=null;async function pe(){return Z?Tt??=Z.catch(()=>{}).then(()=>(Tt=null,pe())):(Z=(async()=>xs({gate:te(),resume:u(async e=>{const t=await qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Z=null}),Z)}u(pe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),pe().catch(e=>c("usage gate failed:",e.message));try{Rs()}catch(e){c("machine check failed:",e.message)}fe.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=wa().getTime()&&Ce().catch(e=>c("poll failed:",e.message))}),us({listening:u(e=>Dn(e)&&me().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ba(),oe.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ce)return;ce=!0,clearInterval(ma),oe.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)he(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),he(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function Sa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}u(Sa,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function me(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=N(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:gs(n),changedAt:Sa(t,n)||null,created:n.created||null,log:k.join(re,t+".log")}})}u(me,"status");const B=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),Ie=new Map,Q=new Map,Et=2e4,ka=1500;async function $a(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ka)});return t.ok?Wn(await t.json()):null}catch{return null}}u($a,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=Q.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&Q.set(o.route,{at:Date.now(),needs:$a(o.port)})}const s=await Promise.all(n.map(o=>Q.get(o.route).needs));return new Map(n.map((o,r)=>{const f=Xn(Ie.get(o.route)??null,s[r],Q.get(o.route).at,Date.now());return f.good?Ie.set(o.route,f.good):Ie.delete(o.route),[o.route,f.needs]}))}u(Dt,"waitingCounts");const Ct=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Qe(null).length,"badgeTotal"),He=new Map;function _a(e){const t=new Map,n=new Map,s={running:nt(),wakes:tt(Cn,{})};for(const o of e)try{const r=Ye(o,s);n.set(o.route,r);let f=He.get(o.route);(!f||f.listen!==r||Date.now()-f.at>=Et)&&(f={at:Date.now(),listen:r,links:Nn(o,{listen:r})},He.set(o.route,f)),t.set(o.route,f.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u(_a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=me(),s=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:f,listens:h}=_a(r),l=Bn(),_=new Map(r.map(i=>[i.route,vs(i.owner?.session)]).filter(([,i])=>i)),S=es(o,t.getTime(),ls,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>Ps(i.dir)).map(i=>[i.route,te({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=ee(),L=hn(t,O);return{data:{code:l.running??null,version:dt,total:Ct(s),start:Zn(w),sites:w,hub:Qn(w,f,Un(l)),home:b.realpathSync(ze.homedir()),starting:S,usage:(({start:i,...d})=>d)(ua({codexShare:Ve(ne())})),pause:te(),notifications:L?L.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(L,t))}:{state:"on",text:"On"},away:!fe.state().present},links:f,rows:n,code:l}}u(X,"sidebarState");function Lt(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const f={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),f["content-encoding"]="gzip"),f["content-length"]=r.length,t.writeHead(n,f),t.end(e.method==="HEAD"?void 0:r)}u(Lt,"sendText");async function xa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Je.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Je.runInContext(b.readFileSync(k.join(xe,o),"utf8"),s);return ks({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(xa,"shellPage");const Me=Us({root:se(),run:va,log:c,pulled:u(()=>yt.restartNow(),"pulled")});function va(){const e=k.join(re,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Fe(jn(),[k.join(se(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(va,"runUpdate"),Me.check(),setInterval(()=>Me.check(),Bs);const x=$s({targets:u(()=>Object.entries(g().sites).filter(([,e])=>ke(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function ge(e){let t="0";try{t=Math.floor(b.statSync(k.join(G,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(ge,"iconUrl");const Ta=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ge(192),sizes:"192x192",type:"image/png"},{src:ge(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function v(e,t){if(!_e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(v,"readJSONBody");const At=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
- <title>${B(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${ge(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${rt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Oa=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...yn({now:t,st:We(t),sidebar:n,rows:o,links:s,rules:$e(),integrations:await Es.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ve(ne()),claudeModel:en(),codex:ne(),awake:{on:vn(),unsupported:Tn()},remembered:new Map(D(g()).map(f=>[f.route,Xe(f.route)]))}),prompts:Qe(null).map(ln),remembered:Xe(null)}}u(we,"hubData");const I=u(async()=>{const e=new Date,t=te({now:e}),n=t.manual==="hub"?On(t.resets_at,e):null;return{...Sn({now:e,st:We(e),rules:$e(),linear:ae(),slack:ra(),pausedUntil:n}),closed:fs()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function ja(e,t,n,s=null){const o=u((a,i,d,p={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...p}),t.end(d)},"send"),r=u((a,i)=>{const d=Hn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),f=u(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Oa(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await ds(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let a;try{const i=await v(e);if(a=i.route,l==="/_hub/reopen-board"){const T=await ps(a);return x.refresh(),r(200,T)}if(U.has(a))return r(409,{error:"That board is already closing."});U.add(a);const d=u(()=>U.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const p=await hs(a,i.name);return x.refresh(),r(200,p)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await v(e)).route,K.has(a))return r(409,{error:"That board is already restarting."});K.add(a),await A.running;const i=await ms(a,{serverUp:pa,portUp:Re,onStarting:u(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{K.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=lt("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ta().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const a=await Me.check();return a.state==="clone"&&(a.commit=Ge("git",["-C",se(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),kt(fe.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await v(e),i=D(g()).find(d=>d.route===a);return i?(Le.add(a),await A.running,_t(),await A.running,He.clear(),x.changed(a),r(200,{route:a,listen:Ye(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/repo"&&e.method==="GET")return ns(n,{repos:H,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?at(S[1]):await ss(S[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return f(await os(a,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return f(await is(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const a=await cs(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(ce)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(p=>p==="forwarded"||p.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(p){return r(p.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Hs(d,{manifest:g()})):r(202,await Cs(d,{manifest:g(),log:c}))}catch(p){if(p instanceof Ds)return r(p.status,{error:p.message});throw p}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",Ta());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(k.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const a=n.searchParams.get("v")===rt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(la),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(l==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};be.add(a),e.on("close",()=>be.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!ke(a))return r(404,{error:"That board is not open."});const i=Qs(tt(k.join(a.dir,"board.json"),{})),d=i.map(C=>C.login),p=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((C,q)=>({...C,level:i[q].level}));return r(200,{people:T,refused:Y.refusals(O[1],d),link:p?`https://${p}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=bs(l.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],p=n.searchParams.get("v")===Ss(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":p}):Lt(e,t,200,{"content-type":d,"cache-control":p},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await I());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,p="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:p}):o(400,"text/plain; charset=utf-8",p);let T;try{T=await Yt(d)}catch(C){if(C.status!==400)throw C;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{xn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return oe.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{js(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await pe(),h?r(200,await I()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),ne({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?Ts(a.get("value")):Os(a.get("value")),await pe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(na(),c("linear: key removed"),await St()):c(`linear: key ${(await sa(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(oa(),c("slack: token removed")):c(`slack: token ${(await ia(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof aa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=ae();if(!a.set||a.refused)return r(200,a);const i=ut();try{const d=await i.teams();return r(200,{...ae(),teams:d.map(({key:p,name:T})=>({key:p,name:T})),projects:d.flatMap(p=>p.projects.map(T=>({id:T.id,name:T.name,team:p.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ae(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=ut();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(p){return r(p.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(p){if(!(p instanceof Oe))throw p;return r(p.status,{error:p.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(ee(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await v(e,64*1024),i=zn($e(),a.rules,a.base??[]),st(i.changes)&&Jn(i.rules)}catch(p){if(!(p instanceof Gn)&&!(p instanceof SyntaxError))throw p;return r(400,{error:p.message})}if(!st(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(p=>p.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Ke();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,kn(a)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const a=await v(e);return r(200,await Ce({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(p){if(p.status)throw p;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await F(p=>{const T=Ae(),C=T.boards.find(Ue=>Ue.owner?.session&&Ue.owner.session===a.session_id)||null;Se(p,T)&&x.refresh();const q=dn(p,a);if(q)return{answer:q};const ye=on(a,{board:C,live:T.running?.get(a.session_id)});return p.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(V(d.route),await vt(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(p=>c(`prompt ${d.id}: push failed: ${p.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(a)})}const L=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return L?Pa(e,t,{id:L[1],op:L[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(ja,"hubRoute");async function Pa(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const f=u(()=>F(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(V(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await f();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ze,Ze));if(await new Promise(y=>{const m=W.get(n)||new Set;W.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&W.get(n)===m&&W.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await f();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const f=await v(e),h=await F(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,f);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return Se(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...W.get(n)||[]])S();return V(l.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const f=await F(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return f&&(V(f.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(Pa,"promptRoute");const Ra=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=N(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(It,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(Ea(t,n))}u(Ht,"sendDown");function Ea(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=It(e,t),f=r==="code"||r==="starting"?"wait":"down",h=N(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=Se(t)?et(t):null;let h;try{h=Fe(o,r,{cwd:t.cwd||t.dir,env:f,stdio:["ignore",a,a],detached:!0})}catch(_){b.writeSync(a,`hub could not start it: ${_.message}
5
+ `),b.closeSync(a),Object.assign(n,{child:null,key:ue(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(a),b.writeFileSync(de(e),String(h.pid)),Object.assign(n,{child:h,key:ue(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,k)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(de(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(hs,"start");const mt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function he(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;fe(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&fe(n,"SIGKILL")},pt)}u(he,"stop");const fe=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),a=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>a(!1)),n.on("connect",()=>a(!0)),n.on("error",()=>a(!1))})}u(Re,"probe");let gt=!0;const B=new Set;let Ee=!1;const K=new Set,fs=3e4;async function ps(e){const t=Date.now()+fs;for(;;){const n=P(g().sites,e),a=j.get(e);if(n&&a?.child&&a.up&&a.key===ue(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(ps,"serverAnswers");async function wt(){if(le)return;bt&&yt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),he(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),he(t),j.delete(t)));for(const t of[...J.keys()])N(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))N(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;gt&&ds(t,n);const a=j.get(t);if(a&&a.child&&a.key!==ue(n)){c(`${t} entry changed; restarting it`),he(t),a.restarts=0,a.nextStart=Date.now()+1500;continue}const o=bt&&a?.child&&a.code&&!a.stopping&&!K.has(t)&&!B.has(t)?et(n):null;if(o&&o!==a.code){if(!(await Ja(n.dir)).length){c(`${t} runs older code than its files; restarting it`),he(t),Object.assign(a,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}a.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),a.waitedOn=o}(!a||!a.child&&Date.now()>=(a.nextStart||0))&&hs(t,n)}gt=!1;for(const[t,n]of j){const a=P(e,t);a&&(n.up=n.child?await Re(a.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(wt,"reconcile");const bt=process.env.KICKFLIP_CODE_RESTARTS!=="off",yt=Fa({root:se(),files:An().files,restart:u(()=>lt("hub"),"restart"),log:c}),ms=setInterval(()=>wt().catch(e=>c("reconcile failed:",e.message)),2e3);wt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const kt=u(async()=>{try{for(const e of await Ga())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await kt();const gs=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+gs,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},W=an(),ws=u(()=>$.startedAt?wn({last:new Date($.startedAt),...W.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(St,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=te();let a;try{a=await Pn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){a={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=a.rate?.remaining??null,a.merged.length){try{await it({manifest:g(),log:c,routes:a.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${a.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:a.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await it({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Na({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{a.failed.push(...(await En({manifest:g()})).failed)}catch(o){a.failed.push(`incoming PRs: ${o.message}`)}a.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${a.failed.length} step${a.failed.length===1?"":"s"} failed, first: ${a.failed[0]} (logged at most hourly)`)),a.changes.length&&c(`github check: ${a.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${a.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of a.linear.moved)c(`linear: ${o} \u2192 ${r}`);a.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${a.linear.failed.length} step${a.linear.failed.length===1?"":"s"} failed, first: ${a.linear.failed[0]} (logged at most hourly)`));try{a.slack=await cs({boards:D(g())})}catch(o){a.slack={sent:[],failed:[o.message]}}for(const o of a.slack.sent)c(`slack: sent ${o}`);a.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${a.slack.failed.length} failed, first: ${a.slack.failed[0]} (logged at most hourly)`));try{a.deaf=await Cn({manifest:g()})}catch(o){a.deaf={pushed:[],failed:[o.message]}}(a.deaf.pushed.length||a.deaf.failed.length)&&c(`not listening: pushed ${a.deaf.pushed.join(", ")||"none"}${a.deaf.failed.length?`; ${a.deaf.failed.join("; ")}`:""}`);try{Mn(g())}catch(o){c("could not record link faults:",o.message)}if(a.flushed=null,a.held=fn(t,n)!==null,e&&!a.held)try{const o=await Ke();o&&(a.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=a,a})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let $t=null;function bs(){$t||=Rn({settings:te(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(bs,"runWakes");const Ce=new Set,I={running:null};function _t(){if(I.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),I.running=Yn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>qn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{I.running=null})}u(_t,"runSessionCheck"),await Ha(g());const ys=Date.now(),V=new Map;function Z(e){e?(X.delete(e),x.changed(e)):x.refresh()}u(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await F(n=>{const a=rn(n,{now:Date.now(),aliveSince:ys}),o=ke(n,Ie());for(const r of a)delete n.prompts[r.id];return{gone:a,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ie=u(()=>({running:nt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ka(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Q=null,Tt=null;async function pe(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,pe())):(Q=(async()=>xa({gate:ne(),resume:u(async e=>{const t=await qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}u(pe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),pe().catch(e=>c("usage gate failed:",e.message));try{Ra()}catch(e){c("machine check failed:",e.message)}W.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ws().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ua({listening:u(e=>Dn(e)&&me().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),bs(),ie.apply(),Me.tick(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(le)return;le=!0,clearInterval(ms),ie.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)fe(n,"SIGTERM");let e=!1;Ia().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),fe(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,a;try{a=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of a){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=S.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function ks(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const a=Pt(t.dir);return jt.set(e,{at:a,checked:Date.now()}),a}u(ks,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function me(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const a=j.get(t)||{},o=N(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?J.has(t)?J.get(t)?"up":"down":"starting":a.child?a.up?"up":"starting":a.state==="crashed"?"down":a.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:a.exitCode??null,owner:n.owner||null,restart:ga(n),changedAt:ks(t,n)||null,created:n.created||null,log:S.join(oe,t+".log")}})}u(me,"status");const U=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),Ae=new Map,X=new Map,Et=2e4,Ss=1500;async function $s(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Ss)});return t.ok?Wn(await t.json()):null}catch{return null}}u($s,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=X.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&X.set(o.route,{at:Date.now(),needs:$s(o.port)})}const a=await Promise.all(n.map(o=>X.get(o.route).needs));return new Map(n.map((o,r)=>{const f=Xn(Ae.get(o.route)??null,a[r],X.get(o.route).at,Date.now());return f.good?Ae.set(o.route,f.good):Ae.delete(o.route),[o.route,f.needs]}))}u(Dt,"waitingCounts");const Lt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Qe(null).length,"badgeTotal"),He=new Map;function _s(e){const t=new Map,n=new Map,a={running:nt(),wakes:tt(Ln,{})};for(const o of e)try{const r=Ye(o,a);n.set(o.route,r);let f=He.get(o.route);(!f||f.listen!==r||Date.now()-f.at>=Et)&&(f={at:Date.now(),listen:r,links:Nn(o,{listen:r})},He.set(o.route,f)),t.set(o.route,f.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u(_s,"boardLinks");async function Y({fresh:e=!1}={}){const t=new Date,n=me(),a=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:f,listens:h}=_s(r),l=Un(),_=new Map(r.map(i=>[i.route,va(i.owner?.session)]).filter(([,i])=>i)),k=ea(o,t.getTime(),la,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(k.map(i=>i.route)),m=new Map(r.filter(i=>Pa(i.dir)).map(i=>[i.route,ne({dir:i.dir})])),w=Vn(n,a,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=te(),C=hn(t,O);return{data:{code:l.running??null,version:dt,total:Lt(a),start:Zn(w),sites:w,hub:Qn(w,f,Bn(l)),home:b.realpathSync(ze.homedir()),starting:k,usage:(({start:i,...d})=>d)(us({codexShare:Ve(ae())})),pause:ne(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(C,t))}:{state:"on",text:"On"},away:!W.state().present},links:f,rows:n,code:l}}u(Y,"sidebarState");function Ct(e,t,n,a,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const f={...a,vary:[a.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),f["content-encoding"]="gzip"),f["content-length"]=r.length,t.writeHead(n,f),t.end(e.method==="HEAD"?void 0:r)}u(Ct,"sendText");async function xs(e,t=null){const n=t?t.sidebar((await Y()).data):(await Y()).data,a=Je.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Je.runInContext(b.readFileSync(S.join(xe,o),"utf8"),a);return Sa({sidebar:a.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(xs,"shellPage");const Me=Ua({root:se(),run:vs,log:c,pulled:u(()=>yt.restartNow(),"pulled"),awayFor:u(()=>{const{awaySince:e}=W.state();return e===null?0:Date.now()-e},"awayFor")});function vs({signal:e}){const t=S.join(oe,"kickflip-update.log");b.writeFileSync(t,`${new Date().toISOString()} kickflip update
6
+ `);const n=b.openSync(t,"a");return c("kickflip update: pulling the new kickflip"),new Promise(a=>{const o=Fe(jn(),[S.join(se(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",n,n]});b.closeSync(n),o.unref(),e.addEventListener("abort",()=>{try{process.kill(-o.pid,"SIGKILL")}catch{}a({ok:!1,error:"killed after 10 minutes"})},{once:!0}),o.on("error",r=>a({ok:!1,error:r.message})),o.on("exit",r=>{const f=b.readFileSync(t,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${r}: ${f}`),a(r===0?{ok:!0}:{ok:!1,error:f.replace(/^kickflip: /,"")})})})}u(vs,"runUpdate"),Me.tick();const x=$a({targets:u(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await Y({fresh:e})).data,"sidebar"),log:c});function ge(e){let t="0";try{t=Math.floor(b.statSync(S.join(G,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(ge,"iconUrl");const Ts=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ge(192),sizes:"192x192",type:"image/png"},{src:ge(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const a of e)if(n+=a,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function v(e,t){if(!_e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(v,"readJSONBody");const It=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <title>${U(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${ge(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${rt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Os=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:a,rows:o,code:r}=await Y();return{...yn({now:t,st:We(t),sidebar:n,rows:o,links:a,rules:$e(),integrations:await Ea.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ve(ae()),claudeModel:en(),codex:ae(),awake:{on:vn(),unsupported:Tn()},remembered:new Map(D(g()).map(f=>[f.route,Xe(f.route)]))}),prompts:Qe(null).map(ln),remembered:Xe(null)}}u(we,"hubData");const A=u(async()=>{const e=new Date,t=ne({now:e}),n=t.manual==="hub"?On(t.resets_at,e):null;return{...kn({now:e,st:We(e),rules:$e(),linear:re(),slack:rs(),pausedUntil:n}),closed:fa()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function js(e,t,n,a=null){const o=u((s,i,d,p={})=>{t.writeHead(s,{"content-type":i,"cache-control":"no-store",...p}),t.end(d)},"send"),r=u((s,i)=>{const d=Hn();return o(s,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),f=u(({route:s,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${s} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:s})),"starting"),h=Os(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await da(_[1]))}catch(s){return r(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let s;try{const i=await v(e);if(s=i.route,l==="/_hub/reopen-board"){const T=await pa(s);return x.refresh(),r(200,T)}if(B.has(s))return r(409,{error:"That board is already closing."});B.add(s);const d=u(()=>B.delete(s),"release");t.once("finish",d),t.once("close",d),await I.running;const p=await ha(s,i.name);return x.refresh(),r(200,p)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await v(e)).route,K.has(s))return r(409,{error:"That board is already restarting."});K.add(s),await I.running;const i=await ma(s,{serverUp:ps,portUp:Re,onStarting:u(d=>c(`restart /${s}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return s&&c(`restart /${s} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{K.delete(s),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=lt("hub");s&&(Ee=!1,c(`hub restart failed: ${s}`))}),r(200,{restarting:!0,command:ts().restartCommand}))}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const s=await Me.check();return s.state==="clone"&&(s.commit=Ge("git",["-C",se(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),St(W.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await v(e),i=D(g()).find(d=>d.route===s);return i?(Ce.add(s),await I.running,_t(),await I.running,He.clear(),x.changed(s),r(200,{route:s,listen:Ye(i)})):r(404,{error:"That board is not open."})}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/repo"&&e.method==="GET")return na(n,{repos:H,manifest:g(),sendJSON:r});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?st(k[1]):await aa(k[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return f(await oa(s,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return f(await ia(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const s=await ca(m[1]);return x.refresh(),r(200,s)}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}if((l==="/_hub/build"||l==="/_hub/pull"||l==="/_hub/kill")&&e.method==="POST"){if(le)return r(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(p=>p==="forwarded"||p.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":l==="/_hub/kill"?"Serves":"Builds"} can only be ${l==="/_hub/kill"?"killed":"started"} from the host over loopback.`});let d;try{d=await v(e)}catch(p){return r(p.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Ma(d,{manifest:g()})):l==="/_hub/kill"?r(200,await Ca(d,{manifest:g()})):r(202,await La(d,{manifest:g(),log:c}))}catch(p){if(p instanceof Da)return r(p.status,{error:p.message,...p.ask?{ask:p.ask}:{}});throw p}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",Ts());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(S.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const s=n.searchParams.get("v")===rt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ls),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:s}=await Y({fresh:n.searchParams.get("fresh")==="1"});return r(200,a?a.sidebar(s):s)}if(l==="/_hub/stream"&&e.method==="GET"){if(a){const s={login:a.login,routes:a.routes,res:t};be.add(s),e.on("close",()=>be.delete(s)),e.on("close",q.watching(a.login))}x.handle(e,t,a&&{routes:a.routes,view:a.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const s=P(g().sites,O[1]);if(!Se(s))return r(404,{error:"That board is not open."});const i=Qa(tt(S.join(s.dir,"board.json"),{})),d=i.map(L=>L.login),p=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=q.people(O[1],d).map((L,ee)=>({...L,level:i[ee].level}));return r(200,{people:T,refused:q.refusals(O[1],d),link:p?`https://${p}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const s=ba(l.slice(10));if(!s){t.writeHead(404).end();return}const i=S.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],p=n.searchParams.get("v")===ka(s)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(s),{"cache-control":p}):Ct(e,t,200,{"content-type":d,"cache-control":p},b.readFileSync(s))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await A());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${s}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const s=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:s.filter(i=>i.status>=200&&i.status<300).length,results:s})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=s.get("set"),d=i==="on"?{on:s.get("value")==="on"}:i==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,p="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:p}):o(400,"text/plain; charset=utf-8",p);let T;try{T=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await A()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{xn(s.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return ie.apply(),h?r(200,await A()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{ja(s.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await pe(),h?r(200,await A()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{qt(s.get("value"),ae({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{tn(s.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?Ta(s.get("value")):Oa(s.get("value")),await pe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(ns(),c("linear: key removed"),await kt()):c(`linear: key ${(await as(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(os(),c("slack: token removed")):c(`slack: token ${(await is(s.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof ss))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const s=re();if(!s.set||s.refused)return r(200,s);const i=ut();try{const d=await i.teams();return r(200,{...re(),teams:d.map(({key:p,name:T})=>({key:p,name:T})),projects:d.flatMap(p=>p.projects.map(T=>({id:T.id,name:T.name,team:p.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...re(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const s=ut();if(!s)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(p){return r(p.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await s.createProject(i.team,d))}catch(p){if(!(p instanceof Oe))throw p;return r(p.status,{error:p.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>s.has(d)).map(d=>[d,s.get(d).trim()]));s.get("set")==="hold"&&(i.hold=s.get("value")||"off");try{Qt(mn(te(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await A()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let s,i;try{s=await v(e,64*1024),i=zn($e(),s.rules,s.base??[]),at(i.changes)&&Jn(i.rules)}catch(p){if(!(p instanceof Gn)&&!(p instanceof SyntaxError))throw p;return r(400,{error:p.message})}if(!at(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(p=>p.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const s=await Ke();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?r(200,Sn(s)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const s=await v(e);return r(200,await Le({flush:s.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(p){if(p.status)throw p;return r(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await F(p=>{const T=Ie(),L=T.boards.find(Be=>Be.owner?.session&&Be.owner.session===s.session_id)||null;ke(p,T)&&x.refresh();const ee=dn(p,s);if(ee)return{answer:ee};const ye=on(s,{board:L,live:T.running?.get(s.session_id)});return p.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await vt(s),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(p=>c(`prompt ${d.id}: push failed: ${p.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(s)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?Ps(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(js,"hubRoute");async function Ps(e,t,{id:n,op:a,url:o,sendJSON:r}){if(a==="wait"&&e.method==="GET"){const f=u(()=>F(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await f();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ze,Ze));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await f();return k?k.answer?h(k):r(200,{state:"pending"}):r(404,{state:"gone"})}if(a==="answer"&&e.method==="POST"){const f=await v(e),h=await F(k=>{const y=P(k.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=sn(y,f);if(w)return{status:400,error:w};y.answer=m;const O=Ie();return ke(k,O),un(k,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(l.route),r(200,{ok:!0})}if(a===null&&e.method==="DELETE"){const f=await F(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return f&&(Z(f.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(Ps,"promptRoute");const Rs=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function At(e,t){const n=j.get(e)||{},a=N(t);return a==="port"?"stopped":a==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(At,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":At(t,n)}),e.end(Es(t,n))}u(Ht,"sendDown");function Es(e,t){const n=j.get(e)||{},a=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${a}`},r=At(e,t),f=r==="code"||r==="starting"?"wait":"down",h=N(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${B(t.title||e)} isn't answering</h1>
12
- <p class="lede">${B(l)}</p>
11
+ <h1>${U(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${U(l)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${Ra({icon:Kn(e,t),title:B(t.title||e),sub:`<span class="st ${f}">${B(o[r])}</span>`})}</div>
14
+ <div class="group">${Rs({icon:Kn(e,t),title:U(t.title||e),sub:`<span class="st ${f}">${U(o[r])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}u(Ea,"downPage");const Ne=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=ea({file:k.join(z,"share-seen.json")}),be=new Set;function Mt(){const e=g().sites;for(const t of be){const n=ct(t.login,e);[...t.routes].some(([s,o])=>n.get(s)!==o)&&(be.delete(t),t.res.destroy())}As()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Nt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Ne(e),r=ct(n,g().sites),f=r.size?Xs({method:e.method,pathname:o,routes:r}):"other",h=Ys(o);if(f==="other")return(!r.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(f!=="pass")return{verdict:f};Y.visit(n,s,h);const l=await Te.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>qs(_,r,l?.name||l?.login),"sidebar")}}}u(Nt,"access");const Ut=Be.createServer((e,t)=>{Nt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Da(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Vs(e,t,n)})});function Da(e,t,n=null){const s=Ne(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ws(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(ot({method:e.method,pathname:s.pathname,search:s.search,dest:ys(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);xa(m,n).then(w=>Lt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:me()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=me();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Ct(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){ja(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],f=P(g().sites,r);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${B(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,l=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=ot({pathname:s.pathname,dest:"document",sites:o})==="shell";if(N(f)==="files"){if(!Rt(f.dir)){Ht(t,r,f);return}Ns(e,t,{dir:f.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Be.request({host:"127.0.0.1",port:f.port,method:e.method,path:l,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,r,f)}),e.pipe(y)}u(Da,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Nt(e);if(s!=="owner"){Zs(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Ne(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const f=`/${o}`;Ws({req:e,socket:t,head:n,port:r.port,path:e.url.slice(f.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${f}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${ht})`));
16
+ </main></body></html>`}u(Es,"downPage");const Ne=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),q=es({file:S.join(z,"share-seen.json")}),be=new Set;function Mt(){const e=g().sites;for(const t of be){const n=ct(t.login,e);[...t.routes].some(([a,o])=>n.get(a)!==o)&&(be.delete(t),t.res.destroy())}Aa()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Nt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const a=e.headers["tailscale-user-name"]||null,{pathname:o}=Ne(e),r=ct(n,g().sites),f=r.size?Xa({method:e.method,pathname:o,routes:r}):"other",h=Ya(o);if(f==="other")return(!r.size||h)&&q.refuse(n,a,h),{verdict:"other"};if(f!=="pass")return{verdict:f};q.visit(n,a,h);const l=await Te.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>qa(_,r,l?.name||l?.login),"sidebar")}}}u(Nt,"access");const Bt=Ue.createServer((e,t)=>{Nt(e).then(({verdict:n,viewer:a})=>{n==="owner"||n==="shared"?Ds(e,t,a):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Va(e,t,n)})});function Ds(e,t,n=null){const a=Ne(e);if(e.method!=="GET"&&e.method!=="HEAD"&&wa(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(ot({method:e.method,pathname:a.pathname,search:a.search,dest:ya(e.headers),sites:o})==="shell"){const m=a.pathname==="/"?"":a.pathname.startsWith("/_hub/")?a.pathname.slice(1):decodeURIComponent(a.pathname.split("/")[1]);xs(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(a.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+a.search}).end();return}if(a.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:me()},null,2));return}if(a.pathname==="/_hub/needs.json"){const m=me();Dt(m,{fresh:a.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(a.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(a.pathname==="/sw.js"||a.pathname==="/manifest.webmanifest"||a.pathname.startsWith("/_hub/")){js(e,t,a,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=a.pathname.split("/")[1],f=P(g().sites,r);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${It("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${U(r)}</h1></main></body></html>`);return}if(a.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+a.search}).end();return}const h="/"+r,l=n?a.pathname.slice(h.length)+a.search:e.url.slice(h.length)||"/",_=ot({pathname:a.pathname,dest:"document",sites:o})==="shell";if(N(f)==="files"){if(!Rt(f.dir)){Ht(t,r,f);return}Ba(e,t,{dir:f.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const y=Ue.request({host:"127.0.0.1",port:f.port,method:e.method,path:l,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,r,f)}),e.pipe(y)}u(Ds,"serve"),Bt.on("upgrade",async(e,t,n)=>{const{verdict:a}=await Nt(e);if(a!=="owner"){Za(t,a==="shared"?"view-only":a==="home"?"other":a);return}const o=Ne(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const f=`/${o}`;Wa({req:e,socket:t,head:n,port:r.port,path:e.url.slice(f.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${f}`]})}),Bt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${ht})`));
package/hub/presence.mjs CHANGED
@@ -1 +1 @@
1
- var m=Object.defineProperty;var a=(r,e)=>m(r,"name",{value:e,configurable:!0});import{execFile as f}from"node:child_process";const d=3*6e4,I=a(r=>new Promise((e,o)=>{f("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function p(r=I){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...n));return{atMac:!t&&s<d,locked:t,idleMs:s}}catch(e){return{atMac:!1,error:String(e.message||e)}}}a(p,"readPresence");function h({read:r=p,clock:e=Date.now}={}){let o=-1/0,t=!1,n=null;const s=a(i=>{const l=e(),c=i||l-o<d,u={present:c,changed:c!==t,back:c&&!t};return u.back&&(n=l),t=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return o=e(),s(!0)},state:a(()=>({present:t,backAt:n}),"state")}}a(h,"ownerTracker");export{d as IDLE_LIMIT_MS,h as ownerTracker,p as readPresence};
1
+ var f=Object.defineProperty;var s=(n,e)=>f(n,"name",{value:e,configurable:!0});import{execFile as I}from"node:child_process";const d=3*6e4,p=s(n=>new Promise((e,a)=>{I("ioreg",n,{timeout:5e3,maxBuffer:32*1024*1024},(t,r)=>t?a(t):e(r))}),"ioreg");async function M(n=p){try{const[e,a]=await Promise.all([n(["-n","Root","-d1"]),n(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),r=[...a.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!r.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const o=Math.round(Math.min(...r));return{atMac:!t&&o<d,locked:t,idleMs:o}}catch(e){return{atMac:!1,error:String(e.message||e)}}}s(M,"readPresence");function w({read:n=M,clock:e=Date.now}={}){let a=-1/0,t=!1,r=null,o=e();const i=s(m=>{const l=e(),c=m||l-a<d,u={present:c,changed:c!==t,back:c&&!t};return u.back&&(r=l),c?o=null:o??=l,t=c,u},"update");return{async check(){return i((await n()).atMac)},beat(){return a=e(),i(!0)},state:s(()=>({present:t,backAt:r,awaySince:o}),"state")}}s(w,"ownerTracker");export{d as IDLE_LIMIT_MS,w as ownerTracker,M as readPresence};
package/hub/try.mjs CHANGED
@@ -1,12 +1,13 @@
1
- var Oe=Object.defineProperty;var a=(o,d)=>Oe(o,"name",{value:d,configurable:!0});import k from"node:fs";import T from"node:path";import Jt from"node:os";import Tt from"node:http";import{execFile as xe,spawn as Ee}from"node:child_process";import{createInterface as zt}from"node:readline";import{promisify as Te}from"node:util";import{listBoards as J,readManifest as Q,readJSON as ft,saveJSON as Yt,appendEvent as je,GITHUB_STATE as _e,REPO_FETCH as Qt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Vt,mainRefOf as Ie}from"./board-repo.mjs";import{gitReachFault as jt,remoteFromInput as Ae,remoteGitEnv as Me}from"./repos.mjs";import{readiness as Pe}from"./readiness.mjs";import{tailscaleBin as Ce}from"../shared/tailscale.mjs";import{buildScriptError as Fe,buildScriptOf as qt}from"../board/build-script.mjs";import{appendStatus as Xt,recordedWorktree as Ne,servedRoundsOpen as Be,setItem as De}from"../board/store.mjs";import{BASE_KEY as U,baseWorktreeOf as Re}from"../board/prototype-base.mjs";import{cardPRs as He,hasPR as We,prKeyOf as Ge}from"../shared/card-prs.mjs";import{hubGate as Le}from"../board/on-host.mjs";import{pipeUpgrade as Ue,refuse as Ke,refuseUpgrade as Je}from"./front-door.mjs";import{shareOf as ze}from"./share.mjs";class A extends Error{static{a(this,"BuildError")}constructor(d,h=400){super(d),this.status=h}}const rt=Te(xe),M=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),Ye=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,V={first:8500,last:8599},_t=a(o=>/^\d+$/.test(String(o))&&Number(o)>=V.first&&Number(o)<=V.last,"isTryPort"),Qe=30*6e4,Zt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Ve=["x-frame-options"],qe=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([h])=>!Ve.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),yt={main:"main",[U]:"prototypes"},H=a(o=>!Object.hasOwn(yt,o),"isCard"),te=a((o,d)=>T.join(o,qt(d)),"scriptAt"),gt=a(o=>k.existsSync(o),"exists"),ee=a((o,d)=>T.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function It(o,d,h){const c=ee(o,d);return k.mkdirSync(T.dirname(c),{recursive:!0}),k.writeFileSync(c,h.message+`
2
- `,{mode:384}),k.chmodSync(c,384),c}a(It,"failureLog");const re=a(o=>new Promise(d=>{const h=Tt.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ie=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),se=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),At=a(o=>new Promise(d=>{const h=a(()=>{clearTimeout(c),clearInterval(y),d()},"finish");ie(o,"SIGTERM");let c=setTimeout(()=>{ie(o,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const y=setInterval(()=>{se(o)||h()},20)}),"endGroup");async function Xe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
3
- `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===o).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await ne(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}a(Xe,"groupMembers");async function ne(o,d){let h="";try{h=(await rt("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(u){h=u.stdout||""}const c=new Map(o.map(u=>[u.pid,u]));let y;for(const u of h.split(`
4
- `))u[0]==="p"?y=c.get(u.slice(1)):u[0]==="n"&&y&&(y.cwd=u.slice(1));for(const u of o){if(!u.cwd&&k.existsSync("/proc"))try{u.cwd=k.readlinkSync(`/proc/${u.pid}/cwd`)}catch{}u.cwd=u.cwd.replace(/ \(deleted\)$/,"")}}a(ne,"readCwds");async function Ze(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
5
- `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await ne(h,["-d","cwd","-Fpn"]),h.filter(c=>oe(c,o))}a(Ze,"processesIn");const tr=a((o,d)=>o===d||o.startsWith(d+T.sep),"insideDir"),oe=a((o,d)=>tr(o.cwd,d)||o.command.includes(d+T.sep)||o.command.endsWith(d),"inWorktree");async function ae(o,d,h){const c=await Ne(o.dir,d),[y,u]=d.split("/"),S=o.files.find(O=>O.slug===y)?.itemFile,g=c?.path||S?.items.find(O=>O.id===u)?.worktree;if(typeof g!="string"||!g||g.split(T.sep).includes(".."))return"";let m=c?.repo;const p=(o.github||S?.repo||"").toLowerCase();if(!m&&p){const O=[o.owner?.repo,o.dir,...Object.values(h?.sites||{}).map(N=>N.owner?.repo)];for(const N of new Set(O.filter(x=>typeof x=="string"&&T.isAbsolute(x))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(N,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await M(N,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!m||c?.path&&!T.isAbsolute(g))return"";try{const O=[T.join(k.realpathSync(Jt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&T.isAbsolute(m))try{O.push(T.join(k.realpathSync(m),".claude","worktrees"))}catch{}const N=T.resolve(m||"/",g),x=k.realpathSync(N);return O.some(z=>N.startsWith(z+T.sep)&&x.startsWith(z+T.sep))&&k.statSync(x).isDirectory()?x:""}catch{return""}}a(ae,"installWorktree");function er({now:o=Date.now,answers:d=re,lingers:h=re,log:c=console.log,waiting:y=a(async()=>null,"waiting"),event:u=je,gate:S=Le}={}){const g=new Map,m=new Map,p=new Map,O=new Set,N=a(()=>[...m.values(),...p.values()],"live"),x=new Set,z=new Set,it=new Map,st=new Map,bt=new Map,nt=new Map;let W=!1,G=null,X=null,pt=null;const ht=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if(g.has(t.dir))return g.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=ft(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},..._t(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}}:{}},i={state:s,file:e,written:JSON.stringify(r)};g.set(t.dir,i),s.main_port&&st.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,l])=>l.state==="running");for(const[l]of n)z.add(D(t,l));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),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),P(t),i}a(C,"stateOf");function P(t){const e=g.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Yt(e.file,e.state),e.written=r)}a(P,"persist");function ce(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),P(t.board)}a(ce,"recordGroup");function le(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,P(t.board))}a(le,"forgetGroup");async function de(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,P(t);for(const s of r){if(!se(s.pgid))continue;let i=[];try{i=await Xe(s.pgid)}catch(n){c(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>oe(n,s.worktree))&&(await At(s.pgid),c(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await Mt(t,s.key,s.port))}}a(de,"sweepGroups");async function Mt(t,e,r){if(!r||!await h(r))return;const s=`Port ${r} still answers after the run was stopped.`;H(e)&&await Xt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>c(`build /${t.route} ${e}: ${i.message}`)),c(`build /${t.route} ${e}: ${s}`)}a(Mt,"checkQuiet");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),Pt=a((t,e)=>H(e)?t.cards[e]:t[yt[e]],"entryOf"),ue=a((t,e,r)=>{H(e)?t.cards[e]=r:t[yt[e]]=r},"setEntry"),Ct=a((t,e)=>{H(e)?delete t.cards[e]:t[yt[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=Pt(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,H(e)&&n?{at:ht()}:{}),P(t)}a(K,"update");function kt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=ft(T.join(r.dir,"data","try.json"),{}).main_port;_t(s)&&st.set(r.dir,Number(s))}}a(kt,"reserveMains");function fe(t){const{board:e,key:r}=t,s=D(e,r),i=r==="main"?st.get(e.dir):it.get(s);if(i)return i;const n=new Set([...st.values(),...it.values()]);let l=V.first;for(;n.has(l)&&l<=V.last;)l++;if(l>V.last)throw new Error(`No try ports available (${V.first}\u2013${V.last}). Stop a served card and retry.`);return r==="main"?(st.set(e.dir,l),C(e).state.main_port=l,P(e)):it.set(s,l),l}a(fe,"publicPort");const ot=a(async t=>{const e=Ce();if(!e)throw new Error("Tailscale isn't installed on this host");return rt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ft(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ft,"removePort");const Nt=a(t=>ze(ft(T.join(t.dir,"board.json"),{})),"sharedWith");async function Bt(t,e){const r=await S(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Nt(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Bt,"relayVerdict");function pe(){for(const t of N()){if(!t.shared?.size)continue;const e=Nt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(pe,"endUnshared");async function he(t){t.https=fe(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Tt.createServer(async(n,l)=>{const $=await Bt(n,t.board);if($!=="owner"&&$!=="shared"){Ke(n,l,$);return}if($==="shared"){const v={login:n.headers["tailscale-user-login"],res:l};i.add(v),l.on("close",()=>i.delete(v))}const b=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Zt.includes(v))),w=Tt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:b},v=>{l.writeHead(v.statusCode,t.key===U?qe(v.headers):v.headers),v.on("data",r),v.pipe(l),v.on("close",()=>{v.complete||l.destroy()})});l.on("close",()=>{l.writableFinished||w.destroy()}),w.on("error",()=>{l.headersSent?l.destroy():l.writeHead(502).end()}),n.pipe(w)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,l,$)=>{const b=await Bt(n,t.board);if(b!=="owner"){Je(l,b==="shared"?"view-only":b);return}s(Ue({req:n,socket:l,head:$,port:t.port,path:n.url,replace:Zt}))}),await new Promise((n,l)=>{t.relay.once("error",l),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await ot(["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(he,"openPort");async function me(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!W&&!e&&await Ft(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||it.delete(D(t.board,t.key))}a(me,"closePort");async function we(){const t=N().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await ot(["serve","status","--json"])).stdout)}catch(r){c(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){c(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await ot(["serve","--bg",`--https=${r.https}`,s])}catch(n){c(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(we,"reserve");function at(t=Q()){if(pt)return pt;kt(t);for(const e of J(t))C(e);return pt=(async()=>{for(const e of J(t))await de(e).catch(r=>c(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await ot(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))_t(s)&&![...m.values()].some(i=>i.https===Number(s))&&await Ft(s)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),pt}a(at,"initialize");async function Dt(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!H(n))return;let l="",$=!1;try{for await(const v of zt({input:k.createReadStream(t.logFile),crlfDelay:1/0}))$||=Ye.test(v),v.trim()&&(l=v)}catch(v){l=`could not read the build log: ${v.message}`}const b=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(j=>j.install?.url))?"app":"web"))==="web"?"Serve":"Build",w=$?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||l;await Xt(i.dir,n,"run-failed",{kind:b,code:e??1,why:w})}a(Dt,"failure");function mt(t){return t.ending||(t.ending=At(t.child.pid)),t.ending}a(mt,"terminate");async function ct(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await mt(t),await t.done}a(ct,"halt");async function Z(t,e=!1){await ct(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||bt.delete(D(t.board,t.key))}a(Z,"stopRun");async function St(t,e,r,s={},i=""){if(!r)throw new A("This card has no valid worktree.");const n=te(r,t.build_target),l=Fe(n);if(l)throw new A(l);if(!k.realpathSync(n).startsWith(k.realpathSync(r)+T.sep))throw new A("Build script must be inside the worktree.");const $=await M(r,"rev-parse","HEAD");if(W)throw new A("The hub is stopping. Try again after it restarts.",503);const{state:b}=C(t),w=Pt(b,e),v={...H(e)?w:{},...s,sha:$,script:!0,state:"running",...H(e)?{at:ht(),head:$}:{},...e===U?{since:ht()}:{}};delete v.port,delete v.https;const j=ee(t,e);k.mkdirSync(T.dirname(j),{recursive:!0});const I=k.openSync(j,"w",384);k.fchmodSync(I,384),i&&k.writeSync(I,i);const _=Ee(n,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});ue(b,e,v);const E=D(t,e),f={child:_,board:t,key:e,worktree:r,sha:$,at:s.at,metadata:s,logFile:j,visited:o(),stopped:!1,port:null,ready:!1};ce(f),m.set(E,f),bt.set(E,$);let F,tt;const B=a(R=>{tt||=new Error(R),mt(f)},"fail");_.on("error",R=>{F=R}),_.stdout.on("data",R=>k.writeSync(I,R)),_.stderr.on("data",R=>k.writeSync(I,R));const lt=zt({input:_.stdout,crlfDelay:1/0});return lt.on("line",R=>{const dt=/^port (\d{1,5})$/.exec(R);if(!dt||f.port||f.stopped)return;const L=Number(dt[1]);if(L<1||L>65535){B("Build script printed an invalid port.");return}f.port=L,f.group.port=L,P(t),clearTimeout(f.deadline);let Y,Et=!1;const Ut=a(async()=>{if(!(Et||f.stopped||f.ready||_.exitCode!==null||_.signalCode!==null)){Et=!0;try{if(await d(L)){if(f.stopped||tt||_.exitCode!==null||_.signalCode!==null||(f.opening=he(f),await f.opening,f.stopped||tt||_.exitCode!==null||_.signalCode!==null))return;f.ready=!0,f.visited=o(),b.kind="web",K(t,e,{state:"ready",port:L,https:f.https}),clearInterval(f.poll),e==="main"&&await wt(t)}else o()>=Y&&B("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{Et=!1}}},"check"),Kt=a(()=>{f.stopped||(Y=o()+6e4,f.poll=setInterval(()=>Ut().catch(ut=>B(ut.message)),250),Ut().catch(ut=>B(ut.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===L?wt(t).then(Kt,ut=>B(ut.message)):Kt()}),f.deadline=setTimeout(()=>B("No port line within 60 minutes."),60*6e4),_.once("exit",()=>{mt(f)}),f.done=new Promise(R=>_.once("close",(dt,L)=>{clearTimeout(f.deadline),clearInterval(f.poll),lt.close(),k.closeSync(I),(async()=>{if(await mt(f),await me(f),le(f),W||await Mt(t,e,f.port),f.finished=!0,f.stopped)return;if(p.get(t.dir)===f){p.delete(t.dir),K(t,e,{serving:void 0});return}if(e==="main"&&!f.ready&&ye(t,f)&&!W){await ge(f);return}const Y=e===U&&!f.port?new Error('Build script exited without printing "port <n>".'):null;tt||F||dt!==0||f.port&&!f.ready||Y?await Dt(f,dt??1,L,tt||F||(f.port&&!f.ready?new Error("Build script exited before its port answered."):Y)):f.ready?(e==="main"?(delete b.main.state,delete b.main.port,delete b.main.https):Ct(b,e),P(t)):(b.kind="app",K(t,e,{state:"ready"}))})().catch(Y=>c(`build /${t.route} ${e}: ${Y.message}`)).finally(()=>{m.get(E)===f&&m.delete(E),R()})})),c(`build /${t.route} ${e}: started; log ${j}`),{status:"started"}}a(St,"launch");async function wt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ct(e))}a(wt,"retire");function ye(t,e){const r=p.get(t.dir);return!!r&&k.readFileSync(e.logFile,"utf8").includes(r.worktree)}a(ye,"refusedByOld");async function ge(t){const{board:e,worktree:r}=t;await wt(e),c(`try /${e.route} main: the new serve refused to run beside the old one; stopped the old serve and serving again in ${r}`);const{serving:s,...i}=t.metadata;try{await St(e,"main",r,i,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
6
- `)}catch(n){Rt(e,i,n)}}a(ge,"handOverFailed");function Rt(t,e,r){r.status!==503&&(C(t).state.main={...e,state:"failed"},It(t,"main",r),P(t))}a(Rt,"mainFailed");async function $e(t,e,r){const s=`try /${t.route} main:`;if(!O.has(t.dir)){O.add(t.dir);try{const i=await Ze(r);for(const n of new Set(i.map(l=>l.pgid))){await At(n);for(const l of i.filter($=>$.pgid===n))c(`${s} stopped pid ${l.pid} (pgid ${n}) in the old main worktree ${r}`)}}catch(i){c(`${s} could not stop the processes in the old main worktree ${r}: ${i.message}`)}}if(gt(r))try{await M(e,"worktree","remove","--force",r);return}catch(i){try{k.rmSync(r,{recursive:!0,force:!0}),c(`${s} git could not remove the old main worktree ${r}; removed the folder: ${(i.stderr||i.message).trim().split(`
7
- `).at(-1)}`),await M(e,"worktree","prune")}catch(n){c(`${s} could not remove the old main worktree ${r}: ${n.message}`)}}}a($e,"removeLegacyMain");async function Ht(t,e,r){const s=D(t,"main");if(x.has(s))throw new A("A build is already running for main.",409);x.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await Z(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const l=a(b=>T.join(Jt.homedir(),".claude","kickflip","try",`${t.route}-main${b}`),"worktreeOf"),$=l(n?.worktree===l("-a")?"-b":"-a");return k.mkdirSync(T.dirname($),{recursive:!0}),await $e(t,e,l("")),gt($)?await M($,"checkout","--detach","--force",r.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",$,r.sha)),await St(t,"main",$,r)}catch(i){throw Rt(t,r,i),i}finally{x.delete(s)}}a(Ht,"launchMain");async function Ot(t,{manifest:e=Q()}={}){if(await at(e),kt(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(w=>!["route","key"].includes(w))||typeof t.route!="string"||typeof t.key!="string")throw new A("Send only the board route and card key.");const{route:r,key:s}=t;if(H(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(w=>w==="."||w==="..")))throw new A("Card key must be <file>/<id>.");const i=J(e).find(w=>w.route===r);if(!i)throw new A(`/${r} is not a board.`,404);if(H(s)&&!i.files.some(({slug:w,itemFile:v})=>v.items.some(j=>`${w}/${j.id}`===s)))throw new A("There is no item with this card key.",404);const n=await y();if(n)throw new A(n,409);const l=D(i,s),$=m.get(l);if(x.has(l)||$&&!$.ready&&!$.finished)throw new A(`A build is already running for ${s==="main"?"main":s===U?"the prototype site":"this card"}.`,409);if(s==="main"){const w=C(i).state.main;if(w?.state==="running")throw new A("A build is already running for main.",409);if(w?.state!=="failed"||!w.sha)throw new A("Main has no failed build to retry.",409);const{sha:v,branch:j,slug:I,at:_,script:E}=w;return Ht(i,et(r,e).repo,{sha:v,branch:j,slug:I,at:_,script:E,...i.build_target?{target:i.build_target}:{}})}x.add(l);const b=z.delete(l);try{m.has(l)&&await Z(m.get(l),!0);const w=s===U?Wt(i):await ae(i,s,e);return await St(i,s,w)}catch(w){throw it.delete(l),b&&w.status!==503&&K(i,s,{state:void 0,sha:void 0}),w}finally{x.delete(l)}}a(Ot,"start");const Wt=a(t=>{const e=Re(t.route);return gt(e)&&k.statSync(e).isDirectory()?k.realpathSync(e):""},"baseWorktree");async function ve(t,e){const{state:r}=C(t),s=D(t,U),i=m.get(s),n=(await Be(t.dir)).length>0&&Wt(t);if(!n){i&&await Z(i),r.prototypes&&(r.prototypes=null,P(t));return}if(i?.ready&&!i.stopped&&!x.has(s)){const l=await M(n,"rev-parse","HEAD");if(l===r.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(i||x.has(s)||r.prototypes?.state==="failed"||await y())return;try{await Ot({route:t.route,key:U},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw r.prototypes={state:"failed"},It(t,U,l),P(t),l}}a(ve,"serveBase");function Gt(t,e,{manifest:r=Q()}={}){const s=J(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Gt,"lookup");async function xt({manifest:t=Q(),routes:e}={}){if(await at(t),kt(t),W)return G;if(G&&e){if(!X){const r={routes:new Set,manifest:t};r.done=G.catch(()=>{}).then(()=>(X=null,xt({manifest:r.manifest,routes:[...r.routes]}))),X=r}X.manifest=t;for(const r of e)X.routes.add(r);return X.done}return G||(G=(async()=>{await we();const r=J(t),s=ft(_e,{}).prs||{};for(const[i,n]of[...p])r.some(l=>l.dir===i)||(p.delete(i),await ct(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||H(i.key)&&i.ready&&o()-i.visited>=Qe){await Z(i);const{state:n}=C(i.board);Ct(n,i.key),P(i.board)}for(const i of r){if(W)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),l=et(i.route,t),$=l.branch||"main",b=Ie(l);for(const{slug:w,itemFile:v}of i.files)for(const j of v.items){const I=`${w}/${j.id}`,_=He(j,i.github||v.repo).map(E=>s[Ge(E)]);if(!(j.lane==="done"||_.length&&_.every(E=>["MERGED","CLOSED"].includes(E?.state))))try{const E=D(i,I),f=await ae(i,I,t);if(f){const B=await M(f,"rev-parse","HEAD");let lt=0;try{lt=Number(await M(f,"rev-list","--count",`${b}..HEAD`))}catch{}Object.assign(n.cards[I]||={},{head:B,ahead:lt,script:gt(te(f,i.build_target))}),P(i)}else if(!z.has(E))continue;const F=n.cards[I];if((z.has(E)||i.auto_build&&F.script&&F.ahead>0&&(F.sha||bt.get(E))!==F.head&&F.state!=="running")&&!x.has(E))try{await Ot({route:i.route,key:I},{manifest:t})}catch(B){if([409,503].includes(B.status))continue;throw K(i,I,{sha:F.head}),await Dt({board:i,key:I,logFile:It(i,I,B)},1),B}}catch(E){c(`try /${i.route} ${I}: ${E.message}`)}}await ve(i,t).catch(w=>c(`try /${i.route} prototypes: ${w.message}`));try{if(!l.repo||await y())continue;const w=await M(l.repo,"rev-parse",b);let v=!1;try{await M(l.repo,"cat-file","-e",`${w}:${qt(i.build_target)}`),v=!0}catch{}if(!v){const F=m.get(D(i,"main"));F&&await Z(F),await wt(i),n.main=null,P(i);continue}const j=n.main;if(j?.state==="running"||j?.sha===w&&(j.target||null)===(i.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Gt(i.route,"main",{manifest:t}))))continue;const I=D(i,"main");if(x.has(I))continue;const _=await M(l.repo,"log","-1","--format=%cI",w);let E=i.github;if(!E)try{E=Vt(await M(l.repo,"remote","get-url","origin"))}catch{}const f={sha:w,branch:$,slug:E||"",at:_,script:v,...i.build_target?{target:i.build_target}:{}};await Ht(i,l.repo,f)}catch(w){c(`try /${i.route} main: ${w.message}`)}}})().finally(()=>{G=null}),G)}a(xt,"tick");function Lt(t,e){if(nt.has(t.dir))return nt.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},P(t);let l,$;try{l=await(s.local?ir:rr)(s.repo,i)}catch(b){$=b.message}if(l===null?delete n.pull:n.pull={at:ht(),...$?{error:$}:{}},P(t),l===null)return null;if($)return c(`pull /${t.route}: ${$}`),{commits:0,error:$};if(l.commits){c(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await u(t,"main-pulled",{branch:i,...l})}catch(b){c(`pull /${t.route}: could not send main-pulled (${b.message})`)}}return{commits:l.commits}})().finally(()=>nt.delete(t.dir));return nt.set(t.dir,r),r}a(Lt,"pullMain");async function be({manifest:t=Q(),routes:e}={}){await at(t);const r=[];for(const s of J(t)){if(W)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Lt(s,t))?.commits&&r.push(s.route)}catch(i){c(`pull /${s.route} failed: ${i.message}`)}}return r}a(be,"pullMains");async function ke(t,{manifest:e=Q()}={}){if(await at(e),W)throw new A("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 A("Send only the board route.");const r=J(e).find(n=>n.route===t.route);if(!r)throw new A(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Lt(r,e);if(!i)throw new A("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&xt({manifest:e,routes:[r.route]}).catch(n=>c(`try /${r.route} main: ${n.message}`)),i}a(ke,"pull");async function Se(){W=!0;const t=Promise.all([...m.values()].map(e=>e.ready?Z(e):ct(e)).concat([...p.values()].map(e=>ct(e))));await Promise.all([t,G,...nt.values()])}return a(Se,"stop"),{start:Ot,tick:xt,stop:Se,lookup:Gt,initialize:at,pull:ke,pullMains:be,endUnshared:pe}}a(er,"createTryManager");const $t=1e4,vt=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:$t,env:Me()})).stdout,"originGit");async function rr(o,d,{run:h=vt}={}){const c=a(async(...p)=>(await h(...p)).trim(),"git"),y=a(p=>c(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),u=`refs/remotes/origin/${d}`,S=await y(`refs/heads/${d}`)||await y(u);try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${u}`),await c(o,"checkout","--quiet","--force","-B",d,u),await c(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${$t/1e3} s`:jt(p.stderr,{repo:o}).error}`)}const g=await y(u),m=S&&S!==g?Number(await c(o,"rev-list","--count",`${S}..${g}`)):0;return{from:S||g,to:g,commits:m}}a(rr,"resetBranch");async function ir(o,d,{run:h=vt}={}){const c=a(async(...u)=>(await h(...u)).trim(),"git"),y=a(u=>{throw new Error(`can\u2019t pull: ${u}`)},"refuse");try{await c(o,"config","--get","remote.origin.url")}catch{return null}try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(u){y(u.killed?`git fetch timed out after ${$t/1e3} s`:jt(u.stderr,{repo:o}).error)}try{const u=await c(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),S=await c(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=a((O,N)=>c(o,"merge-base","--is-ancestor",O,N).then(()=>!0,x=>{if(x.code===1)return!1;throw x}),"isAncestor");if(u===S||await g(S,u))return{from:u,to:u,commits:0};await g(u,S)||y(`${d} has split from origin`);const m=Number(await c(o,"rev-list","--count",`${u}..${S}`)),p=(await c(o,"worktree","list","--porcelain")).split(`
1
+ var Me=Object.defineProperty;var o=(a,d)=>Me(a,"name",{value:d,configurable:!0});import O from"node:fs";import j from"node:path";import qt from"node:os";import Ft from"node:http";import{execFile as Ie,spawn as Pe}from"node:child_process";import{createInterface as Xt}from"node:readline";import{promisify as Be}from"node:util";import{listBoards as z,readManifest as q,readJSON as ot,saveJSON as Zt,appendEvent as Fe,GITHUB_STATE as Ne,REPO_FETCH as te}from"./boards.mjs";import{boardRepo as at,githubFromOrigin as ee,mainRefOf as Ce}from"./board-repo.mjs";import{gitReachFault as Nt,remoteFromInput as Re,remoteGitEnv as De}from"./repos.mjs";import{readiness as We}from"./readiness.mjs";import{tailscaleBin as He}from"../shared/tailscale.mjs";import{buildScriptError as Ge,buildScriptOf as re}from"../board/build-script.mjs";import{appendStatus as se,recordedWorktree as Le,servedRoundsOpen as Ke,setItem as Ue,settingsOf as Je}from"../board/store.mjs";import{readMachineState as Ye}from"../shared/machine-gate.mjs";import{BASE_KEY as Q,baseWorktreeOf as ze}from"../board/prototype-base.mjs";import{cardPRs as Qe,hasPR as Ve,prKeyOf as qe}from"../shared/card-prs.mjs";import{hubGate as Xe}from"../board/on-host.mjs";import{pipeUpgrade as Ze,refuse as tr,refuseUpgrade as er}from"./front-door.mjs";import{shareOf as rr}from"./share.mjs";class T extends Error{static{o(this,"BuildError")}constructor(d,h=400,c){super(d),this.status=h,c&&(this.ask=c)}}const ct=Be(Ie),M=o(async(a,...d)=>(await ct("git",["-C",a,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),sr=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,tt={first:8500,last:8599},Ct=o(a=>/^\d+$/.test(String(a))&&Number(a)>=tt.first&&Number(a)<=tt.last,"isTryPort"),Tt=3,Rt=10*6e4,ir="The host is busy, start this serve anyway?",ie=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],nr=["x-frame-options"],or=o(a=>{const d=Object.fromEntries(Object.entries(a).filter(([h])=>!nr.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),Et={main:"main",[Q]:"prototypes"},P=o(a=>!Object.hasOwn(Et,a),"isCard"),ne=o((a,d)=>j.join(a,re(d)),"scriptAt"),yt=o(a=>O.existsSync(a),"exists"),oe=o((a,d)=>j.join(a.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function jt(a,d,h){const c=oe(a,d);return O.mkdirSync(j.dirname(c),{recursive:!0}),O.writeFileSync(c,h.message+`
2
+ `,{mode:384}),O.chmodSync(c,384),c}o(jt,"failureLog");const ae=o(a=>new Promise(d=>{const h=Ft.get({host:"127.0.0.1",port:a,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ce=o((a,d)=>{try{process.kill(-a,d)}catch{}},"signalGroup"),le=o(a=>{try{return process.kill(-a,0),!0}catch{return!1}},"groupUp"),Dt=o(a=>new Promise(d=>{const h=o(()=>{clearTimeout(c),clearInterval(w),d()},"finish");ce(a,"SIGTERM");let c=setTimeout(()=>{ce(a,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const w=setInterval(()=>{le(a)||h()},20)}),"endGroup");async function ar(a){const{stdout:d}=await ct("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
3
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===a).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await de(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}o(ar,"groupMembers");async function de(a,d){let h="";try{h=(await ct("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(f){h=f.stdout||""}const c=new Map(a.map(f=>[f.pid,f]));let w;for(const f of h.split(`
4
+ `))f[0]==="p"?w=c.get(f.slice(1)):f[0]==="n"&&w&&(w.cwd=f.slice(1));for(const f of a){if(!f.cwd&&O.existsSync("/proc"))try{f.cwd=O.readlinkSync(`/proc/${f.pid}/cwd`)}catch{}f.cwd=f.cwd.replace(/ \(deleted\)$/,"")}}o(de,"readCwds");async function cr(a){const{stdout:d}=await ct("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
5
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await de(h,["-d","cwd","-Fpn"]),h.filter(c=>ue(c,a))}o(cr,"processesIn");const lr=o((a,d)=>a===d||a.startsWith(d+j.sep),"insideDir"),ue=o((a,d)=>lr(a.cwd,d)||a.command.includes(d+j.sep)||a.command.endsWith(d),"inWorktree");async function fe(a,d,h){const c=await Le(a.dir,d),[w,f]=d.split("/"),x=a.files.find($=>$.slug===w)?.itemFile,g=c?.path||x?.items.find($=>$.id===f)?.worktree;if(typeof g!="string"||!g||g.split(j.sep).includes(".."))return"";let S=c?.repo;const p=(a.github||x?.repo||"").toLowerCase();if(!S&&p){const $=[a.owner?.repo,a.dir,...Object.values(h?.sites||{}).map(H=>H.owner?.repo)];for(const H of new Set($.filter(D=>typeof D=="string"&&j.isAbsolute(D))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(H,"remote","get-url","origin"))?.[1].toLowerCase()===p){S=await M(H,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!S||c?.path&&!j.isAbsolute(g))return"";try{const $=[j.join(O.realpathSync(qt.homedir()),".claude","kickflip","worktrees")];if(typeof S=="string"&&j.isAbsolute(S))try{$.push(j.join(O.realpathSync(S),".claude","worktrees"))}catch{}const H=j.resolve(S||"/",g),D=O.realpathSync(H);return $.some(C=>H.startsWith(C+j.sep)&&D.startsWith(C+j.sep))&&O.statSync(D).isDirectory()?D:""}catch{return""}}o(fe,"installWorktree");function dr({now:a=Date.now,answers:d=ae,lingers:h=ae,log:c=console.log,waiting:w=o(async()=>null,"waiting"),event:f=Fe,gate:x=Xe,busy:g=o(()=>Ye().state==="busy","busy")}={}){const S=new Map,p=new Map,$=new Map,H=new Set,D=o(()=>[...p.values(),...$.values()],"live"),C=new Set,lt=new Set,dt=new Map,ut=new Map,gt=new Map,et=new Map,ft=new Map;let W=!1,U=null,rt=null,$t=null;const vt=o(()=>new Date(a()).toISOString(),"stamp");function B(t){if(S.has(t.dir))return S.get(t.dir);const e=j.join(t.dir,"data","try.json"),s=ot(e,{kind:null,main:null,cards:{}}),n={...Array.isArray(s.groups)&&s.groups.length?{groups:s.groups}:{},...Ct(s.main_port)?{main_port:s.main_port}:{},kind:s.kind||null,main:s.main||null,cards:{...s.cards},...s.pull?{pull:{...s.pull,running:void 0}}:{}},r={state:n,file:e,written:JSON.stringify(s)};if(S.set(t.dir,r),n.main_port&&ut.set(t.dir,n.main_port),n.kind==="web")for(const l of Object.values(n.cards))l.state==="ready"&&(l.state="running",delete l.port,delete l.https);const i=Object.entries(n.cards).filter(([,l])=>l.state==="running");for(const[l]of i)lt.add(R(t,l));return n.kind==="web"?(n.cards=Object.fromEntries(Object.entries(n.cards).filter(([,l])=>l.state==="running"||l.killed)),n.main&&n.main.state!=="failed"&&(delete n.main.state,delete n.main.port,delete n.main.https)):n.main?.state==="running"&&(delete n.main.state,delete n.main.sha),n.main&&delete n.main.serving,s.prototypes?.state==="failed"&&(n.prototypes=s.prototypes),A(t),r}o(B,"stateOf");function A(t){const e=S.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Zt(e.file,e.state),e.written=s)}o(A,"persist");function pe(t){const{state:e}=B(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),A(t.board)}o(pe,"recordGroup");function he(t){const{state:e}=B(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(s=>s!==t.group),e.groups.length||delete e.groups,A(t.board))}o(he,"forgetGroup");async function me(t){const{state:e}=B(t),s=e.groups||[];delete e.groups,A(t);for(const n of s){if(!le(n.pgid))continue;let r=[];try{r=await ar(n.pgid)}catch(i){c(`try startup sweep: could not read group ${n.pgid}: ${i.message}`)}r.some(i=>ue(i,n.worktree))&&(await Dt(n.pgid),c(`stopped leftover try /${t.route} ${n.key} (pgid ${n.pgid})`),await Wt(t,n.key,n.port))}}o(me,"sweepGroups");async function Wt(t,e,s){if(!s||!await h(s))return;const n=`Port ${s} still answers after the run was stopped.`;P(e)&&await se(t.dir,e,"run-failed",{kind:"Serve",code:1,why:n}).catch(r=>c(`build /${t.route} ${e}: ${r.message}`)),c(`build /${t.route} ${e}: ${n}`)}o(Wt,"checkQuiet");const R=o((t,e)=>`${t.dir}\0${e}`,"runKey"),Ht=o((t,e)=>P(e)?t.cards[e]:t[Et[e]],"entryOf"),we=o((t,e,s)=>{P(e)?t.cards[e]=s:t[Et[e]]=s},"setEntry"),pt=o((t,e)=>{P(e)?delete t.cards[e]:t[Et[e]]=null},"dropEntry");function J(t,e,s){const{state:n}=B(t),r=Ht(n,e);if(!r)return;const i=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,P(e)&&i?{at:vt()}:{}),A(t)}o(J,"update");function Mt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(s=>s.site)];for(const s of e){if(!s?.dir)continue;const n=ot(j.join(s.dir,"data","try.json"),{}).main_port;Ct(n)&&ut.set(s.dir,Number(n))}}o(Mt,"reserveMains");function ye(t){const{board:e,key:s}=t,n=R(e,s),r=s==="main"?ut.get(e.dir):dt.get(n);if(r)return r;const i=new Set([...ut.values(),...dt.values()]);let l=tt.first;for(;i.has(l)&&l<=tt.last;)l++;if(l>tt.last)throw new Error(`No try ports available (${tt.first}\u2013${tt.last}). Stop a served card and retry.`);return s==="main"?(ut.set(e.dir,l),B(e).state.main_port=l,A(e)):dt.set(n,l),l}o(ye,"publicPort");const ht=o(async t=>{const e=He();if(!e)throw new Error("Tailscale isn't installed on this host");return ct(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Gt(t){try{await ht(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}o(Gt,"removePort");const Lt=o(t=>rr(ot(j.join(t.dir,"board.json"),{})),"sharedWith");async function Kt(t,e){const s=await x(t),n=t.headers["tailscale-user-login"];return s!=="other"||!n||!Lt(e).includes(n)?s:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}o(Kt,"relayVerdict");function ge(){for(const t of D()){if(!t.shared?.size)continue;const e=Lt(t.board);for(const s of t.shared)e.includes(s.login)||(t.shared.delete(s),s.res.destroy())}}o(ge,"endUnshared");async function $e(t){t.https=ye(t);const e=t.sockets=new Set,s=o(r=>{e.add(r),r.on("close",()=>e.delete(r))},"track"),n=t.shared=new Set;if(t.relay=Ft.createServer(async(r,i)=>{const l=await Kt(r,t.board);if(l!=="owner"&&l!=="shared"){tr(r,i,l);return}if(l==="shared"){const E={login:r.headers["tailscale-user-login"],res:i};n.add(E),i.on("close",()=>n.delete(E))}const m=Object.fromEntries(Object.entries(r.headers).filter(([E])=>!ie.includes(E))),k=Ft.request({host:"127.0.0.1",port:t.port,method:r.method,path:r.url,headers:m},E=>{i.writeHead(E.statusCode,t.key===Q?or(E.headers):E.headers),E.pipe(i),E.on("close",()=>{E.complete||i.destroy()})});i.on("close",()=>{i.writableFinished||k.destroy()}),k.on("error",()=>{i.headersSent?i.destroy():i.writeHead(502).end()}),r.pipe(k)}),t.relay.on("connection",s),t.relay.on("upgrade",async(r,i,l)=>{const m=await Kt(r,t.board);if(m!=="owner"){er(i,m==="shared"?"view-only":m);return}s(Ze({req:r,socket:i,head:l,port:t.port,path:r.url,replace:ie}))}),await new Promise((r,i)=>{t.relay.once("error",i),t.relay.listen(0,"127.0.0.1",r)}),!t.stopped){t.exposed=!0;try{await ht(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(r){throw new Error(`Couldn't open try port ${t.https}: ${(r.stderr||r.message).trim()}`)}}}o($e,"openPort");async function ve(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...p.values()].some(s=>s!==t&&s.exposed&&s.https===t.https);if(t.exposed&&!W&&!e&&await Gt(t.https),t.relay){for(const s of t.sockets)s.destroy();await new Promise(s=>t.relay.close(s))}t.keepPort||dt.delete(R(t.board,t.key))}o(ve,"closePort");async function be(){const t=D().filter(s=>s.ready&&s.relay&&!s.closing);if(!t.length)return;let e;try{e=JSON.parse((await ht(["serve","status","--json"])).stdout)}catch(s){c(`try ports check: ${(s.stderr||s.message).trim()}`);return}for(const s of t){if(s.stopped)continue;const n=`http://127.0.0.1:${s.relay.address().port}`,r=Object.entries(e?.Web||{}).find(([i])=>i.endsWith(`:${s.https}`))?.[1];if(r?.Handlers?.["/"]?.Proxy!==n){c(`try port ${s.https}: its serve mapping was ${r?"changed":"gone"}; serving it again`);try{await ht(["serve","--bg",`--https=${s.https}`,n])}catch(i){c(`try port ${s.https}: could not serve it again: ${(i.stderr||i.message).trim()}`)}}}}o(be,"reserve");function st(t=q()){if($t)return $t;Mt(t);for(const e of z(t))B(e);return $t=(async()=>{for(const e of z(t))await me(e).catch(s=>c(`try startup sweep /${e.route}: ${s.message}`));try{const{stdout:e}=await ht(["serve","status","--json"]),s=JSON.parse(e);for(const n of Object.keys(s?.TCP||{}))Ct(n)&&![...p.values()].some(r=>r.https===Number(n))&&await Gt(n)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),$t}o(st,"initialize");async function bt(t,e,s,n){const{board:r,key:i}=t;if(J(r,i,{state:"failed",port:void 0,https:void 0}),!P(i))return;let l="",m=!1;try{for await(const y of Xt({input:O.createReadStream(t.logFile),crlfDelay:1/0}))m||=sr.test(y),y.trim()&&(l=y)}catch(y){l=`could not read the build log: ${y.message}`}const k=t.port||(B(r).state.kind||(r.files.some(({itemFile:y})=>y.items.some(v=>v.install?.url))?"app":"web"))==="web"?"Serve":"Build",E=m?"unlock the login keychain on the host":n?.message||s&&`stopped by ${s}`||l;await se(r.dir,i,"run-failed",{kind:k,code:e??1,why:E})}o(bt,"failure");function kt(t){return t.ending||(t.ending=Dt(t.child.pid)),t.ending}o(kt,"terminate");async function mt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await kt(t),await t.done}o(mt,"halt");function ke(t){const e=(et.get(t)||[]).filter(s=>a()-s<Rt);return et.set(t,e),e.length<Tt}o(ke,"restartsLeft");function Ut(t,e){if(p.has(e)||B(t).state.kind==="app")return null;const s=Je(ot(j.join(t.dir,"board.json"),{}),t).max_serves;return[...p.values()].filter(r=>r.board.dir===t.dir&&P(r.key)&&!r.stopped&&!r.finished).length>=s?{ask:"cap",cap:s}:g()?{ask:"busy"}:null}o(Ut,"servesHeld");async function V(t,e=!1){await mt(t,e),J(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||gt.delete(R(t.board,t.key))}o(V,"stopRun");async function St(t,e,s,n={},r=""){if(!s)throw new T("This card has no valid worktree.");const i=ne(s,t.build_target),l=Ge(i);if(l)throw new T(l);if(!O.realpathSync(i).startsWith(O.realpathSync(s)+j.sep))throw new T("Build script must be inside the worktree.");const m=await M(s,"rev-parse","HEAD");if(W)throw new T("The hub is stopping. Try again after it restarts.",503);const{state:k}=B(t),E=Ht(k,e),y={...P(e)?E:{},...n,sha:m,script:!0,state:"running",...P(e)?{at:vt(),head:m}:{},...e===Q?{since:vt()}:{}};delete y.port,delete y.https,delete y.killed;const v=oe(t,e);O.mkdirSync(j.dirname(v),{recursive:!0});const _=O.openSync(v,"w",384);O.fchmodSync(_,384),r&&O.writeSync(_,r);const b=Pe(i,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});we(k,e,y);const F=R(t,e),u={child:b,board:t,key:e,worktree:s,sha:m,at:n.at,metadata:n,logFile:v,stopped:!1,port:null,ready:!1};pe(u),p.set(F,u),gt.set(F,m);let G,N;const Y=o(I=>{N||=new Error(I),kt(u)},"fail");b.on("error",I=>{G=I}),b.stdout.on("data",I=>O.writeSync(_,I)),b.stderr.on("data",I=>O.writeSync(_,I));const Bt=Xt({input:b.stdout,crlfDelay:1/0});return Bt.on("line",I=>{const K=/^port (\d{1,5})$/.exec(I);if(!K||u.port||u.stopped)return;const L=Number(K[1]);if(L<1||L>65535){Y("Build script printed an invalid port.");return}u.port=L,u.group.port=L,A(t),clearTimeout(u.deadline);let Z,it=!1;const nt=o(async()=>{if(!(it||u.stopped||u.ready||b.exitCode!==null||b.signalCode!==null)){it=!0;try{if(await d(L)){if(u.stopped||N||b.exitCode!==null||b.signalCode!==null||(u.opening=$e(u),await u.opening,u.stopped||N||b.exitCode!==null||b.signalCode!==null))return;u.ready=!0,k.kind="web",J(t,e,{state:"ready",port:L,https:u.https}),clearInterval(u.poll),e==="main"&&await Ot(t)}else a()>=Z&&Y("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{it=!1}}},"check"),xt=o(()=>{u.stopped||(Z=a()+6e4,u.poll=setInterval(()=>nt().catch(wt=>Y(wt.message)),250),nt().catch(wt=>Y(wt.message)))},"probeFrom");e==="main"&&$.get(t.dir)?.port===L?Ot(t).then(xt,wt=>Y(wt.message)):xt()}),u.deadline=setTimeout(()=>Y("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{kt(u)}),u.done=new Promise(I=>b.once("close",(K,L)=>{clearTimeout(u.deadline),clearInterval(u.poll),Bt.close(),O.closeSync(_),(async()=>{await kt(u);const Z=P(e)&&u.ready&&!u.stopped&&!W&&ke(F);if(Z&&(u.keepPort=!0),await ve(u),he(u),W||await Wt(t,e,u.port),u.finished=!0,u.stopped)return;if($.get(t.dir)===u){$.delete(t.dir),J(t,e,{serving:void 0});return}if(e==="main"&&!u.ready&&Se(t,u)&&!W){await Oe(u);return}if(Z){if(!yt(s)){pt(k,e),A(t);return}const nt=[...et.get(F)||[],a()];et.set(F,nt),c(`build /${t.route} ${e}: the serve exited on its own (${L||`code ${K}`}); serving it again (${nt.length} of ${Tt})`),await St(t,e,s,{},`The serve exited on its own; serving it again (${nt.length} of ${Tt} in ${Rt/6e4} minutes).
6
+ `).catch(xt=>bt({board:t,key:e,logFile:jt(t,e,xt)},1));return}if(P(e)&&u.ready&&!u.stopped&&!W){et.delete(F),await bt(u,K??1,L,new Error(`The serve exited on its own ${Tt+1} times within ${Rt/6e4} minutes.`));return}const it=e===Q&&!u.port?new Error('Build script exited without printing "port <n>".'):null;N||G||K!==0||u.port&&!u.ready||it?await bt(u,K??1,L,N||G||(u.port&&!u.ready?new Error("Build script exited before its port answered."):it)):u.ready?(e==="main"?(delete k.main.state,delete k.main.port,delete k.main.https):pt(k,e),A(t)):(k.kind="app",J(t,e,{state:"ready"}))})().catch(Z=>c(`build /${t.route} ${e}: ${Z.message}`)).finally(()=>{p.get(F)===u&&p.delete(F),I()})})),c(`build /${t.route} ${e}: started; log ${v}`),{status:"started"}}o(St,"launch");async function Ot(t){const e=$.get(t.dir);e&&($.delete(t.dir),J(t,"main",{serving:void 0}),await mt(e))}o(Ot,"retire");function Se(t,e){const s=$.get(t.dir);return!!s&&O.readFileSync(e.logFile,"utf8").includes(s.worktree)}o(Se,"refusedByOld");async function Oe(t){const{board:e,worktree:s}=t;await Ot(e),c(`try /${e.route} main: the new serve refused to run beside the old one; stopped the old serve and serving again in ${s}`);const{serving:n,...r}=t.metadata;try{await St(e,"main",s,r,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
7
+ `)}catch(i){Jt(e,r,i)}}o(Oe,"handOverFailed");function Jt(t,e,s){s.status!==503&&(B(t).state.main={...e,state:"failed"},jt(t,"main",s),A(t))}o(Jt,"mainFailed");async function xe(t,e,s){const n=`try /${t.route} main:`;if(!H.has(t.dir)){H.add(t.dir);try{const r=await cr(s);for(const i of new Set(r.map(l=>l.pgid))){await Dt(i);for(const l of r.filter(m=>m.pgid===i))c(`${n} stopped pid ${l.pid} (pgid ${i}) in the old main worktree ${s}`)}}catch(r){c(`${n} could not stop the processes in the old main worktree ${s}: ${r.message}`)}}if(yt(s))try{await M(e,"worktree","remove","--force",s);return}catch(r){try{O.rmSync(s,{recursive:!0,force:!0}),c(`${n} git could not remove the old main worktree ${s}; removed the folder: ${(r.stderr||r.message).trim().split(`
8
+ `).at(-1)}`),await M(e,"worktree","prune")}catch(i){c(`${n} could not remove the old main worktree ${s}: ${i.message}`)}}}o(xe,"removeLegacyMain");async function Yt(t,e,s){const n=R(t,"main");if(C.has(n))throw new T("A build is already running for main.",409);C.add(n);try{const r=p.get(n);r?.ready&&!r.ending?(p.delete(n),$.set(t.dir,r)):r&&await V(r,!0);const i=$.get(t.dir);i&&(s={...s,serving:{sha:i.sha,at:i.at,https:i.https}});const l=o(k=>j.join(qt.homedir(),".claude","kickflip","try",`${t.route}-main${k}`),"worktreeOf"),m=l(i?.worktree===l("-a")?"-b":"-a");return O.mkdirSync(j.dirname(m),{recursive:!0}),await xe(t,e,l("")),yt(m)?await M(m,"checkout","--detach","--force",s.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",m,s.sha)),await St(t,"main",m,s)}catch(r){throw Jt(t,s,r),r}finally{C.delete(n)}}o(Yt,"launchMain");async function It(t,{manifest:e=q(),auto:s=!1}={}){if(await st(e),Mt(e),W)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(v=>!["route","key","anyway"].includes(v))||typeof t.route!="string"||typeof t.key!="string"||!["undefined","boolean"].includes(typeof t.anyway))throw new T("Send only the board route, card key and anyway.");const{route:n,key:r}=t;if(P(r)&&(!/^[\w.-]+\/[\w.-]+$/.test(r)||r.split("/").some(v=>v==="."||v==="..")))throw new T("Card key must be <file>/<id>.");const i=z(e).find(v=>v.route===n);if(!i)throw new T(`/${n} is not a board.`,404);if(P(r)&&!i.files.some(({slug:v,itemFile:_})=>_.items.some(b=>`${v}/${b.id}`===r)))throw new T("There is no item with this card key.",404);const l=await w();if(l)throw new T(l,409);const m=R(i,r),k=p.get(m);if(C.has(m)||k&&!k.ready&&!k.finished)throw new T(`A build is already running for ${r==="main"?"main":r===Q?"the prototype site":"this card"}.`,409);if(r==="main"){const v=B(i).state.main;if(v?.state==="running")throw new T("A build is already running for main.",409);if(v?.state!=="failed"||!v.sha)throw new T("Main has no failed build to retry.",409);const{sha:_,branch:b,slug:F,at:u,script:G}=v;return Yt(i,at(n,e).repo,{sha:_,branch:b,slug:F,at:u,script:G,...i.build_target?{target:i.build_target}:{}})}const E=P(r)&&!s&&!lt.has(m)?Ut(i,m):null;if(E?.ask==="cap")throw new T(`This board is at its cap of ${E.cap} parallel serves. Kill another serve, or raise Maximum parallel serves in Board settings.`,409,"cap");if(E?.ask==="busy"&&!t.anyway)throw new T(ir,409,"busy");C.add(m);const y=lt.delete(m);try{p.has(m)&&await V(p.get(m),!0);const v=r===Q?zt(i):await fe(i,r,e);return await St(i,r,v)}catch(v){throw dt.delete(m),y&&v.status!==503&&J(i,r,{state:void 0,sha:void 0}),v}finally{C.delete(m)}}o(It,"start");async function Te(t,{manifest:e=q()}={}){if(await st(e),W)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(i=>!["route","key"].includes(i))||typeof t.route!="string"||typeof t.key!="string"||!P(t.key))throw new T("Send only the board route and card key.");const s=z(e).find(i=>i.route===t.route);if(!s)throw new T(`/${t.route} is not a board.`,404);const n=R(s,t.key),r=p.get(n);if(!r||r.stopped||r.finished)throw new T("This card has no serve running.",409);return await V(r),et.delete(n),gt.set(n,r.sha),J(s,t.key,{sha:r.sha,killed:!0}),c(`build /${s.route} ${t.key}: killed`),{status:"stopped"}}o(Te,"kill");const zt=o(t=>{const e=ze(t.route);return yt(e)&&O.statSync(e).isDirectory()?O.realpathSync(e):""},"baseWorktree");async function Ee(t,e){const{state:s}=B(t),n=R(t,Q),r=p.get(n),i=(await Ke(t.dir)).length>0&&zt(t);if(!i){r&&await V(r),s.prototypes&&(s.prototypes=null,A(t));return}if(r?.ready&&!r.stopped&&!C.has(n)){const l=await M(i,"rev-parse","HEAD");if(l===s.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(r||C.has(n)||s.prototypes?.state==="failed"||await w())return;try{await It({route:t.route,key:Q},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw s.prototypes={state:"failed"},jt(t,Q,l),A(t),l}}o(Ee,"serveBase");function Qt(t,e,{manifest:s=q()}={}){const n=z(s).find(i=>i.route===t);if(!n)return null;const r=[...p.values()].find(i=>i.board.dir===n.dir&&i.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&i.ready&&!i.stopped&&i.child.exitCode===null&&i.child.signalCode===null);return r?{key:r.key,port:r.port}:null}o(Qt,"lookup");async function Pt({manifest:t=q(),routes:e}={}){if(await st(t),Mt(t),W)return U;if(U&&e){if(!rt){const s={routes:new Set,manifest:t};s.done=U.catch(()=>{}).then(()=>(rt=null,Pt({manifest:s.manifest,routes:[...s.routes]}))),rt=s}rt.manifest=t;for(const s of e)rt.routes.add(s);return rt.done}return U||(U=(async()=>{await be();const s=z(t),n=ot(Ne,{}).prs||{};for(const[r,i]of[...$])s.some(l=>l.dir===r)||($.delete(r),await mt(i));for(const r of[...p.values()])if(!s.some(i=>i.dir===r.board.dir)){await V(r);const{state:i}=B(r.board);pt(i,r.key),A(r.board)}for(const r of s){if(W)return;if(e&&!e.includes(r.route))continue;const{state:i}=B(r),l=at(r.route,t),m=l.branch||"main",k=Ce(l),E=new Set;for(const{slug:y,itemFile:v}of r.files)for(const _ of v.items){const b=`${y}/${_.id}`,F=Qe(_,r.github||v.repo).map(u=>n[qe(u)]);if(!(_.lane==="done"||F.length&&F.every(u=>["MERGED","CLOSED"].includes(u?.state))))try{const u=R(r,b),G=await fe(r,b,t);if(G){E.add(b);const I=await M(G,"rev-parse","HEAD");let K=0;try{K=Number(await M(G,"rev-list","--count",`${k}..HEAD`))}catch{}Object.assign(i.cards[b]||={},{head:I,ahead:K,script:yt(ne(G,r.build_target))}),A(r)}else continue;const N=i.cards[b],Y=lt.has(u);if((Y||r.auto_build&&N.script&&N.ahead>0&&(N.sha||gt.get(u))!==N.head&&N.state!=="running")&&!C.has(u)&&(Y||!Ut(r,u)))try{await It({route:r.route,key:b},{manifest:t,auto:!0})}catch(I){if([409,503].includes(I.status))continue;throw J(r,b,{sha:N.head}),await bt({board:r,key:b,logFile:jt(r,b,I)},1),I}}catch(u){c(`try /${r.route} ${b}: ${u.message}`)}}for(const y of[...p.values()])y.board.dir!==r.dir||!P(y.key)||E.has(y.key)||C.has(R(r,y.key))||(await V(y),pt(i,y.key));for(const y of Object.keys(i.cards))!E.has(y)&&lt.delete(R(r,y))&&pt(i,y);A(r),await Ee(r,t).catch(y=>c(`try /${r.route} prototypes: ${y.message}`));try{if(!l.repo||await w())continue;const y=await M(l.repo,"rev-parse",k);let v=!1;try{await M(l.repo,"cat-file","-e",`${y}:${re(r.build_target)}`),v=!0}catch{}if(!v){const N=p.get(R(r,"main"));N&&await V(N),await Ot(r),i.main=null,A(r);continue}const _=i.main;if(_?.state==="running"||_?.sha===y&&(_.target||null)===(r.build_target||null)&&(_.state==="failed"||_.state==="ready"&&(i.kind!=="web"||Qt(r.route,"main",{manifest:t}))))continue;const b=R(r,"main");if(C.has(b))continue;const F=await M(l.repo,"log","-1","--format=%cI",y);let u=r.github;if(!u)try{u=ee(await M(l.repo,"remote","get-url","origin"))}catch{}const G={sha:y,branch:m,slug:u||"",at:F,script:v,...r.build_target?{target:r.build_target}:{}};await Yt(r,l.repo,G)}catch(y){c(`try /${r.route} main: ${y.message}`)}}})().finally(()=>{U=null}),U)}o(Pt,"tick");function Vt(t,e){if(ft.has(t.dir))return ft.get(t.dir);const s=(async()=>{const n=at(t.route,e),r=n.branch||"main",{state:i}=B(t);i.pull={...i.pull,running:!0},A(t);let l,m;try{l=await(n.local?fr:ur)(n.repo,r)}catch(k){m=k.message}if(l===null?delete i.pull:i.pull={at:vt(),...m?{error:m}:{}},A(t),l===null)return null;if(m)return c(`pull /${t.route}: ${m}`),{commits:0,error:m};if(l.commits){c(`pull /${t.route}: ${r} ${n.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await f(t,"main-pulled",{branch:r,...l})}catch(k){c(`pull /${t.route}: could not send main-pulled (${k.message})`)}}return{commits:l.commits}})().finally(()=>ft.delete(t.dir));return ft.set(t.dir,s),s}o(Vt,"pullMain");async function je({manifest:t=q(),routes:e}={}){await st(t);const s=[];for(const n of z(t)){if(W)break;if(!(e&&!e.includes(n.route)))try{if(!at(n.route,t).repo)continue;(await Vt(n,t))?.commits&&s.push(n.route)}catch(r){c(`pull /${n.route} failed: ${r.message}`)}}return s}o(je,"pullMains");async function _e(t,{manifest:e=q()}={}){if(await st(e),W)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(i=>i!=="route")||typeof t.route!="string")throw new T("Send only the board route.");const s=z(e).find(i=>i.route===t.route);if(!s)throw new T(`/${t.route} is not a board.`,404);const n=at(s.route,e),r=await Vt(s,e);if(!r)throw new T("This board\u2019s folder has no origin to pull from.",409);return(r.commits||!n.local)&&Pt({manifest:e,routes:[s.route]}).catch(i=>c(`try /${s.route} main: ${i.message}`)),r}o(_e,"pull");async function Ae(){W=!0;const t=Promise.all([...p.values()].map(e=>e.ready&&!P(e.key)?V(e):mt(e)).concat([...$.values()].map(e=>mt(e))));await Promise.all([t,U,...ft.values()])}return o(Ae,"stop"),{start:It,kill:Te,tick:Pt,stop:Ae,lookup:Qt,initialize:st,pull:_e,pullMains:je,endUnshared:ge}}o(dr,"createTryManager");const _t=1e4,At=o(async(a,...d)=>(await ct("git",["-C",a,...d],{encoding:"utf8",timeout:_t,env:De()})).stdout,"originGit");async function ur(a,d,{run:h=At}={}){const c=o(async(...p)=>(await h(...p)).trim(),"git"),w=o(p=>c(a,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),f=`refs/remotes/origin/${d}`,x=await w(`refs/heads/${d}`)||await w(f);try{await c(a,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${f}`),await c(a,"checkout","--quiet","--force","-B",d,f),await c(a,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${_t/1e3} s`:Nt(p.stderr,{repo:a}).error}`)}const g=await w(f),S=x&&x!==g?Number(await c(a,"rev-list","--count",`${x}..${g}`)):0;return{from:x||g,to:g,commits:S}}o(ur,"resetBranch");async function fr(a,d,{run:h=At}={}){const c=o(async(...f)=>(await h(...f)).trim(),"git"),w=o(f=>{throw new Error(`can\u2019t pull: ${f}`)},"refuse");try{await c(a,"config","--get","remote.origin.url")}catch{return null}try{await c(a,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(f){w(f.killed?`git fetch timed out after ${_t/1e3} s`:Nt(f.stderr,{repo:a}).error)}try{const f=await c(a,"rev-parse","--verify",`refs/heads/${d}^{commit}`),x=await c(a,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=o(($,H)=>c(a,"merge-base","--is-ancestor",$,H).then(()=>!0,D=>{if(D.code===1)return!1;throw D}),"isAncestor");if(f===x||await g(x,f))return{from:f,to:f,commits:0};await g(f,x)||w(`${d} has split from origin`);const S=Number(await c(a,"rev-list","--count",`${f}..${x}`)),p=(await c(a,"worktree","list","--porcelain")).split(`
8
9
 
9
- `).map(O=>O.split(`
10
- `)).find(O=>O.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&y("local changes"),await c(p,"merge","--ff-only","--quiet",S)):await c(o,"update-ref",`refs/heads/${d}`,S,u),{from:u,to:S,commits:m}}catch(u){if(u.message.startsWith("can\u2019t pull: "))throw u;y(String(u.stderr||u.message).trim().split(`
11
- `).at(-1))}}a(ir,"pullBranch");async function xr({manifest:o=Q(),log:d=console.log,routes:h}={}){const c=h?ft(Qt,{}):{};for(const y of J(o))if(!(h&&!h.includes(y.route)))try{const u=et(y.route,o);if(u.local||!u.repo)continue;const S=u.branch||"main",g=new Date().toISOString(),m=Date.now();try{await vt(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${S}:refs/remotes/origin/${S}`),c[y.route]={at:g,ok:!0}}catch(p){d(`fetch /${y.route} after ${Date.now()-m} ms: ${p.message}`);let O=y.github;if(!O&&!y.remote)try{O=Vt(await M(u.repo,"remote","get-url","origin"))}catch{}c[y.route]={at:g,ok:!1,...jt(p.killed?`git fetch timed out after ${$t/1e3} s`:p.stderr,{github:O||"",remote:y.remote,repo:u.repo})};continue}if(y.remote)try{await sr(y,u.repo,S,d)}catch(p){d(`pushed branches /${y.route}: ${p.message}`)}}catch(u){d(`fetch /${y.route} failed: ${u.message}`)}Yt(Qt,c)}a(xr,"fetchTryBranches");async function sr(o,d,h,c){const y=o.files.flatMap(({slug:g,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!We(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!y.length)return;const u=new Map((await vt(d,"ls-remote","--heads","origin")).split(`
12
- `).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),S=Ae(o.remote)?.host||"the remote";for(const g of y){let m=u.get(g.branch);if(!m)try{m=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(m)try{await M(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&u.has(g.branch)))try{await De(o.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:S}]:["branch-gone",{branch:g.branch,host:S}]})}catch(O){c(`pushed ${g.key}: ${O.message}`)}}}a(sr,"settlePushed");const q=er({waiting:a(()=>Pe.waiting("tailscale"),"waiting")}),Er=a((...o)=>q.start(...o),"startBuild"),Tr=a((...o)=>q.tick(...o),"tryTick"),jr=a((...o)=>q.pull(...o),"pullNow"),_r=a((...o)=>q.pullMains(...o),"pullMains"),Ir=a(()=>q.stop(),"stopTries"),Ar=a(()=>q.endUnshared(),"endUnsharedTries"),Mr=a((...o)=>q.initialize(...o),"initializeTries");export{A as BuildError,V as TRY_PORTS,er as createTryManager,Ar as endUnsharedTries,xr as fetchTryBranches,Mr as initializeTries,ae as installWorktree,_t as isTryPort,ir as pullBranch,_r as pullMains,jr as pullNow,rr as resetBranch,Er as startBuild,Ir as stopTries,Tr as tryTick};
10
+ `).map($=>$.split(`
11
+ `)).find($=>$.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&w("local changes"),await c(p,"merge","--ff-only","--quiet",x)):await c(a,"update-ref",`refs/heads/${d}`,x,f),{from:f,to:x,commits:S}}catch(f){if(f.message.startsWith("can\u2019t pull: "))throw f;w(String(f.stderr||f.message).trim().split(`
12
+ `).at(-1))}}o(fr,"pullBranch");async function Fr({manifest:a=q(),log:d=console.log,routes:h}={}){const c=h?ot(te,{}):{};for(const w of z(a))if(!(h&&!h.includes(w.route)))try{const f=at(w.route,a);if(f.local||!f.repo)continue;const x=f.branch||"main",g=new Date().toISOString(),S=Date.now();try{await At(f.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${x}:refs/remotes/origin/${x}`),c[w.route]={at:g,ok:!0}}catch(p){d(`fetch /${w.route} after ${Date.now()-S} ms: ${p.message}`);let $=w.github;if(!$&&!w.remote)try{$=ee(await M(f.repo,"remote","get-url","origin"))}catch{}c[w.route]={at:g,ok:!1,...Nt(p.killed?`git fetch timed out after ${_t/1e3} s`:p.stderr,{github:$||"",remote:w.remote,repo:f.repo})};continue}if(w.remote)try{await pr(w,f.repo,x,d)}catch(p){d(`pushed branches /${w.route}: ${p.message}`)}}catch(f){d(`fetch /${w.route} failed: ${f.message}`)}Zt(te,c)}o(Fr,"fetchTryBranches");async function pr(a,d,h,c){const w=a.files.flatMap(({slug:g,itemFile:S})=>S.items.filter(p=>p.pushed?.branch&&!Ve(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!w.length)return;const f=new Map((await At(d,"ls-remote","--heads","origin")).split(`
13
+ `).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),x=Re(a.remote)?.host||"the remote";for(const g of w){let S=f.get(g.branch);if(!S)try{S=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(S)try{await M(d,"merge-base","--is-ancestor",S,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&f.has(g.branch)))try{await Ue(a.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:x}]:["branch-gone",{branch:g.branch,host:x}]})}catch($){c(`pushed ${g.key}: ${$.message}`)}}}o(pr,"settlePushed");const X=dr({waiting:o(()=>We.waiting("tailscale"),"waiting")}),Nr=o((...a)=>X.start(...a),"startBuild"),Cr=o((...a)=>X.kill(...a),"killBuild"),Rr=o((...a)=>X.tick(...a),"tryTick"),Dr=o((...a)=>X.pull(...a),"pullNow"),Wr=o((...a)=>X.pullMains(...a),"pullMains"),Hr=o(()=>X.stop(),"stopTries"),Gr=o(()=>X.endUnshared(),"endUnsharedTries"),Lr=o((...a)=>X.initialize(...a),"initializeTries");export{ir as BUSY_ASK,T as BuildError,tt as TRY_PORTS,dr as createTryManager,Gr as endUnsharedTries,Fr as fetchTryBranches,Lr as initializeTries,fe as installWorktree,Ct as isTryPort,Cr as killBuild,fr as pullBranch,Wr as pullMains,Dr as pullNow,ur as resetBranch,Nr as startBuild,Hr as stopTries,Rr as tryTick};
package/hub/upgrade.mjs CHANGED
@@ -1 +1 @@
1
- var y=Object.defineProperty;var e=(n,a)=>y(n,"name",{value:a,configurable:!0});import p from"node:fs";import u from"node:path";const S="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",v=60*6e4,m=e(n=>String(n).split("-")[0].split(".").map(Number),"parts");function g(n,a){const[o,i]=[m(n),m(a)];for(let r=0;r<3;r++)if((o[r]||0)!==(i[r]||0))return(o[r]||0)>(i[r]||0);return!1}e(g,"newer");function _({root:n,fetch:a=globalThis.fetch,run:o,log:i=e(()=>{},"log"),pulled:r=e(()=>{},"pulled")}){const d=p.existsSync(u.join(n,".git")),l=e(()=>JSON.parse(p.readFileSync(u.join(n,"package.json"),"utf8")).version,"installed"),k=l();let c=!1;async function h(){if(d)return{state:"clone"};let s;try{const t=await a(S,{signal:AbortSignal.timeout(15e3)});if(!t.ok)throw new Error(`npm answered ${t.status}`);s=(await t.json()).version}catch(t){return i(`kickflip update check failed: ${t.message}`),{state:"failed",error:t.message}}return g(s,l())?(w(s),{state:"pulling",latest:s}):{state:"same",latest:s}}e(h,"check");function w(s){c||(c=!0,o().then(({ok:t})=>{if(!t)return;const f=l();g(f,k)?r():i(`update left v${f}; npm has ${s}`)},t=>i(`kickflip update failed: ${t.message}`)).finally(()=>{c=!1}))}return e(w,"pull"),{check:h}}e(_,"upgrader");export{v as CHECK_MS,S as LATEST_URL,g as newer,_ as upgrader};
1
+ var L=Object.defineProperty;var e=(i,s)=>L(i,"name",{value:s,configurable:!0});import w from"node:fs";import y from"node:path";const v="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",E=15*6e4,g=15*6e4,M=10*6e4,C=60*6e4,A=e(i=>String(i).split("-")[0].split(".").map(Number),"parts");function h(i,s){const[u,r]=[A(i),A(s)];for(let l=0;l<3;l++)if((u[l]||0)!==(r[l]||0))return(u[l]||0)>(r[l]||0);return!1}e(h,"newer");function D({root:i,fetch:s=globalThis.fetch,run:u,log:r=e(()=>{},"log"),pulled:l=e(()=>{},"pulled"),now:f=Date.now,awayFor:_=e(()=>g,"awayFor")}){const T=w.existsSync(y.join(i,".git")),a=e(()=>JSON.parse(w.readFileSync(y.join(i,"package.json"),"utf8")).version,"installed"),b=a();let p=!1,k=null,o=null,m={line:null,at:0};function j(){if(k===null||f()-k>=E)return S({hold:!0});if(o&&h(o,a())&&_()>=g)return x(o)}e(j,"tick");async function S({hold:d=!1}={}){if(T)return{state:"clone"};k=f();let t;try{const n=await s(v,{signal:AbortSignal.timeout(15e3)});if(!n.ok)throw new Error(`npm answered ${n.status}`);t=(await n.json()).version}catch(n){return r(`kickflip update check failed: ${n.message}`),{state:"failed",error:n.message}}const c=`kickflip update check: npm ${t}, running ${a()}`;return(c!==m.line||f()-m.at>=C)&&(r(c),m={line:c,at:f()}),h(t,a())?d&&_()<g?(o=t,{state:"waiting",latest:t}):(x(t),{state:"pulling",latest:t}):(o=null,{state:"same",latest:t})}e(S,"check");function x(d){if(p)return;p=!0;const t=new AbortController,c=setTimeout(()=>{r("kickflip update killed after 10 minutes"),t.abort()},M).unref();u({signal:t.signal}).then(({ok:n})=>{if(!n)return;const $=a();h($,b)?l():r(`kickflip update left v${$}; npm has ${d}`)},n=>r(`kickflip update failed: ${n.message}`)).finally(()=>{clearTimeout(c),p=!1})}return e(x,"pull"),{check:S,tick:j}}e(D,"upgrader");export{g as AWAY_MS,E as CHECK_MS,v as LATEST_URL,M as PULL_MS,h as newer,D as upgrader};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {