@lovek4d/kickflip 0.6.1 → 0.7.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/CONTEXT.md +60 -48
- package/SKILL.md +20 -14
- package/bin/event-lines.mjs +1 -0
- package/bin/kickflip.mjs +8 -8
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +118 -104
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -9
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +2 -2
- package/board/prototype-page.mjs +1 -1
- package/board/public/app-arch.js +3 -3
- package/board/public/app-banner.js +1 -1
- package/board/public/app-cards.js +17 -9
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -61
- package/board/public/app-needs.js +116 -30
- package/board/public/app-reader.js +54 -44
- package/board/public/app-threads.js +2 -2
- package/board/public/app.html +1 -1
- package/board/public/app.js +23 -13
- package/board/public/dialogs.js +1 -1
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +2 -0
- package/board/public/linear-key.js +1 -1
- package/board/public/new-task.js +1 -1
- package/board/public/progress-review.js +4 -4
- package/board/public/prototype-kit.js +27 -16
- package/board/public/prototype-switcher.js +47 -32
- package/board/public/prototype.html +7 -4
- package/board/public/router.js +35 -7
- package/board/public/shell.js +19 -13
- package/board/public/sidebar.css +4 -4
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +74 -36
- package/board/public/ui-cards.js +56 -37
- package/board/public/ui-enums.js +10 -1
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +6 -6
- package/board/public/ui-order.js +8 -6
- package/board/public/ui-progress-bar.js +6 -7
- package/board/public/ui-questions.js +30 -13
- package/board/public/ui-sidebar.js +3 -2
- package/board/public/ui-threads.js +20 -19
- package/board/public/ui.js +15 -4
- package/board/public/view-hub-settings.js +28 -31
- package/board/public/view-hub.js +7 -9
- package/board/public/view-notes.js +2 -2
- package/board/public/view-questions.js +35 -6
- package/board/public/view-settings.js +63 -31
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -20
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -25
- package/board/tickets.mjs +1 -1
- package/board/transcript.mjs +4 -2
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/tailscale.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Bt=Object.defineProperty;var u=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ft from"node:net";import{spawn as Be,execFileSync as Nt}from"node:child_process";import b from"node:fs";import k from"node:path";import Fe from"node:os";import{fileURLToPath as Gt}from"node:url";import Ne 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 Ge,queueStatus as ze,saveSettings as Qt,readSettings as q,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,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 Ke,answerOf as an,expired as rn,pendingFor as We,promptRecord as on,pushOf as cn,updatePrompts as N,shown as ln,pruneRemembered as ye,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}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 Ze,awakeUnsupported as vn}from"./awake.mjs";import{endOf as Tn}from"./duration.mjs";import{checkGitHub as On,sendWakes as jn}from"./github.mjs";import{checkIncoming as Pn}from"./incoming.mjs";import{listBoards as D,isListening as Rn,isBoardSite as Se,listenState as Qe,RESUME_STATE as En,sendToBoards as Xe,checkListening as Dn,recordHubCode as Ln,hubCode as Cn,runningHubCode as An,boardCode as Ye,recordLinkFaults as In,linksOf as Mn,hubLink as Hn,hubCodeState as Un,readJSON as qe}from"./boards.mjs";import{liveSessions as et}from"./live.mjs";import{RuleError as Bn,applyRules as Fn,changed as tt,readHubRules as ke,writeHubRules as Nn}from"./rules.mjs";import{siteIcon as Gn}from"./icons.mjs";import{readNeeds as zn,sidebarSites as Jn,startRoute as Kn,hubHealth as Wn}from"./home.mjs";import{checkSessions as Vn,compactFullContexts as Zn,startingBoards as Qn}from"./sessions.mjs";import{repoService as Xn,repoLookupRoute as Yn}from"./repos.mjs";import{boardRepo as nt,changeBoardRepo as qn,migrateBoardRepos as es,migrateHubBoard as ts}from"./board-repo.mjs";import{requestBoard as ns,retryBoard as ss,dismissBoard as as,clonePercent as rs,settleStarts as os}from"./new-board.mjs";import{boardClosure as is,closeBoard as cs,closedBoards as ls,reopenBoard as us}from"./close-board.mjs";import{restartBoard as ds,restartable as hs}from"./restart-board.mjs";import{crossSite as fs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ps,colorsVersion as st,destOf as ms,fileVersion as gs,pageOf as at,shellHtml as ws}from"./shell.mjs";import{hubStream as bs}from"./stream.mjs";import{usageReader as ys,checkUsagePause as Ss,contextReading as ks}from"./usage.mjs";import{readUsageGate as ee,setPauseAt as $s,setKeepGoing as _s,setHubPause as xs,boardPausedUntil as vs}from"../shared/usage-gate.mjs";import{checkMachine as Ts}from"../shared/machine-gate.mjs";import{readiness as Os,readCodex as te}from"./readiness.mjs";import{BuildError as js,startBuild as Ps,stopTries as Rs,endUnsharedTries as Es,tryTick as xe,initializeTries as Ds,fetchTryBranches as rt,pullNow as Ls,pullMains as Cs}from"./try.mjs";import{serveFiles as As}from"./files.mjs";import{upgrader as Is,CHECK_MS as Ms}from"./upgrade.mjs";import{restarter as Hs}from"./self-restart.mjs";import{fallBackFromLinear as Us,migrateWaveNames as Bs,openBoardBuilders as Fs}from"../board/store.mjs";import{recordRefusal as Ns}from"../board/permissions.mjs";import{hubGate as ve}from"../board/on-host.mjs";import{pipeUpgrade as Gs,refuse as zs,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as ot,shareReach as Ws,routeOfPath as Vs,sharedSidebar as Zs,shareTracker as Qs}from"./share.mjs";import{restartRunner as it}from"../board/worktree.mjs";import{hubService as Xs}from"../shared/service.mjs";import{cloneRoot as Te}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ne,removeLinearKey as Ys,saveLinearKey as qs,savedClient as ct}from"./linear.mjs";import{SlackError as ea,slackStatus as ta,removeSlackToken as na,saveSlackToken as sa}from"./slack.mjs";import{sendReviews as aa}from"./review-send.mjs";const G=k.dirname(Gt(import.meta.url)),ra=k.join(G,"..","shared","colors.css"),lt=JSON.parse(b.readFileSync(k.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=k.join(Fe.homedir(),".claude","kickflip"),ut=k.join(z,"manifest.json"),se=k.join(z,"logs"),dt=k.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,se,dt])b.mkdirSync(e,{recursive:!0});try{const e=Ln();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 es();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 ts();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 Bs();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)}const M=Xn(),oa=ys(),ae=_n({log:c});ae.apply();let re={version:1,sites:{}};function g(){try{re=JSON.parse(b.readFileSync(ut,"utf8")),re.sites||={}}catch(e){e.code==="ENOENT"?re={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return re}u(g,"loadManifest");const j=new Map,H=new Set;let oe=!1;const ht=5e3,ie=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"),U=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function ce(e){return k.join(dt,e+".pid")}u(ce,"pidFile");function ia(e,t){try{const n=Number(b.readFileSync(ce(e),"utf8")),s=n?Nt("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(ce(e))}catch{}}u(ia,"killStale");function ca(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(se,e+".log"),"a"),[i,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=Se(t)?
|
|
5
|
-
`),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(la,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ua=3e4;async function da(e){const t=Date.now()+ua;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(da,"serverAnswers");async function gt(){if(ie)return;wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...J.keys()])U(R(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ca(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Ns(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&la(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=Bs({root:ve(),files:In().files,restart:l(()=>ct("hub"),"restart"),log:c}),ha=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Yt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const fa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=an(),yt=l(()=>y.startedAt?wn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(St,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Pn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Is({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ra({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Cn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Hn(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=pn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let kt=null;function pa(){kt||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{kt=null})}l(pa,"runWakes");const Ce=new Set,A={running:null};function $t(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Qn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Xn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l($t,"runSessionCheck"),await Cs(g());const ma=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=on(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(_t,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function xt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Gs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(xt,"noteRefusal");let Q=null,Tt=null;async function he(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,he())):(Q=(async()=>$s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{js()}catch(e){c("machine check failed:",e.message)}F.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=yt().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),cs({listening:l(e=>Dn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),re.apply(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function vt(){if(ie)return;ie=!0,clearInterval(ha),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ds().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,jt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(jt,"newestMtime");function ga(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=jt(t.dir);return Ot.set(e,{at:s,checked:Date.now()}),s}l(ga,"changedAt");const Pt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Pt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ps(n),changedAt:ga(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),pe=new Map,Rt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ba,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Rt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Et,"waitingCounts");const Dt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Ln,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Rt)&&(p={at:Date.now(),listen:a,links:Un(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(ya,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Et(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=ya(a),u=Fn(),_=new Map(a.map(o=>[o.route,_s(o.owner?.session)]).filter(([,o])=>o)),k=Yn(i,t.getTime(),is,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>Os(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Kn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=fn(t,O);return{data:{code:u.running??null,version:ut,total:Dt(s),start:Vn(w),sites:w,hub:Zn(w,p,Bn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ia({codexShare:We(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Lt(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Lt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return ys({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(Sa,"shellPage");const Ct=Hs({root:ve(),run:ka,log:c,pulled:l(()=>wt.restartNow(),"pulled")});function ka(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
-
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${i}: ${
|
|
8
|
-
<title>${
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),xa=l(()=>({running:!!y.running,nextAt:yt(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<bn}),"pollState");async function ge(e){await _t();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...Sn({now:t,st:ze(t),poll:xa(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Ps.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(ne()),claudeModel:tn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(un),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:On()},n=te({now:e}),s=n.manual==="hub"?jn(n.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:se(),slack:na(),awake:t,pausedUntil:s}),closed:ds()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ta(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Mn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=_a(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await ls(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await hs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await us(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,K.has(r))return a(409,{error:"That board is already restarting."});K.add(r),await A.running;const o=await fs(r,{serverUp:da,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{K.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ys().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),St(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,$t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return es(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ts(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await as(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await rs(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await os(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await As(d,{manifest:g()})):a(202,await Es(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Rs)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(oa),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Ks(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=gs(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===bs(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Lt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Kt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Vt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Qt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await qt(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:_n}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:xn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vs(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{en(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{nn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?xs(r.get("value")):Ts(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(qs(),c("linear: key removed"),await bt()):c(`linear: key ${(await ea(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(sa(),c("slack: token removed")):c(`slack: token ${(await aa(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ta))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Xt(gn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=Gn(ke(),r.rules,r.base??[]),nt(o.changes)&&zn(o.rules)}catch(f){if(!(f instanceof Nn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,$n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=hn(f,r);if(q)return{answer:q};const be=cn(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await xt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),sn(ln(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await xt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ta,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Je,Je));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=rn(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),dn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(va,"promptRoute");const Oa=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(It,"downWhy");function Mt(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(ja(t,n))}l(Mt,"sendDown");function ja(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=It(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
4
|
+
`),n.code=Se(t)?Ye(t):null;let h;try{h=Be(i,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
|
|
5
|
+
`),b.closeSync(s),Object.assign(n,{child:null,key:ie(t),state:"crashed",exitCode:null,nextStart:Date.now()+ft(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ce(e),String(h.pid)),Object.assign(n,{child:h,key:ie(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const l=u((_,S)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(ce(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()+ft(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(ca,"start");const ft=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;ue(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&ue(n,"SIGKILL")},ht)}u(le,"stop");const ue=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(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let pt=!0;const B=new Set;let Ee=!1;const K=new Set,la=3e4;async function ua(e){const t=Date.now()+la;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ie(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}u(ua,"serverAnswers");async function mt(){if(oe)return;gt&&wt.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`),le(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),le(t),j.delete(t)));for(const t of[...J.keys()])U(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;pt&&ia(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ie(n)){c(`${t} entry changed; restarting it`),le(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=gt&&s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?Ye(n):null;if(i&&i!==s.code){if(!(await Fs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),le(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ca(t,n)}pt=!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(mt,"reconcile");const gt=process.env.KICKFLIP_CODE_RESTARTS!=="off",wt=Hs({root:Te(),files:Cn().files,restart:u(()=>it("hub"),"restart"),log:c}),da=setInterval(()=>mt().catch(e=>c("reconcile failed:",e.message)),2e3);mt().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 bt=u(async()=>{try{for(const e of await Us())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const ha=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ha,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},de=sn(),fa=u(()=>$.startedAt?wn({last:new Date($.startedAt),...de.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function yt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(yt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=q();let s;try{s=await On({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}$.remaining=s.rate?.remaining??null,s.merged.length&&(await rt({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await rt({manifest:g(),log:c});try{const i=await Cs({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(r=>c("pull refresh failed:",r.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await Pn({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.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(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:r}of s.linear.moved)c(`linear: ${i} \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 aa({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-$.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 Dn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{In(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let St=null;function pa(){St||=jn({settings:q(),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(()=>{St=null})}u(pa,"runWakes");const Ce=new Set,A={running:null};function kt(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Vn({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(()=>Zn({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(kt,"runSessionCheck"),await Ds(g());const ma=Date.now(),W=new Map;function V(e){e?(pe.delete(e),x.changed(e)):x.refresh()}u(V,"promptChanged");async function $t(){const{gone:e,rulesChanged:t}=await N(n=>{const s=rn(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:i}});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($t,"prunePrompts");const Ae=u(()=>({running:et(),boards:D(g())}),"promptContext");async function _t(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ns(nt(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(_t,"noteRefusal");let Z=null,xt=null;async function he(){return Z?xt??=Z.catch(()=>{}).then(()=>(xt=null,he())):(Z=(async()=>Ss({gate:ee(),resume:u(async e=>{const t=await Xe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Z=null}),Z)}u(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ts()}catch(e){c("machine check failed:",e.message)}de.check().then(yt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=fa().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),os({listening:u(e=>Rn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),ae.apply(),$t().catch(e=>c("prompt expiry failed:",e.message)).finally(kt)},15e3);function vt(){if(oe)return;oe=!0,clearInterval(da),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)ue(n,"SIGTERM");let e=!1;Rs().finally(()=>{e=!0});const t=Date.now()+ht;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),ue(n,"SIGKILL");process.exit(0)}},50)}u(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Tt=new Map;function Ot(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const r=k.join(e,i.name);if(i.isDirectory())n=Math.max(n,Ot(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Ot,"newestMtime");function ga(e,t){const n=Tt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Ot(t.dir);return Tt.set(e,{at:s,checked:Date.now()}),s}u(ga,"changedAt");const jt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),r=i==="files"?jt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:hs(n),changedAt:ga(t,n)||null,created:n.created||null,log:k.join(se,t+".log")}})}u(fe,"status");const F=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),pe=new Map,Pt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?zn(await t.json()):null}catch{return null}}u(ba,"fetchNeeds");async function Rt(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const r=pe.get(i.route);(t===!0||t?.has?.(i.route)||!r||Date.now()-r.at>=Pt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,r)=>[i.route,s[r]]))}u(Rt,"waitingCounts");const Et=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+We(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:et(),wakes:qe(En,{})};for(const i of e)try{const r=Qe(i,s);n.set(i.route,r);let p=Ie.get(i.route);(!p||p.listen!==r||Date.now()-p.at>=Pt)&&(p={at:Date.now(),listen:r,links:Mn(i,{listen:r})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(r){c(`links of ${i.route} failed:`,r.message),t.set(i.route,[])}return{links:t,listens:n}}u(ya,"boardLinks");async function Q({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Rt(n,{fresh:e}),i=g(),r=D(i),{links:p,listens:h}=ya(r),l=Un(),_=new Map(r.map(o=>[o.route,ks(o.owner?.session)]).filter(([,o])=>o)),S=Qn(i,t.getTime(),rs,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),y=new Set(S.map(o=>o.route)),m=new Map(r.filter(o=>vs(o.dir)).map(o=>[o.route,ee({dir:o.dir})])),w=Jn(n,s,new Set(r.map(o=>o.route)),h,_).filter(o=>!y.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=q(),C=hn(t,O);return{data:{code:l.running??null,version:lt,total:Et(s),start:Kn(w),sites:w,hub:Wn(w,p,Hn(l)),home:b.realpathSync(Fe.homedir()),starting:S,usage:(({start:o,...d})=>d)(oa({codexShare:Je(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(C,t))}:{state:"on",text:"On"},away:!de.state().present},links:p,rows:n,code:l}}u(Q,"sidebarState");function Dt(e,t,n,s,i){let r=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}u(Dt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await Q()).data):(await Q()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync(k.join(_e,i),"utf8"),s);return ws({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}u(Sa,"shellPage");const Lt=Is({root:Te(),run:ka,log:c,pulled:u(()=>wt.restartNow(),"pulled")});function ka(){const e=k.join(se,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
+
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[k.join(Te(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const r=b.readFileSync(e,"utf8").trim().split(`
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${i}: ${r}`),n(i===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(ka,"runUpdate"),Lt.check(),setInterval(()=>Lt.check(),Ms);const x=bs({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 Q({fresh:e})).data,"sidebar"),log:c});function me(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(me,"iconUrl");const $a=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function T(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(T,"readJSONBody");const Ct=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>${F(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${st()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function ge(e){await $t();const t=new Date,{data:n,links:s,rows:i,code:r}=await Q();return{...yn({now:t,st:ze(t),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Os.rows(),code:r,version:lt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(te()),claudeModel:en(),codex:te(),awake:Ze(),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:We(null).map(ln),remembered:Ve(null)}}u(ge,"hubData");const I=u(async()=>{const e=new Date,t={on:Ze(),unsupported:vn()},n=ee({now:e}),s=n.manual==="hub"?Tn(n.resets_at,e):null;return{...Sn({now:e,st:ze(e),rules:ke(),linear:ne(),slack:ta(),awake:t,pausedUntil:s}),closed:ls()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function xa(e,t,n,s=null){const i=u((a,o,d,f={})=>{t.writeHead(a,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),r=u((a,o)=>{const d=An();return i(a,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=u(({route:a,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=_a(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await is(_[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 o=await T(e);if(a=o.route,l==="/_hub/reopen-board"){const v=await us(a);return x.refresh(),r(200,v)}if(B.has(a))return r(409,{error:"That board is already closing."});B.add(a);const d=u(()=>B.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await cs(a,o.name);return x.refresh(),r(200,f)}catch(o){return r(o.status||400,{error:o.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,K.has(a))return r(409,{error:"That board is already restarting."});K.add(a),await A.running;const o=await ds(a,{serverUp:ua,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 ${o.session} resumed`),r(200,o)}catch(o){return a&&c(`restart /${a} failed: ${o.message}`),r(o.status||400,{error:o.message})}finally{K.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await T(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=it("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:Xs().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await T(e),yt(de.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),o=D(g()).find(d=>d.route===a);return o?(Ce.add(a),await A.running,kt(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Qe(o)})):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 M.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 M.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 Yn(n,{repos:M,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"?nt(S[1]):await qn(S[1],await T(e),{repos:M});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 i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await ns(a,{repos:M,changed:u(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await ss(y[1],{repos:M,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return i(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 as(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return i(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(oe)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"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.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 Ls(d,{manifest:g()})):r(202,await Ps(d,{manifest:g(),log:c}))}catch(f){if(f instanceof js)return r(f.status,{error:f.message});throw f}}if(l==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return i(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")===st()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(ra),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync(k.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync(k.join(_e,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await Q({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};we.add(a),e.on("close",()=>we.delete(a)),e.on("close",X.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(!Se(a))return r(404,{error:"That board is not open."});const o=Ks(qe(k.join(a.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await ve.node())?.dnsName||e.headers["x-forwarded-host"],v=X.people(O[1],d).map((L,Y)=>({...L,level:o[Y].level}));return r(200,{people:v,refused:X.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=ps(l.slice(10));if(!a){t.writeHead(404).end();return}const o=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===gs(a)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(a),{"cache-control":f}):Dt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await ge(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 T(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await T(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(o=>o.status>=200&&o.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),o=a.get("set"),d=o==="on"?{on:a.get("value")==="on"}:o==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let v;try{v=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.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(o){if(o.status!==409)throw o;return h?r(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message)}return ae.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{xs(a.get("pause"))}catch(o){if(o.status!==400)throw o;return h?r(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),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"),te({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(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(o){if(o.status!==400)throw o;return h?r(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(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")?$s(a.get("value")):_s(a.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(Ys(),c("linear: key removed"),await bt()):c(`linear: key ${(await qs(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.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")?(na(),c("slack: token removed")):c(`slack: token ${(await sa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ea))throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=ne();if(!a.set||a.refused)return r(200,a);const o=ct();try{const d=await o.teams();return r(200,{...ne(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ne(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=ct();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let o;try{o=await T(e,4*1024)}catch(f){return r(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return r(f.status,{error:f.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(o.hold=a.get("value")||"off");try{Qt(mn(q(),o))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):i(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,o;try{a=await T(e,64*1024),o=Fn(ke(),a.rules,a.base??[]),tt(o.changes)&&Nn(o.rules)}catch(f){if(!(f instanceof Bn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!tt(o.changes))return r(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Xe(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:o.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();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 T(e);return r(200,await Le({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;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:o,rec:d}=await N(f=>{const v=Ae(),L=v.boards.find(He=>He.owner?.session&&He.owner.session===a.session_id)||null;ye(f,v)&&x.refresh();const Y=dn(f,a);if(Y)return{answer:Y};const be=on(a,{board:L,live:v.running?.get(a.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?r(200,{state:"answered",answer:o}):(V(d.route),await _t(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(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(o){if(o.status)throw o;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 _t(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(xa,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=u(()=>N(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 p();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Ke,Ke));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 p();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await N(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,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return ye(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 p=await N(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return p&&(V(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(va,"promptRoute");const Ta=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)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(At,"downWhy");function It(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(Oa(t,n))}u(It,"sendDown");function Oa(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=At(e,t),p=r==="code"||r==="starting"?"wait":"down",h=U(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`${Ct(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
|
-
<h1>${
|
|
12
|
-
<p class="lede">${
|
|
11
|
+
<h1>${F(t.title||e)} isn't answering</h1>
|
|
12
|
+
<p class="lede">${F(l)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${Ta({icon:Gn(e,t),title:F(t.title||e),sub:`<span class="st ${p}">${F(i[r])}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}
|
|
16
|
+
</main></body></html>`}u(Oa,"downPage");const Me=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),X=Qs({file:k.join(z,"share-seen.json")}),we=new Set;function Mt(){const e=g().sites;for(const t of we){const n=ot(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Es()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Ht(e){const t=await ve(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),r=ot(n,g().sites),p=r.size?Ws({method:e.method,pathname:i,routes:r}):"other",h=Vs(i);if(p==="other")return(!r.size||h)&&X.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};X.visit(n,s,h);const l=await ve.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>Zs(_,r,l?.name||l?.login),"sidebar")}}}u(Ht,"access");const Ut=Ue.createServer((e,t)=>{Ht(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?ja(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):zs(e,t,n)})});function ja(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&fs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const i=new Set(Object.keys(g().sites));if(at({method:e.method,pathname:s.pathname,search:s.search,dest:ms(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);Sa(m,n).then(w=>Dt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Rt(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:Et(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/")){xa(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],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${Ct("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${F(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)||"/",_=at({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!jt(p.dir)){It(t,r,p);return}As(e,t,{dir:p.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=Ue.request({host:"127.0.0.1",port:p.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}It(t,r,p)}),e.pipe(y)}u(ja,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ht(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],r=P(g().sites,i);if(!r?.port){t.destroy();return}const p=`/${i}`;Gs({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${ut})`));
|
package/hub/incoming.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:
|
|
1
|
+
var I=Object.defineProperty;var u=(n,r)=>I(n,"name",{value:r,configurable:!0});import d from"node:fs";import w from"node:path";import{INCOMING_FILES as y,listBoards as N,readJSON as L,saveJSON as O}from"./boards.mjs";import{openPRs as R,prFiles as P,githubLogin as v}from"./pr-report.mjs";import{cardPRs as C,storedPRs as E}from"../shared/card-prs.mjs";const k=u((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(e=>e.pr));return n.filter(e=>!e.draft&&!k(e.author,i)&&!s.has(e.number)).map(e=>{const f=new Set(e.files),m=[...new Set(r.filter(o=>o.files.some(l=>f.has(l))).map(o=>o.key))],t=e.requested.some(o=>k(o,i));return m.length||t?{number:e.number,title:e.title,author:e.author,url:e.url,head:e.head,touches:m,asked:t}:null}).filter(Boolean)}u(J,"incomingFor");function M(n,r){const i=w.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;d.mkdirSync(w.dirname(i),{recursive:!0}),d.writeFileSync(s,JSON.stringify(r,null,2)+`
|
|
2
|
+
`),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:R,prFiles:P},login:i=v}){const s=L(y,{}),e=[],f=[];let m;for(const t of N(n).filter(o=>o.auto_review&&o.github))try{const o=t.githubLogin||(m??=await i()),l=await r.openPRs(t.github),$=new Map(l.map(h=>[h.number,h])),p=[];for(const{slug:h,itemFile:F}of t.files)for(const b of F.items.filter(a=>a&&(a.lane!=="done"||E(a).some(c=>c.merged_at))))for(const{number:a}of C(b,t.github).filter(c=>c.repo===t.github)){const c=`${t.github}#${a}`;let g=$.get(a)?.files||s[c];if(!g){const S=await r.prFiles(t.github,a);g=S.files,S.state==="MERGED"&&(s[c]=g)}p.push({key:`${h}/${b.id}`,pr:a,files:g})}M(t.dir,{at:new Date().toISOString(),repo:t.github,prs:J({open:l,cards:p,me:o})}),e.push(t.route)}catch(o){f.push(`${t.route}: ${o.message}`)}return O(y,s),{checked:e,failed:f}}u(q,"checkIncoming");export{q as checkIncoming,J as incomingFor};
|
package/hub/locks.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var f=Object.defineProperty;var
|
|
1
|
+
var f=Object.defineProperty;var a=(r,c)=>f(r,"name",{value:c,configurable:!0});import e from"node:fs";import i from"node:path";function d(r){let c;try{c=Number(e.readFileSync(i.join(r,"pid"),"utf8"))}catch{return!1}try{return process.kill(c,0),!1}catch(o){return o.code==="ESRCH"}}a(d,"holderGone");async function S(r,c,{staleAfter:o=3e4,timeout:s=1e4,heartbeat:l=!1}={}){const t=`${r}.lock`;e.mkdirSync(i.dirname(r),{recursive:!0});const m=performance.now()+s;for(;;)try{e.mkdirSync(t);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(d(t)||Date.now()-e.statSync(t).mtimeMs>o){e.rmSync(t,{recursive:!0});continue}}catch{}if(performance.now()>m)throw Object.assign(new Error(`${i.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(y=>setTimeout(y,100))}try{e.writeFileSync(i.join(t,"pid"),String(process.pid))}catch{}const u=l?setInterval(()=>{try{const n=new Date;e.utimesSync(t,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(u),e.rmSync(t,{recursive:!0,force:!0})}}a(S,"withFileLock");export{S as withFileLock};
|
package/hub/new-board.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as
|
|
2
|
-
`).map(
|
|
1
|
+
var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as B,listBoards as D,readManifest as h,withManifestLock as u,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as L,routeFromName as M,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as v,validateBranch as A,validateRemote as G}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as T}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as q,linearWorks as H,savedClient as K}from"./linear.mjs";const d=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function Y(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(c=>!["name","github","remote","branch","purpose","tickets"].includes(c)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=M(t);L.test(s)||d("Name must contain a letter or digit for its route.");const{github:r,remote:n}=U(e),f=A(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;n&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let o;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:c,...m}=a;o=typeof c=="string"?c.trim():"",(!o||o.length>80)&&d("Name the new Linear project."),a={...m,project:o}}return{route:s,title:t,github:r,remote:n,branch:f,tickets:$(a),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(Y,"details");function U(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:v(e.github)}:{remote:G(e.remote).remote}}i(U,"repoInput");const _=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),E="That board route already exists. Reopen it from Closed boards if it is closed.",V=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),W=i(({purpose:e})=>`--template board --items data/items.json${e?` --description ${V(e)}`:""}`,"joinFlags");function R(e){const t=String(e).split(`
|
|
2
|
+
`).map(s=>s.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(R,"errorLine");async function N(e,t){await u(async()=>{const s=h(),r=j(s.starting,e);!r||j(s.sites,e)||t.phase&&t.phase===r.phase||(s.starting[e]=t.phase?{...r,phase:t.phase,phase_at:new Date().toISOString()}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(s))})}i(N,"recordStart");const I=i(({title:e,purpose:t,repo:s,github:r,remote:n,branch:f,tickets:a},o)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:n},branch:f,tickets:a,phase:o.cloned(r||n)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),O=new Map,se=i(e=>O.get(e),"clonePercent"),z=1e3;async function x(e,{purpose:t,repo:s,github:r,remote:n,branch:f},{run:a,live:o,repos:c,changed:m}){let g,p=-1/0;const y=i(l=>{O.set(e,l),!(Date.now()-p<z)&&(p=Date.now(),m())},"progress");try{try{await c.ensure(r||n,f,{progress:y})}finally{O.delete(e)}await N(e,{phase:"starting"}),m();const l=await F(e,{repo:s,prompt:W({purpose:t}),run:a,live:o,clones:c.base});g={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){g={error:R(l.message)}}try{await N(e,g)}catch(l){return{error:R(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(x,"start");async function ne(e,{run:t,live:s,repos:r=k(),changed:n=i(()=>{},"changed"),linear:f}={}){const a=Y(e),{route:o,github:c,remote:m,newProject:g}=a;let{tickets:p}=a;p.source==="linear"&&!f&&!H()&&d(q);const y=r.path(c||m),l=g===void 0?null:f||K();if(l){_(h(),o)&&d(E);try{p={...p,project:(await l.createProject(p.team,g)).id}}catch(b){d(b.message,b.status||502)}}return await u(async()=>{const b=h();_(b,o)&&d(E),b.starting={...b.starting,[o]:I({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:x(o,{...a,repo:y},{run:t,live:s,repos:r,changed:n})}}i(ne,"requestBoard");function S(e,t,s){const r=j(e.starting,t);return(!r||j(e.sites,t)&&!r.joined_at||!C(r))&&d(`Only a board that didn\u2019t start can be ${s}.`),r}i(S,"failedStart");async function ae(e,{run:t,live:s,stop:r,repos:n=k(),changed:f=i(()=>{},"changed")}={}){const a=S(h(),e,"retried");a.joined_at&&d("Its session joined; a board that is not listening yet can only be dismissed."),a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await T(a,{live:s,stop:r})}catch(c){d(`Couldn\u2019t stop its earlier session: ${c.message}`,502)}const o=await u(async()=>{const c=h(),m=S(c,e,"retried");return c.starting[e]=I({...m,repo:n.path(m.github||m.remote),purpose:m.purpose||""},n),await w(c),c.starting[e]});return{route:e,started:x(e,o,{run:t,live:s,repos:n,changed:f})}}i(ae,"retryBoard");async function ie(e){return u(async()=>{const t=h();return S(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}i(ie,"dismissBoard");async function oe({listening:e=B}={}){return Object.values(h().starting||{}).some(t=>t.joined_at)?u(async()=>{const t=h(),s=new Map(D(t).map(n=>[n.route,n])),r=Object.entries(t.starting||{}).filter(([n,f])=>f.joined_at&&(!s.has(n)||e(s.get(n)))).map(([n])=>n);if(!r.length)return[];for(const n of r)delete t.starting[n];return Object.keys(t.starting).length||delete t.starting,await w(t),r}):[]}i(oe,"settleStarts");export{se as clonePercent,ie as dismissBoard,U as repoInput,ne as requestBoard,ae as retryBoard,oe as settleStarts};
|
package/hub/owner-checks.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var $=Object.defineProperty;var m=(r,t)=>$(r,"name",{value:t,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as g,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const E=3;async function F(r,t,u){const p=t.ownerChecks||{},i={},f=new Set;for(const e of r)for(const{slug:d,itemFile:w}of e.files)for(const n of w.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||n.lane!=="done")continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,d,n.id,c.check]);if(f.has(o))continue;f.add(o);const s=`${d}/${n.id}`;let a;try{a=await y(g(c.check),{since:c.since})}catch(k){const h=(p[o]||0)+1;if(h<E){i[o]=h;continue}try{await b(e.dir,s,c.check,k.message)}catch(l){i[o]=h,u.push(`${e.route}: could not mark ${s}'s check broken (${l.message})`)}continue}if(a.passed)try{await O(e.dir,s,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${s} on its passing check (${k.message})`)}}t.ownerChecks=i}m(F,"runOwnerChecks");export{E as BROKEN_AFTER,F as runOwnerChecks};
|
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
var se=Object.defineProperty;var a=(c,
|
|
2
|
-
`,{mode:384}),O.chmodSync(m,384),m}a($t,"failureLog");const Ie=a(c=>new Promise(f=>{const h=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},m=>{m.destroy(),f(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>f(!1))}),"probe"),Ut=a((c,f)=>{try{process.kill(-c.pid,f)}catch{}},"signalGroup");async function Jt(c,f,h){const m=await we(c.dir,f),[S,p]=f.split("/"),k=c.waves.find(b=>b.slug===S)?.wave,u=m?.path||k?.items.find(b=>b.id===p)?.worktree;if(typeof u!="string"||!u||u.split(T.sep).includes(".."))return"";let g=m?.repo;const $=(c.github||k?.repo||"").toLowerCase();if(!g&&$){const b=[c.owner?.repo,c.dir,...Object.values(h?.sites||{}).map(_=>_.owner?.repo)];for(const _ of new Set(b.filter(P=>typeof P=="string"&&T.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await C(_,"remote","get-url","origin"))?.[1].toLowerCase()===$){g=await C(_,"rev-parse","--show-toplevel");break}}catch{}}if(!m?.path&&!g||m?.path&&!T.isAbsolute(u))return"";try{const b=[T.join(O.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&T.isAbsolute(g))try{b.push(T.join(O.realpathSync(g),".claude","worktrees"))}catch{}const _=T.resolve(g||"/",u),P=O.realpathSync(_);return b.some(W=>_.startsWith(W+T.sep)&&P.startsWith(W+T.sep))&&O.statSync(P).isDirectory()?P:""}catch{return""}}a(Jt,"installWorktree");function Ce({now:c=Date.now,answers:f=Ie,log:h=console.log,waiting:m=a(async()=>null,"waiting"),event:S=ae,gate:p=ke}={}){const k=new Map,u=new Map,g=new Set,$=new Set,b=new Map,_=new Map,P=new Map,W=new Map;let G=!1,H=null,Q=null,ot=null;const lt=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=nt(e,{kind:null,main:null,cards:{}}),i={...yt(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}}:{}},s={state:i,file:e,written:JSON.stringify(r)};k.set(t.dir,s),i.main_port&&_.set(t.dir,i.main_port);const n=Object.entries(i.cards).filter(([,o])=>o.state==="running");for(const[o]of n)$.add(D(t,o));return i.kind==="web"?(i.cards=Object.fromEntries(n),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),r.prototypes?.state==="failed"&&(i.prototypes=r.prototypes),M(t),s}a(N,"stateOf");function M(t){const e=k.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Bt(e.file,e.state),e.written=r)}a(M,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>F(e)?t.cards[e]:t[ct[e]],"entryOf"),zt=a((t,e,r)=>{F(e)?t.cards[e]=r:t[ct[e]]=r},"setEntry"),kt=a((t,e)=>{F(e)?delete t.cards[e]:t[ct[e]]=null},"dropEntry");function X(t,e,r){const{state:i}=N(t),s=St(i,e);if(!s)return;const n=Object.hasOwn(r,"state")&&r.state!==s.state;Object.assign(s,r,F(e)&&n?{at:lt()}:{}),M(t)}a(X,"update");function dt(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 i=nt(T.join(r.dir,"data","try.json"),{}).main_port;yt(i)&&_.set(r.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:r}=t,i=D(e,r),s=r==="main"?_.get(e.dir):b.get(i);if(s)return s;const n=new Set([..._.values(),...b.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return r==="main"?(_.set(e.dir,o),N(e).state.main_port=o,M(e)):b.set(i,o),o}a(Yt,"publicPort");const rt=a(async t=>{const e=pe();if(!e)throw new Error("Tailscale isn't installed on this host");return wt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ot(t){try{await rt(["serve",`--https=${t}`,"off"])}catch(e){h(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ot,"removePort");const Tt=a(t=>xe(nt(T.join(t.dir,"board.json"),{})),"sharedWith");async function Et(t,e){const r=await p(t),i=t.headers["tailscale-user-login"];return r!=="other"||!i||!Tt(e).includes(i)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Et,"relayVerdict");function Vt(){for(const t of u.values()){if(!t.shared?.size)continue;const e=Tt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(Vt,"endUnshared");async function Qt(t){t.https=Yt(t);const e=t.sockets=new Set,r=a(()=>{t.visited=c()},"visit"),i=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),s=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const w=await Et(n,t.board);if(w!=="owner"&&w!=="shared"){Te(n,o,w);return}if(w==="shared"){const y={login:n.headers["tailscale-user-login"],res:o};s.add(y),o.on("close",()=>s.delete(y))}const x=Object.fromEntries(Object.entries(n.headers).filter(([y])=>!Gt.includes(y))),d=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:x},y=>{o.writeHead(y.statusCode,y.headers),y.on("data",r),y.pipe(o),y.on("close",()=>{y.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||d.destroy()}),d.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(d)}),t.relay.on("connection",n=>{r(),i(n)}),t.relay.on("upgrade",async(n,o,w)=>{const x=await Et(n,t.board);if(x!=="owner"){Ee(o,x==="shared"?"view-only":x);return}i(Oe({req:n,socket:o,head:w,port:t.port,path:n.url,replace:Gt}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await rt(["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(Qt,"openPort");async function Xt(t){if(t.closing=!0,await t.opening?.catch(()=>{}),t.exposed&&!G&&await Ot(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||b.delete(D(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...u.values()].filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await rt(["serve","status","--json"])).stdout)}catch(r){h(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){const i=`http://127.0.0.1:${r.relay.address().port}`,s=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(s?.Handlers?.["/"]?.Proxy!==i){h(`try port ${r.https}: its serve mapping was ${s?"changed":"gone"}; serving it again`);try{await rt(["serve","--bg",`--https=${r.https}`,i])}catch(n){h(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(Zt,"reserve");function st(t=J()){if(ot)return ot;dt(t);for(const e of U(t))N(e);return ot=(async()=>{try{const{stdout:e}=await rt(["serve","status","--json"]),r=JSON.parse(e);for(const i of Object.keys(r?.TCP||{}))yt(i)&&![...u.values()].some(s=>s.https===Number(i))&&await Ot(i)}catch(e){h(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ot}a(st,"initialize");async function xt(t,e,r,i){const{board:s,key:n}=t;if(X(s,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;const o=[];let w=!1;try{for await(const d of Dt({input:O.createReadStream(t.logFile),crlfDelay:1/0}))w||=je.test(d),o.push(d),o.length>5&&o.shift()}catch(d){o.push(`Could not read the build log: ${d.message}`)}const x=t.port||(N(s).state.kind||(s.waves.some(({wave:d})=>d.items.some(y=>y.install?.url))?"app":"web"))==="web"?"Serve":"Build";await me(s.dir,n,{text:[`${x} failed (exit ${e??1})`,i?.message,r&&`Stopped by ${r}.`,w&&"The login keychain must be unlocked on the host.",...o].filter(Boolean).join(`
|
|
3
|
-
`)})}a(xt,"failure");function at(t){return t.ending||(Ut(t.child,"SIGTERM"),t.ending=new Promise(e=>{const r=a(()=>{clearTimeout(i),clearInterval(s),e()},"finish");let i=setTimeout(()=>{Ut(t.child,"SIGKILL"),i=setTimeout(r,1e3)},4e3);const s=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{r()}},20)})),t.ending}a(at,"terminate");async function jt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await at(t),await t.done}a(jt,"halt");async function Z(t,e=!1){await jt(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(D(t.board,t.key))}a(Z,"stopRun");async function _t(t,e,r,i={}){if(!r)throw new E("This card has no valid worktree.");const s=Lt(r,t.build_target),n=he(s);if(n)throw new E(n);if(!O.realpathSync(s).startsWith(O.realpathSync(r)+T.sep))throw new E("Build script must be inside the worktree.");const o=await C(r,"rev-parse","HEAD");if(G)throw new E("The hub is stopping. Try again after it restarts.",503);const{state:w}=N(t),x=St(w,e),d={...F(e)?x:{},...i,sha:o,script:!0,state:"running",...F(e)?{at:lt(),head:o}:{}};delete d.port,delete d.https;const y=Kt(t,e);O.mkdirSync(T.dirname(y),{recursive:!0});const j=O.openSync(y,"w",384);O.fchmodSync(j,384);const v=ne(s,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(w,e,d),M(t);const B=D(t,e),l={child:v,board:t,key:e,logFile:y,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),P.set(B,o);let R,I;const q=a(A=>{I||=new Error(A),at(l)},"fail");v.on("error",A=>{R=A}),v.stdout.on("data",A=>O.writeSync(j,A)),v.stderr.on("data",A=>O.writeSync(j,A));const L=Dt({input:v.stdout,crlfDelay:1/0});return L.on("line",A=>{const it=/^port (\d{1,5})$/.exec(A);if(!it||l.port||l.stopped)return;const K=Number(it[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const Mt=a(async()=>{if(!(pt||l.stopped||l.ready||v.exitCode!==null||v.signalCode!==null)){pt=!0;try{if(await f(K)){if(l.stopped||I||v.exitCode!==null||v.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||I||v.exitCode!==null||v.signalCode!==null))return;l.ready=!0,l.visited=c(),w.kind="web",X(t,e,{state:"ready",port:K,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>Mt().catch(ht=>q(ht.message)),250),Mt().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),v.once("exit",()=>{at(l)}),l.done=new Promise(A=>v.once("close",(it,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),O.closeSync(j),(async()=>{if(await at(l),await Xt(l),l.finished=!0,l.stopped)return;const tt=e===z&&!l.port?new Error('Build script exited without printing "port <n>".'):null;I||R||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,K,I||R||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):kt(w,e),M(t)):(w.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>h(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{u.get(B)===l&&u.delete(B),A()})})),h(`build /${t.route} ${e}: started; log ${y}`),{status:"started"}}a(_t,"launch");async function It(t,e,r){const i=D(t,"main");if(g.has(i))throw new E("A build is already running for main.",409);g.add(i);try{u.has(i)&&await Z(u.get(i),!0);const s=T.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return O.mkdirSync(T.dirname(s),{recursive:!0}),gt(s)?await C(s,"checkout","--detach","--force",r.sha):(await C(e,"worktree","prune"),await C(e,"worktree","add","--detach",s,r.sha)),await _t(t,"main",s,r)}catch(s){throw s.status!==503&&(N(t).state.main={...r,state:"failed"},$t(t,"main",s),M(t)),s}finally{g.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=J()}={}){if(await st(e),dt(e),G)throw new E("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new E("Send only the board route and card key.");const{route:r,key:i}=t;if(F(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(d=>d==="."||d==="..")))throw new E("Card key must be <wave>/<id>.");const s=U(e).find(d=>d.route===r);if(!s)throw new E(`/${r} is not a board.`,404);if(F(i)&&!s.waves.some(({slug:d,wave:y})=>y.items.some(j=>`${d}/${j.id}`===i)))throw new E("There is no item with this card key.",404);const n=await m();if(n)throw new E(n,409);const o=D(s,i),w=u.get(o);if(g.has(o)||w&&!w.ready&&!w.finished)throw new E(`A build is already running for ${i==="main"?"main":i===z?"the prototype site":"this card"}.`,409);if(i==="main"){const d=N(s).state.main;if(d?.state==="running")throw new E("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new E("Main has no failed build to retry.",409);const{sha:y,branch:j,slug:v,at:B,script:l}=d;return It(s,et(r,e).repo,{sha:y,branch:j,slug:v,at:B,script:l,...s.build_target?{target:s.build_target}:{}})}g.add(o);const x=$.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=i===z?Ct(s):await Jt(s,i,e);return await _t(s,i,d)}catch(d){throw b.delete(o),x&&d.status!==503&&X(s,i,{state:void 0,sha:void 0}),d}finally{g.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=$e(t.route);return gt(e)&&O.statSync(e).isDirectory()?O.realpathSync(e):""},"baseWorktree");async function qt(t,e){const{state:r}=N(t),i=D(t,z),s=u.get(i);if(!((await ye(t.dir)).length>0&&Ct(t))){s&&await Z(s),r.prototypes&&(r.prototypes=null,M(t));return}if(!(s||g.has(i)||r.prototypes?.state==="failed"||await m()))try{await ut({route:t.route,key:z},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw r.prototypes={state:"failed"},$t(t,z,o),M(t),o}}a(qt,"serveBase");function Pt(t,e,{manifest:r=J()}={}){const i=U(r).find(n=>n.route===t);if(!i)return null;const s=[...u.values()].find(n=>n.board.dir===i.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return s?{key:s.key,port:s.port}:null}a(Pt,"lookup");async function ft({manifest:t=J(),routes:e}={}){if(await st(t),dt(t),G)return H;if(H&&e){if(!Q){const r={routes:new Set,manifest:t};r.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:r.manifest,routes:[...r.routes]}))),Q=r}Q.manifest=t;for(const r of e)Q.routes.add(r);return Q.done}return H||(H=(async()=>{await Zt();const r=U(t),i=nt(ce,{}).prs||{};for(const s of[...u.values()])if(!r.some(n=>n.dir===s.board.dir)||F(s.key)&&s.ready&&c()-s.visited>=_e){await Z(s);const{state:n}=N(s.board);kt(n,s.key),M(s.board)}for(const s of r){if(G)return;if(e&&!e.includes(s.route))continue;const{state:n}=N(s),o=et(s.route,t),w=o.branch||"main",x=le(o);for(const{slug:d,wave:y}of s.waves)for(const j of y.items){const v=`${d}/${j.id}`,B=ve(j,s.github||y.repo).map(l=>i[Se(l)]);if(!(j.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(s,v),R=await Jt(s,v,t);if(R){const L=await C(R,"rev-parse","HEAD");let A=0;try{A=Number(await C(R,"rev-list","--count",`${x}..HEAD`))}catch{}Object.assign(n.cards[v]||={},{head:L,ahead:A,script:gt(Lt(R,s.build_target))}),M(s)}else if(!$.has(l))continue;const I=n.cards[v];if(($.has(l)||s.auto_build&&I.script&&I.ahead>0&&(I.sha||P.get(l))!==I.head&&I.state!=="running")&&!g.has(l))try{await ut({route:s.route,key:v},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(s,v,{sha:I.head}),await xt({board:s,key:v,logFile:$t(s,v,L)},1),L}}catch(l){h(`try /${s.route} ${v}: ${l.message}`)}}await qt(s,t).catch(d=>h(`try /${s.route} prototypes: ${d.message}`));try{if(!o.repo||await m())continue;const d=await C(o.repo,"rev-parse",x);let y=!1;try{await C(o.repo,"cat-file","-e",`${d}:${Wt(s.build_target)}`),y=!0}catch{}if(!y){const I=u.get(D(s,"main"));I&&await Z(I),n.main=null,M(s);continue}const j=n.main;if(j?.state==="running"||j?.sha===d&&(j.target||null)===(s.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Pt(s.route,"main",{manifest:t}))))continue;const v=D(s,"main");if(g.has(v))continue;const B=await C(o.repo,"log","-1","--format=%cI",d);let l=s.github;if(!l)try{l=Ft(await C(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:w,slug:l||"",at:B,script:y,...s.build_target?{target:s.build_target}:{}};await It(s,o.repo,R)}catch(d){h(`try /${s.route} main: ${d.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function At(t,e){if(W.has(t.dir))return W.get(t.dir);const r=(async()=>{const i=et(t.route,e),s=i.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},M(t);let o,w;try{o=await Pe(i.repo,s)}catch(x){w=x.message}if(o===null?delete n.pull:n.pull={at:lt(),...w?{error:w}:{}},M(t),o===null)return null;if(w)return h(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){h(`pull /${t.route}: ${s} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await S(t,"main-pulled",{branch:s,...o})}catch(x){h(`pull /${t.route}: could not send main-pulled (${x.message})`)}}return{commits:o.commits}})().finally(()=>W.delete(t.dir));return W.set(t.dir,r),r}a(At,"pullMain");async function te({manifest:t=J(),routes:e}={}){await st(t);const r=[];for(const i of U(t)){if(G)break;if(e&&!e.includes(i.route))continue;const s=et(i.route,t);!s.local||!s.repo||(await At(i,t))?.commits&&r.push(i.route)}return r}a(te,"pullMains");async function ee(t,{manifest:e=J()}={}){if(await st(e),G)throw new E("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 E("Send only the board route.");const r=U(e).find(n=>n.route===t.route);if(!r)throw new E(`/${t.route} is not a board.`,404);const i=et(r.route,e);if(!i.local||!i.repo)throw new E("This board fetches its branch on every poll; only a local board pulls.",409);const s=await At(r,e);if(!s)throw new E("This board\u2019s folder has no origin to pull from.",409);return s.commits&&ft({manifest:e,routes:[r.route]}).catch(n=>h(`try /${r.route} main: ${n.message}`)),s}a(ee,"pull");async function re(){G=!0;const t=Promise.all([...u.values()].map(e=>e.ready?Z(e):jt(e)));await Promise.all([t,H,...W.values()])}return a(re,"stop"),{start:ut,tick:ft,stop:re,lookup:Pt,initialize:st,pull:ee,pullMains:te,endUnshared:Vt}}a(Ce,"createTryManager");const vt=1e4,bt=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:vt,env:ue()})).stdout,"originGit");async function Pe(c,f,{run:h=bt}={}){const m=a(async(...p)=>(await h(...p)).trim(),"git"),S=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await m(c,"config","--get","remote.origin.url")}catch{return null}try{await m(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){S(p.killed?`git fetch timed out after ${vt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await m(c,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await m(c,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=a((b,_)=>m(c,"merge-base","--is-ancestor",b,_).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||S(`${f} has split from origin`);const g=Number(await m(c,"rev-list","--count",`${p}..${k}`)),$=(await m(c,"worktree","list","--porcelain")).split(`
|
|
1
|
+
var se=Object.defineProperty;var a=(c,u)=>se(c,"name",{value:u,configurable:!0});import T from"node:fs";import E from"node:path";import Nt from"node:os";import mt from"node:http";import{execFile as ie,spawn as ne}from"node:child_process";import{createInterface as Dt}from"node:readline";import{promisify as oe}from"node:util";import{listBoards as J,readManifest as z,readJSON as nt,saveJSON as Rt,appendEvent as ae,GITHUB_STATE as ce,REPO_FETCH as Bt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Ft,mainRefOf as le}from"./board-repo.mjs";import{gitReachFault as Ht,remoteFromInput as de,remoteGitEnv as ue}from"./repos.mjs";import{readiness as fe}from"./readiness.mjs";import{tailscaleBin as pe}from"../shared/tailscale.mjs";import{buildScriptError as he,buildScriptOf as Wt}from"../board/build-script.mjs";import{appendStatus as me,recordedWorktree as ye,servedRoundsOpen as we,setItem as ge}from"../board/store.mjs";import{BASE_KEY as K,baseWorktreeOf as $e}from"../board/prototype-base.mjs";import{cardPRs as be,hasPR as ve,prKeyOf as Se}from"../shared/card-prs.mjs";import{hubGate as ke}from"../board/on-host.mjs";import{pipeUpgrade as Oe,refuse as Te,refuseUpgrade as Ee}from"./front-door.mjs";import{shareOf as xe}from"./share.mjs";class x extends Error{static{a(this,"BuildError")}constructor(u,m=400){super(u),this.status=m}}const yt=oe(ie),j=a(async(c,...u)=>(await yt("git",["-C",c,...u],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),_e=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,Y={first:8500,last:8599},wt=a(c=>/^\d+$/.test(String(c))&&Number(c)>=Y.first&&Number(c)<=Y.last,"isTryPort"),je=30*6e4,Gt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],lt={main:"main",[K]:"prototypes"},F=a(c=>!Object.hasOwn(lt,c),"isCard"),Lt=a((c,u)=>E.join(c,Wt(u)),"scriptAt"),gt=a(c=>T.existsSync(c),"exists"),Kt=a((c,u)=>E.join(c.dir,"data","try",`${u.split("/").at(-1)}.log`),"logFileOf");function $t(c,u,m){const y=Kt(c,u);return T.mkdirSync(E.dirname(y),{recursive:!0}),T.writeFileSync(y,m.message+`
|
|
2
|
+
`,{mode:384}),T.chmodSync(y,384),y}a($t,"failureLog");const Ie=a(c=>new Promise(u=>{const m=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},y=>{y.destroy(),u(!0)});m.on("timeout",()=>m.destroy()),m.on("error",()=>u(!1))}),"probe"),Ut=a((c,u)=>{try{process.kill(-c.pid,u)}catch{}},"signalGroup");async function Jt(c,u,m){const y=await ye(c.dir,u),[S,p]=u.split("/"),k=c.files.find(v=>v.slug===S)?.itemFile,d=y?.path||k?.items.find(v=>v.id===p)?.worktree;if(typeof d!="string"||!d||d.split(E.sep).includes(".."))return"";let g=y?.repo;const $=(c.github||k?.repo||"").toLowerCase();if(!g&&$){const v=[c.owner?.repo,c.dir,...Object.values(m?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(v.filter(P=>typeof P=="string"&&E.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await j(I,"remote","get-url","origin"))?.[1].toLowerCase()===$){g=await j(I,"rev-parse","--show-toplevel");break}}catch{}}if(!y?.path&&!g||y?.path&&!E.isAbsolute(d))return"";try{const v=[E.join(T.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&E.isAbsolute(g))try{v.push(E.join(T.realpathSync(g),".claude","worktrees"))}catch{}const I=E.resolve(g||"/",d),P=T.realpathSync(I);return v.some(W=>I.startsWith(W+E.sep)&&P.startsWith(W+E.sep))&&T.statSync(P).isDirectory()?P:""}catch{return""}}a(Jt,"installWorktree");function Ce({now:c=Date.now,answers:u=Ie,log:m=console.log,waiting:y=a(async()=>null,"waiting"),event:S=ae,gate:p=ke}={}){const k=new Map,d=new Map,g=new Set,$=new Set,v=new Map,I=new Map,P=new Map,W=new Map;let G=!1,H=null,Q=null,ot=null;const at=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=E.join(t.dir,"data","try.json"),s=nt(e,{kind:null,main:null,cards:{}}),i={...wt(s.main_port)?{main_port:s.main_port}:{},kind:s.kind||null,main:s.main||null,cards:{...s.cards},...s.pull?{pull:{...s.pull,running:void 0}}:{}},r={state:i,file:e,written:JSON.stringify(s)};k.set(t.dir,r),i.main_port&&I.set(t.dir,i.main_port);const n=Object.entries(i.cards).filter(([,o])=>o.state==="running");for(const[o]of n)$.add(D(t,o));return i.kind==="web"?(i.cards=Object.fromEntries(n),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),s.prototypes?.state==="failed"&&(i.prototypes=s.prototypes),M(t),r}a(N,"stateOf");function M(t){const e=k.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Rt(e.file,e.state),e.written=s)}a(M,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>F(e)?t.cards[e]:t[lt[e]],"entryOf"),zt=a((t,e,s)=>{F(e)?t.cards[e]=s:t[lt[e]]=s},"setEntry"),kt=a((t,e)=>{F(e)?delete t.cards[e]:t[lt[e]]=null},"dropEntry");function X(t,e,s){const{state:i}=N(t),r=St(i,e);if(!r)return;const n=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,F(e)&&n?{at:at()}:{}),M(t)}a(X,"update");function dt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(s=>s.site)];for(const s of e){if(!s?.dir)continue;const i=nt(E.join(s.dir,"data","try.json"),{}).main_port;wt(i)&&I.set(s.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:s}=t,i=D(e,s),r=s==="main"?I.get(e.dir):v.get(i);if(r)return r;const n=new Set([...I.values(),...v.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return s==="main"?(I.set(e.dir,o),N(e).state.main_port=o,M(e)):v.set(i,o),o}a(Yt,"publicPort");const rt=a(async t=>{const e=pe();if(!e)throw new Error("Tailscale isn't installed on this host");return yt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ot(t){try{await rt(["serve",`--https=${t}`,"off"])}catch(e){m(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ot,"removePort");const Tt=a(t=>xe(nt(E.join(t.dir,"board.json"),{})),"sharedWith");async function Et(t,e){const s=await p(t),i=t.headers["tailscale-user-login"];return s!=="other"||!i||!Tt(e).includes(i)?s:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Et,"relayVerdict");function Vt(){for(const t of d.values()){if(!t.shared?.size)continue;const e=Tt(t.board);for(const s of t.shared)e.includes(s.login)||(t.shared.delete(s),s.res.destroy())}}a(Vt,"endUnshared");async function Qt(t){t.https=Yt(t);const e=t.sockets=new Set,s=a(()=>{t.visited=c()},"visit"),i=a(n=>{e.add(n),n.on("data",s),n.on("close",()=>e.delete(n))},"track"),r=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const w=await Et(n,t.board);if(w!=="owner"&&w!=="shared"){Te(n,o,w);return}if(w==="shared"){const h={login:n.headers["tailscale-user-login"],res:o};r.add(h),o.on("close",()=>r.delete(h))}const _=Object.fromEntries(Object.entries(n.headers).filter(([h])=>!Gt.includes(h))),f=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:_},h=>{o.writeHead(h.statusCode,h.headers),h.on("data",s),h.pipe(o),h.on("close",()=>{h.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||f.destroy()}),f.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(f)}),t.relay.on("connection",n=>{s(),i(n)}),t.relay.on("upgrade",async(n,o,w)=>{const _=await Et(n,t.board);if(_!=="owner"){Ee(o,_==="shared"?"view-only":_);return}i(Oe({req:n,socket:o,head:w,port:t.port,path:n.url,replace:Gt}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await rt(["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(Qt,"openPort");async function Xt(t){if(t.closing=!0,await t.opening?.catch(()=>{}),t.exposed&&!G&&await Ot(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||v.delete(D(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...d.values()].filter(s=>s.ready&&s.relay&&!s.closing);if(!t.length)return;let e;try{e=JSON.parse((await rt(["serve","status","--json"])).stdout)}catch(s){m(`try ports check: ${(s.stderr||s.message).trim()}`);return}for(const s of t){const i=`http://127.0.0.1:${s.relay.address().port}`,r=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${s.https}`))?.[1];if(r?.Handlers?.["/"]?.Proxy!==i){m(`try port ${s.https}: its serve mapping was ${r?"changed":"gone"}; serving it again`);try{await rt(["serve","--bg",`--https=${s.https}`,i])}catch(n){m(`try port ${s.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(Zt,"reserve");function st(t=z()){if(ot)return ot;dt(t);for(const e of J(t))N(e);return ot=(async()=>{try{const{stdout:e}=await rt(["serve","status","--json"]),s=JSON.parse(e);for(const i of Object.keys(s?.TCP||{}))wt(i)&&![...d.values()].some(r=>r.https===Number(i))&&await Ot(i)}catch(e){m(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ot}a(st,"initialize");async function xt(t,e,s,i){const{board:r,key:n}=t;if(X(r,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;let o="",w=!1;try{for await(const h of Dt({input:T.createReadStream(t.logFile),crlfDelay:1/0}))w||=_e.test(h),h.trim()&&(o=h)}catch(h){o=`could not read the build log: ${h.message}`}const _=t.port||(N(r).state.kind||(r.files.some(({itemFile:h})=>h.items.some(O=>O.install?.url))?"app":"web"))==="web"?"Serve":"Build",f=w?"unlock the login keychain on the host":i?.message||s&&`stopped by ${s}`||o;await me(r.dir,n,"run-failed",{kind:_,code:e??1,why:f})}a(xt,"failure");function ct(t){return t.ending||(Ut(t.child,"SIGTERM"),t.ending=new Promise(e=>{const s=a(()=>{clearTimeout(i),clearInterval(r),e()},"finish");let i=setTimeout(()=>{Ut(t.child,"SIGKILL"),i=setTimeout(s,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{s()}},20)})),t.ending}a(ct,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ct(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,s,i={}){if(!s)throw new x("This card has no valid worktree.");const r=Lt(s,t.build_target),n=he(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(s)+E.sep))throw new x("Build script must be inside the worktree.");const o=await j(s,"rev-parse","HEAD");if(G)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:w}=N(t),_=St(w,e),f={...F(e)?_:{},...i,sha:o,script:!0,state:"running",...F(e)?{at:at(),head:o}:{},...e===K?{since:at()}:{}};delete f.port,delete f.https;const h=Kt(t,e);T.mkdirSync(E.dirname(h),{recursive:!0});const O=T.openSync(h,"w",384);T.fchmodSync(O,384);const b=ne(r,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(w,e,f),M(t);const R=D(t,e),l={child:b,board:t,key:e,logFile:h,visited:c(),stopped:!1,port:null,ready:!1};d.set(R,l),P.set(R,o);let B,C;const q=a(A=>{C||=new Error(A),ct(l)},"fail");b.on("error",A=>{B=A}),b.stdout.on("data",A=>T.writeSync(O,A)),b.stderr.on("data",A=>T.writeSync(O,A));const L=Dt({input:b.stdout,crlfDelay:1/0});return L.on("line",A=>{const it=/^port (\d{1,5})$/.exec(A);if(!it||l.port||l.stopped)return;const U=Number(it[1]);if(U<1||U>65535){q("Build script printed an invalid port.");return}l.port=U,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const Mt=a(async()=>{if(!(pt||l.stopped||l.ready||b.exitCode!==null||b.signalCode!==null)){pt=!0;try{if(await u(U)){if(l.stopped||C||b.exitCode!==null||b.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||C||b.exitCode!==null||b.signalCode!==null))return;l.ready=!0,l.visited=c(),w.kind="web",X(t,e,{state:"ready",port:U,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>Mt().catch(ht=>q(ht.message)),250),Mt().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{ct(l)}),l.done=new Promise(A=>b.once("close",(it,U)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(O),(async()=>{if(await ct(l),await Xt(l),l.finished=!0,l.stopped)return;const tt=e===K&&!l.port?new Error('Build script exited without printing "port <n>".'):null;C||B||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,U,C||B||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):kt(w,e),M(t)):(w.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>m(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{d.get(R)===l&&d.delete(R),A()})})),m(`build /${t.route} ${e}: started; log ${h}`),{status:"started"}}a(jt,"launch");async function It(t,e,s){const i=D(t,"main");if(g.has(i))throw new x("A build is already running for main.",409);g.add(i);try{d.has(i)&&await Z(d.get(i),!0);const r=E.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(E.dirname(r),{recursive:!0}),gt(r)?await j(r,"checkout","--detach","--force",s.sha):(await j(e,"worktree","prune"),await j(e,"worktree","add","--detach",r,s.sha)),await jt(t,"main",r,s)}catch(r){throw r.status!==503&&(N(t).state.main={...s,state:"failed"},$t(t,"main",r),M(t)),r}finally{g.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await st(e),dt(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(f=>!["route","key"].includes(f))||typeof t.route!="string"||typeof t.key!="string")throw new x("Send only the board route and card key.");const{route:s,key:i}=t;if(F(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(f=>f==="."||f==="..")))throw new x("Card key must be <file>/<id>.");const r=J(e).find(f=>f.route===s);if(!r)throw new x(`/${s} is not a board.`,404);if(F(i)&&!r.files.some(({slug:f,itemFile:h})=>h.items.some(O=>`${f}/${O.id}`===i)))throw new x("There is no item with this card key.",404);const n=await y();if(n)throw new x(n,409);const o=D(r,i),w=d.get(o);if(g.has(o)||w&&!w.ready&&!w.finished)throw new x(`A build is already running for ${i==="main"?"main":i===K?"the prototype site":"this card"}.`,409);if(i==="main"){const f=N(r).state.main;if(f?.state==="running")throw new x("A build is already running for main.",409);if(f?.state!=="failed"||!f.sha)throw new x("Main has no failed build to retry.",409);const{sha:h,branch:O,slug:b,at:R,script:l}=f;return It(r,et(s,e).repo,{sha:h,branch:O,slug:b,at:R,script:l,...r.build_target?{target:r.build_target}:{}})}g.add(o);const _=$.delete(o);try{d.has(o)&&await Z(d.get(o),!0);const f=i===K?Ct(r):await Jt(r,i,e);return await jt(r,i,f)}catch(f){throw v.delete(o),_&&f.status!==503&&X(r,i,{state:void 0,sha:void 0}),f}finally{g.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=$e(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function qt(t,e){const{state:s}=N(t),i=D(t,K),r=d.get(i),n=(await we(t.dir)).length>0&&Ct(t);if(!n){r&&await Z(r),s.prototypes&&(s.prototypes=null,M(t));return}if(r?.ready&&!r.stopped&&!g.has(i)){const o=await j(n,"rev-parse","HEAD");if(o===s.prototypes?.sha)return;m(`try /${t.route} prototypes: the base moved to ${o.slice(0,7)}; restarting its serve`)}else if(r||g.has(i)||s.prototypes?.state==="failed"||await y())return;try{await ut({route:t.route,key:K},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw s.prototypes={state:"failed"},$t(t,K,o),M(t),o}}a(qt,"serveBase");function Pt(t,e,{manifest:s=z()}={}){const i=J(s).find(n=>n.route===t);if(!i)return null;const r=[...d.values()].find(n=>n.board.dir===i.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Pt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await st(t),dt(t),G)return H;if(H&&e){if(!Q){const s={routes:new Set,manifest:t};s.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:s.manifest,routes:[...s.routes]}))),Q=s}Q.manifest=t;for(const s of e)Q.routes.add(s);return Q.done}return H||(H=(async()=>{await Zt();const s=J(t),i=nt(ce,{}).prs||{};for(const r of[...d.values()])if(!s.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=je){await Z(r);const{state:n}=N(r.board);kt(n,r.key),M(r.board)}for(const r of s){if(G)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),w=o.branch||"main",_=le(o);for(const{slug:f,itemFile:h}of r.files)for(const O of h.items){const b=`${f}/${O.id}`,R=be(O,r.github||h.repo).map(l=>i[Se(l)]);if(!(O.lane==="done"||R.length&&R.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,b),B=await Jt(r,b,t);if(B){const L=await j(B,"rev-parse","HEAD");let A=0;try{A=Number(await j(B,"rev-list","--count",`${_}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:L,ahead:A,script:gt(Lt(B,r.build_target))}),M(r)}else if(!$.has(l))continue;const C=n.cards[b];if(($.has(l)||r.auto_build&&C.script&&C.ahead>0&&(C.sha||P.get(l))!==C.head&&C.state!=="running")&&!g.has(l))try{await ut({route:r.route,key:b},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,b,{sha:C.head}),await xt({board:r,key:b,logFile:$t(r,b,L)},1),L}}catch(l){m(`try /${r.route} ${b}: ${l.message}`)}}await qt(r,t).catch(f=>m(`try /${r.route} prototypes: ${f.message}`));try{if(!o.repo||await y())continue;const f=await j(o.repo,"rev-parse",_);let h=!1;try{await j(o.repo,"cat-file","-e",`${f}:${Wt(r.build_target)}`),h=!0}catch{}if(!h){const C=d.get(D(r,"main"));C&&await Z(C),n.main=null,M(r);continue}const O=n.main;if(O?.state==="running"||O?.sha===f&&(O.target||null)===(r.build_target||null)&&(O.state==="failed"||O.state==="ready"&&(n.kind!=="web"||Pt(r.route,"main",{manifest:t}))))continue;const b=D(r,"main");if(g.has(b))continue;const R=await j(o.repo,"log","-1","--format=%cI",f);let l=r.github;if(!l)try{l=Ft(await j(o.repo,"remote","get-url","origin"))}catch{}const B={sha:f,branch:w,slug:l||"",at:R,script:h,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,B)}catch(f){m(`try /${r.route} main: ${f.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function At(t,e){if(W.has(t.dir))return W.get(t.dir);const s=(async()=>{const i=et(t.route,e),r=i.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},M(t);let o,w;try{o=await Pe(i.repo,r)}catch(_){w=_.message}if(o===null?delete n.pull:n.pull={at:at(),...w?{error:w}:{}},M(t),o===null)return null;if(w)return m(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){m(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await S(t,"main-pulled",{branch:r,...o})}catch(_){m(`pull /${t.route}: could not send main-pulled (${_.message})`)}}return{commits:o.commits}})().finally(()=>W.delete(t.dir));return W.set(t.dir,s),s}a(At,"pullMain");async function te({manifest:t=z(),routes:e}={}){await st(t);const s=[];for(const i of J(t)){if(G)break;if(e&&!e.includes(i.route))continue;const r=et(i.route,t);!r.local||!r.repo||(await At(i,t))?.commits&&s.push(i.route)}return s}a(te,"pullMains");async function ee(t,{manifest:e=z()}={}){if(await st(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const s=J(e).find(n=>n.route===t.route);if(!s)throw new x(`/${t.route} is not a board.`,404);const i=et(s.route,e);if(!i.local||!i.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await At(s,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[s.route]}).catch(n=>m(`try /${s.route} main: ${n.message}`)),r}a(ee,"pull");async function re(){G=!0;const t=Promise.all([...d.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...W.values()])}return a(re,"stop"),{start:ut,tick:ft,stop:re,lookup:Pt,initialize:st,pull:ee,pullMains:te,endUnshared:Vt}}a(Ce,"createTryManager");const bt=1e4,vt=a(async(c,...u)=>(await yt("git",["-C",c,...u],{encoding:"utf8",timeout:bt,env:ue()})).stdout,"originGit");async function Pe(c,u,{run:m=vt}={}){const y=a(async(...p)=>(await m(...p)).trim(),"git"),S=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await y(c,"config","--get","remote.origin.url")}catch{return null}try{await y(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${u}:refs/remotes/origin/${u}`)}catch(p){S(p.killed?`git fetch timed out after ${bt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await y(c,"rev-parse","--verify",`refs/heads/${u}^{commit}`),k=await y(c,"rev-parse","--verify",`refs/remotes/origin/${u}^{commit}`),d=a((v,I)=>y(c,"merge-base","--is-ancestor",v,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await d(k,p))return{from:p,to:p,commits:0};await d(p,k)||S(`${u} has split from origin`);const g=Number(await y(c,"rev-list","--count",`${p}..${k}`)),$=(await y(c,"worktree","list","--porcelain")).split(`
|
|
4
3
|
|
|
5
|
-
`).map(
|
|
6
|
-
`)).find(
|
|
7
|
-
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=
|
|
8
|
-
`).map(
|
|
4
|
+
`).map(v=>v.split(`
|
|
5
|
+
`)).find(v=>v.includes(`branch refs/heads/${u}`))?.[0].slice(9);return $?(await y($,"status","--porcelain","--untracked-files=no")&&S("local changes"),await y($,"merge","--ff-only","--quiet",k)):await y(c,"update-ref",`refs/heads/${u}`,k,p),{from:p,to:k,commits:g}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;S(String(p.stderr||p.message).trim().split(`
|
|
6
|
+
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=z(),log:u=console.log,routes:m}={}){const y=m?nt(Bt,{}):{};for(const S of J(c)){if(m&&!m.includes(S.route))continue;const p=et(S.route,c);if(p.local||!p.repo)continue;const k=p.branch||"main",d=new Date().toISOString(),g=Date.now();try{await vt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),y[S.route]={at:d,ok:!0}}catch($){u(`fetch /${S.route} after ${Date.now()-g} ms: ${$.message}`);let v=S.github;if(!v&&!S.remote)try{v=Ft(await j(p.repo,"remote","get-url","origin"))}catch{}y[S.route]={at:d,ok:!1,...Ht($.killed?`git fetch timed out after ${bt/1e3} s`:$.stderr,{github:v||"",remote:S.remote,repo:p.repo})};continue}if(S.remote)try{await Ae(S,p.repo,k,u)}catch($){u(`pushed branches /${S.route}: ${$.message}`)}}Rt(Bt,y)}a(tr,"fetchTryBranches");async function Ae(c,u,m,y){const S=c.files.flatMap(({slug:d,itemFile:g})=>g.items.filter($=>$.pushed?.branch&&!ve($)&&$.lane!=="done").map($=>({key:`${d}/${$.id}`,branch:$.pushed.branch})));if(!S.length)return;const p=new Map((await vt(u,"ls-remote","--heads","origin")).split(`
|
|
7
|
+
`).map(d=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(d)).filter(Boolean).map(d=>[d[2],d[1]])),k=de(c.remote)?.host||"the remote";for(const d of S){let g=p.get(d.branch);if(!g)try{g=await j(u,"rev-parse","--verify","--quiet",`refs/heads/${d.branch}^{commit}`)}catch{}let $=!1;if(g)try{await j(u,"merge-base","--is-ancestor",g,`refs/remotes/origin/${m}`),$=!0}catch{}if(!(!$&&p.has(d.branch)))try{await ge(c.dir,d.key,{done:!0,force:!0,landed:$,closing:$?["branch-merged",{branch:d.branch,into:m,host:k}]:["branch-gone",{branch:d.branch,host:k}]})}catch(v){y(`pushed ${d.key}: ${v.message}`)}}}a(Ae,"settlePushed");const V=Ce({waiting:a(()=>fe.waiting("tailscale"),"waiting")}),er=a((...c)=>V.start(...c),"startBuild"),rr=a((...c)=>V.tick(...c),"tryTick"),sr=a((...c)=>V.pull(...c),"pullNow"),ir=a((...c)=>V.pullMains(...c),"pullMains"),nr=a(()=>V.stop(),"stopTries"),or=a(()=>V.endUnshared(),"endUnsharedTries"),ar=a((...c)=>V.initialize(...c),"initializeTries");export{x as BuildError,Y as TRY_PORTS,Ce as createTryManager,or as endUnsharedTries,tr as fetchTryBranches,ar as initializeTries,Jt as installWorktree,wt as isTryPort,Pe as pullBranch,ir as pullMains,sr as pullNow,er as startBuild,nr as stopTries,rr as tryTick};
|
package/hub/upgrade.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var y=Object.defineProperty;var e=(n,s)=>y(n,"name",{value:s,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,s){const[a,i]=[m(n),m(s)];for(let r=0;r<3;r++)if((a[r]||0)!==(i[r]||0))return(a[r]||0)>(i[r]||0);return!1}e(g,"newer");function _({root:n,fetch:s=globalThis.fetch,run:a,log:i=e(()=>{},"log"),pulled:r=e(()=>{},"pulled")}){const k=p.existsSync(u.join(n,".git")),l=e(()=>JSON.parse(p.readFileSync(u.join(n,"package.json"),"utf8")).version,"installed"),d=l();let c=!1;async function h(){if(k)return;let o;try{const t=await s(S,{signal:AbortSignal.timeout(15e3)});if(!t.ok)throw new Error(`npm answered ${t.status}`);o=(await t.json()).version}catch(t){i(`kickflip update check failed: ${t.message}`);return}g(o,l())&&w(o)}e(h,"check");function w(o){c||(c=!0,a().then(({ok:t})=>{if(!t)return;const f=l();g(f,d)?r():i(`update left v${f}; npm has ${o}`)},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};
|