@lovek4d/kickflip 0.5.0 → 0.5.3
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 +47 -42
- package/README.md +1 -1
- package/SKILL.md +16 -16
- package/bin/kickflip.mjs +26 -17
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -0
- package/board/bin/board.mjs +65 -18
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +5 -4
- package/board/on-host.mjs +1 -1
- package/board/public/app-asks.js +6 -6
- package/board/public/app-banner.js +7 -13
- package/board/public/app-cards.js +51 -27
- package/board/public/app-core.js +26 -8
- package/board/public/app-header.js +2 -1
- package/board/public/app-lanes.js +55 -23
- package/board/public/app-needs.js +129 -4
- package/board/public/app-reader.js +1 -1
- package/board/public/app-snapshot.js +6 -5
- package/board/public/app-threads.js +70 -59
- package/board/public/app.js +38 -8
- package/board/public/attach.js +2 -2
- package/board/public/dialogs.js +9 -39
- package/board/public/hub-kit.js +4 -2
- package/board/public/linear-key.js +46 -35
- package/board/public/new-task.js +17 -38
- package/board/public/prototype-kit.js +28 -3
- package/board/public/prototype-switcher.js +42 -107
- package/board/public/shell.js +7 -5
- package/board/public/sidebar.js +44 -5
- package/board/public/style.css +108 -37
- package/board/public/ui-cards.js +41 -30
- package/board/public/ui-header.js +2 -2
- package/board/public/ui-order.js +12 -10
- package/board/public/ui-questions.js +25 -38
- package/board/public/ui-sidebar.js +18 -12
- package/board/public/ui-threads.js +3 -3
- package/board/public/ui.js +40 -8
- package/board/public/view-hub-settings.js +11 -10
- package/board/public/view-hub.js +17 -3
- package/board/public/view-questions.js +142 -199
- package/board/public/view-settings.js +191 -72
- package/board/server.mjs +19 -20
- package/board/store.mjs +20 -15
- package/board/transcript.mjs +2 -0
- package/board/worktree.mjs +24 -20
- package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
- package/hooks-plugin/hooks/hooks.json +9 -0
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +5 -5
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/linear.mjs +1 -2
- package/hub/live.mjs +2 -2
- package/hub/locks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/presence.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +1 -2
- package/hub/review-send.mjs +1 -0
- package/hub/secrets.mjs +2 -0
- package/hub/self-restart.mjs +2 -0
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -0
- package/hub/slack.mjs +2 -2
- package/hub/stream.mjs +10 -13
- package/hub/try.mjs +6 -6
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +2 -1
- package/shared/audit.mjs +4 -0
- package/shared/fake-gh.mjs +17 -4
- package/shared/service.mjs +4 -4
- package/shared/usage-gate.mjs +1 -1
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Bt=Object.defineProperty;var l=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ft from"node:net";import{spawn as Be,execFileSync as Nt}from"node:child_process";import b from"node:fs";import k from"node:path";import Fe from"node:os";import{fileURLToPath as Gt}from"node:url";import Ne from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Wt,removeSubscription as Kt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ge,queueStatus as ze,saveSettings as Qt,readSettings as q,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as We,answerOf as sn,expired as rn,pendingFor as Ke,promptRecord as on,pushOf as cn,updatePrompts as B,shown as ln,pruneRemembered as we,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as Ze,nextCheckAt as gn,LOW_POINTS as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as Sn,flushView as kn,SNOOZE_REFUSED as $n,AWAKE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as Tn,awakeUntil as Qe,awakeUnsupported as vn}from"./awake.mjs";import{checkGitHub as On,sendWakes as jn}from"./github.mjs";import{checkIncoming as Pn}from"./incoming.mjs";import{listBoards as E,isListening as Rn,isBoardSite as be,listenState as Xe,RESUME_STATE as En,sendToBoards as Ye,checkListening as Dn,recordHubCode as Ln,hubCode as Cn,runningHubCode as An,boardCode as qe,recordLinkFaults as In,linksOf as Hn,hubLink as Mn,hubCodeState as Un,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Bn,applyRules as Fn,changed as nt,readHubRules as ye,writeHubRules as Nn}from"./rules.mjs";import{siteIcon as Gn}from"./icons.mjs";import{readNeeds as zn,sidebarSites as Jn,startRoute as Wn,hubHealth as Kn}from"./home.mjs";import{checkSessions as Vn,compactFullContexts as Zn,startingBoards as Qn}from"./sessions.mjs";import{repoService as Xn,repoLookupRoute as Yn}from"./repos.mjs";import{boardRepo as at,changeBoardRepo as qn,migrateBoardRepos as ea,migrateHubBoard as ta}from"./board-repo.mjs";import{requestBoard as na,retryBoard as aa,dismissBoard as sa,clonePercent as ra,settleStarts as oa}from"./new-board.mjs";import{boardClosure as ia,closeBoard as ca,closedBoards as la,reopenBoard as ua}from"./close-board.mjs";import{restartBoard as da,restartable as ha}from"./restart-board.mjs";import{crossSite as fa,isJSON as Se}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as ke,appFile as pa,colorsVersion as st,destOf as ma,fileVersion as ga,pageOf as rt,shellHtml as wa}from"./shell.mjs";import{hubStream as ba}from"./stream.mjs";import{usageReader as ya,checkUsagePause as Sa,contextReading as ka}from"./usage.mjs";import{readUsageGate as ot,setPauseAt as $a,setKeepGoing as _a}from"../shared/usage-gate.mjs";import{readiness as xa,readCodex as ee}from"./readiness.mjs";import{BuildError as Ta,startBuild as va,stopTries as Oa,endUnsharedTries as ja,tryTick as $e,initializeTries as Pa,fetchTryBranches as it,pullNow as Ra,pullMains as Ea}from"./try.mjs";import{serveFiles as Da}from"./files.mjs";import{upgrader as La,CHECK_MS as Ca}from"./upgrade.mjs";import{restarter as Aa}from"./self-restart.mjs";import{fallBackFromLinear as Ia,openBoardBuilders as Ha}from"../board/store.mjs";import{recordRefusal as Ma}from"../board/permissions.mjs";import{hubGate as _e}from"../board/on-host.mjs";import{pipeUpgrade as Ua,refuse as Ba,refuseUpgrade as Fa}from"./front-door.mjs";import{shareOf as Na,sharedRoutes as ct,shareReach as Ga,routeOfPath as za,sharedSidebar as Ja,shareTracker as Wa}from"./share.mjs";import{restartRunner as lt}from"../board/worktree.mjs";import{hubService as Ka}from"../shared/service.mjs";import{cloneRoot as xe}from"../shared/repos.mjs";import{LinearError as Te,linearStatus as te,removeLinearKey as Va,saveLinearKey as Za,savedClient as ut}from"./linear.mjs";import{SlackError as Qa,slackStatus as Xa,removeSlackToken as Ya,saveSlackToken as qa}from"./slack.mjs";import{sendReviews as es}from"./review-send.mjs";const F=k.dirname(Gt(import.meta.url)),ts=k.join(F,"..","shared","colors.css"),dt=JSON.parse(b.readFileSync(k.join(F,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),N=k.join(Fe.homedir(),".claude","kickflip"),ht=k.join(N,"manifest.json"),ne=k.join(N,"logs"),ft=k.join(N,"run"),ve=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[N,ne,ft])b.mkdirSync(e,{recursive:!0});try{const e=Ln();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ea();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 ta();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const C=Xn(),ns=ya(),ae=xn({log:c});ae.apply();let se={version:1,sites:{}};function g(){try{se=JSON.parse(b.readFileSync(ht,"utf8")),se.sites||={}}catch(e){e.code==="ENOENT"?se={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return se}l(g,"loadManifest");const j=new Map,A=new Set;let re=!1;const pt=5e3,oe=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Oe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),G=l(e=>Oe(e)?"cmd":e?.port?"port":"files","servesOf"),z=new Map;function ie(e){return k.join(ft,e+".pid")}l(ie,"pidFile");function as(e,t){try{const n=Number(b.readFileSync(ie(e),"utf8")),s=n?Nt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ie(e))}catch{}}l(as,"killStale");function ss(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(ne,e+".log"),"a"),[o,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=
|
|
5
|
-
`),w.closeSync(r),Object.assign(n,{child:null,key:ae(t),state:"crashed",exitCode:null,nextStart:Date.now()+ot(n.restarts++)}),O.set(e,n);return}w.closeSync(r),w.writeFileSync(se(e),String(i.pid)),Object.assign(n,{child:i,key:ae(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),O.set(e,n),c(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const S=l((k,y)=>{if(C.delete(i),n.child!==i)return;try{w.unlinkSync(se(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:k??y,nextStart:Date.now()+ot(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${k??y})`)},"ended");i.on("error",k=>{c(`${e} spawn error: ${k.message}`),i.pid||S(k.code,null)}),i.on("exit",S)}l(Ha,"start");const ot=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function re(e){const t=O.get(e);if(!t||!t.child){O.delete(e);return}t.stopping=!0;const n=t.child;oe(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&oe(n,"SIGKILL")},rt)}l(re,"stop");const oe=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function ke(e){return new Promise(t=>{const n=Et.connect({port:e,host:"127.0.0.1"}),r=l(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}l(ke,"probe");let it=!0;const A=new Set;let $e=!1;const U=new Set,Ia=3e4;async function Ma(e){const t=Date.now()+Ia;for(;;){const n=j(g().sites,e),r=O.get(e);if(n&&r?.child&&r.up&&r.key===ae(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}l(Ma,"serverAnswers");async function ct(){if(ne)return;const{sites:e}=g();for(const t of[...O.keys()])A.has(t)||(j(e,t)?Se(j(e,t))||(c(`${t} no longer has a command; stopping it`),re(t),O.delete(t)):(c(`${t} left the manifest; stopping it`),re(t),O.delete(t)));for(const t of[...F.keys()])B(j(e,t))!=="port"&&F.delete(t);for(const[t,n]of Object.entries(e))B(n)==="port"&&F.set(t,await ke(n.port));for(const[t,n]of Object.entries(e)){if(!Se(n))continue;it&&Aa(t,n);const r=O.get(t);if(r&&r.child&&r.key!==ae(n)){c(`${t} entry changed; restarting it`),re(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}const a=r?.child&&r.code&&!r.stopping&&!U.has(t)&&!A.has(t)?We(n):null;if(a&&a!==r.code){if(!(await qe(n.dir)).length){c(`${t} runs older code than its files; restarting it`),re(t),Object.assign(r,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}r.waitedOn!==a&&c(`${t} runs older code than its files; restarts once no builder runs`),r.waitedOn=a}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&Ha(t,n)}it=!1;for(const[t,n]of O){const r=j(e,t);r&&(n.up=n.child?await ke(r.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(ct,"reconcile");const Ba=setInterval(()=>ct().catch(e=>c("reconcile failed:",e.message)),2e3);ct().catch(e=>c("reconcile failed:",e.message));try{Ft()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const ut=l(async()=>{try{for(const e of await xa())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await ut();const Fa=6e4,lt=60*6e4,b={running:null,startedAt:0,firstAt:Date.now()+Fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0},H=Kt(),dt=l(()=>b.startedAt?rn({last:new Date(b.startedAt),...H.state(),remaining:b.remaining}):new Date(b.firstAt),"nextPollAt");function ht({changed:e,back:t}){e&&_.refresh(),t&&b.startedAt&&!b.running&&_e().catch(n=>c("poll failed:",n.message))}l(ht,"presenceChanged");function _e({flush:e=!0}={}){if(b.running)return b.running;const t=new Date;return b.startedAt=t.getTime(),b.running=(async()=>{const n=V();let r;try{r=await wn({every:Fe({present:!1,remaining:b.remaining}),manifest:g()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}b.remaining=r.rate?.remaining??null,r.merged.length&&(await Ye({manifest:g(),log:c,routes:r.merged}),c(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),we({manifest:g(),routes:r.merged}).catch(a=>c("merge refresh failed:",a.message))),await Ye({manifest:g(),log:c});try{const a=await Sa({manifest:g()});a.length&&we({manifest:g(),routes:a}).catch(d=>c("pull refresh failed:",d.message))}catch(a){c("pull failed:",a.message)}try{r.failed.push(...(await yn({manifest:g()})).failed)}catch(a){r.failed.push(`incoming PRs: ${a.message}`)}r.failed.length&&Date.now()-b.errorLoggedAt>lt&&(b.errorLoggedAt=Date.now(),c(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&c(`github check: ${r.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${r.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:d}of r.linear.moved)c(`linear: ${a} \u2192 ${d}`);r.linear.failed.length&&Date.now()-b.linearErrorLoggedAt>lt&&(b.linearErrorLoggedAt=Date.now(),c(`linear check: ${r.linear.failed.length} step${r.linear.failed.length===1?"":"s"} failed, first: ${r.linear.failed[0]} (logged at most hourly)`));try{r.deaf=await $n({manifest:g()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&c(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{vn(g())}catch(a){c("could not record link faults:",a.message)}if(r.flushed=null,r.held=nn(t,n)!==null,e&&!r.held)try{const a=await Ce();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},c(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){c("push queue flush failed:",a.message)}return b.last=r,r})().finally(()=>{b.running=null}),b.running}l(_e,"runPoll");let ft=null;function Ua(){ft||=bn({settings:V(),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(()=>{ft=null})}l(Ua,"runWakes");const xe=new Set,D={running:null};function pt(){if(D.running||A.size||U.size)return;const e=new Set(xe);xe.clear(),D.running=Mn({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(()=>Bn({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(()=>{D.running=null})}l(pt,"runSessionCheck"),await ba(g());const Na=Date.now(),N=new Map;function G(e){e?(ce.delete(e),_.changed(e)):_.refresh()}l(G,"promptChanged");async function mt(){const{gone:e,rulesChanged:t}=await M(n=>{const r=Zt(n,{now:Date.now(),aliveSince:Na}),a=fe(n,ve());for(const d of r)delete n.prompts[d.id];return{gone:r,rulesChanged:a}});for(const n of e)G(n.route);t&&_.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(mt,"prunePrompts");const ve=l(()=>({running:Je(),boards:R(g())}),"promptContext");async function gt(e){const t=R(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await va(Ve(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(gt,"noteRefusal");let z=null,wt=null;async function Te(){return z?wt??=z.catch(()=>{}).then(()=>(wt=null,Te())):(z=(async()=>la({gate:Xe(),resume:l(async e=>{const t=await ze(R(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{z=null}),z)}l(Te,"checkUsageGate"),setInterval(()=>{we({manifest:g()}).catch(e=>c("try tick failed:",e.message)),W.tick().catch(e=>c("kickflip update tick failed:",e.message)),Te().catch(e=>c("usage gate failed:",e.message)),H.check().then(ht,e=>c("presence check failed:",e.message)).finally(()=>{!b.running&&Date.now()>=dt().getTime()&&_e().catch(e=>c("poll failed:",e.message))}),Qn({listening:l(e=>Sn(e)&&ie().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&_.refresh()},e=>c("settling starts failed:",e.message)),Ua(),ee.apply(),mt().catch(e=>c("prompt expiry failed:",e.message)).finally(pt)},15e3);function bt(){if(ne)return;ne=!0,clearInterval(Ba),ee.stop(),c("hub stopping; stopping sites");for(const n of O.values())n.child&&(n.stopping=!0);for(const n of C)oe(n,"SIGTERM");let e=!1;wa().finally(()=>{e=!0});const t=Date.now()+rt;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)c(`pid ${n.pid} ignored SIGTERM; killing it`),oe(n,"SIGKILL");process.exit(0)}},50)}l(bt,"shutdown"),process.on("SIGTERM",bt),process.on("SIGINT",bt);const yt=new Map;function St(e,t={n:3e3}){let n=0,r;try{r=w.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of r){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const d=$.join(e,a.name);if(a.isDirectory())n=Math.max(n,St(d,t));else try{n=Math.max(n,w.statSync(d).mtimeMs)}catch{}}return n}l(St,"newestMtime");function Ga(e,t){const n=yt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=St(t.dir);return yt.set(e,{at:r,checked:Date.now()}),r}l(Ga,"changedAt");const kt=l(e=>{try{return typeof e=="string"&&w.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ie(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=O.get(t)||{},a=B(n),d=a==="files"?kt(n.dir)?"up":"down":a==="port"?F.has(t)?F.get(t)?"up":"down":"starting":r.child?r.up?"up":"starting":r.state==="crashed"?"down":r.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:a,state:d,exitCode:r.exitCode??null,owner:n.owner||null,restart:na(n),changedAt:Ga(t,n)||null,created:n.created||null,log:$.join(q,t+".log")}})}l(ie,"status");const I=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),ce=new Map,$t=2e4,za=1500;async function Wa(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(za)});return t.ok?Cn(await t.json()):null}catch{return null}}l(Wa,"fetchNeeds");async function _t(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const d=ce.get(a.route);(t===!0||t?.has?.(a.route)||!d||Date.now()-d.at>=$t)&&ce.set(a.route,{at:Date.now(),needs:Wa(a.port)})}const r=await Promise.all(n.map(a=>ce.get(a.route).needs));return new Map(n.map((a,d)=>[a.route,r[d]]))}l(_t,"waitingCounts");const xt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Me(null).length,"badgeTotal"),Oe=new Map;function Ja(e){const t=new Map,n=new Map,r={running:Je(),wakes:Pn(kn,{})};for(const a of e)try{const d=Ge(a,r);n.set(a.route,d);let h=Oe.get(a.route);(!h||h.listen!==d||Date.now()-h.at>=$t)&&(h={at:Date.now(),listen:d,links:Tn(a,{listen:d})},Oe.set(a.route,h)),t.set(a.route,h.links)}catch(d){c(`links of ${a.route} failed:`,d.message),t.set(a.route,[])}return{links:t,listens:n}}l(Ja,"boardLinks");async function ue({fresh:e=!1}={}){const t=new Date,n=ie(),r=await _t(n,{fresh:e}),a=g(),d=R(a),{links:h,listens:i}=Ja(d),S=jn(),k=new Map(d.map(u=>[u.route,da(u.owner?.session)]).filter(([,u])=>u)),y=Fn(a,t.getTime(),Zn,u=>i.get(u)==="listening"&&n.some(f=>f.route===u&&f.state==="up")),m=new Set(y.map(u=>u.route)),p=An(n,r,new Set(d.map(u=>u.route)),i,k).filter(u=>!m.has(u.route)),v=V(),s=tn(t,v);return{data:{code:S.running??null,total:xt(r),start:Hn(p),sites:p,hub:In(p,h,On(S)),home:w.realpathSync(De.homedir()),starting:y,usage:Ca({codexShare:He(Z())}),pause:Xe(),upgrade:W.state(),notifications:s?s.why==="off"?{state:"off",text:"Off"}:{state:"held",text:cn(an(s,t))}:{state:"on",text:"On"},away:!H.state().present},links:h,rows:n,code:S}}l(ue,"sidebarState");function vt(e,t,n,r,a){let d=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};d.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(d=Ct.gzipSync(d),h["content-encoding"]="gzip"),h["content-length"]=d.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:d)}l(vt,"sendText");async function Ka(e){const{data:t}=await ue(),n=Le.createContext({});for(const r of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Le.runInContext(w.readFileSync($.join(ge,r),"utf8"),n);return ia({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:le(180)})}l(Ka,"shellPage");const W=$a({root:tt(),builders:l(async()=>(await Promise.all(R(g()).map(e=>qe(e.dir)))).flat(),"builders"),run:Va,log:c,changed:l(()=>_.refresh(),"changed")});function Va(){const e=$.join(q,"kickflip-update.log");w.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
-
`);const t=
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${
|
|
8
|
-
<title>${
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ze()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Qa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Xa=l(()=>({running:!!b.running,nextAt:dt(),last:b.last,present:H.state().present,every:Fe({present:H.state().present,remaining:b.remaining}),low:b.remaining!==null&&b.remaining<on}),"pollState");async function de(e){await mt();const t=new Date,{data:n,links:r,rows:a,code:d}=await ue();return{...un({now:t,st:Ae(t),poll:Xa(),sidebar:n,rows:a,links:r,rules:pe(),integrations:await pa.rows(),code:d,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:He(Z()),claudeModel:Gt(),codex:Z(),awake:Ue(t),remembered:new Map(R(g()).map(h=>[h.route,Be(h.route)]))}),prompts:Me(null).map(Yt),remembered:Be(null)}}l(de,"hubData");const J=l(async()=>{const e=new Date,t={until:Ue(e),unsupported:gn()};return{...ln({now:e,st:Ae(e),rules:pe(),route:await Jt(),linear:Q(),awake:t}),closed:qn()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ya(e,t,n){const r=l((s,o,u,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(u)},"send"),a=l((s,o)=>{const u=xn();return r(s,"application/json",JSON.stringify(o),u?{"x-hub-code":u}:{})},"sendJSON"),d=l(({route:s,started:o})=>(o.then(({error:u})=>{u&&c(`new board /${s} didn't start: ${u}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Qa(e),i=n.pathname,S=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(S&&e.method==="GET")try{return a(200,await Xn(S[1]))}catch(s){return a(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let s;try{const o=await x(e);if(s=o.route,i==="/_hub/reopen-board"){const T=await ea(s);return _.refresh(),a(200,T)}if(A.has(s))return a(409,{error:"That board is already closing."});A.add(s);const u=l(()=>A.delete(s),"release");t.once("finish",u),t.once("close",u),await D.running;const f=await Yn(s,o.name);return _.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(i==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await x(e)).route,U.has(s))return a(409,{error:"That board is already restarting."});U.add(s),await D.running;const o=await ta(s,{serverUp:Ma,portUp:ke,onStarting:l(u=>c(`restart /${s}: ${u}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&c(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{U.delete(s),_.refresh()}}if(i==="/_hub/restart-hub"&&e.method==="POST")try{return await x(e),$e?a(409,{error:"The hub is already restarting."}):($e=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=Pa("hub");s&&($e=!1,c(`hub restart failed: ${s}`))}),a(200,{restarting:!0,command:Ra().restartCommand}))}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await x(e),a(200,{upgrade:await W.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/presence"&&e.method==="POST")return await x(e),ht(H.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await x(e),o=R(g()).find(u=>u.route===s);return o?(xe.add(s),await D.running,pt(),await D.running,Oe.clear(),_.changed(s),a(200,{route:s,listen:Ge(o)})):a(404,{error:"That board is not open."})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/repo"&&e.method==="GET")return Nn(n,{repos:L,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Ve(k[1]):await Gn(k[1],await x(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,s)}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await x(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return d(await Jn(s,{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(y&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return d(await Kn(y[1],{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const s=await Vn(m[1]);return _.refresh(),a(200,s)}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}if((i==="/_hub/build"||i==="/_hub/pull")&&e.method==="POST"){if(ne)return a(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${i==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let u;try{u=await x(e)}catch(f){return a(f.status||400,{error:i==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return i==="/_hub/pull"?a(200,await ya(u,{manifest:g()})):a(202,await ga(u,{manifest:g(),log:c}))}catch(f){if(f instanceof ma)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",w.readFileSync($.join(X,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Za());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",w.readFileSync($.join(X,`icon-${p[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/colors.css"){const s=n.searchParams.get("v")===Ze()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",w.readFileSync(La),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",w.readFileSync($.join(ge,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return r(200,"text/css; charset=utf-8",w.readFileSync($.join(ge,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await ue({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const s=sa(i.slice(10));if(!s){t.writeHead(404).end();return}const o=$.extname(s),u={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===oa(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,u,w.readFileSync(s),{"cache-control":f}):vt(e,t,200,{"content-type":u,"cache-control":f},w.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await de(e));if(i==="/_hub/api/settings")return a(200,await J());if(i==="/_hub/push/key")return a(200,{key:At()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ht(await x(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:It((await x(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Mt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:s.filter(o=>o.status>=200&&o.status<300).length,results:s})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("set"),u=o==="on"?{on:s.get("value")==="on"}:o==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!u)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let T;try{T=await Ut(u)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:hn}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await J()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{mn(s.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):r(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:fn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return ee.apply(),h?a(200,await J()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Nt(s.get("value"),Z({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{zt(s.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):r(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?ha(s.get("value")):fa(s.get("value")),await Te()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(Ea(),c("linear: key removed"),await ut()):c(`linear: key ${(await Da(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof be))throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await J()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=Q();if(!s.set||s.refused)return a(200,s);const o=nt();try{const u=await o.teams();return a(200,{...Q(),teams:u.map(({key:f,name:T})=>({key:f,name:T})),projects:u.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(u){if(!(u instanceof be))throw u;return a(200,{...Q(),error:u.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=nt();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await x(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const u=typeof o.name=="string"?o.name.trim():"";if(!u||u.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await s.createProject(o.team,u))}catch(f){if(!(f instanceof be))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(u=>s.has(u)).map(u=>[u,s.get(u).trim()]));s.get("set")==="hold"&&(o.hold=s.get("value")||"off");try{Bt(sn(V(),o))}catch(u){if(u.status!==400)throw u;return h?a(400,{error:u.message}):r(400,"text/plain; charset=utf-8",u.message)}return _.refresh(),h?a(200,await J()):P(t,"/_hub/#quiet")}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await x(e,64*1024),o=En(pe(),s.rules,s.base??[]),Ke(o.changes)&&Dn(o.rules)}catch(f){if(!(f instanceof Rn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Ke(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const u=await ze(R(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${u.events.map(f=>f.route).join(", ")||"no board"}${u.failed.length?`; ${u.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...u})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ce();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,dn(s)):P(t,"/_hub/")}if(i==="/_hub/poll"&&e.method==="POST"){const s=await x(e);return a(200,await _e({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:u}=await M(f=>{const T=ve(),K=T.boards.find(Pe=>Pe.owner?.session&&Pe.owner.session===s.session_id)||null;fe(f,T)&&_.refresh();const je=en(f,s);if(je)return{answer:je};const he=Qt(s,{board:K,live:T.running?.get(s.session_id)});return f.prompts[he.id]=he,{rec:he}});return o?a(200,{state:"answered",answer:o}):(G(u.route),await gt(s),c(`prompt ${u.id}: ${u.route?`/${u.route}`:u.cwd} ${u.tool}${u.action?` (${u.action})`:""}`),Wt(Xt(u),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${u.id}: push failed: ${f.message}`)),a(200,{id:u.id,deadline:u.deadline}))}if(i==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await gt(s)})}const v=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return v?qa(e,t,{id:v[1],op:v[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ya,"hubRoute");async function qa(e,t,{id:n,op:r,url:a,sendJSON:d}){if(r==="wait"&&e.method==="GET"){const h=l(()=>M(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=l(m=>(G(m.route),d(200,{state:"answered",answer:m.answer})),"take"),S=await h();if(!S)return d(404,{state:"gone"});if(S.answer)return i(S);const k=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Ie,Ie));if(await new Promise(m=>{const p=N.get(n)||new Set;N.set(n,p);const v=l(()=>{clearTimeout(s),p.delete(v),!p.size&&N.get(n)===p&&N.delete(n),m()},"done"),s=setTimeout(v,k);p.add(v),t.on("close",v)}),t.writableEnded||t.destroyed)return;const y=await h();return y?y.answer?i(y):d(200,{state:"pending"}):d(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await x(e),i=await M(y=>{const m=j(y.prompts,n);if(!m)return{status:404,error:"That prompt has expired."};if(m.answer)return{status:409,error:"That prompt was already answered."};const{answer:p,error:v}=Vt(m,h);if(v)return{status:400,error:v};m.answer=p;const s=ve();return fe(y,s),qt(y,m,s),{rec:m,answer:p}});if(i.error)return d(i.status,{error:i.error});const{rec:S,answer:k}=i;c(`prompt ${n}: ${k.choice}`);for(const y of[...N.get(n)||[]])y();return G(S.route),d(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await M(i=>{const S=j(i.prompts,n);return delete i.prompts[n],S});return h&&(G(h.route),c(`prompt ${n}: its hook gave up`)),d(200,{ok:!0})}return!1}l(qa,"promptRoute");const es=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function Ot(e,t){const n=O.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=B(t),[d,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="files"?["down","down \xB7 its folder is missing"]:n.why==="code"?["wait","restarting \xB7 its code changed"]:n.child?["wait","starting \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${r}`],i=a==="cmd"?"The hub runs every site and restarts one that stops.":a==="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`${Tt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
4
|
+
`),n.code=be(t)?qe(t):null;let h;try{h=Be(o,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch($){b.writeSync(s,`hub could not start it: ${$.message}
|
|
5
|
+
`),b.closeSync(s),Object.assign(n,{child:null,key:oe(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ie(e),String(h.pid)),Object.assign(n,{child:h,key:oe(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&A.add(h);const u=l(($,_)=>{if(A.delete(h),n.child!==h)return;try{b.unlinkSync(ie(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:$??_,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${$??_})`)},"ended");h.on("error",$=>{c(`${e} spawn error: ${$.message}`),h.pid||u($.code,null)}),h.on("exit",u)}l(ss,"start");const mt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ce(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;le(n,"SIGTERM"),setTimeout(()=>{A.has(n)&&le(n,"SIGKILL")},pt)}l(ce,"stop");const le=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function je(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=l(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(je,"probe");let gt=!0;const I=new Set;let Pe=!1;const J=new Set,rs=3e4;async function os(e){const t=Date.now()+rs;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===oe(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}l(os,"serverAnswers");async function wt(){if(re)return;bt.check();const{sites:e}=g();for(const t of[...j.keys()])I.has(t)||(P(e,t)?Oe(P(e,t))||(c(`${t} no longer has a command; stopping it`),ce(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ce(t),j.delete(t)));for(const t of[...z.keys()])G(P(e,t))!=="port"&&z.delete(t);for(const[t,n]of Object.entries(e))G(n)==="port"&&z.set(t,await je(n.port));for(const[t,n]of Object.entries(e)){if(!Oe(n))continue;gt&&as(t,n);const s=j.get(t);if(s&&s.child&&s.key!==oe(n)){c(`${t} entry changed; restarting it`),ce(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=s?.child&&s.code&&!s.stopping&&!J.has(t)&&!I.has(t)?qe(n):null;if(o&&o!==s.code){if(!(await Ha(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ce(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))&&ss(t,n)}gt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await je(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(wt,"reconcile");const bt=Aa({root:xe(),files:Cn().files,restart:l(()=>lt("hub"),"restart"),log:c}),is=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 yt=l(async()=>{try{for(const e of await Ia())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const cs=6e4,Re=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+cs,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},H=an(),St=l(()=>y.startedAt?gn({last:new Date(y.startedAt),...H.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Ee().catch(n=>c("poll failed:",n.message))}l(kt,"presenceChanged");function Ee({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=q();let s;try{s=await On({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await it({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),$e({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))),await it({manifest:g(),log:c});try{const o=await Ea({manifest:g()});o.length&&$e({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Pn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>Re&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(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()-y.linearErrorLoggedAt>Re&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await es({boards:E(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()-y.slackErrorLoggedAt>Re&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Dn({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{In(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 Ge();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 y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Ee,"runPoll");let $t=null;function ls(){$t||=jn({settings:q(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}l(ls,"runWakes");const De=new Set,L={running:null};function _t(){if(L.running||I.size||J.size)return;const e=new Set(De);De.clear(),L.running=Vn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Zn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{L.running=null})}l(_t,"runSessionCheck"),await Pa(g());const us=Date.now(),W=new Map;function K(e){e?(de.delete(e),x.changed(e)):x.refresh()}l(K,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await B(n=>{const s=rn(n,{now:Date.now(),aliveSince:us}),o=we(n,Le());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)K(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(xt,"prunePrompts");const Le=l(()=>({running:tt(),boards:E(g())}),"promptContext");async function Tt(e){const t=E(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ma(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}}l(Tt,"noteRefusal");let V=null,vt=null;async function Ce(){return V?vt??=V.catch(()=>{}).then(()=>(vt=null,Ce())):(V=(async()=>Sa({gate:ot(),resume:l(async e=>{const t=await Ye(E(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{V=null}),V)}l(Ce,"checkUsageGate"),setInterval(()=>{$e({manifest:g()}).catch(e=>c("try tick failed:",e.message)),Ce().catch(e=>c("usage gate failed:",e.message)),H.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Ee().catch(e=>c("poll failed:",e.message))}),oa({listening:l(e=>Rn(e)&&ue().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ls(),ae.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(re)return;re=!0,clearInterval(is),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of A)le(n,"SIGTERM");let e=!1;Oa().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||A.size)&&Date.now()<t)){for(const n of A)c(`pid ${n.pid} ignored SIGTERM; killing it`),le(n,"SIGKILL");process.exit(0)}},50)}l(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const 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}l(Pt,"newestMtime");function ds(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(ds,"changedAt");const Rt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ue(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=G(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?z.has(t)?z.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:ha(n),changedAt:ds(t,n)||null,created:n.created||null,log:k.join(ne,t+".log")}})}l(ue,"status");const M=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),de=new Map,Et=2e4,hs=1500;async function fs(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(hs)});return t.ok?zn(await t.json()):null}catch{return null}}l(fs,"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=de.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&de.set(o.route,{at:Date.now(),needs:fs(o.port)})}const s=await Promise.all(n.map(o=>de.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}l(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ae=new Map;function ps(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(En,{})};for(const o of e)try{const r=Xe(o,s);n.set(o.route,r);let p=Ae.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:r,links:Hn(o,{listen:r})},Ae.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}}l(ps,"boardLinks");async function Z({fresh:e=!1}={}){const t=new Date,n=ue(),s=await Dt(n,{fresh:e}),o=g(),r=E(o),{links:p,listens:h}=ps(r),u=Un(),$=new Map(r.map(a=>[a.route,ka(a.owner?.session)]).filter(([,a])=>a)),_=Qn(o,t.getTime(),ra,a=>h.get(a)==="listening"&&n.some(i=>i.route===a&&i.state==="up")),S=new Set(_.map(a=>a.route)),m=Jn(n,s,new Set(r.map(a=>a.route)),h,$).filter(a=>!S.has(a.route)),w=q(),v=hn(t,w);return{data:{code:u.running??null,total:Lt(s),start:Wn(m),sites:m,hub:Kn(m,p,Mn(u)),home:b.realpathSync(Fe.homedir()),starting:_,usage:ns({codexShare:Je(ee())}),pause:ot(),upgrade:he.state(),notifications:v?v.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(v,t))}:{state:"on",text:"On"},away:!H.state().present},links:p,rows:n,code:u}}l(Z,"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)}l(Ct,"sendText");async function ms(e,t=null){const n=t?t.sidebar((await Z()).data):(await Z()).data,s=Ne.createContext({});for(const o of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync(k.join(ke,o),"utf8"),s);return wa({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:fe(180)})}l(ms,"shellPage");const he=La({root:xe(),running:dt,run:gs,log:c,changed:l(()=>x.refresh(),"changed"),pulled:l(()=>bt.restartNow(),"pulled")});function gs(){const e=k.join(ne,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
+
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[k.join(xe(),"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: /,"")})})})}l(gs,"runUpdate"),he.check(),setInterval(()=>he.check(),Ca);const x=ba({targets:l(()=>Object.entries(g().sites).filter(([,e])=>be(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>E(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await Z({fresh:e})).data,"sidebar"),log:c});function fe(e){let t="0";try{t=Math.floor(b.statSync(k.join(F,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(fe,"iconUrl");const ws=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:fe(192),sizes:"192x192",type:"image/png"},{src:fe(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function D(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}l(D,"readBody");async function T(e,t){if(!Se(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await D(e,t);return n?JSON.parse(n):{}}l(T,"readJSONBody");const At=l((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>${M(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${fe(180)}">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${st()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),bs=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),ys=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:H.state().present,every:Ze({present:H.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<wn}),"pollState");async function pe(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await Z();return{...yn({now:t,st:ze(t),poll:ys(),sidebar:n,rows:o,links:s,rules:ye(),integrations:await xa.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(ee()),claudeModel:en(),codex:ee(),awake:Qe(t),remembered:new Map(E(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(ln),remembered:Ve(null)}}l(pe,"hubData");const U=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:vn()};return{...Sn({now:e,st:ze(e),rules:ye(),linear:te(),slack:Xa(),awake:t}),closed:la()}},"settingsData"),R=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ss(e,t,n,s=null){const o=l((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=l((a,i)=>{const d=An();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({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=bs(e),u=n.pathname,$=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if($&&e.method==="GET")try{return r(200,await ia($[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let a;try{const i=await T(e);if(a=i.route,u==="/_hub/reopen-board"){const O=await ua(a);return x.refresh(),r(200,O)}if(I.has(a))return r(409,{error:"That board is already closing."});I.add(a);const d=l(()=>I.delete(a),"release");t.once("finish",d),t.once("close",d),await L.running;const f=await ca(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,J.has(a))return r(409,{error:"That board is already restarting."});J.add(a),await L.running;const i=await da(a,{serverUp:os,portUp:je,onStarting:l(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{J.delete(a),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await T(e),Pe?r(409,{error:"The hub is already restarting."}):(Pe=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=lt("hub");a&&(Pe=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:Ka().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/upgrade"&&e.method==="POST")try{return await T(e),r(200,{upgrade:await he.request()})}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/presence"&&e.method==="POST")return await T(e),kt(H.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),i=E(g()).find(d=>d.route===a);return i?(De.add(a),await L.running,_t(),await L.running,Ae.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(u==="/_hub/repos"&&e.method==="GET")try{return r(200,await C.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return r(200,await C.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/repo"&&e.method==="GET")return Yn(n,{repos:C,manifest:g(),sendJSON:r});const _=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(_&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?at(_[1]):await qn(_[1],await T(e),{repos:C});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!Se(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await na(a,{repos:C,changed:l(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!Se(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await aa(S[1],{repos:C,changed:l(()=>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(u);if(m&&e.method==="DELETE")try{const a=await sa(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((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(re)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:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?r(200,await Ra(d,{manifest:g()})):r(202,await va(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Ta)return r(f.status,{error:f.message});throw f}}if(u==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(F,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return o(200,"application/manifest+json",ws());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return o(200,"image/png",b.readFileSync(k.join(F,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const a=n.searchParams.get("v")===st()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ts),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(ke,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(ke,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:a}=await Z({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};me.add(a),e.on("close",()=>me.delete(a)),e.on("close",Q.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const v=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(v&&e.method==="GET"){const a=P(g().sites,v[1]);if(!be(a))return r(404,{error:"That board is not open."});const i=Na(et(k.join(a.dir,"board.json"),{})),d=(await _e.node())?.dnsName||e.headers["x-forwarded-host"];return r(200,{people:Q.people(v[1],i),refused:Q.refusals(v[1],i),link:d?`https://${d}/${v[1]}/`:null})}if(u.startsWith("/_hub/app/")){const a=pa(u.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")===ga(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(u==="/_hub/api/hub")return r(200,await pe(e));if(u==="/_hub/api/settings")return r(200,await U());if(u==="/_hub/push/key")return r(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Wt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Kt((await T(e))?.endpoint)});if(u==="/_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(u==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await D(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 O;try{O=await Yt(d)}catch(Y){if(Y.status!==400)throw Y;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return O.dropped&&c(`notifications off: dropped ${O.dropped} queued push${O.dropped===1?"":"es"}`),h?r(200,await U()):R(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{Tn(a.get("keep_awake"))}catch(i){if(i.status===409)return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message);if(i.status!==400)throw i;return h?r(400,{error:_n}):R(t,"/_hub/?refused=keep-awake#keep-awake")}return ae.apply(),h?r(200,await U()):R(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{qt(a.get("value"),ee({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 pe(e)):R(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await D(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 pe(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{u.endsWith("/pause-at")?$a(a.get("value")):_a(a.get("value")),await Ce()}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 pe(e)):R(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await D(e,4096));try{a.has("remove")?(Va(),c("linear: key removed"),await yt()):c(`linear: key ${(await Za(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Te))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 U()):R(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await D(e,4096));try{a.has("remove")?(Ya(),c("slack: token removed")):c(`slack: token ${(await qa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof Qa))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 U()):R(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const a=te();if(!a.set||a.refused)return r(200,a);const i=ut();try{const d=await i.teams();return r(200,{...te(),teams:d.map(({key:f,name:O})=>({key:f,name:O})),projects:d.flatMap(f=>f.projects.map(O=>({id:O.id,name:O.name,team:f.key})))})}catch(d){if(!(d instanceof Te))throw d;return r(200,{...te(),error:d.message})}}if(u==="/_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 T(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 Te))throw f;return r(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await D(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(q(),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 U()):R(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await T(e,64*1024),i=Fn(ye(),a.rules,a.base??[]),nt(i.changes)&&Nn(i.rules)}catch(f){if(!(f instanceof Bn)&&!(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(E(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(u==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,kn(a)):R(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const a=await T(e);return r(200,await Ee({flush:a.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await B(f=>{const O=Le(),Y=O.boards.find(Me=>Me.owner?.session&&Me.owner.session===a.session_id)||null;we(f,O)&&x.refresh();const He=dn(f,a);if(He)return{answer:He};const ge=on(a,{board:Y,live:O.running?.get(a.session_id)});return f.prompts[ge.id]=ge,{rec:ge}});return i?r(200,{state:"answered",answer:i}):(K(d.route),await Tt(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(u==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(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 Tt(a)})}const X=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return X?ks(e,t,{id:X[1],op:X[2]?.slice(1)||null,url:n,sendJSON:r}):!1}l(Ss,"hubRoute");async function ks(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=l(()=>B(S=>{const m=P(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(K(S.route),r(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return r(404,{state:"gone"});if(u.answer)return h(u);const $=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||We,We));if(await new Promise(S=>{const m=W.get(n)||new Set;W.set(n,m);const w=l(()=>{clearTimeout(v),m.delete(w),!m.size&&W.get(n)===m&&W.delete(n),S()},"done"),v=setTimeout(w,$);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const _=await p();return _?_.answer?h(_):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await B(_=>{const S=P(_.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=sn(S,p);if(w)return{status:400,error:w};S.answer=m;const v=Le();return we(_,v),un(_,S,v),{rec:S,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:u,answer:$}=h;c(`prompt ${n}: ${$.choice}`);for(const _ of[...W.get(n)||[]])_();return K(u.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await B(h=>{const u=P(h.prompts,n);return delete h.prompts[n],u});return p&&(K(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}l(ks,"promptRoute");const $s=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o=G(t),[r,p]=o==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:o==="files"?["down","down \xB7 its folder is missing"]:n.why==="code"?["wait","restarting \xB7 its code changed"]:n.child?["wait","starting \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${s}`],h=o==="cmd"?"The hub runs every site and restarts one that stops.":o==="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>${M(t.title||e)} isn't answering</h1>
|
|
12
|
+
<p class="lede">${M(h)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${$s({icon:Gn(e,t),title:M(t.title||e),sub:`<span class="st ${r}">${M(p)}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}l(
|
|
16
|
+
</main></body></html>`}l(It,"downPage");const Ie=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Q=Wa({file:k.join(N,"share-seen.json")}),me=new Set;function Ht(){const e=g().sites;for(const t of me){const n=ct(t.login,e);[...t.routes].some(s=>!n.has(s))&&(me.delete(t),t.res.destroy())}ja()}l(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Mt(e){const t=await _e(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Ie(e),r=ct(n,g().sites),p=r.size?Ga({method:e.method,pathname:o,routes:r}):"other",h=za(o);if(p==="other")return(!r.size||h)&&Q.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Q.visit(n,s,h);const u=await _e.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:l($=>Ja($,r,u?.name||u?.login),"sidebar")}}}l(Mt,"access");const Ut=Ue.createServer((e,t)=>{Mt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?_s(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ba(e,t,n)})});function _s(e,t,n=null){const s=Ie(e);if(e.method!=="GET"&&e.method!=="HEAD"&&fa(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(rt({method:e.method,pathname:s.pathname,search:s.search,dest:ma(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);ms(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(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:ve,pid:process.pid},sites:ue()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=ue();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:Lt(w),sites:Object.fromEntries(m.map(v=>[v.route,w.get(v.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/")){Ss(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${M(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",$=rt({pathname:s.pathname,dest:"document",sites:o})==="shell";if(G(p)==="files"){if(!Rt(p.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(It(r,p));return}Da(e,t,{dir:p.dir,rest:u,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 _={...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"},S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:_},m=>{const w={...m.headers};$&&(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||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ht),S.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(It(r,p))}),e.pipe(S)}l(_s,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Mt(e);if(s!=="owner"){Fa(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Ie(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const p=`/${o}`;Ua({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(ve,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${ve}/ (manifest ${ht})`));
|
package/hub/linear.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),f.chmodSync(r,384),f.renameSync(r,e)}a(I,"writeKeyFile");function L(s,{fetch:e=globalThis.fetch,checked:r=a(()=>{},"checked")}={}){async function u(n,t={}){let i;try{i=await e(C(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new p(`Could not reach Linear: ${c.message}`)}const o=await i.json().catch(()=>null);if(i.status===401||o?.errors?.some(c=>c.extensions?.type==="authentication error"))throw r(!1),new p("Linear refused the key.",400);if(o?.errors?.length){const c=o.errors[0];throw Object.assign(new p(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!i.ok||!o?.data)throw new p(`Linear answered ${i.status}.`);return r(!0),o.data}a(u,"call");const l="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await u(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${l} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new p(`Linear has no team ${n}.`,400)})()}a(d,"team");const m=a((n,t)=>n.projects.nodes.find(i=>i.id===t)||n.projects.nodes.find(i=>i.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new p(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await u("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await u("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const i=await u("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:i.team.projects.nodes.map(o=>({id:o.id,name:o.name}))}}))},async createProject(n,t){const i=await d(n),o=await u("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[i.id]}});if(!o.projectCreate.success)throw new p("Linear did not create the project.");return{...o.projectCreate.project,team:i.key}},async createIssue({team:n,project:t,title:i,description:o}){const c=await d(n),y={teamId:c.id,projectId:m(c,t).id,title:i,...o?{description:o}:{}},h=await u("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:y});if(!h.issueCreate.success)throw new p("Linear did not create the issue.");return h.issueCreate.issue},async issue(n){try{const t=await u("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:i,created:o}={}){const c=await w.issue(n);if(!c||c.assignee!==i&&!o||x(c.state)>={started:1,review:2,done:3}[t])return null;const y=S((await d(c.team)).states.nodes,t);if(!y)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:y.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return y.name},async moveIssue(n,t){const i=await w.issue(n);if(!i)throw new p(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(i.state.type))return null;const o=S((await d(i.team)).states.nodes,t);if(!o||o.id===i.state.id)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:o.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return o.name}};return w}a(L,"linearClient");function x(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(x,"stageOf");function S(s,e){const r=[...s].sort((d,m)=>d.position-m.position),u=a(d=>r.filter(m=>m.type===d),"of"),l=a(d=>/review/i.test(d.name),"review");return e==="started"?u("started").find(d=>/^in progress$/i.test(d.name))||u("started").find(d=>!l(d))||null:e==="review"?u("started").find(l)||null:e==="done"&&(u("completed").find(d=>/^done$/i.test(d.name))||u("completed")[0])||null}a(S,"stateFor");async function N(s,{fetch:e}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new p("Paste a Linear personal API key.",400);const u=new Date().toISOString();let l=!1;const d=await L(r,{fetch:e,checked:a(m=>{l=!m},"checked")}).viewer().catch(m=>{if(!l)throw m;return null});return I(d?{key:r,...d,saved_at:u}:{key:r,saved_at:u,refused_at:u}),$()}a(N,"saveLinearKey");function F(s,e){const r=g();if(r?.key!==s||!!r.refused_at==!e)return;const{refused_at:u,...l}=r;I(e?l:{...l,refused_at:new Date().toISOString()})}a(F,"markChecked");function U(){return f.rmSync(k(),{force:!0}),$()}a(U,"removeLinearKey");function z({fetch:s}={}){const e=g();return e?{...L(e.key,{fetch:s,checked:a(r=>F(e.key,r),"checked")}),saved:e}:null}a(z,"savedClient");export{P as LINEAR_NEEDS_KEY,p as LinearError,L as linearClient,$ as linearStatus,E as linearWorks,g as readLinearKey,U as removeLinearKey,N as saveLinearKey,z as savedClient,x as stageOf,S as stateFor};
|
|
1
|
+
var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
|
package/hub/live.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function
|
|
2
|
-
`)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function
|
|
1
|
+
var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function N(t=l){let e=[];try{e=c.readdirSync(t).filter(n=>n.endsWith(".json"))}catch{}return e.flatMap(n=>{try{const r=JSON.parse(c.readFileSync(f.join(t,n),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(N,"sessionRecords");const b=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"running");function d(t){const e=Number(process.env.KICKFLIP_PS_TIMEOUT_MS)||3e3;try{return h("ps",t,{encoding:"utf8",timeout:e,env:{...process.env,TZ:"UTC"}})}catch(n){return typeof n.status=="number"&&!n.signal?n.stdout||"":null}}s(d,"ps");function m(t){const e=new Map,n=t.filter(b);if(!n.length)return e;const r=d(["-o","pid=,lstart=","-p",n.join(",")]);if(r===null)return null;for(const o of r.split(`
|
|
2
|
+
`)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function _(t,e=m){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const n=u(t.procStart);if(!Number.isFinite(n))return!1;const r=e([t.pid])?.get(t.pid);return Number.isFinite(r)&&Math.abs(n-r)<=a}s(_,"verifiedProcess");function y({dir:t=l,starts:e=m,session:n=null}={}){const r=N(t).filter(i=>!n||i.sessionId===n),o=e([...new Set(r.map(i=>i.pid))]);return o&&new Map(x(r,o).map(i=>[i.sessionId,i]))}s(y,"liveSessions");function I(t){const e=d(["-o","ppid=","-p",String(t)]);return e===null?void 0:Number(e.trim())||null}s(I,"parentOf");function j(t,e,{ppid:n=process.ppid,parent:r=I,live:o=y}={}){if(n!==e[0])return!0;const i=r(e[0]);if(i===null||i!==void 0&&e[1]!==void 0&&i!==e[1])return!0;const p=o({session:t});return p!==null&&!p.has(t)}s(j,"sessionGone");export{l as SESSIONS_DIR,x as liveOf,y as liveSessions,I as parentOf,m as processStarts,d as ps,j as sessionGone,N as sessionRecords,_ as verifiedProcess};
|
package/hub/locks.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var f=Object.defineProperty;var o=(r,c)=>f(r,"name",{value:c,configurable:!0});import t from"node:fs";import a from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:m=1e4,heartbeat:s=!1}={}){const e=`${r}.lock`;t.mkdirSync(a.dirname(r),{recursive:!0});const y=performance.now()+m;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(performance.now()>y)throw Object.assign(new Error(`${a.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=s?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}o(u,"withFileLock");export{u as withFileLock};
|
package/hub/pr-report.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=Object.defineProperty;var a=(t,n)=>R(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const E="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",g="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",w=a(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function d(t,n=process.env){return new Promise((s,r)=>{h("gh",t,{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(o,u,m)=>{if(o)return r(new Error(w(m||o.message)));try{s(JSON.parse(u))}catch(l){r(new Error(w(`unreadable output (${l.message})`)))}})})}a(d,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(t){const n=process.env;if(!t)return n;const s=await new Promise(r=>h("gh",["auth","token","-u",t],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")}},(o,u)=>r(o?"":u.trim())));if(!s)throw new Error(`gh has no ${t} account; run gh auth login`);return{...n,GH_TOKEN:s}}a(y,"envFor");async function O({repo:t,pr:n,replyTo:s,words:r,login:o}){const u=await y(o),m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await d(["api",m,"-f",`body=${v}${r}
|
|
2
2
|
|
|
3
|
-
${b}`],
|
|
3
|
+
${b}`],u)).html_url}catch(l){throw new Error(`${t}#${n}: ${l.message}`)}}a(O,"postToPR");async function A({repo:t,pr:n,head:s,body:r,login:o}){const u=await y(o);try{return(await d(["api",`repos/${t}/pulls/${n}/reviews`,"-f",`body=${r}`,"-f","event=COMMENT","-f",`commit_id=${s}`],u)).html_url}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}}a(A,"postReview");async function T(t){return(await d(["api","--paginate","--slurp",`repos/${t}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}a(T,"collaborators");async function I(t){try{return(await d(["api",`users/${encodeURIComponent(t)}`])).login}catch{return null}}a(I,"githubUser");async function H({repo:t,pr:n,since:s}){try{return(await d(["api","--paginate","--slurp",`repos/${t}/pulls/${n}/reviews`])).flat().filter(o=>Date.parse(o.submitted_at)>Date.parse(s)).map(o=>({login:o.user?.login??null,at:o.submitted_at}))}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}}a(H,"reviewsSince");function L({repo:t,pr:n}){return new Promise((s,r)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,g].filter(Boolean).join(":")},timeout:9e4},(o,u,m)=>o?r(new Error(w(m||o.message))):s())})}a(L,"markReady");const N=a(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function q({repo:t,pr:n,since:s}){let r;try{r=await d(["pr","view",String(n),"--repo",t,"--json",E])}catch(c){throw new Error(`${t}#${n}: ${c.message}`)}const o=a(c=>d(["api","--paginate","--slurp",`repos/${t}/${c}`]).then(p=>p.flat()).catch(()=>[]),"list"),[u,m,l]=await Promise.all([o(`issues/${n}/comments?since=${encodeURIComponent(s)}`),o(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),o(`pulls/${n}/reviews`)]);return{view:r,issueComments:u,reviewComments:m,reviews:l}}a(q,"fetchPR");const $="author { __typename login }",S=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid 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 } } } } }`;function P(t,n=process.env){return new Promise((s,r)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(o,u,m)=>{let l=null;try{l=JSON.parse(u)}catch{}if(l?.data)return s(l);r(new Error(`GitHub query: ${w(o?m||o.message:"unreadable output")}`))})})}a(P,"graphql");const _=a(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user"),C="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function U(t,n=[]){const s=t.map(({repo:l,pr:c},p)=>{const[e,f]=l.split("/");return`pr${p}: repository(owner: ${JSON.stringify(e)}, name: ${JSON.stringify(f)}) { pullRequest(number: ${Number(c)}) { ${S} } }`});n.forEach((l,c)=>{const[p,e]=l.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(p)}, name: ${JSON.stringify(e)}) { ${C} }`)});const{data:r,errors:o=[]}=await P(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),u=Object.fromEntries(n.flatMap((l,c)=>r[`open${c}`]?.pullRequests?[[l,r[`open${c}`].pullRequests.nodes]]:[])),m=t.map(({repo:l,pr:c},p)=>{const e=r[`pr${p}`]?.pullRequest;if(!e)return{error:`${l}#${c}: ${o.find(i=>i.path?.[0]===`pr${p}`)?.message||"GitHub did not answer for it"}`};const f=a(i=>({id:i.databaseId??null,user:_(i.author),created_at:i.createdAt??null,body:i.body??null,html_url:i.url??null}),"comment");return{view:{state:e.state,isDraft:e.isDraft,reviewDecision:e.reviewDecision,mergeable:e.mergeable,mergeStateStatus:e.mergeStateStatus,headRefOid:e.headRefOid,statusCheckRollup:e.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[]},issueComments:(e.comments?.nodes??[]).map(f),reviewComments:(e.reviewThreads?.nodes??[]).flatMap(i=>i.comments?.nodes??[]).map(i=>({...f(i),path:i.path??null,line:i.line??null,in_reply_to_id:i.replyTo?.databaseId??null})),reviews:(e.reviews?.nodes??[]).map(i=>({id:i.databaseId??null,user:_(i.author),submitted_at:i.submittedAt??null,body:i.body??null,state:i.state??null,html_url:i.url??null}))}});return{login:r.viewer?.login??null,rate:r.rateLimit??null,results:m,open:u}}a(U,"fetchPRs");function k({view:t,issueComments:n,reviewComments:s,reviews:r},{pr:o,since:u,me:m}){const l=a((e,f)=>({kind:f,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),c=[...n.map(e=>({...l(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...l(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...r.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>u).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,f)=>e.at<f.at?-1:e.at>f.at?1:0),p=t.statusCheckRollup??[];return{pr:Number(o),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:p.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:p.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:c}}a(k,"reportOf");async function B(t){let n;try{n=await d(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} 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(r=>r.path),requested:(s.reviewRequests||[]).map(r=>r.login).filter(Boolean)}))}a(B,"openPRs");async function j(t,n){let s;try{s=await d(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}return{state:s.state??null,files:(s.files||[]).map(r=>r.path)}}a(j,"prFiles");async function F({env:t}={}){const n=await d(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}a(F,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,E as PR_VIEW_FIELDS,N as checksPassed,T as collaborators,q as fetchPR,U as fetchPRs,F as githubLogin,I as githubUser,L as markReady,B as openPRs,A as postReview,O as postToPR,j as prFiles,k as reportOf,H as reviewsSince};
|
package/hub/presence.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var m=Object.defineProperty;var a=(r,e)=>m(r,"name",{value:e,configurable:!0});import{execFile as f}from"node:child_process";const d=3*6e4,I=a(r=>new Promise((e,o)=>{f("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function p(r=I){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...n));return{atMac:!t&&s<d,locked:t,idleMs:s}}catch(e){return{atMac:!1,error:String(e.message||e)}}}a(p,"readPresence");function h({read:r=p,clock:e=Date.now}={}){let o=-1/0,t=!1,n=null;const s=a(i=>{const l=e(),c=i||l-o<d,u={present:c,changed:c!==t,back:c&&!t};return u.back&&(n=l),t=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return o=e(),s(!0)},state:a(()=>({present:t,backAt:n}),"state")}}a(h,"ownerTracker");export{d as IDLE_LIMIT_MS,h as ownerTracker,p as readPresence};
|
package/hub/push.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`),u.renameSync(o,n)}r(
|
|
1
|
+
var V=Object.defineProperty;var r=(t,e)=>V(t,"name",{value:e,configurable:!0});import h from"node:crypto";import u from"node:fs";import y from"node:path";import k from"node:os";import{validate as X,quietOf as _,flushAt as G,hold as I,notifications as Y,snoozeEnd as Z}from"./schedule.mjs";import{storedEnd as tt}from"./duration.mjs";import{CODEX_SHARE_VALUES as et,codexStatus as K,effectiveCodexShare as nt}from"../shared/codex.mjs";const j=y.join(k.homedir(),".claude","kickflip","push"),L=y.join(j,"vapid.json"),b=y.join(j,"subs.json"),m=y.join(j,"queue.json"),C=m+".lock",z=r(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),F=r(t=>Buffer.from(t).toString("base64url"),"b64u"),D=r(t=>Buffer.from(t,"base64url"),"unb64u"),v=r((t,e)=>h.createHmac("sha256",t).update(e).digest(),"hmac"),f=r((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function J(){try{return JSON.parse(u.readFileSync(L,"utf8"))}catch{}const t=h.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return f(L,t),t}r(J,"vapidKey");function ot(){const t=J();return F(Buffer.concat([Buffer.from([4]),D(t.x),D(t.y)]))}r(ot,"publicKey");function g(t=b){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}r(g,"readSubs");function St(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),f(b,o),{count:o.length,id:N(t.endpoint)}}r(St,"addSubscription");const N=r(t=>h.createHash("sha256").update(t).digest("hex").slice(0,16),"deviceId");function xt(t){const e=g(),n=e.filter(o=>N(o.endpoint)!==t);return n.length!==e.length&&f(b,n),n.length}r(xt,"removeSubscriptionById");function kt(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&f(b,n),n.length}r(kt,"removeSubscription");function rt(t=""){const e=["iPhone","iPad","Android","Windows","Macintosh","Linux"].find(o=>t.includes(o)),n=[["Edg/","Edge"],["OPR/","Opera"],["Firefox/","Firefox"],["FxiOS/","Firefox"],["CriOS/","Chrome"],["Chrome/","Chrome"],["Safari/","Safari"]].find(([o])=>t.includes(o));return{kind:e==="Macintosh"?"Mac":e||"",browser:n?.[1]||""}}r(rt,"deviceNames");function bt(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):b).map(e=>({id:N(e.endpoint),host:new URL(e.endpoint).host,agent:e.agent,added:e.added,...rt(e.agent)}))}r(bt,"subscriptions");function st(t,e){const n=D(t.keys.p256dh),o=h.createECDH("prime256v1"),i=o.generateKeys(),c=v(D(t.keys.auth),o.computeSecret(n)),s=v(c,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),a=h.randomBytes(16),l=v(a,s),x=v(l,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),E=v(l,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),O=h.createCipheriv("aes-128-gcm",x,E),B=Buffer.concat([O.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),O.final(),O.getAuthTag()]),d=Buffer.alloc(4);return d.writeUInt32BE(4096),Buffer.concat([a,d,Buffer.from([i.length]),i,B])}r(st,"encrypt");function it(t){const e=J(),n=F(JSON.stringify({typ:"JWT",alg:"ES256"})),o=F(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=h.sign("sha256",Buffer.from(`${n}.${o}`),{key:h.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${F(i)}, k=${ot()}`}r(it,"authorization");async function M(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}r(M,"hubNeeds");const Et=r(t=>U(g(),t),"sendAll");async function U(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await M()).total}),i=await Promise.all(t.map(async s=>{try{const a=await fetch(s.endpoint,{method:"POST",headers:{authorization:it(s),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:st(s,o),signal:AbortSignal.timeout(15e3)});return{sub:s,status:a.status,error:a.ok?void 0:(await a.text()).slice(0,300)}}catch(a){return{sub:s,status:0,error:String(a.message||a)}}})),c=new Set(i.filter(s=>s.status===404||s.status===410).map(s=>s.sub.endpoint));return c.size&&f(b,g().filter(s=>!c.has(s.endpoint))),i.map(s=>({host:new URL(s.sub.endpoint).host,agent:s.sub.agent,status:s.status,error:s.error,dropped:c.has(s.sub.endpoint)||void 0}))}r(U,"sendTo");const T=r(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=k.homedir()){try{const e=JSON.parse(u.readFileSync(z(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}r(w,"readSettingsFile");function P(t,e=k.homedir()){const n=z(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),u.renameSync(o,n)}r(P,"writeSettingsFile");const Ot=r((t=K(),e=k.homedir())=>nt(w(e).codexShare,t),"readCodexShare");function jt(t,e=K()){if(!et.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return P({...w(),codexShare:t}),t}r(jt,"setCodexShare");const R=["opus","auto","sonnet"],vt=r((t=k.homedir())=>{const e=w(t).claudeModel;return R.includes(e)?e:"auto"},"readClaudeModel");function Bt(t,e=k.homedir()){if(!R.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return P({...w(e),claudeModel:t},e),t}r(Bt,"setClaudeModel");function $(t){return _(w(t))}r($,"readSettings");function At(t){const e=w(t);if(!e.polling&&!e.push)return null;const{push:n,polling:o,...i}=e,{quiet:c}=_(e);return P({...i,quiet:c},t),c}r(At,"migrateSettings");function ct(t){const{settings:e,error:n}=X({notifications:$().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,polling:i,...c}=w();return P({...c,quiet:e.quiet,notifications:e.notifications}),e}r(ct,"saveSettings");async function Ct(t,e=new Date){const n=$(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const s=t.snooze==="off"?null:Z(e,t.snooze,n);if(s===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=s&&tt(s)}const{notifications:i}=ct({...n,notifications:o}),c=i.on?0:await S(()=>{const{items:s}=p();return s.length&&f(m,{items:[]}),s.length});return{notifications:i,dropped:c}}r(Ct,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(m,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}r(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(C);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(C).mtimeMs>3e4){u.rmdirSync(C);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(C)}catch{}}}r(S,"withQueueLock");function at(t=new Date){const e=$(),{items:n}=p(),o=I(t,e);return{settings:e,held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?G(t,e):null}}r(at,"queueStatus");const Q=["action","prompt","fault","reminder"];class ut extends Error{static{r(this,"PushKindError")}}const dt=r(t=>Q.includes(t)?null:`a push needs --kind ${Q.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Ft(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const c=dt(e);if(c)throw new ut(c);if(!g().length)return{noDevice:!0};const s={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},a=I(i,$());if(a?.why==="off")return{dropped:!0};const l=a!==null;let x=!1;if(n||o&&!l){const d=await U(g(),s);if(T(d))return s.tag&&await S(()=>f(m,{items:p().items.filter(A=>A.tag!==s.tag)})),{sent:d};if(n)return{sent:d};x=!0}const E={id:h.randomUUID(),...s,kind:e,queuedAt:i.toISOString()},O=await S(()=>{const d=p(),A=E.tag?d.items.filter(W=>W.tag!==E.tag):d.items,H=A.length<d.items.length;return d.items=[...A,E],f(m,d),{replaced:H}}),B=at(i);return{queued:{...O,count:B.count,nextFlushAt:B.nextFlushAt,held:l,hold:a,sendFailed:x}}}r(Ft,"deliver");async function Dt(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(c=>c.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),f(m,o),i.body):null})}r(Dt,"reviseQueued");async function Pt(t,e){const n=new Set(t),o=r(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),c=i.filter(a=>!o(a)),s=i.length-c.length;return s&&f(m,{items:c}),s}):0}r(Pt,"dropQueued");let q=null;function $t(){return q||=(async()=>{const t=await S(()=>{const c=p();return c.items.length&&f(m,{items:[]}),c.items});if(!t.length)return null;const e=[],n=[],o=[],i=await M();for(const c of Y(t,i.sites)){const s=await U(g(),{title:c.title,body:c.body,url:c.url,tag:c.tag},i);o.push(...s),(T(s)?e:n).push(c)}return n.length&&await S(()=>{const c=p(),s=new Set(c.items.map(l=>l.tag).filter(Boolean)),a=t.filter(l=>n.some(x=>x.members.includes(l.id))&&!(l.tag&&s.has(l.tag)));f(m,{items:[...a,...c.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{q=null}),q}r($t,"flushQueue");export{R as CLAUDE_MODEL_VALUES,Q as PUSH_KINDS,ut as PushKindError,St as addSubscription,Ft as deliver,rt as deviceNames,Pt as dropQueued,$t as flushQueue,At as migrateSettings,ot as publicKey,dt as pushKindError,at as queueStatus,vt as readClaudeModel,Ot as readCodexShare,$ as readSettings,w as readSettingsFile,kt as removeSubscription,xt as removeSubscriptionById,Dt as reviseQueued,ct as saveSettings,Et as sendAll,Bt as setClaudeModel,jt as setCodexShare,Ct as setNotifications,bt as subscriptions,P as writeSettingsFile};
|
package/hub/readiness.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),f.renameSync(a,e),r}i(W,"reportSlack");const D=i((t,n)=>{const s=Math.floor((n-Date.parse(t))/6e4);return s<1?"just now":s<60?`${s} min ago`:s<1440?`${Math.floor(s/60)} h ago`:`${Math.floor(s/1440)} days ago`},"age"),u=i((t,n,s)=>({settings:t,label:`Hub settings \u203A ${n}`,text:s}),"settingsFix");function $(t,{now:n=Date.now(),platform:s=process.platform}={}){const d=i((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:u("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:u("linear","Linear","Add one in")};case"slack":{const r=e?`${e.title}\u2019s session reported ${e.ready?"a":"no"} Slack connector, ${D(e.at,n)}`:"No board session has reported yet";return e?.ready?{ready:!0,detail:r}:{ready:!1,missing:r,fix:{text:"Add Slack to Claude\u2019s connectors. The next board session to start reports it."}}}case"tailscale":{let r,a;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(r=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,a="sudo tailscale set --operator=$USER"):(r="Tailscale / is not serving the hub",a="kickflip doctor --fix"):(r="Tailscale isn't signed in",a="tailscale up"):(r="Tailscale isn't installed on the host",a=F(s)),r?{ready:!1,missing:r,fix:{command:a}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices?{ready:!0,detail:`Pushes on \xB7 ${e.devices} device${e.devices===1?"":"s"}`}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:u("notifications","Notifications","Turn on notifications in")}}},"row");return x.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,r])=>({id:o,name:e,...d(o,t[o]),waits:r}))}i($,"readinessRows");const H=i(t=>(n,s)=>new Promise(d=>{N(n,s,{env:t,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function A({home:t=m.homedir(),env:n=process.env,now:s=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=w({home:t,env:n,now:s,cacheMs:d}),r={github:i(()=>P({env:n}).catch(()=>null),"github"),codex:e,linear:i(()=>R(t),"linear"),slack:i(()=>j(t),"slack"),tailscale:i(()=>I({run:H(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:L({env:n})}),"tailscale"),notifications:i(()=>({on:C(t).notifications.on,devices:O(t).length}),"notifications"),...o},a=new Map;async function g(l,c){if(!["github","tailscale"].includes(l))return r[l]({fresh:c});const p=a.get(l);if(!c&&p&&s()-p.at<d)return p.value;const h=Promise.resolve().then(()=>r[l]());return a.set(l,{at:s(),value:h}),h}i(g,"check");const b=i(async({fresh:l=!1}={})=>$(Object.fromEntries(await Promise.all(x.map(async([c])=>[c,await g(c,l)]))),{now:s()}),"rows");async function S(l){const[c]=$({[l]:await g(l,!1)},{now:s()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return i(S,"waiting"),{rows:b,waiting:S}}i(A,"readinessReader");const M=w(),X=A({probes:{codex:M}});export{M as readCodex,j as readSlackReport,X as readiness,A as readinessReader,$ as readinessRows,W as reportSlack};
|
|
1
|
+
var k=Object.defineProperty;var t=(s,n)=>k(s,"name",{value:n,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as S}from"./pr-report.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as P}from"./linear.mjs";import{slackStatus as T}from"./slack.mjs";import{readSettings as R,subscriptions as C}from"./push.mjs";import{tailscaleState as N}from"../bin/install.mjs";import{tailscaleBin as I,tailscaleInstallCommand as L}from"../shared/tailscale.mjs";const w=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],f=t((s,n,a)=>({settings:s,label:`Hub settings \u203A ${n}`,text:a}),"settingsFix");function x(s,{now:n=Date.now(),platform:a=process.platform}={}){const d=t((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:f("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:f("linear","Linear","Add one in")};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",fix:f("slack","Slack","Paste a new one in")}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`}:{ready:!1,missing:"No Slack token set",fix:f("slack","Slack","Add one in")};case"tailscale":{let i,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(i=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(i="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(i="Tailscale isn't signed in",c="tailscale up"):(i="Tailscale isn't installed on the host",c=L(a)),i?{ready:!1,missing:i,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:i,kind:c,browser:m,added:p})=>({id:i,kind:c,browser:m,added:p}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:f("notifications","Notifications","Turn on notifications in")}}},"row");return w.filter(([o])=>Object.hasOwn(s,o)).map(([o,e,i])=>({id:o,name:e,...d(o,s[o]),waits:i}))}t(x,"readinessRows");const O=t(s=>(n,a)=>new Promise(d=>{$(n,a,{env:s,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function H({home:s=b.homedir(),env:n=process.env,now:a=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=h({home:s,env:n,now:a,cacheMs:d}),i={github:t(()=>S({env:n}).catch(()=>null),"github"),codex:e,linear:t(()=>P(s),"linear"),slack:t(()=>T(s),"slack"),tailscale:t(()=>N({run:O(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:I({env:n})}),"tailscale"),notifications:t(()=>({on:R(s).notifications.on,devices:C(s)}),"notifications"),...o},c=new Map;async function m(l,r){if(!["github","tailscale"].includes(l))return i[l]({fresh:r});const g=c.get(l);if(!r&&g&&a()-g.at<d)return g.value;const u=Promise.resolve().then(()=>i[l]());return c.set(l,{at:a(),value:u}),u}t(m,"check");const p=t(async({fresh:l=!1}={})=>x(Object.fromEntries(await Promise.all(w.map(async([r])=>[r,await m(r,l)]))),{now:a()}),"rows");async function y(l){const[r]=x({[l]:await m(l,!1)},{now:a()});return r.ready||!r.waits?null:r.waits.replace("this",`${r.name}: ${r.missing.replace(/\.$/,"")}`)}return t(y,"waiting"),{rows:p,waiting:y}}t(H,"readinessReader");const A=h(),q=H({probes:{codex:A}});export{A as readCodex,q as readiness,H as readinessReader,x as readinessRows};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var $=Object.defineProperty;var f=(t,s)=>$(t,"name",{value:s,configurable:!0});import{listBoards as p}from"./boards.mjs";import{savedSlack as l}from"./slack.mjs";import{reviewDue as y,reviewSent as h,reviewReact as v,reviewDigestSent as g}from"../board/store.mjs";const u=f(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function k(t,s,d){const a=await y(t.dir,{now:d});if(a.waiting)return{sent:[],failed:[]};const c=[],o=[],r=f(async(e,n)=>{try{await n(),c.push(e)}catch(i){if(u(i))throw i;o.push(`${t.route}: ${e}: ${i.message}`)}},"attempt");for(const e of a.dms)await r(`link to ${e.reviewer} on ${e.key}`,async()=>{const n=await s.openDM(e.slack_user),i=await s.post({channel:n,text:e.text});await h(t.dir,e.key,e.reviewer,{ts:i,channel:n})});for(const e of a.reactions)await r(`${e.emoji} for ${e.reviewer} on ${e.key}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await v(t.dir,e.key,e.reviewer,e.emoji)});for(const e of a.digest?.reviewers||[]){const n=e.items.length;await r(`digest to ${e.name}`,async()=>{const i=await s.openDM(e.slack_user),m=await s.post({channel:i,text:`\u{1F916} ${t.title} digest: ${n} PR${n===1?"":"s"} pending your review`});for(const w of e.items)await s.post({channel:i,thread_ts:m,text:`${w.url} \xB7 ${w.title}${w.days_old?` (${w.days_old} days old)`:""}`});await g(t.dir,e.name,{ts:m,channel:i,items:e.items.map(w=>w.key).join(",")},{now:d})})}return{sent:c,failed:o}}f(k,"sendBoard");async function S({boards:t=p(),fetch:s,now:d=new Date}={}){const a=l({fetch:s}),c=[],o=[];if(!a)return{sent:c,failed:o};for(const r of t)try{const e=await k(r,a,d);c.push(...e.sent.map(n=>`${r.route}: ${n}`)),o.push(...e.failed)}catch(e){if(o.push(`${r.route}: ${e.message}`),u(e))break}return{sent:c,failed:o}}f(S,"sendReviews");export{S as sendReviews};
|
package/hub/secrets.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var d=Object.defineProperty;var c=(r,e)=>d(r,"name",{value:e,configurable:!0});import n from"node:fs";import o from"node:path";import m from"node:os";const l=c((r=m.homedir())=>o.join(r,".claude","kickflip"),"dirOf"),a=c(r=>o.join(l(r),"secrets.json"),"secretsFile");function f(r,e){n.mkdirSync(o.dirname(r),{recursive:!0});const t=`${r}.${process.pid}.tmp`;n.writeFileSync(t,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,{mode:384}),n.chmodSync(t,384),n.renameSync(t,r)}c(f,"write");function p(r){const e=a(r);let t={};try{t=JSON.parse(n.readFileSync(e,"utf8"))||{}}catch{}const i=o.join(l(r),"linear.json");if(n.existsSync(i)){let s=null;try{s=JSON.parse(n.readFileSync(i,"utf8"))}catch{}!t.linear&&s&&(t={...t,linear:s},f(e,t)),n.rmSync(i,{force:!0})}return t}c(p,"readAll");const h=c((r,e)=>p(e)[r]||null,"readSecret");function x(r,e){const{[r]:t,...i}=p();f(a(),e?{...i,[r]:e}:i)}c(x,"writeSecret");export{h as readSecret,a as secretsFile,x as writeSecret};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var y=Object.defineProperty;var e=(n,i)=>y(n,"name",{value:i,configurable:!0});import d from"node:fs";import h from"node:path";import{execFileSync as b}from"node:child_process";const g=["index.lock","MERGE_HEAD","rebase-merge","rebase-apply","CHERRY_PICK_HEAD","REVERT_HEAD"];function w({root:n,files:i,restart:p,log:s=e(()=>{},"log"),git:c=e((...r)=>b("git",r,{cwd:n,encoding:"utf8"}),"git")}){const r=d.existsSync(h.join(n,".git"));let a=!1;function u(){try{const t=c("rev-parse","HEAD").trim();return a=!1,t}catch(t){return a||s(`self-restart can't read HEAD: ${t.message.split(`
|
|
2
|
+
`)[0]}`),a=!0,null}}e(u,"head");const f=r?u():null;let l=null;function o(t,R){if(l===t)return;l=t,s(`restarting the hub: ${R}`);const m=p();m&&s(`hub restart failed: ${m}`)}e(o,"attempt");const E=e(()=>g.some(t=>d.existsSync(h.join(n,".git",t))),"gitBusy"),A=e(()=>{try{return c("diff","--quiet","HEAD","--",...i),!0}catch{return!1}},"filesMatchHead");function H(){if(!r||E())return;const t=u();t===null||f===null||t===f||!A()||o(t,`HEAD moved to ${t.slice(0,7)} and the hub's files match it`)}e(H,"check");function D(){o(`npm-${Date.now()}`,"kickflip update finished")}return e(D,"restartNow"),{check:H,restartNow:D}}e(w,"restarter");export{w as restarter};
|