@lovek4d/kickflip 0.7.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +10 -6
- package/SKILL.md +19 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/board/bin/board.mjs +35 -13
- package/board/commit-message.mjs +6 -0
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +5 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/public/app-header.js +23 -0
- package/board/public/app-needs.js +1 -1
- package/board/public/app-reader.js +6 -2
- package/board/public/app.html +2 -0
- package/board/public/app.js +2 -0
- package/board/public/attach.js +12 -0
- package/board/public/col-resize.js +73 -0
- package/board/public/dialogs.js +3 -0
- package/board/public/prototype-kit.js +1 -1
- package/board/public/prototype.html +1 -0
- package/board/public/sidebar.css +8 -2
- package/board/public/style.css +33 -11
- package/board/public/ui-header.js +2 -2
- package/board/public/ui-sidebar.js +11 -3
- package/board/public/ui-threads.js +1 -0
- package/board/public/view-hub.js +2 -2
- package/board/public/view-questions.js +2 -1
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +18 -18
- package/board/store.mjs +32 -31
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +21 -19
- package/hooks-plugin/hooks/usage-gate.mjs +1 -3
- package/hub/boards.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/fake-gh.mjs +9 -4
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]);let m;if(f){let
|
|
3
|
-
`)[0]})`,"why");try{if(t.source==="linear"){const
|
|
1
|
+
var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import{runRepo as $,validateGithub as x}from"../hub/repos.mjs";import{savedClient as y}from"../hub/linear.mjs";const r=s(t=>{throw Object.assign(new Error(t),{status:400})},"refuse");function N(t){(!t||typeof t!="object"||Array.isArray(t))&&r("Tickets must name a source.");const e={none:["source"],github:["source","account"],linear:["source","workspace","team","project"]};return Object.hasOwn(e,t.source)||r("Ticket source must be none, github or linear."),Object.keys(t).some(n=>!e[t.source].includes(n))&&r("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))&&r("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))&&r("Linear needs a workspace."),(typeof t.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(t.team))&&r("Linear needs an uppercase team key."),(typeof t.project!="string"||!t.project.trim())&&r("Linear needs a project."),{source:"linear",workspace:t.workspace,team:t.team,project:t.project.trim()})}s(N,"ticketSource");const b=s(t=>{const e=t?Number(t[1]):null;return Number.isSafeInteger(e)?e:null},"numberOf"),w=s(t=>b(/^(?:#|GH-)?([1-9]\d*)$/.exec(String(t??""))),"issueNumber"),H=s(t=>t.source==="linear"?t.team:null,"keyOf");function p(t,e){if(!t)return null;const n=H(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))}s(p,"ticketIn");function I(t,e){if(!t)return null;if(t.source==="linear")return p(t,e)?String(e):null;const n=w(e);return n&&/^(?:#|GH-)/.test(String(e))?`#${n}`:null}s(I,"ticketNamed");function E(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 c=`${t.team}-${i}`;return{id:c,url:`https://linear.app/${t.workspace}/issue/${c}`,source:"linear"}}s(E,"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())&&r(`gh has no ${t.account} account; run gh auth login`);const i={...process.env,GH_TOKEN:n.stdout.trim()};return(c,g)=>e(c,g,{env:i,account:t.account})}s(j,"issueRunner");const L=s(async t=>{try{return await t()}catch(e){r(e.message)}},"linearCall");async function C(t,{ticket:e,title:n,body:i="",repo:c},{run:g=$,linearFetch:o}={}){if(t.source==="linear"){e!==void 0&&!p(t,e)&&r(`Ticket must match ${t.team}-<number>.`);const a=y({fetch:o});if(a||r("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(()=>a.createIssue({team:t.team,project:t.project,title:n,description:i}))).identifier,created:!0};const d=await L(()=>a.issue(e));return d||r(`Linear has no issue ${e}.`),d.team!==t.team&&r(`${e} is in Linear's ${d.team} team, not ${t.team}.`),{ticket:d.identifier,created:!1}}x(c);const f=e===void 0?null:w(e);e!==void 0&&!f&&r("GitHub ticket must be an issue number such as #41.");const l=f?["issue","view",String(f),"--repo",c,"--json","number,url"]:["issue","create","--repo",c,"--title",n,"--body",i],h=await(await j(t,g))("gh",l);h.status!==0&&r((h.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
|
+
`)[0]);let m;if(f){let a;try{a=JSON.parse(h.stdout)}catch{r("GitHub returned an invalid issue.")}a.number!==f&&r("GitHub returned a different issue."),m=a.url}else m=h.stdout?.trim();const u=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(m||"");return(!u||u[1].toLowerCase()!==c.toLowerCase()||f&&Number(u[2])!==f||!w(u[2]))&&r("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${u[2]}`,created:!f}}s(C,"resolveTicket");const Z=s(t=>t.startsWith("#")?`GH-${t.slice(1)}`:t,"cardIdOf");function R(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)))]}s(R,"titleTickets");function z({start:t,done:e,handover:n}){return e?"done":t?"started":n?"review":null}s(z,"ticketTarget");const G={started:"In Progress",review:"In Review",done:"Done"};async function _(t,e,n,{repo:i,run:c=$,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=s(l=>`ticket ${o}: could not move to ${G[n]} (${String(l.message||l).split(`
|
|
3
|
+
`)[0]})`,"why");try{if(t.source==="linear"){const u=y({fetch:g});if(!u)return`ticket ${o}: not moved, Hub has no Linear key`;if(!e.ticket_created){const d=await u.issue(o);if(!d)return`ticket ${o}: not moved, Linear has no such issue`;if(d.assignee!==u.saved.viewer?.id)return`ticket ${o}: left as it is, not assigned to you`}const a=await u.moveIssue(o,n);return a?`ticket ${o}: moved to ${a}`:null}const l=o.slice(1),h=await j(t,c);if(!e.ticket_created){const u=await h("gh",["issue","view",l,"--repo",i,"--json","state,assignees"]);if(u.status!==0)throw new Error(u.stderr||"gh issue view failed");const a=await h("gh",["api","user","--jq",".login"]);if(a.status!==0)throw new Error(a.stderr||"gh api user failed");const d=JSON.parse(u.stdout);if(d.state!=="OPEN")return null;if(!(d.assignees||[]).some(k=>k.login?.toLowerCase()===a.stdout.trim().toLowerCase()))return`ticket ${o}: left as it is, not assigned to you`}const m=await h("gh",["issue","close",l,"--repo",i]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${o}: closed`}catch(l){return f(l)}}s(_,"moveTicket");export{Z as cardIdOf,_ as moveTicket,C as resolveTicket,E as ticketBadge,p as ticketIn,I as ticketNamed,N as ticketSource,z as ticketTarget,R as titleTickets};
|
package/board/worktree.mjs
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
var Q=Object.defineProperty;var
|
|
2
|
-
`)[0]}}
|
|
1
|
+
var Q=Object.defineProperty;var a=(t,e)=>Q(t,"name",{value:e,configurable:!0});import u from"node:fs";import $ from"node:path";import R from"node:os";import{randomUUID as X}from"node:crypto";import{spawn as z,spawnSync as C}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 j}from"../bin/install.mjs";import{hubService as ft}from"../shared/service.mjs";import{subjectFault as dt}from"./commit-message.mjs";const ut=$.join($.dirname(V(import.meta.url)),"..","bin","kickflip.mjs"),T=lt,L=a(()=>$.join(R.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),m=a(t=>{throw new Z(t)},"fail");function c(t,...e){const n=C("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]}}a(c,"git");const W=a((t,e)=>c(t,"rev-parse","--short",e).out,"short");function A(t,{repo:e=T(),base:n="main",remote:r=!1,busy:o=[]}={}){const i=$.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 s=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(i,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${s}`),c(i,"rev-parse","--verify","--quiet",s).ok||m(`--worktree: ${i} has no ${n} branch`);const d=$.join(L(),t),h=u.existsSync(d)?mt(i,d,t,{ref:s,base:n,busy:o}):null;c(i,"rev-parse","--verify","--quiet",`refs/heads/${t}`).ok&&m(`--worktree: branch ${t} already exists in ${i}`),u.mkdirSync($.dirname(d),{recursive:!0});const p=c(i,"worktree","add","--quiet","-b",t,d,s);return p.ok||m(`--worktree: git worktree add failed: ${p.err}`),{path:j(d),branch:t,repo:j(i),remade:h}}a(A,"makeWorktree");function mt(t,e,n,{ref:r,base:o,busy:i}){const s=a(l=>m(`--worktree: ${e} ${l}; land or remove it first`),"refuse");i.includes(j(e))&&s("already exists and another open builder record uses it");const d=u.realpathSync(e);c(t,"worktree","list","--porcelain").out.split(`
|
|
3
3
|
|
|
4
4
|
`).some(l=>{const f=l.split(`
|
|
5
|
-
`),v=f.find(
|
|
6
|
-
`).filter(Boolean).length,g=
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
`),v=f.find(x=>x.startsWith("worktree "))?.slice(9);try{return f.includes(`branch refs/heads/${n}`)&&u.realpathSync(v)===d}catch{return!1}})||s("already exists");const p=Number(c(t,"rev-list","--count",`${r}..refs/heads/${n}`).out),k=c(e,"status","--porcelain").out.split(`
|
|
6
|
+
`).filter(Boolean).length,g=a((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...p?[`${g(p,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&s(`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 ${j(e)} from ${o}: it held no commits or changes.`}a(mt,"removeLeftover");function P(t,{repo:e,manifest:n=st()}={}){const r=t?nt(t,n):null;if(!r)return{repo:e??T(),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`)),s=tt(o,et(it($.join(r.dir,"board.json"),{}),o));return{repo:i,base:o.branch||"main",remote:!o.local||s}}a(P,"builderRepo");const Bt=a((t,e,{repo:n,busy:r}={})=>A(e,{...P(t,{repo:n}),busy:r}),"boardWorktree"),Kt=a((t,e,{repo:n,base:r,busy:o})=>A(`${t}-${e}`,{repo:n,base:r,busy:o}),"helperWorktree");function Mt(t,e,n){const r=[];if(c(t.path,"merge-base","--is-ancestor",e.branch,"HEAD").ok)r.push(`${e.branch} is already merged`);else{const i=c(t.path,"merge","--no-edit","-m",`chore: merge the ${e.branch} helper branch
|
|
7
|
+
|
|
8
|
+
Refs: ${n}`,e.branch);if(!i.ok){const s=c(t.path,"diff","--name-only","--diff-filter=U").out.split(`
|
|
9
|
+
`).filter(Boolean);m(s.length?`merging ${e.branch} conflicts in ${s.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${i.err}`)}r.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(u.existsSync(e.path)){const i=c(t.path,"worktree","remove",e.path);i.ok||m(`merged, but could not remove ${e.path}: ${i.err}`),r.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&r.push(`branch deleted: ${e.branch}`),{lines:r}}a(Mt,"mergeHelper");function _t(t,e,n){const{repo:r}=P(t),o=$.resolve(r);c(o,"check-ref-format","--branch",n).ok||m(`"${n}" can't be a branch name`);const i=`refs/remotes/origin/${n}`,s=c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`);s.ok||m(`could not fetch ${n} from origin: ${s.err}`);const d=$.join(L(),e);u.existsSync(d)&&m(`${j(d)} already exists; land or remove it first`),u.mkdirSync($.dirname(d),{recursive:!0});const p=c(o,"rev-parse","--verify","--quiet",`refs/heads/${n}`).ok?c(o,"worktree","add","--quiet",d,n):c(o,"worktree","add","--quiet","--track","-b",n,d,i);return p.ok||m(`git worktree add failed: ${p.err}`),{path:j(d),branch:n,repo:j(o),remade:null}}a(_t,"prWorktree");function ht(t){const e=[],n=a(r=>{for(const o of u.readdirSync(r,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?n($.join(r,o.name)):o.name.endsWith(".test.mjs")&&e.push($.join(r,o.name)))},"walk");return n(t),e.sort()}a(ht,"testFiles");function J(t,{limit:e=900*1e3,testLimit:n=300*1e3}={}){const r=ht(t);if(!r.length)return Promise.resolve({ok:!0,pass:0,fail:0});const{NODE_TEST_CONTEXT:o,...i}=process.env,s=z(O(),["--test","--test-reporter=tap",`--test-timeout=${n}`,...r],{cwd:t,env:i,detached:!0});s.once("exit",()=>{try{process.kill(-s.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"&&g.code!=="EPERM")throw g}});let d="",h="";s.stdout.setEncoding("utf8").on("data",g=>{d+=g}),s.stderr.setEncoding("utf8").on("data",g=>{h+=g});let p=null;const k=setTimeout(()=>{p=C("ps",["-A","-o","pgid=,pid=,args="],{encoding:"utf8"}).stdout.split(`
|
|
10
|
+
`).filter(w=>Number(w.trim().split(/\s+/)[0])===s.pid).map(w=>w.trim()),p.length&&process.kill(-s.pid,"SIGKILL");const g=a(()=>{s.stdout.destroy(),s.stderr.destroy()},"release");s.exitCode===null&&s.signalCode===null?s.once("exit",g):g()},e);return new Promise(g=>s.on("close",(w,S)=>{clearTimeout(k);const y=a(b=>Number(d.match(new RegExp(`^# ${b} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:w===0,pass:y("pass"),fail:y("fail")};if(l.ok){g(l);return}const f=$.join(R.homedir(),".claude","kickflip","logs");try{u.mkdirSync(f,{recursive:!0});const b=$.join(f,`land-${X()}.tap`);u.writeFileSync(b,d+h+(p?`
|
|
9
11
|
# processes still running at the cutoff
|
|
10
|
-
${
|
|
12
|
+
${p.join(`
|
|
11
13
|
`)}
|
|
12
|
-
`:""),{mode:384}),l.log=
|
|
13
|
-
`)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${
|
|
14
|
-
`),n=[],r=[],o=
|
|
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(
|
|
14
|
+
`:""),{mode:384}),l.log=b}catch(b){l.logError=`Could not save the full test output: ${b.message}`}const v=p?.filter(b=>Number(b.split(/\s+/)[1])!==s.pid);p&&(l.hung={after:pt(e),files:r.filter(b=>v.some(G=>G.includes(b))).map(b=>$.relative(t,b))}),l.failures=$t(d);const x=h.trim().split(`
|
|
15
|
+
`)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${x?`: ${x}`:""}`,g(l)}))}a(J,"nodeTests");const pt=a(t=>t%6e4?`${t/1e3} seconds`:`${t/6e4} minutes`,"duration");function $t(t){const e=t.split(`
|
|
16
|
+
`),n=[],r=[],o=a(i=>i.startsWith("'")?i.slice(1,-1).replaceAll("''","'"):i.startsWith('"')?JSON.parse(i):i,"scalar");for(let i=0;i<e.length;i++){const s=/^(\s*)# Subtest: (.*)$/.exec(e[i]);if(s){const l=s[1].length;for(;n.length&&n.at(-1).indent>=l;)n.pop();n.push({indent:l,name:s[2]})}const d=/^(\s*)not ok \d+ - (.*)$/.exec(e[i]);if(!d||/ # (?:TODO|SKIP)\b/i.test(d[2]))continue;const h=d[1].length,p=[...n.filter(l=>l.indent<h).map(l=>l.name),d[2]].join(" > "),k=[];for(let l=i+1;l<e.length&&e[l].startsWith(" ".repeat(h+2));l++)k.push(e[l]);const g=a(l=>k.find(f=>f.startsWith(`${" ".repeat(h+2)}${l}: `))?.slice(h+l.length+4),"field"),w=g("location"),S=g("error");let y=[];if(S&&/^[|>]/.test(S)){const l=k.findIndex(f=>f.startsWith(`${" ".repeat(h+2)}error: `))+1;for(let f=l;f<k.length&&k[f].startsWith(" ".repeat(h+4));f++)y.push(k[f].slice(h+4))}else S&&(y=o(S).split(`
|
|
17
|
+
`));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(x=>/^\w*Error(?: \[[^\]]+\])?:/.test(x));v>=0&&(y=l.slice(v))}r.push(`${p}
|
|
16
18
|
${w?o(w):"file not reported by test runner"}${y.length?`
|
|
17
19
|
`+y.slice(0,3).map(l=>` ${l}`).join(`
|
|
18
|
-
`):""}`)}return r}
|
|
19
|
-
`)[0]}
|
|
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-${
|
|
21
|
-
`).filter(Boolean);for(const i of o){const
|
|
20
|
+
`):""}`)}return r}a($t,"tapFailures");function gt(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(),ut,"restart",e]}const r=C(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
|
|
21
|
+
`)[0]}a(gt,"restartRunner");const N=a(()=>$.join(R.homedir(),".claude","kickflip"),"kickflipDir"),q=a(()=>$.join(N(),"landing"),"landing"),E=a(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"alive"),kt=5e3;async function B(t){const e=[];let n;try{for(const r of t)try{e.push({file:r,ino:u.statSync(r).ino,watcher:u.watch(r)})}catch{return}await new Promise(r=>{for(const{watcher:o}of e)o.on("change",r).on("error",r);n=setTimeout(r,kt),setImmediate(()=>setImmediate(()=>{for(const{file:o,ino:i}of e){let s=null;try{s=u.statSync(o).ino}catch{}s!==i&&r()}}))})}finally{clearTimeout(n);for(const{watcher:r}of e)r.close()}}a(B,"untilGone");const I=a(t=>$.join(N(),`${t}.queue`),"queueDir"),K=a(t=>Number(t.split("-")[2]),"ticketPid"),yt=a((t,e)=>{try{return JSON.parse(u.readFileSync($.join(t,e),"utf8"))}catch{return{}}},"readTicket"),M=a((t,{first:e})=>`${e?`0${String(1e15-Date.parse(e)).padStart(16,"0")}`:"1"}-${t}`,"rank");function D(t){const e=I(t);let n=[];try{n=u.readdirSync(e).filter(r=>!r.endsWith(".tmp"))}catch{}return n.filter(r=>E(K(r))).map(r=>({ticket:r,...yt(e,r)})).sort((r,o)=>M(r.ticket,r)<M(o.ticket,o)?-1:1)}a(D,"queuedAsks");const _=a((t,e,n)=>{const r=$.join(t,`${e}.tmp`);u.writeFileSync(r,JSON.stringify(n)),u.renameSync(r,$.join(t,e))},"writeTicket");let wt=0;async function St(t,e,n,{onWait:r,asked:o=Date.now(),site:i=null,key:s=null,first:d=null}={}){const h=$.join(N(),`${t}.lock`),p=I(t);u.mkdirSync(p,{recursive:!0});const k=`${String(o).padStart(15,"0")}-${String(wt++).padStart(6,"0")}-${process.pid}`;_(p,k,{card:e,site:i,key:s,first:d});let g=null;try{for(;;){const w=D(t),S=w.findIndex(v=>v.ticket===k),y=w.slice(0,S);let l=null;if(!y.length)try{u.writeFileSync(h,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(u.readFileSync(h,"utf8"))}catch{}if(!(Number.isInteger(l?.pid)&&E(l.pid))){u.rmSync(h,{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?$.join(p,y.at(-1).ticket):h,$.join(p,k)])}}finally{u.rmSync($.join(p,k),{force:!0})}try{return await n()}finally{try{JSON.parse(u.readFileSync(h,"utf8")).pid===process.pid&&u.rmSync(h,{force:!0})}catch{}}}a(St,"holdingQueuedLock");const vt=a((t,e,n)=>St("land",t,e,n),"holdingLandLock");function Ht(t,e,n){const r=I("land"),o=D("land").filter(i=>i.key===e&&i.site&&at(i.site,t));for(const{ticket:i,...s}of o)_(r,i,{...s,first:n});return o.length}a(Ht,"moveQueuedLand");function Ut(){const t=bt();return[...t?[{card:t.card,running:!0,first:null}]:[],...D("land").map(({card:e,first:n})=>({card:e,running:!1,first:n??null}))]}a(Ut,"landQueue");function bt(){try{const t=JSON.parse(u.readFileSync($.join(N(),"land.lock"),"utf8"));return Number.isInteger(t?.pid)&&E(t.pid)?t:null}catch{return null}}a(bt,"landLock");async function Yt(t,e,{merge:n=!1}={}){const r=q();u.mkdirSync(r,{recursive:!0});const o=$.join(r,encodeURIComponent(t));let i=!1;for(;;){if(i&&!u.existsSync(o))return{joined:!0};try{u.writeFileSync(o,JSON.stringify({pid:process.pid,at:new Date().toISOString(),...n?{merge:n}:{}}),{flag:"wx"});break}catch(s){if(s.code!=="EEXIST")throw s;let d=null;try{d=JSON.parse(u.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(d?.pid)&&E(d.pid))){u.rmSync(o,{force:!0});continue}i=!0,await B([o])}}try{return await e()}finally{u.rmSync(o,{force:!0})}}a(Yt,"joiningLand");function Gt(t){try{const e=JSON.parse(u.readFileSync($.join(q(),encodeURIComponent(t)),"utf8"));return Number.isInteger(e?.pid)&&E(e.pid)?e:null}catch{return null}}a(Gt,"runningLand");function Qt(t,e){const n=$.join(q(),encodeURIComponent(t));try{e.includes(JSON.parse(u.readFileSync(n,"utf8")).pid)&&u.rmSync(n,{force:!0})}catch{}let r=[];try{r=u.readdirSync(I("land"))}catch{}for(const o of r.filter(i=>e.includes(K(i))))u.rmSync($.join(I("land"),o),{force:!0})}a(Qt,"clearStoppedLand");function jt(t,e,n){const o=c(t,"diff","--name-only","--no-renames","--diff-filter=A",e,n,"--","docs/adr").out.split(`
|
|
22
|
+
`).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-${$.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}a(jt,"refuseNumberedAdr");function xt(t,e,n,r){const o=c(t,"log","--format=%h",`${e}..${n}`).out.split(`
|
|
23
|
+
`).filter(Boolean);for(const i of o){const s=c(t,"log","-1","--format=%s",i).out;/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${s}" still holds a "# Conflicts:" note in its message; amend it out and land again`);const d=r&&dt(s);d&&m(`land: commit ${i} "${s}" ${d}; reword it to the commit standard and land again`);const h=c(t,"log","-1","--format=%(trailers:key=Refs,valueonly,separator=%x00)",i).out.split("\0");r&&!h.includes(r)&&m(`land: commit ${i} "${s}" has no "Refs: ${r}" trailer; add it above Co-Authored-By and land again`)}}a(xt,"refuseBadMessages");function Xt(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`)}a(Xt,"syncBase");async function Et(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
24
|
`)),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}`)}
|
|
24
|
-
`).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${
|
|
25
|
-
`).filter(
|
|
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=
|
|
25
|
+
`)),e(`tests passed: ${o.pass}`)}a(Et,"passTests");async function H({path:t,branch:e,repo:n,base:r,ref:o},{say:i=console.log,runTests:s=J}={}){if(u.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 p=c(t,"rebase","--quiet",r);if(!p.ok){const k=c(t,"diff","--name-only","--diff-filter=U").out.split(`
|
|
26
|
+
`).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${p.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 d=c(n,"log","--format=%h %s",`${r}..${e}`).out.split(`
|
|
27
|
+
`).filter(p=>p&&!p.includes(" "));d.length&&m(`${e} has a commit with an empty subject (${d[0]}); reword it in ${t} and land again`),jt(n,r,e),xt(n,r,e,o);const h=c(n,"rev-parse",e).out;return c(n,"rev-parse","--verify","--quiet",F(e)).out===h?(i(`${e} was tested on ${r} as it is now; not testing it again`),h):(await Et(t,{say:i,runTests:s,outcome:"nothing landed"}),c(n,"update-ref",F(e),h),h)}a(H,"prepareLand");const F=a(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`)}a(U,"onBase");async function zt(t,{hold:e=!1,onWait:n,sync:r=a(()=>{},"sync"),queue:o={},...i}={}){const{say:s=console.log}=i,d=Date.now();for(;;){const h=await H(t,i);if(e||await vt(t.ref??t.branch,()=>(r(),Y(t,h,i)),{onWait:n,asked:d,...o}))return;s(`${t.base} moved while ${t.branch} was tested; testing it on ${t.base} again`)}}a(zt,"landInTurn");async function Vt(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`)}a(Vt,"land");function Y({path:t,branch:e,repo:n,base:r},o,{say:i=console.log,restart:s=gt,boards:d=a(()=>ot(),"boards"),clone:h=T()}={}){if(U(n,r),c(n,"rev-parse",e).out!==o||!c(n,"merge-base","--is-ancestor",r,o).ok)return!1;const p=c(n,"diff","--name-only",r,e).out.split(`
|
|
28
|
+
`).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=u.realpathSync(n)===u.realpathSync(h)?p.filter(f=>!/\.test\.mjs$/.test(f)):[],S=new Set(w.length?rt($.join(n,"hub","hub.mjs")).files:[]);if(w.some(f=>f.startsWith("hub/")||S.has(f))){const f=s("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 d()){const v=s("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}a(Y,"landPrepared");export{Bt as boardWorktree,P as builderRepo,Qt as clearStoppedLand,c as git,Kt as helperWorktree,vt as holdingLandLock,Yt as joiningLand,Vt as land,zt as landInTurn,bt as landLock,Y as landPrepared,Ut as landQueue,A as makeWorktree,Mt as mergeHelper,Ht as moveQueuedLand,J as nodeTests,Et as passTests,_t as prWorktree,H as prepareLand,gt as restartRunner,Gt as runningLand,T as skillRepo,Xt as syncBase,L as worktreesDir};
|
|
@@ -7,7 +7,6 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { readUsageGate, pauseReason, pauseUntilWords } from '../../shared/usage-gate.mjs';
|
|
10
|
-
import { FOREVER } from '../../hub/duration.mjs';
|
|
11
10
|
|
|
12
11
|
function boardDirOf(session) {
|
|
13
12
|
try {
|
|
@@ -27,8 +26,7 @@ try {
|
|
|
27
26
|
} }));
|
|
28
27
|
} else if (['SessionStart', 'UserPromptSubmit'].includes(input.hook_event_name)) {
|
|
29
28
|
const why = gate.manual ? MANUAL[gate.manual] : `Claude's usage is past the pause line (${gate.pause_at}%)`;
|
|
30
|
-
|
|
31
|
-
if (gate.state === 'paused') console.log(`${why}. Running steps finish; start no new work, builders or agents ${pauseUntilWords(gate.resets_at)}; answer the owner's messages briefly with ${answer}.`);
|
|
29
|
+
if (gate.state === 'paused') console.log(`${why}. Running steps finish; builders and agents are held ${pauseUntilWords(gate.resets_at)}; keep sorting, readying, screening and answering.`);
|
|
32
30
|
else if (gate.state === 'going') console.log(`Keep going is on: work runs past the pause line until ${gate.resets_at}, spending usage.`);
|
|
33
31
|
}
|
|
34
32
|
} catch {}
|
package/hub/boards.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
2
|
-
`),p.renameSync(n,e)}
|
|
3
|
-
`),
|
|
4
|
-
`).filter(Boolean)}catch{}return[n,t?
|
|
1
|
+
var z=Object.defineProperty;var a=(e,t)=>z(e,"name",{value:t,configurable:!0});import p from"node:fs";import u from"node:path";import Z from"node:os";import Q from"node:crypto";import{fileURLToPath as X}from"node:url";import{routeRecord as h}from"./routes.mjs";import{validBuildTarget as Y}from"../board/build-script.mjs";import{deliver as ee,readSettings as te}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as ne}from"./schedule.mjs";import{liveSessions as S,ps as re}from"./live.mjs";import{withFileLock as se}from"./locks.mjs";import{dropSessionPrompts as oe,readPrompts as ie,savePrompts as ae,withPromptsLock as ce}from"./prompts.mjs";import{storedPath as v,relinkCellarNode as ue,nodeBin as le}from"../bin/install.mjs";import{hubService as F}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{prLinks as fe,reviewLinks as pe,topicItems as de}from"../board/review.mjs";import{cardPRs as he}from"../shared/card-prs.mjs";const me=u.resolve(u.dirname(X(import.meta.url)),".."),C=u.join(Z.homedir(),".claude"),m=u.join(C,"kickflip"),P=u.join(C,"waves"),$=u.join(m,"manifest.json"),ge=u.join(m,"github-state.json"),we=u.join(m,"wake-state.json"),dt=u.join(m,"incoming-files.json"),M=u.join(m,"deaf-state.json"),x=u.join(m,"resume-state.json"),E=u.join(m,"link-faults.json"),ke=u.join(m,"repo-fetch.json"),B=u.join(m,"hub-code.json"),ye="1970-01-01T00:00:00Z",je=2*6e4,be=15*6e4,f=a((e,t)=>{try{return JSON.parse(p.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;p.mkdirSync(u.dirname(e),{recursive:!0}),p.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),p.renameSync(n,e)}a(k,"saveJSON");const y=a(()=>f($,{sites:{}}),"readManifest"),Se=a(e=>se($,e),"withManifestLock");async function ht({link:e,file:t=F().file}={}){const n=/^\/opt\/homebrew\/Cellar\/node\/[^/]+\/bin\/node$/,s=await Se(async()=>{const i=y(),r=[];for(const[l,c]of Object.entries(i.sites||{})){if(!Array.isArray(c.cmd)||!n.test(c.cmd[0]))continue;const d=le(c.cmd[0],e);d!==c.cmd[0]&&(c.cmd=[d,...c.cmd.slice(1)],r.push(l))}return r.length&&await ve(i),r});let o=!1;if(t&&p.existsSync(t)){const i=p.readFileSync(t,"utf8"),r=ue(i,e);r!==i&&(p.writeFileSync(t,r),o=!0)}return{routes:s,loginItem:o}}a(ht,"migrateCellarNode");function ve(e){return ce(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(r=>r.site)];for(const r of t.filter(Boolean))r.dir&&(r.dir=v(r.dir)),Array.isArray(r.cmd)&&(r.cmd=r.cmd.map(l=>v(l))),r.owner?.repo&&(r.owner.repo=v(r.owner.repo));for(const r of Object.values(e.starting||{}))r.repo&&(r.repo=v(r.repo));const n=y(),s=a((r,l)=>h(r.sites,l)?.owner||h(r.starting,l),"owner"),o=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),i=[];for(const r of o){const l=s(n,r),c=s(e,r);(l?.session!==c?.session||l?.since!==c?.since)&&i.push(l?.session,c?.session)}if(i.some(Boolean)){const r=ie();oe(r,i),ae(r)}k($,e)})}a(ve,"writeManifest");const _=a(e=>`${e.repo}#${e.pr}`,"prKey"),U=a((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),H=a(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),Oe=a(e=>Object.fromEntries((Array.isArray(e)?e:[]).filter(t=>typeof t?.github=="string"&&t.github&&typeof t.name=="string"&&t.name).map(t=>[t.github.toLowerCase(),t.name])),"reviewerLogins"),J=a(e=>Array.isArray(e?.items),"isItemFile"),G=/board[/\\]server\.mjs$/,A=a(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>G.test(t))&&typeof e.dir=="string","isBoardSite"),$e=a(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function xe(e,t,n){if(n==="file"){const o=f(u.resolve(e,t.items),null);return J(o)?[{slug:t.slug||u.basename(t.items,".json"),itemFile:o}]:[]}let s=[];try{s=p.readdirSync(P,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name).sort()}catch{}return s.map(o=>({slug:o,itemFile:f(u.join(P,o,"wave.json"),null)})).filter(o=>J(o.itemFile))}a(xe,"filesOf");function Ee(e,t){return e.flatMap(({slug:n,itemFile:s})=>s.items.filter(o=>o&&o.lane!=="done").flatMap(o=>he(o,t||s.repo).map(i=>({file:n,id:o.id,pr:i.number,repo:i.repo,since:o.gh_since||ye}))))}a(Ee,"prsOf");function T(e,t){const n=f(u.join(t.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",o=xe(t.dir,n,s),i=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:s,files:o,prs:Ee(o,i),github:i,...t.remote&&!i?{remote:t.remote}:{},branch:t.branch,...$e(t)?{local:!0}:{},...t.hub===!0?{hub:!0}:{},auto_build:n.auto_build===!0,...Y(n.build_target)?{build_target:n.build_target}:{},auto_review:n.auto_review===!0,skipDraft:n.skip_draft===!0,autoMerge:n.auto_merge===!0,githubLogin:typeof n.github_login=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(n.github_login)?n.github_login:null,tickets:n.tickets,reviewers:H(n.reviewers),reviewerLogins:Oe(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}a(T,"boardOf");function N(e=y()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>T(t,n))}a(N,"listBoards");function mt(e=y()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?T(...t):void 0}a(mt,"hubBoard");function _e(e,t){const n=a(s=>{try{return p.realpathSync(s)}catch{return u.resolve(s)}},"real");return u.resolve(e)===u.resolve(t)||n(e)===n(t)}a(_e,"samePath");function gt(e,t=y()){const n=Object.entries(t?.sites||{}).find(([,s])=>A(s)&&_e(s.dir,e));return n?T(...n):null}a(gt,"boardAt");async function K(e,t,n,{timeout:s=3e4}={}){if(e.port){let i;try{i=await fetch(`http://127.0.0.1:${e.port}/_claude/events?board=0`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(s)})}catch(r){if(r.name==="TimeoutError")throw new Error(`/${e.route} didn't answer within ${s/1e3} s, so it may already have the ${t} event`);if(r.cause?.code!=="ECONNREFUSED"&&r.cause?.message!=="bad port")throw new Error(`/${e.route} dropped the ${t} event's request (${r.cause?.code||r.cause?.message||r.message}), so it may already have it`)}if(i){if(i.ok)return(await i.json()).id;const r=await i.json().then(l=>l.error,()=>null);throw new Error(`/${e.route} answered ${i.status}${r?` (${r})`:""}, so it may already have the ${t} event`)}}const o={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return p.mkdirSync(u.join(e.dir,"data"),{recursive:!0}),p.appendFileSync(u.join(e.dir,"data","events.jsonl"),JSON.stringify(o)+`
|
|
3
|
+
`),o.id}a(K,"appendEvent");async function wt(e,t,n){const s=[],o=[];for(const i of e)try{s.push({route:i.route,id:await K(i,t,n)})}catch(r){o.push(`${i.route}: could not add the ${t} event (${r.message})`)}return{events:s,failed:o}}a(wt,"sendToBoards");function Ae(e,t=Date.now(),{grace:n=je}={}){const s=f(u.join(e,"data","watcher.json"),null);if(!Number.isInteger(s?.pid))return!1;if(typeof s.exited_at=="string")return t-Date.parse(s.exited_at)<=n;try{process.kill(s.pid,0)}catch(o){if(o.code!=="EPERM")return!1}return W(s.pid)?.includes("wait-events.mjs")??!0}a(Ae,"watcherRunning");const W=a(e=>re(["-p",String(e),"-o","command="]),"commandOf"),V=a(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function kt(e,t,n,{command:s=W,kill:o=process.kill}={}){const i=f(u.join(e,"data","watcher.json"),null);if(!Number.isInteger(i?.pid)||i.pid===process.pid||typeof i.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${V(t)}['"]?\\s+['"]?${V(n)}['"]?(\\s|$)`).test(s(i.pid)??""))return null;try{o(i.pid,"SIGTERM")}catch{return null}return i.pid}a(kt,"stopPreviousWatcher");function Te(e,{now:t=Date.now(),running:n=S()}={}){const s=e.owner?.session;return!!(s&&(n?.has(s)??!0))&&Ae(e.dir,t)}a(Te,"isListening");function L(e,{now:t=Date.now(),running:n=S(),wakes:s=f(x,{})}={}){if(Te(e,{now:t,running:n}))return"listening";const o=e.owner?.session,i=h(s,e.route);return!o||!e.owner.repo||i?.error||i?.pushed_at?"deaf":i?.resumed_at?"waking":n?.has(o)?"deaf":"asleep"}a(L,"listenState");const R=a((e,t)=>u.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),O=a(e=>R(e,e.owner.session),"cursorFile");function yt(e){const t=e.owner?.session?f(O(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const s=h(t,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}a(yt,"watcherCursor");const jt=a(e=>Object.fromEntries(Object.entries(D(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function bt(e,{from:t,to:n=t,mark:s=null}){const o=f(R(e,t),null);if(!o&&!s)return;const i=Object.assign(Object.create(null),o);for(const[r,l]of Object.entries(s||{}))i[r]=Math.min(h(i,r)??l,l);k(R(e,n),i)}a(bt,"carryCursor");function D(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let s=[];try{s=p.readFileSync(u.resolve(e.dir,n),"utf8").split(`
|
|
4
|
+
`).filter(Boolean)}catch{}return[n,t?s.filter(o=>t.test(o)):s]}))}a(D,"watchedLines");function Ne(e){if(!e.owner?.session)return null;const t=f(O(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let s=null;for(const[o,i]of Object.entries(D(e)))for(const r of t?i.slice(h(t,o)||0):i){let l=null;try{l=JSON.parse(r).ts}catch{}const c=Date.parse(l);Number.isNaN(c)||c<n||(!s||c<Date.parse(s))&&(s=l)}return s}a(Ne,"oldestUnheard");async function St({manifest:e,now:t=new Date,running:n=S(),wakes:s=f(x,{})}={}){if(!n)return{pushed:[],failed:[]};const o=Object.assign(Object.create(null),f(M,{})),i=[],r=[];let l=!1;for(const c of N(e)){const d=L(c,{now:t.getTime(),running:n,wakes:s});if(d==="listening"){h(o,c.route)&&(delete o[c.route],l=!0);continue}if(d==="asleep"||d==="waking"||h(o,c.route))continue;const g=Ne(c);if(!g||t-Date.parse(g)<be)continue;const b=Math.round((t-Date.parse(g))/6e4),w=n.has(c.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await ee({title:`${c.title} isn't listening`,body:`An event has waited ${b} min and ${w}`,url:`/${c.route}/`,tag:`deaf-${c.route}`},{kind:"fault",immediate:!0,at:t}),o[c.route]={at:t.toISOString(),since:g},l=!0,i.push(c.route)}catch(q){r.push(`${c.route}: could not push that it isn't listening (${q.message})`)}}return l&&k(M,o),{pushed:i,failed:r}}a(St,"checkListening");const Le=a(e=>u.join(e,"data","handshake.json"),"handshakeFile");async function vt(e,t=y()){const n=N(t).find(o=>o.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!p.existsSync(O(n))&&k(O(n),Object.fromEntries(Object.entries(D(n)).map(([o,i])=>[o,i.length])));const s={id:await K(n,"ping",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(Le(n.dir),{ping:s,ack:null}),{route:e,ping:s.id}}a(vt,"handshake");const Ot=["github","repo-fetch","events","wakes","slack"];function Re(e,t){if(e.remote)return null;const n=t?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=e.prs.filter(i=>t.prs?.[_(i)]?.at!==n.at).map(i=>`#${i.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(e.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const o=new Set(e.prs.map(_)).size;return{link:"github",ok:!0,detail:o?`${U(o,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}a(Re,"githubLink");function De(e){return e?e.ok?{link:"repo-fetch",ok:!0,detail:"The hub's last git fetch of this board's repo worked."}:{link:"repo-fetch",ok:!1,detail:`${e.error.replace(/[.\s]*$/,".")} ${e.hint}.`}:null}a(De,"repoFetchLink");const Ie={listening:{ok:!0,detail:"Its session and a watcher are running."},waking:{ok:!0,detail:"Waking: the hub resumed its session, which listens again once it rejoins."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event or a tap on its page."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},Fe=a(e=>({link:"events",listen:e,...Ie[e]}),"eventsLink");function Ce(e,t,n){return(n?.last?.failed||[]).includes(e.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${t.quiet.end}, when quiet hours end.`}}a(Ce,"wakesLink");function Pe(e,t,n){const s=f(u.join(e.dir,"data","pr-slack.json"),{}),o=[];let i=0;for(const r of e.prs){const l=t?.prs?.[_(r)];if(l?.state!=="OPEN"||l.isDraft)continue;i++;const{itemFile:c}=e.files.find(w=>w.slug===r.file),d=c.items.find(w=>w.id===r.id),g=pe(c,d,`${r.file}/${r.id}`,n,fe(s,[{repo:r.repo,number:r.pr}])),b=[...new Set([...e.reviewers,...H(c.reviewers)])].filter(w=>!Object.hasOwn(g,w));b.length&&o.push(`#${r.pr} has no link for ${b.join(", ")}`)}return o.length?{link:"slack",ok:!1,detail:`${o.join("; ")}.`}:{link:"slack",ok:!0,detail:i?`Every reviewer has the link to ${U(i,"PR")} in review.`:"No PR is in review."}}a(Pe,"slackLink");const Me=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function I(e=u.join(me,"hub","hub.mjs")){const t=u.dirname(u.dirname(e)),n=new Map,s=a(r=>{if(n.has(r))return;let l;try{l=p.readFileSync(r)}catch{return}n.set(r,l);for(const c of l.toString("utf8").matchAll(Me))s(u.resolve(u.dirname(r),c[1]))},"visit");s(e);const o=[...n.keys()].map(r=>u.relative(t,r)).sort(),i=Q.createHash("sha256");for(const r of o)i.update(r).update("\0").update(n.get(u.join(t,r))).update("\0");return{hash:i.digest("hex").slice(0,12),files:o}}a(I,"hubCode");const $t=a(e=>I(u.resolve(e.cwd||e.dir,e.cmd.find(t=>G.test(t)))).hash,"boardCode");function xt(){const e={...I(),pid:process.pid,at:new Date().toISOString()};return k(B,e),e}a(xt,"recordHubCode");const Be=F().restartCommand,Ue=a(()=>f(B,null)?.hash??null,"runningHubCode"),He=a(()=>({running:Ue(),onDisk:I().hash}),"hubCodeState"),Et=a(({running:e,onDisk:t}=He())=>e&&e===t?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${t}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${Be}`},"hubLink"),j=6e4,Je={github:a(e=>(e+10)*j,"github"),"repo-fetch":a(e=>(e+10)*j,"repo-fetch"),events:a(()=>2*j,"events"),wakes:a(e=>(e+10)*j,"wakes"),slack:a(e=>(e+10)*j,"slack")};function Ge(e,t,{now:n,faults:s}){const o=s===void 0,i=o?h(f(E,{}),e)||{}:s,r={},l=t.map(c=>{if(c.ok)return{...c,state:"ok"};const d=h(i,c.link)||new Date(n).toISOString();r[c.link]=d;const g=Je[c.link](ne);return{...c,state:n-Date.parse(d)>=g?"broken":"settling",since:d}});if(o&&JSON.stringify(r)!==JSON.stringify(i)){const c=Object.assign(Object.create(null),f(E,{}));Object.keys(r).length?c[e]=r:delete c[e];try{k(E,c)}catch{}}return l}a(Ge,"settle");function Ke(e,t={}){const n=t.github??f(ge,{}),s=t.settings??te(),o=[Re(e,n),De(h(t.fetches??f(ke,{}),e.route)),Fe(t.listen??L(e)),Ce(e,s,t.wakes??f(we,{})),Pe(e,n,t.topics??de())].filter(Boolean);return Ge(e.route,o,{now:t.now??Date.now(),faults:t.faults})}a(Ke,"linksOf");function _t(e){const t={running:S(),wakes:f(x,{})};if(t.running)for(const n of N(e))Ke(n,{listen:L(n,t)})}a(_t,"recordLinkFaults");export{be as DEAF_AFTER,ge as GITHUB_STATE,Je as GRACE,B as HUB_CODE,dt as INCOMING_FILES,Ot as LINKS,ke as REPO_FETCH,x as RESUME_STATE,we as WAKE_STATE,P as WAVES,K as appendEvent,gt as boardAt,$t as boardCode,$e as buildsLocally,bt as carryCursor,St as checkListening,vt as handshake,Le as handshakeFile,mt as hubBoard,I as hubCode,He as hubCodeState,Et as hubLink,A as isBoardSite,Te as isListening,Ke as linksOf,N as listBoards,L as listenState,ht as migrateCellarNode,Ne as oldestUnheard,_ as prKey,Ee as prsOf,f as readJSON,y as readManifest,xt as recordHubCode,_t as recordLinkFaults,Oe as reviewerLogins,Ue as runningHubCode,_e as samePath,k as saveJSON,wt as sendToBoards,kt as stopPreviousWatcher,jt as watchedCounts,D as watchedLines,yt as watcherCursor,Ae as watcherRunning,Se as withManifestLock,ve as writeManifest};
|
package/hub/home.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var g=Object.defineProperty;var a=(e,t)=>g(e,"name",{value:t,configurable:!0});import{siteIcon as
|
|
1
|
+
var g=Object.defineProperty;var a=(e,t)=>g(e,"name",{value:t,configurable:!0});import{siteIcon as f}from"./icons.mjs";import{contextFull as w}from"./usage.mjs";const c=a(e=>Number.isInteger(e)&&e>0?e:0,"count");function A(e){const{items:t,working:s,summary:n,lanes:r,building:o}=e||{};if(!Array.isArray(t))return null;const u=[...new Set(t.map(i=>String(i?.kind||"").trim()).filter(Boolean))],l=r&&typeof r=="object"&&["review","progress","todo"].every(i=>r[i]===void 0||Number.isInteger(r[i])&&r[i]>=0);return{count:t.length,working:c(s),summary:typeof n=="string"&&n.trim()?n.trim():t.length?`${t.length} waiting${u.length?`: ${u.join(", ")}`:""}`:"",lanes:l?{review:c(r.review),progress:c(r.progress),todo:c(r.todo)}:null,building:o===!0}}a(A,"readNeeds");function h({state:e,working:t,building:s,listen:n}={}){return e==="down"?{tone:"down",word:"down"}:n==="full"?{tone:"down",word:"context full"}:n==="deaf"?{tone:"down",word:"not listening"}:e==="starting"?{tone:"starting",word:"starting"}:e==="stopped"?{tone:"stopped",word:"stopped"}:t?{tone:"working",word:"working"}:n==="waking"?{tone:"asleep",word:"waking\u2026"}:n==="asleep"?{tone:"asleep",word:"asleep"}:s?{tone:"building",word:"implementing"}:null}a(h,"siteStatus");function $(e){return e?[["review","review"],["progress","doing"],["todo","todo"]].filter(([t])=>e[t]>0).map(([t,s])=>({lane:t,text:`${e[t]} ${s}`})):null}a($,"laneChips");const p=a(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc");function m(e,t,s){const n=h({state:e,...t,listen:s}),r=e==="up"?$(t?.lanes):null,o=t?.count?[{lane:"needs",text:`${t.count} need you`},...r||[]]:r,u=n?`<span class="dot t-${n.tone}${n.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${n.tone}">${p(n.word)}</span>`:"",l=o===null?"":o.length?o.map(d=>`<span class="lc lc-${d.lane}">${p(d.text)}</span>`).join(""):"<span>nothing open</span>";if(!u&&!l)return"";const i=`${u}${t?.stale?'<span class="stale-word"> (stale)</span>':""}${l?`<span class="lcs">${l}</span>`:""}`;return t?.stale?`<span class="held">${i}</span>`:i}a(m,"statusLine");const x=15e3;function v(e,t,s,n){return t?{good:{at:s,needs:t},needs:t}:e?{good:e,needs:n-e.at>=x?{...e.needs,stale:!0}:e.needs}:{good:null,needs:null}}a(v,"holdNeeds");function E(e,t,s,n=new Map,r=new Map){return e.map(o=>{const u=t.get(o.route)??null,l=s.has(o.route)?n.get(o.route)??"listening":void 0,i=r.get(o.route)??null;return{route:o.route,title:o.title,board:s.has(o.route),state:o.state,count:u?.count||0,changedAt:o.changedAt||0,created:o.created||null,icon:f(o.route,o),sub:m(o.state,u,w(l,i)?"full":l),ctx:i}})}a(E,"sidebarSites");function N(e){return e.filter(t=>t.board).sort((t,s)=>s.count-t.count||s.changedAt-t.changedAt||t.route.localeCompare(s.route))[0]?.route??null}a(N,"startRoute");const k="runs older code than its files; restart it";function M(e,t=new Map,s=null){const n=e.filter(l=>l.state==="down").length,r=e.reduce((l,i)=>l+(t.get(i.route)||[]).filter(d=>d.state==="broken").length,0),o=e.filter(l=>l.state==="up").length,u=s?.ok===!1;return{tone:n?"down":r||u?"wait":"ok",word:n?`${n} site${n===1?"":"s"} down`:r||u?"degraded":"healthy",detail:`${o} of ${e.length} site${e.length===1?"":"s"} up${r?` \xB7 ${r} link${r===1?"":"s"} failing`:""}${u?` \xB7 ${k}`:""}`}}a(M,"hubHealth");export{x as NEEDS_STALE_MS,k as STALE_CODE,v as holdNeeds,M as hubHealth,$ as laneChips,A as readNeeds,E as sidebarSites,h as siteStatus,N as startRoute,m as statusLine};
|