@lovek4d/kickflip 0.7.0 → 0.7.2
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 +53 -49
- package/SKILL.md +17 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +3 -3
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +113 -105
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +3 -3
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +6 -7
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -62
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +9 -5
- package/board/public/app.js +3 -3
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +11 -13
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +0 -1
- package/board/public/linear-key.js +2 -2
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +3 -3
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +43 -43
- package/board/public/ui-cards.js +25 -20
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +6 -7
- package/board/public/ui-questions.js +17 -13
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +29 -21
- package/board/public/ui.js +23 -6
- package/board/public/view-hub-settings.js +11 -19
- package/board/public/view-hub.js +25 -11
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +7 -7
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -26
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -1
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/starter/server.mjs +3 -3
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Ut=Object.defineProperty;var u=(e,t)=>Ut(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Bt from"node:net";import{spawn as Be,execFileSync as Ft}from"node:child_process";import b from"node:fs";import k from"node:path";import Fe from"node:os";import{fileURLToPath as Nt}from"node:url";import Ne from"node:vm";import Gt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as zt,addSubscription as Jt,removeSubscription as Kt,removeSubscriptionById as Wt,sendAll as Vt,flushQueue as Ge,queueStatus as ze,saveSettings as Zt,readSettings as q,migrateSettings as Qt,setNotifications as Xt,readCodexShare as Je,setCodexShare as Yt,readClaudeModel as qt,setClaudeModel as en,deliver as tn}from"./push.mjs";import{ownerTracker as nn}from"./presence.mjs";import{POLL_MS as Ke,answerOf as sn,expired as an,pendingFor as We,promptRecord as rn,pushOf as on,updatePrompts as N,shown as cn,pruneRemembered as ye,rememberRules as ln,rememberedAnswer as un,rememberedFor as Ve}from"./prompts.mjs";import{hold as dn,heldUntil as hn,holdLine as fn,editQuiet as pn,checkEvery as mn,nextCheckAt as gn}from"./schedule.mjs";import{capital as wn,hubView as bn,settingsView as yn,flushView as Sn,SNOOZE_REFUSED as kn}from"./views.mjs";import{caffeinate as $n,setKeepAwake as _n,awakeOn as xn,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 Ze,RESUME_STATE as En,sendToBoards as Qe,checkListening as Dn,recordHubCode as Ln,hubCode as Cn,runningHubCode as An,boardCode as Xe,recordLinkFaults as In,linksOf as Mn,hubLink as Hn,hubCodeState as Un,readJSON as Ye}from"./boards.mjs";import{liveSessions as qe}from"./live.mjs";import{RuleError as Bn,applyRules as Fn,changed as et,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 tt,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 nt,destOf as ms,fileVersion as gs,pageOf as st,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 at,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 rt,shareReach as Ws,routeOfPath as Vs,sharedSidebar as Zs,shareTracker as Qs}from"./share.mjs";import{restartRunner as ot}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 it}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(Nt(import.meta.url)),ra=k.join(G,"..","shared","colors.css"),ct=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"),lt=k.join(z,"manifest.json"),se=k.join(z,"logs"),ut=k.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,se,ut])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(lt,"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 dt=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(ut,e+".pid")}u(ce,"pidFile");function ia(e,t){try{const n=Number(b.readFileSync(ce(e),"utf8")),s=n?Ft("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(ua,"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=Gt.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 J=new Set,da=3e4;async function ha(e){const t=Date.now()+da;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(ha,"serverAnswers");async function gt(){if(ie)return;wt&&bt.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[...W.keys()])U(R(e,t))!=="port"&&W.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&W.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&la(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=wt&&s?.child&&s.code&&!s.stopping&&!J.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Gs(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))&&ua(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=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Fs({root:ve(),files:Mn().files,restart:l(()=>ct("hub"),"restart"),log:c}),fa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=l(async()=>{try{for(const e of await Ns())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const pa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+pa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=rn(),St=l(()=>y.startedAt?bn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(kt,"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 Rn({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 Ms({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 Dn({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 oa({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 An({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{Un(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=mn(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 $t=null;function ma(){$t||=En({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}l(ma,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||J.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({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(()=>Yn({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 As(g());const ga=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=cn(n,{now:Date.now(),aliveSince:ga}),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(xt,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function Tt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await zs(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(Tt,"noteRefusal");let Q=null,vt=null;async function he(){return Q?vt??=Q.catch(()=>{}).then(()=>(vt=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{Ps()}catch(e){c("machine check failed:",e.message)}F.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(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)),ma(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(fa),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;Ls().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(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const 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,Pt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(Pt,"newestMtime");function wa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(wa,"changedAt");const Rt=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"?Rt(n.dir)?"up":"down":i==="port"?W.has(t)?W.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:ms(n),changedAt:wa(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,Et=2e4,ba=1500;async function ya(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ba)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ya,"fetchNeeds");async function Dt(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>=Et)&&pe.set(i.route,{at:Date.now(),needs:ya(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(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Je(null).length,"badgeTotal"),Ie=new Map;function Sa(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};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>=Et)&&(p={at:Date.now(),listen:a,links:Bn(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(Sa,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Dt(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=Sa(a),u=Nn(),_=new Map(a.map(o=>[o.route,xs(o.owner?.session)]).filter(([,o])=>o)),k=qn(i,t.getTime(),cs,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=>js(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Vn(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=pn(t,O);return{data:{code:u.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Fn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ca({codexShare:Ke(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Sn(gn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Ct(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(Ct,"sendText");async function ka(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 Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(ka,"shellPage");const At=Us({root:ve(),run:$a,log:c,pulled:l(()=>bt.restartNow(),"pulled")});function $a(){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"),xa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ta=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<yn}),"pollState");async function ge(e){await xt();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...kn({now:t,st:ze(t),poll:Ta(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Rs.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ke(ne()),claudeModel:nn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Je(null).map(dn),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:jn()},n=te({now:e}),s=n.manual==="hub"?Pn(n.resets_at,e):null;return{...$n({now:e,st:ze(e),rules:ke(),linear:se(),slack:sa(),awake:t,pausedUntil:s}),closed:hs()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function va(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=Hn();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=xa(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await us(_[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 fs(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 ds(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,J.has(r))return a(409,{error:"That board is already restarting."});J.add(r),await A.running;const o=await ps(r,{serverUp:ha,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{J.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:qs().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),kt(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 ts(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 ns(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 rs(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 os(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 is(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 Is(d,{manifest:g()})):a(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)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(ia),{"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=Vs(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=ws(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")===ys(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Ct(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,Vt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Qt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Xt({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 en(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:xn}):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{On(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:Tn}):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{Os(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{tn(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{sn(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")?Ts(r.get("value")):vs(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")?(ea(),c("linear: key removed"),await yt()):c(`linear: key ${(await ta(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")?(aa(),c("slack: token removed")):c(`slack: token ${(await ra(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof na))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{Yt(wn(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=zn(ke(),r.rules,r.base??[]),nt(o.changes)&&Kn(o.rules)}catch(f){if(!(f instanceof Gn)&&!(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=fn(f,r);if(q)return{answer:q};const be=ln(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 Tt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),an(un(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 Tt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?Oa(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(va,"hubRoute");async function Oa(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"))||We,We));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}=on(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),hn(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(Oa,"promptRoute");const ja=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 Mt(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(Mt,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":Mt(t,n)}),e.end(Pa(t,n))}l(Ht,"sendDown");function Pa(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=Mt(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`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
4
|
+
`),n.code=Se(t)?Xe(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()+ht(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()+ht(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 ht=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")},dt)}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=Bt.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 ft=!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 pt(){if(oe)return;mt&>.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;ft&&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=mt&&s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?Xe(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)}ft=!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(pt,"reconcile");const mt=process.env.KICKFLIP_CODE_RESTARTS!=="off",gt=Hs({root:Te(),files:Cn().files,restart:u(()=>ot("hub"),"restart"),log:c}),da=setInterval(()=>pt().catch(e=>c("reconcile failed:",e.message)),2e3);pt().catch(e=>c("reconcile failed:",e.message));try{Qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const wt=u(async()=>{try{for(const e of await Us())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await wt();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=nn(),fa=u(()=>$.startedAt?gn({last:new Date($.startedAt),...de.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function bt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(bt,"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:mn({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 at({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 at({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=hn(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 yt=null;function pa(){yt||=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(()=>{yt=null})}u(pa,"runWakes");const Ce=new Set,A={running:null};function St(){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(St,"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 kt(){const{gone:e,rulesChanged:t}=await N(n=>{const s=an(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(kt,"prunePrompts");const Ae=u(()=>({running:qe(),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(tt(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,_t=null;async function he(){return Z?_t??=Z.catch(()=>{}).then(()=>(_t=null,he())):(Z=(async()=>Ss({gate:ee(),resume:u(async e=>{const t=await Qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Z=null}),Z)}u(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(bt,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(),kt().catch(e=>c("prompt expiry failed:",e.message)).finally(St)},15e3);function xt(){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()+dt;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(xt,"shutdown"),process.on("SIGTERM",xt),process.on("SIGINT",xt);const vt=new Map;function Tt(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,Tt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Tt,"newestMtime");function ga(e,t){const n=vt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Tt(t.dir);return vt.set(e,{at:s,checked:Date.now()}),s}u(ga,"changedAt");const Ot=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"?Ot(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,jt=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 Pt(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>=jt)&&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(Pt,"waitingCounts");const Rt=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:qe(),wakes:Ye(En,{})};for(const i of e)try{const r=Ze(i,s);n.set(i.route,r);let p=Ie.get(i.route);(!p||p.listen!==r||Date.now()-p.at>=jt)&&(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 Pt(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=dn(t,O);return{data:{code:l.running??null,version:ct,total:Rt(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:wn(fn(C,t))}:{state:"on",text:"On"},away:!de.state().present},links:p,rows:n,code:l}}u(Q,"sidebarState");function Et(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=Gt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}u(Et,"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 Dt=Is({root:Te(),run:ka,log:c,pulled:u(()=>gt.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"),Dt.check(),setInterval(()=>Dt.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 Lt=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=${nt()}"><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 kt();const t=new Date,{data:n,links:s,rows:i,code:r}=await Q();return{...bn({now:t,st:ze(t),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Os.rows(),code:r,version:ct,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(te()),claudeModel:qt(),codex:te(),awake:{on:xn(),unsupported:vn()},remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:We(null).map(cn),remembered:Ve(null)}}u(ge,"hubData");const I=u(async()=>{const e=new Date,t=ee({now:e}),n=t.manual==="hub"?Tn(t.resets_at,e):null;return{...yn({now:e,st:ze(e),rules:ke(),linear:ne(),slack:ta(),pausedUntil:n}),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=ot("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),bt(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,St(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Ze(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"?tt(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")===nt()?"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(Ye(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}):Et(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:zt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Jt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Wt((await T(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Kt((await T(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Vt({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 Xt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:kn}):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{_n(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{Yt(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{en(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 wt()):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=it();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=it();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{Zt(pn(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??[]),et(o.changes)&&Nn(o.rules)}catch(f){if(!(f instanceof Bn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!et(o.changes))return r(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Qe(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,Sn(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=un(f,a);if(Y)return{answer:Y};const be=rn(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})`:""}`),tn(on(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}=sn(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return ye(S,O),ln(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 Ct(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(Ct,"downWhy");function At(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":Ct(t,n)}),e.end(Oa(t,n))}u(At,"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=Ct(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`${Lt(`${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 It(){const e=g().sites;for(const t of we){const n=rt(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Es()}u(It,"endUnshared"),setInterval(It,2e3).unref();async function Mt(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=rt(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(Mt,"access");const Ht=Ue.createServer((e,t)=>{Mt(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(st({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=>Et(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();Pt(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:Rt(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(`${Lt("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)||"/",_=st({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Ot(p.dir)){At(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",It),y.on("error",()=>{if(t.headersSent){t.destroy();return}At(t,r,p)}),e.pipe(y)}u(ja,"serve"),Ht.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Mt(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}`]})}),Ht.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${lt})`));
|
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/linear.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub
|
|
1
|
+
var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub \u203A Integrations \u203A Linear first, or pick GitHub issues or None.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
|
package/hub/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:"none"}:e.tickets;n&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick None 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 N(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(N,"errorLine");async function R(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(R,"recordStart");const x=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 I(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 R(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:N(l.message)}}try{await R(e,g)}catch(l){return{error:N(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(I,"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]:x({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:I(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]=x({...m,repo:n.path(m.github||m.remote),purpose:m.purpose||""},n),await w(c),c.starting[e]});return{route:e,started:I(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/readiness.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=Object.defineProperty;var s=(t,n)=>w(t,"name",{value:n,configurable:!0});import x from"node:os";import{execFile as b}from"node:child_process";import{githubLogin as $}from"./pr-report.mjs";import{codexStatusReader as u}from"../shared/codex.mjs";import{linearStatus as C}from"./linear.mjs";import{slackStatus as S}from"./slack.mjs";import{readSettings as T,subscriptions as P}from"./push.mjs";import{tailscaleState as R}from"../bin/install.mjs";import{tailscaleBin as B,tailscaleInstallCommand as N}from"../shared/tailscale.mjs";const h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],I=s((t,n,a)=>({settings:t,label:`Hub settings \u203A ${n}`,text:a}),"settingsFix");function y(t,{now:n=Date.now(),platform:a=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let i,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(i=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(i="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(i="Tailscale isn't signed in",c="tailscale up"):(i="Tailscale isn't installed on the host",c=N(a)),i?{ready:!1,missing:i,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:i,kind:c,browser:f,added:m})=>({id:i,kind:c,browser:f,added:m}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:I("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,i])=>({id:o,name:e,...l(o,t[o]),waits:i}))}s(y,"readinessRows");const O=s(t=>(n,a)=>new Promise(l=>{b(n,a,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function H({home:t=x.homedir(),env:n=process.env,now:a=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=u({home:t,env:n,now:a,cacheMs:l}),i={github:s(()=>$({env:n}).catch(()=>null),"github"),codex:e,linear:s(()=>C(t),"linear"),slack:s(()=>S(t),"slack"),tailscale:s(()=>R({run:O(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:B({env:n})}),"tailscale"),notifications:s(()=>({on:T(t).notifications.on,devices:P(t)}),"notifications"),...o},c=new Map;async function f(d,r){if(!["github","tailscale"].includes(d))return i[d]({fresh:r});const g=c.get(d);if(!r&&g&&a()-g.at<l)return g.value;const p=Promise.resolve().then(()=>i[d]());return c.set(d,{at:a(),value:p}),p}s(f,"check");const m=s(async({fresh:d=!1}={})=>y(Object.fromEntries(await Promise.all(h.map(async([r])=>[r,await f(r,d)]))),{now:a()}),"rows");async function k(d){const[r]=y({[d]:await f(d,!1)},{now:a()});return r.ready||!r.waits?null:r.waits.replace("this",`${r.name}: ${r.missing.replace(/\.$/,"")}`)}return s(k,"waiting"),{rows:m,waiting:k}}s(H,"readinessReader");const L=u(),q=H({probes:{codex:L}});export{L as readCodex,q as readiness,H as readinessReader,y as readinessRows};
|
package/hub/review-send.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub
|
|
1
|
+
var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub \u203A Integrations \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function L({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(L,"sendReviews");export{L as sendReviews};
|
package/hub/slack.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
var b=Object.defineProperty;var n=(r,e)=>b(r,"name",{value:e,configurable:!0});import u from"node:fs";import h from"node:path";import k from"node:os";import{withFileLock as v}from"./locks.mjs";import{readSecret as $,writeSecret as d}from"./secrets.mjs";const m=["self_dm","owner_slack_user","slack_domain","timezone"],p=n(()=>h.join(k.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(m.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function _(r){const e=p();u.mkdirSync(h.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
|
|
2
|
-
`,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub
|
|
2
|
+
`,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub \u203A Integrations \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};
|