@lovek4d/kickflip 0.1.0 → 0.3.1

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.
Files changed (96) hide show
  1. package/CONTEXT.md +177 -78
  2. package/README.md +46 -5
  3. package/SKILL.md +58 -37
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +87 -32
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/bin/agent.mjs +1 -1
  11. package/board/bin/board.mjs +241 -69
  12. package/board/bin/builder-stop-hook.mjs +4 -3
  13. package/board/domain.mjs +5 -3
  14. package/board/on-host.mjs +1 -0
  15. package/board/public/app-arch.js +75 -0
  16. package/board/public/app-asks.js +33 -14
  17. package/board/public/app-cards.js +242 -49
  18. package/board/public/app-core.js +39 -5
  19. package/board/public/app-header.js +24 -6
  20. package/board/public/app-lanes.js +76 -26
  21. package/board/public/app-needs.js +1 -0
  22. package/board/public/app-snapshot.js +45 -11
  23. package/board/public/app-threads.js +13 -11
  24. package/board/public/app.html +4 -1
  25. package/board/public/app.js +131 -20
  26. package/board/public/dialogs.js +114 -116
  27. package/board/public/hub-kit.js +19 -2
  28. package/board/public/hub.css +24 -18
  29. package/board/public/linear-key.js +141 -0
  30. package/board/public/router.js +6 -3
  31. package/board/public/rules-editor.js +1 -1
  32. package/board/public/shell.js +1 -1
  33. package/board/public/sidebar.css +31 -11
  34. package/board/public/sidebar.js +88 -16
  35. package/board/public/style.css +141 -46
  36. package/board/public/toasts.js +6 -4
  37. package/board/public/ui-cards.js +102 -37
  38. package/board/public/ui-header.js +17 -8
  39. package/board/public/ui-hub.js +30 -12
  40. package/board/public/ui-links.js +18 -10
  41. package/board/public/ui-order.js +18 -0
  42. package/board/public/ui-page-header.js +4 -2
  43. package/board/public/ui-questions.js +37 -16
  44. package/board/public/ui-sidebar.js +49 -11
  45. package/board/public/ui-snapshot.js +15 -9
  46. package/board/public/ui-threads.js +25 -16
  47. package/board/public/ui.js +23 -11
  48. package/board/public/view-hub-settings.js +46 -30
  49. package/board/public/view-hub.js +64 -13
  50. package/board/public/view-notes.js +7 -4
  51. package/board/public/view-polling.js +3 -3
  52. package/board/public/view-questions.js +42 -22
  53. package/board/public/view-settings.js +90 -35
  54. package/board/review.mjs +1 -0
  55. package/board/server.mjs +12 -7
  56. package/board/store.mjs +14 -6
  57. package/board/tickets.mjs +3 -3
  58. package/board/worktree.mjs +14 -13
  59. package/docs/hub-session.md +3 -3
  60. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  61. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  62. package/hub/awake.mjs +1 -0
  63. package/hub/board-repo.mjs +1 -1
  64. package/hub/boards.mjs +4 -4
  65. package/hub/close-board.mjs +1 -1
  66. package/hub/duration.mjs +1 -0
  67. package/hub/github.mjs +4 -5
  68. package/hub/home.mjs +1 -1
  69. package/hub/hub.mjs +12 -10
  70. package/hub/linear.mjs +2 -2
  71. package/hub/new-board.mjs +2 -2
  72. package/hub/pr-report.mjs +1 -0
  73. package/hub/push.mjs +2 -2
  74. package/hub/readiness.mjs +2 -0
  75. package/hub/repos.mjs +3 -2
  76. package/hub/restart-board.mjs +1 -0
  77. package/hub/routes.mjs +1 -1
  78. package/hub/schedule.mjs +1 -1
  79. package/hub/sessions.mjs +2 -2
  80. package/hub/shell.mjs +2 -1
  81. package/hub/slack.mjs +2 -0
  82. package/hub/stream.mjs +12 -12
  83. package/hub/sw.js +1 -1
  84. package/hub/trust.mjs +2 -0
  85. package/hub/try.mjs +4 -3
  86. package/hub/upgrade.mjs +1 -0
  87. package/hub/usage.mjs +3 -3
  88. package/hub/views.mjs +1 -1
  89. package/package.json +1 -1
  90. package/shared/colors.css +7 -5
  91. package/shared/fake-gh.mjs +54 -0
  92. package/shared/service.mjs +16 -16
  93. package/shared/skills.mjs +2 -0
  94. package/shared/usage-gate.mjs +1 -1
  95. package/starter/public/claude.js +3 -2
  96. package/bin/web-ts.mjs +0 -2
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // A Claude Code Stop hook for a session that owns a board in the manifest. It blocks the turn from ending while that
3
+ // board has no live watcher, naming the command that relaunches it, or while a message there reads Working on it with
4
+ // no answer, naming each (its board server's /api/working). Claude Code sets stop_hook_active while a Stop hook keeps
5
+ // the turn going, so it blocks at most once a turn and a session whose Bash is broken can't loop. Any error lets the
6
+ // turn end (docs/adr/0131-the-cli-and-board-server-do-the-boards-housekeeping.md,
7
+ // docs/adr/0133-the-board-records-what-it-knows-and-commands-make-the-rule-calls.md).
8
+ import fs from 'node:fs';
9
+
10
+ process.stdout.on('error', () => {});
11
+
12
+ async function workingOn(board) {
13
+ try {
14
+ const r = await fetch(`http://127.0.0.1:${board.port}/api/working`, { signal: AbortSignal.timeout(3000) });
15
+ return r.ok ? (await r.json()).working || [] : [];
16
+ } catch { return []; }
17
+ }
18
+
19
+ try {
20
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
21
+ if (input.hook_event_name === 'Stop' && !input.stop_hook_active && typeof input.session_id === 'string' && input.session_id) {
22
+ const { listBoards, watcherRunning } = await import('../../hub/boards.mjs');
23
+ const { watcherCommand } = await import('../../bin/install.mjs');
24
+ const owned = listBoards().filter((b) => b.owner?.session === input.session_id);
25
+ // A watcher launched as a background task this turn may not have written data/watcher.json yet. Claude Code lists
26
+ // only in-flight tasks in background_tasks.
27
+ const tasks = Array.isArray(input.background_tasks) ? input.background_tasks : [];
28
+ const launched = (route) => tasks.some((t) => typeof t?.command === 'string'
29
+ && new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${route}['"]?\\s`).test(`${t.command} `));
30
+ const silent = owned.filter((b) => !launched(b.route) && !watcherRunning(b.dir, Date.now(), { grace: 0 }));
31
+ const reasons = [];
32
+ if (silent.length) {
33
+ const steps = silent.map((b) => `/${b.route}: ${watcherCommand(b.route, input.session_id)}`);
34
+ reasons.push(`Your board has no watcher, so its events wait unheard. Relaunch it as a background Bash task, then end your turn: ${steps.join('; ')}`);
35
+ }
36
+ const owed = (await Promise.all(owned.map(async (b) => (await workingOn(b)).map((w) => `/${b.route} ${w.kind} ${w.id}${w.card ? ` on ${w.card}` : ''}`)))).flat();
37
+ if (owed.length) reasons.push(`These read Working on it with no answer: ${owed.join('; ')}. Answer each with board.mjs reply or a thread line on its card, even a short "Looking into it" while an agent works, then end your turn.`);
38
+ if (reasons.length) console.log(JSON.stringify({ decision: 'block', reason: reasons.join(' ') }));
39
+ }
40
+ } catch {}
41
+ process.exitCode = 0;
package/hub/awake.mjs ADDED
@@ -0,0 +1 @@
1
+ var m=Object.defineProperty;var t=(e,i)=>m(e,"name",{value:i,configurable:!0});import{spawn as _}from"node:child_process";import{FOREVER as c,durationEnd as S,endOf as E,storedEnd as g}from"./duration.mjs";import{readSettingsFile as d,writeSettingsFile as k}from"./push.mjs";const x={linux:"Linux",win32:"Windows",freebsd:"FreeBSD"},w=t((e=process.platform)=>e==="darwin"?null:`macOS only \xB7 this host runs ${x[e]||e}`,"awakeUnsupported"),O=t((e=new Date,i=d())=>E(i.keep_awake_until,e),"awakeUntil");function T(e,{now:i=new Date,platform:n=process.platform}={}){const o=w(n);if(o)throw Object.assign(new Error(`Keep awake runs caffeinate, ${o}.`),{status:409});const a=e==="off"?null:S(i,e);if(a===null&&e!=="off")throw Object.assign(new Error("Keep awake must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, forever or a date and time; or off."),{status:400});const{keep_awake_until:l,...s}=d();return k(a?{...s,keep_awake_until:g(a)}:s),a}t(T,"setKeepAwake");function b({log:e=t(()=>{},"log"),platform:i=process.platform}={}){let n=null,o=null;const a=t(()=>{n&&(e(`keep awake: stopping caffeinate (pid ${n.pid})`),n.kill("SIGTERM"),n=null)},"end");function l(s=new Date){clearTimeout(o);const u=d(),f=w(i)?null:O(s,u);if(!f){if(u.keep_awake_until&&!w(i)){const{keep_awake_until:r,...p}=u;k(p)}a();return}if(!n){const r=_("caffeinate",["-ims","-w",String(process.pid)],{stdio:"ignore"});r.on("error",p=>{e(`keep awake: caffeinate failed: ${p.message}`),n===r&&(n=null)}),r.on("exit",()=>{n===r&&(n=null)}),n=r,e(`keep awake: caffeinate -ims (pid ${r.pid}) until ${f===c?"turned off":f.toISOString()}`)}f!==c&&(o=setTimeout(()=>l(),Math.min(f-s,2**31-1)))}return t(l,"apply"),{apply:l,stop:t(()=>{clearTimeout(o),a()},"stop")}}t(b,"caffeinate");export{w as awakeUnsupported,O as awakeUntil,b as caffeinate,T as setKeepAwake};
@@ -1 +1 @@
1
- var k=Object.defineProperty;var s=(r,t)=>k(r,"name",{value:t,configurable:!0});import{readManifest as b,withManifestLock as g,writeManifest as m,isBoardSite as p,listBoards as A,appendEvent as R}from"./boards.mjs";import{routeRecord as w}from"./routes.mjs";import{repoService as v,validateGithub as S,validateBranch as l,runRepo as x,githubFromOrigin as E}from"./repos.mjs";import{storedPath as f}from"../bin/install.mjs";import{cloneRoot as C}from"../shared/repos.mjs";import{githubFromOrigin as K}from"./repos.mjs";const d=s((r,t=400)=>{throw Object.assign(new Error(r),{status:t})},"refuse");function D(r,t=b()){const e=w(t.sites,r);return p(e)||d("Board not found.",404),e.hub===!0?{hub:!0,github:"",branch:"",repo:f(C())}:{github:e.github||"",branch:e.branch||"",repo:f(e.owner?.repo||"")}}s(D,"boardRepo");function y(r,t){const e=D(r,t);return e.hub&&d("The hub board works on kickflip\u2019s own clone, so its repo can\u2019t change.",409),e}s(y,"changeable");async function P(r,t,{repos:e=v(),event:a=R}={}){y(r),(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(i=>!["github","branch"].includes(i)))&&d("Send only github and branch.");const n=S(t.github),o=l(t.branch),c=f(await e.ensure(n,o));return g(async()=>{const i=b(),h=y(r,i),u=w(i.sites,r);Object.assign(u,{github:n,branch:o,updated:new Date().toISOString()}),u.owner={...u.owner,repo:c},await m(i);const O=A(i).find(j=>j.route===r),B=await a(O,"repo-changed",{github:n,branch:o,repo:c,previous:h});return{github:n,branch:o,repo:c,id:B}})}s(P,"changeBoardRepo");async function M(r,t){const e=await r("git",["-C",t,"symbolic-ref","--quiet","--short","refs/remotes/origin/HEAD"]),a=await r("git",["-C",t,"symbolic-ref","--quiet","--short","HEAD"]);for(const n of[e.status===0&&e.stdout.trim().replace(/^origin\//,""),a.status===0&&a.stdout.trim()])try{if(n)return l(n)}catch{}return""}s(M,"defaultBranch");async function T({run:r=x}={}){return g(async()=>{const t=b(),e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes("board-github")||!Object.keys(t.sites||{}).length)return[];const a=[];for(const[n,o]of Object.entries(t.sites||{})){if(!p(o)||o.github||o.hub)continue;const c=o.owner?.repo||o.dir,i=await r("git",["-C",c,"remote","get-url","origin"]),h=i.status===0?E(i.stdout):null;if(!h)continue;o.github=h;const u=o.branch||await M(r,c);u&&(o.branch=u),a.push(n)}return t.migrated=[...e,"board-github"],await m(t),a})}s(T,"migrateBoardRepos");export{D as boardRepo,P as changeBoardRepo,K as githubFromOrigin,T as migrateBoardRepos};
1
+ var x=Object.defineProperty;var d=(o,t)=>x(o,"name",{value:t,configurable:!0});import{readManifest as f,withManifestLock as h,writeManifest as g,isBoardSite as l,listBoards as E,appendEvent as H,buildsLocally as v}from"./boards.mjs";import{routeRecord as m}from"./routes.mjs";import{repoService as D,validateBranch as O,runRepo as j,githubFromOrigin as A}from"./repos.mjs";import{repoInput as q}from"./new-board.mjs";import{boardConfig as I}from"../board/store.mjs";import{storedPath as p}from"../bin/install.mjs";import{cloneRoot as L}from"../shared/repos.mjs";import{githubFromOrigin as Y}from"./repos.mjs";const w=d((o,t=400)=>{throw Object.assign(new Error(o),{status:t})},"refuse");function B(o,t=f()){const e=m(t.sites,o);l(e)||w("Board not found.",404);const s=v(e);return{github:e.github||"",...e.remote?{remote:e.remote}:{},branch:e.branch||"",repo:p(e.owner?.repo||""),...s?{local:!0}:{}}}d(B,"boardRepo");async function Q(o,t,{repos:e=D(),event:s=H}={}){B(o),(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(n=>!["github","remote","branch"].includes(n)))&&w("Send only github or remote, and branch.");const{github:i="",remote:r}=q(t),a=O(t.branch),u=m(f().sites,o).dir;r&&(await I(u)).tickets?.source==="github"&&w("This board files its cards as GitHub issues. Switch Tickets to Internal or Linear first.",409);const c=p(await e.ensure(i||r,a));return h(async()=>{const n=f(),k=B(o,n),b=m(n.sites,o);Object.assign(b,{github:i,remote:r,branch:a,updated:new Date().toISOString()}),delete b.local,i||delete b.github,r||delete b.remote,b.owner={...b.owner,repo:c},await g(n);const C=E(n).find(S=>S.route===o),y={github:i,...r?{remote:r}:{},branch:a,repo:c},R=await s(C,"repo-changed",{...y,previous:k});return{...y,id:R}})}d(Q,"changeBoardRepo");async function M(o,t){const e=await o("git",["-C",t,"symbolic-ref","--quiet","--short","refs/remotes/origin/HEAD"]),s=await o("git",["-C",t,"symbolic-ref","--quiet","--short","HEAD"]);for(const i of[e.status===0&&e.stdout.trim().replace(/^origin\//,""),s.status===0&&s.stdout.trim()])try{if(i)return O(i)}catch{}return""}d(M,"defaultBranch");async function U({run:o=j}={}){return h(async()=>{const t=f(),e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes("board-github")||!Object.keys(t.sites||{}).length)return[];const s=[];for(const[i,r]of Object.entries(t.sites||{})){if(!l(r)||r.github)continue;const a=r.owner?.repo||r.dir,u=await o("git",["-C",a,"remote","get-url","origin"]),c=u.status===0?A(u.stdout):null;if(!c)continue;r.github=c;const n=r.branch||await M(o,a);n&&(r.branch=n),s.push(i)}return t.migrated=[...e,"board-github"],await g(t),s})}d(U,"migrateBoardRepos");async function V({run:o=j,clone:t=L()}={}){return h(async()=>{const e=f(),s=Array.isArray(e.migrated)?e.migrated:[];if(s.includes("hub-local")||!Object.keys(e.sites||{}).length)return null;const i=Object.entries(e.sites||{}).find(([,r])=>r?.hub===!0&&l(r)&&r.local!==!0);if(i){const[,r]=i,a=r.owner?.repo||p(t),u=await o("git",["-C",a,"remote","get-url","origin"]),c=r.github||(u.status===0?A(u.stdout):null),n=await o("git",["-C",a,"symbolic-ref","--quiet","--short","HEAD"]);Object.assign(r,{local:!0,...c?{github:c}:{},...!r.branch&&n.status===0&&n.stdout.trim()?{branch:n.stdout.trim()}:{},owner:{...r.owner,repo:a}})}return e.migrated=[...s,"hub-local"],await g(e),i?i[0]:null})}d(V,"migrateHubBoard");export{B as boardRepo,Q as changeBoardRepo,Y as githubFromOrigin,U as migrateBoardRepos,V as migrateHubBoard};
package/hub/boards.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var z=Object.defineProperty;var o=(t,e)=>z(t,"name",{value:e,configurable:!0});import m from"node:fs";import c from"node:path";import Z from"node:os";import q from"node:crypto";import{execFileSync as Q}from"node:child_process";import{fileURLToPath as X}from"node:url";import{routeRecord as d}from"./routes.mjs";import{deliver as Y,readSettings as tt}from"./push.mjs";import{DEFAULTS as et,windowAt as F}from"./schedule.mjs";import{liveSessions as j}from"./live.mjs";import{withFileLock as nt}from"./locks.mjs";import{dropSessionPrompts as st,readPrompts as rt,savePrompts as ot,withPromptsLock as it}from"./prompts.mjs";import{storedPath as y}from"../bin/install.mjs";import{hubService as at}from"../shared/service.mjs";const ct=c.resolve(c.dirname(X(import.meta.url)),".."),A=c.join(Z.homedir(),".claude"),w=c.join(A,"kickflip"),P=c.join(A,"waves"),b=c.join(w,"manifest.json"),ut=c.join(w,"github-state.json"),lt=c.join(w,"wake-state.json"),C=c.join(w,"deaf-state.json"),E=c.join(w,"resume-state.json"),_=c.join(w,"link-faults.json"),B=c.join(w,"hub-code.json"),ft=c.join(w,"push","subs.json"),M=c.join(A,"slack-bridge","topics"),dt=["merged","dropped"],pt="1970-01-01T00:00:00Z",ht=2*6e4,mt=15*6e4,f=o((t,e)=>{try{return JSON.parse(m.readFileSync(t,"utf8"))}catch{return e}},"readJSON");function k(t,e){const n=`${t}.${process.pid}.tmp`;m.mkdirSync(c.dirname(t),{recursive:!0}),m.writeFileSync(n,JSON.stringify(e,null,2)+`
2
- `),m.renameSync(n,t)}o(k,"saveJSON");const g=o(()=>f(b,{sites:{}}),"readManifest"),wt=o(t=>nt(b,t),"withManifestLock");function kt(t){return it(()=>{const e=[...Object.values(t.sites||{}),...Object.values(t.closed||{}).map(i=>i.site)];for(const i of e.filter(Boolean))i.dir&&(i.dir=y(i.dir)),Array.isArray(i.cmd)&&(i.cmd=i.cmd.map(u=>y(u))),i.owner?.repo&&(i.owner.repo=y(i.owner.repo));for(const i of Object.values(t.starting||{}))i.repo&&(i.repo=y(i.repo));const n=g(),s=o((i,u)=>d(i.sites,u)?.owner||d(i.starting,u),"owner"),r=new Set([...Object.keys(n.sites||{}),...Object.keys(t.sites||{}),...Object.keys(n.starting||{}),...Object.keys(t.starting||{})]),a=[];for(const i of r){const u=s(n,i),l=s(t,i);(u?.session!==l?.session||u?.since!==l?.since)&&a.push(u?.session,l?.session)}if(a.some(Boolean)){const i=rt();st(i,a),ot(i)}k(b,t)})}o(kt,"writeManifest");const $=o(t=>`${t.repo}#${t.pr}`,"prKey"),T=o((t,e)=>`${t} ${e}${t===1?"":"s"}`,"plural"),H=o(t=>(Array.isArray(t)?t:[]).map(e=>typeof e=="string"?e:e?.name).filter(e=>typeof e=="string"&&e),"names"),U=o(t=>Array.isArray(t?.items),"isWave"),v=o(t=>Array.isArray(t?.cmd)&&t.cmd.some(e=>/board[/\\]server\.mjs$/.test(e))&&typeof t.dir=="string","isBoardSite");function gt(t,e,n){if(n==="file"){const r=f(c.resolve(t,e.items),null);return U(r)?[{slug:e.slug||c.basename(e.items,".json"),wave:r}]:[]}let s=[];try{s=m.readdirSync(P,{withFileTypes:!0}).filter(r=>r.isDirectory()).map(r=>r.name).sort()}catch{}return s.map(r=>({slug:r,wave:f(c.join(P,r,"wave.json"),null)})).filter(r=>U(r.wave))}o(gt,"wavesOf");function jt(t,e){return t.flatMap(({slug:n,wave:s})=>{const r=e||s.repo;return r?s.items.filter(a=>Number.isInteger(a?.pr)&&!dt.includes(a.status)).map(a=>({wave:n,id:a.id,pr:a.pr,repo:r,since:a.gh_since||pt})):[]})}o(jt,"prsOf");function N(t,e){const n=f(c.join(e.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",r=gt(e.dir,n,s);return{route:t,dir:e.dir,port:e.port,title:n.title||e.title||t,kind:s,waves:r,prs:jt(r,e.github),github:e.github,branch:e.branch,auto_build:n.auto_build===!0,reviewers:H(n.reviewers),owner:e.owner||null,watch:Array.isArray(e.watch)&&e.watch.length?e.watch:["data/events.jsonl"],filter:e.filter||null}}o(N,"boardOf");function D(t=g()){return Object.entries(t?.sites||{}).filter(([,e])=>v(e)).map(([e,n])=>N(e,n))}o(D,"listBoards");function ne(t=g()){const e=Object.entries(t?.sites||{}).find(([,n])=>n?.hub===!0&&v(n));return e?N(...e):void 0}o(ne,"hubBoard");const J="hub-flag";function se(){return wt(async()=>{const t=f(b,null);if(!t||typeof t.sites!="object"||!t.sites)return null;const e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes(J))return null;const n=d(t.sites,"hub-orchestrator"),s=v(n)&&!Object.values(t.sites).some(r=>r?.hub===!0);return s&&(n.hub=!0),t.migrated=[...e,J],await kt(t),s?"hub-orchestrator":null})}o(se,"migrateHubFlag");function yt(t,e){const n=o(s=>{try{return m.realpathSync(s)}catch{return c.resolve(s)}},"real");return c.resolve(t)===c.resolve(e)||n(t)===n(e)}o(yt,"samePath");function re(t,e=g()){const n=Object.entries(e?.sites||{}).find(([,s])=>v(s)&&yt(s.dir,t));return n?N(...n):null}o(re,"boardAt");async function G(t,e,n){try{const r=await fetch(`http://127.0.0.1:${t.port}/_claude/events`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:e,...n}),signal:AbortSignal.timeout(5e3)});if(r.ok)return(await r.json()).id}catch{}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:e,data:n};return m.mkdirSync(c.join(t.dir,"data"),{recursive:!0}),m.appendFileSync(c.join(t.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
- `),s.id}o(G,"appendEvent");async function oe(t,e,n){const s=[],r=[];for(const a of t)try{s.push({route:a.route,id:await G(a,e,n)})}catch(i){r.push(`${a.route}: could not add the ${e} event (${i.message})`)}return{events:s,failed:r}}o(oe,"sendToBoards");function bt(t,e=Date.now()){const n=f(c.join(t,"data","watcher.json"),null);if(!Number.isInteger(n?.pid))return!1;if(typeof n.exited_at=="string"&&e-Date.parse(n.exited_at)<=ht)return!0;try{process.kill(n.pid,0)}catch(s){if(s.code!=="EPERM")return!1}try{return Q("ps",["-p",String(n.pid),"-o","command="],{encoding:"utf8",timeout:3e3}).includes("wait-events.mjs")}catch{return!1}}o(bt,"watcherRunning");function vt(t,{now:e=Date.now(),running:n}={}){const s=t.owner?.session;return!!(s&&(n??j()).has(s))&&bt(t.dir,e)}o(vt,"isListening");function I(t,{now:e=Date.now(),running:n=j(),wakes:s=f(E,{})}={}){if(vt(t,{now:e,running:n}))return"listening";const r=t.owner?.session,a=d(s,t.route);return r&&t.owner.repo&&!n.has(r)&&!a?.error&&!a?.pushed_at?"asleep":"deaf"}o(I,"listenState");const L=o((t,e)=>c.join(t.dir,"data",`.watch-cursor-${e}.json`),"cursorPath"),S=o(t=>L(t,t.owner.session),"cursorFile");function ie(t){const e=t.owner?.session?f(S(t),{}):{};return Object.fromEntries(t.watch.map(n=>{const s=d(e,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}o(ie,"watcherCursor");const ae=o(t=>Object.fromEntries(Object.entries(R(t)).map(([e,n])=>[e,n.length])),"watchedCounts");function ce(t,{from:e,to:n=e,mark:s=null}){const r=f(L(t,e),null);if(!r&&!s)return;const a=Object.assign(Object.create(null),r);for(const[i,u]of Object.entries(s||{}))a[i]=Math.min(d(a,i)??u,u);k(L(t,n),a)}o(ce,"carryCursor");function R(t){const e=t.filter?new RegExp(t.filter):null;return Object.fromEntries(t.watch.map(n=>{let s=[];try{s=m.readFileSync(c.resolve(t.dir,n),"utf8").split(`
4
- `).filter(Boolean)}catch{}return[n,e?s.filter(r=>e.test(r)):s]}))}o(R,"watchedLines");function St(t){if(!t.owner?.session)return null;const e=f(S(t),null),n=e?0:Date.parse(t.owner.since||"")||0;let s=null;for(const[r,a]of Object.entries(R(t)))for(const i of e?a.slice(d(e,r)||0):a){let u=null;try{u=JSON.parse(i).ts}catch{}const l=Date.parse(u);Number.isNaN(l)||l<n||(!s||l<Date.parse(s))&&(s=u)}return s}o(St,"oldestUnheard");async function ue({manifest:t,now:e=new Date,running:n=j()}={}){const s=Object.assign(Object.create(null),f(C,{})),r=[],a=[];let i=!1;const u=f(E,{});for(const l of D(t)){const p=I(l,{now:e.getTime(),running:n,wakes:u});if(p==="listening"){d(s,l.route)&&(delete s[l.route],i=!0);continue}if(p==="asleep"||d(s,l.route))continue;const h=St(l);if(!h||e-Date.parse(h)<mt)continue;const x=Math.round((e-Date.parse(h))/6e4),W=n.has(l.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await Y({title:`${l.title} isn't listening`,body:`An event has waited ${x} min and ${W}`,url:`/${l.route}/`,tag:`deaf-${l.route}`},{kind:"fault",immediate:!0,at:e}),s[l.route]={at:e.toISOString(),since:h},i=!0,r.push(l.route)}catch(V){a.push(`${l.route}: could not push that it isn't listening (${V.message})`)}}return i&&k(C,s),{pushed:r,failed:a}}o(ue,"checkListening");const Ot=o(t=>c.join(t,"data","handshake.json"),"handshakeFile");async function le(t,e=g()){const n=D(e).find(r=>r.route===t);if(!n)throw new Error(`/${t} is not a board site in the manifest`);n.owner?.session&&!m.existsSync(S(n))&&k(S(n),Object.fromEntries(Object.entries(R(n)).map(([r,a])=>[r,a.length])));const s={id:await G(n,"hello",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(Ot(n.dir),{hello:s,ack:null}),{route:t,hello:s.id}}o(le,"handshake");const fe=["github","events","wakes","push","slack"];function xt(){let t=[];try{t=m.readdirSync(M).filter(e=>e.endsWith(".json"))}catch{}return Object.assign({},...t.map(e=>f(c.join(M,e),{}).items||{}))}o(xt,"topicItems");function At(t,e){const n=e?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=t.prs.filter(a=>e.prs?.[$(a)]?.at!==n.at).map(a=>`#${a.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(t.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const r=new Set(t.prs.map($)).size;return{link:"github",ok:!0,detail:r?`${T(r,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}o(At,"githubLink");const Et={listening:{ok:!0,detail:"Its session and a watcher are running."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},_t=o(t=>({link:"events",listen:t,...Et[t]}),"eventsLink");function $t(t,e,n){const s=e?.wakes||[];return s.length?(n?.last?.failed||[]).includes(t.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${s.join(", ")}.`}:{link:"wakes",ok:!1,detail:"No daily wakes are set (Hub, Polling)."}}o($t,"wakesLink");const Tt=o(t=>t?{link:"push",ok:!0,detail:`${T(t,"device")} subscribed.`}:{link:"push",ok:!1,detail:"No device has turned on notifications."},"pushLink");function Nt(t,e,n,s,r){const a=e?.review_sent;return!!(d(d(t.review_link_ts,e?.pr),s)||d(d(t.review_link_ts,n),s)||d(e?.review_link_ts,s)||(Array.isArray(a)?a.includes(s):d(a,s))||d(d(r,n)?.review_link_ts,s))}o(Nt,"sentTo");function Dt(t,e,n){const s=[];let r=0;for(const a of t.prs){const i=e?.prs?.[$(a)];if(i?.state!=="OPEN"||i.isDraft)continue;r++;const{wave:u}=t.waves.find(h=>h.slug===a.wave),l=u.items.find(h=>h.id===a.id),p=[...new Set([...t.reviewers,...H(u.reviewers)])].filter(h=>!Nt(u,l,`${a.wave}/${a.id}`,h,n));p.length&&s.push(`#${a.pr} has no link for ${p.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:r?`Every reviewer has the link to ${T(r,"PR")} in review.`:"No PR is in review."}}o(Dt,"slackLink");const It=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function K(t=c.join(ct,"hub","hub.mjs")){const e=c.dirname(c.dirname(t)),n=new Map,s=o(i=>{if(n.has(i))return;let u;try{u=m.readFileSync(i)}catch{return}n.set(i,u);for(const l of u.toString("utf8").matchAll(It))s(c.resolve(c.dirname(i),l[1]))},"visit");s(t);const r=[...n.keys()].map(i=>c.relative(e,i)).sort(),a=q.createHash("sha256");for(const i of r)a.update(i).update("\0").update(n.get(c.join(e,i))).update("\0");return{hash:a.digest("hex").slice(0,12),files:r}}o(K,"hubCode");function de(){const t={...K(),pid:process.pid,at:new Date().toISOString()};return k(B,t),t}o(de,"recordHubCode");const Lt=at().restartCommand,Rt=o(()=>f(B,null)?.hash??null,"runningHubCode"),Ft=o(()=>({running:Rt(),onDisk:K().hash}),"hubCodeState"),pe=o(({running:t,onDisk:e}=Ft())=>t&&t===e?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${e}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${Lt}`},"hubLink"),O=6e4,Pt={github:o(t=>(t+10)*O,"github"),events:o(()=>2*O,"events"),wakes:o(t=>(t+10)*O,"wakes"),push:o(()=>0,"push"),slack:o(t=>(t+10)*O,"slack")};function Ct(t,e,n){const s=n?.windows?.length?n:et;return Math.max(F(new Date(t),s).every_minutes,F(new Date(e),s).every_minutes)}o(Ct,"everyFor");function Bt(t,e,{settings:n,now:s,faults:r}){const a=r===void 0,i=a?d(f(_,{}),t)||{}:r,u={},l=e.map(p=>{if(p.ok)return{...p,state:"ok"};const h=d(i,p.link)||new Date(s).toISOString();u[p.link]=h;const x=Pt[p.link](Ct(Date.parse(h),s,n));return{...p,state:s-Date.parse(h)>=x?"broken":"settling",since:h}});if(a&&JSON.stringify(u)!==JSON.stringify(i)){const p=Object.assign(Object.create(null),f(_,{}));Object.keys(u).length?p[t]=u:delete p[t];try{k(_,p)}catch{}}return l}o(Bt,"settle");function Mt(t,e={}){const n=e.github??f(ut,{}),s=e.settings??tt(),r=[At(t,n),_t(e.listen??I(t)),$t(t,s,e.wakes??f(lt,{})),Tt(e.devices??(f(ft,[])||[]).length),Dt(t,n,e.topics??xt())];return Bt(t.route,r,{settings:s,now:e.now??Date.now(),faults:e.faults})}o(Mt,"linksOf");function he(t){const e={running:j(),wakes:f(E,{})};for(const n of D(t))Mt(n,{listen:I(n,e)})}o(he,"recordLinkFaults");export{mt as DEAF_AFTER,ut as GITHUB_STATE,Pt as GRACE,B as HUB_CODE,fe as LINKS,E as RESUME_STATE,lt as WAKE_STATE,G as appendEvent,re as boardAt,ce as carryCursor,ue as checkListening,le as handshake,Ot as handshakeFile,ne as hubBoard,K as hubCode,Ft as hubCodeState,pe as hubLink,v as isBoardSite,vt as isListening,Mt as linksOf,D as listBoards,I as listenState,se as migrateHubFlag,St as oldestUnheard,$ as prKey,jt as prsOf,f as readJSON,g as readManifest,de as recordHubCode,he as recordLinkFaults,Rt as runningHubCode,yt as samePath,k as saveJSON,oe as sendToBoards,ae as watchedCounts,R as watchedLines,ie as watcherCursor,bt as watcherRunning,wt as withManifestLock,kt as writeManifest};
1
+ var G=Object.defineProperty;var o=(e,t)=>G(e,"name",{value:t,configurable:!0});import d from"node:fs";import c from"node:path";import z from"node:os";import V from"node:crypto";import{execFileSync as Z}from"node:child_process";import{fileURLToPath as q}from"node:url";import{routeRecord as m}from"./routes.mjs";import{deliver as Q,readSettings as X}from"./push.mjs";import{DEFAULTS as Y,windowAt as R}from"./schedule.mjs";import{liveSessions as j}from"./live.mjs";import{withFileLock as ee}from"./locks.mjs";import{dropSessionPrompts as te,readPrompts as ne,savePrompts as se,withPromptsLock as re}from"./prompts.mjs";import{storedPath as v}from"../bin/install.mjs";import{hubService as oe}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{reviewLinks as ie,topicItems as ae}from"../board/review.mjs";const ce=c.resolve(c.dirname(q(import.meta.url)),".."),F=c.join(z.homedir(),".claude"),g=c.join(F,"kickflip"),I=c.join(F,"waves"),O=c.join(g,"manifest.json"),ue=c.join(g,"github-state.json"),le=c.join(g,"wake-state.json"),P=c.join(g,"deaf-state.json"),x=c.join(g,"resume-state.json"),E=c.join(g,"link-faults.json"),fe=c.join(g,"repo-fetch.json"),C=c.join(g,"hub-code.json"),pe=["merged","dropped"],he="1970-01-01T00:00:00Z",de=2*6e4,me=15*6e4,f=o((e,t)=>{try{return JSON.parse(d.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;d.mkdirSync(c.dirname(e),{recursive:!0}),d.writeFileSync(n,JSON.stringify(t,null,2)+`
2
+ `),d.renameSync(n,e)}o(k,"saveJSON");const y=o(()=>f(O,{sites:{}}),"readManifest"),rt=o(e=>ee(O,e),"withManifestLock");function ot(e){return re(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(a=>a.site)];for(const a of t.filter(Boolean))a.dir&&(a.dir=v(a.dir)),Array.isArray(a.cmd)&&(a.cmd=a.cmd.map(u=>v(u))),a.owner?.repo&&(a.owner.repo=v(a.owner.repo));for(const a of Object.values(e.starting||{}))a.repo&&(a.repo=v(a.repo));const n=y(),s=o((a,u)=>m(a.sites,u)?.owner||m(a.starting,u),"owner"),r=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),i=[];for(const a of r){const u=s(n,a),l=s(e,a);(u?.session!==l?.session||u?.since!==l?.since)&&i.push(u?.session,l?.session)}if(i.some(Boolean)){const a=ne();te(a,i),se(a)}k(O,e)})}o(ot,"writeManifest");const A=o(e=>`${e.repo}#${e.pr}`,"prKey"),M=o((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),B=o(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),we=o(e=>Object.fromEntries((Array.isArray(e)?e:[]).filter(t=>typeof t?.github=="string"&&t.github&&typeof t.name=="string"&&t.name).map(t=>[t.github.toLowerCase(),t.name])),"reviewerLogins"),H=o(e=>Array.isArray(e?.items),"isWave"),$=o(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>/board[/\\]server\.mjs$/.test(t))&&typeof e.dir=="string","isBoardSite"),it=o(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function ge(e,t,n){if(n==="file"){const r=f(c.resolve(e,t.items),null);return H(r)?[{slug:t.slug||c.basename(t.items,".json"),wave:r}]:[]}let s=[];try{s=d.readdirSync(I,{withFileTypes:!0}).filter(r=>r.isDirectory()).map(r=>r.name).sort()}catch{}return s.map(r=>({slug:r,wave:f(c.join(I,r,"wave.json"),null)})).filter(r=>H(r.wave))}o(ge,"wavesOf");function ke(e,t){return e.flatMap(({slug:n,wave:s})=>{const r=t||s.repo;return r?s.items.filter(i=>Number.isInteger(i?.pr)&&!pe.includes(i.status)).map(i=>({wave:n,id:i.id,pr:i.pr,repo:r,since:i.gh_since||he})):[]})}o(ke,"prsOf");function T(e,t){const n=f(c.join(t.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",r=ge(t.dir,n,s),i=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:s,waves:r,prs:ke(r,i),github:i,...t.remote&&!i?{remote:t.remote}:{},branch:t.branch,auto_build:n.auto_build===!0,githubLogin:typeof n.github_login=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(n.github_login)?n.github_login:null,tickets:n.tickets,reviewers:B(n.reviewers),reviewerLogins:we(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}o(T,"boardOf");function D(e=y()){return Object.entries(e?.sites||{}).filter(([,t])=>$(t)).map(([t,n])=>T(t,n))}o(D,"listBoards");function at(e=y()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&$(n));return t?T(...t):void 0}o(at,"hubBoard");function ye(e,t){const n=o(s=>{try{return d.realpathSync(s)}catch{return c.resolve(s)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}o(ye,"samePath");function ct(e,t=y()){const n=Object.entries(t?.sites||{}).find(([,s])=>$(s)&&ye(s.dir,e));return n?T(...n):null}o(ct,"boardAt");async function U(e,t,n){try{const r=await fetch(`http://127.0.0.1:${e.port}/_claude/events`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(5e3)});if(r.ok)return(await r.json()).id}catch{}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return d.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),d.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
+ `),s.id}o(U,"appendEvent");async function ut(e,t,n){const s=[],r=[];for(const i of e)try{s.push({route:i.route,id:await U(i,t,n)})}catch(a){r.push(`${i.route}: could not add the ${t} event (${a.message})`)}return{events:s,failed:r}}o(ut,"sendToBoards");function be(e,t=Date.now(),{grace:n=de}={}){const s=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(s?.pid))return!1;if(typeof s.exited_at=="string")return t-Date.parse(s.exited_at)<=n;try{process.kill(s.pid,0)}catch(r){if(r.code!=="EPERM")return!1}try{return Z("ps",["-p",String(s.pid),"-o","command="],{encoding:"utf8",timeout:3e3}).includes("wait-events.mjs")}catch{return!1}}o(be,"watcherRunning");function je(e,{now:t=Date.now(),running:n}={}){const s=e.owner?.session;return!!(s&&(n??j()).has(s))&&be(e.dir,t)}o(je,"isListening");function _(e,{now:t=Date.now(),running:n=j(),wakes:s=f(x,{})}={}){if(je(e,{now:t,running:n}))return"listening";const r=e.owner?.session,i=m(s,e.route);return!r||!e.owner.repo||i?.error||i?.pushed_at?"deaf":i?.resumed_at?"waking":n.has(r)?"deaf":"asleep"}o(_,"listenState");const L=o((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),S=o(e=>L(e,e.owner.session),"cursorFile");function lt(e){const t=e.owner?.session?f(S(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const s=m(t,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}o(lt,"watcherCursor");const ft=o(e=>Object.fromEntries(Object.entries(N(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function pt(e,{from:t,to:n=t,mark:s=null}){const r=f(L(e,t),null);if(!r&&!s)return;const i=Object.assign(Object.create(null),r);for(const[a,u]of Object.entries(s||{}))i[a]=Math.min(m(i,a)??u,u);k(L(e,n),i)}o(pt,"carryCursor");function N(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let s=[];try{s=d.readFileSync(c.resolve(e.dir,n),"utf8").split(`
4
+ `).filter(Boolean)}catch{}return[n,t?s.filter(r=>t.test(r)):s]}))}o(N,"watchedLines");function ve(e){if(!e.owner?.session)return null;const t=f(S(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let s=null;for(const[r,i]of Object.entries(N(e)))for(const a of t?i.slice(m(t,r)||0):i){let u=null;try{u=JSON.parse(a).ts}catch{}const l=Date.parse(u);Number.isNaN(l)||l<n||(!s||l<Date.parse(s))&&(s=u)}return s}o(ve,"oldestUnheard");async function ht({manifest:e,now:t=new Date,running:n=j()}={}){const s=Object.assign(Object.create(null),f(P,{})),r=[],i=[];let a=!1;const u=f(x,{});for(const l of D(e)){const p=_(l,{now:t.getTime(),running:n,wakes:u});if(p==="listening"){m(s,l.route)&&(delete s[l.route],a=!0);continue}if(p==="asleep"||p==="waking"||m(s,l.route))continue;const h=ve(l);if(!h||t-Date.parse(h)<me)continue;const w=Math.round((t-Date.parse(h))/6e4),K=n.has(l.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await Q({title:`${l.title} isn't listening`,body:`An event has waited ${w} min and ${K}`,url:`/${l.route}/`,tag:`deaf-${l.route}`},{kind:"fault",immediate:!0,at:t}),s[l.route]={at:t.toISOString(),since:h},a=!0,r.push(l.route)}catch(W){i.push(`${l.route}: could not push that it isn't listening (${W.message})`)}}return a&&k(P,s),{pushed:r,failed:i}}o(ht,"checkListening");const Se=o(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function dt(e,t=y()){const n=D(t).find(r=>r.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!d.existsSync(S(n))&&k(S(n),Object.fromEntries(Object.entries(N(n)).map(([r,i])=>[r,i.length])));const s={id:await U(n,"hello",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(Se(n.dir),{hello:s,ack:null}),{route:e,hello:s.id}}o(dt,"handshake");const mt=["github","repo-fetch","events","wakes","slack"];function Oe(e,t){if(e.remote)return null;const n=t?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=e.prs.filter(i=>t.prs?.[A(i)]?.at!==n.at).map(i=>`#${i.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(e.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const r=new Set(e.prs.map(A)).size;return{link:"github",ok:!0,detail:r?`${M(r,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}o(Oe,"githubLink");function xe(e){return e?e.ok?{link:"repo-fetch",ok:!0,detail:"The hub's last git fetch of this board's repo worked."}:{link:"repo-fetch",ok:!1,detail:`${e.error.replace(/[.\s]*$/,".")} ${e.hint}.`}:null}o(xe,"repoFetchLink");const Ee={listening:{ok:!0,detail:"Its session and a watcher are running."},waking:{ok:!0,detail:"Waking: the hub resumed its session, which listens again once it rejoins."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event or a tap on its page."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},Ae=o(e=>({link:"events",listen:e,...Ee[e]}),"eventsLink");function $e(e,t,n){const s=t?.wakes||[];return s.length?(n?.last?.failed||[]).includes(e.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${s.join(", ")}.`}:{link:"wakes",ok:!1,detail:"No daily wakes are set (Hub, Polling)."}}o($e,"wakesLink");function Te(e,t,n){const s=[];let r=0;for(const i of e.prs){const a=t?.prs?.[A(i)];if(a?.state!=="OPEN"||a.isDraft)continue;r++;const{wave:u}=e.waves.find(w=>w.slug===i.wave),l=u.items.find(w=>w.id===i.id),p=ie(u,l,`${i.wave}/${i.id}`,n),h=[...new Set([...e.reviewers,...B(u.reviewers)])].filter(w=>!Object.hasOwn(p,w));h.length&&s.push(`#${i.pr} has no link for ${h.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:r?`Every reviewer has the link to ${M(r,"PR")} in review.`:"No PR is in review."}}o(Te,"slackLink");const De=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function J(e=c.join(ce,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,s=o(a=>{if(n.has(a))return;let u;try{u=d.readFileSync(a)}catch{return}n.set(a,u);for(const l of u.toString("utf8").matchAll(De))s(c.resolve(c.dirname(a),l[1]))},"visit");s(e);const r=[...n.keys()].map(a=>c.relative(t,a)).sort(),i=V.createHash("sha256");for(const a of r)i.update(a).update("\0").update(n.get(c.join(t,a))).update("\0");return{hash:i.digest("hex").slice(0,12),files:r}}o(J,"hubCode");function wt(){const e={...J(),pid:process.pid,at:new Date().toISOString()};return k(C,e),e}o(wt,"recordHubCode");const _e=oe().restartCommand,Le=o(()=>f(C,null)?.hash??null,"runningHubCode"),Ne=o(()=>({running:Le(),onDisk:J().hash}),"hubCodeState"),gt=o(({running:e,onDisk:t}=Ne())=>e&&e===t?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${t}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${_e}`},"hubLink"),b=6e4,Re={github:o(e=>(e+10)*b,"github"),"repo-fetch":o(e=>(e+10)*b,"repo-fetch"),events:o(()=>2*b,"events"),wakes:o(e=>(e+10)*b,"wakes"),slack:o(e=>(e+10)*b,"slack")};function Fe(e,t,n){const s=n?.windows?.length?n:Y;return Math.max(R(new Date(e),s).every_minutes,R(new Date(t),s).every_minutes)}o(Fe,"everyFor");function Ie(e,t,{settings:n,now:s,faults:r}){const i=r===void 0,a=i?m(f(E,{}),e)||{}:r,u={},l=t.map(p=>{if(p.ok)return{...p,state:"ok"};const h=m(a,p.link)||new Date(s).toISOString();u[p.link]=h;const w=Re[p.link](Fe(Date.parse(h),s,n));return{...p,state:s-Date.parse(h)>=w?"broken":"settling",since:h}});if(i&&JSON.stringify(u)!==JSON.stringify(a)){const p=Object.assign(Object.create(null),f(E,{}));Object.keys(u).length?p[e]=u:delete p[e];try{k(E,p)}catch{}}return l}o(Ie,"settle");function Pe(e,t={}){const n=t.github??f(ue,{}),s=t.settings??X(),r=[Oe(e,n),xe(m(t.fetches??f(fe,{}),e.route)),Ae(t.listen??_(e)),$e(e,s,t.wakes??f(le,{})),Te(e,n,t.topics??ae())].filter(Boolean);return Ie(e.route,r,{settings:s,now:t.now??Date.now(),faults:t.faults})}o(Pe,"linksOf");function kt(e){const t={running:j(),wakes:f(x,{})};for(const n of D(e))Pe(n,{listen:_(n,t)})}o(kt,"recordLinkFaults");export{me as DEAF_AFTER,ue as GITHUB_STATE,Re as GRACE,C as HUB_CODE,mt as LINKS,fe as REPO_FETCH,x as RESUME_STATE,le as WAKE_STATE,I as WAVES,U as appendEvent,ct as boardAt,it as buildsLocally,pt as carryCursor,ht as checkListening,dt as handshake,Se as handshakeFile,at as hubBoard,J as hubCode,Ne as hubCodeState,gt as hubLink,$ as isBoardSite,je as isListening,Pe as linksOf,D as listBoards,_ as listenState,ve as oldestUnheard,A as prKey,ke as prsOf,f as readJSON,y as readManifest,wt as recordHubCode,kt as recordLinkFaults,we as reviewerLogins,Le as runningHubCode,ye as samePath,k as saveJSON,ut as sendToBoards,ft as watchedCounts,N as watchedLines,lt as watcherCursor,be as watcherRunning,rt as withManifestLock,ot as writeManifest};
@@ -1 +1 @@
1
- var S=Object.defineProperty;var c=(s,i)=>S(s,"name",{value:i,configurable:!0});import O from"node:fs";import{carryCursor as k,isBoardSite as m,listBoards as y,readManifest as l,watcherCursor as x,withManifestLock as b,writeManifest as w}from"./boards.mjs";import{routeRecord as d,validRoute as j}from"./routes.mjs";import{stopSession as C,withSessionLock as g}from"./sessions.mjs";import{liveSessions as v}from"./live.mjs";import{closeBoardBuilders as M,openBoardBuilders as N}from"../board/store.mjs";const n=c((s,i=400)=>{throw Object.assign(new Error(s),{status:i})},"refuse"),u=c(s=>{j(s)||n("Choose a board route.")},"checkRoute");function p(s,i){u(s);const o=y(i).find(f=>f.route===s);return o||n("That board is not open.",404),o}c(p,"boardOf");async function L(s,i=l()){const o=p(s,i);return{route:s,title:o.title,hub:d(i.sites,s).hub===!0,prs:o.prs,builders:await N(o.dir)}}c(L,"boardClosure");function A(s=l()){return Object.entries(s.closed||{}).filter(([,i])=>m(i?.site)).map(([i,o])=>({route:i,title:o.title||o.site.title||i,closedAt:o.closed_at,dir:o.site.dir}))}c(A,"closedBoards");async function K(s,i,{live:o,stop:f}={}){return u(s),g(s,()=>b(async()=>{const r=l(),e=p(s,r),t=d(r.sites,s);t.hub===!0&&n("The hub board cannot be closed while it is flagged as the hub.",409),i!==e.title&&n("Type the board\u2019s name exactly to close it."),d(r.closed,s)&&n("That route already has a closed board.",409);const a=t.owner?.session&&y(r).filter(h=>h.route!==s&&h.owner?.session===t.owner.session);a?.length&&n(`This session is also recorded by ${a.map(h=>`${h.title} (/${h.route})`).join(", ")}; close refused.`,409);const T=x(e);await C(t.owner,{live:o,stop:f});const B=await M(e.dir);return r.closed={...r.closed,[s]:{site:t,title:e.title,cursor:T,closed_at:new Date().toISOString()}},delete r.sites[s],await w(r),{route:s,filesKept:t.dir,buildersClosed:B.length,next:"/_hub/"}}))}c(K,"closeBoard");async function q(s,{run:i,live:o}={}){return u(s),g(s,async({resume:f})=>{const r=await b(async()=>{const e=l(),t=d(e.closed,s);return m(t?.site)||n("That board is not closed.",404),(d(e.sites,s)||d(e.starting,s))&&n("That route is already in use.",409),Object.values(e.sites||{}).some(a=>a.port===t.site.port)&&n("The board\u2019s recorded port is in use by another site.",409),O.statSync(t.site.dir,{throwIfNoEntry:!1})?.isDirectory()||n("The board\u2019s folder is missing.",409),(!t.site.owner?.session||!t.site.owner.repo)&&n("The board has no recorded session and folder to resume.",409),e.sites||={},e.sites[s]=t.site,t.cursor&&k(p(s,e),{from:t.site.owner.session,mark:t.cursor}),delete e.closed[s],Object.keys(e.closed).length||delete e.closed,await w(e),t});try{const e=await f({run:i,live:o,mark:r.cursor});return{route:s,session:e.session}}catch(e){const t=d(l().sites,s);if(e.reason==="live"&&t?.owner?.session&&(o||v)().has(t.owner.session))return{route:s,session:t.owner.session};throw await b(async()=>{const a=l();JSON.stringify(d(a.sites,s))===JSON.stringify(r.site)&&(delete a.sites[s],a.closed={...a.closed,[s]:r},await w(a))}),e}})}c(q,"reopenBoard");export{L as boardClosure,K as closeBoard,A as closedBoards,q as reopenBoard};
1
+ var T=Object.defineProperty;var d=(s,i)=>T(s,"name",{value:i,configurable:!0});import O from"node:fs";import{carryCursor as k,isBoardSite as y,listBoards as g,readManifest as w,watcherCursor as x,withManifestLock as m,writeManifest as p}from"./boards.mjs";import{routeRecord as l,validRoute as j}from"./routes.mjs";import{stopSession as C,withSessionLock as B}from"./sessions.mjs";import{liveSessions as v}from"./live.mjs";import{closeBoardBuilders as M,openBoardBuilders as N}from"../board/store.mjs";const n=d((s,i=400)=>{throw Object.assign(new Error(s),{status:i})},"refuse"),b=d(s=>{j(s)||n("Choose a board route.")},"checkRoute");function u(s,i){b(s);const o=g(i).find(f=>f.route===s);return o||n("That board is not open.",404),o}d(u,"boardOf");async function L(s,i=w()){const o=u(s,i);return{route:s,title:o.title,prs:o.prs,builders:await N(o.dir)}}d(L,"boardClosure");function A(s=w()){return Object.entries(s.closed||{}).filter(([,i])=>y(i?.site)).map(([i,o])=>({route:i,title:o.title||o.site.title||i,closedAt:o.closed_at,dir:o.site.dir}))}d(A,"closedBoards");async function K(s,i,{live:o,stop:f}={}){return b(s),B(s,()=>m(async()=>{const r=w(),c=u(s,r),e=l(r.sites,s);i!==c.title&&n("Type the board\u2019s name exactly to close it."),l(r.closed,s)&&n("That route already has a closed board.",409);const t=e.owner?.session&&g(r).filter(h=>h.route!==s&&h.owner?.session===e.owner.session);t?.length&&n(`This session is also recorded by ${t.map(h=>`${h.title} (/${h.route})`).join(", ")}; close refused.`,409);const a=x(c);await C(e.owner,{live:o,stop:f});const S=await M(c.dir);return r.closed={...r.closed,[s]:{site:e,title:c.title,cursor:a,closed_at:new Date().toISOString()}},delete r.sites[s],await p(r),{route:s,filesKept:e.dir,buildersClosed:S.length,next:"/_hub/"}}))}d(K,"closeBoard");async function q(s,{run:i,tmux:o,live:f}={}){return b(s),B(s,async({resume:r})=>{const c=await m(async()=>{const e=w(),t=l(e.closed,s);return y(t?.site)||n("That board is not closed.",404),(l(e.sites,s)||l(e.starting,s))&&n("That route is already in use.",409),Object.values(e.sites||{}).some(a=>a.port===t.site.port)&&n("The board\u2019s recorded port is in use by another site.",409),O.statSync(t.site.dir,{throwIfNoEntry:!1})?.isDirectory()||n("The board\u2019s folder is missing.",409),(!t.site.owner?.session||!t.site.owner.repo)&&n("The board has no recorded session and folder to resume.",409),e.sites||={},e.sites[s]=t.site,t.cursor&&k(u(s,e),{from:t.site.owner.session,mark:t.cursor}),delete e.closed[s],Object.keys(e.closed).length||delete e.closed,await p(e),t});try{const e=await r({run:i,tmux:o,live:f,mark:c.cursor});return{route:s,session:e.session}}catch(e){const t=l(w().sites,s);if(e.reason==="live"&&t?.owner?.session&&(f||v)().has(t.owner.session))return{route:s,session:t.owner.session};throw await m(async()=>{const a=w();JSON.stringify(l(a.sites,s))===JSON.stringify(c.site)&&(delete a.sites[s],a.closed={...a.closed,[s]:c},await p(a))}),e}})}d(q,"reopenBoard");export{L as boardClosure,K as closeBoard,A as closedBoards,q as reopenBoard};
@@ -0,0 +1 @@
1
+ var s=Object.defineProperty;var r=(t,e)=>s(t,"name",{value:e,configurable:!0});const o="forever",x=Object.freeze([["1h","1h"],["3h","3h"],["6h","6h"],["1d","1d"],[o,"Forever"]]),u=30*864e5,_=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})?$/,i={m:6e4,h:36e5,d:864e5,w:7*864e5};function f(t,e){const n=String(e);if(n===o)return o;const c=/^([1-9]\d{0,3})([mhdw])$/.exec(n),d=c?new Date(t.getTime()+Number(c[1])*i[c[2]]):_.test(n)?new Date(n):null;return l(t,d)?d:null}r(f,"durationEnd");const l=r((t,e)=>!!e&&e>t&&e-t<=u,"inRange");function h(t,e){if(t===o)return o;const n=typeof t=="string"?new Date(t):null;return n&&n>e?n:null}r(h,"endOf");const a=r(t=>t===o?o:t.toISOString(),"storedEnd");export{_ as AT_RE,x as CHOICES,o as FOREVER,u as MAX_MS,f as durationEnd,h as endOf,l as inRange,a as storedEnd};
package/hub/github.mjs CHANGED
@@ -1,5 +1,4 @@
1
- var B=Object.defineProperty;var h=(e,s)=>B(e,"name",{value:s,configurable:!0});import D from"node:fs";import O from"node:path";import J from"node:os";import{execFile as z}from"node:child_process";import{wakesDue as C,localDay as K}from"./schedule.mjs";import{GITHUB_STATE as A,WAKE_STATE as G,listBoards as M,appendEvent as j,prKey as k,readJSON as H,saveJSON as _}from"./boards.mjs";const P=O.join(J.homedir(),".claude","skills","monitor","monitor-gh.sh"),W="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",E=["ci","review","comments","merged","mergeable","draft"],b=h(e=>e&&e!=="UNKNOWN"?e:null,"known");function F(e){const r=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:r?String(r.id):null,eventAt:r?.at||null}}h(F,"summarize");function U(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),E.filter(i=>r.has(i));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
2
- `)!==s.failing.join(`
3
- `))&&r.add("ci"),e.reviewDecision!==s.reviewDecision&&r.add("review"),s.eventId&&s.eventId!==e.eventId&&r.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&r.add("merged");for(const i of["mergeable","mergeStateStatus"])b(e[i])&&b(s[i])&&e[i]!==s[i]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),E.filter(i=>r.has(i))}h(U,"changesBetween");function R(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!b(s[n])&&b(e?.[n])&&(r[n]=e[n]);return r}h(R,"carry");function L({pr:e,repo:s,since:r}){return new Promise((n,i)=>{z("/bin/zsh",[P,String(e),r],{env:{...process.env,PATH:W,MONITOR_REPO:s},timeout:9e4,maxBuffer:16*1024*1024},(a,f,c)=>{if(a)return i(new Error(`${s}#${e}: ${String(c||a.message).trim().split(`
4
- `)[0].slice(0,200)}`));try{n(JSON.parse(f))}catch(g){i(new Error(`${s}#${e}: unreadable output (${g.message})`))}})})}h(L,"runMonitorGh");function q(e,s){const r=O.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;D.mkdirSync(O.dirname(r),{recursive:!0}),D.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
5
- `),D.renameSync(n,r)}h(q,"writeChecked");async function te({every:e,manifest:s}){const r=new Date().toISOString(),n=H(A,{prs:{}});n.prs||={},n.acknowledged||={};const i=M(s),a=new Map;for(const t of i.flatMap(l=>l.prs)){const l=a.get(k(t));(!l||t.since<l.since)&&a.set(k(t),t)}const f=[],c={},g={};for(const[t,l]of a){try{c[t]=F(await L(l))}catch(p){c[t]=null,f.push(p.message);continue}g[t]=[]}const u=[],m=[],I=new Set;for(const t of i){const l=t.prs.map(o=>{const d=k(o),w=JSON.stringify([t.route,o.wave,o.id]),S=n.acknowledged[d]?.[w]||n.prs[d],v=c[d]?U(S,c[d]):[];return v.length&&(g[d]=E.filter(T=>g[d].includes(T)||v.includes(T))),{p:o,key:d,recipient:w,previous:S,changes:v}}),p=l.filter(o=>o.changes.length);let $=t.prs.every(o=>c[k(o)]),N=!0;if(p.length)try{u.push({route:t.route,id:await j(t,"github-change",{prs:p.map(({p:o,changes:d})=>({wave:o.wave,id:o.id,pr:o.pr,changes:d}))})})}catch(o){f.push(`${t.route}: could not add the github-change event (${o.message})`);for(const{key:d}of p)I.add(d);$=N=!1}for(const{key:o,recipient:d,previous:w,changes:S}of l)c[o]&&(N||!S.length)&&(n.acknowledged[o]||={},n.acknowledged[o][d]=R(w,c[o]));if($)try{q(t.dir,t.prs.length?e:null),m.push(t.route)}catch(o){f.push(`${t.route}: could not write checked.json (${o.message})`)}}const y=[];for(const[t,l]of Object.entries(c))!l||I.has(t)||(g[t].length&&y.push({repo:a.get(t).repo,pr:a.get(t).pr,changes:g[t]}),n.prs[t]={...R(n.prs[t],l),at:r},delete n.acknowledged[t]);for(const t of Object.keys(n.acknowledged))a.has(t)||delete n.acknowledged[t];return n.lastCheck={at:r,watched:a.size,failed:f.length,changed:y.length,boards:m},_(A,n),{at:r,watched:a.size,failed:f,changes:y,events:u,checked:m}}h(te,"checkGitHub");async function ne({now:e=new Date,settings:s,manifest:r}){const n=H(G,{});n.sent||={};const i=C(e,s,n.sent),a=[],f=[],c=[];if(!i.length)return{due:i,events:a,failed:f};const g=M(r);for(const u of g)try{a.push({route:u.route,id:await j(u,"check-github",{reason:"morning"})})}catch(m){f.push(`${u.route}: could not add the morning check-github event (${m.message})`),c.push(u.route)}if(a.length||!g.length){for(const u of i)n.sent[u]=K(e);n.last={at:e.toISOString(),due:i,routes:a.map(u=>u.route),failed:c},_(G,n)}return{due:i,events:a,failed:f}}h(ne,"sendWakes");export{E as CHANGE_KINDS,R as carry,U as changesBetween,te as checkGitHub,ne as sendWakes,F as summarize};
1
+ var x=Object.defineProperty;var y=(e,n)=>x(e,"name",{value:n,configurable:!0});import J from"node:fs";import P from"node:path";import"node:os";import{savedClient as F}from"./linear.mjs";import{fetchPR as Q,reportOf as X,githubLogin as Y}from"./pr-report.mjs";import{wakesDue as Z,localDay as ee}from"./schedule.mjs";import{GITHUB_STATE as z,WAKE_STATE as H,WAVES as K,listBoards as _,appendEvent as q,prKey as R,readJSON as W,saveJSON as U,reviewerLogins as te}from"./boards.mjs";import{appendThread as ne,setItem as se}from"../board/store.mjs";const C=["ci","review","comments","merged","mergeable","draft"],T=y(e=>e&&e!=="UNKNOWN"?e:null,"known");function oe(e){const o=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:o?String(o.id):null,eventAt:o?.at||null}}y(oe,"summarize");function re(e,n){const o=new Set;if(!e)return n.state==="MERGED"&&o.add("merged"),n.eventId&&o.add("comments"),C.filter(i=>o.has(i));(n.pending===0&&(e.pending>0||e.head!==n.head)||e.failing.join(`
2
+ `)!==n.failing.join(`
3
+ `))&&o.add("ci"),e.reviewDecision!==n.reviewDecision&&o.add("review"),n.eventId&&n.eventId!==e.eventId&&o.add("comments"),n.state==="MERGED"&&e.state!=="MERGED"&&o.add("merged");for(const i of["mergeable","mergeStateStatus"])T(e[i])&&T(n[i])&&e[i]!==n[i]&&o.add("mergeable");return e.isDraft!==n.isDraft&&o.add("draft"),C.filter(i=>o.has(i))}y(re,"changesBetween");function V(e,n){const o={...n};for(const s of["mergeable","mergeStateStatus"])!T(n[s])&&T(e?.[s])&&(o[s]=e[s]);return o}y(V,"carry");function ie(e,n){const o=P.join(e,"data","checked.json"),s=`${o}.${process.pid}.tmp`;J.mkdirSync(P.dirname(o),{recursive:!0}),J.writeFileSync(s,JSON.stringify(n?{at:new Date().toISOString(),every:n}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
+ `),J.renameSync(s,o)}y(ie,"writeChecked");async function ke({every:e,manifest:n}){const o=new Date().toISOString(),s=W(z,{prs:{}});s.prs||={},s.acknowledged||={},s.threaded||={};const i=_(n),l=[];let d=null;if(i.some(t=>!t.githubLogin&&t.prs.length))try{d=await Y()}catch(t){l.push(t.message)}const m=y((t,g)=>JSON.stringify([R(t),g]),"fetchKey"),f=new Map,r=new Map,c=new Map;for(const t of i){t.me=t.githubLogin||d;for(const g of t.prs){const w=R(g);f.set(w,g);const N=c.get(w)||new Set;if(N.add(t.me),c.set(w,N),!t.me)continue;const I=m(g,t.me),a=r.get(I);(!a||g.since<a.since)&&r.set(I,{...g,me:t.me})}}const h={},k={},u=new Map,p=Object.fromEntries([...f.keys()].map(t=>[t,[]]));for(const[t,g]of r)try{k[t]=X(await Q(g),g);const w=oe(k[t]);h[t]=w,u.has(R(g))||u.set(R(g),w)}catch(w){h[t]=null,l.push(w.message)}const S=[],E=[],$=new Set,D=new Map,b=new Set;for(const t of i){const g=t.prs.map(a=>{const v=R(a),G=JSON.stringify([t.route,a.wave,a.id]),M=s.acknowledged[v]?.[G]||s.prs[v],A=h[m(a,t.me)],B=A?re(M,A):[];return B.length&&(p[v]=C.filter(L=>p[v].includes(L)||B.includes(L))),{p:a,key:v,recipient:G,previous:M,next:A,changes:B}});for(const a of g){const v=k[m(a.p,t.me)];v&&await ce(t,a,v,{state:s,merged:D,failed:l})&&b.add(t.route)}const w=g.filter(a=>a.changes.length);let N=g.every(a=>a.next),I=!0;if(w.length)try{S.push({route:t.route,id:await q(t,"github-change",{prs:w.map(({p:a,changes:v})=>({wave:a.wave,id:a.id,pr:a.pr,changes:v}))})})}catch(a){l.push(`${t.route}: could not add the github-change event (${a.message})`);for(const{key:v}of w)$.add(v);N=I=!1}for(const{key:a,recipient:v,previous:G,next:M,changes:A}of g)M&&(I||!A.length)&&(s.acknowledged[a]||={},s.acknowledged[a][v]=V(G,M));if(N)try{ie(t.dir,t.prs.length?e:null),E.push(t.route)}catch(a){l.push(`${t.route}: could not write checked.json (${a.message})`)}}const O=[];for(const[t,g]of f){const w=u.get(t);!w||$.has(t)||(p[t].length&&O.push({repo:g.repo,pr:g.pr,changes:p[t]}),s.prs[t]={...V(s.prs[t],w),at:o},c.get(t).size===1&&delete s.acknowledged[t])}for(const t of Object.keys(s.acknowledged))f.has(t)||delete s.acknowledged[t];for(const t of Object.keys(s.threaded))f.has(t)||delete s.threaded[t];s.lastCheck={at:o,watched:f.size,failed:l.length,changed:O.length,boards:E},U(z,s);const j={moved:[],failed:[]};try{await de(i,h,m,j)}catch(t){j.failed.push(`linear: ${t.message}`)}return{at:o,watched:f.size,failed:l,changes:O,events:S,checked:E,merged:[...b],linear:j}}y(ke,"checkGitHub");function ae(e){const n=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],n].filter(Boolean).join(" ")}y(ae,"lineText");async function ce(e,{p:n,key:o,recipient:s},i,{state:l,merged:d,failed:m}){const f=`${n.wave}/${n.id}`,r=new Set(l.threaded[o]?.[s]||[]),c=e.waves.find(u=>u.slug===n.wave)?.wave,h={...e.reviewerLogins,...te(c?.reviewers)};for(const u of i.events){const p=`${u.kind}:${u.id}`;if(!["user","reviewer"].includes(u.who)||u.at<=n.since||r.has(p))continue;const S=ae(u);try{S&&await ne(e.dir,f,{text:S,via:"github",who:u.who==="user"?"love":h[String(u.login).toLowerCase()]||u.login,link:u.url||`https://github.com/${n.repo}/pull/${n.pr}`}),r.add(p)}catch(E){m.push(`${e.route}: could not write ${f}'s GitHub line (${E.message})`)}}if(r.size&&((l.threaded[o]||={})[s]=[...r]),i.state!=="MERGED")return!1;const k=JSON.stringify([e.kind==="waves"?K:e.dir,n.wave,n.id]);if(!d.has(k))try{await se(e.dir,f,{status:"merged",force:!0}),d.set(k,!0)}catch(u){m.push(`${e.route}: could not record ${f} merged (${u.message})`),d.set(k,!1)}return d.get(k)}y(ce,"recordOnCard");async function de(e,n,o,s){const i=F();if(!i)return;const l=new Map;for(const d of e){const m=new Map(d.prs.map(r=>[JSON.stringify([r.wave,r.id]),r])),f=d.kind==="waves"?K:d.dir;for(const{slug:r,wave:c}of d.waves)for(const h of c.items){if(h.status==="dropped")continue;const k=new Set(h.linear||[]);d.tickets?.source==="linear"&&h.ticket&&k.add(h.ticket);const u=m.get(JSON.stringify([r,h.id])),p=u&&n[o(u,d.me)],S=["merged","done"].includes(h.status)||p?.state==="MERGED",E={merged:S,ready:p?.state==="OPEN"&&!p.isDraft,hasPR:!!h.pr&&(S||p?.state==="OPEN"),failed:!!(u&&!p)},$=JSON.stringify([f,r,h.id]);for(const D of k){l.has(D)||l.set(D,{items:new Map,scoped:!1,created:!1});const b=l.get(D),O=b.items.get($);b.items.set($,{...E,failed:E.failed||O?.failed}),b.scoped||=!!p,b.created||=h.ticket===D&&!!h.ticket_created}}}for(const[d,m]of l){const f=[...m.items.values()];if(!m.scoped||f.some(c=>c.failed))continue;const r=f.every(c=>c.merged)?"done":f.every(c=>c.merged||c.ready)?"review":f.some(c=>c.hasPR)?"started":null;if(r)try{const c=await i.advanceIssue(d,r,{owner:i.saved.viewer.id,created:m.created});c&&s.moved.push({id:d,state:c})}catch(c){s.failed.push(`linear ${d}: ${c.message}`)}}}y(de,"advanceLinear");async function Se({now:e=new Date,settings:n,manifest:o}){const s=W(H,{});s.sent||={};const i=Z(e,n,s.sent),l=[],d=[],m=[];if(!i.length)return{due:i,events:l,failed:d};const f=_(o);for(const r of f)try{l.push({route:r.route,id:await q(r,"check-github",{reason:"morning"})})}catch(c){d.push(`${r.route}: could not add the morning check-github event (${c.message})`),m.push(r.route)}if(l.length||!f.length){for(const r of i)s.sent[r]=ee(e);s.last={at:e.toISOString(),due:i,routes:l.map(r=>r.route),failed:m},U(H,s)}return{due:i,events:l,failed:d}}y(Se,"sendWakes");export{C as CHANGE_KINDS,V as carry,re as changesBetween,ke as checkGitHub,Se as sendWakes,oe as summarize};
package/hub/home.mjs CHANGED
@@ -1 +1 @@
1
- var c=Object.defineProperty;var u=(e,n)=>c(e,"name",{value:n,configurable:!0});import{siteIcon as g}from"./icons.mjs";const d=u(e=>Number.isInteger(e)&&e>0?e:0,"count");function k(e){const{items:n,working:r,summary:o,lanes:t,building:i}=e||{};if(!Array.isArray(n))return null;const s=[...new Set(n.map(a=>String(a?.kind||"").trim()).filter(Boolean))],l=t&&typeof t=="object"&&["review","progress","todo"].every(a=>t[a]===void 0||Number.isInteger(t[a])&&t[a]>=0);return{count:n.length,working:d(r),summary:typeof o=="string"&&o.trim()?o.trim():n.length?`${n.length} waiting${s.length?`: ${s.join(", ")}`:""}`:"",lanes:l?{review:d(t.review),progress:d(t.progress),todo:d(t.todo)}:null,building:i===!0}}u(k,"readNeeds");function f({state:e,working:n,count:r,building:o,listen:t}={}){return e==="down"?{tone:"down",word:"Down"}:t==="deaf"?{tone:"down",word:"Not listening"}:e==="starting"?{tone:"starting",word:"Starting"}:e==="stopped"?{tone:"starting",word:"Stopped"}:r?{tone:"needs",word:`${r} need you`}:n?{tone:"working",word:"Working"}:t==="asleep"?{tone:"asleep",word:"Asleep"}:o?{tone:"building",word:"Building"}:null}u(f,"siteStatus");function w(e){return e?[["review","review"],["progress","doing"],["todo","todo"]].filter(([n])=>e[n]>0).map(([n,r])=>({lane:n,text:`${e[n]} ${r}`})):null}u(w,"laneChips");const p=u(e=>String(e??"").replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n]),"esc");function h(e,n,r){const o=f({state:e,...n,listen:r}),t=e==="up"?w(n?.lanes):null,i=o?`<span class="dot t-${o.tone}${o.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${o.tone}">${p(o.word)}</span>`:"",s=t===null?"":t.length?t.map(l=>`<span class="lc lc-${l.lane}">${p(l.text)}</span>`).join(""):"<span>nothing open</span>";return i||s?`${i}${s?`<span class="lcs">${s}</span>`:""}`:""}u(h,"statusLine");function x(e,n,r,o=new Map){return e.map(t=>{const i=n.get(t.route)??null,s=r.has(t.route)?o.get(t.route)??"listening":void 0;return{route:t.route,title:t.title,board:r.has(t.route),state:t.state,count:i?.count||0,changedAt:t.changedAt||0,created:t.created||null,icon:g(t.route,t),sub:h(t.state,i,s)}})}u(x,"sidebarSites");function y(e){return e.filter(n=>n.board).sort((n,r)=>r.count-n.count||r.changedAt-n.changedAt||n.route.localeCompare(r.route))[0]?.route??null}u(y,"startRoute");const $="runs older code than its files; restart it";function A(e,n=new Map,r=null){const o=e.filter(s=>s.state==="down"||(n.get(s.route)||[]).some(l=>l.state==="broken")),t=e.filter(s=>s.state==="up").length,i=r?.ok===!1;return{tone:o.length?"down":i?"wait":"ok",word:o.length?`${o.length} broken`:i?"Needs a restart":"Healthy",detail:`${t} of ${e.length} site${e.length===1?"":"s"} up${i?` \xB7 ${$}`:""}`,broken:o.map(s=>s.route)}}u(A,"hubHealth");export{$ as STALE_CODE,A as hubHealth,w as laneChips,k as readNeeds,x as sidebarSites,f as siteStatus,y as startRoute,h as statusLine};
1
+ var g=Object.defineProperty;var l=(t,e)=>g(t,"name",{value:e,configurable:!0});import{siteIcon as w}from"./icons.mjs";const d=l(t=>Number.isInteger(t)&&t>0?t:0,"count");function b(t){const{items:e,working:i,summary:r,lanes:n,building:o}=t||{};if(!Array.isArray(e))return null;const s=[...new Set(e.map(a=>String(a?.kind||"").trim()).filter(Boolean))],u=n&&typeof n=="object"&&["review","progress","todo"].every(a=>n[a]===void 0||Number.isInteger(n[a])&&n[a]>=0);return{count:e.length,working:d(i),summary:typeof r=="string"&&r.trim()?r.trim():e.length?`${e.length} waiting${s.length?`: ${s.join(", ")}`:""}`:"",lanes:u?{review:d(n.review),progress:d(n.progress),todo:d(n.todo)}:null,building:o===!0}}l(b,"readNeeds");function f({state:t,working:e,count:i,building:r,listen:n}={}){return t==="down"?{tone:"down",word:"Down"}:n==="deaf"?{tone:"down",word:"not listening"}:t==="starting"?{tone:"starting",word:"Starting"}:t==="stopped"?{tone:"stopped",word:"Stopped"}:i?{tone:"needs",word:`${i} need you`}:e?{tone:"working",word:"Working"}:n==="waking"?{tone:"asleep",word:"waking\u2026"}:n==="asleep"?{tone:"asleep",word:"asleep"}:r?{tone:"building",word:"Implementing"}:null}l(f,"siteStatus");function $(t){return t?[["review","review"],["progress","doing"],["todo","todo"]].filter(([e])=>t[e]>0).map(([e,i])=>({lane:e,text:`${t[e]} ${i}`})):null}l($,"laneChips");const p=l(t=>String(t??"").replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");function h(t,e,i){const r=f({state:t,...e,listen:i}),n=t==="up"?$(e?.lanes):null,o=r?`<span class="dot t-${r.tone}${r.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${r.tone}">${p(r.word)}</span>`:"",s=n===null?"":n.length?n.map(u=>`<span class="lc lc-${u.lane}">${p(u.text)}</span>`).join(""):"<span>nothing open</span>";return o||s?`${o}${s?`<span class="lcs">${s}</span>`:""}`:""}l(h,"statusLine");function y(t,e,i,r=new Map,n=new Map){return t.map(o=>{const s=e.get(o.route)??null,u=i.has(o.route)?r.get(o.route)??"listening":void 0;return{route:o.route,title:o.title,board:i.has(o.route),state:o.state,count:s?.count||0,changedAt:o.changedAt||0,created:o.created||null,icon:w(o.route,o),sub:h(o.state,s,u),ctx:n.get(o.route)??null}})}l(y,"sidebarSites");function S(t){return t.filter(e=>e.board).sort((e,i)=>i.count-e.count||i.changedAt-e.changedAt||e.route.localeCompare(i.route))[0]?.route??null}l(S,"startRoute");const m="runs older code than its files; restart it";function A(t,e=new Map,i=null){const r=t.filter(u=>u.state==="down").length,n=t.reduce((u,a)=>u+(e.get(a.route)||[]).filter(c=>c.state==="broken").length,0),o=t.filter(u=>u.state==="up").length,s=i?.ok===!1;return{tone:r?"down":n||s?"wait":"ok",word:r?`${r} site${r===1?"":"s"} down`:n||s?"Degraded":"Healthy",detail:`${o} of ${t.length} site${t.length===1?"":"s"} up${n?` \xB7 ${n} link${n===1?"":"s"} failing`:""}${s?` \xB7 ${m}`:""}`}}l(A,"hubHealth");export{m as STALE_CODE,A as hubHealth,$ as laneChips,b as readNeeds,y as sidebarSites,f as siteStatus,S as startRoute,h as statusLine};
package/hub/hub.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
- var ut=Object.defineProperty;var u=(e,t)=>ut(e,"name",{value:t,configurable:!0});import Se from"node:http";import $e from"node:net";import{spawn as lt,execFileSync as dt}from"node:child_process";import b from"node:fs";import S from"node:path";import xe from"node:os";import{fileURLToPath as ht}from"node:url";import ve from"node:vm";import ft from"node:zlib";import{routeRecord as T}from"./routes.mjs";import{publicKey as pt,addSubscription as mt,sendAll as gt,flushQueue as _e,queueStatus as ee,saveSettings as wt,readSettings as D,migrateSettings as bt,setNotifications as yt,readCodexShare as ke,setCodexShare as St,deliver as $t,pushRoute as xt}from"./push.mjs";import{POLL_MS as Te,answerOf as vt,expired as _t,pendingFor as je,promptRecord as kt,pushOf as Tt,updatePrompts as H,shown as jt,pruneRemembered as te,rememberRules as Ot,rememberedAnswer as Pt,rememberedFor as Oe}from"./prompts.mjs";import{nextSlot as Rt,windowAt as Et,hold as Dt,heldUntil as Ht,holdLine as Lt,editPolling as Ct}from"./schedule.mjs";import{capital as It,hubView as At,settingsView as Mt,pollingView as Bt,flushView as Ft,SNOOZE_REFUSED as Nt}from"./views.mjs";import{checkGitHub as Ut,sendWakes as Gt}from"./github.mjs";import{listBoards as O,isBoardSite as zt,listenState as Jt,RESUME_STATE as Wt,sendToBoards as Pe,checkListening as Kt,recordHubCode as Vt,runningHubCode as Zt,migrateHubFlag as Xt,recordLinkFaults as Qt,linksOf as Yt,hubLink as qt,hubCodeState as en,readJSON as tn}from"./boards.mjs";import{liveSessions as Re}from"./live.mjs";import{RuleError as nn,applyRules as sn,changed as Ee,readHubRules as ne,writeHubRules as an}from"./rules.mjs";import{siteIcon as on}from"./icons.mjs";import{readNeeds as rn,sidebarSites as cn,startRoute as un,hubHealth as ln}from"./home.mjs";import{checkSessions as dn,startingBoards as hn}from"./sessions.mjs";import{repoService as fn,repoLookupRoute as pn}from"./repos.mjs";import{boardRepo as mn,changeBoardRepo as gn,migrateBoardRepos as wn}from"./board-repo.mjs";import{requestBoard as bn,dismissBoard as yn}from"./new-board.mjs";import{boardClosure as Sn,closeBoard as $n,closedBoards as xn,reopenBoard as vn}from"./close-board.mjs";import{crossSite as _n,isJSON as De}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as se,appFile as kn,colorsVersion as He,destOf as Tn,fileVersion as jn,pageOf as Le,shellHtml as On}from"./shell.mjs";import{hubStream as Pn}from"./stream.mjs";import{usageReader as Rn,checkUsagePause as En}from"./usage.mjs";import{readUsageGate as Ce,setPauseAt as Dn,setKeepGoing as Hn}from"../shared/usage-gate.mjs";import{codexStatusReader as Ln}from"../shared/codex.mjs";import{BuildError as Cn,startBuild as In,stopTries as An,tryTick as Mn,servingTry as Ie,initializeTries as Bn,fetchTryBranches as Fn}from"./try.mjs";import{migrateOnStart as Nn}from"../bin/install.mjs";import{serveFiles as Un}from"./files.mjs";import{LinearError as ae,linearStatus as oe,removeLinearKey as Gn,saveLinearKey as zn,savedClient as Ae}from"./linear.mjs";const U=S.dirname(ht(import.meta.url)),Jn=S.join(U,"..","shared","colors.css"),h=u((...e)=>console.log(new Date().toISOString(),...e),"log"),Me=Nn();for(const e of Me.steps)h(`migration: ${e}`);for(const e of Me.conflicts)h(`migration failed: ${e}`);const G=S.join(xe.homedir(),".claude","kickflip"),Be=S.join(G,"manifest.json"),re=S.join(G,"logs"),Fe=S.join(G,"run"),ie=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[G,re,Fe])b.mkdirSync(e,{recursive:!0});try{const e=Vt();h(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){h("could not record the hub code hash:",e.message)}try{const e=await Xt();e&&h(`manifest: flagged /${e} as the hub board`)}catch(e){h("hub board migration failed:",e.message)}try{const e=await wn();e.length&&h(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){h("board repo migration failed:",e.message)}const L=fn(),Wn=Rn(),z=Ln();let J={version:1,sites:{}};function w(){try{J=JSON.parse(b.readFileSync(Be,"utf8")),J.sites||={}}catch(e){e.code==="ENOENT"?J={version:1,sites:{}}:h("manifest unreadable, keeping the last good copy:",e.message)}return J}u(w,"loadManifest");const x=new Map,R=new Set;let W=!1;const Ne=5e3,ce=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),ue=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),C=u(e=>ue(e)?"cmd":e?.port?"port":"files","servesOf"),I=new Map;function K(e){return S.join(Fe,e+".pid")}u(K,"pidFile");function Kn(e,t){try{const n=Number(b.readFileSync(K(e),"utf8")),o=n?dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";o&&o.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),h(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(K(e))}catch{}}u(Kn,"killStale");function Vn(e,t){const n=x.get(e)||{restarts:0},o=b.openSync(S.join(re,e+".log"),"a"),[s,...l]=t.cmd,c={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(o,`
2
+ var $t=Object.defineProperty;var c=(e,t)=>$t(e,"name",{value:t,configurable:!0});import Te from"node:http";import Oe from"node:net";import{spawn as je,execFileSync as _t}from"node:child_process";import g from"node:fs";import k from"node:path";import Pe from"node:os";import{fileURLToPath as xt}from"node:url";import Re from"node:vm";import vt from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as Tt,addSubscription as Ot,removeSubscription as jt,sendAll as Pt,flushQueue as Ee,queueStatus as le,saveSettings as Rt,readSettings as A,migrateSettings as Et,setNotifications as Dt,readCodexShare as De,setCodexShare as Lt,readClaudeModel as Ct,setClaudeModel as Ht,deliver as At,pushRoute as It}from"./push.mjs";import{POLL_MS as Le,answerOf as Mt,expired as Bt,pendingFor as Ce,promptRecord as Ft,pushOf as Ut,updatePrompts as I,shown as Nt,pruneRemembered as de,rememberRules as Gt,rememberedAnswer as zt,rememberedFor as He}from"./prompts.mjs";import{nextSlot as Kt,windowAt as Wt,hold as Jt,heldUntil as Vt,holdLine as Zt,editPolling as Xt}from"./schedule.mjs";import{capital as Qt,hubView as Yt,settingsView as qt,pollingView as en,flushView as tn,SNOOZE_REFUSED as nn,AWAKE_REFUSED as an}from"./views.mjs";import{caffeinate as sn,setKeepAwake as rn,awakeUntil as Ae,awakeUnsupported as on}from"./awake.mjs";import{checkGitHub as cn,sendWakes as un}from"./github.mjs";import{listBoards as R,isBoardSite as ln,listenState as Ie,RESUME_STATE as dn,sendToBoards as Me,checkListening as hn,recordHubCode as fn,runningHubCode as pn,recordLinkFaults as mn,linksOf as gn,hubLink as wn,hubCodeState as bn,readJSON as yn}from"./boards.mjs";import{liveSessions as Be}from"./live.mjs";import{RuleError as Sn,applyRules as kn,changed as Fe,readHubRules as he,writeHubRules as $n}from"./rules.mjs";import{siteIcon as _n}from"./icons.mjs";import{readNeeds as xn,sidebarSites as vn,startRoute as Tn,hubHealth as On}from"./home.mjs";import{checkSessions as jn,startingBoards as Pn}from"./sessions.mjs";import{repoService as Rn,repoLookupRoute as En}from"./repos.mjs";import{boardRepo as Dn,changeBoardRepo as Ln,migrateBoardRepos as Cn,migrateHubBoard as Hn}from"./board-repo.mjs";import{requestBoard as An,retryBoard as In,dismissBoard as Mn}from"./new-board.mjs";import{boardClosure as Bn,closeBoard as Fn,closedBoards as Un,reopenBoard as Nn}from"./close-board.mjs";import{restartBoard as Gn,restartable as zn}from"./restart-board.mjs";import{crossSite as Kn,isJSON as fe}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as pe,appFile as Wn,colorsVersion as Ue,destOf as Jn,fileVersion as Vn,pageOf as Ne,shellHtml as Zn}from"./shell.mjs";import{hubStream as Xn}from"./stream.mjs";import{usageReader as Qn,checkUsagePause as Yn,contextReading as qn}from"./usage.mjs";import{readUsageGate as Ge,setPauseAt as ea,setKeepGoing as ta}from"../shared/usage-gate.mjs";import{readiness as na,readCodex as W}from"./readiness.mjs";import{BuildError as aa,startBuild as sa,stopTries as ra,tryTick as ze,initializeTries as oa,fetchTryBranches as Ke}from"./try.mjs";import{serveFiles as ia}from"./files.mjs";import{upgrader as ca,CHECK_MS as ua}from"./upgrade.mjs";import{openBoardBuilders as la}from"../board/store.mjs";import{cloneRoot as We}from"../shared/repos.mjs";import{LinearError as me,linearStatus as J,removeLinearKey as da,saveLinearKey as ha,savedClient as Je}from"./linear.mjs";const V=k.dirname(xt(import.meta.url)),fa=k.join(V,"..","shared","colors.css"),u=c((...e)=>console.log(new Date().toISOString(),...e),"log"),Z=k.join(Pe.homedir(),".claude","kickflip"),Ve=k.join(Z,"manifest.json"),X=k.join(Z,"logs"),Ze=k.join(Z,"run"),ge=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Z,X,Ze])g.mkdirSync(e,{recursive:!0});try{const e=fn();u(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){u("could not record the hub code hash:",e.message)}try{const e=await Cn();e.length&&u(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){u("board repo migration failed:",e.message)}try{const e=await Hn();e&&u(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){u("hub board migration failed:",e.message)}const L=Rn(),pa=Qn(),Q=sn({log:u});Q.apply();let Y={version:1,sites:{}};function w(){try{Y=JSON.parse(g.readFileSync(Ve,"utf8")),Y.sites||={}}catch(e){e.code==="ENOENT"?Y={version:1,sites:{}}:u("manifest unreadable, keeping the last good copy:",e.message)}return Y}c(w,"loadManifest");const T=new Map,C=new Set;let q=!1;const Xe=5e3,ee=c(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),we=c(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),M=c(e=>we(e)?"cmd":e?.port?"port":"files","servesOf"),B=new Map;function te(e){return k.join(Ze,e+".pid")}c(te,"pidFile");function ma(e,t){try{const n=Number(g.readFileSync(te(e),"utf8")),r=n?_t("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),u(`stopped stale ${e} (pgid ${n})`))}catch{}try{g.unlinkSync(te(e))}catch{}}c(ma,"killStale");function ga(e,t){const n=T.get(e)||{restarts:0},r=g.openSync(k.join(X,e+".log"),"a"),[a,...l]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};g.writeSync(r,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `);let f;try{f=lt(s,l,{cwd:t.cwd||t.dir,env:c,stdio:["ignore",o,o],detached:!0})}catch(y){b.writeSync(o,`hub could not start it: ${y.message}
5
- `),b.closeSync(o),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+Ue(n.restarts++)}),x.set(e,n);return}b.closeSync(o),b.writeFileSync(K(e),String(f.pid)),Object.assign(n,{child:f,key:ce(t),state:"starting",since:Date.now(),exitCode:null}),x.set(e,n),h(`started ${e} pid ${f.pid}`),f.pid&&R.add(f);const g=u((y,m)=>{if(R.delete(f),n.child!==f)return;try{b.unlinkSync(K(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:y??m,nextStart:Date.now()+Ue(n.restarts++)}),n.stopping=!1,h(`${e} exited (${y??m})`)},"ended");f.on("error",y=>{h(`${e} spawn error: ${y.message}`),f.pid||g(y.code,null)}),f.on("exit",g)}u(Vn,"start");const Ue=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=x.get(e);if(!t||!t.child){x.delete(e);return}t.stopping=!0;const n=t.child;V(n,"SIGTERM"),setTimeout(()=>{R.has(n)&&V(n,"SIGKILL")},Ne)}u(le,"stop");const V=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ge(e){return new Promise(t=>{const n=$e.connect({port:e,host:"127.0.0.1"}),o=u(s=>{n.destroy(),t(s)},"done");n.setTimeout(800,()=>o(!1)),n.on("connect",()=>o(!0)),n.on("error",()=>o(!1))})}u(Ge,"probe");let ze=!0;const A=new Set;async function Je(){if(W)return;const{sites:e}=w();for(const t of[...x.keys()])A.has(t)||(T(e,t)?ue(T(e,t))||(h(`${t} no longer has a command; stopping it`),le(t),x.delete(t)):(h(`${t} left the manifest; stopping it`),le(t),x.delete(t)));for(const t of[...I.keys()])C(T(e,t))!=="port"&&I.delete(t);for(const[t,n]of Object.entries(e))C(n)==="port"&&I.set(t,await Ge(n.port));for(const[t,n]of Object.entries(e)){if(!ue(n))continue;ze&&Kn(t,n);const o=x.get(t);if(o&&o.child&&o.key!==ce(n)){h(`${t} entry changed; restarting it`),le(t),o.restarts=0,o.nextStart=Date.now()+1500;continue}(!o||!o.child&&Date.now()>=(o.nextStart||0))&&Vn(t,n)}ze=!1;for(const[t,n]of x){const o=T(e,t);o&&(n.up=n.child?await Ge(o.port):!1,n.child&&n.up&&(n.state="up"))}}u(Je,"reconcile");const Zn=setInterval(()=>Je().catch(e=>h("reconcile failed:",e.message)),2e3);Je().catch(e=>h("reconcile failed:",e.message));try{bt()&&h("settings: migrated the push schedule to the polling interval")}catch(e){h("settings migration failed:",e.message)}const Xn=6e4,Qn=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Xn,last:null,errorLoggedAt:0},We=u(()=>new Date($.startedAt?Rt(new Date($.startedAt),D()).getTime():$.firstAt),"nextPollAt");function Ke({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=D();let o;try{o=await Ut({every:Et(t,n).every_minutes,manifest:w()})}catch(s){o={at:t.toISOString(),watched:0,failed:[s.message],changes:[],events:[],checked:[]}}await Fn({manifest:w(),log:h}),o.failed.length&&Date.now()-$.errorLoggedAt>Qn&&($.errorLoggedAt=Date.now(),h(`github check: ${o.failed.length} step${o.failed.length===1?"":"s"} failed, first: ${o.failed[0]} (logged at most hourly)`)),o.changes.length&&h(`github check: ${o.changes.map(s=>`#${s.pr} ${s.changes.join(",")}`).join("; ")} \u2192 ${o.events.map(s=>s.route).join(", ")||"no board"}`);try{o.deaf=await Kt({manifest:w()})}catch(s){o.deaf={pushed:[],failed:[s.message]}}(o.deaf.pushed.length||o.deaf.failed.length)&&h(`not listening: pushed ${o.deaf.pushed.join(", ")||"none"}${o.deaf.failed.length?`; ${o.deaf.failed.join("; ")}`:""}`);try{Qt(w())}catch(s){h("could not record link faults:",s.message)}if(o.flushed=null,o.held=Ht(t,n)!==null,e&&!o.held)try{const s=await _e();s&&(o.flushed={count:s.count,sent:s.sent,ok:s.ok},h(`push queue: sent ${s.sent} of ${s.count} queued`))}catch(s){h("push queue flush failed:",s.message)}return $.last=o,o})().finally(()=>{$.running=null}),$.running}u(Ke,"runPoll");let Ve=null;function Yn(){Ve||=Gt({settings:D(),manifest:w()}).then(e=>{e.due.length&&h(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("wake failed:",e.message)).finally(()=>{Ve=null})}u(Yn,"runWakes");const Z={running:null};function qn(){Z.running||A.size||(Z.running=dn({manifest:w()}).then(e=>{(e.resumed.length||e.pushed.length||e.failed.length)&&h(`sessions: resumed ${e.resumed.join(", ")||"none"}; pushed ${e.pushed.join(", ")||"none"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("session check failed:",e.message)).finally(()=>{Z.running=null}))}u(qn,"runSessionCheck"),Bn(w());const es=Date.now(),M=new Map;function B(e){e?(X.delete(e),v.changed(e)):v.refresh()}u(B,"promptChanged");async function Ze(){const{gone:e,rulesChanged:t}=await H(n=>{const o=_t(n,{now:Date.now(),aliveSince:es}),s=te(n,de());for(const l of o)delete n.prompts[l.id];return{gone:o,rulesChanged:s}});for(const n of e)B(n.route);t&&v.refresh(),e.length&&h(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(Ze,"prunePrompts");const de=u(()=>({running:Re(),boards:O(w())}),"promptContext");let he=!1;async function Xe(){if(!he){he=!0;try{await En({gate:Ce(),resume:u(async e=>{const t=await Pe(O(w()),"usage-resumed",{window:e});for(const n of t.failed)h(n)},"resume")})}finally{he=!1}}}u(Xe,"checkUsageGate"),setInterval(()=>{Mn({manifest:w()}).catch(e=>h("try tick failed:",e.message)),Xe().catch(e=>h("usage gate failed:",e.message)),!$.running&&Date.now()>=We().getTime()&&Ke().catch(e=>h("poll failed:",e.message)),Yn(),Ze().catch(e=>h("prompt expiry failed:",e.message)).finally(qn)},15e3);function Qe(){if(W)return;W=!0,clearInterval(Zn),h("hub stopping; stopping sites");for(const n of x.values())n.child&&(n.stopping=!0);for(const n of R)V(n,"SIGTERM");let e=!1;An().finally(()=>{e=!0});const t=Date.now()+Ne;setInterval(()=>{if(!((!e||R.size)&&Date.now()<t)){for(const n of R)h(`pid ${n.pid} ignored SIGTERM; killing it`),V(n,"SIGKILL");process.exit(0)}},50)}u(Qe,"shutdown"),process.on("SIGTERM",Qe),process.on("SIGINT",Qe);const Ye=new Map;function qe(e,t={n:3e3}){let n=0,o;try{o=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const s of o){if(t.n--<=0)break;if(s.name.startsWith(".")||s.name==="node_modules")continue;const l=S.join(e,s.name);if(s.isDirectory())n=Math.max(n,qe(l,t));else try{n=Math.max(n,b.statSync(l).mtimeMs)}catch{}}return n}u(qe,"newestMtime");function ts(e,t){const n=Ye.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const o=qe(t.dir);return Ye.set(e,{at:o,checked:Date.now()}),o}u(ts,"changedAt");const et=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const o=x.get(t)||{},s=C(n),l=s==="files"?et(n.dir)?"up":"down":s==="port"?I.has(t)?I.get(t)?"up":"down":"starting":o.child?o.up?"up":"starting":o.state==="crashed"?"down":o.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:s,state:l,exitCode:o.exitCode??null,owner:n.owner||null,changedAt:ts(t,n)||null,created:n.created||null,log:S.join(re,t+".log")}})}u(fe,"status");const j=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),X=new Map,tt=2e4,ns=1500;async function ss(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ns)});return t.ok?rn(await t.json()):null}catch{return null}}u(ss,"fetchNeeds");async function nt(e,{fresh:t=!1}={}){const n=e.filter(s=>s.state==="up"&&s.owner&&s.port);for(const s of n){const l=X.get(s.route);(t===!0||t?.has?.(s.route)||!l||Date.now()-l.at>=tt)&&X.set(s.route,{at:Date.now(),needs:ss(s.port)})}const o=await Promise.all(n.map(s=>X.get(s.route).needs));return new Map(n.map((s,l)=>[s.route,o[l]]))}u(nt,"waitingCounts");const st=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+je(null).length,"badgeTotal"),F={at:0,value:null};function as(e){if(F.value&&Date.now()-F.at<tt)return F.value;const t=new Map,n=new Map,o={running:Re(),wakes:tn(Wt,{})};for(const s of e)try{const l=Jt(s,o);n.set(s.route,l),t.set(s.route,Yt(s,{listen:l}))}catch(l){h(`links of ${s.route} failed:`,l.message),t.set(s.route,[])}return Object.assign(F,{at:Date.now(),value:{links:t,listens:n}}),F.value}u(as,"boardLinks");async function Q({fresh:e=!1}={}){const t=new Date,n=fe(),o=await nt(n,{fresh:e}),s=w(),l=O(s),{links:c,listens:f}=as(l),g=en(),y=cn(n,o,new Set(l.map(r=>r.route)),f),m=D(),p=Dt(t,m);return{data:{code:g.running??null,total:st(o),start:un(y),sites:y,hub:ln(y,c,qt(g)),home:b.realpathSync(xe.homedir()),starting:hn(s,t.getTime()),usage:Wn({codexShare:ke(z())}),pause:Ce(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:It(Lt(p,t))}:{state:"on",text:"On"}},links:c,rows:n,code:g}}u(Q,"sidebarState");function at(e,t,n,o,s){let l=Buffer.isBuffer(s)?s:Buffer.from(s);const c={...o,vary:[o.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=ft.gzipSync(l),c["content-encoding"]="gzip"),c["content-length"]=l.length,t.writeHead(n,c),t.end(e.method==="HEAD"?void 0:l)}u(at,"sendText");async function os(e){const{data:t}=await Q(),n=ve.createContext({});for(const o of["ui-page-header.js","ui-sidebar.js"])ve.runInContext(b.readFileSync(S.join(se,o),"utf8"),n);return On({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:Y(180)})}u(os,"shellPage");const v=Pn({targets:u(()=>Object.entries(w().sites).filter(([,e])=>zt(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),sidebar:u(async({fresh:e})=>(await Q({fresh:e})).data,"sidebar"),log:h});function Y(e){let t="0";try{t=Math.floor(b.statSync(S.join(U,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(Y,"iconUrl");const rs=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:Y(192),sizes:"192x192",type:"image/png"},{src:Y(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const o of e)if(n+=o,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function k(e,t){if(!De(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(k,"readJSONBody");const pe=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">
6
- <title>${j(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${Y(180)}">
7
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${He()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),is=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),ot=u(()=>({running:!!$.running,nextAt:We(),last:$.last}),"pollState");async function me(e){await Ze();const t=new Date,{data:n,links:o,rows:s,code:l}=await Q();return{...At({now:t,st:ee(t),poll:ot(),sidebar:n,rows:s,links:o,rules:ne(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:ke(z()),codex:z(),remembered:new Map(O(w()).map(c=>[c.route,Oe(c.route)]))}),prompts:je(null).map(jt),remembered:Oe(null)}}u(me,"hubData");const ge=u(async()=>{const e=new Date;return{...Mt({now:e,st:ee(e),rules:ne(),route:await xt(),linear:oe()}),closed:xn()}},"settingsData"),rt=u(e=>{const t=new Date;return Bt({now:t,st:ee(t),poll:ot(),select:e})},"pollingData"),P=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function cs(e,t,n){const o=u((a,r,i,d={})=>{t.writeHead(a,{"content-type":r,"cache-control":"no-store",...d}),t.end(i)},"send"),s=u((a,r)=>{const i=Zt();return o(a,"application/json",JSON.stringify(r),i?{"x-hub-code":i}:{})},"sendJSON"),l=is(e),c=n.pathname,f=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(c);if(f&&e.method==="GET")try{return s(200,await Sn(f[1]))}catch(a){return s(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(c)&&e.method==="POST"){let a;try{const r=await k(e);if(a=r.route,c==="/_hub/reopen-board"){const _=await vn(a);return v.refresh(),s(200,_)}if(A.has(a))return s(409,{error:"That board is already closing."});A.add(a);const i=u(()=>A.delete(a),"release");t.once("finish",i),t.once("close",i),await Z.running;const d=await $n(a,r.name);return v.refresh(),s(200,d)}catch(r){return s(r.status||400,{error:r.message})}}if(c==="/_hub/repos"&&e.method==="GET")try{return s(200,await L.list(w(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/branches"&&e.method==="GET")try{return s(200,await L.branches(n.searchParams.get("github")))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/repo"&&e.method==="GET")return pn(n,{repos:L,manifest:w(),sendJSON:s});const g=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(c);if(g&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?mn(g[1]):await gn(g[1],await k(e),{repos:L});return e.method==="POST"&&v.refresh(),s(200,a)}catch(a){return s(a.status||400,{error:a.message})}if(c==="/_hub/new-board"&&e.method==="POST"){if(!De(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await k(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{const{route:r,started:i}=await bn(a,{repos:L,changed:u(()=>v.refresh(),"changed")});return i.then(({error:d})=>{d&&h(`new board /${r} didn't start: ${d}`),v.refresh()}),v.refresh(),s(200,{route:r})}catch(r){if(r.status)return o(r.status,"text/plain; charset=utf-8",r.message);throw r}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(c);if(y&&e.method==="DELETE")try{const a=await yn(y[1]);return v.refresh(),s(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if(c==="/_hub/build"&&e.method==="POST"){if(W)return s(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],r=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(r)||e.headers.origin||Object.keys(e.headers).some(d=>d==="forwarded"||d.startsWith("x-forwarded-")))return s(403,{error:"Builds can only be started from the host over loopback."});let i;try{i=await k(e)}catch(d){return s(d.status||400,{error:"Send the board route and card key as JSON."})}try{return s(202,await In(i,{manifest:w(),log:h}))}catch(d){if(d instanceof Cn)return s(d.status,{error:d.message});throw d}}if(c==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(U,"sw.js")),{"service-worker-allowed":"/"});if(c==="/manifest.webmanifest")return o(200,"application/manifest+json",rs());const m=/^\/_hub\/icon-(180|192|512)\.png$/.exec(c);if(m)try{return o(200,"image/png",b.readFileSync(S.join(U,`icon-${m[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/colors.css"){const a=n.searchParams.get("v")===He()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(Jn),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(c==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(se,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(se,"hub.css")));if(c==="/_hub/sidebar.json")return s(200,(await Q({fresh:n.searchParams.get("fresh")==="1"})).data);if(c==="/_hub/stream"&&e.method==="GET"){v.handle(e,t);return}if(c.startsWith("/_hub/app/")){const a=kn(c.slice(10));if(!a){t.writeHead(404).end();return}const r=S.extname(a),i={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[r],d=n.searchParams.get("v")===jn(a)?"public, max-age=31536000, immutable":"no-cache";return r===".png"?o(200,i,b.readFileSync(a),{"cache-control":d}):at(e,t,200,{"content-type":i,"cache-control":d},b.readFileSync(a))}if(c==="/_hub/api/hub")return s(200,await me(e));if(c==="/_hub/api/settings")return s(200,await ge());if(c==="/_hub/api/polling")return s(200,rt(n.searchParams.get("w")));if(c==="/_hub/push/key")return s(200,{key:pt()});if(c==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return s(200,{count:mt(await k(e),`https://${a}/`,e.headers["user-agent"])})}if(c==="/_hub/push/test"&&e.method==="POST"){const a=await gt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return s(200,{sent:a.filter(r=>r.status>=200&&r.status<300).length,results:a})}if(c==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("set"),i=r==="on"?{on:a.get("value")==="on"}:r==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,d="Pass set=on or set=weekends with value=on, or snooze.";if(!i)return l?s(400,{error:d}):o(400,"text/plain; charset=utf-8",d);let _;try{_=await yt(i)}catch(N){if(N.status!==400)throw N;return l?s(400,{error:Nt}):P(t,"/_hub/?refused=snooze#notifications")}return _.dropped&&h(`notifications off: dropped ${_.dropped} queued push${_.dropped===1?"":"es"}`),l?s(200,await ge()):P(t,"/_hub/#notifications")}if(c==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{St(a.get("value"),z({fresh:!0}))}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(c)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{c.endsWith("/pause-at")?Dn(a.get("value")):Hn(a.get("value")),await Xe()}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(c==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?Gn():await zn(a.get("key"))}catch(r){if(!(r instanceof ae))throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return h(`linear: key ${a.has("remove")?"removed":"saved"}`),l?s(200,await ge()):P(t,"/_hub/#linear")}if(c==="/_hub/api/linear"&&e.method==="GET"){const a=Ae();if(!a)return s(200,{set:!1});try{const r=await a.teams();return s(200,{...oe(),teams:r.map(({key:i,name:d})=>({key:i,name:d})),projects:r.flatMap(i=>i.projects.map(d=>({id:d.id,name:d.name,team:i.key})))})}catch(r){if(!(r instanceof ae))throw r;return s(200,{...oe(),error:r.message})}}if(c==="/_hub/linear/projects"&&e.method==="POST"){const a=Ae();if(!a)return s(400,{error:"Add a Linear key on Hub first."});let r;try{r=await k(e,4*1024)}catch(d){return s(d.status||400,{error:"Send the team and project name as JSON."})}const i=typeof r.name=="string"?r.name.trim():"";if(!i||i.length>80||typeof r.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(r.team))return s(400,{error:"Name the project and its team."});try{return s(200,await a.createProject(r.team,i))}catch(d){if(!(d instanceof ae))throw d;return s(d.status,{error:d.message})}}if(c==="/_hub/settings/polling"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("op")||"",i=Ct(D(),{op:r,w:a.get("w")||"",value:(a.get("value")||"").trim()});try{if(i.error)throw Object.assign(new Error(i.error),{status:400});wt(i.input)}catch(d){if(d.status!==400)throw d;return l?s(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return l?s(200,rt(i.select)):P(t,`/_hub/?w=${encodeURIComponent(i.select)}#polling`)}if(c==="/_hub/settings/rules"&&e.method==="POST"){let a,r;try{a=await k(e,64*1024),r=sn(ne(),a.rules,a.base??[]),Ee(r.changes)&&an(r.rules)}catch(d){if(!(d instanceof nn)&&!(d instanceof SyntaxError))throw d;return s(400,{error:d.message})}if(!Ee(r.changes))return s(200,{rules:r.rules,changed:!1,events:[],failed:[]});const i=await Pe(O(w()),"rules",{scope:"hub",...r.changes});return h(`hub rules saved: rules \u2192 ${i.events.map(d=>d.route).join(", ")||"no board"}${i.failed.length?`; ${i.failed.join("; ")}`:""}`),s(200,{rules:r.rules,changed:!0,...i})}if(c==="/_hub/push/flush"&&e.method==="POST"){const a=await _e();return a&&h(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),l?s(200,Ft(a)):P(t,`/_hub/settings/polling?flushed=${a?a.ok?a.count:"failed":0}`)}if(c==="/_hub/poll"&&e.method==="POST"){const a=await k(e);return s(200,await Ke({flush:a.flush!==!1}))}if(c==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await k(e,8*1024*1024)}catch(d){if(d.status)throw d;return s(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return s(400,{error:"Send a PermissionRequest hook input."});const{answer:r,rec:i}=await H(d=>{const _=de(),N=_.boards.find(ye=>ye.owner?.session&&ye.owner.session===a.session_id)||null;te(d,_)&&v.refresh();const be=Pt(d,a);if(be)return{answer:be};const q=kt(a,{board:N,live:_.running.get(a.session_id)});return d.prompts[q.id]=q,{rec:q}});return r?s(200,{state:"answered",answer:r}):(B(i.route),h(`prompt ${i.id}: ${i.route?`/${i.route}`:i.cwd} ${i.tool}${i.action?` (${i.action})`:""}`),$t(Tt(i),{kind:"prompt",immediate:!0}).catch(d=>h(`prompt ${i.id}: push failed: ${d.message}`)),s(200,{id:i.id,deadline:i.deadline}))}const p=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(c);return p?us(e,t,{id:p[1],op:p[2]?.slice(1)||null,url:n,sendJSON:s}):!1}u(cs,"hubRoute");async function us(e,t,{id:n,op:o,url:s,sendJSON:l}){if(o==="wait"&&e.method==="GET"){const c=u(()=>H(p=>{const a=T(p.prompts,n);return a?.answer?delete p.prompts[n]:a&&(a.seen=new Date().toISOString()),a}),"seen"),f=u(p=>(B(p.route),l(200,{state:"answered",answer:p.answer})),"take"),g=await c();if(!g)return l(404,{state:"gone"});if(g.answer)return f(g);const y=Math.max(0,Math.min(Number(s.searchParams.get("ms"))||Te,Te));if(await new Promise(p=>{const a=M.get(n)||new Set;M.set(n,a);const r=u(()=>{clearTimeout(i),a.delete(r),!a.size&&M.get(n)===a&&M.delete(n),p()},"done"),i=setTimeout(r,y);a.add(r),t.on("close",r)}),t.writableEnded||t.destroyed)return;const m=await c();return m?m.answer?f(m):l(200,{state:"pending"}):l(404,{state:"gone"})}if(o==="answer"&&e.method==="POST"){const c=await k(e),f=await H(m=>{const p=T(m.prompts,n);if(!p)return{status:404,error:"That prompt has expired."};if(p.answer)return{status:409,error:"That prompt was already answered."};const{answer:a,error:r}=vt(p,c);if(r)return{status:400,error:r};p.answer=a;const i=de();return te(m,i),Ot(m,p,i),{rec:p,answer:a}});if(f.error)return l(f.status,{error:f.error});const{rec:g,answer:y}=f;h(`prompt ${n}: ${y.choice}`);for(const m of[...M.get(n)||[]])m();return B(g.route),l(200,{ok:!0})}if(o===null&&e.method==="DELETE"){const c=await H(f=>{const g=T(f.prompts,n);return delete f.prompts[n],g});return c&&(B(c.route),h(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}u(us,"promptRoute");const ls=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=x.get(e)||{},o=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",s=C(t),[l,c]=s==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:s==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${o}`],f=s==="cmd"?"The hub runs every site and restarts one that stops.":s==="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`${pe(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `);let i;try{i=je(a,l,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(b){g.writeSync(r,`hub could not start it: ${b.message}
5
+ `),g.closeSync(r),Object.assign(n,{child:null,key:ee(t),state:"crashed",exitCode:null,nextStart:Date.now()+Qe(n.restarts++)}),T.set(e,n);return}g.closeSync(r),g.writeFileSync(te(e),String(i.pid)),Object.assign(n,{child:i,key:ee(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),T.set(e,n),u(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const y=c((b,S)=>{if(C.delete(i),n.child!==i)return;try{g.unlinkSync(te(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:b??S,nextStart:Date.now()+Qe(n.restarts++)}),n.stopping=!1,u(`${e} exited (${b??S})`)},"ended");i.on("error",b=>{u(`${e} spawn error: ${b.message}`),i.pid||y(b.code,null)}),i.on("exit",y)}c(ga,"start");const Qe=c(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function be(e){const t=T.get(e);if(!t||!t.child){T.delete(e);return}t.stopping=!0;const n=t.child;ne(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&ne(n,"SIGKILL")},Xe)}c(be,"stop");const ne=c((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ye(e){return new Promise(t=>{const n=Oe.connect({port:e,host:"127.0.0.1"}),r=c(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}c(Ye,"probe");let qe=!0;const F=new Set,ae=new Set,wa=3e4;async function ba(e){const t=Date.now()+wa;for(;;){const n=j(w().sites,e),r=T.get(e);if(n&&r?.child&&r.up&&r.key===ee(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}c(ba,"serverAnswers");async function et(){if(q)return;const{sites:e}=w();for(const t of[...T.keys()])F.has(t)||(j(e,t)?we(j(e,t))||(u(`${t} no longer has a command; stopping it`),be(t),T.delete(t)):(u(`${t} left the manifest; stopping it`),be(t),T.delete(t)));for(const t of[...B.keys()])M(j(e,t))!=="port"&&B.delete(t);for(const[t,n]of Object.entries(e))M(n)==="port"&&B.set(t,await Ye(n.port));for(const[t,n]of Object.entries(e)){if(!we(n))continue;qe&&ma(t,n);const r=T.get(t);if(r&&r.child&&r.key!==ee(n)){u(`${t} entry changed; restarting it`),be(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&ga(t,n)}qe=!1;for(const[t,n]of T){const r=j(e,t);r&&(n.up=n.child?await Ye(r.port):!1,n.child&&n.up&&(n.state="up"))}}c(et,"reconcile");const ya=setInterval(()=>et().catch(e=>u("reconcile failed:",e.message)),2e3);et().catch(e=>u("reconcile failed:",e.message));try{Et()&&u("settings: migrated the push schedule to the polling interval")}catch(e){u("settings migration failed:",e.message)}const Sa=6e4,tt=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Sa,last:null,errorLoggedAt:0,linearErrorLoggedAt:0},nt=c(()=>new Date($.startedAt?Kt(new Date($.startedAt),A()).getTime():$.firstAt),"nextPollAt");function at({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=A();let r;try{r=await cn({every:Wt(t,n).every_minutes,manifest:w()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]}}}r.merged.length&&(await Ke({manifest:w(),log:u,routes:r.merged}),u(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),ze({manifest:w(),routes:r.merged}).catch(a=>u("merge refresh failed:",a.message))),await Ke({manifest:w(),log:u}),r.failed.length&&Date.now()-$.errorLoggedAt>tt&&($.errorLoggedAt=Date.now(),u(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&u(`github check: ${r.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${r.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:l}of r.linear.moved)u(`linear: ${a} \u2192 ${l}`);r.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>tt&&($.linearErrorLoggedAt=Date.now(),u(`linear check: ${r.linear.failed.length} step${r.linear.failed.length===1?"":"s"} failed, first: ${r.linear.failed[0]} (logged at most hourly)`));try{r.deaf=await hn({manifest:w()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&u(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{mn(w())}catch(a){u("could not record link faults:",a.message)}if(r.flushed=null,r.held=Vt(t,n)!==null,e&&!r.held)try{const a=await Ee();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},u(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){u("push queue flush failed:",a.message)}return $.last=r,r})().finally(()=>{$.running=null}),$.running}c(at,"runPoll");let st=null;function ka(){st||=un({settings:A(),manifest:w()}).then(e=>{e.due.length&&u(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>u("wake failed:",e.message)).finally(()=>{st=null})}c(ka,"runWakes");const ye=new Set,D={running:null};function rt(){if(D.running||F.size||ae.size)return;const e=new Set(ye);ye.clear(),D.running=jn({manifest:w(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&u(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>u("session check failed:",t.message)).finally(()=>{D.running=null})}c(rt,"runSessionCheck"),await oa(w());const $a=Date.now(),U=new Map;function N(e){e?(se.delete(e),_.changed(e)):_.refresh()}c(N,"promptChanged");async function ot(){const{gone:e,rulesChanged:t}=await I(n=>{const r=Bt(n,{now:Date.now(),aliveSince:$a}),a=de(n,Se());for(const l of r)delete n.prompts[l.id];return{gone:r,rulesChanged:a}});for(const n of e)N(n.route);t&&_.refresh(),e.length&&u(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}c(ot,"prunePrompts");const Se=c(()=>({running:Be(),boards:R(w())}),"promptContext");let G=null,it=null;async function ke(){return G?it??=G.catch(()=>{}).then(()=>(it=null,ke())):(G=(async()=>Yn({gate:Ge(),resume:c(async e=>{const t=await Me(R(w()),"usage-resumed",{window:e});for(const n of t.failed)u(n)},"resume")}))().finally(()=>{G=null}),G)}c(ke,"checkUsageGate"),setInterval(()=>{ze({manifest:w()}).catch(e=>u("try tick failed:",e.message)),z.tick().catch(e=>u("kickflip update tick failed:",e.message)),ke().catch(e=>u("usage gate failed:",e.message)),!$.running&&Date.now()>=nt().getTime()&&at().catch(e=>u("poll failed:",e.message)),ka(),Q.apply(),ot().catch(e=>u("prompt expiry failed:",e.message)).finally(rt)},15e3);function ct(){if(q)return;q=!0,clearInterval(ya),Q.stop(),u("hub stopping; stopping sites");for(const n of T.values())n.child&&(n.stopping=!0);for(const n of C)ne(n,"SIGTERM");let e=!1;ra().finally(()=>{e=!0});const t=Date.now()+Xe;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)u(`pid ${n.pid} ignored SIGTERM; killing it`),ne(n,"SIGKILL");process.exit(0)}},50)}c(ct,"shutdown"),process.on("SIGTERM",ct),process.on("SIGINT",ct);const ut=new Map;function lt(e,t={n:3e3}){let n=0,r;try{r=g.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of r){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const l=k.join(e,a.name);if(a.isDirectory())n=Math.max(n,lt(l,t));else try{n=Math.max(n,g.statSync(l).mtimeMs)}catch{}}return n}c(lt,"newestMtime");function _a(e,t){const n=ut.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=lt(t.dir);return ut.set(e,{at:r,checked:Date.now()}),r}c(_a,"changedAt");const dt=c(e=>{try{return typeof e=="string"&&g.statSync(e).isDirectory()}catch{return!1}},"isFolder");function $e(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=T.get(t)||{},a=M(n),l=a==="files"?dt(n.dir)?"up":"down":a==="port"?B.has(t)?B.get(t)?"up":"down":"starting":r.child?r.up?"up":"starting":r.state==="crashed"?"down":r.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:a,state:l,exitCode:r.exitCode??null,owner:n.owner||null,restart:zn(n),changedAt:_a(t,n)||null,created:n.created||null,log:k.join(X,t+".log")}})}c($e,"status");const H=c(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),se=new Map,ht=2e4,xa=1500;async function va(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(xa)});return t.ok?xn(await t.json()):null}catch{return null}}c(va,"fetchNeeds");async function ft(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const l=se.get(a.route);(t===!0||t?.has?.(a.route)||!l||Date.now()-l.at>=ht)&&se.set(a.route,{at:Date.now(),needs:va(a.port)})}const r=await Promise.all(n.map(a=>se.get(a.route).needs));return new Map(n.map((a,l)=>[a.route,r[l]]))}c(ft,"waitingCounts");const pt=c(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ce(null).length,"badgeTotal"),_e=new Map;function Ta(e){const t=new Map,n=new Map,r={running:Be(),wakes:yn(dn,{})};for(const a of e)try{const l=Ie(a,r);n.set(a.route,l);let h=_e.get(a.route);(!h||h.listen!==l||Date.now()-h.at>=ht)&&(h={at:Date.now(),listen:l,links:gn(a,{listen:l})},_e.set(a.route,h)),t.set(a.route,h.links)}catch(l){u(`links of ${a.route} failed:`,l.message),t.set(a.route,[])}return{links:t,listens:n}}c(Ta,"boardLinks");async function re({fresh:e=!1}={}){const t=new Date,n=$e(),r=await ft(n,{fresh:e}),a=w(),l=R(a),{links:h,listens:i}=Ta(l),y=bn(),b=new Map(l.map(s=>[s.route,qn(s.owner?.session)]).filter(([,s])=>s)),S=vn(n,r,new Set(l.map(s=>s.route)),i,b),m=A(),p=Jt(t,m);return{data:{code:y.running??null,total:pt(r),start:Tn(S),sites:S,hub:On(S,h,wn(y)),home:g.realpathSync(Pe.homedir()),starting:Pn(a,t.getTime()),usage:pa({codexShare:De(W())}),pause:Ge(),upgrade:z.state(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Qt(Zt(p,t))}:{state:"on",text:"On"}},links:h,rows:n,code:y}}c(re,"sidebarState");function mt(e,t,n,r,a){let l=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=vt.gzipSync(l),h["content-encoding"]="gzip"),h["content-length"]=l.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:l)}c(mt,"sendText");async function Oa(e){const{data:t}=await re(),n=Re.createContext({});for(const r of["ui-page-header.js","ui-sidebar.js"])Re.runInContext(g.readFileSync(k.join(pe,r),"utf8"),n);return Zn({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:oe(180)})}c(Oa,"shellPage");const z=ca({root:We(),builders:c(async()=>(await Promise.all(R(w()).map(e=>la(e.dir)))).flat(),"builders"),run:ja,log:u,changed:c(()=>_.refresh(),"changed")});function ja(){const e=k.join(X,"kickflip-update.log");g.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=g.openSync(e,"a");return u("running kickflip update"),new Promise(n=>{const r=je(process.execPath,[k.join(We(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});g.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const l=g.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);u(`kickflip update exited ${a}: ${l}`),n(a===0?{ok:!0}:{ok:!1,error:l.replace(/^kickflip: /,"")})})})}c(ja,"runUpdate"),z.check(),setInterval(()=>z.check(),ua);const _=Xn({targets:c(()=>Object.entries(w().sites).filter(([,e])=>ln(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:c(()=>R(w()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:c(async({fresh:e})=>(await re({fresh:e})).data,"sidebar"),log:u});function oe(e){let t="0";try{t=Math.floor(g.statSync(k.join(V,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}c(oe,"iconUrl");const Pa=c(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:oe(192),sizes:"192x192",type:"image/png"},{src:oe(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const r of e)if(n+=r,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}c(E,"readBody");async function O(e,t){if(!fe(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):{}}c(O,"readJSONBody");const gt=c((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>${H(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${oe(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ue()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ra=c(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),wt=c(()=>({running:!!$.running,nextAt:nt(),last:$.last}),"pollState");async function ie(e){await ot();const t=new Date,{data:n,links:r,rows:a,code:l}=await re();return{...Yt({now:t,st:le(t),poll:wt(),sidebar:n,rows:a,links:r,rules:he(),integrations:await na.rows(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:De(W()),claudeModel:Ct(),codex:W(),awake:Ae(t),remembered:new Map(R(w()).map(h=>[h.route,He(h.route)]))}),prompts:Ce(null).map(Nt),remembered:He(null)}}c(ie,"hubData");const ce=c(async()=>{const e=new Date,t={until:Ae(e),unsupported:on()};return{...qt({now:e,st:le(e),rules:he(),route:await It(),linear:J(),awake:t}),closed:Un()}},"settingsData"),bt=c(e=>{const t=new Date;return en({now:t,st:le(t),poll:wt(),select:e})},"pollingData"),P=c((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ea(e,t,n){const r=c((s,o,d,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=c((s,o)=>{const d=pn();return r(s,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),l=c(({route:s,started:o})=>(o.then(({error:d})=>{d&&u(`new board /${s} didn't start: ${d}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Ra(e),i=n.pathname,y=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(y&&e.method==="GET")try{return a(200,await Bn(y[1]))}catch(s){return a(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let s;try{const o=await O(e);if(s=o.route,i==="/_hub/reopen-board"){const v=await Nn(s);return _.refresh(),a(200,v)}if(F.has(s))return a(409,{error:"That board is already closing."});F.add(s);const d=c(()=>F.delete(s),"release");t.once("finish",d),t.once("close",d),await D.running;const f=await Fn(s,o.name);return _.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(i==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await O(e)).route,ae.has(s))return a(409,{error:"That board is already restarting."});ae.add(s),await D.running;const o=await Gn(s,{serverUp:ba});return u(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&u(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{ae.delete(s),_.refresh()}}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await O(e),a(200,{upgrade:await z.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await O(e),o=R(w()).find(d=>d.route===s);return o?(ye.add(s),await D.running,rt(),await D.running,_e.clear(),_.changed(s),a(200,{route:s,listen:Ie(o)})):a(404,{error:"That board is not open."})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(w(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/repo"&&e.method==="GET")return En(n,{repos:L,manifest:w(),sendJSON:a});const b=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(b&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Dn(b[1]):await Ln(b[1],await O(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,s)}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await O(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return l(await An(s,{repos:L,changed:c(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(S&&e.method==="POST"){if(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return l(await In(S[1],{repos:L,changed:c(()=>_.refresh(),"changed")}))}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const s=await Mn(m[1]);return _.refresh(),a(200,s)}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}if(i==="/_hub/build"&&e.method==="POST"){if(q)return a(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:"Builds can only be started from the host over loopback."});let d;try{d=await O(e)}catch(f){return a(f.status||400,{error:"Send the board route and card key as JSON."})}try{return a(202,await sa(d,{manifest:w(),log:u}))}catch(f){if(f instanceof aa)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",g.readFileSync(k.join(V,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Pa());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",g.readFileSync(k.join(V,`icon-${p[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/colors.css"){const s=n.searchParams.get("v")===Ue()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",g.readFileSync(fa),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",g.readFileSync(k.join(pe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return r(200,"text/css; charset=utf-8",g.readFileSync(k.join(pe,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await re({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const s=Wn(i.slice(10));if(!s){t.writeHead(404).end();return}const o=k.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===Vn(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,d,g.readFileSync(s),{"cache-control":f}):mt(e,t,200,{"content-type":d,"cache-control":f},g.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await ie(e));if(i==="/_hub/api/settings")return a(200,await ce());if(i==="/_hub/api/polling")return a(200,bt(n.searchParams.get("w")));if(i==="/_hub/push/key")return a(200,{key:Tt()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ot(await O(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:jt((await O(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Pt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:s.filter(o=>o.status>=200&&o.status<300).length,results:s})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("set"),d=o==="on"?{on:s.get("value")==="on"}:o==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let v;try{v=await Dt(d)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:nn}):P(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&u(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?a(200,await ce()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{rn(s.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):r(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:an}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return Q.apply(),h?a(200,await ce()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Lt(s.get("value"),W({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Ht(s.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):r(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?ea(s.get("value")):ta(s.get("value")),await ke()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(da(),u("linear: key removed")):u(`linear: key ${(await ha(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof me))throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ce()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=J();if(!s.set||s.refused)return a(200,s);const o=Je();try{const d=await o.teams();return a(200,{...J(),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 me))throw d;return a(200,{...J(),error:d.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=Je();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await O(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await s.createProject(o.team,d))}catch(f){if(!(f instanceof me))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/polling"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("op")||"",d=Xt(A(),{op:o,w:s.get("w")||"",value:(s.get("value")||"").trim()});try{if(d.error)throw Object.assign(new Error(d.error),{status:400});Rt(d.input)}catch(f){if(f.status!==400)throw f;return h?a(400,{error:f.message}):r(400,"text/plain; charset=utf-8",f.message)}return h?a(200,bt(d.select)):P(t,`/_hub/?w=${encodeURIComponent(d.select)}#polling`)}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await O(e,64*1024),o=kn(he(),s.rules,s.base??[]),Fe(o.changes)&&$n(o.rules)}catch(f){if(!(f instanceof Sn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Fe(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Me(R(w()),"rules",{scope:"hub",...o.changes});return u(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ee();return s&&u(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,tn(s)):P(t,`/_hub/settings/polling?flushed=${s?s.ok?s.count:"failed":0}`)}if(i==="/_hub/poll"&&e.method==="POST"){const s=await O(e);return a(200,await at({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await O(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await I(f=>{const v=Se(),K=v.boards.find(ve=>ve.owner?.session&&ve.owner.session===s.session_id)||null;de(f,v)&&_.refresh();const xe=zt(f,s);if(xe)return{answer:xe};const ue=Ft(s,{board:K,live:v.running.get(s.session_id)});return f.prompts[ue.id]=ue,{rec:ue}});return o?a(200,{state:"answered",answer:o}):(N(d.route),u(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),At(Ut(d),{kind:"prompt",immediate:!0}).catch(f=>u(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}const x=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return x?Da(e,t,{id:x[1],op:x[2]?.slice(1)||null,url:n,sendJSON:a}):!1}c(Ea,"hubRoute");async function Da(e,t,{id:n,op:r,url:a,sendJSON:l}){if(r==="wait"&&e.method==="GET"){const h=c(()=>I(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=c(m=>(N(m.route),l(200,{state:"answered",answer:m.answer})),"take"),y=await h();if(!y)return l(404,{state:"gone"});if(y.answer)return i(y);const b=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Le,Le));if(await new Promise(m=>{const p=U.get(n)||new Set;U.set(n,p);const x=c(()=>{clearTimeout(s),p.delete(x),!p.size&&U.get(n)===p&&U.delete(n),m()},"done"),s=setTimeout(x,b);p.add(x),t.on("close",x)}),t.writableEnded||t.destroyed)return;const S=await h();return S?S.answer?i(S):l(200,{state:"pending"}):l(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await O(e),i=await I(S=>{const m=j(S.prompts,n);if(!m)return{status:404,error:"That prompt has expired."};if(m.answer)return{status:409,error:"That prompt was already answered."};const{answer:p,error:x}=Mt(m,h);if(x)return{status:400,error:x};m.answer=p;const s=Se();return de(S,s),Gt(S,m,s),{rec:m,answer:p}});if(i.error)return l(i.status,{error:i.error});const{rec:y,answer:b}=i;u(`prompt ${n}: ${b.choice}`);for(const S of[...U.get(n)||[]])S();return N(y.route),l(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await I(i=>{const y=j(i.prompts,n);return delete i.prompts[n],y});return h&&(N(h.route),u(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}c(Da,"promptRoute");const La=c(({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 yt(e,t){const n=T.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=M(t),[l,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${r}`],i=a==="cmd"?"The hub runs every site and restarts one that stops.":a==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${gt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
8
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
9
- <h1>${j(t.title||e)} isn't answering</h1>
10
- <p class="lede">${j(f)}</p>
11
+ <h1>${H(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${H(i)}</p>
11
13
  <h2 class="label">Site</h2>
12
- <div class="group">${ls({icon:on(e,t),title:j(t.title||e),sub:`<span class="st ${l}">${j(c)}</span>`})}</div>
14
+ <div class="group">${La({icon:_n(e,t),title:H(t.title||e),sub:`<span class="st ${l}">${H(h)}</span>`})}</div>
13
15
  <p class="note">This page retries every 5 seconds.</p>
14
- </main></body></html>`}u(it,"downPage");const we=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),ct=Se.createServer((e,t)=>{const n=we(e);if(e.method!=="GET"&&e.method!=="HEAD"&&_n(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=/^\/([^/]+)\/try\/([^/]+)(\/|$)/.exec(n.pathname);let s=null;if(o&&O(w()).some(r=>r.route===o[1])){if(!o[3]){t.writeHead(302,{location:n.pathname+"/"+n.search}).end();return}if(s=Ie(o[1],o[2],{manifest:w(),visit:!0}),!s){t.writeHead(404,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(`${pe("Not being served")}<a class="back" href="/${j(o[1])}/">\u2039 Board</a><h1>${j(o[2].toUpperCase())} isn't being served. Tap Serve on its card.</h1></main></body></html>`);return}}const l=new Set(Object.keys(w().sites));if(!s&&Le({method:e.method,pathname:n.pathname,search:n.search,dest:Tn(e.headers),sites:l})==="shell"){const r=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);os(r).then(i=>at(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},i)).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ie,pid:process.pid},sites:fe()},null,2));return}if(n.pathname==="/_hub/needs.json"){const r=fe();nt(r,{fresh:n.searchParams.get("fresh")==="1"}).then(i=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:st(i),sites:Object.fromEntries(r.map(d=>[d.route,i.get(d.route)??null]))}))}).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){cs(e,t,n).then(r=>{r===!1&&t.writeHead(404).end()}).catch(r=>{t.headersSent||t.writeHead(r.status||500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const c=n.pathname.split("/")[1],f=T(w().sites,c);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${pe("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${j(c)}</h1></main></body></html>`);return}if(n.pathname==="/"+c){t.writeHead(302,{location:"/"+c+"/"+n.search}).end();return}const g=s?`/${c}/try/${o[2]}`:"/"+c,y=e.url.slice(g.length)||"/",m=Le({pathname:n.pathname,dest:"document",sites:l})==="shell";if(!s&&C(f)==="files"){if(!et(f.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f));return}Un(e,t,{dir:f.dir,rest:y,prefix:g,headers:m?{vary:"sec-fetch-dest"}:{}}).catch(r=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const p={...e.headers,"x-forwarded-prefix":g,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},a=Se.request({host:"127.0.0.1",port:s?.port||f.port,method:e.method,path:y,headers:p},r=>{const i={...r.headers};m&&(i.vary=[i.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof i.location=="string"&&i.location.startsWith("/")&&!i.location.startsWith("//")&&!i.location.startsWith(g+"/")&&i.location!==g&&(i.location=g+i.location),t.writeHead(r.statusCode,i),r.pipe(t),r.on("close",()=>{r.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||a.destroy()}),a.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f))}),e.pipe(a)});ct.on("upgrade",(e,t,n)=>{const o=we(e).pathname.split("/")[1],s=T(w().sites,o),l=/^\/[^/]+\/try\/([^/]+)\//.exec(we(e).pathname),c=l&&O(w()).some(p=>p.route===o),f=c?Ie(o,l[1],{manifest:w(),visit:!0}):s;if(!f?.port){t.destroy();return}const g=c?`/${o}/try/${l[1]}`:`/${o}`,y=e.url.slice(g.length)||"/",m=$e.connect(f.port,"127.0.0.1",()=>{const p=[`${e.method} ${y} HTTP/${e.httpVersion}`];for(let a=0;a<e.rawHeaders.length;a+=2)e.rawHeaders[a].toLowerCase()!=="x-forwarded-prefix"&&p.push(`${e.rawHeaders[a]}: ${e.rawHeaders[a+1]}`);p.push(`X-Forwarded-Prefix: ${g}`),m.write(p.join(`\r
16
+ </main></body></html>`}c(yt,"downPage");const St=c(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),kt=Te.createServer((e,t)=>{const n=St(e);if(e.method!=="GET"&&e.method!=="HEAD"&&Kn(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const r=new Set(Object.keys(w().sites));if(Ne({method:e.method,pathname:n.pathname,search:n.search,dest:Jn(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Oa(m).then(p=>mt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},p)).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ge,pid:process.pid},sites:$e()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=$e();ft(m,{fresh:n.searchParams.get("fresh")==="1"}).then(p=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:pt(p),sites:Object.fromEntries(m.map(x=>[x.route,p.get(x.route)??null]))}))}).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){Ea(e,t,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=n.pathname.split("/")[1],l=j(w().sites,a);if(!l){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${gt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${H(a)}</h1></main></body></html>`);return}if(n.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+n.search}).end();return}const h="/"+a,i=e.url.slice(h.length)||"/",y=Ne({pathname:n.pathname,dest:"document",sites:r})==="shell";if(M(l)==="files"){if(!dt(l.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(yt(a,l));return}ia(e,t,{dir:l.dir,rest:i,prefix:h,headers:y?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const b={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},S=Te.request({host:"127.0.0.1",port:l.port,method:e.method,path:i,headers:b},m=>{const p={...m.headers};y&&(p.vary=[p.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof p.location=="string"&&p.location.startsWith("/")&&!p.location.startsWith("//")&&!p.location.startsWith(h+"/")&&p.location!==h&&(p.location=h+p.location),t.writeHead(m.statusCode,p),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),S.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(yt(a,l))}),e.pipe(S)});kt.on("upgrade",(e,t,n)=>{const r=St(e).pathname.split("/")[1],a=j(w().sites,r);if(!a?.port){t.destroy();return}const l=`/${r}`,h=e.url.slice(l.length)||"/",i=Oe.connect(a.port,"127.0.0.1",()=>{const y=[`${e.method} ${h} HTTP/${e.httpVersion}`];for(let b=0;b<e.rawHeaders.length;b+=2)e.rawHeaders[b].toLowerCase()!=="x-forwarded-prefix"&&y.push(`${e.rawHeaders[b]}: ${e.rawHeaders[b+1]}`);y.push(`X-Forwarded-Prefix: ${l}`),i.write(y.join(`\r
15
17
  `)+`\r
16
18
  \r
17
- `),n&&n.length&&m.write(n),m.pipe(t),t.pipe(m)});m.on("error",()=>t.destroy()),t.on("error",()=>m.destroy()),t.on("close",()=>m.destroy()),m.on("close",()=>t.destroy())}),ct.listen(ie,"127.0.0.1",()=>h(`hub on http://127.0.0.1:${ie}/ (manifest ${Be})`));
19
+ `),n&&n.length&&i.write(n),i.pipe(t),t.pipe(i)});i.on("error",()=>t.destroy()),t.on("error",()=>i.destroy()),t.on("close",()=>i.destroy()),i.on("close",()=>t.destroy())}),kt.listen(ge,"127.0.0.1",()=>u(`hub on http://127.0.0.1:${ge}/ (manifest ${Ve})`));