@lovek4d/kickflip 0.7.1 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +25 -21
- package/SKILL.md +13 -9
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +1 -1
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +68 -50
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/onboarding.mjs +3 -3
- package/board/permissions.mjs +2 -2
- package/board/public/app-asks.js +2 -14
- package/board/public/app-banner.js +8 -3
- package/board/public/app-cards.js +7 -20
- package/board/public/app-core.js +2 -2
- package/board/public/app-lanes.js +7 -15
- package/board/public/app-reader.js +7 -3
- package/board/public/app-snapshot.js +1 -1
- package/board/public/app.js +1 -1
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +10 -12
- package/board/public/hub.css +0 -3
- package/board/public/linear-key.js +1 -1
- package/board/public/sidebar.css +1 -1
- package/board/public/style.css +42 -36
- package/board/public/ui-cards.js +33 -27
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-links.js +0 -1
- package/board/public/ui-order.js +1 -1
- package/board/public/ui-questions.js +8 -6
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +26 -22
- package/board/public/ui.js +15 -9
- package/board/public/view-hub-settings.js +25 -30
- package/board/public/view-hub.js +23 -5
- package/board/public/view-questions.js +1 -1
- package/board/public/view-settings.js +4 -4
- package/board/server.mjs +18 -18
- package/board/store.mjs +30 -27
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +15 -15
- package/hub/board-repo.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/linear.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/pr-report.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/repos.mjs +3 -3
- package/hub/review-send.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +8 -6
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/fake-gh.mjs +19 -4
- package/shared/gh.mjs +4 -0
- package/shared/owner-check.mjs +4 -3
- package/shared/service.mjs +12 -12
- package/starter/server.mjs +3 -3
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]);let
|
|
3
|
-
`)[0]})`,"why");try{if(
|
|
1
|
+
var x=Object.defineProperty;var r=(t,e)=>x(t,"name",{value:e,configurable:!0});import{runRepo as k,validateGithub as O}from"../hub/repos.mjs";import{savedClient as $}from"../hub/linear.mjs";const s=r(t=>{throw Object.assign(new Error(t),{status:400})},"refuse");function I(t){(!t||typeof t!="object"||Array.isArray(t))&&s("Tickets must name a source.");const e={none:["source"],github:["source","account"],linear:["source","workspace","team","project"]};return Object.hasOwn(e,t.source)||s("Ticket source must be none, github or linear."),Object.keys(t).some(n=>!e[t.source].includes(n))&&s("Unknown ticket source field."),t.source==="none"?null:t.source==="github"?t.account===void 0||t.account===""?{source:"github"}:((typeof t.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(t.account))&&s("The gh account must be a GitHub login."),{source:"github",account:t.account}):((typeof t.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(t.workspace))&&s("Linear needs a workspace."),(typeof t.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(t.team))&&s("Linear needs an uppercase team key."),(typeof t.project!="string"||!t.project.trim())&&s("Linear needs a project."),{source:"linear",workspace:t.workspace,team:t.team,project:t.project.trim()})}r(I,"ticketSource");const b=r(t=>{const e=t?Number(t[1]):null;return Number.isSafeInteger(e)?e:null},"numberOf"),y=r(t=>b(/^#?([1-9]\d*)$/.exec(String(t??""))),"issueNumber"),S=r(t=>t.source==="linear"?t.team:null,"keyOf");function p(t,e){if(!t)return null;const n=S(t),i=String(e??"");return n?i.startsWith(`${n}-`)?b(/^([1-9]\d*)$/.exec(i.slice(n.length+1))):null:b(/^#([1-9]\d*)$/.exec(i))}r(p,"ticketIn");function N(t,e,{repo:n}={}){const i=p(t,e);if(!i)return null;if(t.source==="github")return n?{id:`#${i}`,url:`https://github.com/${n}/issues/${i}`,source:"github"}:null;const u=`${t.team}-${i}`;return{id:u,url:`https://linear.app/${t.workspace}/issue/${u}`,source:"linear"}}r(N,"ticketBadge");async function j(t,e){if(!t.account)return e;const n=await e("gh",["auth","token","-u",t.account]);(n.status!==0||!n.stdout?.trim())&&s(`gh has no ${t.account} account; run gh auth login`);const i={...process.env,GH_TOKEN:n.stdout.trim()};return(u,g)=>e(u,g,{env:i,account:t.account})}r(j,"issueRunner");const L=r(async t=>{try{return await t()}catch(e){s(e.message)}},"linearCall");async function E(t,{ticket:e,title:n,body:i="",repo:u},{run:g=k,linearFetch:o}={}){if(t.source==="linear"){e!==void 0&&!p(t,e)&&s(`Ticket must match ${t.team}-<number>.`);const c=$({fetch:o});if(c||s("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub \u203A Integrations \u203A Linear, or pick another ticket source."),e===void 0)return{ticket:(await L(()=>c.createIssue({team:t.team,project:t.project,title:n,description:i}))).identifier,created:!0};const d=await L(()=>c.issue(e));return d||s(`Linear has no issue ${e}.`),d.team!==t.team&&s(`${e} is in Linear's ${d.team} team, not ${t.team}.`),{ticket:d.identifier,created:!1}}O(u);const f=e===void 0?null:y(e);e!==void 0&&!f&&s("GitHub ticket must be an issue number such as #41.");const h=f?["issue","view",String(f),"--repo",u,"--json","number,url"]:["issue","create","--repo",u,"--title",n,"--body",i],l=await(await j(t,g))("gh",h);l.status!==0&&s((l.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
|
+
`)[0]);let m;if(f){let c;try{c=JSON.parse(l.stdout)}catch{s("GitHub returned an invalid issue.")}c.number!==f&&s("GitHub returned a different issue."),m=c.url}else m=l.stdout?.trim();const a=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(m||"");return(!a||a[1].toLowerCase()!==u.toLowerCase()||f&&Number(a[2])!==f||!y(a[2]))&&s("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${a[2]}`,created:!f}}r(E,"resolveTicket");const C=r(t=>t.startsWith("#")?`GH-${t.slice(1)}`:t,"cardIdOf");function Z(t,e){const n=t.source==="github"?String(e??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(e??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(i=>p(t,i)))]}r(Z,"titleTickets");function R({start:t,done:e,handover:n}){return e?"done":t?"started":n?"review":null}r(R,"ticketTarget");const T={started:"In Progress",review:"In Review",done:"Done"};async function z(t,e,n,{repo:i,run:u=k,linearFetch:g}={}){const o=e.ticket;if(!t||!n||!o||!p(t,o)||t.source==="github"&&n!=="done")return null;if(t.source==="github"&&!i)return`ticket ${o}: not closed, the board has no GitHub repo`;const f=r(h=>`ticket ${o}: could not move to ${T[n]} (${String(h.message||h).split(`
|
|
3
|
+
`)[0]})`,"why");try{if(t.source==="linear"){const a=$({fetch:g});if(!a)return`ticket ${o}: not moved, Hub has no Linear key`;if(!e.ticket_created){const d=await a.issue(o);if(!d)return`ticket ${o}: not moved, Linear has no such issue`;if(d.assignee!==a.saved.viewer?.id)return`ticket ${o}: left as it is, not assigned to you`}const c=await a.moveIssue(o,n);return c?`ticket ${o}: moved to ${c}`:null}const h=o.slice(1),l=await j(t,u);if(!e.ticket_created){const a=await l("gh",["issue","view",h,"--repo",i,"--json","state,assignees"]);if(a.status!==0)throw new Error(a.stderr||"gh issue view failed");const c=await l("gh",["api","user","--jq",".login"]);if(c.status!==0)throw new Error(c.stderr||"gh api user failed");const d=JSON.parse(a.stdout);if(d.state!=="OPEN")return null;if(!(d.assignees||[]).some(w=>w.login?.toLowerCase()===c.stdout.trim().toLowerCase()))return`ticket ${o}: left as it is, not assigned to you`}const m=await l("gh",["issue","close",h,"--repo",i]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${o}: closed`}catch(h){return f(h)}}r(z,"moveTicket");export{C as cardIdOf,z as moveTicket,E as resolveTicket,N as ticketBadge,p as ticketIn,I as ticketSource,R as ticketTarget,Z as titleTickets};
|
package/board/worktree.mjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]}}s(c,"git");const
|
|
1
|
+
var Q=Object.defineProperty;var s=(t,e)=>Q(t,"name",{value:e,configurable:!0});import d from"node:fs";import p from"node:path";import T from"node:os";import{randomUUID as X}from"node:crypto";import{spawn as z,spawnSync as q}from"node:child_process";import{fileURLToPath as V}from"node:url";import{BoardError as Z,opensPRs as tt,settingsOf as et}from"./store.mjs";import{boardAt as nt,hubCode as rt,listBoards as ot,readJSON as it,readManifest as st,samePath as at}from"../hub/boards.mjs";import{boardRepo as ct}from"../hub/board-repo.mjs";import{cloneRoot as lt}from"../shared/repos.mjs";import{nodeBin as O,storedPath as x}from"../bin/install.mjs";import{hubService as ft}from"../shared/service.mjs";const dt=p.join(p.dirname(V(import.meta.url)),"..","bin","kickflip.mjs"),C=lt,L=s(()=>p.join(T.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),m=s(t=>{throw new Z(t)},"fail");function c(t,...e){const n=q("git",["-C",t,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:n.status===0,out:(n.stdout||"").trim(),err:(n.stderr||n.error?.message||"").trim().split(`
|
|
2
|
+
`)[0]}}s(c,"git");const W=s((t,e)=>c(t,"rev-parse","--short",e).out,"short");function A(t,{repo:e=C(),base:n="main",remote:r=!1,busy:o=[]}={}){const i=p.resolve(e);c(i,"rev-parse","--git-dir").ok||m(`--repo ${i} is not a git repo`),c(i,"check-ref-format","--branch",t).ok||m(`--worktree: "${t}" can't be a branch name`),c(i,"check-ref-format","--branch",n).ok||m("--worktree: invalid base branch");const a=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(i,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${a}`),c(i,"rev-parse","--verify","--quiet",a).ok||m(`--worktree: ${i} has no ${n} branch`);const u=p.join(L(),t),$=d.existsSync(u)?ut(i,u,t,{ref:a,base:n,busy:o}):null;c(i,"rev-parse","--verify","--quiet",`refs/heads/${t}`).ok&&m(`--worktree: branch ${t} already exists in ${i}`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(i,"worktree","add","--quiet","-b",t,u,a);return h.ok||m(`--worktree: git worktree add failed: ${h.err}`),{path:x(u),branch:t,repo:x(i),remade:$}}s(A,"makeWorktree");function ut(t,e,n,{ref:r,base:o,busy:i}){const a=s(l=>m(`--worktree: ${e} ${l}; land or remove it first`),"refuse");i.includes(x(e))&&a("already exists and another open builder record uses it");const u=d.realpathSync(e);c(t,"worktree","list","--porcelain").out.split(`
|
|
3
3
|
|
|
4
4
|
`).some(l=>{const f=l.split(`
|
|
5
5
|
`),v=f.find(b=>b.startsWith("worktree "))?.slice(9);try{return f.includes(`branch refs/heads/${n}`)&&d.realpathSync(v)===u}catch{return!1}})||a("already exists");const h=Number(c(t,"rev-list","--count",`${r}..refs/heads/${n}`).out),k=c(e,"status","--porcelain").out.split(`
|
|
6
|
-
`).filter(Boolean).length,g=s((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...h?[`${g(h,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&a(`holds ${w.join(" and ")}`);const S=c(t,"worktree","remove","--force",e);S.ok||m(`--worktree: could not remove the leftover ${e}: ${S.err}`);const y=c(t,"branch","--quiet","-D",n);return y.ok||m(`--worktree: could not delete the leftover branch ${n}: ${y.err}`),`Remade the leftover worktree ${
|
|
7
|
-
`).filter(Boolean);m(i.length?`merging ${e.branch} conflicts in ${i.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${o.err}`)}n.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(d.existsSync(e.path)){const o=c(t.path,"worktree","remove",e.path);o.ok||m(`merged, but could not remove ${e.path}: ${o.err}`),n.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&n.push(`branch deleted: ${e.branch}`),{lines:n}}s(
|
|
8
|
-
`).filter(w=>Number(w.trim().split(/\s+/)[0])===a.pid).map(w=>w.trim()),h.length&&process.kill(-a.pid,"SIGKILL");const g=s(()=>{a.stdout.destroy(),a.stderr.destroy()},"release");a.exitCode===null&&a.signalCode===null?a.once("exit",g):g()},e);return new Promise(g=>a.on("close",(w,S)=>{clearTimeout(k);const y=s(
|
|
6
|
+
`).filter(Boolean).length,g=s((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...h?[`${g(h,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&a(`holds ${w.join(" and ")}`);const S=c(t,"worktree","remove","--force",e);S.ok||m(`--worktree: could not remove the leftover ${e}: ${S.err}`);const y=c(t,"branch","--quiet","-D",n);return y.ok||m(`--worktree: could not delete the leftover branch ${n}: ${y.err}`),`Remade the leftover worktree ${x(e)} from ${o}: it held no commits or changes.`}s(ut,"removeLeftover");function P(t,{repo:e,manifest:n=st()}={}){const r=t?nt(t,n):null;if(!r)return{repo:e??C(),base:"main",remote:!1};const o=ct(r.route,n),i=e??(o.repo||m(`/${r.route} has no folder recorded for its session; rejoin it from its repo folder`)),a=tt(o,et(it(p.join(r.dir,"board.json"),{}),o));return{repo:i,base:o.branch||"main",remote:!o.local||a}}s(P,"builderRepo");const Pt=s((t,e,{repo:n,busy:r}={})=>A(e,{...P(t,{repo:n}),busy:r}),"boardWorktree"),Jt=s((t,e,{repo:n,base:r,busy:o})=>A(`${t}-${e}`,{repo:n,base:r,busy:o}),"helperWorktree");function Bt(t,e){const n=[];if(c(t.path,"merge-base","--is-ancestor",e.branch,"HEAD").ok)n.push(`${e.branch} is already merged`);else{const o=c(t.path,"merge","--no-edit",e.branch);if(!o.ok){const i=c(t.path,"diff","--name-only","--diff-filter=U").out.split(`
|
|
7
|
+
`).filter(Boolean);m(i.length?`merging ${e.branch} conflicts in ${i.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${o.err}`)}n.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(d.existsSync(e.path)){const o=c(t.path,"worktree","remove",e.path);o.ok||m(`merged, but could not remove ${e.path}: ${o.err}`),n.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&n.push(`branch deleted: ${e.branch}`),{lines:n}}s(Bt,"mergeHelper");function Kt(t,e,n){const{repo:r}=P(t),o=p.resolve(r);c(o,"check-ref-format","--branch",n).ok||m(`"${n}" can't be a branch name`);const i=`refs/remotes/origin/${n}`,a=c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`);a.ok||m(`could not fetch ${n} from origin: ${a.err}`);const u=p.join(L(),e);d.existsSync(u)&&m(`${x(u)} already exists; land or remove it first`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(o,"rev-parse","--verify","--quiet",`refs/heads/${n}`).ok?c(o,"worktree","add","--quiet",u,n):c(o,"worktree","add","--quiet","--track","-b",n,u,i);return h.ok||m(`git worktree add failed: ${h.err}`),{path:x(u),branch:n,repo:x(o),remade:null}}s(Kt,"prWorktree");function mt(t){const e=[],n=s(r=>{for(const o of d.readdirSync(r,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?n(p.join(r,o.name)):o.name.endsWith(".test.mjs")&&e.push(p.join(r,o.name)))},"walk");return n(t),e.sort()}s(mt,"testFiles");function J(t,{limit:e=900*1e3,testLimit:n=300*1e3}={}){const r=mt(t);if(!r.length)return Promise.resolve({ok:!0,pass:0,fail:0});const{NODE_TEST_CONTEXT:o,...i}=process.env,a=z(O(),["--test","--test-reporter=tap",`--test-timeout=${n}`,...r],{cwd:t,env:i,detached:!0});a.once("exit",()=>{try{process.kill(-a.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"&&g.code!=="EPERM")throw g}});let u="",$="";a.stdout.setEncoding("utf8").on("data",g=>{u+=g}),a.stderr.setEncoding("utf8").on("data",g=>{$+=g});let h=null;const k=setTimeout(()=>{h=q("ps",["-A","-o","pgid=,pid=,args="],{encoding:"utf8"}).stdout.split(`
|
|
8
|
+
`).filter(w=>Number(w.trim().split(/\s+/)[0])===a.pid).map(w=>w.trim()),h.length&&process.kill(-a.pid,"SIGKILL");const g=s(()=>{a.stdout.destroy(),a.stderr.destroy()},"release");a.exitCode===null&&a.signalCode===null?a.once("exit",g):g()},e);return new Promise(g=>a.on("close",(w,S)=>{clearTimeout(k);const y=s(j=>Number(u.match(new RegExp(`^# ${j} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:w===0,pass:y("pass"),fail:y("fail")};if(l.ok){g(l);return}const f=p.join(T.homedir(),".claude","kickflip","logs");try{d.mkdirSync(f,{recursive:!0});const j=p.join(f,`land-${X()}.tap`);d.writeFileSync(j,u+$+(h?`
|
|
9
9
|
# processes still running at the cutoff
|
|
10
10
|
${h.join(`
|
|
11
11
|
`)}
|
|
12
|
-
`:""),{mode:384}),l.log=
|
|
13
|
-
`)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${b?`: ${b}`:""}`,g(l)}))}s(
|
|
12
|
+
`:""),{mode:384}),l.log=j}catch(j){l.logError=`Could not save the full test output: ${j.message}`}const v=h?.filter(j=>Number(j.split(/\s+/)[1])!==a.pid);h&&(l.hung={after:ht(e),files:r.filter(j=>v.some(G=>G.includes(j))).map(j=>p.relative(t,j))}),l.failures=pt(u);const b=$.trim().split(`
|
|
13
|
+
`)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${b?`: ${b}`:""}`,g(l)}))}s(J,"nodeTests");const ht=s(t=>t%6e4?`${t/1e3} seconds`:`${t/6e4} minutes`,"duration");function pt(t){const e=t.split(`
|
|
14
14
|
`),n=[],r=[],o=s(i=>i.startsWith("'")?i.slice(1,-1).replaceAll("''","'"):i.startsWith('"')?JSON.parse(i):i,"scalar");for(let i=0;i<e.length;i++){const a=/^(\s*)# Subtest: (.*)$/.exec(e[i]);if(a){const l=a[1].length;for(;n.length&&n.at(-1).indent>=l;)n.pop();n.push({indent:l,name:a[2]})}const u=/^(\s*)not ok \d+ - (.*)$/.exec(e[i]);if(!u||/ # (?:TODO|SKIP)\b/i.test(u[2]))continue;const $=u[1].length,h=[...n.filter(l=>l.indent<$).map(l=>l.name),u[2]].join(" > "),k=[];for(let l=i+1;l<e.length&&e[l].startsWith(" ".repeat($+2));l++)k.push(e[l]);const g=s(l=>k.find(f=>f.startsWith(`${" ".repeat($+2)}${l}: `))?.slice($+l.length+4),"field"),w=g("location"),S=g("error");let y=[];if(S&&/^[|>]/.test(S)){const l=k.findIndex(f=>f.startsWith(`${" ".repeat($+2)}error: `))+1;for(let f=l;f<k.length&&k[f].startsWith(" ".repeat($+4));f++)y.push(k[f].slice($+4))}else S&&(y=o(S).split(`
|
|
15
15
|
`));if(y[0]==="test failed"){const l=[];let f=i-1;for(e[f]?.startsWith("# Subtest: ")&&f--;f>=0&&e[f].startsWith("# ")&&!e[f].startsWith("# Subtest: ");f--)l.unshift(e[f].slice(2));const v=l.findIndex(b=>/^\w*Error(?: \[[^\]]+\])?:/.test(b));v>=0&&(y=l.slice(v))}r.push(`${h}
|
|
16
16
|
${w?o(w):"file not reported by test runner"}${y.length?`
|
|
17
17
|
`+y.slice(0,3).map(l=>` ${l}`).join(`
|
|
18
|
-
`):""}`)}return r}s(
|
|
19
|
-
`)[0]}s(
|
|
20
|
-
`).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&m(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${p.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}s(
|
|
21
|
-
`).filter(Boolean);for(const i of o){const a=c(t,"log","-1","--format=%s",i).out;r&&!a.startsWith(`${r}: `)&&m(`land: commit ${i} "${a}" doesn't start with "${r}: "; reword it and land again`),/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${a}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}s(xt,"refuseBadMessages");function
|
|
18
|
+
`):""}`)}return r}s(pt,"tapFailures");function $t(t,e){let n;if(process.env.KICKFLIP_RESTART_CMD){try{n=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(n)||!n.length||!n.every(o=>typeof o=="string"))&&m("KICKFLIP_RESTART_CMD must be a JSON array of strings"),n=[...n,t,...e?[e]:[]]}else{if(t==="hub")return ft().restart();n=[O(),dt,"restart",e]}const r=q(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
|
|
19
|
+
`)[0]}s($t,"restartRunner");const N=s(()=>p.join(T.homedir(),".claude","kickflip"),"kickflipDir"),D=s(()=>p.join(N(),"landing"),"landing"),E=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"alive"),gt=5e3;async function B(t){const e=[];let n;try{for(const r of t)try{e.push({file:r,ino:d.statSync(r).ino,watcher:d.watch(r)})}catch{return}await new Promise(r=>{for(const{watcher:o}of e)o.on("change",r).on("error",r);n=setTimeout(r,gt),setImmediate(()=>setImmediate(()=>{for(const{file:o,ino:i}of e){let a=null;try{a=d.statSync(o).ino}catch{}a!==i&&r()}}))})}finally{clearTimeout(n);for(const{watcher:r}of e)r.close()}}s(B,"untilGone");const I=s(t=>p.join(N(),`${t}.queue`),"queueDir"),K=s(t=>Number(t.split("-")[2]),"ticketPid"),kt=s((t,e)=>{try{return JSON.parse(d.readFileSync(p.join(t,e),"utf8"))}catch{return{}}},"readTicket"),M=s((t,{first:e})=>`${e?`0${String(1e15-Date.parse(e)).padStart(16,"0")}`:"1"}-${t}`,"rank");function R(t){const e=I(t);let n=[];try{n=d.readdirSync(e).filter(r=>!r.endsWith(".tmp"))}catch{}return n.filter(r=>E(K(r))).map(r=>({ticket:r,...kt(e,r)})).sort((r,o)=>M(r.ticket,r)<M(o.ticket,o)?-1:1)}s(R,"queuedAsks");const _=s((t,e,n)=>{const r=p.join(t,`${e}.tmp`);d.writeFileSync(r,JSON.stringify(n)),d.renameSync(r,p.join(t,e))},"writeTicket");let yt=0;async function wt(t,e,n,{onWait:r,asked:o=Date.now(),site:i=null,key:a=null,first:u=null}={}){const $=p.join(N(),`${t}.lock`),h=I(t);d.mkdirSync(h,{recursive:!0});const k=`${String(o).padStart(15,"0")}-${String(yt++).padStart(6,"0")}-${process.pid}`;_(h,k,{card:e,site:i,key:a,first:u});let g=null;try{for(;;){const w=R(t),S=w.findIndex(v=>v.ticket===k),y=w.slice(0,S);let l=null;if(!y.length)try{d.writeFileSync($,JSON.stringify({card:e,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(v){if(v.code!=="EEXIST")throw v;try{l=JSON.parse(d.readFileSync($,"utf8"))}catch{}if(!(Number.isInteger(l?.pid)&&E(l.pid))){d.rmSync($,{force:!0});continue}}const f=l?.card??(y.length?`the ${t} ahead`:null);f&&f!==g&&(g=f,await r?.(f)),await B([y.length?p.join(h,y.at(-1).ticket):$,p.join(h,k)])}}finally{d.rmSync(p.join(h,k),{force:!0})}try{return await n()}finally{try{JSON.parse(d.readFileSync($,"utf8")).pid===process.pid&&d.rmSync($,{force:!0})}catch{}}}s(wt,"holdingQueuedLock");const St=s((t,e,n)=>wt("land",t,e,n),"holdingLandLock");function Mt(t,e,n){const r=I("land"),o=R("land").filter(i=>i.key===e&&i.site&&at(i.site,t));for(const{ticket:i,...a}of o)_(r,i,{...a,first:n});return o.length}s(Mt,"moveQueuedLand");function _t(){const t=vt();return[...t?[{card:t.card,running:!0,first:null}]:[],...R("land").map(({card:e,first:n})=>({card:e,running:!1,first:n??null}))]}s(_t,"landQueue");function vt(){try{const t=JSON.parse(d.readFileSync(p.join(N(),"land.lock"),"utf8"));return Number.isInteger(t?.pid)&&E(t.pid)?t:null}catch{return null}}s(vt,"landLock");async function Ht(t,e,{merge:n=!1}={}){const r=D();d.mkdirSync(r,{recursive:!0});const o=p.join(r,encodeURIComponent(t));let i=!1;for(;;){if(i&&!d.existsSync(o))return{joined:!0};try{d.writeFileSync(o,JSON.stringify({pid:process.pid,at:new Date().toISOString(),...n?{merge:n}:{}}),{flag:"wx"});break}catch(a){if(a.code!=="EEXIST")throw a;let u=null;try{u=JSON.parse(d.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(u?.pid)&&E(u.pid))){d.rmSync(o,{force:!0});continue}i=!0,await B([o])}}try{return await e()}finally{d.rmSync(o,{force:!0})}}s(Ht,"joiningLand");function Ut(t){try{const e=JSON.parse(d.readFileSync(p.join(D(),encodeURIComponent(t)),"utf8"));return Number.isInteger(e?.pid)&&E(e.pid)?e:null}catch{return null}}s(Ut,"runningLand");function Yt(t,e){const n=p.join(D(),encodeURIComponent(t));try{e.includes(JSON.parse(d.readFileSync(n,"utf8")).pid)&&d.rmSync(n,{force:!0})}catch{}let r=[];try{r=d.readdirSync(I("land"))}catch{}for(const o of r.filter(i=>e.includes(K(i))))d.rmSync(p.join(I("land"),o),{force:!0})}s(Yt,"clearStoppedLand");function jt(t,e,n){const o=c(t,"diff","--name-only","--no-renames","--diff-filter=A",e,n,"--","docs/adr").out.split(`
|
|
20
|
+
`).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&m(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${p.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}s(jt,"refuseNumberedAdr");function xt(t,e,n,r){const o=c(t,"log","--format=%h",`${e}..${n}`).out.split(`
|
|
21
|
+
`).filter(Boolean);for(const i of o){const a=c(t,"log","-1","--format=%s",i).out;r&&!a.startsWith(`${r}: `)&&m(`land: commit ${i} "${a}" doesn't start with "${r}: "; reword it and land again`),/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${a}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}s(xt,"refuseBadMessages");function Gt(t,e){const n=c(t,"fetch","--quiet","--no-tags","origin",`+refs/heads/${e}:refs/remotes/origin/${e}`);if(n.ok||m(`could not fetch ${e} from origin into ${t}: ${n.err}`),c(t,"symbolic-ref","--quiet","--short","HEAD").out!==e)return;const r=c(t,"merge","--quiet","--ff-only",`refs/remotes/origin/${e}`);r.ok||m(`${e} in ${t} doesn't fast-forward to origin's (${r.err}); bring it level with origin/${e} and land again`)}s(Gt,"syncBase");async function bt(t,{say:e=console.log,runTests:n=J,outcome:r}){const o=await n(t),i=o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[];o.hung&&m([`tests hung past ${o.hung.after} in ${o.hung.files.join(", ")||"the test runner"}; ${r}`,...i].join(`
|
|
22
22
|
`)),o.ok||m([`tests failed in ${t} (${o.fail} failing); ${r}`,...o.failures?.length?o.failures:[o.error||"test runner failed without test diagnostics"],...i].join(`
|
|
23
|
-
`)),e(`tests passed: ${o.pass}`)}s(
|
|
24
|
-
`).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${h.err}`),m(`rebasing ${e} onto ${r} conflicts in ${k.join(", ")}; resolve it in ${t} and land again`)}i(`rebased ${e} onto ${r} (${
|
|
25
|
-
`).filter(h=>h&&!h.includes(" "));u.length&&m(`${e} has a commit with an empty subject (${u[0]}); reword it in ${t} and land again`),
|
|
26
|
-
`).filter(Boolean),k=c(n,"merge","--quiet","--ff-only",o);k.ok||m(`could not fast-forward ${r} in ${n}: ${k.err}`),i(`${r} fast-forwarded to ${
|
|
23
|
+
`)),e(`tests passed: ${o.pass}`)}s(bt,"passTests");async function H({path:t,branch:e,repo:n,base:r,ref:o},{say:i=console.log,runTests:a=J}={}){if(d.existsSync(t)||m(`no worktree at ${t}`),U(n,r),c(t,"status","--porcelain").out&&m(`${t} has uncommitted changes; commit them there first`),Number(c(n,"rev-list","--count",`${r}..${e}`).out)===0&&m(`${e} has no commits beyond ${r}; nothing to land`),!c(n,"merge-base","--is-ancestor",r,e).ok){const h=c(t,"rebase","--quiet",r);if(!h.ok){const k=c(t,"diff","--name-only","--diff-filter=U").out.split(`
|
|
24
|
+
`).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${h.err}`),m(`rebasing ${e} onto ${r} conflicts in ${k.join(", ")}; resolve it in ${t} and land again`)}i(`rebased ${e} onto ${r} (${W(n,r)})`)}const u=c(n,"log","--format=%h %s",`${r}..${e}`).out.split(`
|
|
25
|
+
`).filter(h=>h&&!h.includes(" "));u.length&&m(`${e} has a commit with an empty subject (${u[0]}); reword it in ${t} and land again`),jt(n,r,e),xt(n,r,e,o);const $=c(n,"rev-parse",e).out;return c(n,"rev-parse","--verify","--quiet",F(e)).out===$?(i(`${e} was tested on ${r} as it is now; not testing it again`),$):(await bt(t,{say:i,runTests:a,outcome:"nothing landed"}),c(n,"update-ref",F(e),$),$)}s(H,"prepareLand");const F=s(t=>`refs/kickflip/tested/${t}`,"testedRef");function U(t,e){const n=c(t,"symbolic-ref","--quiet","--short","HEAD").out;n!==e&&m(`${t} is on ${n||"a detached HEAD"}, not ${e}; switch it to ${e} (git -C ${t} switch ${e}) and land again`)}s(U,"onBase");async function Qt(t,{hold:e=!1,onWait:n,sync:r=s(()=>{},"sync"),queue:o={},...i}={}){const{say:a=console.log}=i,u=Date.now();for(;;){const $=await H(t,i);if(e||await St(t.ref??t.branch,()=>(r(),Y(t,$,i)),{onWait:n,asked:u,...o}))return;a(`${t.base} moved while ${t.branch} was tested; testing it on ${t.base} again`)}}s(Qt,"landInTurn");async function Xt(t,{hold:e=!1,...n}={}){const r=await H(t,n);e||Y(t,r,n)||m(`${t.base} moved while ${t.branch} was tested; land again`)}s(Xt,"land");function Y({path:t,branch:e,repo:n,base:r},o,{say:i=console.log,restart:a=$t,boards:u=s(()=>ot(),"boards"),clone:$=C()}={}){if(U(n,r),c(n,"rev-parse",e).out!==o||!c(n,"merge-base","--is-ancestor",r,o).ok)return!1;const h=c(n,"diff","--name-only",r,e).out.split(`
|
|
26
|
+
`).filter(Boolean),k=c(n,"merge","--quiet","--ff-only",o);k.ok||m(`could not fast-forward ${r} in ${n}: ${k.err}`),i(`${r} fast-forwarded to ${W(n,r)} in ${n}`);const g=[],w=d.realpathSync(n)===d.realpathSync($)?h.filter(f=>!/\.test\.mjs$/.test(f)):[],S=new Set(w.length?rt(p.join(n,"hub","hub.mjs")).files:[]);if(w.some(f=>f.startsWith("hub/")||S.has(f))){const f=a("hub");f?g.push(`the hub didn't restart (${f})`):i("hub restarted, with every site")}else if(w.some(f=>f.startsWith("board/")))for(const f of u()){const v=a("board",f.route);v?g.push(`/${f.route} didn't restart (${v})`):i(`board restarted: ${f.route}`)}const y=c(n,"worktree","remove",t);y.ok?i(`worktree removed: ${t}`):g.push(`could not remove ${t} (${y.err})`);const l=c(n,"branch","--quiet","-d",e);return l.ok?i(`branch deleted: ${e}`):g.push(`could not delete branch ${e} (${l.err})`),c(n,"update-ref","-d",F(e)),g.length&&m(`landed, but ${g.join("; ")}`),!0}s(Y,"landPrepared");export{Pt as boardWorktree,P as builderRepo,Yt as clearStoppedLand,c as git,Jt as helperWorktree,St as holdingLandLock,Ht as joiningLand,Xt as land,Qt as landInTurn,vt as landLock,Y as landPrepared,_t as landQueue,A as makeWorktree,Bt as mergeHelper,Mt as moveQueuedLand,J as nodeTests,bt as passTests,Kt as prWorktree,H as prepareLand,$t as restartRunner,Ut as runningLand,C as skillRepo,Gt as syncBase,L as worktreesDir};
|
package/hub/board-repo.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var S=Object.defineProperty;var d=(e,r)=>S(e,"name",{value:r,configurable:!0});import{readManifest as h,withManifestLock as f,writeManifest as g,isBoardSite as l,listBoards as v,appendEvent as C,buildsLocally as E}from"./boards.mjs";import{routeRecord as m}from"./routes.mjs";import{repoService as H,validateBranch as
|
|
1
|
+
var S=Object.defineProperty;var d=(e,r)=>S(e,"name",{value:r,configurable:!0});import{readManifest as h,withManifestLock as f,writeManifest as g,isBoardSite as l,listBoards as v,appendEvent as C,buildsLocally as E}from"./boards.mjs";import{routeRecord as m}from"./routes.mjs";import{repoService as H,validateBranch as L,runRepo as O,githubFromOrigin as j,defaultBranch as M}from"./repos.mjs";import{repoInput as D}from"./new-board.mjs";import{boardConfig as F}from"../board/store.mjs";import{storedPath as p}from"../bin/install.mjs";import{cloneRoot as I}from"../shared/repos.mjs";import{githubFromOrigin as Y}from"./repos.mjs";const w=d((e,r=400)=>{throw Object.assign(new Error(e),{status:r})},"refuse");function B(e,r=h()){const o=m(r.sites,e);l(o)||w("Board not found.",404);const a=E(o);return{github:o.github||"",...o.remote?{remote:o.remote}:{},branch:o.branch||"",repo:p(o.owner?.repo||""),...a?{local:!0}:{},...o.hub===!0?{hub:!0}:{}}}d(B,"boardRepo");const K=d(e=>e.local?`refs/heads/${e.branch||"main"}`:`refs/remotes/origin/${e.branch||"main"}`,"mainRefOf");async function Q(e,r,{repos:o=H(),event:a=C}={}){B(e),(!r||typeof r!="object"||Array.isArray(r)||Object.keys(r).some(i=>!["github","remote","branch"].includes(i)))&&w("Send only github or remote, and branch.");const{github:n="",remote:t}=D(r),s=L(r.branch),u=m(h().sites,e).dir;t&&(await F(u)).tickets?.source==="github"&&w("This board files its cards as GitHub issues. Switch Tickets to None or Linear first.",409);const c=p(await o.ensure(n||t,s));return f(async()=>{const i=h(),A=B(e,i),b=m(i.sites,e);Object.assign(b,{github:n,remote:t,branch:s,updated:new Date().toISOString()}),delete b.local,n||delete b.github,t||delete b.remote,b.owner={...b.owner,repo:c},await g(i);const R=v(i).find(x=>x.route===e),y={github:n,...t?{remote:t}:{},branch:s,repo:c},k=await a(R,"repo-changed",{...y,previous:A});return{...y,id:k}})}d(Q,"changeBoardRepo");async function U({run:e=O}={}){return f(async()=>{const r=h(),o=Array.isArray(r.migrated)?r.migrated:[];if(o.includes("board-github")||!Object.keys(r.sites||{}).length)return[];const a=[];for(const[n,t]of Object.entries(r.sites||{})){if(!l(t)||t.github)continue;const s=t.owner?.repo||t.dir,u=await e("git",["-C",s,"remote","get-url","origin"]),c=u.status===0?j(u.stdout):null;if(!c)continue;t.github=c;const i=t.branch||await M(s,e);i&&(t.branch=i),a.push(n)}return r.migrated=[...o,"board-github"],await g(r),a})}d(U,"migrateBoardRepos");async function V({run:e=O,clone:r=I()}={}){return f(async()=>{const o=h(),a=Array.isArray(o.migrated)?o.migrated:[];if(a.includes("hub-local")||!Object.keys(o.sites||{}).length)return null;const n=Object.entries(o.sites||{}).find(([,t])=>t?.hub===!0&&l(t)&&t.local!==!0);if(n){const[,t]=n,s=t.owner?.repo||p(r),u=await e("git",["-C",s,"remote","get-url","origin"]),c=t.github||(u.status===0?j(u.stdout):null),i=await e("git",["-C",s,"symbolic-ref","--quiet","--short","HEAD"]);Object.assign(t,{local:!0,...c?{github:c}:{},...!t.branch&&i.status===0&&i.stdout.trim()?{branch:i.stdout.trim()}:{},owner:{...t.owner,repo:s}})}return o.migrated=[...a,"hub-local"],await g(o),n?n[0]:null})}d(V,"migrateHubBoard");export{B as boardRepo,Q as changeBoardRepo,Y as githubFromOrigin,K as mainRefOf,U as migrateBoardRepos,V as migrateHubBoard};
|
package/hub/github.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import
|
|
1
|
+
var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import K from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as W,fetchPRs as ue,markReady as me,reportOf as pe}from"./pr-report.mjs";import{wakesDue as ge,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:[],url:e.url??null,mergedAt:e.mergedAt??null,headRefName:e.headRefName??null,headRefOid:e.headRefOid??null,updatedAt:e.updatedAt??null,checks:e.checks??null,reviews:e.reviews??{},codexOpen:Number(e.codexOpen)||0}}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
|
-
`))&&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=
|
|
4
|
-
`),F.renameSync(n,r)}v(Re,"writeChecked");async function
|
|
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=K.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;F.mkdirSync(K.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 We({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 g=new Map(c.map((t,p)=>[P(t),o?o.results[p]:null])),h=o?.login??null,u=v((t,p)=>JSON.stringify([P(t),p]),"fetchKey"),y=new Map,R=new Map,E=new Map;for(const t of i){t.me=t.githubLogin||h;for(const p of t.prs){const k=P(p);y.set(k,p);const N=E.get(k)||new Set;if(N.add(t.me),E.set(k,N),!t.me)continue;const C=u(p,t.me),l=R.get(C);(!l||p.since<l.since)&&R.set(C,{...p,me:t.me})}}const m={},O={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),S=new Set;for(const[t,p]of R){const k=g.get(P(p));if(!k?.view){m[t]=null,k?.error&&!S.has(k.error)&&(S.add(k.error),a.push(k.error));continue}O[t]=pe(k,p);const N=Ee(O[t]);m[t]=N,w.has(P(p))||w.set(P(p),N)}const _=[],B=[],q=new Set,ae=new Map,x=new Set,ce=new Set,de=new Set;for(const t of i){const p=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(z=>D[$].includes(z)||L.includes(z))),{p:l,key:$,recipient:A,previous:M,next:I,changes:L}});for(const l of p){const $=O[u(l.p,t.me)],A=p.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})&&x.add(t.route),t.skipDraft&&await be(t,l,g.get(l.key)?.view,ce,a),t.autoMerge&&await Ce(t,l,g.get(l.key)?.view,de,a)}const k=p.filter(l=>l.changes.length);let N=p.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 p)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,p]of y){const k=w.get(t);!k||q.has(t)||(D[t].length&&J.push({repo:p.repo,pr:p.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:[...x],linear:j,rate:o?.rate??null}}v(We,"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}`,g=f.get(o.id);try{await se(e.dir,d,{pr:g}),await te(e.dir,d,"pr-found",{pr:g,branch:o.id}),n.push({file:i,id:o.id,pr:g,changes:["linked"]})}catch(h){r.push(`${e.route}: could not link PR #${g} 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}`,g=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||g.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}`}),g.add(O)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}g.size&&((c.threaded[r]||={})[n]=[...g]);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||!W(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"||!W(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:g}of c.files)for(const h of g.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 g={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:g,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:`${g} ${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=ge(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(g){c.push(`${d.route}: could not add the morning check-github event (${g.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,We 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 Bt=Object.defineProperty;var
|
|
2
|
+
var Bt=Object.defineProperty;var l=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Ft}from"node:child_process";import b from"node:fs";import k from"node:path";import Ne from"node:os";import{fileURLToPath as Gt}from"node:url";import Fe from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Wt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ge,queueStatus as ze,saveSettings as Qt,readSettings as F,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as sn}from"./presence.mjs";import{POLL_MS as Ke,answerOf as an,expired as rn,pendingFor as We,promptRecord as on,pushOf as cn,updatePrompts as G,shown as ln,pruneRemembered as ye,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as gn,nextCheckAt as wn,quietAt as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as vn,awakeOn as Tn,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{nodeBin as Pn}from"../bin/install.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Ze,RESUME_STATE as Cn,sendToBoards as Qe,checkListening as An,recordHubCode as In,hubCode as Mn,runningHubCode as Hn,boardCode as Xe,recordLinkFaults as Un,linksOf as Bn,hubLink as Nn,hubCodeState as Fn,readJSON as Ye}from"./boards.mjs";import{liveSessions as qe}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as et,readHubRules as ke,writeHubRules as Jn}from"./rules.mjs";import{siteIcon as Kn}from"./icons.mjs";import{readNeeds as Wn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as tt,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ws,colorsVersion as nt,destOf as bs,fileVersion as ys,pageOf as st,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as ee,setPauseAt as vs,setKeepGoing as Ts,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as te}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as xe,initializeTries as As,fetchTryBranches as at,pullNow as Is,pullMains as Ms}from"./try.mjs";import{serveFiles as Hs}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Ns}from"./self-restart.mjs";import{fallBackFromLinear as Fs,migrateWaveNames as Gs,openBoardBuilders as zs}from"../board/store.mjs";import{recordRefusal as Js}from"../board/permissions.mjs";import{hubGate as ve}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Vs}from"./front-door.mjs";import{shareEntries as Zs,sharedRoutes as rt,shareReach as Qs,routeOfPath as Xs,sharedSidebar as Ys,shareTracker as qs}from"./share.mjs";import{restartRunner as ot}from"../board/worktree.mjs";import{hubService as ea}from"../shared/service.mjs";import{cloneRoot as Te}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as ne,removeLinearKey as ta,saveLinearKey as na,savedClient as it}from"./linear.mjs";import{SlackError as sa,slackStatus as aa,removeSlackToken as ra,saveSlackToken as oa}from"./slack.mjs";import{sendReviews as ia}from"./review-send.mjs";const z=k.dirname(Gt(import.meta.url)),ca=k.join(z,"..","shared","colors.css"),ct=JSON.parse(b.readFileSync(k.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),J=k.join(Ne.homedir(),".claude","kickflip"),lt=k.join(J,"manifest.json"),se=k.join(J,"logs"),ut=k.join(J,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[J,se,ut])b.mkdirSync(e,{recursive:!0});try{const e=In();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ss();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await Gs();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}const M=es(),la=$s(),ae=xn({log:c});ae.apply();let re={version:1,sites:{}};function g(){try{re=JSON.parse(b.readFileSync(lt,"utf8")),re.sites||={}}catch(e){e.code==="ENOENT"?re={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return re}l(g,"loadManifest");const j=new Map,H=new Set;let oe=!1;const dt=5e3,ie=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),K=new Map;function ce(e){return k.join(ut,e+".pid")}l(ce,"pidFile");function ua(e,t){try{const n=Number(b.readFileSync(ce(e),"utf8")),s=n?Ft("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ce(e))}catch{}}l(ua,"killStale");function da(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(se,e+".log"),"a"),[o,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=Se(t)?
|
|
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=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[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(
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${
|
|
8
|
-
<title>${
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${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">')}
|
|
4
|
+
`),n.code=Se(t)?Xe(t):null;let h;try{h=Be(o,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
|
|
5
|
+
`),b.closeSync(s),Object.assign(n,{child:null,key:ie(t),state:"crashed",exitCode:null,nextStart:Date.now()+ht(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ce(e),String(h.pid)),Object.assign(n,{child:h,key:ie(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,S)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(ce(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??S,nextStart:Date.now()+ht(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??S})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(da,"start");const ht=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;ue(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&ue(n,"SIGKILL")},dt)}l(le,"stop");const ue=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let ft=!0;const B=new Set;let Ee=!1;const W=new Set,ha=3e4;async function fa(e){const t=Date.now()+ha;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ie(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}l(fa,"serverAnswers");async function pt(){if(oe)return;mt&>.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),le(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),le(t),j.delete(t)));for(const t of[...K.keys()])U(P(e,t))!=="port"&&K.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&K.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;ft&&ua(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ie(n)){c(`${t} entry changed; restarting it`),le(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=mt&&s?.child&&s.code&&!s.stopping&&!W.has(t)&&!B.has(t)?Xe(n):null;if(o&&o!==s.code){if(!(await zs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),le(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&da(t,n)}ft=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(pt,"reconcile");const mt=process.env.KICKFLIP_CODE_RESTARTS!=="off",gt=Ns({root:Te(),files:Mn().files,restart:l(()=>ot("hub"),"restart"),log:c}),pa=setInterval(()=>pt().catch(e=>c("reconcile failed:",e.message)),2e3);pt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const wt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await wt();const ma=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ma,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},de=sn(),bt=l(()=>bn(new Date,F()),"quietNow"),ga=l(()=>$.startedAt?wn({last:new Date($.startedAt),...de.state(),quiet:bt(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function yt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&!bt()&&Le().catch(n=>c("poll failed:",n.message))}l(yt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=F();let s;try{s=await Rn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=s.rate?.remaining??null,s.merged.length){try{await at({manifest:g(),log:c,routes:s.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await at({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Ms({manifest:g()});o.length&&xe({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ia({boards:D(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Ge();o&&(s.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=s,s})().finally(()=>{$.running=null}),$.running}l(Le,"runPoll");let St=null;function wa(){St||=En({settings:F(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{St=null})}l(wa,"runWakes");const Ce=new Set,A={running:null};function kt(){if(A.running||B.size||W.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l(kt,"runSessionCheck"),await As(g());const ba=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function $t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=rn(n,{now:Date.now(),aliveSince:ba}),o=ye(n,Ae());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l($t,"prunePrompts");const Ae=l(()=>({running:qe(),boards:D(g())}),"promptContext");async function _t(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Js(tt(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(_t,"noteRefusal");let Q=null,xt=null;async function he(){return Q?xt??=Q.catch(()=>{}).then(()=>(xt=null,he())):(Q=(async()=>_s({gate:ee(),resume:l(async e=>{const t=await Qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}de.check().then(yt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ga().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),wa(),ae.apply(),$t().catch(e=>c("prompt expiry failed:",e.message)).finally(kt)},15e3);function vt(){if(oe)return;oe=!0,clearInterval(pa),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)ue(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+dt;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),ue(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Tt=new Map;function Ot(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.join(e,o.name);if(o.isDirectory())n=Math.max(n,Ot(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}l(Ot,"newestMtime");function ya(e,t){const n=Tt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Ot(t.dir);return Tt.set(e,{at:s,checked:Date.now()}),s}l(ya,"changedAt");const jt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=U(n),r=o==="files"?jt(n.dir)?"up":"down":o==="port"?K.has(t)?K.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:ya(t,n)||null,created:n.created||null,log:k.join(se,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),pe=new Map,Pt=2e4,Sa=1500;async function ka(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Sa)});return t.ok?Wn(await t.json()):null}catch{return null}}l(ka,"fetchNeeds");async function Rt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=pe.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Pt)&&pe.set(o.route,{at:Date.now(),needs:ka(o.port)})}const s=await Promise.all(n.map(o=>pe.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}l(Rt,"waitingCounts");const Et=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+We(null).length,"badgeTotal"),Ie=new Map;function $a(e){const t=new Map,n=new Map,s={running:qe(),wakes:Ye(Cn,{})};for(const o of e)try{const r=Ze(o,s);n.set(o.route,r);let p=Ie.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Pt)&&(p={at:Date.now(),listen:r,links:Bn(o,{listen:r})},Ie.set(o.route,p)),t.set(o.route,p.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}l($a,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Rt(n,{fresh:e}),o=g(),r=D(o),{links:p,listens:h}=$a(r),u=Fn(),_=new Map(r.map(i=>[i.route,xs(i.owner?.session)]).filter(([,i])=>i)),S=qn(o,t.getTime(),cs,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(S.map(i=>i.route)),m=new Map(r.filter(i=>js(i.dir)).map(i=>[i.route,ee({dir:i.dir})])),w=Vn(n,s,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=F(),C=hn(t,O);return{data:{code:u.running??null,version:ct,total:Et(s),start:Zn(w),sites:w,hub:Qn(w,p,Nn(u)),home:b.realpathSync(Ne.homedir()),starting:S,usage:(({start:i,...d})=>d)(la({codexShare:Je(te())})),pause:ee(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(pn(C,t))}:{state:"on",text:"On"},away:!de.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Dt(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}l(Dt,"sendText");async function _a(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Fe.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Fe.runInContext(b.readFileSync(k.join(_e,o),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(_a,"shellPage");const Lt=Us({root:Te(),run:xa,log:c,pulled:l(()=>gt.restartNow(),"pulled")});function xa(){const e=k.join(se,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
|
+
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(Pn(),[k.join(Te(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}l(xa,"runUpdate"),Lt.check(),setInterval(()=>Lt.check(),Bs);const x=ks({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync(k.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const va=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function T(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(T,"readJSONBody");const Ct=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
|
+
<title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${nt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ta=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function ge(e){await $t();const t=new Date,{data:n,links:s,rows:o,code:r}=await X();return{...Sn({now:t,st:ze(t),sidebar:n,rows:o,links:s,rules:ke(),integrations:await Rs.rows(),code:r,version:ct,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(te()),claudeModel:en(),codex:te(),awake:{on:Tn(),unsupported:On()},remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:We(null).map(ln),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t=ee({now:e}),n=t.manual==="hub"?jn(t.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:ne(),slack:aa(),pausedUntil:n}),closed:hs()}},"settingsData"),R=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Oa(e,t,n,s=null){const o=l((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=l((a,i)=>{const d=Hn();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=Ta(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return r(200,await us(_[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let a;try{const i=await T(e);if(a=i.route,u==="/_hub/reopen-board"){const v=await fs(a);return x.refresh(),r(200,v)}if(B.has(a))return r(409,{error:"That board is already closing."});B.add(a);const d=l(()=>B.delete(a),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,W.has(a))return r(409,{error:"That board is already restarting."});W.add(a),await A.running;const i=await ps(a,{serverUp:fa,portUp:Re,onStarting:l(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{W.delete(a),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await T(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=ot("hub");a&&(Ee=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:ea().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/presence"&&e.method==="POST")return await T(e),yt(de.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),i=D(g()).find(d=>d.route===a);return i?(Ce.add(a),await A.running,kt(),await A.running,Ie.clear(),x.changed(a),r(200,{route:a,listen:Ze(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return r(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return r(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:M,manifest:g(),sendJSON:r});const S=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(S&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?tt(S[1]):await ns(S[1],await T(e),{repos:M});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(a,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(y&&e.method==="POST"){if(!$e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(y[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const a=await is(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(oe)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return r(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?r(200,await Is(d,{manifest:g()})):r(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return r(f.status,{error:f.message});throw f}}if(u==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return o(200,"application/manifest+json",va());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return o(200,"image/png",b.readFileSync(k.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const a=n.searchParams.get("v")===nt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ca),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:a}=await X({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};we.add(a),e.on("close",()=>we.delete(a)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const a=P(g().sites,O[1]);if(!Se(a))return r(404,{error:"That board is not open."});const i=Zs(Ye(k.join(a.dir,"board.json"),{})),d=i.map(L=>L.login),f=(await ve.node())?.dnsName||e.headers["x-forwarded-host"],v=Y.people(O[1],d).map((L,q)=>({...L,level:i[q].level}));return r(200,{people:v,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const a=ws(u.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],f=n.searchParams.get("v")===ys(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":f}):Dt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(u==="/_hub/api/hub")return r(200,await ge(e));if(u==="/_hub/api/settings")return r(200,await I());if(u==="/_hub/push/key")return r(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await T(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):o(400,"text/plain; charset=utf-8",f);let v;try{v=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:_n}):R(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?r(200,await I()):R(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{vn(a.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return ae.apply(),h?r(200,await I()):R(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{Os(a.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await he(),h?r(200,await I()):R(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{qt(a.get("value"),te({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{tn(a.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?vs(a.get("value")):Ts(a.get("value")),await he()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await ge(e)):R(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ta(),c("linear: key removed"),await wt()):c(`linear: key ${(await na(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?(ra(),c("slack: token removed")):c(`slack: token ${(await oa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof sa))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await I()):R(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const a=ne();if(!a.set||a.refused)return r(200,a);const i=it();try{const d=await i.teams();return r(200,{...ne(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...ne(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const a=it();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await T(e,4*1024)}catch(f){return r(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await a.createProject(i.team,d))}catch(f){if(!(f instanceof Oe))throw f;return r(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(F(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await I()):R(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await T(e,64*1024),i=zn(ke(),a.rules,a.base??[]),et(i.changes)&&Jn(i.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!et(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,$n(a)):R(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const a=await T(e);return r(200,await Le({flush:a.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await G(f=>{const v=Ae(),L=v.boards.find(He=>He.owner?.session&&He.owner.session===a.session_id)||null;ye(f,v)&&x.refresh();const q=dn(f,a);if(q)return{answer:q};const be=on(a,{board:L,live:v.running?.get(a.session_id)});return f.prompts[be.id]=be,{rec:be}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await _t(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await _t(a)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?ja(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}l(Oa,"hubRoute");async function ja(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),u=await p();if(!u)return r(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ke,Ke));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const S=await p();return S?S.answer?h(S):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await G(S=>{const y=P(S.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,p);if(w)return{status:400,error:w};y.answer=m;const O=Ae();return ye(S,O),un(S,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const S of[...V.get(n)||[]])S();return Z(u.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=P(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}l(ja,"promptRoute");const Pa=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function At(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(At,"downWhy");function It(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":At(t,n)}),e.end(Ra(t,n))}l(It,"sendDown");function Ra(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},r=At(e,t),p=r==="code"||r==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${Ct(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
|
-
<h1>${
|
|
12
|
-
<p class="lede">${
|
|
11
|
+
<h1>${N(t.title||e)} isn't answering</h1>
|
|
12
|
+
<p class="lede">${N(u)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${Pa({icon:Kn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(o[r])}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}
|
|
16
|
+
</main></body></html>`}l(Ra,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=qs({file:k.join(J,"share-seen.json")}),we=new Set;function Mt(){const e=g().sites;for(const t of we){const n=rt(t.login,e);[...t.routes].some(([s,o])=>n.get(s)!==o)&&(we.delete(t),t.res.destroy())}Cs()}l(Mt,"endUnshared"),setInterval(Mt,2e3).unref();async function Ht(e){const t=await ve(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Me(e),r=rt(n,g().sites),p=r.size?Qs({method:e.method,pathname:o,routes:r}):"other",h=Xs(o);if(p==="other")return(!r.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await ve.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:l(_=>Ys(_,r,u?.name||u?.login),"sidebar")}}}l(Ht,"access");const Ut=Ue.createServer((e,t)=>{Ht(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ea(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ea(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(st({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);_a(m,n).then(w=>Dt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Rt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Et(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Oa(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${Ct("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=st({pathname:s.pathname,dest:"document",sites:o})==="shell";if(U(p)==="files"){if(!jt(p.dir)){It(t,r,p);return}Hs(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const S={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete S["x-kickflip-shared"],n&&(S["x-kickflip-shared"]=n.login);const y=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:S},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Mt),y.on("error",()=>{if(t.headersSent){t.destroy();return}It(t,r,p)}),e.pipe(y)}l(Ea,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ht(e);if(s!=="owner"){Vs(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Me(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const p=`/${o}`;Ks({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${lt})`));
|