@lovek4d/kickflip 0.7.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +10 -6
- package/SKILL.md +19 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/board/bin/board.mjs +35 -13
- package/board/commit-message.mjs +6 -0
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +5 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/public/app-header.js +23 -0
- package/board/public/app-needs.js +1 -1
- package/board/public/app-reader.js +6 -2
- package/board/public/app.html +2 -0
- package/board/public/app.js +2 -0
- package/board/public/attach.js +12 -0
- package/board/public/col-resize.js +73 -0
- package/board/public/dialogs.js +3 -0
- package/board/public/prototype-kit.js +1 -1
- package/board/public/prototype.html +1 -0
- package/board/public/sidebar.css +8 -2
- package/board/public/style.css +33 -11
- package/board/public/ui-header.js +2 -2
- package/board/public/ui-sidebar.js +11 -3
- package/board/public/ui-threads.js +1 -0
- package/board/public/view-hub.js +2 -2
- package/board/public/view-questions.js +2 -1
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +18 -18
- package/board/store.mjs +32 -31
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +21 -19
- package/hooks-plugin/hooks/usage-gate.mjs +1 -3
- package/hub/boards.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/fake-gh.mjs +9 -4
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Bt=Object.defineProperty;var u=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Be from"node:http";import Ft from"node:net";import{spawn as Fe,execFileSync as Ge}from"node:child_process";import b from"node:fs";import k from"node:path";import ze from"node:os";import{fileURLToPath as Gt}from"node:url";import Je from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ke,queueStatus as We,saveSettings as Qt,readSettings as ee,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Ve,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ze,answerOf as an,expired as rn,pendingFor as Qe,promptRecord as on,pushOf as cn,updatePrompts as F,shown as ln,pruneRemembered as Se,rememberRules as un,rememberedAnswer as dn,rememberedFor as Xe}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as Sn,flushView as kn,SNOOZE_REFUSED as $n}from"./views.mjs";import{caffeinate as _n,setKeepAwake as xn,awakeOn as vn,awakeUnsupported as Tn}from"./awake.mjs";import{endOf as On}from"./duration.mjs";import{nodeBin as jn}from"../bin/install.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as ke,listenState as Ye,RESUME_STATE as Cn,sendToBoards as qe,checkListening as Ln,recordHubCode as An,hubCode as In,runningHubCode as Hn,boardCode as et,recordLinkFaults as Mn,linksOf as Nn,hubLink as Un,hubCodeState as Bn,readJSON as tt,migrateCellarNode as Fn}from"./boards.mjs";import{liveSessions as nt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as st,readHubRules as $e,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn,holdNeeds as Xn}from"./home.mjs";import{checkSessions as Yn,compactFullContexts as qn,startingBoards as es}from"./sessions.mjs";import{repoService as ts,repoLookupRoute as ns}from"./repos.mjs";import{boardRepo as at,changeBoardRepo as ss,migrateBoardRepos as as,migrateHubBoard as rs}from"./board-repo.mjs";import{requestBoard as os,retryBoard as is,dismissBoard as cs,clonePercent as ls,settleStarts as us}from"./new-board.mjs";import{boardClosure as ds,closeBoard as hs,closedBoards as fs,reopenBoard as ps}from"./close-board.mjs";import{restartBoard as ms,restartable as gs}from"./restart-board.mjs";import{crossSite as ws,isJSON as _e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as xe,appFile as bs,colorsVersion as rt,destOf as ys,fileVersion as Ss,pageOf as ot,shellHtml as ks}from"./shell.mjs";import{hubStream as $s}from"./stream.mjs";import{usageReader as _s,checkUsagePause as xs,contextReading as vs}from"./usage.mjs";import{readUsageGate as te,setPauseAt as Ts,setKeepGoing as Os,setHubPause as js,boardPausedUntil as Ps}from"../shared/usage-gate.mjs";import{checkMachine as Rs}from"../shared/machine-gate.mjs";import{readiness as Es,readCodex as ne}from"./readiness.mjs";import{BuildError as Ds,startBuild as Cs,stopTries as Ls,endUnsharedTries as As,tryTick as ve,initializeTries as Is,fetchTryBranches as it,pullNow as Hs,pullMains as Ms}from"./try.mjs";import{serveFiles as Ns}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Fs}from"./self-restart.mjs";import{fallBackFromLinear as Gs,migrateWaveNames as zs,openBoardBuilders as Js}from"../board/store.mjs";import{recordRefusal as Ks}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ws,refuse as Vs,refuseUpgrade as Zs}from"./front-door.mjs";import{shareEntries as Qs,sharedRoutes as ct,shareReach as Xs,routeOfPath as Ys,sharedSidebar as qs,shareTracker as ea}from"./share.mjs";import{restartRunner as lt}from"../board/worktree.mjs";import{hubService as ta}from"../shared/service.mjs";import{cloneRoot as se}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ae,removeLinearKey as na,saveLinearKey as sa,savedClient as ut}from"./linear.mjs";import{SlackError as aa,slackStatus as ra,removeSlackToken as oa,saveSlackToken as ia}from"./slack.mjs";import{sendReviews as ca}from"./review-send.mjs";const G=k.dirname(Gt(import.meta.url)),la=k.join(G,"..","shared","colors.css"),dt=JSON.parse(b.readFileSync(k.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=k.join(ze.homedir(),".claude","kickflip"),ht=k.join(z,"manifest.json"),re=k.join(z,"logs"),ft=k.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,re,ft])b.mkdirSync(e,{recursive:!0});try{const e=An();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await as();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await rs();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await zs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}try{const{routes:e,loginItem:t}=await Fn();e.length&&c(`manifest: ${e.map(n=>`/${n}`).join(", ")} spawn node by Homebrew's link`),t&&c("hub login item file: spawns node by Homebrew's link")}catch(e){c("Cellar node migration failed:",e.message)}const H=ts(),ua=_s(),oe=_n({log:c});oe.apply();let ie={version:1,sites:{}};function g(){try{ie=JSON.parse(b.readFileSync(ht,"utf8")),ie.sites||={}}catch(e){e.code==="ENOENT"?ie={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return ie}u(g,"loadManifest");const j=new Map,M=new Set;let ce=!1;const pt=5e3,le=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),N=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function ue(e){return k.join(ft,e+".pid")}u(ue,"pidFile");function da(e,t){try{const n=Number(b.readFileSync(ue(e),"utf8")),s=n?Ge("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ue(e))}catch{}}u(da,"killStale");function ha(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(re,e+".log"),"a"),[o,...r]=t.cmd,f={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=ke(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&&M.add(h);const l=u((_,S)=>{if(M.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:_??S,nextStart:Date.now()+pt(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(da,"start");const pt=u(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(()=>{M.has(n)&&de(n,"SIGKILL")},ft)}u(ue,"stop");const de=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const W=new Set,ha=3e4;async function fa(e){const t=Date.now()+ha;for(;;){const n=P(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(o=>setTimeout(o,250))}}u(fa,"serverAnswers");async function gt(){if(ie)return;wt&&bt.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`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...K.keys()])U(P(e,t))!=="port"&&K.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&K.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ua(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 o=wt&&s?.child&&s.code&&!s.stopping&&!W.has(t)&&!B.has(t)?qe(n):null;if(o&&o!==s.code){if(!(await zs(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!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&da(t,n)}mt=!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(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Ns({root:ne(),files:Hn().files,restart:u(()=>ct("hub"),"restart"),log:c}),pa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=u(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const ma=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ma,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},he=sn(),St=u(()=>bn(new Date,F()),"quietNow"),ga=u(()=>$.startedAt?wn({last:new Date($.startedAt),...he.state(),quiet:St(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&!St()&&Le().catch(n=>c("poll failed:",n.message))}u(kt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=F();let s;try{s=await Rn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await ot({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await ot({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Hs({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ia({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Je();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let $t=null;function wa(){$t||=En({settings:F(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(wa,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||W.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})}u(_t,"runSessionCheck"),await As(g());const ba=Date.now(),V=new Map;function Z(e){e?(me.delete(e),x.changed(e)):x.refresh()}u(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=rn(n,{now:Date.now(),aliveSince:ba}),o=Se(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ae=u(()=>({running:tt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Js(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Q=null,Tt=null;async function fe(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,fe())):(Q=(async()=>_s({gate:ee(),resume:u(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)}u(fe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),fe().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}he.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ga().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:u(e=>Ln(e)&&pe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),wa(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(pa),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)de(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function ya(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}u(ya,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function pe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=U(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?K.has(t)?K.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:ya(t,n)||null,created:n.created||null,log:k.join(ae,t+".log")}})}u(pe,"status");const N=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),me=new Map,Et=2e4,Sa=1500;async function ka(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Sa)});return t.ok?Wn(await t.json()):null}catch{return null}}u(ka,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=me.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&me.set(o.route,{at:Date.now(),needs:ka(o.port)})}const s=await Promise.all(n.map(o=>me.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}u(Dt,"waitingCounts");const Lt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ze(null).length,"badgeTotal"),Ie=new Map;function $a(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};for(const o of e)try{const r=Xe(o,s);n.set(o.route,r);let p=Ie.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:r,links:Bn(o,{listen:r})},Ie.set(o.route,p)),t.set(o.route,p.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u($a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=pe(),s=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:p,listens:h}=$a(r),l=Fn(),_=new Map(r.map(i=>[i.route,xs(i.owner?.session)]).filter(([,i])=>i)),S=qn(o,t.getTime(),cs,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>js(i.dir)).map(i=>[i.route,ee({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=F(),C=hn(t,O);return{data:{code:l.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Nn(l)),home:b.realpathSync(Ge.homedir()),starting:S,usage:(({start:i,...d})=>d)(la({codexShare:We(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(pn(C,t))}:{state:"on",text:"On"},away:!he.state().present},links:p,rows:n,code:l}}u(X,"sidebarState");function Ct(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}u(Ct,"sendText");async function _a(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=ze.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])ze.runInContext(b.readFileSync(k.join(xe,o),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(_a,"shellPage");const He=Us({root:ne(),run:xa,log:c,pulled:u(()=>bt.restartNow(),"pulled")});function xa(){const e=k.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=
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(
|
|
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"),Ta=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...Sn({now:t,st:Ke(t),sidebar:n,rows:o,links:s,rules:$e(),integrations:await Rs.rows(),code:r,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(te()),claudeModel:en(),codex:te(),awake:{on:Tn(),unsupported:On()},remembered:new Map(D(g()).map(p=>[p.route,Qe(p.route)]))}),prompts:Ze(null).map(ln),remembered:Qe(null)}}u(we,"hubData");const I=u(async()=>{const e=new Date,t=ee({now:e}),n=t.manual==="hub"?jn(t.resets_at,e):null;return{...kn({now:e,st:Ke(e),rules:$e(),linear:se(),slack:aa(),pausedUntil:n}),closed:hs()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Oa(e,t,n,s=null){const o=u((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=u((a,i)=>{const d=Mn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=u(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Ta(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await us(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let a;try{const i=await v(e);if(a=i.route,l==="/_hub/reopen-board"){const T=await fs(a);return x.refresh(),r(200,T)}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 ds(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await v(e)).route,W.has(a))return r(409,{error:"That board is already restarting."});W.add(a),await A.running;const i=await ps(a,{serverUp:fa,portUp:Re,onStarting:u(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{W.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=ct("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ea().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const a=await He.check();return a.state==="clone"&&(a.commit=Fe("git",["-C",ne(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),kt(he.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await v(e),i=D(g()).find(d=>d.route===a);return i?(Ce.add(a),await A.running,_t(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Xe(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:H,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?st(S[1]):await ns(S[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(a,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const a=await is(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(ie)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(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 v(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 Is(d,{manifest:g()})):r(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return r(f.status,{error:f.message});throw f}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(z,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",va());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(k.join(z,`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")===at()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ca),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(l==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};be.add(a),e.on("close",()=>be.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!ke(a))return r(404,{error:"That board is not open."});const i=Zs(et(k.join(a.dir,"board.json"),{})),d=i.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:i[q].level}));return r(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=ws(l.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],f=n.searchParams.get("v")===ys(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await I());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):o(400,"text/plain; charset=utf-8",f);let T;try{T=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:_n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{vn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return re.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{Os(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await fe(),h?r(200,await I()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),te({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?vs(a.get("value")):Ts(a.get("value")),await fe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ta(),c("linear: key removed"),await yt()):c(`linear: key ${(await na(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ra(),c("slack: token removed")):c(`slack: token ${(await oa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof sa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=se();if(!a.set||a.refused)return r(200,a);const i=lt();try{const d=await i.teams();return r(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 r(200,{...se(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=lt();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(f){return r(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(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)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(F(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await v(e,64*1024),i=zn($e(),a.rules,a.base??[]),nt(i.changes)&&Jn(i.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!nt(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...i.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:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Je();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,$n(a)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const a=await v(e);return r(200,await Le({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await v(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:i,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(Ue=>Ue.owner?.session&&Ue.owner.session===a.session_id)||null;Se(f,T)&&x.refresh();const q=dn(f,a);if(q)return{answer:q};const ye=on(a,{board:L,live:T.running?.get(a.session_id)});return f.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await vt(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(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 v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?ja(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(Oa,"hubRoute");async function ja(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=u(()=>G(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await p();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ve,Ve));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const 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 v(e),h=await G(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return Se(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...V.get(n)||[]])S();return Z(l.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(ja,"promptRoute");const Pa=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(It,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(Ra(t,n))}u(Ht,"sendDown");function Ra(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=It(e,t),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`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
4
|
+
`),n.code=ke(t)?et(t):null;let h;try{h=Fe(o,r,{cwd:t.cwd||t.dir,env:f,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
|
|
5
|
+
`),b.closeSync(s),Object.assign(n,{child:null,key:le(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ue(e),String(h.pid)),Object.assign(n,{child:h,key:le(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,S)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(ue(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(ha,"start");const mt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function de(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;he(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&he(n,"SIGKILL")},pt)}u(de,"stop");const he=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let gt=!0;const U=new Set;let Ee=!1;const K=new Set,fa=3e4;async function pa(e){const t=Date.now()+fa;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===le(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(pa,"serverAnswers");async function wt(){if(ce)return;bt&&yt.check();const{sites:e}=g();for(const t of[...j.keys()])U.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),de(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),de(t),j.delete(t)));for(const t of[...J.keys()])N(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))N(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;gt&&da(t,n);const s=j.get(t);if(s&&s.child&&s.key!==le(n)){c(`${t} entry changed; restarting it`),de(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=bt&&s?.child&&s.code&&!s.stopping&&!K.has(t)&&!U.has(t)?et(n):null;if(o&&o!==s.code){if(!(await Js(n.dir)).length){c(`${t} runs older code than its files; restarting it`),de(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ha(t,n)}gt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(wt,"reconcile");const bt=process.env.KICKFLIP_CODE_RESTARTS!=="off",yt=Fs({root:se(),files:In().files,restart:u(()=>lt("hub"),"restart"),log:c}),ma=setInterval(()=>wt().catch(e=>c("reconcile failed:",e.message)),2e3);wt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const St=u(async()=>{try{for(const e of await Gs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await St();const ga=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ga,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},fe=sn(),wa=u(()=>$.startedAt?wn({last:new Date($.startedAt),...fe.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Ce().catch(n=>c("poll failed:",n.message))}u(kt,"presenceChanged");function Ce({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=ee();let s;try{s=await Pn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await it({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await it({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Ms({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ca({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Ln({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Mn(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Ke();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Ce,"runPoll");let $t=null;function ba(){$t||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(ba,"runWakes");const Le=new Set,A={running:null};function _t(){if(A.running||U.size||K.size)return;const e=new Set(Le);Le.clear(),A.running=Yn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>qn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}u(_t,"runSessionCheck"),await Is(g());const ya=Date.now(),W=new Map;function V(e){e?(Q.delete(e),x.changed(e)):x.refresh()}u(V,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await F(n=>{const s=rn(n,{now:Date.now(),aliveSince:ya}),o=Se(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)V(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ae=u(()=>({running:nt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ks(at(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Z=null,Tt=null;async function pe(){return Z?Tt??=Z.catch(()=>{}).then(()=>(Tt=null,pe())):(Z=(async()=>xs({gate:te(),resume:u(async e=>{const t=await qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Z=null}),Z)}u(pe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),pe().catch(e=>c("usage gate failed:",e.message));try{Rs()}catch(e){c("machine check failed:",e.message)}fe.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=wa().getTime()&&Ce().catch(e=>c("poll failed:",e.message))}),us({listening:u(e=>Dn(e)&&me().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ba(),oe.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ce)return;ce=!0,clearInterval(ma),oe.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)he(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),he(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function Sa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}u(Sa,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function me(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=N(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:gs(n),changedAt:Sa(t,n)||null,created:n.created||null,log:k.join(re,t+".log")}})}u(me,"status");const B=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),Ie=new Map,Q=new Map,Et=2e4,ka=1500;async function $a(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ka)});return t.ok?Wn(await t.json()):null}catch{return null}}u($a,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=Q.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&Q.set(o.route,{at:Date.now(),needs:$a(o.port)})}const s=await Promise.all(n.map(o=>Q.get(o.route).needs));return new Map(n.map((o,r)=>{const f=Xn(Ie.get(o.route)??null,s[r],Q.get(o.route).at,Date.now());return f.good?Ie.set(o.route,f.good):Ie.delete(o.route),[o.route,f.needs]}))}u(Dt,"waitingCounts");const Ct=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Qe(null).length,"badgeTotal"),He=new Map;function _a(e){const t=new Map,n=new Map,s={running:nt(),wakes:tt(Cn,{})};for(const o of e)try{const r=Ye(o,s);n.set(o.route,r);let f=He.get(o.route);(!f||f.listen!==r||Date.now()-f.at>=Et)&&(f={at:Date.now(),listen:r,links:Nn(o,{listen:r})},He.set(o.route,f)),t.set(o.route,f.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u(_a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=me(),s=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:f,listens:h}=_a(r),l=Bn(),_=new Map(r.map(i=>[i.route,vs(i.owner?.session)]).filter(([,i])=>i)),S=es(o,t.getTime(),ls,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>Ps(i.dir)).map(i=>[i.route,te({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=ee(),L=hn(t,O);return{data:{code:l.running??null,version:dt,total:Ct(s),start:Zn(w),sites:w,hub:Qn(w,f,Un(l)),home:b.realpathSync(ze.homedir()),starting:S,usage:(({start:i,...d})=>d)(ua({codexShare:Ve(ne())})),pause:te(),notifications:L?L.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(L,t))}:{state:"on",text:"On"},away:!fe.state().present},links:f,rows:n,code:l}}u(X,"sidebarState");function Lt(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const f={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),f["content-encoding"]="gzip"),f["content-length"]=r.length,t.writeHead(n,f),t.end(e.method==="HEAD"?void 0:r)}u(Lt,"sendText");async function xa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Je.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Je.runInContext(b.readFileSync(k.join(xe,o),"utf8"),s);return ks({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(xa,"shellPage");const Me=Us({root:se(),run:va,log:c,pulled:u(()=>yt.restartNow(),"pulled")});function va(){const e=k.join(re,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
+
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Fe(jn(),[k.join(se(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(va,"runUpdate"),Me.check(),setInterval(()=>Me.check(),Bs);const x=$s({targets:u(()=>Object.entries(g().sites).filter(([,e])=>ke(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function ge(e){let t="0";try{t=Math.floor(b.statSync(k.join(G,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(ge,"iconUrl");const Ta=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ge(192),sizes:"192x192",type:"image/png"},{src:ge(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function v(e,t){if(!_e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(v,"readJSONBody");const At=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
|
+
<title>${B(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${ge(180)}">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${rt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Oa=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...yn({now:t,st:We(t),sidebar:n,rows:o,links:s,rules:$e(),integrations:await Es.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ve(ne()),claudeModel:en(),codex:ne(),awake:{on:vn(),unsupported:Tn()},remembered:new Map(D(g()).map(f=>[f.route,Xe(f.route)]))}),prompts:Qe(null).map(ln),remembered:Xe(null)}}u(we,"hubData");const I=u(async()=>{const e=new Date,t=te({now:e}),n=t.manual==="hub"?On(t.resets_at,e):null;return{...Sn({now:e,st:We(e),rules:$e(),linear:ae(),slack:ra(),pausedUntil:n}),closed:fs()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function ja(e,t,n,s=null){const o=u((a,i,d,p={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...p}),t.end(d)},"send"),r=u((a,i)=>{const d=Hn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),f=u(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Oa(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await ds(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let a;try{const i=await v(e);if(a=i.route,l==="/_hub/reopen-board"){const T=await ps(a);return x.refresh(),r(200,T)}if(U.has(a))return r(409,{error:"That board is already closing."});U.add(a);const d=u(()=>U.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const p=await hs(a,i.name);return x.refresh(),r(200,p)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await v(e)).route,K.has(a))return r(409,{error:"That board is already restarting."});K.add(a),await A.running;const i=await ms(a,{serverUp:pa,portUp:Re,onStarting:u(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{K.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=lt("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ta().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const a=await Me.check();return a.state==="clone"&&(a.commit=Ge("git",["-C",se(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),kt(fe.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await v(e),i=D(g()).find(d=>d.route===a);return i?(Le.add(a),await A.running,_t(),await A.running,He.clear(),x.changed(a),r(200,{route:a,listen:Ye(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/repo"&&e.method==="GET")return ns(n,{repos:H,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?at(S[1]):await ss(S[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return f(await os(a,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return f(await is(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const a=await cs(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(ce)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(p=>p==="forwarded"||p.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(p){return r(p.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Hs(d,{manifest:g()})):r(202,await Cs(d,{manifest:g(),log:c}))}catch(p){if(p instanceof Ds)return r(p.status,{error:p.message});throw p}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",Ta());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(k.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const a=n.searchParams.get("v")===rt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(la),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(l==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};be.add(a),e.on("close",()=>be.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!ke(a))return r(404,{error:"That board is not open."});const i=Qs(tt(k.join(a.dir,"board.json"),{})),d=i.map(C=>C.login),p=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((C,q)=>({...C,level:i[q].level}));return r(200,{people:T,refused:Y.refusals(O[1],d),link:p?`https://${p}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=bs(l.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],p=n.searchParams.get("v")===Ss(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":p}):Lt(e,t,200,{"content-type":d,"cache-control":p},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await I());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,p="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:p}):o(400,"text/plain; charset=utf-8",p);let T;try{T=await Yt(d)}catch(C){if(C.status!==400)throw C;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{xn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return oe.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{js(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await pe(),h?r(200,await I()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),ne({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?Ts(a.get("value")):Os(a.get("value")),await pe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(na(),c("linear: key removed"),await St()):c(`linear: key ${(await sa(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(oa(),c("slack: token removed")):c(`slack: token ${(await ia(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof aa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=ae();if(!a.set||a.refused)return r(200,a);const i=ut();try{const d=await i.teams();return r(200,{...ae(),teams:d.map(({key:p,name:T})=>({key:p,name:T})),projects:d.flatMap(p=>p.projects.map(T=>({id:T.id,name:T.name,team:p.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ae(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=ut();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(p){return r(p.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(p){if(!(p instanceof Oe))throw p;return r(p.status,{error:p.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(ee(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await v(e,64*1024),i=zn($e(),a.rules,a.base??[]),st(i.changes)&&Jn(i.rules)}catch(p){if(!(p instanceof Gn)&&!(p instanceof SyntaxError))throw p;return r(400,{error:p.message})}if(!st(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(p=>p.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Ke();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,kn(a)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const a=await v(e);return r(200,await Ce({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(p){if(p.status)throw p;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await F(p=>{const T=Ae(),C=T.boards.find(Ue=>Ue.owner?.session&&Ue.owner.session===a.session_id)||null;Se(p,T)&&x.refresh();const q=dn(p,a);if(q)return{answer:q};const ye=on(a,{board:C,live:T.running?.get(a.session_id)});return p.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(V(d.route),await vt(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(p=>c(`prompt ${d.id}: push failed: ${p.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(a)})}const L=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return L?Pa(e,t,{id:L[1],op:L[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(ja,"hubRoute");async function Pa(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const f=u(()=>F(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(V(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await f();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ze,Ze));if(await new Promise(y=>{const m=W.get(n)||new Set;W.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&W.get(n)===m&&W.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await f();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const f=await v(e),h=await F(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,f);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return Se(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...W.get(n)||[]])S();return V(l.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const f=await F(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return f&&(V(f.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(Pa,"promptRoute");const Ra=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=N(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(It,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(Ea(t,n))}u(Ht,"sendDown");function Ea(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=It(e,t),f=r==="code"||r==="starting"?"wait":"down",h=N(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
|
-
<h1>${
|
|
12
|
-
<p class="lede">${
|
|
11
|
+
<h1>${B(t.title||e)} isn't answering</h1>
|
|
12
|
+
<p class="lede">${B(l)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${Ra({icon:Kn(e,t),title:B(t.title||e),sub:`<span class="st ${f}">${B(o[r])}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}u(
|
|
16
|
+
</main></body></html>`}u(Ea,"downPage");const Ne=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=ea({file:k.join(z,"share-seen.json")}),be=new Set;function Mt(){const e=g().sites;for(const t of be){const n=ct(t.login,e);[...t.routes].some(([s,o])=>n.get(s)!==o)&&(be.delete(t),t.res.destroy())}As()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Nt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Ne(e),r=ct(n,g().sites),f=r.size?Xs({method:e.method,pathname:o,routes:r}):"other",h=Ys(o);if(f==="other")return(!r.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(f!=="pass")return{verdict:f};Y.visit(n,s,h);const l=await Te.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>qs(_,r,l?.name||l?.login),"sidebar")}}}u(Nt,"access");const Ut=Be.createServer((e,t)=>{Nt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Da(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Vs(e,t,n)})});function Da(e,t,n=null){const s=Ne(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ws(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(ot({method:e.method,pathname:s.pathname,search:s.search,dest:ys(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);xa(m,n).then(w=>Lt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:me()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=me();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Ct(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){ja(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],f=P(g().sites,r);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${B(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,l=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=ot({pathname:s.pathname,dest:"document",sites:o})==="shell";if(N(f)==="files"){if(!Rt(f.dir)){Ht(t,r,f);return}Ns(e,t,{dir:f.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Be.request({host:"127.0.0.1",port:f.port,method:e.method,path:l,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,r,f)}),e.pipe(y)}u(Da,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Nt(e);if(s!=="owner"){Zs(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Ne(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const f=`/${o}`;Ws({req:e,socket:t,head:n,port:r.port,path:e.url.slice(f.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${f}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${ht})`));
|
package/hub/pr-report.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var _=Object.defineProperty;var r=(e,n)=>_(e,"name",{value:n,configurable:!0});import{gh as f,PRIMARY as O}from"../shared/gh.mjs";const S="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",C="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=r((e=process.env)=>({...e,PATH:[e.PATH,C].filter(Boolean).join(":")}),"withPath"),w=r(e=>String(e).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");async function
|
|
1
|
+
var _=Object.defineProperty;var r=(e,n)=>_(e,"name",{value:n,configurable:!0});import{gh as f,PRIMARY as O}from"../shared/gh.mjs";const S="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",C="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=r((e=process.env)=>({...e,PATH:[e.PATH,C].filter(Boolean).join(":")}),"withPath"),w=r(e=>String(e).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");async function h(e,{env:n,account:s}={}){const a=await f(e,{env:g(n),account:s,timeout:9e4,maxBuffer:16777216});if(a.status!==0)throw Object.assign(new Error(w(a.stderr||`gh exited ${a.status}`)),{limited:a.limited});try{return JSON.parse(a.stdout)}catch(i){throw new Error(w(`unreadable output (${i.message})`))}}r(h,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(e){if(!e)return{env:process.env,account:O};const n=await f(["auth","token","-u",e],{env:g()}),s=n.status===0?n.stdout.trim():"";if(!s)throw new Error(`gh has no ${e} account; run gh auth login`);return{env:{...process.env,GH_TOKEN:s},account:e}}r(y,"asLogin");async function L({repo:e,pr:n,replyTo:s,words:a,login:i}){const l=await y(i),p=s===void 0?`repos/${e}/issues/${n}/comments`:`repos/${e}/pulls/${n}/comments/${s}/replies`;try{return(await h(["api",p,"-f",`body=${v}${a}
|
|
2
2
|
|
|
3
|
-
${b}`],
|
|
3
|
+
${b}`],l)).html_url}catch(u){throw new Error(`${e}#${n}: ${u.message}`)}}r(L,"postToPR");async function U({repo:e,pr:n,head:s,body:a,login:i}){const l=await y(i);try{return(await h(["api",`repos/${e}/pulls/${n}/reviews`,"-f",`body=${a}`,"-f","event=COMMENT","-f",`commit_id=${s}`],l)).html_url}catch(p){throw new Error(`${e}#${n}: ${p.message}`)}}r(U,"postReview");async function k(e){return(await h(["api","--paginate","--slurp",`repos/${e}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}r(k,"collaborators");async function q(e){try{return(await h(["api",`users/${encodeURIComponent(e)}`])).login}catch{return null}}r(q,"githubUser");async function F({repo:e,pr:n,since:s}){try{return(await h(["api","--paginate","--slurp",`repos/${e}/pulls/${n}/reviews`])).flat().filter(i=>Date.parse(i.submitted_at)>Date.parse(s)).map(i=>({login:i.user?.login??null,at:i.submitted_at}))}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}}r(F,"reviewsSince");async function j({repo:e,pr:n}){const s=await f(["pr","ready",String(n),"--repo",e],{env:g(),timeout:9e4});if(s.status!==0)throw new Error(w(s.stderr||`gh exited ${s.status}`))}r(j,"markReady");const M=r(e=>e.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function H({repo:e,pr:n,since:s,comments:a=!0}){let i;try{i=await h(["pr","view",String(n),"--repo",e,"--json",S])}catch(t){throw Object.assign(new Error(`${e}#${n}: ${t.message}`),{limited:t.limited})}const l=r(t=>h(["api","--paginate","--slurp",`repos/${e}/${t}`]).then(d=>d.flat()).catch(d=>{throw Object.assign(new Error(`${e}#${n}: ${d.message}`),{limited:d.limited})}),"list"),p=Promise.resolve([]),[u,c,m]=await Promise.all([a?l(`issues/${n}/comments?since=${encodeURIComponent(s)}`):p,a?l(`pulls/${n}/comments?since=${encodeURIComponent(s)}`):p,l(`pulls/${n}/reviews`)]);return{view:i,issueComments:u,reviewComments:c,reviews:m}}r(H,"fetchPR");const $="author { __typename login }",x=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles url mergedAt headRefName updatedAt latestReviews(first: 50) { nodes { state ${$} } } recent: commits(last: 20) { nodes { commit { oid messageHeadline } } } commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;async function A(e){const n=await f(["api","graphql","-f",`query=${e}`],{env:g(),timeout:9e4,maxBuffer:67108864});let s=null;try{s=JSON.parse(n.stdout)}catch{}if(s?.data)return s;throw new Error(`GitHub query: ${w(n.status!==0?n.stderr||`gh exited ${n.status}`:"unreadable output")}`)}r(A,"graphql");const E=r(e=>e?.login?{login:e.__typename==="Bot"?`${e.login}[bot]`:e.login}:null,"user");function D(e){const n=new Set(e.map(s=>s.in_reply_to_id).filter(Boolean));return e.filter(s=>!s.in_reply_to_id&&/codex/i.test(s.user?.login||"")&&!n.has(s.id)).length}r(D,"codexOpen");function P(e){let n=0,s=0;for(const a of e){const i=(a.conclusion||a.state||"").toUpperCase(),l=(a.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(i)?n++:(l&&l!=="COMPLETED"||i==="PENDING"||i==="EXPECTED")&&s++}return{total:e.length,fail:n,pending:s,state:n?"fail":s?"pending":e.length?"pass":"none"}}r(P,"checksOf");const I="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function B(e,n=[]){const s=e.map(({repo:u,pr:c},m)=>{const[t,d]=u.split("/");return`pr${m}: repository(owner: ${JSON.stringify(t)}, name: ${JSON.stringify(d)}) { pullRequest(number: ${Number(c)}) { ${x} } }`});n.forEach((u,c)=>{const[m,t]=u.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(m)}, name: ${JSON.stringify(t)}) { ${I} }`)});const{data:a,errors:i=[]}=await A(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),l=Object.fromEntries(n.flatMap((u,c)=>a[`open${c}`]?.pullRequests?[[u,a[`open${c}`].pullRequests.nodes]]:[])),p=e.map(({repo:u,pr:c},m)=>{const t=a[`pr${m}`]?.pullRequest;if(!t)return{error:`${u}#${c}: ${i.find(o=>o.path?.[0]===`pr${m}`)?.message||"GitHub did not answer for it"}`};const d=r(o=>({id:o.databaseId??null,user:E(o.author),created_at:o.createdAt??null,body:o.body??null,html_url:o.url??null}),"comment"),R=(t.reviewThreads?.nodes??[]).flatMap(o=>o.comments?.nodes??[]).map(o=>({...d(o),path:o.path??null,line:o.line??null,in_reply_to_id:o.replyTo?.databaseId??null}));return{view:{state:t.state,isDraft:t.isDraft,reviewDecision:t.reviewDecision,mergeable:t.mergeable,mergeStateStatus:t.mergeStateStatus,headRefOid:t.headRefOid,statusCheckRollup:t.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,changedFiles:t.changedFiles??null,approved:(t.latestReviews?.nodes??[]).some(o=>o.state==="APPROVED"),commits:(t.recent?.nodes??[]).map(o=>({oid:o.commit.oid.slice(0,10),subject:o.commit.messageHeadline})),url:t.url??null,mergedAt:t.mergedAt??null,headRefName:t.headRefName??null,updatedAt:t.updatedAt??null,reviews:Object.fromEntries((t.latestReviews?.nodes??[]).filter(o=>o.author?.login).map(o=>[o.author.login,o.state])),codexOpen:D(R)},issueComments:(t.comments?.nodes??[]).map(d),reviewComments:R,reviews:(t.reviews?.nodes??[]).map(o=>({id:o.databaseId??null,user:E(o.author),submitted_at:o.submittedAt??null,body:o.body??null,state:o.state??null,html_url:o.url??null}))}});return{login:a.viewer?.login??null,rate:a.rateLimit??null,results:p,open:l}}r(B,"fetchPRs");function J({view:e,issueComments:n,reviewComments:s,reviews:a},{pr:i,since:l,me:p}){const u=r((t,d)=>({kind:d,id:t.id??null,login:t.user?.login??null,at:t.created_at??null,body:t.body??null}),"comment"),c=[...n.map(t=>({...u(t,"issue_comment"),url:t.html_url??null})),...s.map(t=>({...u(t,"review_comment"),path:t.path??null,line:t.line??null,in_reply_to:t.in_reply_to_id??null,url:t.html_url??null})),...a.map(t=>({kind:"review",id:t.id??null,login:t.user?.login??null,at:t.submitted_at??null,body:t.body??null,state:t.state??null,url:t.html_url??null}))].filter(t=>t.at!=null&&t.at>l).map(t=>({...t,who:t.login===p?(t.body??"").includes(b)?"me":"user":/codex/.test(t.login)?"codex":/\[bot\]$|^linear/.test(t.login)?"bot":"reviewer"})).filter(t=>t.who!=="me"&&t.who!=="bot").sort((t,d)=>t.at<d.at?-1:t.at>d.at?1:0),m=e.statusCheckRollup??[];return{pr:Number(i),state:e.state??null,isDraft:e.isDraft??null,reviewDecision:e.reviewDecision??null,mergeable:e.mergeable??null,mergeStateStatus:e.mergeStateStatus??null,head:e.headRefOid?.slice(0,10)??null,failing:m.filter(t=>/FAILURE|ERROR/.test(t.conclusion??t.state??"")).map(t=>t.name??t.context??null),pending:m.filter(t=>(t.status??"COMPLETED")!=="COMPLETED").length,events:c,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.changedFiles??null,approved:!!e.approved,commits:e.commits??[],url:e.url??null,mergedAt:e.mergedAt??null,headRefName:e.headRefName??null,headRefOid:e.headRefOid??null,updatedAt:e.updatedAt??null,checks:P(m),reviews:e.reviews??{},codexOpen:e.codexOpen??0}}r(J,"reportOf");async function G(e){let n;try{n=await h(["pr","list","--repo",e,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${e} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(a=>a.path),requested:(s.reviewRequests||[]).map(a=>a.login).filter(Boolean)}))}r(G,"openPRs");async function K(e,n){let s;try{s=await h(["pr","view",String(n),"--repo",e,"--json","state,files"])}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}return{state:s.state??null,files:(s.files||[]).map(a=>a.path)}}r(K,"prFiles");async function Q({env:e}={}){const n=await h(["api","user"],{env:e});if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}r(Q,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,S as PR_VIEW_FIELDS,P as checksOf,M as checksPassed,k as collaborators,H as fetchPR,B as fetchPRs,Q as githubLogin,q as githubUser,j as markReady,G as openPRs,U as postReview,L as postToPR,K as prFiles,J as reportOf,F as reviewsSince};
|
package/hub/readiness.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as
|
|
1
|
+
var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as R}from"../shared/gh.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as N}from"./linear.mjs";import{slackStatus as P}from"./slack.mjs";import{readSettings as B,subscriptions as I}from"./push.mjs";import{tailscaleState as A}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const O={slack:"Reviewer DMs and digests wait on this."},y=[["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",null],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],E="Can't read replies in the review threads: add im:history to the Slack app, reinstall it and paste its new token.",L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const d=s((r,e)=>{switch(r){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}: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",...e.unheard?{note:E}:{}}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=H(a)),n?{ready:!1,missing:n,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:n,kind:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return y.filter(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const F=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function G({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=h({home:t,env:i,now:a,cacheMs:d}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>N(t),"linear"),slack:s(()=>P(t),"slack"),tailscale:s(()=>A({run:F(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:B(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&R({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(y.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??O[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(G,"readinessReader");const U=h(),Z=G({probes:{codex:U}});export{U as readCodex,Z as readiness,G as readinessReader,k as readinessRows};
|
package/hub/review-send.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=Object.defineProperty;var o=(t,r)=>R(t,"name",{value:r,configurable:!0});import _ from"node:fs";import N from"node:path";import{appendEvent as x,listBoards as D}from"./boards.mjs";import{savedSlack as H,slackStatus as M,refusalUnpushed as P,markRefusalPushed as T,missingScope as g,setHearing as k,hearingUnpushed as j,markHearingPushed as q,slackUserId as B}from"./slack.mjs";import{deliver as $}from"./push.mjs";import{reviewDue as I,queuePushes as U,postLink as A,postReply as E,recordReaction as W,postDigest as C,openDayThread as F,heardThreads as J,markHeard as K,boardPosts as L,ANSWER_MARK as O}from"../board/store.mjs";const v=o(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function z(t,r,p){await U(t.dir);const n=await I(t.dir,{now:p});if(n.waiting)return{sent:[],failed:[]};const h=[],c=[],s=o(async(e,a)=>{try{await a(),h.push(e)}catch(w){if(v(w))throw w;c.push(`${t.route}: ${e}: ${w.message}`)}},"attempt");for(const e of(n.digest?.reviewers||[]).filter(a=>a.owed))await s(`digest to ${e.name}`,async()=>{await C(t.dir,n.digest.date,e,{channel:e.sent?.channel||await r.openDM(e.slack_user),header:o(a=>`${t.title} digest: ${a} PR${a===1?"":"s"} pending your review`,"header"),post:o(a=>r.post(a),"post")})});const d=new Map,i=o((e,a)=>(d.has(e)||d.set(e,(async()=>F(t.dir,n.date,e,{channel:await a(),header:`${t.title} reviews`,post:o(w=>r.post(w),"post")}))()),d.get(e)),"dayThread");for(const e of n.links)await s(`link to ${e.reviewer} on ${e.pr}`,async()=>{const a=await i(e.reviewer,()=>r.openDM(e.slack_user));await A(t.dir,e,a,()=>r.post({channel:a.channel,thread_ts:a.ts,text:e.text}))});for(const e of n.replies)await s(`${e.id} to ${e.reviewer} on ${e.pr}`,async()=>{const a=e.thread_ts||(await i(e.reviewer,async()=>e.channel)).ts;await E(t.dir,e,()=>r.post({channel:e.channel,thread_ts:a,text:e.text}))});for(const e of n.reactions)await s(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await r.react({channel:e.channel,ts:e.ts,name:e.emoji}),await W(t.dir,e)});return{sent:h,failed:c}}o(z,"sendBoard");const G=o(t=>{try{return(JSON.parse(_.readFileSync(N.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function Q(t,r){return!P()||!t.some(G)?[]:(await $({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:r}),T(),["pushed that Slack refused the token"])}o(Q,"pushRefusal");async function V(t,r,p){const n=[],h=[],c=B(),s=new Map;for(const i of t)try{for(const e of await J(i.dir,p))s.set(e.channel,[...s.get(e.channel)||[],{board:i,thread:e}])}catch(e){h.push(`${i.route}: reading its day threads: ${e.message}`)}const d=o(async(i,e)=>{try{await e()}catch(a){if(g(a))throw a;h.push(`${i}: ${a.message}`)}},"attempt");for(const[i,e]of s)await d(`hearing DM ${i}`,async()=>{const a=e.map(u=>u.thread.ts).sort((u,f)=>Number(u)-Number(f))[0],w=new Map((await r.history({channel:i,oldest:a})).map(u=>[u.ts,u.latest_reply]));k(!0);for(const{board:u,thread:f}of e){const m=f.heard||f.ts;Number(w.get(f.ts))>Number(m)&&await d(`${u.route}: hearing ${f.reviewer}'s ${f.date} thread`,async()=>{const S=(await r.replies({channel:i,ts:f.ts,oldest:m})).filter(l=>Number(l.ts)>Number(m)),b=await L(u.dir);for(const l of S){if(!l.subtype&&!l.bot_id&&!b.has(l.ts)&&!(l.user===c&&l.text?.startsWith(O))){const y=l.user===c?"owner":"reviewer";await x(u,"slack-question",{reviewer:f.reviewer,channel:i,thread_ts:f.ts,ts:l.ts,from:y,answer:y==="reviewer"||/@kickflip\b/i.test(l.text||"")}),n.push(`${u.route}: slack-question from ${y==="owner"?"the owner":f.reviewer}`)}await K(u.dir,f,l.ts)}})}});return{sent:n,failed:h}}o(V,"hearBoards");async function X(t,r,p){try{return await V(t,r,p)}catch(n){return g(n)?(k(!1),j()?(await $({title:"Slack can\u2019t read the review threads",body:"Reviewers\u2019 messages to the board wait unheard until Hub\u2019s Slack app has im:history: add it, reinstall the app and paste its new token in Hub \u203A Integrations \u203A Slack. Sends go on.",url:"/_hub/settings#slack",tag:"slack-unheard"},{kind:"fault",at:p}),q(),{sent:["pushed that Slack can't read the review threads"],failed:[]}):{sent:[],failed:[]}):{sent:[],failed:[`hearing the day threads: ${n.message}`]}}}o(X,"hear");async function ne({boards:t=D(),fetch:r,now:p=new Date}={}){const n=H({fetch:r}),h=[],c=[];if(!n)return{sent:h,failed:c};for(const s of t)try{const d=await z(s,n,p);h.push(...d.sent.map(i=>`${s.route}: ${i}`)),c.push(...d.failed)}catch(d){if(c.push(`${s.route}: ${d.message}`),v(d))break}if(!M().refused)try{const s=await X(t,n,p);h.push(...s.sent),c.push(...s.failed)}catch(s){c.push(`unheard push: ${s.message}`)}try{h.push(...await Q(t,p))}catch(s){c.push(`refused-token push: ${s.message}`)}return{sent:h,failed:c}}o(ne,"sendReviews");export{ne as sendReviews};
|
package/hub/schedule.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var S=Object.defineProperty;var i=(t,e)=>S(t,"name",{value:e,configurable:!0});import{FOREVER as c,durationEnd as O,inRange as H}from"./duration.mjs";const w=Object.freeze({quiet:Object.freeze({start:"22:00",end:"08:00",hold:!0}),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),N="never",f=Object.freeze({present:1,away:5,lowPresent:5,lowAway:15}),A=1e3,Q=f.lowAway,h=/^([01]\d|2[0-3]):([0-5]\d)$/,m=f.away*6e4,d=i(t=>{const[e,n]=t.split(":").map(Number);return e*60+n},"minutes"),a=i((t,e,n=0)=>{const[o,r]=e.split(":").map(Number);return new Date(t.getFullYear(),t.getMonth(),t.getDate()+n,o,r)},"atOn"),y=i((t,e)=>{const n=a(t,e);return n>t?n:a(t,e,1)},"nextAt"),q=i((t,e)=>{const n=a(t,e);return n<=t?n:a(t,e,-1)},"lastAt"),x=i(t=>({quiet:{...t.quiet},notifications:{...t.notifications}}),"copy");function v({present:t,remaining:e=null}){const n=e!==null&&e<A;return t?n?f.lowPresent:f.present:n?f.lowAway:f.away}i(v,"checkEvery");function U({last:t,present:e,
|
|
1
|
+
var S=Object.defineProperty;var i=(t,e)=>S(t,"name",{value:e,configurable:!0});import{FOREVER as c,durationEnd as O,inRange as H}from"./duration.mjs";const w=Object.freeze({quiet:Object.freeze({start:"22:00",end:"08:00",hold:!0}),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),N="never",f=Object.freeze({present:1,away:5,lowPresent:5,lowAway:15}),A=1e3,Q=f.lowAway,h=/^([01]\d|2[0-3]):([0-5]\d)$/,m=f.away*6e4,d=i(t=>{const[e,n]=t.split(":").map(Number);return e*60+n},"minutes"),a=i((t,e,n=0)=>{const[o,r]=e.split(":").map(Number);return new Date(t.getFullYear(),t.getMonth(),t.getDate()+n,o,r)},"atOn"),y=i((t,e)=>{const n=a(t,e);return n>t?n:a(t,e,1)},"nextAt"),q=i((t,e)=>{const n=a(t,e);return n<=t?n:a(t,e,-1)},"lastAt"),x=i(t=>({quiet:{...t.quiet},notifications:{...t.notifications}}),"copy");function v({present:t,remaining:e=null}){const n=e!==null&&e<A;return t?n?f.lowPresent:f.present:n?f.lowAway:f.away}i(v,"checkEvery");function U({last:t,present:e,remaining:n=null,backAt:o=null}){return o&&o>t?new Date(o):new Date(t.getTime()+v({present:e,remaining:n})*6e4)}i(U,"nextCheckAt");function _(t){const e=t?.on??w.notifications.on,n=t?.weekends_off??w.notifications.weekends_off,o=t?.snoozed_until??null;return typeof e!="boolean"?{error:"Notifications must be on or off."}:typeof n!="boolean"?{error:"Off on weekends must be on or off."}:o!==null&&o!==c&&(typeof o!="string"||Number.isNaN(Date.parse(o)))?{error:"Snooze must end at a date and time, or be forever."}:{notifications:{on:e,weekends_off:n,snoozed_until:o===null||o===c?o:new Date(o).toISOString()}}}i(_,"validateNotifications");function b(t){const{start:e,end:n,hold:o=w.quiet.hold}=t?.quiet||{},{notifications:r,error:u}=_(t?.notifications);return u?{error:u}:typeof e!="string"||!h.test(e)?{error:"Quiet hours' start must be HH:MM (24-hour)."}:typeof n!="string"||!h.test(n)?{error:"Quiet hours' end must be HH:MM (24-hour)."}:e===n?{error:"Quiet hours must start and end at different times."}:typeof o!="boolean"?{error:"Hold pushes must be on or off."}:{settings:{quiet:{start:e,end:n,hold:o},notifications:r}}}i(b,"validate");function T(t){const e=(Array.isArray(t?.windows)?t.windows:[]).map(n=>n?.from).filter(n=>typeof n=="string"&&h.test(n)).sort((n,o)=>d(n)-d(o));return e.length<2?null:{start:e.at(-1),end:e[0],hold:t.hold_overnight!==!1}}i(T,"fromPolling");function W(t){const e=_(t?.notifications).notifications||x(w).notifications;return b({quiet:t?.quiet,notifications:e}).settings||b({quiet:T(t?.polling),notifications:e}).settings||{...x(w),notifications:e}}i(W,"quietOf");function Y(t,e){const n={...t.quiet};return"start"in e&&(n.start=e.start),"end"in e&&(n.end=e.end),"hold"in e&&(n.hold=e.hold==="on"),{quiet:n,notifications:t.notifications}}i(Y,"editQuiet");function $(t,e){const n=t.getHours()*60+t.getMinutes(),[o,r]=[d(e.quiet.start),d(e.quiet.end)];return o<r?n>=o&&n<r:n>=o||n<r}i($,"quietAt");function K(t,e){if(!$(t,e))return null;const n=q(t,e.quiet.start),o=(d(e.quiet.end)-d(e.quiet.start)+1440)%1440,r=new Date(n.getFullYear(),n.getMonth(),n.getDate(),n.getHours(),n.getMinutes()+o/2);return r<=t?r:null}i(K,"quietMiddleOf");function C(t,e){return e.quiet.hold&&$(t,e)&&t-q(t,e.quiet.start)>m}i(C,"holdsPushes");function D(t,e){if(!e.notifications?.weekends_off)return null;let n=a(t,e.quiet.start,-((t.getDay()-5+7)%7));n>t&&(n=a(n,e.quiet.start,-7));const o=a(n,e.quiet.end,3);return t-n>m&&t<o?o:null}i(D,"weekendEnd");function R(t,e){if(e.notifications?.on===!1)return{until:N,why:"off"};if(e.notifications?.snoozed_until===c)return{until:c,why:"snoozed"};const n=e.notifications?.snoozed_until?new Date(e.notifications.snoozed_until):null,o=i(s=>(n>s?["snoozed",n]:null)||D(s,e)&&["weekend",D(s,e)]||C(s,e)&&["overnight",y(s,e.quiet.end)],"at"),r=o(t);if(!r)return null;let u=r[1];for(let s=o(u),p=0;s&&p<8;s=o(u),p++)u=s[1];return{until:u,why:r[0]}}i(R,"hold");function j(t,e){return R(t,e)?.until??null}i(j,"heldUntil");function V(t,e){return j(t,e)??new Date(t.getTime()+f.away*6e4)}i(V,"flushAt");function G(t,e,n){const o=String(e);let r;if(o==="morning")r=y(t,n.quiet.end);else if(o==="tonight")r=y(t,n.quiet.start);else if(o==="monday")r=a(t,n.quiet.end,(8-t.getDay())%7),r<=t&&(r=a(r,n.quiet.end,7));else return O(t,o);return H(t,r)?r:null}i(G,"snoozeEnd");function B(t,e=new Date){if(t.why==="off")return"notifications are off";if(t.until===c)return"snoozed until you end it";const n=k(t.until,e);return t.why==="snoozed"?`snoozed until ${n}`:t.why==="weekend"?`held for the weekend until ${n}`:`held overnight until ${n}`}i(B,"holdLine");function F(t){return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`}i(F,"localDay");function J(t,e,n={}){const o=e.quiet.end;return a(t,o)<=t&&n?.[o]!==F(t)?[o]:[]}i(J,"wakesDue");const I=i(t=>/^\/([^/?#]+)(?:[/?#]|$)/.exec(t||"")?.[1]??null,"routeOf");function X(t,e={}){const n=new Map;t.forEach((r,u)=>{r.tag&&n.set(r.tag,u)});const o=new Map;return t.filter((r,u)=>!r.tag||n.get(r.tag)===u).forEach(r=>{const u=I(r.url)??Symbol(r.id);o.set(u,[...o.get(u)||[],r])}),[...o].flatMap(([r,u])=>{if(u.length===1){const[l]=u;return[{title:l.title,body:l.body||"",url:l.url||"/",tag:l.tag||void 0,kind:l.kind,members:[l.id]}]}const s=e[r];if(s&&!s.count)return[];const p=r==="_hub"?"Hub":r,g=u.map(l=>l.title),z=s?`${s.count} need${s.count===1?"s":""} you on ${p}`:`${u.length} updates on ${p}`,E=s?s.summary:[...g.slice(0,2),...g.length>2?[`and ${g.length-2} more`]:[]].join(" \xB7 "),M=u.every(l=>l.kind==="action")?"action":u.find(l=>l.kind!=="action").kind;return[{title:z,body:E,url:`/${r}/`,tag:`summary:${r}`,kind:M,members:u.map(l=>l.id)}]})}i(X,"notifications");const Z=i((t,e=new Date)=>t===c?"once the snooze ends":k(t,e),"sendWords");function k(t,e=new Date){const n=t.getHours(),o=`${n%12||12}:${String(t.getMinutes()).padStart(2,"0")}${n<12?"am":"pm"}`,r=Math.round((a(t,"00:00")-a(e,"00:00"))/864e5);return r===0?o:r===1?`${o} tomorrow`:`${o} ${t.toDateString().slice(0,10)}`}i(k,"clock");export{f as CHECK_MINUTES,w as DEFAULTS,A as LOW_POINTS,N as NEVER,Q as SLOWEST_CHECK_MINUTES,v as checkEvery,k as clock,Y as editQuiet,V as flushAt,T as fromPolling,j as heldUntil,R as hold,B as holdLine,C as holdsPushes,F as localDay,U as nextCheckAt,X as notifications,$ as quietAt,K as quietMiddleOf,W as quietOf,Z as sendWords,G as snoozeEnd,b as validate,J as wakesDue};
|
package/hub/slack.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),
|
|
1
|
+
var O=Object.defineProperty;var n=(e,t)=>O(e,"name",{value:t,configurable:!0});import m from"node:fs";import p from"node:path";import w from"node:os";import{withFileLock as $}from"./locks.mjs";import{readSecret as D,writeSecret as d}from"./secrets.mjs";const f=["self_dm","owner_slack_user","slack_domain","timezone"],k=n(()=>p.join(w.homedir(),".claude","kickflip","slack.json"),"fileOf"),S=n(e=>{try{return JSON.parse(m.readFileSync(e,"utf8"))||{}}catch{return{}}},"read"),y=n(e=>Object.fromEntries(f.map(t=>[t,typeof e?.[t]=="string"?e[t]:""])),"identityOf");function g(e){const t=k();m.mkdirSync(p.dirname(t),{recursive:!0});const r=`${t}.${process.pid}.tmp`;return m.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,{mode:384}),m.chmodSync(r,384),m.renameSync(r,t),e}n(g,"save");function x(){const e=y(S(k()));if(f.some(r=>e[r]))return e;const t=y(S(p.join(w.homedir(),".claude","slack-bridge","state.json")).config);return f.some(r=>t[r])?g(t):e}n(x,"readSlack");async function N(e){for(const[t,r]of Object.entries(e)){if(!f.includes(t)||typeof r!="string")throw new Error(`Unknown Slack setting ${t}`);if(t==="self_dm"&&r&&!/^D[A-Z0-9]+$/.test(r))throw new Error("self-dm must be a DM channel id");if(t==="owner_slack_user"&&r&&!/^U[A-Z0-9]+$/.test(r))throw new Error("user must be a Slack user id");if(t==="slack_domain"&&r&&!/^[a-z0-9][a-z0-9-]*$/.test(r))throw new Error("domain must be a Slack workspace domain");if(t==="timezone"&&r)try{new Intl.DateTimeFormat("en",{timeZone:r})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return $(k(),()=>g({...x(),...e}))}n(N,"setSlack");function B(e=new Date){const t=x().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,r=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(e).map(({type:c,value:u})=>[c,u]));return{date:`${r.year}-${r.month}-${r.day}`,open:!["Sat","Sun"].includes(r.weekday)&&Number(r.hour)>=9&&Number(r.hour)<17}}n(B,"slackDay");class h extends Error{static{n(this,"SlackError")}constructor(t,r=502){super(t),this.status=r}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),I=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function v(e,{fetch:t=globalThis.fetch,checked:r=n(()=>{},"checked")}={}){async function c(a,i={}){let s;try{const o=await t(`${E()}/${a}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${e}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(s=await o.json().catch(()=>null),!s)throw new Error(`Slack answered ${o.status}`)}catch(o){throw new h(`Could not reach Slack: ${o.message}`)}if(I.includes(s.error))throw r(!1),new h("Slack refused the token.",400);if(r(!0),!s.ok)throw new h(`Slack: ${s.error}`);return s}return n(c,"call"),{async who(){const{team:a,team_id:i,user:s,user_id:o}=await c("auth.test");return{team:a,team_id:i,user:s,user_id:o}},async people(){const a=[];let i="";do{const s=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const o of s.members)!o.deleted&&!o.is_bot&&o.id!=="USLACKBOT"&&a.push({id:o.id,name:o.profile?.real_name||o.real_name||o.name});i=s.response_metadata?.next_cursor||""}while(i);return a.sort((s,o)=>s.name.localeCompare(o.name))},openDM:n(async a=>(await c("conversations.open",{users:a})).channel.id,"openDM"),post:n(async({channel:a,text:i,thread_ts:s})=>(await c("chat.postMessage",{channel:a,text:i,unfurl_links:!1,...s?{thread_ts:s}:{}})).ts,"post"),async react({channel:a,ts:i,name:s}){try{await c("reactions.add",{channel:a,timestamp:i,name:s})}catch(o){if(o.message!=="Slack: already_reacted")throw o}},history:n(({channel:a,oldest:i})=>u("conversations.history",{channel:a,oldest:i,inclusive:!0}),"history"),replies:n(({channel:a,ts:i,oldest:s})=>u("conversations.replies",{channel:a,ts:i,...s?{oldest:s}:{}}),"replies")};async function u(a,i){const s=[];let o="";do{const _=await c(a,{...i,limit:200,...o?{cursor:o}:{}});s.push(..._.messages),o=_.response_metadata?.next_cursor||""}while(o);return s}n(u,"pages")}n(v,"slackClient");const K=n(e=>e.message==="Slack: missing_scope","missingScope"),l=n(e=>{const t=D("slack",e);return typeof t?.token=="string"&&t.token?t:null},"savedToken");function b(e){const t=l(e);return t?{set:!0,...t.refused_at?{refused:!0}:{},...t.unheard_at?{unheard:!0}:{},...t.team?{team:t.team,user:t.user}:{}}:{set:!1}}n(b,"slackStatus");const L=n(()=>l()?.user_id,"slackUserId"),P="Slack needs Hub\u2019s Slack token; add it in Hub \u203A Integrations \u203A Slack first.";async function Z(e,{fetch:t}={}){const r=typeof e=="string"?e.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(r))throw new h("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let u=!1;const a=await v(r,{fetch:t,checked:n(i=>{u=!i},"checked")}).who().catch(i=>{if(!u)throw i;return null});return d("slack",a?{token:r,...a,saved_at:c}:{token:r,saved_at:c,refused_at:c}),b()}n(Z,"saveSlackToken");function H(){return d("slack",null),b()}n(H,"removeSlackToken");function T(e,t){const r=l();if(r?.token!==e||!!r.refused_at==!t)return;const{refused_at:c,refusal_pushed_at:u,...a}=r;d("slack",t?a:{...a,refused_at:new Date().toISOString()})}n(T,"markChecked");const J=n(()=>{const e=l();return!!(e?.refused_at&&!e.refusal_pushed_at)},"refusalUnpushed");function M(){const e=l();e?.refused_at&&d("slack",{...e,refusal_pushed_at:new Date().toISOString()})}n(M,"markRefusalPushed");function R(e){const t=l();if(!t||!!t.unheard_at==!e)return;const{unheard_at:r,unheard_pushed_at:c,...u}=t;d("slack",e?u:{...u,unheard_at:new Date().toISOString()})}n(R,"setHearing");const q=n(()=>{const e=l();return!!(e?.unheard_at&&!e.unheard_pushed_at)},"hearingUnpushed");function G(){const e=l();e?.unheard_at&&d("slack",{...e,unheard_pushed_at:new Date().toISOString()})}n(G,"markHearingPushed");function Q({fetch:e}={}){const t=l();return t?v(t.token,{fetch:e,checked:n(r=>T(t.token,r),"checked")}):null}n(Q,"savedSlack");export{P as SLACK_NEEDS_TOKEN,h as SlackError,q as hearingUnpushed,G as markHearingPushed,M as markRefusalPushed,K as missingScope,x as readSlack,J as refusalUnpushed,H as removeSlackToken,Z as saveSlackToken,Q as savedSlack,R as setHearing,N as setSlack,v as slackClient,B as slackDay,b as slackStatus,L as slackUserId};
|
package/hub/views.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var D=Object.defineProperty;var i=(e,t)=>D(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as L,FOREVER as w,durationEnd as V,endOf as K}from"./duration.mjs";import{siteIcon as F}from"./icons.mjs";import{codexStatus as M}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),H=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),p=i(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),c(o,o)},"timeOfDay"),E=i((e,t)=>e.hold?v(e.hold,t):"pushes go out after each check","deviceLine"),W=i(e=>`${p(e.quiet.start)} \u2013 ${p(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),B="checks every minute while you're here, every 5 when away";function Z(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(u=>!u.ok),l=o.filter(u=>u.state==="broken"),r=t.find(u=>u.link==="events"),a=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",d=l.length?`degraded \xB7 ${a}`:o.length?`up \xB7 ${a} \xB7 ${o.length} settling`:`up \xB7 ${a} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:d,lines:o.map(u=>({link:u.link,detail:`${u.state==="broken"?"":"settling: "}${u.detail}`}))}}i(Z,"siteHealth");const x=[{value:"always",label:"Always on",description:i(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:i(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:i(e=>`${e} implements and reviews.`,"description")}],j=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:x.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?x.find(o=>o.value===e).description(A[n]):t.words,fix:t.fix}),"codexView"),A={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},q=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],J=i(e=>({model:e,builder:A[e],choices:q.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:q.find(t=>t.value===e).description}),"claudeView");function re({now:e,st:t,sidebar:n,rows:o,links:l=new Map,code:r={},host:a="",version:d,rules:u=[],remembered:y=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:I=M({installed:!1}),awake:T={on:!1,unsupported:null}}){const g=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),P=O?` \xB7 runs the code in its files (${r.onDisk})`:"",z=i(s=>({route:s.route,title:s.title,icon:F(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:y.get(s.route)||[],restart:g.has(s.route)&&s.restart===!0}),"site"),R=[["Boards",o.filter(s=>g.has(s.route))],["Pages",o.filter(s=>!g.has(s.route))]].filter(([,s])=>s.length).map(([s,U])=>({label:s,sites:U.map(z)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${P}`,restart:!O},awake:T,groups:R,integrations:f,usage:n.usage,pause:n.pause,codex:j(h,I,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:E(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?H(u.length,"rule"):"None yet",host:a,version:d,total:n.total}}i(re,"hubView");const ae="That snooze ends in the past or more than 30 days on. Pick another time.",C="until you end it",N="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),_=i((e,t)=>L.map(([n,o])=>({value:n,label:o,until:$(V(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?K(e.snoozed_until,t):null,"snoozedUntil");function le({now:e,st:t,rules:n=[],linear:o={set:!1},slack:l={set:!1},pausedUntil:r=null}){const a=t.settings,d=a.notifications,u=Q(d,e),y=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,C)}`,h=r&&`Paused ${$(r,e,N)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:_(e,N)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:E(t,e),on:{on:d.on,sub:d.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${y} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:_(e,C)}:null,weekends:{on:d.weekends_off,sub:`Pushes wait from Friday ${p(a.quiet.start)} until Monday ${p(a.quiet.end)}. Urgent ones still go.`},quiet:ee(a,e),rules:n,linear:G(o),slack:Y(l)}}i(le,"settingsView");const X={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:history","im:write","reactions:write","users:read"]}}},b={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(X))}`,label:"a new Slack app"};function Y(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${b.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:b,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:b,summary:"No token"}}i(Y,"slackWords");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function G(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}i(G,"linearWords");function ee(e,t){const n=e.quiet,[o,l]=[p(n.start),p(n.end)],r=m(t,"tonight",e),a=[["Snooze tonight",`Until ${c(r,t)}${n.hold?`, then quiet hours hold pushes until ${c(m(r,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${c(m(t,"morning",e),t)}.`],["Snooze monday",`Until ${c(m(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${l}.`:"Off."],["Daily wake",`Every board wakes once at ${l}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:W(e),hint:B,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${l}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:a.map(([d,u])=>({title:d,sub:u}))}}i(ee,"quietView");function de(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}i(de,"flushView");export{B as QUIET_HINT,ae as SNOOZE_REFUSED,ue as capital,_ as durationChoices,de as flushView,re as hubView,H as plural,W as quietSummary,le as settingsView,Z as siteHealth};
|