@lovek4d/kickflip 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +32 -16
- package/SKILL.md +8 -6
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +8 -8
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +50 -28
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -3
- package/board/headless.mjs +4 -3
- package/board/merge3.mjs +1 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -2
- package/board/public/app-asks.js +6 -89
- package/board/public/app-cards.js +19 -10
- package/board/public/app-core.js +1 -20
- package/board/public/app-reader.js +174 -0
- package/board/public/app-threads.js +49 -35
- package/board/public/app.html +2 -0
- package/board/public/app.js +28 -16
- package/board/public/hub.css +1 -0
- package/board/public/new-task.js +121 -0
- package/board/public/shell.js +4 -0
- package/board/public/sidebar.css +1 -0
- package/board/public/style.css +91 -3
- package/board/public/ui-cards.js +46 -21
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-phase-bars.js +4 -4
- package/board/public/ui-questions.js +47 -18
- package/board/public/ui-sidebar.js +9 -3
- package/board/public/ui-threads.js +25 -12
- package/board/public/ui.js +42 -12
- package/board/public/view-frame.js +1 -0
- package/board/public/view-questions.js +45 -16
- package/board/public/view-settings.js +38 -19
- package/board/server.mjs +20 -18
- package/board/store.mjs +15 -13
- package/board/worktree.mjs +20 -20
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +20 -0
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +9 -12
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -0
- package/hub/readiness.mjs +2 -2
- package/hub/sessions.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/package.json +1 -1
- package/shared/owner-check.mjs +3 -0
- package/shared/tailscale.mjs +1 -0
- package/shared/usage-gate.mjs +1 -1
package/hub/hub.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Rt=Object.defineProperty;var l=(e,t)=>Rt(e,"name",{value:t,configurable:!0});import Re from"node:http";import Et from"node:net";import{spawn as Ee,execFileSync as Dt}from"node:child_process";import w from"node:fs";import $ from"node:path";import De from"node:os";import{fileURLToPath as Lt}from"node:url";import Le from"node:vm";import Ct from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as At,addSubscription as Ht,removeSubscription as It,sendAll as Mt,flushQueue as Ce,queueStatus as Ae,saveSettings as Bt,readSettings as V,migrateSettings as Ft,setNotifications as Ut,readCodexShare as He,setCodexShare as Nt,readClaudeModel as Gt,setClaudeModel as zt,deliver as Wt,pushRoute as Jt}from"./push.mjs";import{ownerTracker as Kt}from"./presence.mjs";import{POLL_MS as Ie,answerOf as Vt,expired as Zt,pendingFor as Me,promptRecord as Qt,pushOf as Xt,updatePrompts as M,shown as Yt,pruneRemembered as fe,rememberRules as qt,rememberedAnswer as en,rememberedFor as Be}from"./prompts.mjs";import{hold as tn,heldUntil as nn,holdLine as an,editQuiet as sn,checkEvery as Fe,nextCheckAt as rn,LOW_POINTS as on}from"./schedule.mjs";import{capital as cn,hubView as un,settingsView as ln,flushView as dn,SNOOZE_REFUSED as hn,AWAKE_REFUSED as fn}from"./views.mjs";import{caffeinate as pn,setKeepAwake as mn,awakeUntil as Ue,awakeUnsupported as gn}from"./awake.mjs";import{checkGitHub as wn,sendWakes as bn}from"./github.mjs";import{checkIncoming as yn}from"./incoming.mjs";import{listBoards as R,isListening as Sn,isBoardSite as Ne,listenState as Ge,RESUME_STATE as kn,sendToBoards as ze,checkListening as $n,recordHubCode as _n,runningHubCode as xn,boardCode as We,recordLinkFaults as vn,linksOf as Tn,hubLink as On,hubCodeState as jn,readJSON as Pn}from"./boards.mjs";import{liveSessions as Je}from"./live.mjs";import{RuleError as Rn,applyRules as En,changed as Ke,readHubRules as pe,writeHubRules as Dn}from"./rules.mjs";import{siteIcon as Ln}from"./icons.mjs";import{readNeeds as Cn,sidebarSites as An,startRoute as Hn,hubHealth as In}from"./home.mjs";import{checkSessions as Mn,compactFullContexts as Bn,startingBoards as Fn}from"./sessions.mjs";import{repoService as Un,repoLookupRoute as Nn}from"./repos.mjs";import{boardRepo as Ve,changeBoardRepo as Gn,migrateBoardRepos as zn,migrateHubBoard as Wn}from"./board-repo.mjs";import{requestBoard as Jn,retryBoard as Kn,dismissBoard as Vn,clonePercent as Zn,settleStarts as Qn}from"./new-board.mjs";import{boardClosure as Xn,closeBoard as Yn,closedBoards as qn,reopenBoard as ea}from"./close-board.mjs";import{restartBoard as ta,restartable as na}from"./restart-board.mjs";import{crossSite as aa,isJSON as me}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as ge,appFile as sa,colorsVersion as Ze,destOf as ra,fileVersion as oa,pageOf as Qe,shellHtml as ia}from"./shell.mjs";import{hubStream as ca}from"./stream.mjs";import{usageReader as ua,checkUsagePause as la,contextReading as da}from"./usage.mjs";import{readUsageGate as Xe,setPauseAt as ha,setKeepGoing as fa}from"../shared/usage-gate.mjs";import{readiness as pa,readCodex as Z}from"./readiness.mjs";import{BuildError as ma,startBuild as ga,stopTries as wa,tryTick as we,initializeTries as ba,fetchTryBranches as Ye,pullNow as ya,pullMains as Sa}from"./try.mjs";import{serveFiles as ka}from"./files.mjs";import{upgrader as $a,CHECK_MS as _a}from"./upgrade.mjs";import{fallBackFromLinear as xa,openBoardBuilders as qe}from"../board/store.mjs";import{recordRefusal as va}from"../board/permissions.mjs";import{hubGate as et}from"../board/on-host.mjs";import{pipeUpgrade as Ta,refuse as Oa,refuseUpgrade as ja}from"./front-door.mjs";import{restartRunner as Pa}from"../board/worktree.mjs";import{hubService as Ra}from"../shared/service.mjs";import{cloneRoot as tt}from"../shared/repos.mjs";import{LinearError as be,linearStatus as Q,removeLinearKey as Ea,saveLinearKey as Da,savedClient as nt}from"./linear.mjs";const X=$.dirname(Lt(import.meta.url)),La=$.join(X,"..","shared","colors.css"),c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),Y=$.join(De.homedir(),".claude","kickflip"),at=$.join(Y,"manifest.json"),q=$.join(Y,"logs"),st=$.join(Y,"run"),ye=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Y,q,st])w.mkdirSync(e,{recursive:!0});try{const e=_n();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 zn();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 Wn();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 L=Un(),Ca=ua(),ee=pn({log:c});ee.apply();let te={version:1,sites:{}};function g(){try{te=JSON.parse(w.readFileSync(at,"utf8")),te.sites||={}}catch(e){e.code==="ENOENT"?te={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return te}l(g,"loadManifest");const O=new Map,C=new Set;let ne=!1;const rt=5e3,ae=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Se=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),B=l(e=>Se(e)?"cmd":e?.port?"port":"files","servesOf"),F=new Map;function se(e){return $.join(st,e+".pid")}l(se,"pidFile");function Aa(e,t){try{const n=Number(w.readFileSync(se(e),"utf8")),r=n?Dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{w.unlinkSync(se(e))}catch{}}l(Aa,"killStale");function Ha(e,t){const n=O.get(e)||{restarts:0},r=w.openSync($.join(q,e+".log"),"a"),[a,...d]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};w.writeSync(r,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=
|
|
5
|
-
`),w.closeSync(s),Object.assign(n,{child:null,key:ae(t),state:"crashed",exitCode:null,nextStart:Date.now()+it(n.restarts++)}),O.set(e,n);return}w.closeSync(s),w.writeFileSync(re(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=u((b,k)=>{if(C.delete(i),n.child!==i)return;try{w.unlinkSync(re(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:b??k,nextStart:Date.now()+it(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${b??k})`)},"ended");i.on("error",b=>{c(`${e} spawn error: ${b.message}`),i.pid||S(b.code,null)}),i.on("exit",S)}u(Ea,"start");const it=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function se(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")},ot)}u(se,"stop");const oe=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Se(e){return new Promise(t=>{const n=Ee.connect({port:e,host:"127.0.0.1"}),s=u(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Se,"probe");let ct=!0;const H=new Set;let ke=!1;const N=new Set,Da=3e4;async function La(e){const t=Date.now()+Da;for(;;){const n=j(g().sites,e),s=O.get(e);if(n&&s?.child&&s.up&&s.key===ae(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}u(La,"serverAnswers");async function ut(){if(ne)return;const{sites:e}=g();for(const t of[...O.keys()])H.has(t)||(j(e,t)?ye(j(e,t))||(c(`${t} no longer has a command; stopping it`),se(t),O.delete(t)):(c(`${t} left the manifest; stopping it`),se(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 Se(n.port));for(const[t,n]of Object.entries(e)){if(!ye(n))continue;ct&&Ra(t,n);const s=O.get(t);if(s&&s.child&&s.key!==ae(n)){c(`${t} entry changed; restarting it`),se(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const a=s?.child&&s.code&&!s.stopping&&!N.has(t)&&!H.has(t)?Je(n):null;if(a&&a!==s.code){if(!(await et(n.dir)).length){c(`${t} runs older code than its files; restarting it`),se(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==a&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=a}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&Ea(t,n)}ct=!1;for(const[t,n]of O){const s=j(e,t);s&&(n.up=n.child?await Se(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(ut,"reconcile");const Ca=setInterval(()=>ut().catch(e=>c("reconcile failed:",e.message)),2e3);ut().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 lt=u(async()=>{try{for(const e of await $a())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await lt();const Ha=6e4,dt=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+Ha,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0},A=Kt(),ht=u(()=>y.startedAt?sn({last:new Date(y.startedAt),...A.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function ft({changed:e,back:t}){e&&_.refresh(),t&&y.startedAt&&!y.running&&$e().catch(n=>c("poll failed:",n.message))}u(ft,"presenceChanged");function $e({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=V();let s;try{s=await wn({every:Ne({present:!1,remaining:y.remaining}),manifest:g()})}catch(a){s={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await qe({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ge({manifest:g(),routes:s.merged}).catch(a=>c("merge refresh failed:",a.message))),await qe({manifest:g(),log:c});try{const a=await ba({manifest:g()});a.length&&ge({manifest:g(),routes:a}).catch(l=>c("pull refresh failed:",l.message))}catch(a){c("pull failed:",a.message)}try{s.failed.push(...(await yn({manifest:g()})).failed)}catch(a){s.failed.push(`incoming PRs: ${a.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>dt&&(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(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:l}of s.linear.moved)c(`linear: ${a} \u2192 ${l}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>dt&&(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.deaf=await kn({manifest:g()})}catch(a){s.deaf={pushed:[],failed:[a.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{xn(g())}catch(a){c("could not record link faults:",a.message)}if(s.flushed=null,s.held=nn(t,n)!==null,e&&!s.held)try{const a=await He();a&&(s.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 y.last=s,s})().finally(()=>{y.running=null}),y.running}u($e,"runPoll");let pt=null;function Aa(){pt||=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(()=>{pt=null})}u(Aa,"runWakes");const _e=new Set,D={running:null};function mt(){if(D.running||H.size||N.size)return;const e=new Set(_e);_e.clear(),D.running=In({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(()=>Mn({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})}u(mt,"runSessionCheck"),await ga(g());const Ia=Date.now(),U=new Map;function G(e){e?(ie.delete(e),_.changed(e)):_.refresh()}u(G,"promptChanged");async function gt(){const{gone:e,rulesChanged:t}=await M(n=>{const s=Zt(n,{now:Date.now(),aliveSince:Ia}),a=he(n,xe());for(const l of s)delete n.prompts[l.id];return{gone:s,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`)}u(gt,"prunePrompts");const xe=u(()=>({running:Ke(),boards:R(g())}),"promptContext");async function wt(e){const t=R(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await _a(Ze(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(wt,"noteRefusal");let z=null,bt=null;async function Te(){return z?bt??=z.catch(()=>{}).then(()=>(bt=null,Te())):(z=(async()=>ca({gate:Ye(),resume:u(async e=>{const t=await We(R(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{z=null}),z)}u(Te,"checkUsageGate"),setInterval(()=>{ge({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)),A.check().then(ft,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=ht().getTime()&&$e().catch(e=>c("poll failed:",e.message))}),Aa(),ee.apply(),gt().catch(e=>c("prompt expiry failed:",e.message)).finally(mt)},15e3);function yt(){if(ne)return;ne=!0,clearInterval(Ca),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;ma().finally(()=>{e=!0});const t=Date.now()+ot;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)}u(yt,"shutdown"),process.on("SIGTERM",yt),process.on("SIGINT",yt);const St=new Map;function kt(e,t={n:3e3}){let n=0,s;try{s=w.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of s){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const l=$.join(e,a.name);if(a.isDirectory())n=Math.max(n,kt(l,t));else try{n=Math.max(n,w.statSync(l).mtimeMs)}catch{}}return n}u(kt,"newestMtime");function Ma(e,t){const n=St.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=kt(t.dir);return St.set(e,{at:s,checked:Date.now()}),s}u(Ma,"changedAt");const $t=u(e=>{try{return typeof e=="string"&&w.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ve(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=O.get(t)||{},a=B(n),l=a==="files"?$t(n.dir)?"up":"down":a==="port"?F.has(t)?F.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:a,state:l,exitCode:s.exitCode??null,owner:n.owner||null,restart:ea(n),changedAt:Ma(t,n)||null,created:n.created||null,log:$.join(q,t+".log")}})}u(ve,"status");const I=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),ie=new Map,_t=2e4,Ba=1500;async function Fa(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Ba)});return t.ok?Ln(await t.json()):null}catch{return null}}u(Fa,"fetchNeeds");async function xt(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const l=ie.get(a.route);(t===!0||t?.has?.(a.route)||!l||Date.now()-l.at>=_t)&&ie.set(a.route,{at:Date.now(),needs:Fa(a.port)})}const s=await Promise.all(n.map(a=>ie.get(a.route).needs));return new Map(n.map((a,l)=>[a.route,s[l]]))}u(xt,"waitingCounts");const Tt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Be(null).length,"badgeTotal"),Oe=new Map;function Na(e){const t=new Map,n=new Map,s={running:Ke(),wakes:jn(Sn,{})};for(const a of e)try{const l=ze(a,s);n.set(a.route,l);let h=Oe.get(a.route);(!h||h.listen!==l||Date.now()-h.at>=_t)&&(h={at:Date.now(),listen:l,links:Tn(a,{listen:l})},Oe.set(a.route,h)),t.set(a.route,h.links)}catch(l){c(`links of ${a.route} failed:`,l.message),t.set(a.route,[])}return{links:t,listens:n}}u(Na,"boardLinks");async function ce({fresh:e=!1}={}){const t=new Date,n=ve(),s=await xt(n,{fresh:e}),a=g(),l=R(a),{links:h,listens:i}=Na(l),S=On(),b=new Map(l.map(r=>[r.route,ua(r.owner?.session)]).filter(([,r])=>r)),k=Cn(n,s,new Set(l.map(r=>r.route)),i,b),m=V(),p=tn(t,m);return{data:{code:S.running??null,total:Tt(s),start:Hn(k),sites:k,hub:An(k,h,vn(S)),home:w.realpathSync(Le.homedir()),starting:Bn(a,t.getTime(),Vn),usage:Pa({codexShare:Ie(Z())}),pause:Ye(),upgrade:W.state(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:cn(an(p,t))}:{state:"on",text:"On"},away:!A.state().present},links:h,rows:n,code:S}}u(ce,"sidebarState");function vt(e,t,n,s,a){let l=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=Ct.gzipSync(l),h["content-encoding"]="gzip"),h["content-length"]=l.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:l)}u(vt,"sendText");async function Ua(e){const{data:t}=await ce(),n=Ce.createContext({});for(const s of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Ce.runInContext(w.readFileSync($.join(me,s),"utf8"),n);return sa({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:ue(180)})}u(Ua,"shellPage");const W=Sa({root:nt(),builders:u(async()=>(await Promise.all(R(g()).map(e=>et(e.dir)))).flat(),"builders"),run:Ga,log:c,changed:u(()=>_.refresh(),"changed")});function Ga(){const e=$.join(q,"kickflip-update.log");w.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
-
`);const t=w.openSync(e,"a");return c("running kickflip update"),new Promise(n=>{const
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${a}: ${
|
|
8
|
-
<title>${I(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Qe()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Wa=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ja=u(()=>({running:!!y.running,nextAt:ht(),last:y.last,present:A.state().present,every:Ne({present:A.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<on}),"pollState");async function le(e){await gt();const t=new Date,{data:n,links:s,rows:a,code:l}=await ce();return{...un({now:t,st:Ae(t),poll:Ja(),sidebar:n,rows:a,links:s,rules:fe(),integrations:await ha.rows(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ie(Z()),claudeModel:Gt(),codex:Z(),awake:Ue(t),remembered:new Map(R(g()).map(h=>[h.route,Fe(h.route)]))}),prompts:Be(null).map(Yt),remembered:Fe(null)}}u(le,"hubData");const J=u(async()=>{const e=new Date,t={until:Ue(e),unsupported:gn()};return{...ln({now:e,st:Ae(e),rules:fe(),route:await Jt(),linear:Q(),awake:t}),closed:Xn()}},"settingsData"),P=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ka(e,t,n){const s=u((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=u((r,o)=>{const d=_n();return s(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),l=u(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),_.refresh()}),_.refresh(),a(200,{route:r})),"starting"),h=Wa(e),i=n.pathname,S=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(S&&e.method==="GET")try{return a(200,await Zn(S[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let r;try{const o=await x(e);if(r=o.route,i==="/_hub/reopen-board"){const v=await Yn(r);return _.refresh(),a(200,v)}if(H.has(r))return a(409,{error:"That board is already closing."});H.add(r);const d=u(()=>H.delete(r),"release");t.once("finish",d),t.once("close",d),await D.running;const f=await Qn(r,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"){if(!await tt(e))return a(403,{error:"Only the signed-in owner can restart a board."});let r;try{if(r=(await x(e)).route,N.has(r))return a(409,{error:"That board is already restarting."});N.add(r),await D.running;const o=await qn(r,{serverUp:La,portUp:Se,onStarting:u(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{N.delete(r),_.refresh()}}if(i==="/_hub/restart-hub"&&e.method==="POST")try{return await x(e),await tt(e)?ke?a(409,{error:"The hub is already restarting."}):(ke=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=xa("hub");r&&(ke=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ta().restartCommand})):a(403,{error:"Only the signed-in owner can restart the hub."})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await x(e),a(200,{upgrade:await W.request()})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/presence"&&e.method==="POST")return await x(e),ft(A.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(i==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await x(e),o=R(g()).find(d=>d.route===r);return o?(_e.add(r),await D.running,mt(),await D.running,Oe.clear(),_.changed(r),a(200,{route:r,listen:ze(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(i==="/_hub/repo"&&e.method==="GET")return Nn(n,{repos:L,manifest:g(),sendJSON:a});const b=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(b&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?Ze(b[1]):await Un(b[1],await x(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!pe(e.headers))return s(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await x(e)}catch{return s(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return l(await Wn(r,{repos:L,changed:u(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return s(o.status,"text/plain; charset=utf-8",o.message);throw o}}const k=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(k&&e.method==="POST"){if(!pe(e.headers))return s(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return l(await Jn(k[1],{repos:L,changed:u(()=>_.refresh(),"changed")}))}catch(r){if(r.status)return s(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const r=await Kn(m[1]);return _.refresh(),a(200,r)}catch(r){if(r.status)return s(r.status,"text/plain; charset=utf-8",r.message);throw r}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 r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${i==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=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 wa(d,{manifest:g()})):a(202,await pa(d,{manifest:g(),log:c}))}catch(f){if(f instanceof fa)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return s(200,"text/javascript; charset=utf-8",w.readFileSync($.join(X,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return s(200,"application/manifest+json",za());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return s(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 r=n.searchParams.get("v")===Qe()?"public, max-age=31536000, immutable":"no-cache";try{return s(200,"text/css; charset=utf-8",w.readFileSync(ja),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return s(200,"image/png",w.readFileSync($.join(me,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return s(200,"text/css; charset=utf-8",w.readFileSync($.join(me,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await ce({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const r=na(i.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===ra(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?s(200,d,w.readFileSync(r),{"cache-control":f}):vt(e,t,200,{"content-type":d,"cache-control":f},w.readFileSync(r))}if(i==="/_hub/api/hub")return a(200,await le(e));if(i==="/_hub/api/settings")return a(200,await J());if(i==="/_hub/push/key")return a(200,{key:Ht()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:At(await x(e),`https://${r}/`,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 r=await Mt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):s(400,"text/plain; charset=utf-8",f);let v;try{v=await Nt(d)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:hn}):P(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?a(200,await J()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{mn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):s(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 r=new URLSearchParams(await E(e));try{Ut(r.get("value"),Z({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):s(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{zt(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):s(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?la(r.get("value")):da(r.get("value")),await Te()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):s(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(va(),c("linear: key removed"),await lt()):c(`linear: key ${(await Oa(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof we))throw o;return h?a(o.status,{error:o.message}):s(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 r=Q();if(!r.set||r.refused)return a(200,r);const o=at();try{const d=await o.teams();return a(200,{...Q(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof we))throw d;return a(200,{...Q(),error:d.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const r=at();if(!r)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 d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof we))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Bt(rn(V(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):s(400,"text/plain; charset=utf-8",d.message)}return _.refresh(),h?a(200,await J()):P(t,"/_hub/#quiet")}if(i==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await x(e,64*1024),o=Rn(fe(),r.rules,r.base??[]),Ve(o.changes)&&En(o.rules)}catch(f){if(!(f instanceof Pn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Ve(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await We(R(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(i==="/_hub/push/flush"&&e.method==="POST"){const r=await He();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,dn(r)):P(t,"/_hub/")}if(i==="/_hub/poll"&&e.method==="POST"){const r=await x(e);return a(200,await $e({flush:r.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await x(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await M(f=>{const v=xe(),K=v.boards.find(Pe=>Pe.owner?.session&&Pe.owner.session===r.session_id)||null;he(f,v)&&_.refresh();const je=en(f,r);if(je)return{answer:je};const de=Qt(r,{board:K,live:v.running?.get(r.session_id)});return f.prompts[de.id]=de,{rec:de}});return o?a(200,{state:"answered",answer:o}):(G(d.route),await wt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),Wt(Xt(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(i==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await x(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await wt(r)})}const T=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return T?Va(e,t,{id:T[1],op:T[2]?.slice(1)||null,url:n,sendJSON:a}):!1}u(Ka,"hubRoute");async function Va(e,t,{id:n,op:s,url:a,sendJSON:l}){if(s==="wait"&&e.method==="GET"){const h=u(()=>M(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=u(m=>(G(m.route),l(200,{state:"answered",answer:m.answer})),"take"),S=await h();if(!S)return l(404,{state:"gone"});if(S.answer)return i(S);const b=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Me,Me));if(await new Promise(m=>{const p=U.get(n)||new Set;U.set(n,p);const T=u(()=>{clearTimeout(r),p.delete(T),!p.size&&U.get(n)===p&&U.delete(n),m()},"done"),r=setTimeout(T,b);p.add(T),t.on("close",T)}),t.writableEnded||t.destroyed)return;const k=await h();return k?k.answer?i(k):l(200,{state:"pending"}):l(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const h=await x(e),i=await M(k=>{const m=j(k.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:T}=Vt(m,h);if(T)return{status:400,error:T};m.answer=p;const r=xe();return he(k,r),qt(k,m,r),{rec:m,answer:p}});if(i.error)return l(i.status,{error:i.error});const{rec:S,answer:b}=i;c(`prompt ${n}: ${b.choice}`);for(const k of[...U.get(n)||[]])k();return G(S.route),l(200,{ok:!0})}if(s===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`)),l(200,{ok:!0})}return!1}u(Va,"promptRoute");const Za=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function jt(e,t){const n=O.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=B(t),[l,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${s}`],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`${Ot(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
4
|
+
`),n.code=Ne(t)?We(t):null;let i;try{i=Ee(a,d,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(k){w.writeSync(r,`hub could not start it: ${k.message}
|
|
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=w.openSync(e,"a");return c("running kickflip update"),new Promise(n=>{const r=Ee(process.execPath,[$.join(tt(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});w.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const d=w.readFileSync(e,"utf8").trim().split(`
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${a}: ${d}`),n(a===0?{ok:!0}:{ok:!1,error:d.replace(/^kickflip: /,"")})})})}l(Va,"runUpdate"),W.check(),setInterval(()=>W.check(),_a);const _=ca({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Ne(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>R(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await ue({fresh:e})).data,"sidebar"),log:c});function le(e){let t="0";try{t=Math.floor(w.statSync($.join(X,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(le,"iconUrl");const Za=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:le(192),sizes:"192x192",type:"image/png"},{src:le(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const r of e)if(n+=r,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function x(e,t){if(!me(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(x,"readJSONBody");const Tt=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>${I(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${le(180)}">
|
|
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">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
11
|
<h1>${I(t.title||e)} isn't answering</h1>
|
|
12
12
|
<p class="lede">${I(i)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${es({icon:Ln(e,t),title:I(t.title||e),sub:`<span class="st ${d}">${I(h)}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}
|
|
17
|
-
`)+`\r
|
|
18
|
-
\r
|
|
19
|
-
`),n&&n.length&&i.write(n),i.pipe(t),t.pipe(i)});i.on("error",()=>t.destroy()),t.on("error",()=>i.destroy()),t.on("close",()=>i.destroy()),i.on("close",()=>t.destroy())}),Rt.listen(be,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${be}/ (manifest ${rt})`));
|
|
16
|
+
</main></body></html>`}l(Ot,"downPage");const jt=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Pt=Re.createServer((e,t)=>{et(e).then(n=>n==="owner"?ts(e,t):Oa(e,t,n))});function ts(e,t){const n=jt(e);if(e.method!=="GET"&&e.method!=="HEAD"&&aa(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 r=new Set(Object.keys(g().sites));if(Qe({method:e.method,pathname:n.pathname,search:n.search,dest:ra(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Ka(m).then(p=>vt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},p)).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ye,pid:process.pid},sites:ie()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=ie();_t(m,{fresh:n.searchParams.get("fresh")==="1"}).then(p=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:xt(p),sites:Object.fromEntries(m.map(v=>[v.route,p.get(v.route)??null]))}))}).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){Ya(e,t,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 a=n.pathname.split("/")[1],d=j(g().sites,a);if(!d){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${Tt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${I(a)}</h1></main></body></html>`);return}if(n.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+n.search}).end();return}const h="/"+a,i=e.url.slice(h.length)||"/",S=Qe({pathname:n.pathname,dest:"document",sites:r})==="shell";if(B(d)==="files"){if(!kt(d.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d));return}ka(e,t,{dir:d.dir,rest:i,prefix:h,headers:S?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},y=Re.request({host:"127.0.0.1",port:d.port,method:e.method,path:i,headers:k},m=>{const p={...m.headers};S&&(p.vary=[p.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof p.location=="string"&&p.location.startsWith("/")&&!p.location.startsWith("//")&&!p.location.startsWith(h+"/")&&p.location!==h&&(p.location=h+p.location),t.writeHead(m.statusCode,p),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),y.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d))}),e.pipe(y)}l(ts,"serve"),Pt.on("upgrade",async(e,t,n)=>{const r=await et(e);if(r!=="owner"){ja(t,r);return}const a=jt(e).pathname.split("/")[1],d=j(g().sites,a);if(!d?.port){t.destroy();return}const h=`/${a}`;Ta({req:e,socket:t,head:n,port:d.port,path:e.url.slice(h.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${h}`]})}),Pt.listen(ye,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${ye}/ (manifest ${at})`));
|
package/hub/new-board.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).map(
|
|
1
|
+
var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as v,listBoards as B,readManifest as h,withManifestLock as u,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as D,routeFromName as L,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as M,validateBranch as A,validateRemote as G}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as T}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as q,linearWorks as H,savedClient as K}from"./linear.mjs";const d=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function Y(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(c=>!["name","github","remote","branch","purpose","tickets"].includes(c)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),n=L(t);D.test(n)||d("Name must contain a letter or digit for its route.");const{github:r,remote:s}=U(e),f=A(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;s&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let o;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:c,...m}=a;o=typeof c=="string"?c.trim():"",(!o||o.length>80)&&d("Name the new Linear project."),a={...m,project:o}}return{route:n,title:t,github:r,remote:s,branch:f,tickets:$(a),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(Y,"details");function U(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:M(e.github)}:{remote:G(e.remote).remote}}i(U,"repoInput");const _=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),E="That board route already exists. Reopen it from Closed boards if it is closed.",V=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),W=i(({purpose:e})=>`--template board --items data/wave.json${e?` --description ${V(e)}`:""}`,"joinFlags");function R(e){const t=String(e).split(`
|
|
2
|
+
`).map(n=>n.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(R,"errorLine");async function N(e,t){await u(async()=>{const n=h(),r=j(n.starting,e);!r||j(n.sites,e)||t.phase&&t.phase===r.phase||(n.starting[e]=t.phase?{...r,phase:t.phase,phase_at:new Date().toISOString()}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(n))})}i(N,"recordStart");const I=i(({title:e,purpose:t,repo:n,github:r,remote:s,branch:f,tickets:a},o)=>({title:e,purpose:t,repo:n,...r?{github:r}:{remote:s},branch:f,tickets:a,phase:o.cloned(r||s)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),O=new Map,ne=i(e=>O.get(e),"clonePercent"),z=1e3;async function x(e,{purpose:t,repo:n,github:r,remote:s,branch:f},{run:a,live:o,repos:c,changed:m}){let g,p=-1/0;const y=i(l=>{O.set(e,l),!(Date.now()-p<z)&&(p=Date.now(),m())},"progress");try{try{await c.ensure(r||s,f,{progress:y})}finally{O.delete(e)}await N(e,{phase:"starting"}),m();const l=await F(e,{repo:n,prompt:W({purpose:t}),run:a,live:o,clones:c.base});g={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){g={error:R(l.message)}}try{await N(e,g)}catch(l){return{error:R(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(x,"start");async function se(e,{run:t,live:n,repos:r=k(),changed:s=i(()=>{},"changed"),linear:f}={}){const a=Y(e),{route:o,github:c,remote:m,newProject:g}=a;let{tickets:p}=a;p.source==="linear"&&!f&&!H()&&d(q);const y=r.path(c||m),l=g===void 0?null:f||K();if(l){_(h(),o)&&d(E);try{p={...p,project:(await l.createProject(p.team,g)).id}}catch(b){d(b.message,b.status||502)}}return await u(async()=>{const b=h();_(b,o)&&d(E),b.starting={...b.starting,[o]:I({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:x(o,{...a,repo:y},{run:t,live:n,repos:r,changed:s})}}i(se,"requestBoard");function S(e,t,n){const r=j(e.starting,t);return(!r||j(e.sites,t)&&!r.joined_at||!C(r))&&d(`Only a board that didn\u2019t start can be ${n}.`),r}i(S,"failedStart");async function ae(e,{run:t,live:n,stop:r,repos:s=k(),changed:f=i(()=>{},"changed")}={}){const a=S(h(),e,"retried");a.joined_at&&d("Its session joined; a board that is not listening yet can only be dismissed."),a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await T(a,{live:n,stop:r})}catch(c){d(`Couldn\u2019t stop its earlier session: ${c.message}`,502)}const o=await u(async()=>{const c=h(),m=S(c,e,"retried");return c.starting[e]=I({...m,repo:s.path(m.github||m.remote),purpose:m.purpose||""},s),await w(c),c.starting[e]});return{route:e,started:x(e,o,{run:t,live:n,repos:s,changed:f})}}i(ae,"retryBoard");async function ie(e){return u(async()=>{const t=h();return S(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}i(ie,"dismissBoard");async function oe({listening:e=v}={}){return Object.values(h().starting||{}).some(t=>t.joined_at)?u(async()=>{const t=h(),n=new Map(B(t).map(s=>[s.route,s])),r=Object.entries(t.starting||{}).filter(([s,f])=>f.joined_at&&(!n.has(s)||e(n.get(s)))).map(([s])=>s);if(!r.length)return[];for(const s of r)delete t.starting[s];return Object.keys(t.starting).length||delete t.starting,await w(t),r}):[]}i(oe,"settleStarts");export{ne as clonePercent,ie as dismissBoard,U as repoInput,se as requestBoard,ae as retryBoard,oe as settleStarts};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var g=Object.defineProperty;var w=(r,s)=>g(r,"name",{value:s,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,s,u){const m=s.ownerChecks||{},i={},d=new Set;for(const e of r)for(const{slug:f,wave:p}of e.waves)for(const n of p.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||!["done","merged"].includes(n.status))continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,f,n.id,c.check]);if(d.has(o))continue;d.add(o);const t=`${f}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(m[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,t,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${t}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,t,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${t} on its passing check (${k.message})`)}}s.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
|
package/hub/readiness.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var T=Object.defineProperty;var i=(t,
|
|
2
|
-
`,{mode:384}),f.renameSync(a,e),
|
|
1
|
+
var T=Object.defineProperty;var i=(t,n)=>T(t,"name",{value:n,configurable:!0});import f from"node:fs";import m from"node:os";import y from"node:path";import{execFile as N}from"node:child_process";import{githubLogin as P}from"./pr-report.mjs";import{codexStatusReader as w}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{readSettings as C,subscriptions as O}from"./push.mjs";import{tailscaleState as I}from"../bin/install.mjs";import{tailscaleBin as L,tailscaleInstallCommand as F}from"../shared/tailscale.mjs";const x=[["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."]],k=i(t=>y.join(t,".claude","kickflip","integrations.json"),"reportFile");function j(t=m.homedir()){try{return JSON.parse(f.readFileSync(k(t),"utf8")).slack||null}catch{return null}}i(j,"readSlackReport");function W(t,{home:n=m.homedir(),now:s=new Date,route:d=null,title:o="Board"}={}){const e=k(n),r={ready:t,at:s.toISOString(),route:d,title:o};f.mkdirSync(y.dirname(e),{recursive:!0});const a=`${e}.${process.pid}.tmp`;return f.writeFileSync(a,JSON.stringify({slack:r},null,2)+`
|
|
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};
|
package/hub/sessions.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var ne=Object.defineProperty;var a=(e,s)=>ne(e,"name",{value:s,configurable:!0});import
|
|
2
|
-
${Y(e,s)}`],"hubArgv");function V(e,s,{cwd:t,tmux:
|
|
1
|
+
var ne=Object.defineProperty;var a=(e,s)=>ne(e,"name",{value:s,configurable:!0});import x from"node:fs";import w from"node:path";import j from"node:os";import{spawn as K,spawnSync as oe}from"node:child_process";import{fileURLToPath as ie}from"node:url";import{joinName as re,routeRecord as h,validRoute as ae}from"./routes.mjs";import{withFileLock as ce}from"./locks.mjs";import{RESUME_STATE as D,carryCursor as ue,hubBoard as W,isListening as de,listBoards as L,listenState as le,oldestUnheard as fe,readManifest as I,readJSON as P,saveJSON as J,watchedCounts as pe,watchedLines as me,watcherCursor as he,withManifestLock as we,writeManifest as ge}from"./boards.mjs";import{readUsageGate as be,heldUsageEvent as ke}from"../shared/usage-gate.mjs";import{liveSessions as _,sessionRecords as $e,processStarts as ye,verifiedProcess as Ie}from"./live.mjs";import{deliver as Se}from"./push.mjs";import{WAIT_MS as _e}from"./prompts.mjs";import{nodeBin as H,storedPath as X}from"../bin/install.mjs";import{reposDir as Te}from"../shared/repos.mjs";import{isClone as xe}from"./repos.mjs";import{trustFolder as je}from"./trust.mjs";import{contextFull as Oe,contextReading as ve}from"./usage.mjs";const O=X(w.resolve(w.dirname(ie(import.meta.url)),"..")),Ae=3*6e4,Ee=5*6e4,Ce=15*6e4,q=w.join(j.homedir(),".claude","kickflip","compact-state.json"),De=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ce,"startFailed");function Le(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Le,"startStep");function ps(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=De(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Le(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a(ps,"startingBoards");const Pe=_e/1e3+60,Ue=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${H()}" "${w.join(O,"hub","permission-hook.mjs")}"`,timeout:Pe}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${H()}" "${w.join(O,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ne(e,s){if(!ae(e))throw new m("Choose a board route.","none");return ce(w.join(j.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Xe(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ne,"withSessionLock");function v(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=w.join(j.homedir(),".local","bin","claude");return x.existsSync(e)?e:"claude"}a(v,"claudeBin");const A=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Q(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>x.existsSync(e))||"tmux"}a(Q,"tmuxBin");function U(e){const s=oe(Q(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!A(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(U,"runTmux");const S=a(e=>`kickflip-${e}`,"tmuxName"),N=a((e,s)=>e(["has-session","-t",`=${S(s)}`]).status===0,"hasTmux");async function ms({manifest:e=I(),now:s=new Date,running:t=_(),wakes:r=P(D,{}),tmux:i=U}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),P(q,{})),o=[],l=[];let u=!1;const c=W(e)?.route;for(const p of L(e)){const f=ve(p.owner?.session);if(!Oe(le(p,{now:s.getTime(),running:t,wakes:r}),f)){h(n,p.route)&&(delete n[p.route],u=!0);continue}if(p.route!==c||h(n,p.route)||!N(i,p.route))continue;const d=`=${S(p.route)}:`,$=i(["send-keys","-t",d,"-l","/compact"]),k=$.status===0?i(["send-keys","-t",d,"Enter"]):$,g=k.status===0?null:(k.stderr||k.stdout).trim().slice(0,300)||`tmux exited ${k.status}`;n[p.route]={session:p.owner.session,percent:f.percent,at:s.toISOString(),...g?{error:g}:{}},u=!0,g?l.push(`${p.route}: could not send /compact to tmux session ${S(p.route)} (${g})`):o.push(p.route)}return u&&J(q,n),{compacted:o,failed:l}}a(ms,"compactFullContexts");function B(e,{cwd:s,timeout:t=6e4}){const r=v(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!A(n)));return i.CLAUDE_BG_ISOLATION="none",w.isAbsolute(r)&&(i.PATH=`${w.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",l="",u;try{u=K(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(f){n({status:1,stdout:o,stderr:f.message});return}const c=a(f=>{clearTimeout(p),u.stdout.destroy(),u.stderr.destroy(),n({status:f,stdout:o,stderr:l})},"done"),p=setTimeout(()=>{try{u.kill("SIGKILL")}catch{}c(1)},t);u.stdout.on("data",f=>{o+=f}),u.stderr.on("data",f=>{l+=f}),u.on("error",f=>{l+=f.message,c(1)}),u.on("close",f=>c(f??1)),u.on("exit",f=>setTimeout(()=>c(f??1),300)),u.unref()})}a(B,"runClaude");async function Be(e,{cwd:s,run:t=B,starts:r=ye,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!Ie(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Be,"runSessionStop");async function hs(e,{live:s=_,stop:t=Be}={}){if(!e?.session)return;const r=s();if(!r)throw new m(ee(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||j.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(l=>setTimeout(l,100))}}a(hs,"stopSession");function Me(e){const s=`=${S(e)}`;return Re(Q(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(Me,"attachTmux");function Re(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!A(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const l=K(e,s,{env:t,stdio:"inherit"});l.on("error",u=>{i(),o(new m(u.message,"failed"))}),l.on("close",u=>{i(),n({status:u??1})})})}a(Re,"runForeground");const M=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Ue,...s?["--model",s]:[]],"common"),Fe=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),E=a((e,s)=>`--name ${Fe(re(e,s))}`,"boardFlag"),Ge=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...M({name:s,model:t}),`/kickflip ${E(e,s)}${r?` ${r}`:""}`],"startArgv"),Ke=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${w.join(O,"bin","kickflip.mjs")} join ${E(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),We=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:M({name:t,model:r}),Ke(s,e,t)],"resumeArgv"),Y=a((e,s)=>`You are the hub session. Read \`${w.join(O,"docs","hub-session.md")}\`, then rejoin /${e} (join ${E(e,s)}) and relaunch its watcher.`,"hubPrompt"),z=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...M({name:s,model:r}),t?`${Y(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${E(e,s)} --hub
|
|
2
|
+
${Y(e,s)}`],"hubArgv");function V(e,s,{cwd:t,tmux:r}){const i=v(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(A)])].flatMap(l=>["-u",l]),o=r(["new-session","-d","-s",S(e),"-c",t,"/usr/bin/env",...n,...w.isAbsolute(i)?[`PATH=${w.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(V,"startTmux");async function ws({manifest:e=I(),repo:s,tmux:t=U,attach:r=Me,live:i=_}={}){const n=W(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!N(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");F(o,i);const l=o?.repo||w.resolve(s||process.cwd());if(!x.statSync(l,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${l} is gone; rejoin from its new folder with --repo`:`no folder at ${l}`,"none");V(n.route,z({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:l,tmux:t})}return r(n.route)}a(ws,"hubSession");function R(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(R,"sessionOf");async function Z(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=$e().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(Z,"startedId");async function Je(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Je,"isBackground");const ee=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy Mac. Try again in a moment.`,"unknownLiveness");function F(e,s){if(!e)return;const t=s();if(!t)throw new m(ee(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(F,"refuseLive");async function se(e,{from:s,to:t,mark:r}){await we(async()=>{const i=I(),n=L(i).find(l=>l.route===e);if(n?.owner?.session===s&&(r||t!==s)&&ue(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await ge(i)})}a(se,"moveOwner");async function gs(e,{manifest:s=I(),repo:t,model:r,prompt:i,run:n=B,live:o=_,clones:l=Te()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const u=R(s,e);F(u,o);const c=w.resolve(t||u?.repo||process.cwd());if(!x.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(xe(c,l))try{je(c)}catch(g){throw new m(`Couldn't trust this folder for Claude Code: ${g.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,f=Ge({route:e,name:p,model:r||u?.model,prompt:i}),d=await n(f,{cwd:c});if(d.status!==0){const g=(d.stderr||"").trim()||(d.stdout||"").trim();throw new m(g?`claude --bg failed (${d.status}): ${g.slice(0,300)}`:`claude exited ${d.status} with no output; check \`claude agents\``,"failed")}const{session:$,confirmed:k}=await Z(d.stdout);if(!$)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${d.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:$,confirmed:k,repo:X(c),model:r||u?.model||null,name:p,argv:[v(),...f],output:d.stdout.trim()}}a(gs,"startSession");function He(e,s={}){return Ne(e,({resume:t})=>t(s))}a(He,"resumeSession");async function Xe(e,{manifest:s=I(),run:t=B,tmux:r=U,live:i=_,mark:n=null}={}){if(h(I().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=R(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(F(o,i),h(s.sites,e)?.hub===!0){if(N(r,e))throw new m(`/${e}'s tmux session ${S(e)} is still open; attach with kickflip hub`,"live");const d=V(e,z({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await se(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:d,output:`in tmux session ${S(e)}`}}const l=await Je(o.session,{run:t,cwd:o.repo}),u=We({id:o.session,route:e,name:o.title,model:o.model,background:l}),c=await t(u,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:f}=await Z(c.stdout,o.session);return await se(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:f,repo:o.repo,argv:[v(),...u],output:c.stdout.trim()}}a(Xe,"resumeRecordedSession");function qe(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:l}){if(s)return{action:"none",entry:null};const u=new Date(l).toISOString(),c=e?{...e}:{down_since:u};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||l-Date.parse(c.resumed_at)>=Ee?{action:"push",entry:{...c,pushed_at:u}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([f,d])=>d>(h(e.mark,f)??0)))||o||n&&l-Date.parse(c.down_since)>=Ae?{action:"resume",entry:{...c,resumed_at:u}}:{action:"none",entry:c}}a(qe,"resumeStep");async function bs({manifest:e=I(),now:s=new Date,live:t=_,asked:r=new Set,listening:i=a((u,c)=>de(u,{now:s.getTime(),running:c}),"listening"),resume:n=a((u,c)=>He(u,{manifest:e,live:t,mark:c}),"resume"),push:o=Se,gate:l=be({now:s})}={}){const u=t();if(!u)return{resumed:[],pushed:[],failed:[]};const c=P(D,{}),p=Object.create(null),f={resumed:[],pushed:[],failed:[]};for(const d of L(e)){const $=R(e,d.route),k=!!($&&u.has($.session)),g=i(d,u);let{action:T,entry:b}=qe(h(c,d.route),{listening:g,live:k,recorded:!!$?.repo,now:s.getTime(),asked:r.has(d.route),counts:g?null:pe(d),waiting:!g&&!k&&fe(d)!==null});if(T==="resume"&&l.state==="paused"){const y=h(c,d.route)?.mark||he(d);Object.entries(me(d)).flatMap(([C,te])=>te.slice(h(y,C)||0)).some(C=>!ke(C))||(T="none",delete b.resumed_at)}if(T==="resume")try{await n(d.route,b.mark),f.resumed.push(d.route)}catch(y){y.reason==="unknown"?delete b.resumed_at:b={...b,error:y.message},f.failed.push(`${d.route}: could not resume its session (${y.message})`)}else if(T==="push"){const y=b.error?`The hub couldn't resume it: ${b.error}`:k?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${d.route}, or claude agents.`;try{await o({title:`${d.title}'s session didn't come back`,body:y,url:`/${d.route}/`,tag:`resume-${d.route}`},{kind:"fault",immediate:!0,at:s}),f.pushed.push(d.route)}catch(G){b={...b,pushed_at:void 0},f.failed.push(`${d.route}: could not push that its session didn't come back (${G.message})`)}}b&&(p[d.route]=b)}return JSON.stringify(p)!==JSON.stringify(c)&&J(D,p),f}a(bs,"checkSessions");export{Ue as BG_SETTINGS,Ee as COMEBACK_WAIT,Pe as PROMPT_HOOK_TIMEOUT_S,Ae as RESUME_AFTER,Ce as STARTING_WAIT,m as SessionError,Me as attachTmux,bs as checkSessions,v as claudeBin,ms as compactFullContexts,z as hubArgv,ws as hubSession,A as inherited,We as resumeArgv,Ke as resumePrompt,He as resumeSession,qe as resumeStep,B as runClaude,Be as runSessionStop,U as runTmux,R as sessionOf,Ge as startArgv,De as startFailed,gs as startSession,ps as startingBoards,hs as stopSession,Q as tmuxBin,S as tmuxName,Ne as withSessionLock};
|