@lovek4d/kickflip 0.7.0 → 0.7.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 (59) hide show
  1. package/CONTEXT.md +43 -43
  2. package/SKILL.md +12 -10
  3. package/bin/event-lines.mjs +1 -1
  4. package/bin/kickflip.mjs +2 -2
  5. package/board/agents.mjs +2 -2
  6. package/board/arch-review.mjs +4 -4
  7. package/board/bin/board.mjs +87 -87
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +8 -8
  11. package/board/hub-lines.mjs +1 -0
  12. package/board/onboarding.mjs +1 -1
  13. package/board/public/app-arch.js +1 -1
  14. package/board/public/app-cards.js +5 -6
  15. package/board/public/app-header.js +1 -1
  16. package/board/public/app-lanes.js +37 -59
  17. package/board/public/app-needs.js +16 -11
  18. package/board/public/app-reader.js +2 -2
  19. package/board/public/app.js +2 -2
  20. package/board/public/dialogs.js +1 -1
  21. package/board/public/hub-kit.js +1 -1
  22. package/board/public/hub.css +2 -0
  23. package/board/public/linear-key.js +1 -1
  24. package/board/public/new-task.js +1 -1
  25. package/board/public/prototype-kit.js +2 -5
  26. package/board/public/router.js +1 -1
  27. package/board/public/sidebar.css +2 -2
  28. package/board/public/sidebar.js +2 -2
  29. package/board/public/style.css +20 -29
  30. package/board/public/ui-cards.js +19 -17
  31. package/board/public/ui-hub.js +1 -5
  32. package/board/public/ui-links.js +5 -5
  33. package/board/public/ui-order.js +5 -6
  34. package/board/public/ui-questions.js +12 -10
  35. package/board/public/ui-threads.js +10 -7
  36. package/board/public/ui.js +11 -2
  37. package/board/public/view-hub-settings.js +28 -31
  38. package/board/public/view-hub.js +7 -9
  39. package/board/public/view-notes.js +1 -1
  40. package/board/public/view-questions.js +18 -5
  41. package/board/public/view-settings.js +3 -3
  42. package/board/render-check.mjs +10 -10
  43. package/board/review.mjs +1 -1
  44. package/board/server.mjs +18 -19
  45. package/board/stop.mjs +1 -1
  46. package/board/store.mjs +26 -25
  47. package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
  48. package/hub/awake.mjs +1 -1
  49. package/hub/boards.mjs +4 -4
  50. package/hub/github.mjs +3 -3
  51. package/hub/hub.mjs +11 -11
  52. package/hub/incoming.mjs +2 -2
  53. package/hub/locks.mjs +1 -1
  54. package/hub/new-board.mjs +2 -2
  55. package/hub/owner-checks.mjs +1 -1
  56. package/hub/try.mjs +6 -7
  57. package/hub/views.mjs +1 -1
  58. package/package.json +1 -1
  59. package/shared/usage-gate.mjs +1 -1
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // A PreToolUse hook on Edit|Write that refuses a hand edit of the files board.mjs owns on a board site: the site's
3
- // board.json, every .json and .md under its data/, the wave file(s) board.json's `items` names, and the .md notes beside
4
- // a ~/.claude/waves wave file. It denies, not asks, because a headless session has nobody to ask, and the reason names
3
+ // board.json, every .json and .md under its data/, the item file(s) board.json's `items` names, and the .md notes beside
4
+ // a ~/.claude/waves item file. It denies, not asks, because a headless session has nobody to ask, and the reason names
5
5
  // the board.mjs command that makes the change. Drafts (<site>/drafts/, or any folder outside data/) and Bash writes stay
6
6
  // open (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md,
7
7
  // docs/adr/2026-11-09-1100-the-board-owns-a-cards-notes-file.md).
@@ -15,22 +15,22 @@ process.stdout.on('error', () => {});
15
15
  const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
16
16
  const inside = (dir, file) => !path.relative(dir, file).startsWith('..') && !path.isAbsolute(path.relative(dir, file));
17
17
 
18
- // → the file's kind on this board ('wave', 'board', 'notes' or 'data'), or null when it is not board-owned
18
+ // → the file's kind on this board ('file', 'board', 'notes' or 'data'), or null when it is not board-owned
19
19
  function ownedKind(board, file) {
20
20
  const dir = real(board.dir);
21
21
  let items = 'waves';
22
22
  try { items = JSON.parse(fs.readFileSync(path.join(dir, 'board.json'), 'utf8')).items || 'waves'; } catch {}
23
23
  if (file === path.join(dir, 'board.json')) return 'board';
24
- if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return 'wave';
24
+ if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return 'file';
25
25
  if (file.endsWith('.md') && (inside(path.join(dir, 'data'), file) || (items === 'waves' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file)))) return 'notes';
26
26
  if (file.endsWith('.json') && inside(path.join(dir, 'data'), file)) return 'data';
27
27
  return null;
28
28
  }
29
29
 
30
30
  const HINTS = {
31
- wave: 'A card changes with `board.mjs --site <route> item add|set <wave>/<id> …`',
31
+ file: 'A card changes with `board.mjs --site <route> item add|set <file>/<id> …`',
32
32
  board: 'Board settings change with `board.mjs --site <route> rules|reviewers …` or the settings page',
33
- notes: 'A card\'s notes change with `board.mjs --site <route> item notes <wave>/<id> --heading "<heading>" --file <draft.md>`, the draft written outside data/',
33
+ notes: 'A card\'s notes change with `board.mjs --site <route> item notes <file>/<id> --heading "<heading>" --file <draft.md>`, the draft written outside data/',
34
34
  data: 'Board data changes with a board.mjs command (`item set`, `thread`, `topic add`, `question add --file <draft>`, `reply`, `builder …`); its usage text lists them',
35
35
  };
36
36
 
package/hub/awake.mjs CHANGED
@@ -1 +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
+ var l=Object.defineProperty;var n=(e,r)=>l(e,"name",{value:r,configurable:!0});import{spawn as w}from"node:child_process";import{readSettingsFile as f,writeSettingsFile as c}from"./push.mjs";const d={linux:"Linux",win32:"Windows",freebsd:"FreeBSD"},k=n((e=process.platform)=>e==="darwin"?null:`macOS only \xB7 this host runs ${d[e]||e}`,"awakeUnsupported"),_=n((e=f())=>e.keep_awake===!0,"awakeOn");function g(e,{platform:r=process.platform}={}){const t=k(r);if(t)throw Object.assign(new Error(`Keep awake runs caffeinate, ${t}.`),{status:409});const{keep_awake:s,keep_awake_until:o,...a}=f();c(e?{...a,keep_awake:!0}:a)}n(g,"setKeepAwake");function $({log:e=n(()=>{},"log"),platform:r=process.platform}={}){let t=null;const s=n(()=>{t&&(e(`keep awake: stopping caffeinate (pid ${t.pid})`),t.kill("SIGTERM"),t=null)},"end");function o(){const a=f();if("keep_awake_until"in a){const{keep_awake_until:p,...u}=a;return c(p==="forever"?{...u,keep_awake:!0}:u),o()}if(k(r)||!_(a)){s();return}if(t)return;const i=w("caffeinate",["-s","-w",String(process.pid)],{stdio:"ignore"});i.on("error",p=>{e(`keep awake: caffeinate failed: ${p.message}`),t===i&&(t=null)}),i.on("exit",()=>{t===i&&(t=null)}),t=i,e(`keep awake: caffeinate -s (pid ${i.pid}) until turned off`)}return n(o,"apply"),{apply:o,stop:s}}n($,"caffeinate");export{_ as awakeOn,k as awakeUnsupported,$ as caffeinate,g as setKeepAwake};
package/hub/boards.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var q=Object.defineProperty;var i=(e,t)=>q(e,"name",{value:t,configurable:!0});import f from"node:fs";import c from"node:path";import z from"node:os";import Z from"node:crypto";import{fileURLToPath as Q}from"node:url";import{routeRecord as d}from"./routes.mjs";import{validBuildTarget as X}from"../board/build-script.mjs";import{deliver as Y,readSettings as ee}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as te}from"./schedule.mjs";import{liveSessions as v,ps as ne}from"./live.mjs";import{withFileLock as re}from"./locks.mjs";import{dropSessionPrompts as se,readPrompts as oe,savePrompts as ie,withPromptsLock as ae}from"./prompts.mjs";import{storedPath as S}from"../bin/install.mjs";import{hubService as ce}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{prLinks as ue,reviewLinks as le,topicItems as pe}from"../board/review.mjs";import{cardPRs as fe}from"../shared/card-prs.mjs";const de=c.resolve(c.dirname(Q(import.meta.url)),".."),C=c.join(z.homedir(),".claude"),m=c.join(C,"kickflip"),F=c.join(C,"waves"),$=c.join(m,"manifest.json"),he=c.join(m,"github-state.json"),me=c.join(m,"wake-state.json"),ut=c.join(m,"incoming-files.json"),P=c.join(m,"deaf-state.json"),E=c.join(m,"resume-state.json"),x=c.join(m,"link-faults.json"),we=c.join(m,"repo-fetch.json"),M=c.join(m,"hub-code.json"),ge="1970-01-01T00:00:00Z",ke=2*6e4,je=15*6e4,p=i((e,t)=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;f.mkdirSync(c.dirname(e),{recursive:!0}),f.writeFileSync(n,JSON.stringify(t,null,2)+`
2
- `),f.renameSync(n,e)}i(k,"saveJSON");const j=i(()=>p($,{sites:{}}),"readManifest"),lt=i(e=>re($,e),"withManifestLock");function pt(e){return ae(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(o=>o.site)];for(const o of t.filter(Boolean))o.dir&&(o.dir=S(o.dir)),Array.isArray(o.cmd)&&(o.cmd=o.cmd.map(l=>S(l))),o.owner?.repo&&(o.owner.repo=S(o.owner.repo));for(const o of Object.values(e.starting||{}))o.repo&&(o.repo=S(o.repo));const n=j(),r=i((o,l)=>d(o.sites,l)?.owner||d(o.starting,l),"owner"),s=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),a=[];for(const o of s){const l=r(n,o),u=r(e,o);(l?.session!==u?.session||l?.since!==u?.since)&&a.push(l?.session,u?.session)}if(a.some(Boolean)){const o=oe();se(o,a),ie(o)}k($,e)})}i(pt,"writeManifest");const _=i(e=>`${e.repo}#${e.pr}`,"prKey"),B=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),U=i(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),be=i(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=i(e=>Array.isArray(e?.items),"isWave"),J=/board[/\\]server\.mjs$/,A=i(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>J.test(t))&&typeof e.dir=="string","isBoardSite"),ye=i(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function ve(e,t,n){if(n==="file"){const s=p(c.resolve(e,t.items),null);return H(s)?[{slug:t.slug||c.basename(t.items,".json"),wave:s}]:[]}let r=[];try{r=f.readdirSync(F,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name).sort()}catch{}return r.map(s=>({slug:s,wave:p(c.join(F,s,"wave.json"),null)})).filter(s=>H(s.wave))}i(ve,"wavesOf");function Se(e,t){return e.flatMap(({slug:n,wave:r})=>r.items.filter(s=>s&&s.lane!=="done").flatMap(s=>fe(s,t||r.repo).map(a=>({wave:n,id:s.id,pr:a.number,repo:a.repo,since:s.gh_since||ge}))))}i(Se,"prsOf");function T(e,t){const n=p(c.join(t.dir,"board.json"),{}),r=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",s=ve(t.dir,n,r),a=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:r,waves:s,prs:Se(s,a),github:a,...t.remote&&!a?{remote:t.remote}:{},branch:t.branch,...ye(t)?{local:!0}:{},...t.hub===!0?{hub:!0}:{},auto_build:n.auto_build===!0,...X(n.build_target)?{build_target:n.build_target}:{},auto_review:n.auto_review===!0,skipDraft:n.skip_draft===!0,autoMerge:n.auto_merge===!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:U(n.reviewers),reviewerLogins:be(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}i(T,"boardOf");function N(e=j()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>T(t,n))}i(N,"listBoards");function ft(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?T(...t):void 0}i(ft,"hubBoard");function Oe(e,t){const n=i(r=>{try{return f.realpathSync(r)}catch{return c.resolve(r)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}i(Oe,"samePath");function dt(e,t=j()){const n=Object.entries(t?.sites||{}).find(([,r])=>A(r)&&Oe(r.dir,e));return n?T(...n):null}i(dt,"boardAt");async function W(e,t,n,{timeout:r=3e4}={}){if(e.port){let a;try{a=await fetch(`http://127.0.0.1:${e.port}/_claude/events?board=0`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(r)})}catch(o){if(o.name==="TimeoutError")throw new Error(`/${e.route} didn't answer within ${r/1e3} s, so it may already have the ${t} event`);if(o.cause?.code!=="ECONNREFUSED"&&o.cause?.message!=="bad port")throw new Error(`/${e.route} dropped the ${t} event's request (${o.cause?.code||o.cause?.message||o.message}), so it may already have it`)}if(a){if(a.ok)return(await a.json()).id;const o=await a.json().then(l=>l.error,()=>null);throw new Error(`/${e.route} answered ${a.status}${o?` (${o})`:""}, so it may already have the ${t} event`)}}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return f.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),f.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
- `),s.id}i(W,"appendEvent");async function ht(e,t,n){const r=[],s=[];for(const a of e)try{r.push({route:a.route,id:await W(a,t,n)})}catch(o){s.push(`${a.route}: could not add the ${t} event (${o.message})`)}return{events:r,failed:s}}i(ht,"sendToBoards");function $e(e,t=Date.now(),{grace:n=ke}={}){const r=p(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(r?.pid))return!1;if(typeof r.exited_at=="string")return t-Date.parse(r.exited_at)<=n;try{process.kill(r.pid,0)}catch(s){if(s.code!=="EPERM")return!1}return G(r.pid)?.includes("wait-events.mjs")??!0}i($e,"watcherRunning");const G=i(e=>ne(["-p",String(e),"-o","command="]),"commandOf"),K=i(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function mt(e,t,n,{command:r=G,kill:s=process.kill}={}){const a=p(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(a?.pid)||a.pid===process.pid||typeof a.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${K(t)}['"]?\\s+['"]?${K(n)}['"]?(\\s|$)`).test(r(a.pid)??""))return null;try{s(a.pid,"SIGTERM")}catch{return null}return a.pid}i(mt,"stopPreviousWatcher");function Ee(e,{now:t=Date.now(),running:n=v()}={}){const r=e.owner?.session;return!!(r&&(n?.has(r)??!0))&&$e(e.dir,t)}i(Ee,"isListening");function L(e,{now:t=Date.now(),running:n=v(),wakes:r=p(E,{})}={}){if(Ee(e,{now:t,running:n}))return"listening";const s=e.owner?.session,a=d(r,e.route);return!s||!e.owner.repo||a?.error||a?.pushed_at?"deaf":a?.resumed_at?"waking":n?.has(s)?"deaf":"asleep"}i(L,"listenState");const R=i((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),O=i(e=>R(e,e.owner.session),"cursorFile");function wt(e){const t=e.owner?.session?p(O(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const r=d(t,n);return[n,Number.isSafeInteger(r)&&r>=0?r:0]}))}i(wt,"watcherCursor");const gt=i(e=>Object.fromEntries(Object.entries(D(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function kt(e,{from:t,to:n=t,mark:r=null}){const s=p(R(e,t),null);if(!s&&!r)return;const a=Object.assign(Object.create(null),s);for(const[o,l]of Object.entries(r||{}))a[o]=Math.min(d(a,o)??l,l);k(R(e,n),a)}i(kt,"carryCursor");function D(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let r=[];try{r=f.readFileSync(c.resolve(e.dir,n),"utf8").split(`
4
- `).filter(Boolean)}catch{}return[n,t?r.filter(s=>t.test(s)):r]}))}i(D,"watchedLines");function xe(e){if(!e.owner?.session)return null;const t=p(O(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let r=null;for(const[s,a]of Object.entries(D(e)))for(const o of t?a.slice(d(t,s)||0):a){let l=null;try{l=JSON.parse(o).ts}catch{}const u=Date.parse(l);Number.isNaN(u)||u<n||(!r||u<Date.parse(r))&&(r=l)}return r}i(xe,"oldestUnheard");async function jt({manifest:e,now:t=new Date,running:n=v(),wakes:r=p(E,{})}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),p(P,{})),a=[],o=[];let l=!1;for(const u of N(e)){const h=L(u,{now:t.getTime(),running:n,wakes:r});if(h==="listening"){d(s,u.route)&&(delete s[u.route],l=!0);continue}if(h==="asleep"||h==="waking"||d(s,u.route))continue;const w=xe(u);if(!w||t-Date.parse(w)<je)continue;const y=Math.round((t-Date.parse(w))/6e4),g=n.has(u.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:`${u.title} isn't listening`,body:`An event has waited ${y} min and ${g}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:w},l=!0,a.push(u.route)}catch(V){o.push(`${u.route}: could not push that it isn't listening (${V.message})`)}}return l&&k(P,s),{pushed:a,failed:o}}i(jt,"checkListening");const _e=i(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function bt(e,t=j()){const n=N(t).find(s=>s.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!f.existsSync(O(n))&&k(O(n),Object.fromEntries(Object.entries(D(n)).map(([s,a])=>[s,a.length])));const r={id:await W(n,"ping",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(_e(n.dir),{ping:r,ack:null}),{route:e,ping:r.id}}i(bt,"handshake");const yt=["github","repo-fetch","events","wakes","slack"];function Ae(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 r=e.prs.filter(a=>t.prs?.[_(a)]?.at!==n.at).map(a=>`#${a.pr}`);if(r.length)return{link:"github",ok:!1,detail:`${[...new Set(r)].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 s=new Set(e.prs.map(_)).size;return{link:"github",ok:!0,detail:s?`${B(s,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}i(Ae,"githubLink");function Te(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}i(Te,"repoFetchLink");const Ne={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."}},Le=i(e=>({link:"events",listen:e,...Ne[e]}),"eventsLink");function Re(e,t,n){return(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 ${t.quiet.end}, when quiet hours end.`}}i(Re,"wakesLink");function De(e,t,n){const r=p(c.join(e.dir,"data","pr-slack.json"),{}),s=[];let a=0;for(const o of e.prs){const l=t?.prs?.[_(o)];if(l?.state!=="OPEN"||l.isDraft)continue;a++;const{wave:u}=e.waves.find(g=>g.slug===o.wave),h=u.items.find(g=>g.id===o.id),w=le(u,h,`${o.wave}/${o.id}`,n,ue(r,[{repo:o.repo,number:o.pr}])),y=[...new Set([...e.reviewers,...U(u.reviewers)])].filter(g=>!Object.hasOwn(w,g));y.length&&s.push(`#${o.pr} has no link for ${y.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:a?`Every reviewer has the link to ${B(a,"PR")} in review.`:"No PR is in review."}}i(De,"slackLink");const Ie=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function I(e=c.join(de,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,r=i(o=>{if(n.has(o))return;let l;try{l=f.readFileSync(o)}catch{return}n.set(o,l);for(const u of l.toString("utf8").matchAll(Ie))r(c.resolve(c.dirname(o),u[1]))},"visit");r(e);const s=[...n.keys()].map(o=>c.relative(t,o)).sort(),a=Z.createHash("sha256");for(const o of s)a.update(o).update("\0").update(n.get(c.join(t,o))).update("\0");return{hash:a.digest("hex").slice(0,12),files:s}}i(I,"hubCode");const vt=i(e=>I(c.resolve(e.cwd||e.dir,e.cmd.find(t=>J.test(t)))).hash,"boardCode");function St(){const e={...I(),pid:process.pid,at:new Date().toISOString()};return k(M,e),e}i(St,"recordHubCode");const Ce=ce().restartCommand,Fe=i(()=>p(M,null)?.hash??null,"runningHubCode"),Pe=i(()=>({running:Fe(),onDisk:I().hash}),"hubCodeState"),Ot=i(({running:e,onDisk:t}=Pe())=>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 ${Ce}`},"hubLink"),b=6e4,Me={github:i(e=>(e+10)*b,"github"),"repo-fetch":i(e=>(e+10)*b,"repo-fetch"),events:i(()=>2*b,"events"),wakes:i(e=>(e+10)*b,"wakes"),slack:i(e=>(e+10)*b,"slack")};function Be(e,t,{now:n,faults:r}){const s=r===void 0,a=s?d(p(x,{}),e)||{}:r,o={},l=t.map(u=>{if(u.ok)return{...u,state:"ok"};const h=d(a,u.link)||new Date(n).toISOString();o[u.link]=h;const w=Me[u.link](te);return{...u,state:n-Date.parse(h)>=w?"broken":"settling",since:h}});if(s&&JSON.stringify(o)!==JSON.stringify(a)){const u=Object.assign(Object.create(null),p(x,{}));Object.keys(o).length?u[e]=o:delete u[e];try{k(x,u)}catch{}}return l}i(Be,"settle");function Ue(e,t={}){const n=t.github??p(he,{}),r=t.settings??ee(),s=[Ae(e,n),Te(d(t.fetches??p(we,{}),e.route)),Le(t.listen??L(e)),Re(e,r,t.wakes??p(me,{})),De(e,n,t.topics??pe())].filter(Boolean);return Be(e.route,s,{now:t.now??Date.now(),faults:t.faults})}i(Ue,"linksOf");function $t(e){const t={running:v(),wakes:p(E,{})};if(t.running)for(const n of N(e))Ue(n,{listen:L(n,t)})}i($t,"recordLinkFaults");export{je as DEAF_AFTER,he as GITHUB_STATE,Me as GRACE,M as HUB_CODE,ut as INCOMING_FILES,yt as LINKS,we as REPO_FETCH,E as RESUME_STATE,me as WAKE_STATE,F as WAVES,W as appendEvent,dt as boardAt,vt as boardCode,ye as buildsLocally,kt as carryCursor,jt as checkListening,bt as handshake,_e as handshakeFile,ft as hubBoard,I as hubCode,Pe as hubCodeState,Ot as hubLink,A as isBoardSite,Ee as isListening,Ue as linksOf,N as listBoards,L as listenState,xe as oldestUnheard,_ as prKey,Se as prsOf,p as readJSON,j as readManifest,St as recordHubCode,$t as recordLinkFaults,be as reviewerLogins,Fe as runningHubCode,Oe as samePath,k as saveJSON,ht as sendToBoards,mt as stopPreviousWatcher,gt as watchedCounts,D as watchedLines,wt as watcherCursor,$e as watcherRunning,lt as withManifestLock,pt as writeManifest};
1
+ var q=Object.defineProperty;var i=(e,t)=>q(e,"name",{value:t,configurable:!0});import p from"node:fs";import c from"node:path";import z from"node:os";import Z from"node:crypto";import{fileURLToPath as Q}from"node:url";import{routeRecord as d}from"./routes.mjs";import{validBuildTarget as X}from"../board/build-script.mjs";import{deliver as Y,readSettings as ee}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as te}from"./schedule.mjs";import{liveSessions as S,ps as ne}from"./live.mjs";import{withFileLock as re}from"./locks.mjs";import{dropSessionPrompts as se,readPrompts as oe,savePrompts as ie,withPromptsLock as ae}from"./prompts.mjs";import{storedPath as v}from"../bin/install.mjs";import{hubService as ce}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{prLinks as ue,reviewLinks as le,topicItems as fe}from"../board/review.mjs";import{cardPRs as pe}from"../shared/card-prs.mjs";const de=c.resolve(c.dirname(Q(import.meta.url)),".."),F=c.join(z.homedir(),".claude"),m=c.join(F,"kickflip"),C=c.join(F,"waves"),$=c.join(m,"manifest.json"),he=c.join(m,"github-state.json"),me=c.join(m,"wake-state.json"),ut=c.join(m,"incoming-files.json"),P=c.join(m,"deaf-state.json"),E=c.join(m,"resume-state.json"),x=c.join(m,"link-faults.json"),ge=c.join(m,"repo-fetch.json"),M=c.join(m,"hub-code.json"),we="1970-01-01T00:00:00Z",ke=2*6e4,je=15*6e4,f=i((e,t)=>{try{return JSON.parse(p.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;p.mkdirSync(c.dirname(e),{recursive:!0}),p.writeFileSync(n,JSON.stringify(t,null,2)+`
2
+ `),p.renameSync(n,e)}i(k,"saveJSON");const j=i(()=>f($,{sites:{}}),"readManifest"),lt=i(e=>re($,e),"withManifestLock");function ft(e){return ae(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(o=>o.site)];for(const o of t.filter(Boolean))o.dir&&(o.dir=v(o.dir)),Array.isArray(o.cmd)&&(o.cmd=o.cmd.map(l=>v(l))),o.owner?.repo&&(o.owner.repo=v(o.owner.repo));for(const o of Object.values(e.starting||{}))o.repo&&(o.repo=v(o.repo));const n=j(),r=i((o,l)=>d(o.sites,l)?.owner||d(o.starting,l),"owner"),s=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),a=[];for(const o of s){const l=r(n,o),u=r(e,o);(l?.session!==u?.session||l?.since!==u?.since)&&a.push(l?.session,u?.session)}if(a.some(Boolean)){const o=oe();se(o,a),ie(o)}k($,e)})}i(ft,"writeManifest");const _=i(e=>`${e.repo}#${e.pr}`,"prKey"),B=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),U=i(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),be=i(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=i(e=>Array.isArray(e?.items),"isItemFile"),J=/board[/\\]server\.mjs$/,A=i(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>J.test(t))&&typeof e.dir=="string","isBoardSite"),ye=i(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function Se(e,t,n){if(n==="file"){const s=f(c.resolve(e,t.items),null);return H(s)?[{slug:t.slug||c.basename(t.items,".json"),itemFile:s}]:[]}let r=[];try{r=p.readdirSync(C,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name).sort()}catch{}return r.map(s=>({slug:s,itemFile:f(c.join(C,s,"wave.json"),null)})).filter(s=>H(s.itemFile))}i(Se,"filesOf");function ve(e,t){return e.flatMap(({slug:n,itemFile:r})=>r.items.filter(s=>s&&s.lane!=="done").flatMap(s=>pe(s,t||r.repo).map(a=>({file:n,id:s.id,pr:a.number,repo:a.repo,since:s.gh_since||we}))))}i(ve,"prsOf");function T(e,t){const n=f(c.join(t.dir,"board.json"),{}),r=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",s=Se(t.dir,n,r),a=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:r,files:s,prs:ve(s,a),github:a,...t.remote&&!a?{remote:t.remote}:{},branch:t.branch,...ye(t)?{local:!0}:{},...t.hub===!0?{hub:!0}:{},auto_build:n.auto_build===!0,...X(n.build_target)?{build_target:n.build_target}:{},auto_review:n.auto_review===!0,skipDraft:n.skip_draft===!0,autoMerge:n.auto_merge===!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:U(n.reviewers),reviewerLogins:be(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}i(T,"boardOf");function N(e=j()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>T(t,n))}i(N,"listBoards");function pt(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?T(...t):void 0}i(pt,"hubBoard");function Oe(e,t){const n=i(r=>{try{return p.realpathSync(r)}catch{return c.resolve(r)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}i(Oe,"samePath");function dt(e,t=j()){const n=Object.entries(t?.sites||{}).find(([,r])=>A(r)&&Oe(r.dir,e));return n?T(...n):null}i(dt,"boardAt");async function G(e,t,n,{timeout:r=3e4}={}){if(e.port){let a;try{a=await fetch(`http://127.0.0.1:${e.port}/_claude/events?board=0`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(r)})}catch(o){if(o.name==="TimeoutError")throw new Error(`/${e.route} didn't answer within ${r/1e3} s, so it may already have the ${t} event`);if(o.cause?.code!=="ECONNREFUSED"&&o.cause?.message!=="bad port")throw new Error(`/${e.route} dropped the ${t} event's request (${o.cause?.code||o.cause?.message||o.message}), so it may already have it`)}if(a){if(a.ok)return(await a.json()).id;const o=await a.json().then(l=>l.error,()=>null);throw new Error(`/${e.route} answered ${a.status}${o?` (${o})`:""}, so it may already have the ${t} event`)}}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return p.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),p.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
+ `),s.id}i(G,"appendEvent");async function ht(e,t,n){const r=[],s=[];for(const a of e)try{r.push({route:a.route,id:await G(a,t,n)})}catch(o){s.push(`${a.route}: could not add the ${t} event (${o.message})`)}return{events:r,failed:s}}i(ht,"sendToBoards");function $e(e,t=Date.now(),{grace:n=ke}={}){const r=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(r?.pid))return!1;if(typeof r.exited_at=="string")return t-Date.parse(r.exited_at)<=n;try{process.kill(r.pid,0)}catch(s){if(s.code!=="EPERM")return!1}return K(r.pid)?.includes("wait-events.mjs")??!0}i($e,"watcherRunning");const K=i(e=>ne(["-p",String(e),"-o","command="]),"commandOf"),W=i(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function mt(e,t,n,{command:r=K,kill:s=process.kill}={}){const a=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(a?.pid)||a.pid===process.pid||typeof a.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${W(t)}['"]?\\s+['"]?${W(n)}['"]?(\\s|$)`).test(r(a.pid)??""))return null;try{s(a.pid,"SIGTERM")}catch{return null}return a.pid}i(mt,"stopPreviousWatcher");function Ee(e,{now:t=Date.now(),running:n=S()}={}){const r=e.owner?.session;return!!(r&&(n?.has(r)??!0))&&$e(e.dir,t)}i(Ee,"isListening");function L(e,{now:t=Date.now(),running:n=S(),wakes:r=f(E,{})}={}){if(Ee(e,{now:t,running:n}))return"listening";const s=e.owner?.session,a=d(r,e.route);return!s||!e.owner.repo||a?.error||a?.pushed_at?"deaf":a?.resumed_at?"waking":n?.has(s)?"deaf":"asleep"}i(L,"listenState");const R=i((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),O=i(e=>R(e,e.owner.session),"cursorFile");function gt(e){const t=e.owner?.session?f(O(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const r=d(t,n);return[n,Number.isSafeInteger(r)&&r>=0?r:0]}))}i(gt,"watcherCursor");const wt=i(e=>Object.fromEntries(Object.entries(D(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function kt(e,{from:t,to:n=t,mark:r=null}){const s=f(R(e,t),null);if(!s&&!r)return;const a=Object.assign(Object.create(null),s);for(const[o,l]of Object.entries(r||{}))a[o]=Math.min(d(a,o)??l,l);k(R(e,n),a)}i(kt,"carryCursor");function D(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let r=[];try{r=p.readFileSync(c.resolve(e.dir,n),"utf8").split(`
4
+ `).filter(Boolean)}catch{}return[n,t?r.filter(s=>t.test(s)):r]}))}i(D,"watchedLines");function xe(e){if(!e.owner?.session)return null;const t=f(O(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let r=null;for(const[s,a]of Object.entries(D(e)))for(const o of t?a.slice(d(t,s)||0):a){let l=null;try{l=JSON.parse(o).ts}catch{}const u=Date.parse(l);Number.isNaN(u)||u<n||(!r||u<Date.parse(r))&&(r=l)}return r}i(xe,"oldestUnheard");async function jt({manifest:e,now:t=new Date,running:n=S(),wakes:r=f(E,{})}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),f(P,{})),a=[],o=[];let l=!1;for(const u of N(e)){const h=L(u,{now:t.getTime(),running:n,wakes:r});if(h==="listening"){d(s,u.route)&&(delete s[u.route],l=!0);continue}if(h==="asleep"||h==="waking"||d(s,u.route))continue;const g=xe(u);if(!g||t-Date.parse(g)<je)continue;const y=Math.round((t-Date.parse(g))/6e4),w=n.has(u.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:`${u.title} isn't listening`,body:`An event has waited ${y} min and ${w}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:g},l=!0,a.push(u.route)}catch(V){o.push(`${u.route}: could not push that it isn't listening (${V.message})`)}}return l&&k(P,s),{pushed:a,failed:o}}i(jt,"checkListening");const _e=i(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function bt(e,t=j()){const n=N(t).find(s=>s.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!p.existsSync(O(n))&&k(O(n),Object.fromEntries(Object.entries(D(n)).map(([s,a])=>[s,a.length])));const r={id:await G(n,"ping",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(_e(n.dir),{ping:r,ack:null}),{route:e,ping:r.id}}i(bt,"handshake");const yt=["github","repo-fetch","events","wakes","slack"];function Ae(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 r=e.prs.filter(a=>t.prs?.[_(a)]?.at!==n.at).map(a=>`#${a.pr}`);if(r.length)return{link:"github",ok:!1,detail:`${[...new Set(r)].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 s=new Set(e.prs.map(_)).size;return{link:"github",ok:!0,detail:s?`${B(s,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}i(Ae,"githubLink");function Te(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}i(Te,"repoFetchLink");const Ne={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."}},Le=i(e=>({link:"events",listen:e,...Ne[e]}),"eventsLink");function Re(e,t,n){return(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 ${t.quiet.end}, when quiet hours end.`}}i(Re,"wakesLink");function De(e,t,n){const r=f(c.join(e.dir,"data","pr-slack.json"),{}),s=[];let a=0;for(const o of e.prs){const l=t?.prs?.[_(o)];if(l?.state!=="OPEN"||l.isDraft)continue;a++;const{itemFile:u}=e.files.find(w=>w.slug===o.file),h=u.items.find(w=>w.id===o.id),g=le(u,h,`${o.file}/${o.id}`,n,ue(r,[{repo:o.repo,number:o.pr}])),y=[...new Set([...e.reviewers,...U(u.reviewers)])].filter(w=>!Object.hasOwn(g,w));y.length&&s.push(`#${o.pr} has no link for ${y.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:a?`Every reviewer has the link to ${B(a,"PR")} in review.`:"No PR is in review."}}i(De,"slackLink");const Ie=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function I(e=c.join(de,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,r=i(o=>{if(n.has(o))return;let l;try{l=p.readFileSync(o)}catch{return}n.set(o,l);for(const u of l.toString("utf8").matchAll(Ie))r(c.resolve(c.dirname(o),u[1]))},"visit");r(e);const s=[...n.keys()].map(o=>c.relative(t,o)).sort(),a=Z.createHash("sha256");for(const o of s)a.update(o).update("\0").update(n.get(c.join(t,o))).update("\0");return{hash:a.digest("hex").slice(0,12),files:s}}i(I,"hubCode");const St=i(e=>I(c.resolve(e.cwd||e.dir,e.cmd.find(t=>J.test(t)))).hash,"boardCode");function vt(){const e={...I(),pid:process.pid,at:new Date().toISOString()};return k(M,e),e}i(vt,"recordHubCode");const Fe=ce().restartCommand,Ce=i(()=>f(M,null)?.hash??null,"runningHubCode"),Pe=i(()=>({running:Ce(),onDisk:I().hash}),"hubCodeState"),Ot=i(({running:e,onDisk:t}=Pe())=>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 ${Fe}`},"hubLink"),b=6e4,Me={github:i(e=>(e+10)*b,"github"),"repo-fetch":i(e=>(e+10)*b,"repo-fetch"),events:i(()=>2*b,"events"),wakes:i(e=>(e+10)*b,"wakes"),slack:i(e=>(e+10)*b,"slack")};function Be(e,t,{now:n,faults:r}){const s=r===void 0,a=s?d(f(x,{}),e)||{}:r,o={},l=t.map(u=>{if(u.ok)return{...u,state:"ok"};const h=d(a,u.link)||new Date(n).toISOString();o[u.link]=h;const g=Me[u.link](te);return{...u,state:n-Date.parse(h)>=g?"broken":"settling",since:h}});if(s&&JSON.stringify(o)!==JSON.stringify(a)){const u=Object.assign(Object.create(null),f(x,{}));Object.keys(o).length?u[e]=o:delete u[e];try{k(x,u)}catch{}}return l}i(Be,"settle");function Ue(e,t={}){const n=t.github??f(he,{}),r=t.settings??ee(),s=[Ae(e,n),Te(d(t.fetches??f(ge,{}),e.route)),Le(t.listen??L(e)),Re(e,r,t.wakes??f(me,{})),De(e,n,t.topics??fe())].filter(Boolean);return Be(e.route,s,{now:t.now??Date.now(),faults:t.faults})}i(Ue,"linksOf");function $t(e){const t={running:S(),wakes:f(E,{})};if(t.running)for(const n of N(e))Ue(n,{listen:L(n,t)})}i($t,"recordLinkFaults");export{je as DEAF_AFTER,he as GITHUB_STATE,Me as GRACE,M as HUB_CODE,ut as INCOMING_FILES,yt as LINKS,ge as REPO_FETCH,E as RESUME_STATE,me as WAKE_STATE,C as WAVES,G as appendEvent,dt as boardAt,St as boardCode,ye as buildsLocally,kt as carryCursor,jt as checkListening,bt as handshake,_e as handshakeFile,pt as hubBoard,I as hubCode,Pe as hubCodeState,Ot as hubLink,A as isBoardSite,Ee as isListening,Ue as linksOf,N as listBoards,L as listenState,xe as oldestUnheard,_ as prKey,ve as prsOf,f as readJSON,j as readManifest,vt as recordHubCode,$t as recordLinkFaults,be as reviewerLogins,Ce as runningHubCode,Oe as samePath,k as saveJSON,ht as sendToBoards,mt as stopPreviousWatcher,wt as watchedCounts,D as watchedLines,gt as watcherCursor,$e as watcherRunning,lt as withManifestLock,ft as writeManifest};
package/hub/github.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var le=Object.defineProperty;var S=(e,s)=>le(e,"name",{value:s,configurable:!0});import x from"node:fs";import U from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as V,fetchPRs as ue,markReady as me,reportOf as ge}from"./pr-report.mjs";import{wakesDue as pe,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as F,WAKE_STATE as Q,WAVES as X,listBoards as Y,appendEvent as Z,prKey as M,readJSON as ee,saveJSON as te,reviewerLogins as we}from"./boards.mjs";import{appendThread as H,setItem as se,mergedLine as ye,mergeCard as ve}from"../board/store.mjs";import{allLanded as re,cardPRs as ke,leastReady as $e,prNames as Se}from"../shared/card-prs.mjs";const ne=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as T,hubAudit as ae}from"../shared/audit.mjs";const _=["ci","review","comments","merged","mergeable","draft","linked"],G=S(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(e){const n=(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:n?String(n.id):null,eventAt:n?.at||null,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,commits:Array.isArray(e.commits)?e.commits:[]}}S(Ee,"summarize");function Re(e,s){const n=new Set;if(!e)return s.state==="MERGED"&&n.add("merged"),s.eventId&&n.add("comments"),_.filter(a=>n.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
1
+ var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import W from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as x,fetchPRs as ue,markReady as me,reportOf as ge}from"./pr-report.mjs";import{wakesDue as pe,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as U,WAKE_STATE as V,WAVES as Q,listBoards as X,appendEvent as Y,prKey as P,readJSON as Z,saveJSON as ee,reviewerLogins as we}from"./boards.mjs";import{appendMessage as ye,appendStatus as te,setItem as se,mergeCard as ke}from"../board/store.mjs";import{allLanded as ne,cardPRs as Se,leastReady as $e,prNames as ve}from"../shared/card-prs.mjs";const re=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as G,hubAudit as ie}from"../shared/audit.mjs";const H=["ci","review","comments","merged","mergeable","draft","linked"],T=v(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(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,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,commits:Array.isArray(e.commits)?e.commits:[]}}v(Ee,"summarize");function Oe(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),H.filter(i=>r.has(i));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
2
2
  `)!==s.failing.join(`
3
- `))&&n.add("ci"),e.reviewDecision!==s.reviewDecision&&n.add("review"),s.eventId&&s.eventId!==e.eventId&&n.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&n.add("merged");for(const a of["mergeable","mergeStateStatus"])G(e[a])&&G(s[a])&&e[a]!==s[a]&&n.add("mergeable");return e.isDraft!==s.isDraft&&n.add("draft"),_.filter(a=>n.has(a))}S(Re,"changesBetween");function oe(e,s){const n={...s};for(const r of["mergeable","mergeStateStatus"])!G(s[r])&&G(e?.[r])&&(n[r]=e[r]);return n}S(oe,"carry");function Oe(e,s){const n=U.join(e,"data","checked.json"),r=`${n}.${process.pid}.tmp`;x.mkdirSync(U.dirname(n),{recursive:!0}),x.writeFileSync(r,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
- `),x.renameSync(r,n)}S(Oe,"writeChecked");async function Ue({every:e,manifest:s}){const n=new Date().toISOString(),r=ee(F,{prs:{}});r.prs||={},r.acknowledged||={},r.threaded||={};const a=Y(s),i=[],c=[...new Map(a.flatMap(t=>t.prs).map(t=>[M(t),{repo:t.repo,pr:t.pr}])).values()],f=[...new Set(a.flatMap(Ne))];let o=null;if(c.length||f.length)try{o=await ue(c,f)}catch(t){i.push(t.message)}const d=new Map(a.map(t=>[t.route,[]]));if(o)for(const t of a)d.set(t.route,await Pe(t,o.open,i));const p=new Map(c.map((t,g)=>[M(t),o?o.results[g]:null])),h=o?.login??null,m=S((t,g)=>JSON.stringify([M(t),g]),"fetchKey"),y=new Map,O=new Map,R=new Map;for(const t of a){t.me=t.githubLogin||h;for(const g of t.prs){const v=M(g);y.set(v,g);const N=R.get(v)||new Set;if(N.add(t.me),R.set(v,N),!t.me)continue;const C=m(g,t.me),l=O.get(C);(!l||g.since<l.since)&&O.set(C,{...g,me:t.me})}}const u={},E={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),k=new Set;for(const[t,g]of O){const v=p.get(M(g));if(!v?.view){u[t]=null,v?.error&&!k.has(v.error)&&(k.add(v.error),i.push(v.error));continue}E[t]=ge(v,g);const N=Ee(E[t]);u[t]=N,w.has(M(g))||w.set(M(g),N)}const q=[],B=[],z=new Set,ie=new Map,K=new Set,ce=new Set,de=new Set;for(const t of a){const g=t.prs.map(l=>{const $=M(l),P=JSON.stringify([t.route,l.wave,l.id]),A=r.acknowledged[$]?.[P]||r.prs[$],I=u[m(l,t.me)],j=I?Re(A,I):[];return j.length&&(D[$]=_.filter(W=>D[$].includes(W)||j.includes(W))),{p:l,key:$,recipient:P,previous:A,next:I,changes:j}});for(const l of g){const $=E[m(l.p,t.me)],P=g.filter(A=>A.p.wave===l.p.wave&&A.p.id===l.p.id);$&&await Me(t,l,$,P,{state:r,merged:ie,failed:i})&&K.add(t.route),t.skipDraft&&await be(t,l,p.get(l.key)?.view,ce,i),t.autoMerge&&await Ce(t,l,p.get(l.key)?.view,de,i)}const v=g.filter(l=>l.changes.length);let N=g.every(l=>l.next),C=!0;if(v.length||d.get(t.route).length)try{const l=[...v.map(({p:$,changes:P})=>({wave:$.wave,id:$.id,pr:$.pr,changes:P})),...d.get(t.route)];q.push({route:t.route,id:await Z(t,"github-change",{prs:l})})}catch(l){i.push(`${t.route}: could not add the github-change event (${l.message})`);for(const{key:$}of v)z.add($);N=C=!1}for(const{key:l,recipient:$,previous:P,next:A,changes:I}of g)A&&(C||!I.length)&&(r.acknowledged[l]||={},r.acknowledged[l][$]=oe(P,A));if(N)try{Oe(t.dir,t.prs.length?e:null),B.push(t.route)}catch(l){i.push(`${t.route}: could not write checked.json (${l.message})`)}}const J=[];for(const[t,g]of y){const v=w.get(t);!v||z.has(t)||(D[t].length&&J.push({repo:g.repo,pr:g.pr,changes:D[t]}),r.prs[t]={...oe(r.prs[t],v),at:n},R.get(t).size===1&&delete r.acknowledged[t])}for(const t of Object.keys(r.acknowledged))y.has(t)||delete r.acknowledged[t];for(const t of Object.keys(r.threaded))y.has(t)||delete r.threaded[t];await De(a,r,i),r.lastCheck={at:n,watched:y.size,failed:i.length,changed:J.length,boards:B},te(F,r);const L={moved:[],failed:[]};try{await Ie(a,u,m,L)}catch(t){L.failed.push(`linear: ${t.message}`)}return{at:n,watched:y.size,failed:i,changes:J,events:q,checked:B,merged:[...K],linear:L,rate:o?.rate??null}}S(Ue,"checkGitHub");const Ne=S(e=>e.github?[e.github]:[...new Set(e.waves.map(s=>s.wave.repo).filter(Boolean))],"repoList");async function Pe(e,s,n){const r=[];for(const{slug:a,wave:i}of e.waves){const c=e.github||i.repo,f=new Map((s[c]||[]).filter(o=>!o.isCrossRepository).map(o=>[o.headRefName,o.number]));for(const o of i.items){if(!f.has(o.id)||o.lane==="done"||ke(o,c).some(h=>h.repo===c))continue;const d=`${a}/${o.id}`,p=f.get(o.id);try{await se(e.dir,d,{pr:p}),await H(e.dir,d,{text:`Found PR #${p} on branch ${o.id}.`}),r.push({wave:a,id:o.id,pr:p,changes:["linked"]})}catch(h){n.push(`${e.route}: could not link PR #${p} to ${d} (${h.message})`)}}}return r}S(Pe,"linkHandPRs");function Ae(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}S(Ae,"lineText");async function Me(e,{p:s,key:n,recipient:r},a,i,{state:c,merged:f,failed:o}){const d=`${s.wave}/${s.id}`,p=new Set(c.threaded[n]?.[r]||[]),h=e.waves.find(u=>u.slug===s.wave)?.wave,m={...e.reviewerLogins,...we(h?.reviewers)};for(const u of a.events){const E=`${u.kind}:${u.id}`;if(!["user","reviewer"].includes(u.who)||u.at<=s.since||p.has(E))continue;const w=Ae(u);try{w&&await H(e.dir,d,{text:w,via:"github",who:u.who==="user"?"love":m[String(u.login).toLowerCase()]||u.login,link:u.url||`https://github.com/${s.repo}/pull/${s.pr}`}),p.add(E)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}p.size&&((c.threaded[n]||={})[r]=[...p]);const y=i.map(u=>u.next);if(!ne.includes(a.state)||!y.every(u=>ne.includes(u?.state)))return!1;const O=re(y),R=JSON.stringify([e.kind==="waves"?X:e.dir,s.wave,s.id]);if(!f.has(R)){const u=e.github||e.waves.find(k=>k.slug===s.wave)?.wave.repo,E=i.filter(k=>k.next.state==="MERGED").map(k=>({repo:k.p.repo,number:k.p.pr})),w=i.map(k=>({repo:k.p.repo,number:k.p.pr})),D=O?ye(E,u):`Closed: ${Se(w,u)} closed unmerged.`;try{await se(e.dir,d,{merged:E,force:!0,text:D}),f.set(R,O)}catch(k){o.push(`${e.route}: could not record ${d} ${O?"merged":"closed"} (${k.message})`),f.set(R,!1)}}return f.get(R)}S(Me,"recordOnCard");async function be(e,{p:s,key:n},r,a,i){if(a.has(n)||r?.state!=="OPEN"||!r.isDraft||!V(r.statusCheckRollup))return;a.add(n);const c={call:"ready-draft",key:`${s.wave}/${s.id}`,reason:`The board skips the draft and PR #${s.pr}'s CI passed.`,inputs:{repo:s.repo,pr:s.pr,isDraft:r.isDraft,checks:"passed"}};try{await me({repo:s.repo,pr:s.pr})}catch(f){b(T(e.dir),{...c,result:"failed",reason:`${c.reason} gh failed: ${f.message}`},{once:!0}),i.push(`${e.route}: could not mark ${n} ready for review (${f.message})`);return}b(T(e.dir),{...c,result:"marked ready"});try{await H(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(f){i.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${f.message})`)}}S(be,"readyGreenDraft");async function Ce(e,{p:s},n,r,a){const i=`${s.wave}/${s.id}`,c=JSON.stringify([e.dir,i]);if(r.has(c)||n?.state!=="OPEN"||n.isDraft||n.reviewDecision!=="APPROVED"||n.mergeStateStatus!=="CLEAN"||!V(n.statusCheckRollup))return;r.add(c);const f={call:"auto-merge",key:i,reason:`Auto-merge is on and PR #${s.pr} is approved, green and clean.`,inputs:{repo:s.repo,pr:s.pr,reviewDecision:n.reviewDecision,mergeStateStatus:n.mergeStateStatus,checks:"passed"}};try{await ve(e.dir,i),b(T(e.dir),{...f,result:"merged"})}catch(o){b(T(e.dir),{...f,result:"not merged",reason:`${f.reason} ${o.message}`},{once:!0}),o.message.startsWith("Not merged:")||a.push(`${e.route}: could not merge ${i} (${o.message})`)}}S(Ce,"mergeApproved");async function Ie(e,s,n,r){const a=fe();if(!a)return;const i=new Map;for(const c of e){const f=Map.groupBy(c.prs,d=>JSON.stringify([d.wave,d.id])),o=c.kind==="waves"?X:c.dir;for(const{slug:d,wave:p}of c.waves)for(const h of p.items){const m=new Set(h.linear||[]);c.tickets?.source==="linear"&&h.ticket&&m.add(h.ticket);const y=(f.get(JSON.stringify([d,h.id]))||[]).map(w=>s[n(w,c.me)]||null),O=y[$e(y)],R=h.lane==="done"||re(y),u={merged:R,ready:O?.state==="OPEN"&&!O.isDraft,hasPR:y.length>0&&(R||y.some(w=>w?.state==="OPEN")),failed:y.some(w=>!w)},E=JSON.stringify([o,d,h.id]);for(const w of m){i.has(w)||i.set(w,{items:new Map,scoped:!1,created:!1,cards:[]});const D=i.get(w),k=D.items.get(E);D.items.set(E,{...u,failed:u.failed||k?.failed}),D.scoped||=y.some(Boolean),D.created||=h.ticket===w&&!!h.ticket_created,D.cards.push(`${c.route}:${d}/${h.id}`)}}}for(const[c,f]of i){const o=[...f.items.values()];if(!f.scoped||o.some(m=>m.failed))continue;const d=o.every(m=>m.merged)?"done":o.every(m=>m.merged||m.ready)?"review":o.some(m=>m.hasPR)?"started":null;if(!d)continue;const p={done:"Every card on it has merged.",review:"Every card on it has merged or has a PR ready for review.",started:"A card on it has a PR."}[d],h={call:"linear-move",key:c,reason:p,inputs:{target:d,cards:f.cards,items:o}};try{const m=await a.advanceIssue(c,d,{owner:a.saved.viewer.id,created:f.created});m&&(r.moved.push({id:c,state:m}),b(ae(),{...h,result:`moved to ${m}`}))}catch(m){b(ae(),{...h,result:"failed",reason:`${p} ${m.message}`},{once:!0}),r.failed.push(`linear ${c}: ${m.message}`)}}}S(Ie,"advanceLinear");async function Ve({now:e=new Date,settings:s,manifest:n}){const r=ee(Q,{});r.sent||={};const a=pe(e,s,r.sent),i=[],c=[],f=[];if(!a.length)return{due:a,events:i,failed:c};const o=Y(n);for(const d of o)try{i.push({route:d.route,id:await Z(d,"check-github",{reason:"morning"})})}catch(p){c.push(`${d.route}: could not add the morning check-github event (${p.message})`),f.push(d.route)}if(i.length||!o.length){for(const d of a)r.sent[d]=he(e);r.last={at:e.toISOString(),due:a,routes:i.map(d=>d.route),failed:f},te(Q,r)}return{due:a,events:i,failed:c}}S(Ve,"sendWakes");export{_ as CHANGE_KINDS,oe as carry,Re as changesBetween,Ue as checkGitHub,Ve as sendWakes,Ee as summarize};
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"])T(e[i])&&T(s[i])&&e[i]!==s[i]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),H.filter(i=>r.has(i))}v(Oe,"changesBetween");function oe(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!T(s[n])&&T(e?.[n])&&(r[n]=e[n]);return r}v(oe,"carry");function Re(e,s){const r=W.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;F.mkdirSync(W.dirname(r),{recursive:!0}),F.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
+ `),F.renameSync(n,r)}v(Re,"writeChecked");async function xe({every:e,manifest:s}){const r=new Date().toISOString(),n=Z(U,{prs:{}});n.prs||={},n.acknowledged||={},n.threaded||={};const i=X(s),a=[],c=[...new Map(i.flatMap(t=>t.prs).map(t=>[P(t),{repo:t.repo,pr:t.pr}])).values()],f=[...new Set(i.flatMap(Ne))];let o=null;if(c.length||f.length)try{o=await ue(c,f)}catch(t){a.push(t.message)}const d=new Map(i.map(t=>[t.route,[]]));if(o)for(const t of i)d.set(t.route,await Ae(t,o.open,a));const p=new Map(c.map((t,g)=>[P(t),o?o.results[g]:null])),h=o?.login??null,u=v((t,g)=>JSON.stringify([P(t),g]),"fetchKey"),y=new Map,R=new Map,E=new Map;for(const t of i){t.me=t.githubLogin||h;for(const g of t.prs){const k=P(g);y.set(k,g);const N=E.get(k)||new Set;if(N.add(t.me),E.set(k,N),!t.me)continue;const C=u(g,t.me),l=R.get(C);(!l||g.since<l.since)&&R.set(C,{...g,me:t.me})}}const m={},O={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),S=new Set;for(const[t,g]of R){const k=p.get(P(g));if(!k?.view){m[t]=null,k?.error&&!S.has(k.error)&&(S.add(k.error),a.push(k.error));continue}O[t]=ge(k,g);const N=Ee(O[t]);m[t]=N,w.has(P(g))||w.set(P(g),N)}const _=[],B=[],q=new Set,ae=new Map,z=new Set,ce=new Set,de=new Set;for(const t of i){const g=t.prs.map(l=>{const $=P(l),A=JSON.stringify([t.route,l.file,l.id]),M=n.acknowledged[$]?.[A]||n.prs[$],I=m[u(l,t.me)],L=I?Oe(M,I):[];return L.length&&(D[$]=H.filter(K=>D[$].includes(K)||L.includes(K))),{p:l,key:$,recipient:A,previous:M,next:I,changes:L}});for(const l of g){const $=O[u(l.p,t.me)],A=g.filter(M=>M.p.file===l.p.file&&M.p.id===l.p.id);$&&await Pe(t,l,$,A,{state:n,merged:ae,failed:a})&&z.add(t.route),t.skipDraft&&await be(t,l,p.get(l.key)?.view,ce,a),t.autoMerge&&await Ce(t,l,p.get(l.key)?.view,de,a)}const k=g.filter(l=>l.changes.length);let N=g.every(l=>l.next),C=!0;if(k.length||d.get(t.route).length)try{const l=[...k.map(({p:$,changes:A})=>({file:$.file,id:$.id,pr:$.pr,changes:A})),...d.get(t.route)];_.push({route:t.route,id:await Y(t,"github-change",{prs:l})})}catch(l){a.push(`${t.route}: could not add the github-change event (${l.message})`);for(const{key:$}of k)q.add($);N=C=!1}for(const{key:l,recipient:$,previous:A,next:M,changes:I}of g)M&&(C||!I.length)&&(n.acknowledged[l]||={},n.acknowledged[l][$]=oe(A,M));if(N)try{Re(t.dir,t.prs.length?e:null),B.push(t.route)}catch(l){a.push(`${t.route}: could not write checked.json (${l.message})`)}}const J=[];for(const[t,g]of y){const k=w.get(t);!k||q.has(t)||(D[t].length&&J.push({repo:g.repo,pr:g.pr,changes:D[t]}),n.prs[t]={...oe(n.prs[t],k),at:r},E.get(t).size===1&&delete n.acknowledged[t])}for(const t of Object.keys(n.acknowledged))y.has(t)||delete n.acknowledged[t];for(const t of Object.keys(n.threaded))y.has(t)||delete n.threaded[t];await De(i,n,a),n.lastCheck={at:r,watched:y.size,failed:a.length,changed:J.length,boards:B},ee(U,n);const j={moved:[],failed:[]};try{await Ie(i,m,u,j)}catch(t){j.failed.push(`linear: ${t.message}`)}return{at:r,watched:y.size,failed:a,changes:J,events:_,checked:B,merged:[...z],linear:j,rate:o?.rate??null}}v(xe,"checkGitHub");const Ne=v(e=>e.github?[e.github]:[...new Set(e.files.map(s=>s.itemFile.repo).filter(Boolean))],"repoList");async function Ae(e,s,r){const n=[];for(const{slug:i,itemFile:a}of e.files){const c=e.github||a.repo,f=new Map((s[c]||[]).filter(o=>!o.isCrossRepository).map(o=>[o.headRefName,o.number]));for(const o of a.items){if(!f.has(o.id)||o.lane==="done"||Se(o,c).some(h=>h.repo===c))continue;const d=`${i}/${o.id}`,p=f.get(o.id);try{await se(e.dir,d,{pr:p}),await te(e.dir,d,"pr-found",{pr:p,branch:o.id}),n.push({file:i,id:o.id,pr:p,changes:["linked"]})}catch(h){r.push(`${e.route}: could not link PR #${p} to ${d} (${h.message})`)}}}return n}v(Ae,"linkHandPRs");function Me(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}v(Me,"lineText");async function Pe(e,{p:s,key:r,recipient:n},i,a,{state:c,merged:f,failed:o}){const d=`${s.file}/${s.id}`,p=new Set(c.threaded[r]?.[n]||[]),h=e.files.find(m=>m.slug===s.file)?.itemFile,u={...e.reviewerLogins,...we(h?.reviewers)};for(const m of i.events){const O=`${m.kind}:${m.id}`;if(!["user","reviewer"].includes(m.who)||m.at<=s.since||p.has(O))continue;const w=Me(m);try{w&&await ye(e.dir,d,{text:w,via:"github",who:m.who==="user"?"love":u[String(m.login).toLowerCase()]||m.login,link:m.url||`https://github.com/${s.repo}/pull/${s.pr}`}),p.add(O)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}p.size&&((c.threaded[r]||={})[n]=[...p]);const y=a.map(m=>m.next);if(!re.includes(i.state)||!y.every(m=>re.includes(m?.state)))return!1;const R=ne(y),E=JSON.stringify([e.kind==="waves"?Q:e.dir,s.file,s.id]);if(!f.has(E)){const m=e.github||e.files.find(S=>S.slug===s.file)?.itemFile.repo,O=a.filter(S=>S.next.state==="MERGED").map(S=>({repo:S.p.repo,number:S.p.pr})),w=a.map(S=>({repo:S.p.repo,number:S.p.pr})),D=R?void 0:["prs-closed",{prs:ve(w,m)}];try{await se(e.dir,d,{merged:O,force:!0,closing:D}),f.set(E,R)}catch(S){o.push(`${e.route}: could not record ${d} ${R?"merged":"closed"} (${S.message})`),f.set(E,!1)}}return f.get(E)}v(Pe,"recordOnCard");async function be(e,{p:s,key:r},n,i,a){if(i.has(r)||n?.state!=="OPEN"||!n.isDraft||!x(n.statusCheckRollup))return;i.add(r);const c={call:"ready-draft",key:`${s.file}/${s.id}`,reason:`The board skips the draft and PR #${s.pr}'s CI passed.`,inputs:{repo:s.repo,pr:s.pr,isDraft:n.isDraft,checks:"passed"}};try{await me({repo:s.repo,pr:s.pr})}catch(f){b(G(e.dir),{...c,result:"failed",reason:`${c.reason} gh failed: ${f.message}`},{once:!0}),a.push(`${e.route}: could not mark ${r} ready for review (${f.message})`);return}b(G(e.dir),{...c,result:"marked ready"});try{await te(e.dir,`${s.file}/${s.id}`,"ci-passed",{pr:s.pr})}catch(f){a.push(`${e.route}: could not write ${s.file}/${s.id}'s ready line (${f.message})`)}}v(be,"readyGreenDraft");async function Ce(e,{p:s},r,n,i){const a=`${s.file}/${s.id}`,c=JSON.stringify([e.dir,a]);if(n.has(c)||r?.state!=="OPEN"||r.isDraft||r.reviewDecision!=="APPROVED"||r.mergeStateStatus!=="CLEAN"||!x(r.statusCheckRollup))return;n.add(c);const f={call:"auto-merge",key:a,reason:`Auto-merge is on and PR #${s.pr} is approved, green and clean.`,inputs:{repo:s.repo,pr:s.pr,reviewDecision:r.reviewDecision,mergeStateStatus:r.mergeStateStatus,checks:"passed"}};try{await ke(e.dir,a),b(G(e.dir),{...f,result:"merged"})}catch(o){b(G(e.dir),{...f,result:"not merged",reason:`${f.reason} ${o.message}`},{once:!0}),o.message.startsWith("Not merged:")||i.push(`${e.route}: could not merge ${a} (${o.message})`)}}v(Ce,"mergeApproved");async function Ie(e,s,r,n){const i=fe();if(!i)return;const a=new Map;for(const c of e){const f=Map.groupBy(c.prs,d=>JSON.stringify([d.file,d.id])),o=c.kind==="waves"?Q:c.dir;for(const{slug:d,itemFile:p}of c.files)for(const h of p.items){const u=new Set(h.linear||[]);c.tickets?.source==="linear"&&h.ticket&&u.add(h.ticket);const y=(f.get(JSON.stringify([d,h.id]))||[]).map(w=>s[r(w,c.me)]||null),R=y[$e(y)],E=h.lane==="done"||ne(y),m={merged:E,ready:R?.state==="OPEN"&&!R.isDraft,hasPR:y.length>0&&(E||y.some(w=>w?.state==="OPEN")),failed:y.some(w=>!w)},O=JSON.stringify([o,d,h.id]);for(const w of u){a.has(w)||a.set(w,{items:new Map,scoped:!1,created:!1,cards:[]});const D=a.get(w),S=D.items.get(O);D.items.set(O,{...m,failed:m.failed||S?.failed}),D.scoped||=y.some(Boolean),D.created||=h.ticket===w&&!!h.ticket_created,D.cards.push(`${c.route}:${d}/${h.id}`)}}}for(const[c,f]of a){const o=[...f.items.values()];if(!f.scoped||o.some(u=>u.failed))continue;const d=o.every(u=>u.merged)?"done":o.every(u=>u.merged||u.ready)?"review":o.some(u=>u.hasPR)?"started":null;if(!d)continue;const p={done:"Every card on it has merged.",review:"Every card on it has merged or has a PR ready for review.",started:"A card on it has a PR."}[d],h={call:"linear-move",key:c,reason:p,inputs:{target:d,cards:f.cards,items:o}};try{const u=await i.advanceIssue(c,d,{owner:i.saved.viewer.id,created:f.created});u&&(n.moved.push({id:c,state:u}),b(ie(),{...h,result:`moved to ${u}`}))}catch(u){b(ie(),{...h,result:"failed",reason:`${p} ${u.message}`},{once:!0}),n.failed.push(`linear ${c}: ${u.message}`)}}}v(Ie,"advanceLinear");async function Ue({now:e=new Date,settings:s,manifest:r}){const n=Z(V,{});n.sent||={};const i=pe(e,s,n.sent),a=[],c=[],f=[];if(!i.length)return{due:i,events:a,failed:c};const o=X(r);for(const d of o)try{a.push({route:d.route,id:await Y(d,"check-github",{reason:"morning"})})}catch(p){c.push(`${d.route}: could not add the morning check-github event (${p.message})`),f.push(d.route)}if(a.length||!o.length){for(const d of i)n.sent[d]=he(e);n.last={at:e.toISOString(),due:i,routes:a.map(d=>d.route),failed:f},ee(V,n)}return{due:i,events:a,failed:c}}v(Ue,"sendWakes");export{H as CHANGE_KINDS,oe as carry,Oe as changesBetween,xe as checkGitHub,Ue as sendWakes,Ee as summarize};
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Nt=Object.defineProperty;var l=(e,t)=>Nt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Gt from"node:net";import{spawn as Be,execFileSync as zt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as Kt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Vt,removeSubscription as Zt,removeSubscriptionById as Qt,sendAll as Xt,flushQueue as Ge,queueStatus as ze,saveSettings as Yt,readSettings as ee,migrateSettings as qt,setNotifications as en,readCodexShare as Ke,setCodexShare as tn,readClaudeModel as nn,setClaudeModel as sn,deliver as an}from"./push.mjs";import{ownerTracker as rn}from"./presence.mjs";import{POLL_MS as We,answerOf as on,expired as cn,pendingFor as Je,promptRecord as ln,pushOf as un,updatePrompts as G,shown as dn,pruneRemembered as ye,rememberRules as hn,rememberedAnswer as fn,rememberedFor as Ve}from"./prompts.mjs";import{hold as pn,heldUntil as mn,holdLine as gn,editQuiet as wn,checkEvery as Ze,nextCheckAt as bn,LOW_POINTS as yn}from"./schedule.mjs";import{capital as Sn,hubView as kn,settingsView as $n,flushView as _n,SNOOZE_REFUSED as xn,AWAKE_REFUSED as Tn}from"./views.mjs";import{caffeinate as vn,setKeepAwake as On,awakeUntil as Qe,awakeUnsupported as jn}from"./awake.mjs";import{endOf as Pn}from"./duration.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Xe,RESUME_STATE as Cn,sendToBoards as Ye,checkListening as An,recordHubCode as In,hubCode as Mn,runningHubCode as Hn,boardCode as qe,recordLinkFaults as Un,linksOf as Bn,hubLink as Fn,hubCodeState as Nn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as nt,readHubRules as ke,writeHubRules as Kn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ws,colorsVersion as at,destOf as bs,fileVersion as ys,pageOf as rt,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as te,setPauseAt as Ts,setKeepGoing as vs,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as ne}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as xe,initializeTries as As,fetchTryBranches as ot,pullNow as Is,pullMains as Ms}from"./try.mjs";import{serveFiles as Hs}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Fs}from"./self-restart.mjs";import{fallBackFromLinear as Ns,openBoardBuilders as Gs}from"../board/store.mjs";import{recordRefusal as zs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Vs,sharedRoutes as it,shareReach as Zs,routeOfPath as Qs,sharedSidebar as Xs,shareTracker as Ys}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as qs}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as ea,saveLinearKey as ta,savedClient as lt}from"./linear.mjs";import{SlackError as na,slackStatus as sa,removeSlackToken as aa,saveSlackToken as ra}from"./slack.mjs";import{sendReviews as oa}from"./review-send.mjs";const z=$.dirname(Kt(import.meta.url)),ia=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),K=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(K,"manifest.json"),ae=$.join(K,"logs"),ht=$.join(K,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[K,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=In();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ss();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=es(),ca=$s(),re=vn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),W=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function la(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?zt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(la,"killStale");function ua(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
2
+ var Bt=Object.defineProperty;var u=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ft from"node:net";import{spawn as Be,execFileSync as Nt}from"node:child_process";import b from"node:fs";import k from"node:path";import Fe from"node:os";import{fileURLToPath as Gt}from"node:url";import Ne from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ge,queueStatus as ze,saveSettings as Qt,readSettings as q,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ke,answerOf as an,expired as rn,pendingFor as We,promptRecord as on,pushOf as cn,updatePrompts as N,shown as ln,pruneRemembered as ye,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as Sn,flushView as kn,SNOOZE_REFUSED as $n}from"./views.mjs";import{caffeinate as _n,setKeepAwake as xn,awakeOn as Ze,awakeUnsupported as vn}from"./awake.mjs";import{endOf as Tn}from"./duration.mjs";import{checkGitHub as On,sendWakes as jn}from"./github.mjs";import{checkIncoming as Pn}from"./incoming.mjs";import{listBoards as D,isListening as Rn,isBoardSite as Se,listenState as Qe,RESUME_STATE as En,sendToBoards as Xe,checkListening as Dn,recordHubCode as Ln,hubCode as Cn,runningHubCode as An,boardCode as Ye,recordLinkFaults as In,linksOf as Mn,hubLink as Hn,hubCodeState as Un,readJSON as qe}from"./boards.mjs";import{liveSessions as et}from"./live.mjs";import{RuleError as Bn,applyRules as Fn,changed as tt,readHubRules as ke,writeHubRules as Nn}from"./rules.mjs";import{siteIcon as Gn}from"./icons.mjs";import{readNeeds as zn,sidebarSites as Jn,startRoute as Kn,hubHealth as Wn}from"./home.mjs";import{checkSessions as Vn,compactFullContexts as Zn,startingBoards as Qn}from"./sessions.mjs";import{repoService as Xn,repoLookupRoute as Yn}from"./repos.mjs";import{boardRepo as nt,changeBoardRepo as qn,migrateBoardRepos as es,migrateHubBoard as ts}from"./board-repo.mjs";import{requestBoard as ns,retryBoard as ss,dismissBoard as as,clonePercent as rs,settleStarts as os}from"./new-board.mjs";import{boardClosure as is,closeBoard as cs,closedBoards as ls,reopenBoard as us}from"./close-board.mjs";import{restartBoard as ds,restartable as hs}from"./restart-board.mjs";import{crossSite as fs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ps,colorsVersion as st,destOf as ms,fileVersion as gs,pageOf as at,shellHtml as ws}from"./shell.mjs";import{hubStream as bs}from"./stream.mjs";import{usageReader as ys,checkUsagePause as Ss,contextReading as ks}from"./usage.mjs";import{readUsageGate as ee,setPauseAt as $s,setKeepGoing as _s,setHubPause as xs,boardPausedUntil as vs}from"../shared/usage-gate.mjs";import{checkMachine as Ts}from"../shared/machine-gate.mjs";import{readiness as Os,readCodex as te}from"./readiness.mjs";import{BuildError as js,startBuild as Ps,stopTries as Rs,endUnsharedTries as Es,tryTick as xe,initializeTries as Ds,fetchTryBranches as rt,pullNow as Ls,pullMains as Cs}from"./try.mjs";import{serveFiles as As}from"./files.mjs";import{upgrader as Is,CHECK_MS as Ms}from"./upgrade.mjs";import{restarter as Hs}from"./self-restart.mjs";import{fallBackFromLinear as Us,migrateWaveNames as Bs,openBoardBuilders as Fs}from"../board/store.mjs";import{recordRefusal as Ns}from"../board/permissions.mjs";import{hubGate as ve}from"../board/on-host.mjs";import{pipeUpgrade as Gs,refuse as zs,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as ot,shareReach as Ws,routeOfPath as Vs,sharedSidebar as Zs,shareTracker as Qs}from"./share.mjs";import{restartRunner as it}from"../board/worktree.mjs";import{hubService as Xs}from"../shared/service.mjs";import{cloneRoot as Te}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ne,removeLinearKey as Ys,saveLinearKey as qs,savedClient as ct}from"./linear.mjs";import{SlackError as ea,slackStatus as ta,removeSlackToken as na,saveSlackToken as sa}from"./slack.mjs";import{sendReviews as aa}from"./review-send.mjs";const G=k.dirname(Gt(import.meta.url)),ra=k.join(G,"..","shared","colors.css"),lt=JSON.parse(b.readFileSync(k.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=k.join(Fe.homedir(),".claude","kickflip"),ut=k.join(z,"manifest.json"),se=k.join(z,"logs"),dt=k.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,se,dt])b.mkdirSync(e,{recursive:!0});try{const e=Ln();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await es();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await ts();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await Bs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}const M=Xn(),oa=ys(),ae=_n({log:c});ae.apply();let re={version:1,sites:{}};function g(){try{re=JSON.parse(b.readFileSync(ut,"utf8")),re.sites||={}}catch(e){e.code==="ENOENT"?re={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return re}u(g,"loadManifest");const j=new Map,H=new Set;let oe=!1;const ht=5e3,ie=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function ce(e){return k.join(dt,e+".pid")}u(ce,"pidFile");function ia(e,t){try{const n=Number(b.readFileSync(ce(e),"utf8")),s=n?Nt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ce(e))}catch{}}u(ia,"killStale");function ca(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(se,e+".log"),"a"),[i,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=Se(t)?qe(t):null;let h;try{h=Be(i,a,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
- `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(ua,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Gt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const J=new Set,da=3e4;async function ha(e){const t=Date.now()+da;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(ha,"serverAnswers");async function gt(){if(ie)return;wt&&bt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...W.keys()])U(R(e,t))!=="port"&&W.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&W.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&la(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=wt&&s?.child&&s.code&&!s.stopping&&!J.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Gs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ua(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Fs({root:ve(),files:Mn().files,restart:l(()=>ct("hub"),"restart"),log:c}),fa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=l(async()=>{try{for(const e of await Ns())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const pa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+pa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=rn(),St=l(()=>y.startedAt?bn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(kt,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Rn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Ms({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await oa({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=mn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let $t=null;function ma(){$t||=En({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}l(ma,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||J.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l(_t,"runSessionCheck"),await As(g());const ga=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=cn(n,{now:Date.now(),aliveSince:ga}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(xt,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function Tt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await zs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(Tt,"noteRefusal");let Q=null,vt=null;async function he(){return Q?vt??=Q.catch(()=>{}).then(()=>(vt=null,he())):(Q=(async()=>_s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}F.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ma(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(fa),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,Pt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(Pt,"newestMtime");function wa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(wa,"changedAt");const Rt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Rt(n.dir)?"up":"down":i==="port"?W.has(t)?W.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:wa(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Et=2e4,ba=1500;async function ya(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ba)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ya,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Et)&&pe.set(i.route,{at:Date.now(),needs:ya(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Je(null).length,"badgeTotal"),Ie=new Map;function Sa(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:a,links:Bn(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(Sa,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Dt(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=Sa(a),u=Nn(),_=new Map(a.map(o=>[o.route,xs(o.owner?.session)]).filter(([,o])=>o)),k=qn(i,t.getTime(),cs,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>js(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Vn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=pn(t,O);return{data:{code:u.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Fn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ca({codexShare:Ke(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Sn(gn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Ct(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Ct,"sendText");async function ka(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(ka,"shellPage");const At=Us({root:ve(),run:$a,log:c,pulled:l(()=>bt.restartNow(),"pulled")});function $a(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[$.join(ve(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const a=b.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l($a,"runUpdate"),At.check(),setInterval(()=>At.check(),Bs);const x=ks({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const _a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function v(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(v,"readJSONBody");const It=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
- <title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),xa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ta=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<yn}),"pollState");async function ge(e){await xt();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...kn({now:t,st:ze(t),poll:Ta(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Rs.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ke(ne()),claudeModel:nn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Je(null).map(dn),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:jn()},n=te({now:e}),s=n.manual==="hub"?Pn(n.resets_at,e):null;return{...$n({now:e,st:ze(e),rules:ke(),linear:se(),slack:sa(),awake:t,pausedUntil:s}),closed:hs()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function va(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Hn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=xa(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await us(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await fs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,J.has(r))return a(409,{error:"That board is already restarting."});J.add(r),await A.running;const o=await ps(r,{serverUp:ha,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{J.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:qs().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),kt(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,_t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ns(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await is(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await Is(d,{manifest:g()})):a(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",_a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(ia),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Vs(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=ws(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===ys(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Vt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Qt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Xt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await en(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:xn}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{On(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:Tn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{Os(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{tn(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{sn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?Ts(r.get("value")):vs(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(ea(),c("linear: key removed"),await yt()):c(`linear: key ${(await ta(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(aa(),c("slack: token removed")):c(`slack: token ${(await ra(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof na))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Yt(wn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=zn(ke(),r.rules,r.base??[]),nt(o.changes)&&Kn(o.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,_n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=fn(f,r);if(q)return{answer:q};const be=ln(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await Tt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),an(un(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await Tt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?Oa(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(va,"hubRoute");async function Oa(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||We,We));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=on(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),hn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(Oa,"promptRoute");const ja=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function Mt(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(Mt,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":Mt(t,n)}),e.end(Pa(t,n))}l(Ht,"sendDown");function Pa(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=Mt(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=Se(t)?Ye(t):null;let h;try{h=Be(i,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
+ `),b.closeSync(s),Object.assign(n,{child:null,key:ie(t),state:"crashed",exitCode:null,nextStart:Date.now()+ft(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ce(e),String(h.pid)),Object.assign(n,{child:h,key:ie(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const l=u((_,S)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(ce(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+ft(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(ca,"start");const ft=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;ue(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&ue(n,"SIGKILL")},ht)}u(le,"stop");const ue=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=u(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Re,"probe");let pt=!0;const B=new Set;let Ee=!1;const K=new Set,la=3e4;async function ua(e){const t=Date.now()+la;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ie(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}u(ua,"serverAnswers");async function mt(){if(oe)return;gt&&wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),le(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),le(t),j.delete(t)));for(const t of[...J.keys()])U(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;pt&&ia(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ie(n)){c(`${t} entry changed; restarting it`),le(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=gt&&s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?Ye(n):null;if(i&&i!==s.code){if(!(await Fs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),le(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ca(t,n)}pt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(mt,"reconcile");const gt=process.env.KICKFLIP_CODE_RESTARTS!=="off",wt=Hs({root:Te(),files:Cn().files,restart:u(()=>it("hub"),"restart"),log:c}),da=setInterval(()=>mt().catch(e=>c("reconcile failed:",e.message)),2e3);mt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=u(async()=>{try{for(const e of await Us())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const ha=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ha,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},de=sn(),fa=u(()=>$.startedAt?wn({last:new Date($.startedAt),...de.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function yt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(yt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=q();let s;try{s=await On({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}$.remaining=s.rate?.remaining??null,s.merged.length&&(await rt({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await rt({manifest:g(),log:c});try{const i=await Cs({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(r=>c("pull refresh failed:",r.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await Pn({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:r}of s.linear.moved)c(`linear: ${i} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await aa({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Dn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{In(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let St=null;function pa(){St||=jn({settings:q(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{St=null})}u(pa,"runWakes");const Ce=new Set,A={running:null};function kt(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Vn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Zn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}u(kt,"runSessionCheck"),await Ds(g());const ma=Date.now(),W=new Map;function V(e){e?(pe.delete(e),x.changed(e)):x.refresh()}u(V,"promptChanged");async function $t(){const{gone:e,rulesChanged:t}=await N(n=>{const s=rn(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:i}});for(const n of e)V(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u($t,"prunePrompts");const Ae=u(()=>({running:et(),boards:D(g())}),"promptContext");async function _t(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ns(nt(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(_t,"noteRefusal");let Z=null,xt=null;async function he(){return Z?xt??=Z.catch(()=>{}).then(()=>(xt=null,he())):(Z=(async()=>Ss({gate:ee(),resume:u(async e=>{const t=await Xe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Z=null}),Z)}u(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ts()}catch(e){c("machine check failed:",e.message)}de.check().then(yt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=fa().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),os({listening:u(e=>Rn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),ae.apply(),$t().catch(e=>c("prompt expiry failed:",e.message)).finally(kt)},15e3);function vt(){if(oe)return;oe=!0,clearInterval(da),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)ue(n,"SIGTERM");let e=!1;Rs().finally(()=>{e=!0});const t=Date.now()+ht;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),ue(n,"SIGKILL");process.exit(0)}},50)}u(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Tt=new Map;function Ot(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const r=k.join(e,i.name);if(i.isDirectory())n=Math.max(n,Ot(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Ot,"newestMtime");function ga(e,t){const n=Tt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Ot(t.dir);return Tt.set(e,{at:s,checked:Date.now()}),s}u(ga,"changedAt");const jt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),r=i==="files"?jt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:hs(n),changedAt:ga(t,n)||null,created:n.created||null,log:k.join(se,t+".log")}})}u(fe,"status");const F=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Pt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?zn(await t.json()):null}catch{return null}}u(ba,"fetchNeeds");async function Rt(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const r=pe.get(i.route);(t===!0||t?.has?.(i.route)||!r||Date.now()-r.at>=Pt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,r)=>[i.route,s[r]]))}u(Rt,"waitingCounts");const Et=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+We(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:et(),wakes:qe(En,{})};for(const i of e)try{const r=Qe(i,s);n.set(i.route,r);let p=Ie.get(i.route);(!p||p.listen!==r||Date.now()-p.at>=Pt)&&(p={at:Date.now(),listen:r,links:Mn(i,{listen:r})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(r){c(`links of ${i.route} failed:`,r.message),t.set(i.route,[])}return{links:t,listens:n}}u(ya,"boardLinks");async function Q({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Rt(n,{fresh:e}),i=g(),r=D(i),{links:p,listens:h}=ya(r),l=Un(),_=new Map(r.map(o=>[o.route,ks(o.owner?.session)]).filter(([,o])=>o)),S=Qn(i,t.getTime(),rs,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),y=new Set(S.map(o=>o.route)),m=new Map(r.filter(o=>vs(o.dir)).map(o=>[o.route,ee({dir:o.dir})])),w=Jn(n,s,new Set(r.map(o=>o.route)),h,_).filter(o=>!y.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=q(),C=hn(t,O);return{data:{code:l.running??null,version:lt,total:Et(s),start:Kn(w),sites:w,hub:Wn(w,p,Hn(l)),home:b.realpathSync(Fe.homedir()),starting:S,usage:(({start:o,...d})=>d)(oa({codexShare:Je(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(C,t))}:{state:"on",text:"On"},away:!de.state().present},links:p,rows:n,code:l}}u(Q,"sidebarState");function Dt(e,t,n,s,i){let r=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}u(Dt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await Q()).data):(await Q()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync(k.join(_e,i),"utf8"),s);return ws({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}u(Sa,"shellPage");const Lt=Is({root:Te(),run:ka,log:c,pulled:u(()=>wt.restartNow(),"pulled")});function ka(){const e=k.join(se,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[k.join(Te(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const r=b.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${i}: ${r}`),n(i===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}u(ka,"runUpdate"),Lt.check(),setInterval(()=>Lt.check(),Ms);const x=bs({targets:u(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await Q({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync(k.join(G,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(me,"iconUrl");const $a=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function T(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(T,"readJSONBody");const Ct=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <title>${F(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${st()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function ge(e){await $t();const t=new Date,{data:n,links:s,rows:i,code:r}=await Q();return{...yn({now:t,st:ze(t),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Os.rows(),code:r,version:lt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(te()),claudeModel:en(),codex:te(),awake:Ze(),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:We(null).map(ln),remembered:Ve(null)}}u(ge,"hubData");const I=u(async()=>{const e=new Date,t={on:Ze(),unsupported:vn()},n=ee({now:e}),s=n.manual==="hub"?Tn(n.resets_at,e):null;return{...Sn({now:e,st:ze(e),rules:ke(),linear:ne(),slack:ta(),awake:t,pausedUntil:s}),closed:ls()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function xa(e,t,n,s=null){const i=u((a,o,d,f={})=>{t.writeHead(a,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),r=u((a,o)=>{const d=An();return i(a,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=u(({route:a,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=_a(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await is(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let a;try{const o=await T(e);if(a=o.route,l==="/_hub/reopen-board"){const v=await us(a);return x.refresh(),r(200,v)}if(B.has(a))return r(409,{error:"That board is already closing."});B.add(a);const d=u(()=>B.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await cs(a,o.name);return x.refresh(),r(200,f)}catch(o){return r(o.status||400,{error:o.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,K.has(a))return r(409,{error:"That board is already restarting."});K.add(a),await A.running;const o=await ds(a,{serverUp:ua,portUp:Re,onStarting:u(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${o.session} resumed`),r(200,o)}catch(o){return a&&c(`restart /${a} failed: ${o.message}`),r(o.status||400,{error:o.message})}finally{K.delete(a),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await T(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=it("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:Xs().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/presence"&&e.method==="POST")return await T(e),yt(de.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),o=D(g()).find(d=>d.route===a);return o?(Ce.add(a),await A.running,kt(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Qe(o)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(l==="/_hub/repo"&&e.method==="GET")return Yn(n,{repos:M,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?nt(S[1]):await qn(S[1],await T(e),{repos:M});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await ns(a,{repos:M,changed:u(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await ss(y[1],{repos:M,changed:u(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return i(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const a=await as(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return i(a.status,"text/plain; charset=utf-8",a.message);throw a}if((l==="/_hub/build"||l==="/_hub/pull")&&e.method==="POST"){if(oe)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!a.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 r(403,{error:`${l==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Ls(d,{manifest:g()})):r(202,await Ps(d,{manifest:g(),log:c}))}catch(f){if(f instanceof js)return r(f.status,{error:f.message});throw f}}if(l==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return i(200,"image/png",b.readFileSync(k.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const a=n.searchParams.get("v")===st()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(ra),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync(k.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync(k.join(_e,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:a}=await Q({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(l==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};we.add(a),e.on("close",()=>we.delete(a)),e.on("close",X.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!Se(a))return r(404,{error:"That board is not open."});const o=Ks(qe(k.join(a.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await ve.node())?.dnsName||e.headers["x-forwarded-host"],v=X.people(O[1],d).map((L,Y)=>({...L,level:o[Y].level}));return r(200,{people:v,refused:X.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const a=ps(l.slice(10));if(!a){t.writeHead(404).end();return}const o=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===gs(a)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(a),{"cache-control":f}):Dt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(l==="/_hub/api/hub")return r(200,await ge(e));if(l==="/_hub/api/settings")return r(200,await I());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await T(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(o=>o.status>=200&&o.status<300).length,results:a})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),o=a.get("set"),d=o==="on"?{on:a.get("value")==="on"}:o==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let v;try{v=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{xn(a.get("keep_awake")==="on")}catch(o){if(o.status!==409)throw o;return h?r(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message)}return ae.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{xs(a.get("pause"))}catch(o){if(o.status!==400)throw o;return h?r(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?r(200,await I()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),te({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(o){if(o.status!==400)throw o;return h?r(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?$s(a.get("value")):_s(a.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(Ys(),c("linear: key removed"),await bt()):c(`linear: key ${(await qs(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(na(),c("slack: token removed")):c(`slack: token ${(await sa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ea))throw o;return h?r(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const a=ne();if(!a.set||a.refused)return r(200,a);const o=ct();try{const d=await o.teams();return r(200,{...ne(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ne(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const a=ct();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let o;try{o=await T(e,4*1024)}catch(f){return r(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 r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return r(f.status,{error:f.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(o.hold=a.get("value")||"off");try{Qt(mn(q(),o))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let a,o;try{a=await T(e,64*1024),o=Fn(ke(),a.rules,a.base??[]),tt(o.changes)&&Nn(o.rules)}catch(f){if(!(f instanceof Bn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!tt(o.changes))return r(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Xe(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:o.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,kn(a)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const a=await T(e);return r(200,await Le({flush:a.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await N(f=>{const v=Ae(),L=v.boards.find(He=>He.owner?.session&&He.owner.session===a.session_id)||null;ye(f,v)&&x.refresh();const Y=dn(f,a);if(Y)return{answer:Y};const be=on(a,{board:L,live:v.running?.get(a.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?r(200,{state:"answered",answer:o}):(V(d.route),await _t(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(o){if(o.status)throw o;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await _t(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(xa,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=u(()=>N(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(V(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await p();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Ke,Ke));if(await new Promise(y=>{const m=W.get(n)||new Set;W.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&W.get(n)===m&&W.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await p();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await N(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return ye(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...W.get(n)||[]])S();return V(l.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await N(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return p&&(V(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(va,"promptRoute");const Ta=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 At(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(At,"downWhy");function It(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":At(t,n)}),e.end(Oa(t,n))}u(It,"sendDown");function Oa(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=At(e,t),p=r==="code"||r==="starting"?"wait":"down",h=U(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${Ct(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${N(t.title||e)} isn't answering</h1>
12
- <p class="lede">${N(u)}</p>
11
+ <h1>${F(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${F(l)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${ja({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
14
+ <div class="group">${Ta({icon:Gn(e,t),title:F(t.title||e),sub:`<span class="st ${p}">${F(i[r])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(Pa,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Ys({file:$.join(K,"share-seen.json")}),we=new Set;function Ut(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Cs()}l(Ut,"endUnshared"),setInterval(Ut,2e3).unref();async function Bt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Zs({method:e.method,pathname:i,routes:a}):"other",h=Qs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Xs(_,a,u?.name||u?.login),"sidebar")}}}l(Bt,"access");const Ft=Ue.createServer((e,t)=>{Bt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ra(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ra(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);ka(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){va(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${It("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Rt(p.dir)){Ht(t,a,p);return}Hs(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ut),S.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,a,p)}),e.pipe(S)}l(Ra,"serve"),Ft.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Bt(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;Ks({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ft.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
16
+ </main></body></html>`}u(Oa,"downPage");const Me=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),X=Qs({file:k.join(z,"share-seen.json")}),we=new Set;function Mt(){const e=g().sites;for(const t of we){const n=ot(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Es()}u(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Ht(e){const t=await ve(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),r=ot(n,g().sites),p=r.size?Ws({method:e.method,pathname:i,routes:r}):"other",h=Vs(i);if(p==="other")return(!r.size||h)&&X.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};X.visit(n,s,h);const l=await ve.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>Zs(_,r,l?.name||l?.login),"sidebar")}}}u(Ht,"access");const Ut=Ue.createServer((e,t)=>{Ht(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?ja(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):zs(e,t,n)})});function ja(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&fs(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 i=new Set(Object.keys(g().sites));if(at({method:e.method,pathname:s.pathname,search:s.search,dest:ms(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);Sa(m,n).then(w=>Dt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Rt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Et(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){xa(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${Ct("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${F(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,l=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=at({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!jt(p.dir)){It(t,r,p);return}As(e,t,{dir:p.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:l,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}It(t,r,p)}),e.pipe(y)}u(ja,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ht(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],r=P(g().sites,i);if(!r?.port){t.destroy();return}const p=`/${i}`;Gs({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${ut})`));
package/hub/incoming.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var N=Object.defineProperty;var u=(n,r)=>N(n,"name",{value:r,configurable:!0});import d from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as P}from"./pr-report.mjs";import{cardPRs as C,storedPRs as E}from"../shared/card-prs.mjs";const k=u((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(e=>e.pr));return n.filter(e=>!e.draft&&!k(e.author,i)&&!s.has(e.number)).map(e=>{const f=new Set(e.files),m=[...new Set(r.filter(o=>o.files.some(l=>f.has(l))).map(o=>o.key))],t=e.requested.some(o=>k(o,i));return m.length||t?{number:e.number,title:e.title,author:e.author,url:e.url,head:e.head,touches:m,asked:t}:null}).filter(Boolean)}u(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;d.mkdirSync(S.dirname(i),{recursive:!0}),d.writeFileSync(s,JSON.stringify(r,null,2)+`
2
- `),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=P}){const s=F(y,{}),e=[],f=[];let m;for(const t of v(n).filter(o=>o.auto_review&&o.github))try{const o=t.githubLogin||(m??=await i()),l=await r.openPRs(t.github),$=new Map(l.map(h=>[h.number,h])),p=[];for(const{slug:h,wave:I}of t.waves)for(const b of I.items.filter(a=>a&&(a.lane!=="done"||E(a).some(c=>c.merged_at))))for(const{number:a}of C(b,t.github).filter(c=>c.repo===t.github)){const c=`${t.github}#${a}`;let g=$.get(a)?.files||s[c];if(!g){const w=await r.prFiles(t.github,a);g=w.files,w.state==="MERGED"&&(s[c]=g)}p.push({key:`${h}/${b.id}`,pr:a,files:g})}M(t.dir,{at:new Date().toISOString(),repo:t.github,prs:J({open:l,cards:p,me:o})}),e.push(t.route)}catch(o){f.push(`${t.route}: ${o.message}`)}return L(y,s),{checked:e,failed:f}}u(q,"checkIncoming");export{q as checkIncoming,J as incomingFor};
1
+ var I=Object.defineProperty;var u=(n,r)=>I(n,"name",{value:r,configurable:!0});import d from"node:fs";import w from"node:path";import{INCOMING_FILES as y,listBoards as N,readJSON as L,saveJSON as O}from"./boards.mjs";import{openPRs as R,prFiles as P,githubLogin as v}from"./pr-report.mjs";import{cardPRs as C,storedPRs as E}from"../shared/card-prs.mjs";const k=u((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(e=>e.pr));return n.filter(e=>!e.draft&&!k(e.author,i)&&!s.has(e.number)).map(e=>{const f=new Set(e.files),m=[...new Set(r.filter(o=>o.files.some(l=>f.has(l))).map(o=>o.key))],t=e.requested.some(o=>k(o,i));return m.length||t?{number:e.number,title:e.title,author:e.author,url:e.url,head:e.head,touches:m,asked:t}:null}).filter(Boolean)}u(J,"incomingFor");function M(n,r){const i=w.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;d.mkdirSync(w.dirname(i),{recursive:!0}),d.writeFileSync(s,JSON.stringify(r,null,2)+`
2
+ `),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:R,prFiles:P},login:i=v}){const s=L(y,{}),e=[],f=[];let m;for(const t of N(n).filter(o=>o.auto_review&&o.github))try{const o=t.githubLogin||(m??=await i()),l=await r.openPRs(t.github),$=new Map(l.map(h=>[h.number,h])),p=[];for(const{slug:h,itemFile:F}of t.files)for(const b of F.items.filter(a=>a&&(a.lane!=="done"||E(a).some(c=>c.merged_at))))for(const{number:a}of C(b,t.github).filter(c=>c.repo===t.github)){const c=`${t.github}#${a}`;let g=$.get(a)?.files||s[c];if(!g){const S=await r.prFiles(t.github,a);g=S.files,S.state==="MERGED"&&(s[c]=g)}p.push({key:`${h}/${b.id}`,pr:a,files:g})}M(t.dir,{at:new Date().toISOString(),repo:t.github,prs:J({open:l,cards:p,me:o})}),e.push(t.route)}catch(o){f.push(`${t.route}: ${o.message}`)}return O(y,s),{checked:e,failed:f}}u(q,"checkIncoming");export{q as checkIncoming,J as incomingFor};
package/hub/locks.mjs CHANGED
@@ -1 +1 @@
1
- var f=Object.defineProperty;var o=(r,c)=>f(r,"name",{value:c,configurable:!0});import t from"node:fs";import a from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:m=1e4,heartbeat:s=!1}={}){const e=`${r}.lock`;t.mkdirSync(a.dirname(r),{recursive:!0});const y=performance.now()+m;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(performance.now()>y)throw Object.assign(new Error(`${a.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=s?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}o(u,"withFileLock");export{u as withFileLock};
1
+ var f=Object.defineProperty;var a=(r,c)=>f(r,"name",{value:c,configurable:!0});import e from"node:fs";import i from"node:path";function d(r){let c;try{c=Number(e.readFileSync(i.join(r,"pid"),"utf8"))}catch{return!1}try{return process.kill(c,0),!1}catch(o){return o.code==="ESRCH"}}a(d,"holderGone");async function S(r,c,{staleAfter:o=3e4,timeout:s=1e4,heartbeat:l=!1}={}){const t=`${r}.lock`;e.mkdirSync(i.dirname(r),{recursive:!0});const m=performance.now()+s;for(;;)try{e.mkdirSync(t);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(d(t)||Date.now()-e.statSync(t).mtimeMs>o){e.rmSync(t,{recursive:!0});continue}}catch{}if(performance.now()>m)throw Object.assign(new Error(`${i.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(y=>setTimeout(y,100))}try{e.writeFileSync(i.join(t,"pid"),String(process.pid))}catch{}const u=l?setInterval(()=>{try{const n=new Date;e.utimesSync(t,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(u),e.rmSync(t,{recursive:!0,force:!0})}}a(S,"withFileLock");export{S as withFileLock};