@lovek4d/kickflip 0.7.4 → 0.7.8

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.
@@ -86,7 +86,7 @@
86
86
  }
87
87
  // An arrival (arrive) slides in with a glow and settles (style.css .arrived).
88
88
  const pillHtml = (t, arrived = false) => `<button class="${arrived ? 'arrived ' : ''}${t.progress ? 'progress' : ''}" data-topic="${esc(t.id)}" aria-pressed="${t.id === topicId}" title="${esc(t.title)}"><span class="name">${esc(topicName(t))}</span><span class="n">${t.state === 'resolved' ? 'resolved' : t.open}</span></button>`;
89
- const topicHead = (t) => t ? `<div class="topichead muted small"><span>${esc(t.title)}${t.summary ? ` · ${esc(t.summary)}` : ''}</span>${roundChip(t.round)}</div>` : '';
89
+ const topicHead = (t) => t ? `<div class="topichead"><button class="topic-title" data-open-card="${esc(t.id)}">${esc(t.title)}</button>${t.summary ? `<span class="muted small">${esc(t.summary)}</span>` : ''}${roundChip(t.round)}</div>` : '';
90
90
 
91
91
  // Only open questions changed here since their last answer, with files attached, reopened with a pick, or with a
92
92
  // suggested option pre-checked and never answered, are sent; a sent one is read-only.
@@ -368,7 +368,6 @@
368
368
  scroll(0);
369
369
  }
370
370
 
371
- let lastTap = { id: null, at: 0 };
372
371
  // item:<key> is a card, wave:<slug> or file:<slug> a band. Embedded, the board is beside the pane; alone, it is the page before this one.
373
372
  function openOnBoard(id) {
374
373
  if (ctx.embedded) return ctx.openOnBoard(id);
@@ -382,17 +381,17 @@
382
381
  if (eye) { askPrototype(byId(eye.closest('section.q').dataset.q), eye); return; }
383
382
  const reopenBadge = ev.target.closest && ev.target.closest('[data-reopen]');
384
383
  if (reopenBadge) { if (!reopenBadge.disabled) reopen(byId(reopenBadge.dataset.reopen), reopenBadge); return; }
384
+ const title = ev.target.closest && ev.target.closest('[data-open-card]');
385
+ if (title) { openOnBoard(title.dataset.openCard); return; }
385
386
  const b = ev.target.closest && ev.target.closest('[data-topic]');
386
387
  if (b) {
387
- // Two taps on one pill within 300ms open its card on the board; a touch screen sends no dblclick.
388
- const now = Date.now();
389
- if (lastTap.id === b.dataset.topic && now - lastTap.at < 300) { lastTap = { id: null, at: 0 }; openOnBoard(b.dataset.topic); return; }
390
- lastTap = { id: b.dataset.topic, at: now };
388
+ // Embedded in the desktop pane, one click also focuses the topic's card on the board; a phone only picks the topic.
391
389
  soloId = null; setTopic(b.dataset.topic); sentLine = ''; shown = '';
392
390
  const first = topics.find((t) => t.id === topicId)?.questions[0];
393
391
  if (first) setHash(first);
394
392
  ctx.onTopic?.(topicId);
395
393
  render(); scroll(0);
394
+ if (ctx.embedded) openOnBoard(topicId);
396
395
  }
397
396
  });
398
397
  B.on(root, 'change', (ev) => {
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ // A Claude Code PostToolUse hook that shows a board's session the owner's new words while it works, after its next tool
3
+ // call, instead of only at its watcher's next wake: each thread message, ask and ask edit (OWNER_WORDS) past the
4
+ // watcher's cursor that it hasn't shown yet, with its sent time and age, its plain line and its event line. It marks
5
+ // the ones the watcher marks working (WORKING_KINDS), so they read thinking… on the board at once, and records them in
6
+ // the session's shown file, so the watcher's next wake prints them again tagged already shown. A subagent's tool call
7
+ // carries agent_id and gets nothing. Any error shows nothing
8
+ // (docs/adr/2026-11-10-1056-the-owners-words-reach-the-orchestrator-after-its-next-tool-call.md).
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ process.stdout.on('error', () => {});
13
+
14
+ const EVENTS = 'data/events.jsonl';
15
+ const readJSON = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
16
+ const parsed = (line) => { try { return JSON.parse(line); } catch { return null; } };
17
+ function age(ts, now) {
18
+ const min = Math.floor((now - Date.parse(ts)) / 60_000);
19
+ if (!(min >= 1)) return 'just now';
20
+ return min < 60 ? `${min} min ago` : `${Math.floor(min / 60)} h ${min % 60} min ago`;
21
+ }
22
+
23
+ async function shownOn(board, session, now) {
24
+ const { OWNER_WORDS, plainLine, shownFile } = await import('../../bin/event-lines.mjs');
25
+ const { markWorking, WORKING_KINDS } = await import('../../board/store.mjs');
26
+ const cursor = readJSON(path.join(board.dir, 'data', `.watch-cursor-${session}.json`));
27
+ if (!Number.isInteger(cursor?.[EVENTS])) return [];
28
+ let lines = [];
29
+ try { lines = fs.readFileSync(path.join(board.dir, EVENTS), 'utf8').split('\n').filter(Boolean).slice(cursor[EVENTS]); } catch {}
30
+ const pending = lines.map((line) => ({ line, e: parsed(line) })).filter(({ e }) => OWNER_WORDS.includes(e?.kind) && typeof e.id === 'string');
31
+ const file = shownFile(board.dir, session);
32
+ // Ids the watcher has since woken past drop out.
33
+ const before = (readJSON(file)?.ids || []).filter((id) => pending.some(({ e }) => e.id === id));
34
+ const fresh = pending.filter(({ e }) => !before.includes(e.id));
35
+ if (!fresh.length) return [];
36
+ await markWorking(board.dir, fresh.filter(({ e }) => WORKING_KINDS.includes(e.kind)).map(({ e }) => e.id));
37
+ fs.writeFileSync(file, JSON.stringify({ ids: [...before, ...fresh.map(({ e }) => e.id)] }));
38
+ const out = [`The owner wrote on /${board.route} while you worked. Read it now: their words come before what you were doing. Your watcher prints each again on its next wake, tagged already shown.`];
39
+ for (const { line, e } of fresh) out.push(`sent ${e.ts}, ${age(e.ts, now)}: ${await plainLine(board.dir, line)}`, `event ${EVENTS} ${line}`);
40
+ return out;
41
+ }
42
+
43
+ try {
44
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
45
+ if (input.hook_event_name === 'PostToolUse' && !input.agent_id && typeof input.session_id === 'string' && input.session_id) {
46
+ const { listBoards } = await import('../../hub/boards.mjs');
47
+ const owned = listBoards().filter((b) => b.owner?.session === input.session_id);
48
+ const now = Date.now();
49
+ const lines = (await Promise.all(owned.map((b) => shownOn(b, input.session_id, now).catch(() => [])))).flat();
50
+ if (lines.length) console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: lines.join('\n') } }));
51
+ }
52
+ } catch {}
53
+ process.exitCode = 0;
@@ -39,6 +39,16 @@
39
39
  ]
40
40
  }
41
41
  ],
42
+ "PostToolUse": [
43
+ {
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/board-messages.mjs\""
48
+ }
49
+ ]
50
+ }
51
+ ],
42
52
  "PreToolUse": [
43
53
  {
44
54
  "matcher": "Agent|Task",
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Bt=Object.defineProperty;var l=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Ft}from"node:child_process";import b from"node:fs";import k from"node:path";import Ne from"node:os";import{fileURLToPath as Gt}from"node:url";import Fe from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ge,queueStatus as ze,saveSettings as Qt,readSettings as F,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ke,answerOf as an,expired as rn,pendingFor as We,promptRecord as on,pushOf as cn,updatePrompts as G,shown as ln,pruneRemembered as ye,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn,quietAt as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as vn,awakeOn as Tn,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{nodeBin as Pn}from"../bin/install.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Ze,RESUME_STATE as Cn,sendToBoards as Qe,checkListening as An,recordHubCode as In,hubCode as Mn,runningHubCode as Hn,boardCode as Xe,recordLinkFaults as Un,linksOf as Bn,hubLink as Nn,hubCodeState as Fn,readJSON as Ye}from"./boards.mjs";import{liveSessions as qe}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as et,readHubRules as ke,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as tt,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ws,colorsVersion as nt,destOf as bs,fileVersion as ys,pageOf as st,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as ee,setPauseAt as vs,setKeepGoing as Ts,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as te}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as xe,initializeTries as As,fetchTryBranches as at,pullNow as Is,pullMains as Ms}from"./try.mjs";import{serveFiles as Hs}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Ns}from"./self-restart.mjs";import{fallBackFromLinear as Fs,migrateWaveNames as Gs,openBoardBuilders as zs}from"../board/store.mjs";import{recordRefusal as Js}from"../board/permissions.mjs";import{hubGate as ve}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Vs}from"./front-door.mjs";import{shareEntries as Zs,sharedRoutes as rt,shareReach as Qs,routeOfPath as Xs,sharedSidebar as Ys,shareTracker as qs}from"./share.mjs";import{restartRunner as ot}from"../board/worktree.mjs";import{hubService as ea}from"../shared/service.mjs";import{cloneRoot as Te}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ne,removeLinearKey as ta,saveLinearKey as na,savedClient as it}from"./linear.mjs";import{SlackError as sa,slackStatus as aa,removeSlackToken as ra,saveSlackToken as oa}from"./slack.mjs";import{sendReviews as ia}from"./review-send.mjs";const z=k.dirname(Gt(import.meta.url)),ca=k.join(z,"..","shared","colors.css"),ct=JSON.parse(b.readFileSync(k.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),J=k.join(Ne.homedir(),".claude","kickflip"),lt=k.join(J,"manifest.json"),se=k.join(J,"logs"),ut=k.join(J,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[J,se,ut])b.mkdirSync(e,{recursive:!0});try{const e=In();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 ss();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 as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await Gs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}const M=es(),la=$s(),ae=xn({log:c});ae.apply();let re={version:1,sites:{}};function g(){try{re=JSON.parse(b.readFileSync(lt,"utf8")),re.sites||={}}catch(e){e.code==="ENOENT"?re={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return re}l(g,"loadManifest");const j=new Map,H=new Set;let oe=!1;const dt=5e3,ie=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),K=new Map;function ce(e){return k.join(ut,e+".pid")}l(ce,"pidFile");function ua(e,t){try{const n=Number(b.readFileSync(ce(e),"utf8")),s=n?Ft("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ce(e))}catch{}}l(ua,"killStale");function da(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(se,e+".log"),"a"),[o,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
2
+ var Nt=Object.defineProperty;var u=(e,t)=>Nt(e,"name",{value:t,configurable:!0});import Be from"node:http";import Ft from"node:net";import{spawn as Ne,execFileSync as Fe}from"node:child_process";import b from"node:fs";import k from"node:path";import Ge from"node:os";import{fileURLToPath as Gt}from"node:url";import ze from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Je,queueStatus as Ke,saveSettings as Qt,readSettings as F,migrateSettings as Xt,setNotifications as Yt,readCodexShare as We,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ve,answerOf as an,expired as rn,pendingFor as Ze,promptRecord as on,pushOf as cn,updatePrompts as G,shown as ln,pruneRemembered as Se,rememberRules as un,rememberedAnswer as dn,rememberedFor as Qe}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn,quietAt as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as vn,awakeOn as Tn,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{nodeBin as Pn}from"../bin/install.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as ke,listenState as Xe,RESUME_STATE as Cn,sendToBoards as Ye,checkListening as An,recordHubCode as In,hubCode as Hn,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Un,linksOf as Bn,hubLink as Nn,hubCodeState as Fn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as nt,readHubRules as $e,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as _e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as xe,appFile as ws,colorsVersion as at,destOf as bs,fileVersion as ys,pageOf as rt,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as ee,setPauseAt as vs,setKeepGoing as Ts,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as te}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as ve,initializeTries as As,fetchTryBranches as ot,pullNow as Is,pullMains as Hs}from"./try.mjs";import{serveFiles as Ms}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Ns}from"./self-restart.mjs";import{fallBackFromLinear as Fs,migrateWaveNames as Gs,openBoardBuilders as zs}from"../board/store.mjs";import{recordRefusal as Js}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Vs}from"./front-door.mjs";import{shareEntries as Zs,sharedRoutes as it,shareReach as Qs,routeOfPath as Xs,sharedSidebar as Ys,shareTracker as qs}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as ea}from"../shared/service.mjs";import{cloneRoot as ne}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as ta,saveLinearKey as na,savedClient as lt}from"./linear.mjs";import{SlackError as sa,slackStatus as aa,removeSlackToken as ra,saveSlackToken as oa}from"./slack.mjs";import{sendReviews as ia}from"./review-send.mjs";const z=k.dirname(Gt(import.meta.url)),ca=k.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync(k.join(z,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),J=k.join(Ge.homedir(),".claude","kickflip"),dt=k.join(J,"manifest.json"),ae=k.join(J,"logs"),ht=k.join(J,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[J,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=In();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 ss();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 as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await Gs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}const H=es(),la=$s(),re=xn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}u(g,"loadManifest");const j=new Map,M=new Set;let ie=!1;const ft=5e3,ce=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),K=new Map;function le(e){return k.join(ht,e+".pid")}u(le,"pidFile");function ua(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?Fe("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}u(ua,"killStale");function da(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(ae,e+".log"),"a"),[o,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=Se(t)?Xe(t):null;let h;try{h=Be(o,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
- `),b.closeSync(s),Object.assign(n,{child:null,key:ie(t),state:"crashed",exitCode:null,nextStart:Date.now()+ht(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ce(e),String(h.pid)),Object.assign(n,{child:h,key:ie(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,S)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(ce(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+ht(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(da,"start");const ht=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;ue(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&ue(n,"SIGKILL")},dt)}l(le,"stop");const ue=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let ft=!0;const B=new Set;let Ee=!1;const W=new Set,ha=3e4;async function fa(e){const t=Date.now()+ha;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ie(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}l(fa,"serverAnswers");async function pt(){if(oe)return;mt&&gt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),le(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),le(t),j.delete(t)));for(const t of[...K.keys()])U(P(e,t))!=="port"&&K.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&K.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;ft&&ua(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ie(n)){c(`${t} entry changed; restarting it`),le(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=mt&&s?.child&&s.code&&!s.stopping&&!W.has(t)&&!B.has(t)?Xe(n):null;if(o&&o!==s.code){if(!(await zs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),le(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&da(t,n)}ft=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(pt,"reconcile");const mt=process.env.KICKFLIP_CODE_RESTARTS!=="off",gt=Ns({root:Te(),files:Mn().files,restart:l(()=>ot("hub"),"restart"),log:c}),pa=setInterval(()=>pt().catch(e=>c("reconcile failed:",e.message)),2e3);pt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const wt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await wt();const ma=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ma,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},de=sn(),bt=l(()=>bn(new Date,F()),"quietNow"),ga=l(()=>$.startedAt?wn({last:new Date($.startedAt),...de.state(),quiet:bt(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function yt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&!bt()&&Le().catch(n=>c("poll failed:",n.message))}l(yt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=F();let s;try{s=await Rn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await at({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await at({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Ms({manifest:g()});o.length&&xe({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ia({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Ge();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}l(Le,"runPoll");let St=null;function wa(){St||=En({settings:F(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{St=null})}l(wa,"runWakes");const Ce=new Set,A={running:null};function kt(){if(A.running||B.size||W.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l(kt,"runSessionCheck"),await As(g());const ba=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function $t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=rn(n,{now:Date.now(),aliveSince:ba}),o=ye(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l($t,"prunePrompts");const Ae=l(()=>({running:qe(),boards:D(g())}),"promptContext");async function _t(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Js(tt(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(_t,"noteRefusal");let Q=null,xt=null;async function he(){return Q?xt??=Q.catch(()=>{}).then(()=>(xt=null,he())):(Q=(async()=>_s({gate:ee(),resume:l(async e=>{const t=await Qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}de.check().then(yt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ga().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),wa(),ae.apply(),$t().catch(e=>c("prompt expiry failed:",e.message)).finally(kt)},15e3);function vt(){if(oe)return;oe=!0,clearInterval(pa),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)ue(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+dt;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),ue(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Tt=new Map;function Ot(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Ot(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}l(Ot,"newestMtime");function ya(e,t){const n=Tt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Ot(t.dir);return Tt.set(e,{at:s,checked:Date.now()}),s}l(ya,"changedAt");const jt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=U(n),r=o==="files"?jt(n.dir)?"up":"down":o==="port"?K.has(t)?K.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:ya(t,n)||null,created:n.created||null,log:k.join(se,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Pt=2e4,Sa=1500;async function ka(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Sa)});return t.ok?Wn(await t.json()):null}catch{return null}}l(ka,"fetchNeeds");async function Rt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=pe.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Pt)&&pe.set(o.route,{at:Date.now(),needs:ka(o.port)})}const s=await Promise.all(n.map(o=>pe.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}l(Rt,"waitingCounts");const Et=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+We(null).length,"badgeTotal"),Ie=new Map;function $a(e){const t=new Map,n=new Map,s={running:qe(),wakes:Ye(Cn,{})};for(const o of e)try{const r=Ze(o,s);n.set(o.route,r);let p=Ie.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Pt)&&(p={at:Date.now(),listen:r,links:Bn(o,{listen:r})},Ie.set(o.route,p)),t.set(o.route,p.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}l($a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Rt(n,{fresh:e}),o=g(),r=D(o),{links:p,listens:h}=$a(r),u=Fn(),_=new Map(r.map(i=>[i.route,xs(i.owner?.session)]).filter(([,i])=>i)),S=qn(o,t.getTime(),cs,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>js(i.dir)).map(i=>[i.route,ee({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=F(),C=hn(t,O);return{data:{code:u.running??null,version:ct,total:Et(s),start:Zn(w),sites:w,hub:Qn(w,p,Nn(u)),home:b.realpathSync(Ne.homedir()),starting:S,usage:(({start:i,...d})=>d)(la({codexShare:Je(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(pn(C,t))}:{state:"on",text:"On"},away:!de.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Dt(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}l(Dt,"sendText");async function _a(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Fe.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Fe.runInContext(b.readFileSync(k.join(_e,o),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(_a,"shellPage");const Lt=Us({root:Te(),run:xa,log:c,pulled:l(()=>gt.restartNow(),"pulled")});function xa(){const e=k.join(se,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(Pn(),[k.join(Te(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}l(xa,"runUpdate"),Lt.check(),setInterval(()=>Lt.check(),Bs);const x=ks({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync(k.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const va=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function T(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(T,"readJSONBody");const Ct=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>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${nt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ta=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function ge(e){await $t();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...Sn({now:t,st:ze(t),sidebar:n,rows:o,links:s,rules:ke(),integrations:await Rs.rows(),code:r,version:ct,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(te()),claudeModel:en(),codex:te(),awake:{on:Tn(),unsupported:On()},remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:We(null).map(ln),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t=ee({now:e}),n=t.manual==="hub"?jn(t.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:ne(),slack:aa(),pausedUntil:n}),closed:hs()}},"settingsData"),R=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Oa(e,t,n,s=null){const o=l((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=l((a,i)=>{const d=Hn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Ta(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return r(200,await us(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let a;try{const i=await T(e);if(a=i.route,u==="/_hub/reopen-board"){const v=await fs(a);return x.refresh(),r(200,v)}if(B.has(a))return r(409,{error:"That board is already closing."});B.add(a);const d=l(()=>B.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,W.has(a))return r(409,{error:"That board is already restarting."});W.add(a),await A.running;const i=await ps(a,{serverUp:fa,portUp:Re,onStarting:l(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{W.delete(a),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await T(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=ot("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ea().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/presence"&&e.method==="POST")return await T(e),yt(de.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),i=D(g()).find(d=>d.route===a);return i?(Ce.add(a),await A.running,kt(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Ze(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return r(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return r(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:M,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?tt(S[1]):await ns(S[1],await T(e),{repos:M});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(a,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(y&&e.method==="POST"){if(!$e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(y[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const a=await is(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(oe)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return r(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?r(200,await Is(d,{manifest:g()})):r(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return r(f.status,{error:f.message});throw f}}if(u==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return o(200,"application/manifest+json",va());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return o(200,"image/png",b.readFileSync(k.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const a=n.searchParams.get("v")===nt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ca),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};we.add(a),e.on("close",()=>we.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!Se(a))return r(404,{error:"That board is not open."});const i=Zs(Ye(k.join(a.dir,"board.json"),{})),d=i.map(L=>L.login),f=(await ve.node())?.dnsName||e.headers["x-forwarded-host"],v=Y.people(O[1],d).map((L,q)=>({...L,level:i[q].level}));return r(200,{people:v,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const a=ws(u.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],f=n.searchParams.get("v")===ys(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":f}):Dt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(u==="/_hub/api/hub")return r(200,await ge(e));if(u==="/_hub/api/settings")return r(200,await I());if(u==="/_hub/push/key")return r(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await T(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):o(400,"text/plain; charset=utf-8",f);let v;try{v=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:_n}):R(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{vn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return ae.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{Os(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await he(),h?r(200,await I()):R(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),te({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?vs(a.get("value")):Ts(a.get("value")),await he()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ta(),c("linear: key removed"),await wt()):c(`linear: key ${(await na(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ra(),c("slack: token removed")):c(`slack: token ${(await oa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof sa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const a=ne();if(!a.set||a.refused)return r(200,a);const i=it();try{const d=await i.teams();return r(200,{...ne(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ne(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const a=it();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await T(e,4*1024)}catch(f){return r(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(f){if(!(f instanceof Oe))throw f;return r(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(F(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await T(e,64*1024),i=zn(ke(),a.rules,a.base??[]),et(i.changes)&&Jn(i.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!et(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,$n(a)):R(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const a=await T(e);return r(200,await Le({flush:a.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await G(f=>{const v=Ae(),L=v.boards.find(He=>He.owner?.session&&He.owner.session===a.session_id)||null;ye(f,v)&&x.refresh();const q=dn(f,a);if(q)return{answer:q};const be=on(a,{board:L,live:v.running?.get(a.session_id)});return f.prompts[be.id]=be,{rec:be}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await _t(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await _t(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?ja(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}l(Oa,"hubRoute");async function ja(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),u=await p();if(!u)return r(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ke,Ke));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await p();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await G(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return ye(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...V.get(n)||[]])S();return Z(u.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=P(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}l(ja,"promptRoute");const Pa=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 At(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(At,"downWhy");function It(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":At(t,n)}),e.end(Ra(t,n))}l(It,"sendDown");function Ra(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=At(e,t),p=r==="code"||r==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${Ct(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=ke(t)?qe(t):null;let h;try{h=Ne(o,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
+ `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,S)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(da,"start");const pt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&de(n,"SIGKILL")},ft)}u(ue,"stop");const de=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const W=new Set,ha=3e4;async function fa(e){const t=Date.now()+ha;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(fa,"serverAnswers");async function gt(){if(ie)return;wt&&bt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...K.keys()])U(P(e,t))!=="port"&&K.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&K.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ua(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=wt&&s?.child&&s.code&&!s.stopping&&!W.has(t)&&!B.has(t)?qe(n):null;if(o&&o!==s.code){if(!(await zs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&da(t,n)}mt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Ns({root:ne(),files:Hn().files,restart:u(()=>ct("hub"),"restart"),log:c}),pa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=u(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const ma=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ma,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},he=sn(),St=u(()=>bn(new Date,F()),"quietNow"),ga=u(()=>$.startedAt?wn({last:new Date($.startedAt),...he.state(),quiet:St(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&!St()&&Le().catch(n=>c("poll failed:",n.message))}u(kt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=F();let s;try{s=await Rn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await ot({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await ot({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Hs({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ia({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Je();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let $t=null;function wa(){$t||=En({settings:F(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(wa,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||W.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}u(_t,"runSessionCheck"),await As(g());const ba=Date.now(),V=new Map;function Z(e){e?(me.delete(e),x.changed(e)):x.refresh()}u(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=rn(n,{now:Date.now(),aliveSince:ba}),o=Se(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ae=u(()=>({running:tt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Js(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Q=null,Tt=null;async function fe(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,fe())):(Q=(async()=>_s({gate:ee(),resume:u(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}u(fe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),fe().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}he.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ga().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:u(e=>Ln(e)&&pe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),wa(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(pa),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)de(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function ya(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}u(ya,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function pe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=U(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?K.has(t)?K.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:ya(t,n)||null,created:n.created||null,log:k.join(ae,t+".log")}})}u(pe,"status");const N=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),me=new Map,Et=2e4,Sa=1500;async function ka(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Sa)});return t.ok?Wn(await t.json()):null}catch{return null}}u(ka,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=me.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&me.set(o.route,{at:Date.now(),needs:ka(o.port)})}const s=await Promise.all(n.map(o=>me.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}u(Dt,"waitingCounts");const Lt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ze(null).length,"badgeTotal"),Ie=new Map;function $a(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};for(const o of e)try{const r=Xe(o,s);n.set(o.route,r);let p=Ie.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:r,links:Bn(o,{listen:r})},Ie.set(o.route,p)),t.set(o.route,p.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u($a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=pe(),s=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:p,listens:h}=$a(r),l=Fn(),_=new Map(r.map(i=>[i.route,xs(i.owner?.session)]).filter(([,i])=>i)),S=qn(o,t.getTime(),cs,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>js(i.dir)).map(i=>[i.route,ee({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=F(),C=hn(t,O);return{data:{code:l.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Nn(l)),home:b.realpathSync(Ge.homedir()),starting:S,usage:(({start:i,...d})=>d)(la({codexShare:We(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(pn(C,t))}:{state:"on",text:"On"},away:!he.state().present},links:p,rows:n,code:l}}u(X,"sidebarState");function Ct(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}u(Ct,"sendText");async function _a(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=ze.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])ze.runInContext(b.readFileSync(k.join(xe,o),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(_a,"shellPage");const He=Us({root:ne(),run:xa,log:c,pulled:u(()=>bt.restartNow(),"pulled")});function xa(){const e=k.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Ne(Pn(),[k.join(ne(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(xa,"runUpdate"),He.check(),setInterval(()=>He.check(),Bs);const x=ks({targets:u(()=>Object.entries(g().sites).filter(([,e])=>ke(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function ge(e){let t="0";try{t=Math.floor(b.statSync(k.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(ge,"iconUrl");const va=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ge(192),sizes:"192x192",type:"image/png"},{src:ge(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function v(e,t){if(!_e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(v,"readJSONBody");const At=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${ge(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ta=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...Sn({now:t,st:Ke(t),sidebar:n,rows:o,links:s,rules:$e(),integrations:await Rs.rows(),code:r,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(te()),claudeModel:en(),codex:te(),awake:{on:Tn(),unsupported:On()},remembered:new Map(D(g()).map(p=>[p.route,Qe(p.route)]))}),prompts:Ze(null).map(ln),remembered:Qe(null)}}u(we,"hubData");const I=u(async()=>{const e=new Date,t=ee({now:e}),n=t.manual==="hub"?jn(t.resets_at,e):null;return{...kn({now:e,st:Ke(e),rules:$e(),linear:se(),slack:aa(),pausedUntil:n}),closed:hs()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Oa(e,t,n,s=null){const o=u((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=u((a,i)=>{const d=Mn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=u(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Ta(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await us(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let a;try{const i=await v(e);if(a=i.route,l==="/_hub/reopen-board"){const T=await fs(a);return x.refresh(),r(200,T)}if(B.has(a))return r(409,{error:"That board is already closing."});B.add(a);const d=u(()=>B.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await v(e)).route,W.has(a))return r(409,{error:"That board is already restarting."});W.add(a),await A.running;const i=await ps(a,{serverUp:fa,portUp:Re,onStarting:u(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{W.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=ct("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ea().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const a=await He.check();return a.state==="clone"&&(a.commit=Fe("git",["-C",ne(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),kt(he.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await v(e),i=D(g()).find(d=>d.route===a);return i?(Ce.add(a),await A.running,_t(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Xe(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:H,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?st(S[1]):await ns(S[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(a,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const a=await is(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(ie)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return r(f.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Is(d,{manifest:g()})):r(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return r(f.status,{error:f.message});throw f}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(z,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",va());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(k.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const a=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ca),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(l==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};be.add(a),e.on("close",()=>be.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!ke(a))return r(404,{error:"That board is not open."});const i=Zs(et(k.join(a.dir,"board.json"),{})),d=i.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:i[q].level}));return r(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=ws(l.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],f=n.searchParams.get("v")===ys(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await I());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):o(400,"text/plain; charset=utf-8",f);let T;try{T=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:_n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{vn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return re.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{Os(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await fe(),h?r(200,await I()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),te({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?vs(a.get("value")):Ts(a.get("value")),await fe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ta(),c("linear: key removed"),await yt()):c(`linear: key ${(await na(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ra(),c("slack: token removed")):c(`slack: token ${(await oa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof sa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=se();if(!a.set||a.refused)return r(200,a);const i=lt();try{const d=await i.teams();return r(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...se(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=lt();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(f){return r(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(f){if(!(f instanceof Oe))throw f;return r(f.status,{error:f.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(F(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await v(e,64*1024),i=zn($e(),a.rules,a.base??[]),nt(i.changes)&&Jn(i.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!nt(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Je();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,$n(a)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const a=await v(e);return r(200,await Le({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(Ue=>Ue.owner?.session&&Ue.owner.session===a.session_id)||null;Se(f,T)&&x.refresh();const q=dn(f,a);if(q)return{answer:q};const ye=on(a,{board:L,live:T.running?.get(a.session_id)});return f.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await vt(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?ja(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(Oa,"hubRoute");async function ja(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=u(()=>G(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await p();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ve,Ve));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await p();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return Se(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...V.get(n)||[]])S();return Z(l.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(ja,"promptRoute");const Pa=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(It,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(Ra(t,n))}u(Ht,"sendDown");function Ra(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=It(e,t),p=r==="code"||r==="starting"?"wait":"down",h=U(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
11
  <h1>${N(t.title||e)} isn't answering</h1>
12
- <p class="lede">${N(u)}</p>
12
+ <p class="lede">${N(l)}</p>
13
13
  <h2 class="label">Site</h2>
14
14
  <div class="group">${Pa({icon:Kn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(o[r])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(Ra,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=qs({file:k.join(J,"share-seen.json")}),we=new Set;function Mt(){const e=g().sites;for(const t of we){const n=rt(t.login,e);[...t.routes].some(([s,o])=>n.get(s)!==o)&&(we.delete(t),t.res.destroy())}Cs()}l(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Ht(e){const t=await ve(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Me(e),r=rt(n,g().sites),p=r.size?Qs({method:e.method,pathname:o,routes:r}):"other",h=Xs(o);if(p==="other")return(!r.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await ve.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:l(_=>Ys(_,r,u?.name||u?.login),"sidebar")}}}l(Ht,"access");const Ut=Ue.createServer((e,t)=>{Ht(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ea(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ea(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(st({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);_a(m,n).then(w=>Dt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Rt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Et(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Oa(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${Ct("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=st({pathname:s.pathname,dest:"document",sites:o})==="shell";if(U(p)==="files"){if(!jt(p.dir)){It(t,r,p);return}Hs(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}It(t,r,p)}),e.pipe(y)}l(Ea,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ht(e);if(s!=="owner"){Vs(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Me(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const p=`/${o}`;Ks({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${lt})`));
16
+ </main></body></html>`}u(Ra,"downPage");const Me=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=qs({file:k.join(J,"share-seen.json")}),be=new Set;function Mt(){const e=g().sites;for(const t of be){const n=it(t.login,e);[...t.routes].some(([s,o])=>n.get(s)!==o)&&(be.delete(t),t.res.destroy())}Cs()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Ut(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Me(e),r=it(n,g().sites),p=r.size?Qs({method:e.method,pathname:o,routes:r}):"other",h=Xs(o);if(p==="other")return(!r.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const l=await Te.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>Ys(_,r,l?.name||l?.login),"sidebar")}}}u(Ut,"access");const Bt=Be.createServer((e,t)=>{Ut(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ea(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ea(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);_a(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:pe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=pe();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Oa(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,l=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:o})==="shell";if(U(p)==="files"){if(!Rt(p.dir)){Ht(t,r,p);return}Ms(e,t,{dir:p.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Be.request({host:"127.0.0.1",port:p.port,method:e.method,path:l,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,r,p)}),e.pipe(y)}u(Ea,"serve"),Bt.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ut(e);if(s!=="owner"){Vs(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Me(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const p=`/${o}`;Ks({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Bt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
package/hub/readiness.mjs CHANGED
@@ -1 +1 @@
1
- var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,d;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,d="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",d="kickflip doctor --fix"):(n="Tailscale isn't signed in",d="tailscale up"):(n="Tailscale isn't installed on the host",d=H(a)),n?{ready:!1,missing:n,fix:{command:d}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:d,browser:f,added:m})=>({id:n,kind:d,browser:f,added:m}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...l(o,t[o]),waits:n}))}s(k,"readinessRows");const A=s(t=>(i,a)=>new Promise(l=>{$(i,a,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=y({home:t,env:i,now:a,cacheMs:l}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:A(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...o},d=new Map;async function f(r,c){if(!["github","tailscale"].includes(r))return n[r]({fresh:c});const p=r==="github"&&P({file:T(t),now:a()});if(p)return{limited:p};const g=d.get(r);if(!c&&g&&a()-g.at<l)return g.value;const u=Promise.resolve().then(()=>n[r]());return d.set(r,{at:a(),value:u}),u}s(f,"check");const m=s(async({fresh:r=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([c])=>[c,await f(c,r)]))),{now:a()}),"rows");async function w(r){const[c]=k({[r]:await f(r,!1)},{now:a()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:m,waiting:w}}s(F,"readinessReader");const E=y(),X=F({probes:{codex:E}});export{E as readCodex,X as readiness,F as readinessReader,k as readinessRows};
1
+ var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as A,tailscaleInstallCommand as D}from"../shared/tailscale.mjs";const H={slack:"Reviewer DMs and digests wait on this."},h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack",null],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const d=s((r,e)=>{switch(r){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=D(a)),n?{ready:!1,missing:n,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const E=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=y({home:t,env:i,now:a,cacheMs:d}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:E(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:A({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&P({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??H[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(F,"readinessReader");const G=y(),Y=F({probes:{codex:G}});export{G as readCodex,Y as readiness,F as readinessReader,k as readinessRows};