@lovek4d/kickflip 0.7.2 → 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 +10 -10
- package/SKILL.md +5 -3
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +42 -32
- 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/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 +6 -19
- package/board/public/app-core.js +2 -2
- package/board/public/app-lanes.js +5 -12
- package/board/public/app-snapshot.js +1 -1
- package/board/public/style.css +11 -14
- package/board/public/ui-cards.js +28 -25
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-links.js +0 -1
- package/board/public/ui-questions.js +3 -3
- package/board/public/ui-threads.js +5 -6
- package/board/public/ui.js +3 -5
- package/board/public/view-hub.js +4 -2
- package/board/public/view-questions.js +1 -1
- package/board/server.mjs +18 -18
- package/board/store.mjs +30 -26
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +15 -15
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/repos.mjs +3 -3
- package/hub/schedule.mjs +1 -1
- package/hub/try.mjs +8 -6
- 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/hub/pr-report.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var _=Object.defineProperty;var
|
|
1
|
+
var _=Object.defineProperty;var r=(e,n)=>_(e,"name",{value:n,configurable:!0});import{gh as f,PRIMARY as O}from"../shared/gh.mjs";const S="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",C="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=r((e=process.env)=>({...e,PATH:[e.PATH,C].filter(Boolean).join(":")}),"withPath"),w=r(e=>String(e).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");async function m(e,{env:n,account:s}={}){const a=await f(e,{env:g(n),account:s,timeout:9e4,maxBuffer:16777216});if(a.status!==0)throw Object.assign(new Error(w(a.stderr||`gh exited ${a.status}`)),{limited:a.limited});try{return JSON.parse(a.stdout)}catch(l){throw new Error(w(`unreadable output (${l.message})`))}}r(m,"gh");const x="\u{1F916} ",b="<!-- claude-bridge -->";async function y(e){if(!e)return{env:process.env,account:O};const n=await f(["auth","token","-u",e],{env:g()}),s=n.status===0?n.stdout.trim():"";if(!s)throw new Error(`gh has no ${e} account; run gh auth login`);return{env:{...process.env,GH_TOKEN:s},account:e}}r(y,"asLogin");async function L({repo:e,pr:n,replyTo:s,words:a,login:l}){const d=await y(l),p=s===void 0?`repos/${e}/issues/${n}/comments`:`repos/${e}/pulls/${n}/comments/${s}/replies`;try{return(await m(["api",p,"-f",`body=${x}${a}
|
|
2
2
|
|
|
3
|
-
${b}`],
|
|
3
|
+
${b}`],d)).html_url}catch(u){throw new Error(`${e}#${n}: ${u.message}`)}}r(L,"postToPR");async function U({repo:e,pr:n,head:s,body:a,login:l}){const d=await y(l);try{return(await m(["api",`repos/${e}/pulls/${n}/reviews`,"-f",`body=${a}`,"-f","event=COMMENT","-f",`commit_id=${s}`],d)).html_url}catch(p){throw new Error(`${e}#${n}: ${p.message}`)}}r(U,"postReview");async function k(e){return(await m(["api","--paginate","--slurp",`repos/${e}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}r(k,"collaborators");async function q(e){try{return(await m(["api",`users/${encodeURIComponent(e)}`])).login}catch{return null}}r(q,"githubUser");async function F({repo:e,pr:n,since:s}){try{return(await m(["api","--paginate","--slurp",`repos/${e}/pulls/${n}/reviews`])).flat().filter(l=>Date.parse(l.submitted_at)>Date.parse(s)).map(l=>({login:l.user?.login??null,at:l.submitted_at}))}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}}r(F,"reviewsSince");async function M({repo:e,pr:n}){const s=await f(["pr","ready",String(n),"--repo",e],{env:g(),timeout:9e4});if(s.status!==0)throw new Error(w(s.stderr||`gh exited ${s.status}`))}r(M,"markReady");const j=r(e=>e.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function H({repo:e,pr:n,since:s}){let a;try{a=await m(["pr","view",String(n),"--repo",e,"--json",S])}catch(i){throw Object.assign(new Error(`${e}#${n}: ${i.message}`),{limited:i.limited})}const l=r(i=>m(["api","--paginate","--slurp",`repos/${e}/${i}`]).then(c=>c.flat()).catch(()=>[]),"list"),[d,p,u]=await Promise.all([l(`issues/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/reviews`)]);return{view:a,issueComments:d,reviewComments:p,reviews:u}}r(H,"fetchPR");const $="author { __typename login }",v=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles url mergedAt headRefName updatedAt latestReviews(first: 50) { nodes { state ${$} } } recent: commits(last: 20) { nodes { commit { oid messageHeadline } } } commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;async function A(e){const n=await f(["api","graphql","-f",`query=${e}`],{env:g(),timeout:9e4,maxBuffer:67108864});let s=null;try{s=JSON.parse(n.stdout)}catch{}if(s?.data)return s;throw new Error(`GitHub query: ${w(n.status!==0?n.stderr||`gh exited ${n.status}`:"unreadable output")}`)}r(A,"graphql");const E=r(e=>e?.login?{login:e.__typename==="Bot"?`${e.login}[bot]`:e.login}:null,"user");function D(e){const n=new Set(e.map(s=>s.in_reply_to_id).filter(Boolean));return e.filter(s=>!s.in_reply_to_id&&/codex/i.test(s.user?.login||"")&&!n.has(s.id)).length}r(D,"codexOpen");function P(e){let n=0,s=0;for(const a of e){const l=(a.conclusion||a.state||"").toUpperCase(),d=(a.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(l)?n++:(d&&d!=="COMPLETED"||l==="PENDING"||l==="EXPECTED")&&s++}return{total:e.length,fail:n,pending:s,state:n?"fail":s?"pending":e.length?"pass":"none"}}r(P,"checksOf");const I="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function B(e,n=[]){const s=e.map(({repo:u,pr:i},c)=>{const[t,h]=u.split("/");return`pr${c}: repository(owner: ${JSON.stringify(t)}, name: ${JSON.stringify(h)}) { pullRequest(number: ${Number(i)}) { ${v} } }`});n.forEach((u,i)=>{const[c,t]=u.split("/");s.push(`open${i}: repository(owner: ${JSON.stringify(c)}, name: ${JSON.stringify(t)}) { ${I} }`)});const{data:a,errors:l=[]}=await A(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),d=Object.fromEntries(n.flatMap((u,i)=>a[`open${i}`]?.pullRequests?[[u,a[`open${i}`].pullRequests.nodes]]:[])),p=e.map(({repo:u,pr:i},c)=>{const t=a[`pr${c}`]?.pullRequest;if(!t)return{error:`${u}#${i}: ${l.find(o=>o.path?.[0]===`pr${c}`)?.message||"GitHub did not answer for it"}`};const h=r(o=>({id:o.databaseId??null,user:E(o.author),created_at:o.createdAt??null,body:o.body??null,html_url:o.url??null}),"comment"),R=(t.reviewThreads?.nodes??[]).flatMap(o=>o.comments?.nodes??[]).map(o=>({...h(o),path:o.path??null,line:o.line??null,in_reply_to_id:o.replyTo?.databaseId??null}));return{view:{state:t.state,isDraft:t.isDraft,reviewDecision:t.reviewDecision,mergeable:t.mergeable,mergeStateStatus:t.mergeStateStatus,headRefOid:t.headRefOid,statusCheckRollup:t.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,changedFiles:t.changedFiles??null,approved:(t.latestReviews?.nodes??[]).some(o=>o.state==="APPROVED"),commits:(t.recent?.nodes??[]).map(o=>({oid:o.commit.oid.slice(0,10),subject:o.commit.messageHeadline})),url:t.url??null,mergedAt:t.mergedAt??null,headRefName:t.headRefName??null,updatedAt:t.updatedAt??null,reviews:Object.fromEntries((t.latestReviews?.nodes??[]).filter(o=>o.author?.login).map(o=>[o.author.login,o.state])),codexOpen:D(R)},issueComments:(t.comments?.nodes??[]).map(h),reviewComments:R,reviews:(t.reviews?.nodes??[]).map(o=>({id:o.databaseId??null,user:E(o.author),submitted_at:o.submittedAt??null,body:o.body??null,state:o.state??null,html_url:o.url??null}))}});return{login:a.viewer?.login??null,rate:a.rateLimit??null,results:p,open:d}}r(B,"fetchPRs");function J({view:e,issueComments:n,reviewComments:s,reviews:a},{pr:l,since:d,me:p}){const u=r((t,h)=>({kind:h,id:t.id??null,login:t.user?.login??null,at:t.created_at??null,body:t.body??null}),"comment"),i=[...n.map(t=>({...u(t,"issue_comment"),url:t.html_url??null})),...s.map(t=>({...u(t,"review_comment"),path:t.path??null,line:t.line??null,in_reply_to:t.in_reply_to_id??null,url:t.html_url??null})),...a.map(t=>({kind:"review",id:t.id??null,login:t.user?.login??null,at:t.submitted_at??null,body:t.body??null,state:t.state??null,url:t.html_url??null}))].filter(t=>t.at!=null&&t.at>d).map(t=>({...t,who:t.login===p?(t.body??"").includes(b)?"me":"user":/codex/.test(t.login)?"codex":/\[bot\]$|^linear/.test(t.login)?"bot":"reviewer"})).filter(t=>t.who!=="me"&&t.who!=="bot").sort((t,h)=>t.at<h.at?-1:t.at>h.at?1:0),c=e.statusCheckRollup??[];return{pr:Number(l),state:e.state??null,isDraft:e.isDraft??null,reviewDecision:e.reviewDecision??null,mergeable:e.mergeable??null,mergeStateStatus:e.mergeStateStatus??null,head:e.headRefOid?.slice(0,10)??null,failing:c.filter(t=>/FAILURE|ERROR/.test(t.conclusion??t.state??"")).map(t=>t.name??t.context??null),pending:c.filter(t=>(t.status??"COMPLETED")!=="COMPLETED").length,events:i,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.changedFiles??null,approved:!!e.approved,commits:e.commits??[],url:e.url??null,mergedAt:e.mergedAt??null,headRefName:e.headRefName??null,headRefOid:e.headRefOid??null,updatedAt:e.updatedAt??null,checks:P(c),reviews:e.reviews??{},codexOpen:e.codexOpen??0}}r(J,"reportOf");async function G(e){let n;try{n=await m(["pr","list","--repo",e,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${e} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(a=>a.path),requested:(s.reviewRequests||[]).map(a=>a.login).filter(Boolean)}))}r(G,"openPRs");async function K(e,n){let s;try{s=await m(["pr","view",String(n),"--repo",e,"--json","state,files"])}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}return{state:s.state??null,files:(s.files||[]).map(a=>a.path)}}r(K,"prFiles");async function Q({env:e}={}){const n=await m(["api","user"],{env:e});if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}r(Q,"githubLogin");export{b as CLAUDE_MARK,x as CLAUDE_PREFIX,S as PR_VIEW_FIELDS,P as checksOf,j as checksPassed,k as collaborators,H as fetchPR,B as fetchPRs,Q as githubLogin,q as githubUser,M as markReady,G as openPRs,U as postReview,L as postToPR,K as prFiles,J as reportOf,F as reviewsSince};
|
package/hub/readiness.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,d;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,d="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",d="kickflip doctor --fix"):(n="Tailscale isn't signed in",d="tailscale up"):(n="Tailscale isn't installed on the host",d=H(a)),n?{ready:!1,missing:n,fix:{command:d}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:d,browser:f,added:m})=>({id:n,kind:d,browser:f,added:m}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...l(o,t[o]),waits:n}))}s(k,"readinessRows");const A=s(t=>(i,a)=>new Promise(l=>{$(i,a,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=y({home:t,env:i,now:a,cacheMs:l}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:A(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...o},d=new Map;async function f(r,c){if(!["github","tailscale"].includes(r))return n[r]({fresh:c});const p=r==="github"&&P({file:T(t),now:a()});if(p)return{limited:p};const g=d.get(r);if(!c&&g&&a()-g.at<l)return g.value;const u=Promise.resolve().then(()=>n[r]());return d.set(r,{at:a(),value:u}),u}s(f,"check");const m=s(async({fresh:r=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([c])=>[c,await f(c,r)]))),{now:a()}),"rows");async function w(r){const[c]=k({[r]:await f(r,!1)},{now:a()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:m,waiting:w}}s(F,"readinessReader");const E=y(),X=F({probes:{codex:E}});export{E as readCodex,X as readiness,F as readinessReader,k as readinessRows};
|
package/hub/repos.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var Y=Object.defineProperty;var i=(t,e)=>Y(t,"name",{value:e,configurable:!0});import v from"node:fs";import h from"node:path";import{spawn as Z}from"node:child_process";import{reposDir as A}from"../shared/repos.mjs";import{withFileLock as
|
|
2
|
-
`).map(g=>g.trim()).filter(Boolean),l=(o.find(g=>/^remote:/i.test(g))||o.at(-1)||"git failed without a message").replace(/(\w+:\/\/)[^/\s@]+@/g,"$1"),m=O(s),y=m?`check git can reach ${m.host}: git ls-remote ${m.remote}`:e&&
|
|
3
|
-
`))?`git's github.com login can't see ${e}; run gh auth setup-git`:`run git -C ${r} fetch origin to see the whole error`;return{error:l,hint:y}}i(
|
|
1
|
+
var Y=Object.defineProperty;var i=(t,e)=>Y(t,"name",{value:e,configurable:!0});import v from"node:fs";import h from"node:path";import{spawn as Z}from"node:child_process";import{gh as tt}from"../shared/gh.mjs";import{reposDir as A}from"../shared/repos.mjs";import{withFileLock as et}from"./locks.mjs";import{storedPath as T}from"../bin/install.mjs";const w=i((t,e=400)=>{throw Object.assign(new Error(t),{status:e})},"refuse");function S(t){return(typeof t!="string"||t.length>140||!/^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?\/[a-z\d_.-]{1,100}$/i.test(t)||[".","..",".git"].includes(t.split("/")[1].toLowerCase()))&&w("Repo must be owner/name on GitHub."),t}i(S,"validateGithub");function E(t){return(typeof t!="string"||!t||t.length>255||t==="@"||t.startsWith("-")||/[\s\x00-\x1f\x7f~^:?*\[\\]/.test(t)||t.includes("..")||t.includes("@{")||t.split("/").some(e=>!e||e.startsWith(".")||e.endsWith(".")||e.endsWith(".lock")))&&w("Choose a valid branch name."),t}i(E,"validateBranch");const C=i((t,e)=>{try{return t(e),!0}catch{return!1}},"valid");function st(t){const e=/^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec(t.trim());return e?W(e[1]):null}i(st,"githubFromOrigin");const G=/^[a-z\d](?:[a-z\d-]*[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]*[a-z\d])?)+$/i,I=/^[\w~][\w.~-]{0,99}$|^\.[\w~-][\w.~-]{0,98}$/;function O(t){if(typeof t!="string")return null;const e=t.trim().replace(/\/+$/,""),s=/^https:\/\/([^/@\s?#]+?)(?::\d{1,5})?\/([^\s?#]+)$/i.exec(e)||/^ssh:\/\/(?:[^@/:\s]+@)?([^/@:\s?#]+)(?::\d{1,5})?\/([^\s?#]+)$/i.exec(e)||/^[^@/:\s]+@([^/@:\s?#]+):([^\s?#]+)$/.exec(e);if(!s||e.length>400||!G.test(s[1])||/^(?:www\.)?github\.com$/i.test(s[1]))return null;const r=s[2].replace(/^\/+/,"").replace(/\.git$/i,"").split("/");return r.length>10||!r.every(o=>I.test(o)&&o!=="..")?null:{remote:e,host:s[1].toLowerCase(),path:r.join("/")}}i(O,"remoteFromInput");const M="Paste a git link: https://host/path, ssh://host/path or user@host:path.",k=i(t=>O(t)||w(M),"validateRemote"),$=i(t=>typeof t=="string"&&t.includes(":"),"isRemote"),b=i(t=>$(t)?(({host:e,path:s})=>[e,...s.toLowerCase().split("/")])(k(t)):S(t).toLowerCase().split("/"),"repoParts"),B=i(t=>$(t)?(({host:e,path:s})=>`${e}/${s}`)(k(t)):S(t),"repoName"),D=i((t,e)=>{try{return b(t).join("/")===b(e).join("/")}catch{return!1}},"sameRepo");function W(t){if(typeof t!="string")return null;const e=t.trim(),s=/^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s?#]+\/[^/\s?#]+)(?:\/[^\s]*)?(?:[?#][^\s]*)?$/i.exec(e),r=s?s[1].replace(/\.git$/i,""):e;try{return S(r)}catch{return null}}i(W,"githubFromInput");async function St(t,{repos:e,manifest:s,sendJSON:r}){try{return r(200,await e.lookup(t.searchParams.get("github")??t.searchParams.get("remote"),s))}catch(o){return r(o.status||502,{error:o.message})}}i(St,"repoLookupRoute");function z(t,e,{onStderr:s,...r}={}){return t==="gh"?tt(e,{maxBuffer:256*1024*1024,...r}):new Promise(o=>{const l=Z(t,e,{...r,shell:!1,stdio:["ignore","pipe","pipe"]});let m="",y="";l.stdout.on("data",g=>{m+=g}),l.stderr.on("data",g=>{y+=g,s?.(String(g))}),l.on("error",g=>o({status:1,stdout:m,stderr:g.message})),l.on("close",g=>o({status:g,stdout:m,stderr:y}))})}i(z,"runRepo");async function xt(t,e=z){const s=await e("git",["-C",t,"symbolic-ref","--quiet","--short","refs/remotes/origin/HEAD"]),r=await e("git",["-C",t,"symbolic-ref","--quiet","--short","HEAD"]);for(const o of[s.status===0&&s.stdout.trim().replace(/^origin\//,""),r.status===0&&r.stdout.trim()])if(o&&C(E,o))return o;return""}i(xt,"defaultBranch");function x(t,e=A()){const s=b(t),r=h.resolve(e),o=h.resolve(r,...s);o.startsWith(`${r}${h.sep}`)||w("Clone path must stay inside the clone folder.");let l=r;for(const y of["",...s])y&&(l=h.join(l,y)),v.lstatSync(l,{throwIfNoEntry:!1})?.isSymbolicLink()&&w("Clone path must stay inside the clone folder; symlinks are refused.");let m=r;for(;!v.existsSync(m);)m=h.dirname(m);return v.realpathSync(m)!==m&&w("Clone path must stay inside the clone folder; symlinks are refused."),o}i(x,"repoPath");const P=i(t=>v.lstatSync(h.join(t,".git"),{throwIfNoEntry:!1})?.isDirectory()===!0,"hasGit");function Ct(t,e=A()){let s,r,o;try{s=v.realpathSync(t),r=v.realpathSync(e),o=h.relative(r,s).split(h.sep)}catch{return!1}if(o.length===2&&C(S,o.join("/")))return P(s);if(o.length<2||o.length>11||!G.test(o[0])||!o.slice(1).every(l=>I.test(l)&&l!==".."))return!1;for(let l=2;l<o.length;l++)if(P(h.join(r,...o.slice(0,l))))return!1;return P(s)}i(Ct,"isClone");function nt(t){let e="",s=0,r=0;return o=>{const l=(e+o).split(/[\r\n]/);e=l.pop();for(const m of l){const y=/^(Receiving objects|Resolving deltas):\s+(\d+)%/.exec(m.trim());y&&(y[1]==="Receiving objects"?s=Number(y[2]):r=Number(y[2]),t(Math.floor(s*.9+r*.1)))}}}i(nt,"cloneMeter");const ot=/^(?:Cloning into |remote: (?:Enumerating|Counting|Compressing|Total) |Receiving objects:|Resolving deltas:|Updating files:)/;function rt(t,e){return(String(t||"").split(/\n\s*\n/).map(r=>r.split(/[\r\n]+/).map(o=>o.trim()).filter(o=>o&&!ot.test(o))).find(r=>r.length)?.at(-1)||e).replace(/(\w+:\/\/)[^/\s@]+@/g,"$1")}i(rt,"errorLine");const _=i((t,e)=>rt(t.stderr||t.stdout,e),"failure"),it=i(async(t,e,s,r)=>{const o=await t(e,s,r);return o.status!==0&&w(_(o,`${e} failed`),502),o.stdout},"checked"),R=i(async(t,e)=>JSON.parse(await it(t,"gh",e)),"ghJSON");async function H(t,e,s){v.lstatSync(h.join(e,".git"),{throwIfNoEntry:!1})?.isDirectory()||w("The clone path must have a real .git metadata directory; symlinks are refused.");const r=await t("git",["-C",e,"rev-parse","--show-toplevel"]);(r.status!==0||h.resolve(r.stdout.trim())!==v.realpathSync(e))&&w("The clone path must be the root of a Git work tree.");const o=await t("git",["-C",e,"remote","get-url","origin"]);o.status===0&&($(s)?D(o.stdout.trim(),s):st(o.stdout)?.toLowerCase()===s.toLowerCase())||w(`The clone path origin must be ${$(s)?B(s):`github.com/${s}`}.`)}i(H,"verifyClone");const F=i((t,e)=>t("git",["-c","credential.helper=",...e],{env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"publicGit"),at=i(t=>["-c","credential.helper=","-c","credential.https://github.com.helper=!gh auth git-credential",...t],"githubGitArgv"),ct=i((t,e,s={})=>t("git",at(e),{...s,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"githubGit"),lt=i((t=process.env)=>({...t,GIT_TERMINAL_PROMPT:"0",GIT_SSH_COMMAND:t.GIT_SSH_COMMAND||"ssh -o BatchMode=yes"}),"remoteGitEnv"),L=i((t,e,s={})=>t("git",e,{...s,env:lt()}),"remoteGit"),U=i(t=>$(t)?L:ct,"gitFor"),ut=i(t=>$(t)?t:`https://github.com/${t}.git`,"cloneURL"),ft=/not found|authentication failed|could not read username|invalid username or password|permission denied|terminal prompts disabled|\b403\b/i;function ht(t,{github:e="",remote:s="",repo:r}){const o=String(t||"").split(`
|
|
2
|
+
`).map(g=>g.trim()).filter(Boolean),l=(o.find(g=>/^remote:/i.test(g))||o.at(-1)||"git failed without a message").replace(/(\w+:\/\/)[^/\s@]+@/g,"$1"),m=O(s),y=m?`check git can reach ${m.host}: git ls-remote ${m.remote}`:e&&ft.test(o.join(`
|
|
3
|
+
`))?`git's github.com login can't see ${e}; run gh auth setup-git`:`run git -C ${r} fetch origin to see the whole error`;return{error:l,hint:y}}i(ht,"gitReachFault");const N=i((t,e)=>{const{error:s,hint:r}=ht(t.stderr||t.stdout,{remote:e,repo:""});return`${s.replace(/[.\s]*$/,".")} ${r[0].toUpperCase()}${r.slice(1)}.`},"reachError");async function q(t,e,s,r){const o=await U(s)(t,["-C",e,"fetch","--no-tags","origin",`+refs/heads/${r}:refs/remotes/origin/${r}`]);o.status!==0&&w(`Could not fetch branch "${r}" from ${B(s)}: ${_(o,"git fetch failed")}`,502)}i(q,"fetchBranch");function bt({run:t=z,base:e=A(),now:s=Date.now}={}){let r,o;const l=new Map,m=i(n=>({github:n.nameWithOwner,private:n.isPrivate,pushedAt:n.pushedAt||"",defaultBranch:n.defaultBranchRef?.name||"",description:n.description||""}),"repoRow"),y=i((n,u)=>{const a=n.github||n.remote,c=x(a,e),p=Object.entries(u.sites||{}).filter(([,f])=>D(f.github||f.remote,a)&&typeof f.owner?.repo=="string"&&T(h.resolve(f.owner.repo)).toLowerCase()===T(c).toLowerCase()).map(([f,d])=>({route:f,title:d.title||f}));return{...n,clone:c,cloned:v.lstatSync(h.join(c,".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,shared:p}},"decorate");async function g(n,u){const a=await n(t,["ls-remote","--symref",u,"HEAD","refs/heads/*"]);if(a.status!==0)return{failed:a};const c=a.stdout,p=[...new Set([...c.matchAll(/^[\da-f]+\s+refs\/heads\/(.+)$/gm)].map(d=>d[1]).filter(d=>C(E,d)))],f=/^ref:\s+refs\/heads\/(.+)\s+HEAD$/m.exec(c)?.[1];return{defaultBranch:p.includes(f)?f:p[0]||"",branches:p}}i(g,"lsRemote");async function J(n){const u=await g(F,`https://github.com/${n}.git`);return u.failed?null:u}i(J,"remoteBranches");async function K(n){const u=await g(L,n);u.failed&&w(N(u.failed,n),404);const{host:a,path:c}=k(n);return{remote:n,host:a,name:`${a}/${c}`,github:"",pushedAt:"",defaultBranch:u.defaultBranch,description:""}}i(K,"findRemote");async function Q(n){const u=await t("gh",["repo","view",n,"--json","nameWithOwner,isPrivate,description,pushedAt,defaultBranchRef"]);if(u.status===0){const c=JSON.parse(u.stdout);return S(c.nameWithOwner),m(c)}return(await F(t,["ls-remote","--heads",`https://github.com/${n}.git`])).status!==0?null:{github:n,private:!1,pushedAt:"",defaultBranch:"",description:""}}i(Q,"find");async function V(){if((await t("gh",["auth","status"])).status!==0)return{signedIn:!1,repos:[]};const u=await R(t,["api","user"]),a=await R(t,["api","user/orgs","--paginate","--slurp"]),c=[...new Set([u.login,...a.flat().map(d=>d.login)])],p=await Promise.all(c.map(d=>R(t,["repo","list",d,"--limit","100000","--json","nameWithOwner,isPrivate,pushedAt,defaultBranchRef,description"]))),f=new Map;for(const d of p.flat())C(S,d.nameWithOwner)&&f.set(d.nameWithOwner.toLowerCase(),m(d));return{signedIn:!0,login:u.login,owners:c,repos:[...f.values()].sort((d,j)=>(j.pushedAt||"").localeCompare(d.pushedAt||"")||d.github.localeCompare(j.github))}}return i(V,"discover"),{base:h.resolve(e),path:i(n=>x(n,e),"path"),cloned:i(n=>v.lstatSync(h.join(x(n,e),".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,"cloned"),async list(n={},{fresh:u=!1}={}){return(u||!r||s()-r.at>=6e4)&&(o||=V().then(a=>(r={at:s(),value:a},a)).finally(()=>{o=null}),await o),{...r.value,repos:r.value.repos.map(a=>y(a,n))}},async lookup(n,u={}){const a=W(n),c=a?null:O(n);!a&&!c&&w($(n)?M:"Repo must be owner/name on GitHub.");const p=a?a.toLowerCase():b(c.remote).join("/");let f=l.get(p);if(!f||s()-f.at>=6e4){for(const[j,X]of l)s()-X.at>=6e4&&l.delete(j);f={at:s(),pending:a?Q(a):K(c.remote)},l.set(p,f),f.pending.catch(()=>{l.get(p)===f&&l.delete(p)})}const d=await f.pending;return d||w("Not found, or you don't have access.",404),y({...d,listed:!1},u)},async branches(n){if($(n)){k(n);const a=await g(L,n);return a.failed&&w(N(a.failed,n),502),a}const u=S(n);try{const a=await R(t,["api",`repos/${u}`]),c=await R(t,["api",`repos/${u}/branches?per_page=100`,"--paginate","--slurp"]);return{defaultBranch:a.default_branch,branches:c.flat().map(p=>p.name).filter(p=>C(E,p))}}catch(a){const c=await J(u);if(!c)throw a;return c}},async ensure(n,u="main",{progress:a}={}){const c=x(n,e);return E(u),v.mkdirSync(h.dirname(c),{recursive:!0}),et(h.join(h.resolve(e),".locks",...b(n)),async()=>{if(x(n,e),v.existsSync(c))return await H(t,c,n),await q(t,c,n,u),c;const p=v.mkdtempSync(h.join(h.resolve(e),".clone-"));try{const f=await U(n)(t,["clone","--progress",ut(n),h.join(p,"repo")],a?{onStderr:nt(a)}:{});f.status!==0&&w($(n)?N(f,n):_(f,"git clone failed"),502),await H(t,h.join(p,"repo"),n),await q(t,h.join(p,"repo"),n,u),x(n,e),v.renameSync(h.join(p,"repo"),c)}finally{v.rmSync(p,{recursive:!0,force:!0})}return c},{timeout:10*6e4,heartbeat:!0})}}}i(bt,"repoService");export{nt as cloneMeter,xt as defaultBranch,ht as gitReachFault,W as githubFromInput,st as githubFromOrigin,at as githubGitArgv,Ct as isClone,$ as isRemote,O as remoteFromInput,lt as remoteGitEnv,St as repoLookupRoute,B as repoName,b as repoParts,x as repoPath,bt as repoService,z as runRepo,E as validateBranch,S as validateGithub,k as validateRemote};
|
package/hub/schedule.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var S=Object.defineProperty;var i=(t,e)=>S(t,"name",{value:e,configurable:!0});import{FOREVER as c,durationEnd as O,inRange as H}from"./duration.mjs";const w=Object.freeze({quiet:Object.freeze({start:"22:00",end:"08:00",hold:!0}),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),N="never",f=Object.freeze({present:1,away:5,lowPresent:5,lowAway:15}),A=1e3,Q=f.lowAway,h=/^([01]\d|2[0-3]):([0-5]\d)$/,m=f.away*6e4,d=i(t=>{const[e,n]=t.split(":").map(Number);return e*60+n},"minutes"),a=i((t,e,n=0)=>{const[o,r]=e.split(":").map(Number);return new Date(t.getFullYear(),t.getMonth(),t.getDate()+n,o,r)},"atOn"),y=i((t,e)=>{const n=a(t,e);return n>t?n:a(t,e,1)},"nextAt"),q=i((t,e)=>{const n=a(t,e);return n<=t?n:a(t,e,-1)},"lastAt"),x=i(t=>({quiet:{...t.quiet},notifications:{...t.notifications}}),"copy");function v({present:t,remaining:e=null}){const n=e!==null&&e<A;return t?n?f.lowPresent:f.present:n?f.lowAway:f.away}i(v,"checkEvery");function U({last:t,present:e,remaining:
|
|
1
|
+
var S=Object.defineProperty;var i=(t,e)=>S(t,"name",{value:e,configurable:!0});import{FOREVER as c,durationEnd as O,inRange as H}from"./duration.mjs";const w=Object.freeze({quiet:Object.freeze({start:"22:00",end:"08:00",hold:!0}),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),N="never",f=Object.freeze({present:1,away:5,lowPresent:5,lowAway:15}),A=1e3,Q=f.lowAway,h=/^([01]\d|2[0-3]):([0-5]\d)$/,m=f.away*6e4,d=i(t=>{const[e,n]=t.split(":").map(Number);return e*60+n},"minutes"),a=i((t,e,n=0)=>{const[o,r]=e.split(":").map(Number);return new Date(t.getFullYear(),t.getMonth(),t.getDate()+n,o,r)},"atOn"),y=i((t,e)=>{const n=a(t,e);return n>t?n:a(t,e,1)},"nextAt"),q=i((t,e)=>{const n=a(t,e);return n<=t?n:a(t,e,-1)},"lastAt"),x=i(t=>({quiet:{...t.quiet},notifications:{...t.notifications}}),"copy");function v({present:t,remaining:e=null}){const n=e!==null&&e<A;return t?n?f.lowPresent:f.present:n?f.lowAway:f.away}i(v,"checkEvery");function U({last:t,present:e,quiet:n=!1,remaining:o=null,backAt:r=null}){return r&&r>t&&!n?new Date(r):new Date(t.getTime()+v({present:e&&!n,remaining:o})*6e4)}i(U,"nextCheckAt");function _(t){const e=t?.on??w.notifications.on,n=t?.weekends_off??w.notifications.weekends_off,o=t?.snoozed_until??null;return typeof e!="boolean"?{error:"Notifications must be on or off."}:typeof n!="boolean"?{error:"Off on weekends must be on or off."}:o!==null&&o!==c&&(typeof o!="string"||Number.isNaN(Date.parse(o)))?{error:"Snooze must end at a date and time, or be forever."}:{notifications:{on:e,weekends_off:n,snoozed_until:o===null||o===c?o:new Date(o).toISOString()}}}i(_,"validateNotifications");function b(t){const{start:e,end:n,hold:o=w.quiet.hold}=t?.quiet||{},{notifications:r,error:s}=_(t?.notifications);return s?{error:s}:typeof e!="string"||!h.test(e)?{error:"Quiet hours' start must be HH:MM (24-hour)."}:typeof n!="string"||!h.test(n)?{error:"Quiet hours' end must be HH:MM (24-hour)."}:e===n?{error:"Quiet hours must start and end at different times."}:typeof o!="boolean"?{error:"Hold pushes must be on or off."}:{settings:{quiet:{start:e,end:n,hold:o},notifications:r}}}i(b,"validate");function T(t){const e=(Array.isArray(t?.windows)?t.windows:[]).map(n=>n?.from).filter(n=>typeof n=="string"&&h.test(n)).sort((n,o)=>d(n)-d(o));return e.length<2?null:{start:e.at(-1),end:e[0],hold:t.hold_overnight!==!1}}i(T,"fromPolling");function W(t){const e=_(t?.notifications).notifications||x(w).notifications;return b({quiet:t?.quiet,notifications:e}).settings||b({quiet:T(t?.polling),notifications:e}).settings||{...x(w),notifications:e}}i(W,"quietOf");function Y(t,e){const n={...t.quiet};return"start"in e&&(n.start=e.start),"end"in e&&(n.end=e.end),"hold"in e&&(n.hold=e.hold==="on"),{quiet:n,notifications:t.notifications}}i(Y,"editQuiet");function $(t,e){const n=t.getHours()*60+t.getMinutes(),[o,r]=[d(e.quiet.start),d(e.quiet.end)];return o<r?n>=o&&n<r:n>=o||n<r}i($,"quietAt");function K(t,e){if(!$(t,e))return null;const n=q(t,e.quiet.start),o=(d(e.quiet.end)-d(e.quiet.start)+1440)%1440,r=new Date(n.getFullYear(),n.getMonth(),n.getDate(),n.getHours(),n.getMinutes()+o/2);return r<=t?r:null}i(K,"quietMiddleOf");function C(t,e){return e.quiet.hold&&$(t,e)&&t-q(t,e.quiet.start)>m}i(C,"holdsPushes");function D(t,e){if(!e.notifications?.weekends_off)return null;let n=a(t,e.quiet.start,-((t.getDay()-5+7)%7));n>t&&(n=a(n,e.quiet.start,-7));const o=a(n,e.quiet.end,3);return t-n>m&&t<o?o:null}i(D,"weekendEnd");function R(t,e){if(e.notifications?.on===!1)return{until:N,why:"off"};if(e.notifications?.snoozed_until===c)return{until:c,why:"snoozed"};const n=e.notifications?.snoozed_until?new Date(e.notifications.snoozed_until):null,o=i(u=>(n>u?["snoozed",n]:null)||D(u,e)&&["weekend",D(u,e)]||C(u,e)&&["overnight",y(u,e.quiet.end)],"at"),r=o(t);if(!r)return null;let s=r[1];for(let u=o(s),p=0;u&&p<8;u=o(s),p++)s=u[1];return{until:s,why:r[0]}}i(R,"hold");function j(t,e){return R(t,e)?.until??null}i(j,"heldUntil");function V(t,e){return j(t,e)??new Date(t.getTime()+f.away*6e4)}i(V,"flushAt");function G(t,e,n){const o=String(e);let r;if(o==="morning")r=y(t,n.quiet.end);else if(o==="tonight")r=y(t,n.quiet.start);else if(o==="monday")r=a(t,n.quiet.end,(8-t.getDay())%7),r<=t&&(r=a(r,n.quiet.end,7));else return O(t,o);return H(t,r)?r:null}i(G,"snoozeEnd");function B(t,e=new Date){if(t.why==="off")return"notifications are off";if(t.until===c)return"snoozed until you end it";const n=k(t.until,e);return t.why==="snoozed"?`snoozed until ${n}`:t.why==="weekend"?`held for the weekend until ${n}`:`held overnight until ${n}`}i(B,"holdLine");function F(t){return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`}i(F,"localDay");function J(t,e,n={}){const o=e.quiet.end;return a(t,o)<=t&&n?.[o]!==F(t)?[o]:[]}i(J,"wakesDue");const I=i(t=>/^\/([^/?#]+)(?:[/?#]|$)/.exec(t||"")?.[1]??null,"routeOf");function X(t,e={}){const n=new Map;t.forEach((r,s)=>{r.tag&&n.set(r.tag,s)});const o=new Map;return t.filter((r,s)=>!r.tag||n.get(r.tag)===s).forEach(r=>{const s=I(r.url)??Symbol(r.id);o.set(s,[...o.get(s)||[],r])}),[...o].flatMap(([r,s])=>{if(s.length===1){const[l]=s;return[{title:l.title,body:l.body||"",url:l.url||"/",tag:l.tag||void 0,kind:l.kind,members:[l.id]}]}const u=e[r];if(u&&!u.count)return[];const p=r==="_hub"?"Hub":r,g=s.map(l=>l.title),z=u?`${u.count} need${u.count===1?"s":""} you on ${p}`:`${s.length} updates on ${p}`,E=u?u.summary:[...g.slice(0,2),...g.length>2?[`and ${g.length-2} more`]:[]].join(" \xB7 "),M=s.every(l=>l.kind==="action")?"action":s.find(l=>l.kind!=="action").kind;return[{title:z,body:E,url:`/${r}/`,tag:`summary:${r}`,kind:M,members:s.map(l=>l.id)}]})}i(X,"notifications");const Z=i((t,e=new Date)=>t===c?"once the snooze ends":k(t,e),"sendWords");function k(t,e=new Date){const n=t.getHours(),o=`${n%12||12}:${String(t.getMinutes()).padStart(2,"0")}${n<12?"am":"pm"}`,r=Math.round((a(t,"00:00")-a(e,"00:00"))/864e5);return r===0?o:r===1?`${o} tomorrow`:`${o} ${t.toDateString().slice(0,10)}`}i(k,"clock");export{f as CHECK_MINUTES,w as DEFAULTS,A as LOW_POINTS,N as NEVER,Q as SLOWEST_CHECK_MINUTES,v as checkEvery,k as clock,Y as editQuiet,V as flushAt,T as fromPolling,j as heldUntil,R as hold,B as holdLine,C as holdsPushes,F as localDay,U as nextCheckAt,X as notifications,$ as quietAt,K as quietMiddleOf,W as quietOf,Z as sendWords,G as snoozeEnd,b as validate,J as wakesDue};
|
package/hub/try.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),T.chmodSync(y,384),y}a($t,"failureLog");const Ie=a(c=>new Promise(u=>{const m=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},y=>{y.destroy(),u(!0)});m.on("timeout",()=>m.destroy()),m.on("error",()=>u(!1))}),"probe"),Ut=a((c,u)=>{try{process.kill(-c.pid,u)}catch{}},"signalGroup");async function Jt(c,u,m){const y=await ye(c.dir,u),[S,p]=u.split("/"),k=c.files.find(v=>v.slug===S)?.itemFile,d=y?.path||k?.items.find(v=>v.id===p)?.worktree;if(typeof d!="string"||!d||d.split(E.sep).includes(".."))return"";let g=y?.repo;const $=(c.github||k?.repo||"").toLowerCase();if(!g&&$){const v=[c.owner?.repo,c.dir,...Object.values(m?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(v.filter(P=>typeof P=="string"&&E.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await j(I,"remote","get-url","origin"))?.[1].toLowerCase()===$){g=await j(I,"rev-parse","--show-toplevel");break}}catch{}}if(!y?.path&&!g||y?.path&&!E.isAbsolute(d))return"";try{const v=[E.join(T.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&E.isAbsolute(g))try{v.push(E.join(T.realpathSync(g),".claude","worktrees"))}catch{}const I=E.resolve(g||"/",d),P=T.realpathSync(I);return v.some(W=>I.startsWith(W+E.sep)&&P.startsWith(W+E.sep))&&T.statSync(P).isDirectory()?P:""}catch{return""}}a(Jt,"installWorktree");function Ce({now:c=Date.now,answers:u=Ie,log:m=console.log,waiting:y=a(async()=>null,"waiting"),event:S=ae,gate:p=ke}={}){const k=new Map,d=new Map,g=new Set,$=new Set,v=new Map,I=new Map,P=new Map,W=new Map;let G=!1,H=null,Q=null,ot=null;const at=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=E.join(t.dir,"data","try.json"),s=nt(e,{kind:null,main:null,cards:{}}),i={...wt(s.main_port)?{main_port:s.main_port}:{},kind:s.kind||null,main:s.main||null,cards:{...s.cards},...s.pull?{pull:{...s.pull,running:void 0}}:{}},r={state:i,file:e,written:JSON.stringify(s)};k.set(t.dir,r),i.main_port&&I.set(t.dir,i.main_port);const n=Object.entries(i.cards).filter(([,o])=>o.state==="running");for(const[o]of n)$.add(D(t,o));return i.kind==="web"?(i.cards=Object.fromEntries(n),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),s.prototypes?.state==="failed"&&(i.prototypes=s.prototypes),M(t),r}a(N,"stateOf");function M(t){const e=k.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Rt(e.file,e.state),e.written=s)}a(M,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>F(e)?t.cards[e]:t[lt[e]],"entryOf"),zt=a((t,e,s)=>{F(e)?t.cards[e]=s:t[lt[e]]=s},"setEntry"),kt=a((t,e)=>{F(e)?delete t.cards[e]:t[lt[e]]=null},"dropEntry");function X(t,e,s){const{state:i}=N(t),r=St(i,e);if(!r)return;const n=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,F(e)&&n?{at:at()}:{}),M(t)}a(X,"update");function dt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(s=>s.site)];for(const s of e){if(!s?.dir)continue;const i=nt(E.join(s.dir,"data","try.json"),{}).main_port;wt(i)&&I.set(s.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:s}=t,i=D(e,s),r=s==="main"?I.get(e.dir):v.get(i);if(r)return r;const n=new Set([...I.values(),...v.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return s==="main"?(I.set(e.dir,o),N(e).state.main_port=o,M(e)):v.set(i,o),o}a(Yt,"publicPort");const rt=a(async t=>{const e=pe();if(!e)throw new Error("Tailscale isn't installed on this host");return yt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ot(t){try{await rt(["serve",`--https=${t}`,"off"])}catch(e){m(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ot,"removePort");const Tt=a(t=>xe(nt(E.join(t.dir,"board.json"),{})),"sharedWith");async function Et(t,e){const s=await p(t),i=t.headers["tailscale-user-login"];return s!=="other"||!i||!Tt(e).includes(i)?s:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Et,"relayVerdict");function Vt(){for(const t of d.values()){if(!t.shared?.size)continue;const e=Tt(t.board);for(const s of t.shared)e.includes(s.login)||(t.shared.delete(s),s.res.destroy())}}a(Vt,"endUnshared");async function Qt(t){t.https=Yt(t);const e=t.sockets=new Set,s=a(()=>{t.visited=c()},"visit"),i=a(n=>{e.add(n),n.on("data",s),n.on("close",()=>e.delete(n))},"track"),r=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const w=await Et(n,t.board);if(w!=="owner"&&w!=="shared"){Te(n,o,w);return}if(w==="shared"){const h={login:n.headers["tailscale-user-login"],res:o};r.add(h),o.on("close",()=>r.delete(h))}const _=Object.fromEntries(Object.entries(n.headers).filter(([h])=>!Gt.includes(h))),f=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:_},h=>{o.writeHead(h.statusCode,h.headers),h.on("data",s),h.pipe(o),h.on("close",()=>{h.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||f.destroy()}),f.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(f)}),t.relay.on("connection",n=>{s(),i(n)}),t.relay.on("upgrade",async(n,o,w)=>{const _=await Et(n,t.board);if(_!=="owner"){Ee(o,_==="shared"?"view-only":_);return}i(Oe({req:n,socket:o,head:w,port:t.port,path:n.url,replace:Gt}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await rt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(Qt,"openPort");async function Xt(t){if(t.closing=!0,await t.opening?.catch(()=>{}),t.exposed&&!G&&await Ot(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||v.delete(D(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...d.values()].filter(s=>s.ready&&s.relay&&!s.closing);if(!t.length)return;let e;try{e=JSON.parse((await rt(["serve","status","--json"])).stdout)}catch(s){m(`try ports check: ${(s.stderr||s.message).trim()}`);return}for(const s of t){const i=`http://127.0.0.1:${s.relay.address().port}`,r=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${s.https}`))?.[1];if(r?.Handlers?.["/"]?.Proxy!==i){m(`try port ${s.https}: its serve mapping was ${r?"changed":"gone"}; serving it again`);try{await rt(["serve","--bg",`--https=${s.https}`,i])}catch(n){m(`try port ${s.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(Zt,"reserve");function st(t=z()){if(ot)return ot;dt(t);for(const e of J(t))N(e);return ot=(async()=>{try{const{stdout:e}=await rt(["serve","status","--json"]),s=JSON.parse(e);for(const i of Object.keys(s?.TCP||{}))wt(i)&&![...d.values()].some(r=>r.https===Number(i))&&await Ot(i)}catch(e){m(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ot}a(st,"initialize");async function xt(t,e,s,i){const{board:r,key:n}=t;if(X(r,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;let o="",w=!1;try{for await(const h of Dt({input:T.createReadStream(t.logFile),crlfDelay:1/0}))w||=_e.test(h),h.trim()&&(o=h)}catch(h){o=`could not read the build log: ${h.message}`}const _=t.port||(N(r).state.kind||(r.files.some(({itemFile:h})=>h.items.some(O=>O.install?.url))?"app":"web"))==="web"?"Serve":"Build",f=w?"unlock the login keychain on the host":i?.message||s&&`stopped by ${s}`||o;await me(r.dir,n,"run-failed",{kind:_,code:e??1,why:f})}a(xt,"failure");function ct(t){return t.ending||(Ut(t.child,"SIGTERM"),t.ending=new Promise(e=>{const s=a(()=>{clearTimeout(i),clearInterval(r),e()},"finish");let i=setTimeout(()=>{Ut(t.child,"SIGKILL"),i=setTimeout(s,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{s()}},20)})),t.ending}a(ct,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ct(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,s,i={}){if(!s)throw new x("This card has no valid worktree.");const r=Lt(s,t.build_target),n=he(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(s)+E.sep))throw new x("Build script must be inside the worktree.");const o=await j(s,"rev-parse","HEAD");if(G)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:w}=N(t),_=St(w,e),f={...F(e)?_:{},...i,sha:o,script:!0,state:"running",...F(e)?{at:at(),head:o}:{},...e===K?{since:at()}:{}};delete f.port,delete f.https;const h=Kt(t,e);T.mkdirSync(E.dirname(h),{recursive:!0});const O=T.openSync(h,"w",384);T.fchmodSync(O,384);const b=ne(r,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(w,e,f),M(t);const R=D(t,e),l={child:b,board:t,key:e,logFile:h,visited:c(),stopped:!1,port:null,ready:!1};d.set(R,l),P.set(R,o);let B,C;const q=a(A=>{C||=new Error(A),ct(l)},"fail");b.on("error",A=>{B=A}),b.stdout.on("data",A=>T.writeSync(O,A)),b.stderr.on("data",A=>T.writeSync(O,A));const L=Dt({input:b.stdout,crlfDelay:1/0});return L.on("line",A=>{const it=/^port (\d{1,5})$/.exec(A);if(!it||l.port||l.stopped)return;const U=Number(it[1]);if(U<1||U>65535){q("Build script printed an invalid port.");return}l.port=U,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const Mt=a(async()=>{if(!(pt||l.stopped||l.ready||b.exitCode!==null||b.signalCode!==null)){pt=!0;try{if(await u(U)){if(l.stopped||C||b.exitCode!==null||b.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||C||b.exitCode!==null||b.signalCode!==null))return;l.ready=!0,l.visited=c(),w.kind="web",X(t,e,{state:"ready",port:U,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>Mt().catch(ht=>q(ht.message)),250),Mt().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{ct(l)}),l.done=new Promise(A=>b.once("close",(it,U)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(O),(async()=>{if(await ct(l),await Xt(l),l.finished=!0,l.stopped)return;const tt=e===K&&!l.port?new Error('Build script exited without printing "port <n>".'):null;C||B||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,U,C||B||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):kt(w,e),M(t)):(w.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>m(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{d.get(R)===l&&d.delete(R),A()})})),m(`build /${t.route} ${e}: started; log ${h}`),{status:"started"}}a(jt,"launch");async function It(t,e,s){const i=D(t,"main");if(g.has(i))throw new x("A build is already running for main.",409);g.add(i);try{d.has(i)&&await Z(d.get(i),!0);const r=E.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(E.dirname(r),{recursive:!0}),gt(r)?await j(r,"checkout","--detach","--force",s.sha):(await j(e,"worktree","prune"),await j(e,"worktree","add","--detach",r,s.sha)),await jt(t,"main",r,s)}catch(r){throw r.status!==503&&(N(t).state.main={...s,state:"failed"},$t(t,"main",r),M(t)),r}finally{g.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await st(e),dt(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(f=>!["route","key"].includes(f))||typeof t.route!="string"||typeof t.key!="string")throw new x("Send only the board route and card key.");const{route:s,key:i}=t;if(F(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(f=>f==="."||f==="..")))throw new x("Card key must be <file>/<id>.");const r=J(e).find(f=>f.route===s);if(!r)throw new x(`/${s} is not a board.`,404);if(F(i)&&!r.files.some(({slug:f,itemFile:h})=>h.items.some(O=>`${f}/${O.id}`===i)))throw new x("There is no item with this card key.",404);const n=await y();if(n)throw new x(n,409);const o=D(r,i),w=d.get(o);if(g.has(o)||w&&!w.ready&&!w.finished)throw new x(`A build is already running for ${i==="main"?"main":i===K?"the prototype site":"this card"}.`,409);if(i==="main"){const f=N(r).state.main;if(f?.state==="running")throw new x("A build is already running for main.",409);if(f?.state!=="failed"||!f.sha)throw new x("Main has no failed build to retry.",409);const{sha:h,branch:O,slug:b,at:R,script:l}=f;return It(r,et(s,e).repo,{sha:h,branch:O,slug:b,at:R,script:l,...r.build_target?{target:r.build_target}:{}})}g.add(o);const _=$.delete(o);try{d.has(o)&&await Z(d.get(o),!0);const f=i===K?Ct(r):await Jt(r,i,e);return await jt(r,i,f)}catch(f){throw v.delete(o),_&&f.status!==503&&X(r,i,{state:void 0,sha:void 0}),f}finally{g.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=$e(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function qt(t,e){const{state:s}=N(t),i=D(t,K),r=d.get(i),n=(await we(t.dir)).length>0&&Ct(t);if(!n){r&&await Z(r),s.prototypes&&(s.prototypes=null,M(t));return}if(r?.ready&&!r.stopped&&!g.has(i)){const o=await j(n,"rev-parse","HEAD");if(o===s.prototypes?.sha)return;m(`try /${t.route} prototypes: the base moved to ${o.slice(0,7)}; restarting its serve`)}else if(r||g.has(i)||s.prototypes?.state==="failed"||await y())return;try{await ut({route:t.route,key:K},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw s.prototypes={state:"failed"},$t(t,K,o),M(t),o}}a(qt,"serveBase");function Pt(t,e,{manifest:s=z()}={}){const i=J(s).find(n=>n.route===t);if(!i)return null;const r=[...d.values()].find(n=>n.board.dir===i.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Pt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await st(t),dt(t),G)return H;if(H&&e){if(!Q){const s={routes:new Set,manifest:t};s.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:s.manifest,routes:[...s.routes]}))),Q=s}Q.manifest=t;for(const s of e)Q.routes.add(s);return Q.done}return H||(H=(async()=>{await Zt();const s=J(t),i=nt(ce,{}).prs||{};for(const r of[...d.values()])if(!s.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=je){await Z(r);const{state:n}=N(r.board);kt(n,r.key),M(r.board)}for(const r of s){if(G)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),w=o.branch||"main",_=le(o);for(const{slug:f,itemFile:h}of r.files)for(const O of h.items){const b=`${f}/${O.id}`,R=be(O,r.github||h.repo).map(l=>i[Se(l)]);if(!(O.lane==="done"||R.length&&R.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,b),B=await Jt(r,b,t);if(B){const L=await j(B,"rev-parse","HEAD");let A=0;try{A=Number(await j(B,"rev-list","--count",`${_}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:L,ahead:A,script:gt(Lt(B,r.build_target))}),M(r)}else if(!$.has(l))continue;const C=n.cards[b];if(($.has(l)||r.auto_build&&C.script&&C.ahead>0&&(C.sha||P.get(l))!==C.head&&C.state!=="running")&&!g.has(l))try{await ut({route:r.route,key:b},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,b,{sha:C.head}),await xt({board:r,key:b,logFile:$t(r,b,L)},1),L}}catch(l){m(`try /${r.route} ${b}: ${l.message}`)}}await qt(r,t).catch(f=>m(`try /${r.route} prototypes: ${f.message}`));try{if(!o.repo||await y())continue;const f=await j(o.repo,"rev-parse",_);let h=!1;try{await j(o.repo,"cat-file","-e",`${f}:${Wt(r.build_target)}`),h=!0}catch{}if(!h){const C=d.get(D(r,"main"));C&&await Z(C),n.main=null,M(r);continue}const O=n.main;if(O?.state==="running"||O?.sha===f&&(O.target||null)===(r.build_target||null)&&(O.state==="failed"||O.state==="ready"&&(n.kind!=="web"||Pt(r.route,"main",{manifest:t}))))continue;const b=D(r,"main");if(g.has(b))continue;const R=await j(o.repo,"log","-1","--format=%cI",f);let l=r.github;if(!l)try{l=Ft(await j(o.repo,"remote","get-url","origin"))}catch{}const B={sha:f,branch:w,slug:l||"",at:R,script:h,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,B)}catch(f){m(`try /${r.route} main: ${f.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function At(t,e){if(W.has(t.dir))return W.get(t.dir);const s=(async()=>{const i=et(t.route,e),r=i.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},M(t);let o,w;try{o=await Pe(i.repo,r)}catch(_){w=_.message}if(o===null?delete n.pull:n.pull={at:at(),...w?{error:w}:{}},M(t),o===null)return null;if(w)return m(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){m(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await S(t,"main-pulled",{branch:r,...o})}catch(_){m(`pull /${t.route}: could not send main-pulled (${_.message})`)}}return{commits:o.commits}})().finally(()=>W.delete(t.dir));return W.set(t.dir,s),s}a(At,"pullMain");async function te({manifest:t=z(),routes:e}={}){await st(t);const s=[];for(const i of J(t)){if(G)break;if(e&&!e.includes(i.route))continue;const r=et(i.route,t);!r.local||!r.repo||(await At(i,t))?.commits&&s.push(i.route)}return s}a(te,"pullMains");async function ee(t,{manifest:e=z()}={}){if(await st(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const s=J(e).find(n=>n.route===t.route);if(!s)throw new x(`/${t.route} is not a board.`,404);const i=et(s.route,e);if(!i.local||!i.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await At(s,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[s.route]}).catch(n=>m(`try /${s.route} main: ${n.message}`)),r}a(ee,"pull");async function re(){G=!0;const t=Promise.all([...d.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...W.values()])}return a(re,"stop"),{start:ut,tick:ft,stop:re,lookup:Pt,initialize:st,pull:ee,pullMains:te,endUnshared:Vt}}a(Ce,"createTryManager");const bt=1e4,vt=a(async(c,...u)=>(await yt("git",["-C",c,...u],{encoding:"utf8",timeout:bt,env:ue()})).stdout,"originGit");async function Pe(c,u,{run:m=vt}={}){const y=a(async(...p)=>(await m(...p)).trim(),"git"),S=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await y(c,"config","--get","remote.origin.url")}catch{return null}try{await y(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${u}:refs/remotes/origin/${u}`)}catch(p){S(p.killed?`git fetch timed out after ${bt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await y(c,"rev-parse","--verify",`refs/heads/${u}^{commit}`),k=await y(c,"rev-parse","--verify",`refs/remotes/origin/${u}^{commit}`),d=a((v,I)=>y(c,"merge-base","--is-ancestor",v,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await d(k,p))return{from:p,to:p,commits:0};await d(p,k)||S(`${u} has split from origin`);const g=Number(await y(c,"rev-list","--count",`${p}..${k}`)),$=(await y(c,"worktree","list","--porcelain")).split(`
|
|
1
|
+
var we=Object.defineProperty;var a=(o,d)=>we(o,"name",{value:d,configurable:!0});import j from"node:fs";import O from"node:path";import Gt from"node:os";import Ot from"node:http";import{execFile as ye,spawn as ge}from"node:child_process";import{createInterface as Lt}from"node:readline";import{promisify as $e}from"node:util";import{listBoards as z,readManifest as V,readJSON as lt,saveJSON as Ut,appendEvent as ve,GITHUB_STATE as be,REPO_FETCH as Kt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Jt,mainRefOf as ke}from"./board-repo.mjs";import{gitReachFault as Et,remoteFromInput as Se,remoteGitEnv as Oe}from"./repos.mjs";import{readiness as Ee}from"./readiness.mjs";import{tailscaleBin as Te}from"../shared/tailscale.mjs";import{buildScriptError as xe,buildScriptOf as zt}from"../board/build-script.mjs";import{appendStatus as Yt,recordedWorktree as je,servedRoundsOpen as _e,setItem as Ie}from"../board/store.mjs";import{BASE_KEY as L,baseWorktreeOf as Ae}from"../board/prototype-base.mjs";import{cardPRs as Me,hasPR as Pe,prKeyOf as Ce}from"../shared/card-prs.mjs";import{hubGate as Ne}from"../board/on-host.mjs";import{pipeUpgrade as De,refuse as Fe,refuseUpgrade as Re}from"./front-door.mjs";import{shareOf as Be}from"./share.mjs";class _ extends Error{static{a(this,"BuildError")}constructor(d,y=400){super(d),this.status=y}}const dt=$e(ye),I=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),He=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,q={first:8500,last:8599},Tt=a(o=>/^\d+$/.test(String(o))&&Number(o)>=q.first&&Number(o)<=q.last,"isTryPort"),We=30*6e4,Qt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Ge=["x-frame-options"],Le=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([y])=>!Ge.includes(y)));if(typeof d["content-security-policy"]=="string"){const y=d["content-security-policy"].split(";").filter(l=>!/^\s*frame-ancestors\b/i.test(l)).join(";").trim();y?d["content-security-policy"]=y:delete d["content-security-policy"]}return d},"withoutFraming"),ht={main:"main",[L]:"prototypes"},R=a(o=>!Object.hasOwn(ht,o),"isCard"),Vt=a((o,d)=>O.join(o,zt(d)),"scriptAt"),mt=a(o=>j.existsSync(o),"exists"),qt=a((o,d)=>O.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function xt(o,d,y){const l=qt(o,d);return j.mkdirSync(O.dirname(l),{recursive:!0}),j.writeFileSync(l,y.message+`
|
|
2
|
+
`,{mode:384}),j.chmodSync(l,384),l}a(xt,"failureLog");const Xt=a(o=>new Promise(d=>{const y=Ot.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},l=>{l.destroy(),d(!0)});y.on("timeout",()=>y.destroy()),y.on("error",()=>d(!1))}),"probe"),Zt=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),te=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),ee=a(o=>new Promise(d=>{const y=a(()=>{clearTimeout(l),clearInterval(g),d()},"finish");Zt(o,"SIGTERM");let l=setTimeout(()=>{Zt(o,"SIGKILL"),l=setTimeout(y,1e3)},4e3);const g=setInterval(()=>{te(o)||y()},20)}),"endGroup");async function Ue(o){const{stdout:d}=await dt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),y=d.split(`
|
|
3
|
+
`).map(l=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(l)).filter(l=>l&&Number(l[2])===o).map(l=>({pid:l[1],command:l[3],cwd:""}));if(!y.length)return y;try{const{stdout:l}=await dt("lsof",["-a","-d","cwd","-Fpn","-p",y.map(f=>f.pid).join(",")],{encoding:"utf8",timeout:5e3});let g;for(const f of l.split(`
|
|
4
|
+
`))f[0]==="p"?g=y.find(k=>k.pid===f.slice(1)):f[0]==="n"&&g&&(g.cwd=f.slice(1))}catch{}return y}a(Ue,"groupMembers");const Ke=a((o,d)=>o===d||o.startsWith(d+O.sep),"insideDir"),Je=a((o,d)=>Ke(o.cwd,d)||o.command.includes(d+O.sep)||o.command.endsWith(d),"inWorktree");async function re(o,d,y){const l=await je(o.dir,d),[g,f]=d.split("/"),k=o.files.find(S=>S.slug===g)?.itemFile,$=l?.path||k?.items.find(S=>S.id===f)?.worktree;if(typeof $!="string"||!$||$.split(O.sep).includes(".."))return"";let m=l?.repo;const p=(o.github||k?.repo||"").toLowerCase();if(!m&&p){const S=[o.owner?.repo,o.dir,...Object.values(y?.sites||{}).map(E=>E.owner?.repo)];for(const E of new Set(S.filter(M=>typeof M=="string"&&O.isAbsolute(M))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await I(E,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await I(E,"rev-parse","--show-toplevel");break}}catch{}}if(!l?.path&&!m||l?.path&&!O.isAbsolute($))return"";try{const S=[O.join(j.realpathSync(Gt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&O.isAbsolute(m))try{S.push(O.join(j.realpathSync(m),".claude","worktrees"))}catch{}const E=O.resolve(m||"/",$),M=j.realpathSync(E);return S.some(U=>E.startsWith(U+O.sep)&&M.startsWith(U+O.sep))&&j.statSync(M).isDirectory()?M:""}catch{return""}}a(re,"installWorktree");function ze({now:o=Date.now,answers:d=Xt,lingers:y=Xt,log:l=console.log,waiting:g=a(async()=>null,"waiting"),event:f=ve,gate:k=Ne}={}){const $=new Map,m=new Map,p=new Map,S=a(()=>[...m.values(),...p.values()],"live"),E=new Set,M=new Set,U=new Map,rt=new Map,gt=new Map,it=new Map;let H=!1,W=null,Z=null,ut=null;const ft=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if($.has(t.dir))return $.get(t.dir);const e=O.join(t.dir,"data","try.json"),r=lt(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},...Tt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},i={state:s,file:e,written:JSON.stringify(r)};$.set(t.dir,i),s.main_port&&rt.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,c])=>c.state==="running");for(const[c]of n)M.add(D(t,c));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),A(t),i}a(C,"stateOf");function A(t){const e=$.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Ut(e.file,e.state),e.written=r)}a(A,"persist");function ie(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),A(t.board)}a(ie,"recordGroup");function se(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,A(t.board))}a(se,"forgetGroup");async function ne(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,A(t);for(const s of r){if(!te(s.pgid))continue;let i=[];try{i=await Ue(s.pgid)}catch(n){l(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>Je(n,s.worktree))&&(await ee(s.pgid),l(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await jt(t,s.key,s.port))}}a(ne,"sweepGroups");async function jt(t,e,r){if(!r||!await y(r))return;const s=`Port ${r} still answers after the run was stopped.`;R(e)&&await Yt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>l(`build /${t.route} ${e}: ${i.message}`)),l(`build /${t.route} ${e}: ${s}`)}a(jt,"checkQuiet");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),_t=a((t,e)=>R(e)?t.cards[e]:t[ht[e]],"entryOf"),oe=a((t,e,r)=>{R(e)?t.cards[e]=r:t[ht[e]]=r},"setEntry"),It=a((t,e)=>{R(e)?delete t.cards[e]:t[ht[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=_t(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,R(e)&&n?{at:ft()}:{}),A(t)}a(K,"update");function $t(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=lt(O.join(r.dir,"data","try.json"),{}).main_port;Tt(s)&&rt.set(r.dir,Number(s))}}a($t,"reserveMains");function ae(t){const{board:e,key:r}=t,s=D(e,r),i=r==="main"?rt.get(e.dir):U.get(s);if(i)return i;const n=new Set([...rt.values(),...U.values()]);let c=q.first;for(;n.has(c)&&c<=q.last;)c++;if(c>q.last)throw new Error(`No try ports available (${q.first}\u2013${q.last}). Stop a served card and retry.`);return r==="main"?(rt.set(e.dir,c),C(e).state.main_port=c,A(e)):U.set(s,c),c}a(ae,"publicPort");const st=a(async t=>{const e=Te();if(!e)throw new Error("Tailscale isn't installed on this host");return dt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function At(t){try{await st(["serve",`--https=${t}`,"off"])}catch(e){l(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(At,"removePort");const Mt=a(t=>Be(lt(O.join(t.dir,"board.json"),{})),"sharedWith");async function Pt(t,e){const r=await k(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Mt(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Pt,"relayVerdict");function ce(){for(const t of S()){if(!t.shared?.size)continue;const e=Mt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(ce,"endUnshared");async function le(t){t.https=ae(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Ot.createServer(async(n,c)=>{const w=await Pt(n,t.board);if(w!=="owner"&&w!=="shared"){Fe(n,c,w);return}if(w==="shared"){const v={login:n.headers["tailscale-user-login"],res:c};i.add(v),c.on("close",()=>i.delete(v))}const T=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Qt.includes(v))),h=Ot.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:T},v=>{c.writeHead(v.statusCode,t.key===L?Le(v.headers):v.headers),v.on("data",r),v.pipe(c),v.on("close",()=>{v.complete||c.destroy()})});c.on("close",()=>{c.writableFinished||h.destroy()}),h.on("error",()=>{c.headersSent?c.destroy():c.writeHead(502).end()}),n.pipe(h)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,c,w)=>{const T=await Pt(n,t.board);if(T!=="owner"){Re(c,T==="shared"?"view-only":T);return}s(De({req:n,socket:c,head:w,port:t.port,path:n.url,replace:Qt}))}),await new Promise((n,c)=>{t.relay.once("error",c),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await st(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(le,"openPort");async function de(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!H&&!e&&await At(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||U.delete(D(t.board,t.key))}a(de,"closePort");async function ue(){const t=S().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await st(["serve","status","--json"])).stdout)}catch(r){l(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){l(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await st(["serve","--bg",`--https=${r.https}`,s])}catch(n){l(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(ue,"reserve");function nt(t=V()){if(ut)return ut;$t(t);for(const e of z(t))C(e);return ut=(async()=>{for(const e of z(t))await ne(e).catch(r=>l(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await st(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))Tt(s)&&![...m.values()].some(i=>i.https===Number(s))&&await At(s)}catch(e){l(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ut}a(nt,"initialize");async function Ct(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!R(n))return;let c="",w=!1;try{for await(const v of Lt({input:j.createReadStream(t.logFile),crlfDelay:1/0}))w||=He.test(v),v.trim()&&(c=v)}catch(v){c=`could not read the build log: ${v.message}`}const T=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(x=>x.install?.url))?"app":"web"))==="web"?"Serve":"Build",h=w?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||c;await Yt(i.dir,n,"run-failed",{kind:T,code:e??1,why:h})}a(Ct,"failure");function pt(t){return t.ending||(t.ending=ee(t.child.pid)),t.ending}a(pt,"terminate");async function ot(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await pt(t),await t.done}a(ot,"halt");async function tt(t,e=!1){await ot(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||gt.delete(D(t.board,t.key))}a(tt,"stopRun");async function Nt(t,e,r,s={}){if(!r)throw new _("This card has no valid worktree.");const i=Vt(r,t.build_target),n=xe(i);if(n)throw new _(n);if(!j.realpathSync(i).startsWith(j.realpathSync(r)+O.sep))throw new _("Build script must be inside the worktree.");const c=await I(r,"rev-parse","HEAD");if(H)throw new _("The hub is stopping. Try again after it restarts.",503);const{state:w}=C(t),T=_t(w,e),h={...R(e)?T:{},...s,sha:c,script:!0,state:"running",...R(e)?{at:ft(),head:c}:{},...e===L?{since:ft()}:{}};delete h.port,delete h.https;const v=qt(t,e);j.mkdirSync(O.dirname(v),{recursive:!0});const x=j.openSync(v,"w",384);j.fchmodSync(x,384);const b=ge(i,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});oe(w,e,h);const F=D(t,e),u={child:b,board:t,key:e,worktree:r,sha:c,at:s.at,logFile:v,visited:o(),stopped:!1,port:null,ready:!1};ie(u),m.set(F,u),gt.set(F,c);let B,P;const Y=a(N=>{P||=new Error(N),pt(u)},"fail");b.on("error",N=>{B=N}),b.stdout.on("data",N=>j.writeSync(x,N)),b.stderr.on("data",N=>j.writeSync(x,N));const J=Lt({input:b.stdout,crlfDelay:1/0});return J.on("line",N=>{const at=/^port (\d{1,5})$/.exec(N);if(!at||u.port||u.stopped)return;const G=Number(at[1]);if(G<1||G>65535){Y("Build script printed an invalid port.");return}u.port=G,u.group.port=G,A(t),clearTimeout(u.deadline);let Q,St=!1;const Ht=a(async()=>{if(!(St||u.stopped||u.ready||b.exitCode!==null||b.signalCode!==null)){St=!0;try{if(await d(G)){if(u.stopped||P||b.exitCode!==null||b.signalCode!==null||(u.opening=le(u),await u.opening,u.stopped||P||b.exitCode!==null||b.signalCode!==null))return;u.ready=!0,u.visited=o(),w.kind="web",K(t,e,{state:"ready",port:G,https:u.https}),clearInterval(u.poll),e==="main"&&await vt(t)}else o()>=Q&&Y("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{St=!1}}},"check"),Wt=a(()=>{u.stopped||(Q=o()+6e4,u.poll=setInterval(()=>Ht().catch(ct=>Y(ct.message)),250),Ht().catch(ct=>Y(ct.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===G?vt(t).then(Wt,ct=>Y(ct.message)):Wt()}),u.deadline=setTimeout(()=>Y("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{pt(u)}),u.done=new Promise(N=>b.once("close",(at,G)=>{clearTimeout(u.deadline),clearInterval(u.poll),J.close(),j.closeSync(x),(async()=>{if(await pt(u),await de(u),se(u),H||await jt(t,e,u.port),u.finished=!0,u.stopped)return;if(p.get(t.dir)===u){p.delete(t.dir),K(t,e,{serving:void 0});return}const Q=e===L&&!u.port?new Error('Build script exited without printing "port <n>".'):null;P||B||at!==0||u.port&&!u.ready||Q?await Ct(u,at??1,G,P||B||(u.port&&!u.ready?new Error("Build script exited before its port answered."):Q)):u.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):It(w,e),A(t)):(w.kind="app",K(t,e,{state:"ready"}))})().catch(Q=>l(`build /${t.route} ${e}: ${Q.message}`)).finally(()=>{m.get(F)===u&&m.delete(F),N()})})),l(`build /${t.route} ${e}: started; log ${v}`),{status:"started"}}a(Nt,"launch");async function vt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ot(e))}a(vt,"retire");async function Dt(t,e,r){const s=D(t,"main");if(E.has(s))throw new _("A build is already running for main.",409);E.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await tt(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const c=a(T=>O.join(Gt.homedir(),".claude","kickflip","try",`${t.route}-main${T}`),"worktreeOf"),w=c(n?.worktree===c("-a")?"-b":"-a");return j.mkdirSync(O.dirname(w),{recursive:!0}),mt(c(""))&&await I(e,"worktree","remove","--force",c("")),mt(w)?await I(w,"checkout","--detach","--force",r.sha):(await I(e,"worktree","prune"),await I(e,"worktree","add","--detach",w,r.sha)),await Nt(t,"main",w,r)}catch(i){throw i.status!==503&&(C(t).state.main={...r,state:"failed"},xt(t,"main",i),A(t)),i}finally{E.delete(s)}}a(Dt,"launchMain");async function bt(t,{manifest:e=V()}={}){if(await nt(e),$t(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(h=>!["route","key"].includes(h))||typeof t.route!="string"||typeof t.key!="string")throw new _("Send only the board route and card key.");const{route:r,key:s}=t;if(R(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(h=>h==="."||h==="..")))throw new _("Card key must be <file>/<id>.");const i=z(e).find(h=>h.route===r);if(!i)throw new _(`/${r} is not a board.`,404);if(R(s)&&!i.files.some(({slug:h,itemFile:v})=>v.items.some(x=>`${h}/${x.id}`===s)))throw new _("There is no item with this card key.",404);const n=await g();if(n)throw new _(n,409);const c=D(i,s),w=m.get(c);if(E.has(c)||w&&!w.ready&&!w.finished)throw new _(`A build is already running for ${s==="main"?"main":s===L?"the prototype site":"this card"}.`,409);if(s==="main"){const h=C(i).state.main;if(h?.state==="running")throw new _("A build is already running for main.",409);if(h?.state!=="failed"||!h.sha)throw new _("Main has no failed build to retry.",409);const{sha:v,branch:x,slug:b,at:F,script:u}=h;return Dt(i,et(r,e).repo,{sha:v,branch:x,slug:b,at:F,script:u,...i.build_target?{target:i.build_target}:{}})}E.add(c);const T=M.delete(c);try{m.has(c)&&await tt(m.get(c),!0);const h=s===L?Ft(i):await re(i,s,e);return await Nt(i,s,h)}catch(h){throw U.delete(c),T&&h.status!==503&&K(i,s,{state:void 0,sha:void 0}),h}finally{E.delete(c)}}a(bt,"start");const Ft=a(t=>{const e=Ae(t.route);return mt(e)&&j.statSync(e).isDirectory()?j.realpathSync(e):""},"baseWorktree");async function fe(t,e){const{state:r}=C(t),s=D(t,L),i=m.get(s),n=(await _e(t.dir)).length>0&&Ft(t);if(!n){i&&await tt(i),r.prototypes&&(r.prototypes=null,A(t));return}if(i?.ready&&!i.stopped&&!E.has(s)){const c=await I(n,"rev-parse","HEAD");if(c===r.prototypes?.sha)return;l(`try /${t.route} prototypes: the base moved to ${c.slice(0,7)}; restarting its serve`)}else if(i||E.has(s)||r.prototypes?.state==="failed"||await g())return;try{await bt({route:t.route,key:L},{manifest:e})}catch(c){if([409,503].includes(c.status))return;throw r.prototypes={state:"failed"},xt(t,L,c),A(t),c}}a(fe,"serveBase");function Rt(t,e,{manifest:r=V()}={}){const s=z(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Rt,"lookup");async function kt({manifest:t=V(),routes:e}={}){if(await nt(t),$t(t),H)return W;if(W&&e){if(!Z){const r={routes:new Set,manifest:t};r.done=W.catch(()=>{}).then(()=>(Z=null,kt({manifest:r.manifest,routes:[...r.routes]}))),Z=r}Z.manifest=t;for(const r of e)Z.routes.add(r);return Z.done}return W||(W=(async()=>{await ue();const r=z(t),s=lt(be,{}).prs||{};for(const[i,n]of[...p])r.some(c=>c.dir===i)||(p.delete(i),await ot(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||R(i.key)&&i.ready&&o()-i.visited>=We){await tt(i);const{state:n}=C(i.board);It(n,i.key),A(i.board)}for(const i of r){if(H)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),c=et(i.route,t),w=c.branch||"main",T=ke(c);for(const{slug:h,itemFile:v}of i.files)for(const x of v.items){const b=`${h}/${x.id}`,F=Me(x,i.github||v.repo).map(u=>s[Ce(u)]);if(!(x.lane==="done"||F.length&&F.every(u=>["MERGED","CLOSED"].includes(u?.state))))try{const u=D(i,b),B=await re(i,b,t);if(B){const J=await I(B,"rev-parse","HEAD");let N=0;try{N=Number(await I(B,"rev-list","--count",`${T}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:J,ahead:N,script:mt(Vt(B,i.build_target))}),A(i)}else if(!M.has(u))continue;const P=n.cards[b];if((M.has(u)||i.auto_build&&P.script&&P.ahead>0&&(P.sha||gt.get(u))!==P.head&&P.state!=="running")&&!E.has(u))try{await bt({route:i.route,key:b},{manifest:t})}catch(J){if([409,503].includes(J.status))continue;throw K(i,b,{sha:P.head}),await Ct({board:i,key:b,logFile:xt(i,b,J)},1),J}}catch(u){l(`try /${i.route} ${b}: ${u.message}`)}}await fe(i,t).catch(h=>l(`try /${i.route} prototypes: ${h.message}`));try{if(!c.repo||await g())continue;const h=await I(c.repo,"rev-parse",T);let v=!1;try{await I(c.repo,"cat-file","-e",`${h}:${zt(i.build_target)}`),v=!0}catch{}if(!v){const P=m.get(D(i,"main"));P&&await tt(P),await vt(i),n.main=null,A(i);continue}const x=n.main;if(x?.state==="running"||x?.sha===h&&(x.target||null)===(i.build_target||null)&&(x.state==="failed"||x.state==="ready"&&(n.kind!=="web"||Rt(i.route,"main",{manifest:t}))))continue;const b=D(i,"main");if(E.has(b))continue;const F=await I(c.repo,"log","-1","--format=%cI",h);let u=i.github;if(!u)try{u=Jt(await I(c.repo,"remote","get-url","origin"))}catch{}const B={sha:h,branch:w,slug:u||"",at:F,script:v,...i.build_target?{target:i.build_target}:{}};await Dt(i,c.repo,B)}catch(h){l(`try /${i.route} main: ${h.message}`)}}})().finally(()=>{W=null}),W)}a(kt,"tick");function Bt(t,e){if(it.has(t.dir))return it.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},A(t);let c,w;try{c=await(s.local?Qe:Ye)(s.repo,i)}catch(T){w=T.message}if(c===null?delete n.pull:n.pull={at:ft(),...w?{error:w}:{}},A(t),c===null)return null;if(w)return l(`pull /${t.route}: ${w}`),{commits:0,error:w};if(c.commits){l(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${c.commits} commit${c.commits===1?"":"s"} to ${c.to.slice(0,7)}`);try{await f(t,"main-pulled",{branch:i,...c})}catch(T){l(`pull /${t.route}: could not send main-pulled (${T.message})`)}}return{commits:c.commits}})().finally(()=>it.delete(t.dir));return it.set(t.dir,r),r}a(Bt,"pullMain");async function pe({manifest:t=V(),routes:e}={}){await nt(t);const r=[];for(const s of z(t)){if(H)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Bt(s,t))?.commits&&r.push(s.route)}catch(i){l(`pull /${s.route} failed: ${i.message}`)}}return r}a(pe,"pullMains");async function he(t,{manifest:e=V()}={}){if(await nt(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new _("Send only the board route.");const r=z(e).find(n=>n.route===t.route);if(!r)throw new _(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Bt(r,e);if(!i)throw new _("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&kt({manifest:e,routes:[r.route]}).catch(n=>l(`try /${r.route} main: ${n.message}`)),i}a(he,"pull");async function me(){H=!0;const t=Promise.all([...m.values()].map(e=>e.ready?tt(e):ot(e)).concat([...p.values()].map(e=>ot(e))));await Promise.all([t,W,...it.values()])}return a(me,"stop"),{start:bt,tick:kt,stop:me,lookup:Rt,initialize:nt,pull:he,pullMains:pe,endUnshared:ce}}a(ze,"createTryManager");const wt=1e4,yt=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:wt,env:Oe()})).stdout,"originGit");async function Ye(o,d,{run:y=yt}={}){const l=a(async(...p)=>(await y(...p)).trim(),"git"),g=a(p=>l(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),f=`refs/remotes/origin/${d}`,k=await g(`refs/heads/${d}`)||await g(f);try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${f}`),await l(o,"checkout","--quiet","--force","-B",d,f),await l(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${wt/1e3} s`:Et(p.stderr,{repo:o}).error}`)}const $=await g(f),m=k&&k!==$?Number(await l(o,"rev-list","--count",`${k}..${$}`)):0;return{from:k||$,to:$,commits:m}}a(Ye,"resetBranch");async function Qe(o,d,{run:y=yt}={}){const l=a(async(...f)=>(await y(...f)).trim(),"git"),g=a(f=>{throw new Error(`can\u2019t pull: ${f}`)},"refuse");try{await l(o,"config","--get","remote.origin.url")}catch{return null}try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(f){g(f.killed?`git fetch timed out after ${wt/1e3} s`:Et(f.stderr,{repo:o}).error)}try{const f=await l(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),k=await l(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),$=a((S,E)=>l(o,"merge-base","--is-ancestor",S,E).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(f===k||await $(k,f))return{from:f,to:f,commits:0};await $(f,k)||g(`${d} has split from origin`);const m=Number(await l(o,"rev-list","--count",`${f}..${k}`)),p=(await l(o,"worktree","list","--porcelain")).split(`
|
|
3
5
|
|
|
4
|
-
`).map(
|
|
5
|
-
`)).find(
|
|
6
|
-
`).at(-1))}}a(
|
|
7
|
-
`).map(
|
|
6
|
+
`).map(S=>S.split(`
|
|
7
|
+
`)).find(S=>S.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await l(p,"status","--porcelain","--untracked-files=no")&&g("local changes"),await l(p,"merge","--ff-only","--quiet",k)):await l(o,"update-ref",`refs/heads/${d}`,k,f),{from:f,to:k,commits:m}}catch(f){if(f.message.startsWith("can\u2019t pull: "))throw f;g(String(f.stderr||f.message).trim().split(`
|
|
8
|
+
`).at(-1))}}a(Qe,"pullBranch");async function yr({manifest:o=V(),log:d=console.log,routes:y}={}){const l=y?lt(Kt,{}):{};for(const g of z(o))if(!(y&&!y.includes(g.route)))try{const f=et(g.route,o);if(f.local||!f.repo)continue;const k=f.branch||"main",$=new Date().toISOString(),m=Date.now();try{await yt(f.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),l[g.route]={at:$,ok:!0}}catch(p){d(`fetch /${g.route} after ${Date.now()-m} ms: ${p.message}`);let S=g.github;if(!S&&!g.remote)try{S=Jt(await I(f.repo,"remote","get-url","origin"))}catch{}l[g.route]={at:$,ok:!1,...Et(p.killed?`git fetch timed out after ${wt/1e3} s`:p.stderr,{github:S||"",remote:g.remote,repo:f.repo})};continue}if(g.remote)try{await Ve(g,f.repo,k,d)}catch(p){d(`pushed branches /${g.route}: ${p.message}`)}}catch(f){d(`fetch /${g.route} failed: ${f.message}`)}Ut(Kt,l)}a(yr,"fetchTryBranches");async function Ve(o,d,y,l){const g=o.files.flatMap(({slug:$,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!Pe(p)&&p.lane!=="done").map(p=>({key:`${$}/${p.id}`,branch:p.pushed.branch})));if(!g.length)return;const f=new Map((await yt(d,"ls-remote","--heads","origin")).split(`
|
|
9
|
+
`).map($=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec($)).filter(Boolean).map($=>[$[2],$[1]])),k=Se(o.remote)?.host||"the remote";for(const $ of g){let m=f.get($.branch);if(!m)try{m=await I(d,"rev-parse","--verify","--quiet",`refs/heads/${$.branch}^{commit}`)}catch{}let p=!1;if(m)try{await I(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${y}`),p=!0}catch{}if(!(!p&&f.has($.branch)))try{await Ie(o.dir,$.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:$.branch,into:y,host:k}]:["branch-gone",{branch:$.branch,host:k}]})}catch(S){l(`pushed ${$.key}: ${S.message}`)}}}a(Ve,"settlePushed");const X=ze({waiting:a(()=>Ee.waiting("tailscale"),"waiting")}),gr=a((...o)=>X.start(...o),"startBuild"),$r=a((...o)=>X.tick(...o),"tryTick"),vr=a((...o)=>X.pull(...o),"pullNow"),br=a((...o)=>X.pullMains(...o),"pullMains"),kr=a(()=>X.stop(),"stopTries"),Sr=a(()=>X.endUnshared(),"endUnsharedTries"),Or=a((...o)=>X.initialize(...o),"initializeTries");export{_ as BuildError,q as TRY_PORTS,ze as createTryManager,Sr as endUnsharedTries,yr as fetchTryBranches,Or as initializeTries,re as installWorktree,Tt as isTryPort,Qe as pullBranch,br as pullMains,vr as pullNow,Ye as resetBranch,gr as startBuild,kr as stopTries,$r as tryTick};
|
package/package.json
CHANGED
package/shared/fake-gh.mjs
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
var p=Object.defineProperty;var t=(
|
|
2
|
-
`).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(
|
|
1
|
+
var p=Object.defineProperty;var t=(r,e)=>p(r,"name",{value:e,configurable:!0});import n from"node:fs";import l from"node:path";import{PR_VIEW_FIELDS as g}from"../hub/pr-report.mjs";const a=t((r,e,s,i)=>{const o=l.join(r,e,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(i))},"write"),y=t((r,e,s)=>a(r,"pr",e,s),"writePR"),x=t((r,e,s)=>a(r,"issue-comments",e,s),"writeIssueComments"),b=t((r,e,s)=>a(r,"review-comments",e,s),"writeReviewComments"),v=t((r,e,s)=>a(r,"reviews",e,s),"writeReviews"),_=t((r,e,s)=>a(r,"merge",e,s),"writeMerge"),S=t((r,e,s)=>a(r,"ready",e,s),"writeReady"),j=t((r,e,s)=>a(r,"open",e,s),"writeOpen"),R=t((r,e)=>a(r,"","user",e),"writeUser"),O=t((r,e,s={login:e})=>a(r,"github-users",e,s),"writeGithubUser"),q=t((r,e,s)=>a(r,"collaborators",e.replace("/","__"),s),"writeCollaborators"),N=t((r,e)=>a(r,"","accounts",e),"writeAccounts"),c=t((r,e)=>{try{return n.readFileSync(l.join(r,e),"utf8").trim().split(`
|
|
2
|
+
`).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(r=>c(r,"calls.jsonl"),"calls"),k=t((r,e)=>a(r,"","rate",e),"writeRate"),A=t((r,e)=>a(r,"","limit",e),"writeLimit"),E=t(r=>f(r).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,o])=>`${s}/${i}#${o}`)),"queried"),J=t(r=>c(r,"tokens.jsonl"),"tokens");function P({head:r="abc1234567",failing:e=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:r,statusCheckRollup:[...e.map(o=>({__typename:"CheckRun",name:o,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(o,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(P,"prView");function I(r){const e=l.join(r,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const dir = ${JSON.stringify(
|
|
5
|
+
const dir = ${JSON.stringify(r)};
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
fs.appendFileSync(path.join(dir, 'calls.jsonl'), JSON.stringify(args) + '\\n');
|
|
8
8
|
fs.appendFileSync(path.join(dir, 'tokens.jsonl'), JSON.stringify({ args, token: process.env.GH_TOKEN || null }) + '\\n');
|
|
9
9
|
const fail = () => { console.error('no fixture for: ' + args.join(' ')); process.exit(1); };
|
|
10
|
+
let limit = null;
|
|
11
|
+
try { limit = JSON.parse(fs.readFileSync(path.join(dir, 'limit.json'), 'utf8')); } catch {}
|
|
12
|
+
if (args[0] === 'api' && args[1] === 'rate_limit') {
|
|
13
|
+
const budget = (b) => (limit?.[b] ? { limit: 5000, remaining: 0, reset: Math.floor(Date.parse(limit[b]) / 1000) } : { limit: 5000, remaining: 4990, reset: Math.floor(Date.now() / 1000) + 3600 });
|
|
14
|
+
console.log(JSON.stringify({ resources: { core: budget('core'), graphql: budget('graphql') } }));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const spends = args[0] === 'api' ? (args.slice(1).find((a) => !a.startsWith('-')) === 'graphql' ? 'graphql' : 'core')
|
|
18
|
+
: ['pr', 'issue', 'repo'].includes(args[0]) ? 'graphql' : args[0] === 'run' || (args[0] === 'auth' && args[1] !== 'token') ? 'core' : null;
|
|
19
|
+
if (spends && limit?.secondary) { console.error('gh: You have exceeded a secondary rate limit. Please wait a few minutes before you try again. (HTTP 403)'); process.exit(1); }
|
|
20
|
+
if (spends && limit?.[spends]) {
|
|
21
|
+
if (args[1] === 'graphql') console.log(JSON.stringify({ errors: [{ type: 'RATE_LIMITED', message: 'API rate limit already exceeded for user ID 1.' }] }));
|
|
22
|
+
console.error(spends === 'graphql' ? 'GraphQL: API rate limit already exceeded for user ID 1.' : 'gh: API rate limit exceeded for user ID 1. (HTTP 403)');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
10
25
|
// The repo's own fixture for PR n, else the one for n in any repo.
|
|
11
26
|
const fixture = (folder, repo, n) => {
|
|
12
27
|
const own = path.join(dir, folder, repo.replace('/', '__') + '__' + n + '.json');
|
|
@@ -115,4 +130,4 @@ if (list) {
|
|
|
115
130
|
try { if (JSON.parse(value).fail) fail(); } catch {}
|
|
116
131
|
console.log(value);
|
|
117
132
|
}
|
|
118
|
-
`,{mode:493}),e}t(
|
|
133
|
+
`,{mode:493}),e}t(I,"fakeGh");export{f as calls,I as fakeGh,P as prView,E as queried,J as tokens,N as writeAccounts,q as writeCollaborators,O as writeGithubUser,x as writeIssueComments,A as writeLimit,_ as writeMerge,j as writeOpen,y as writePR,k as writeRate,S as writeReady,b as writeReviewComments,v as writeReviews,R as writeUser};
|
package/shared/gh.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var q=Object.defineProperty;var u=(t,e)=>q(t,"name",{value:e,configurable:!0});import f from"node:fs";import F from"node:os";import h from"node:path";import{execFile as N,spawnSync as v}from"node:child_process";import{clock as I}from"../hub/schedule.mjs";const p="primary",g=u((t=F.homedir())=>h.join(t,".claude","kickflip","gh-limit.json"),"gateFile");function y(t){const[e,n]=t;if(e==="auth")return n==="token"||n==="git-credential"?null:"core";if(e==="api"){const r=t.slice(1).find(s=>!s.startsWith("-"));return r==="graphql"?"graphql":r==="rate_limit"?null:"core"}return e==="run"?"core":["pr","issue","repo"].includes(e)?"graphql":null}u(y,"budgetOf");const S=u(t=>{try{return JSON.parse(f.readFileSync(t,"utf8"))}catch{return{}}},"readGate");function w({account:t=p,budget:e,now:n=Date.now(),file:r=g()}={}){return Object.entries(S(r)[t]||{}).filter(([o,c])=>(!e||o===e)&&Date.parse(c)>n).map(([,o])=>o).sort().at(-1)||null}u(w,"limitedUntil");const b=u((t,e=p,n=new Date)=>`GitHub rate limited${e===p?"":` for ${e}`}, resets at ${I(new Date(t),n)}`,"limitWords"),O=u(t=>/API rate limit (already )?exceeded/i.test(t)&&!/secondary rate limit/i.test(t),"hitLimit");function x(t,e,n){let r;try{r=JSON.parse(n).resources}catch{return!1}const s=["core","graphql"].filter(a=>r?.[a]?.remaining===0&&Number(r[a].reset)*1e3>Date.now());if(!s.length)return!1;const o=S(t),c=Date.now();for(const[a,l]of Object.entries(o)){for(const[d,m]of Object.entries(l))Date.parse(m)>c||delete l[d];Object.keys(l).length||delete o[a]}o[e]={...o[e],...Object.fromEntries(s.map(a=>[a,new Date(r[a].reset*1e3).toISOString()]))},f.mkdirSync(h.dirname(t),{recursive:!0});const i=`${t}.${process.pid}.tmp`;return f.writeFileSync(i,JSON.stringify(o,null,2)+`
|
|
2
|
+
`),f.renameSync(i,t),!0}u(x,"record");const $=u((t,e,n)=>{const r=y(t),s=r&&w({account:e,budget:r,file:n});return s?{status:1,stdout:"",stderr:b(s,e),limited:s}:null},"refusal"),D=u((t,e,n,r)=>{const s=w({account:n,budget:y(e),file:r});return s?{...t,stderr:b(s,n),limited:s}:t},"gated");async function W(t,{env:e=process.env,account:n=p,file:r=g(),...s}={}){const o=$(t,n,r);if(o)return o;const c=u(l=>new Promise(d=>N("gh",l,{...s,env:e},(m,j,k)=>d({status:m?typeof m.code=="number"?m.code:1:0,stdout:String(j??""),stderr:String(k||(m?m.message:""))}))),"run"),i=await c(t);if(i.status===0||!O(`${i.stderr}
|
|
3
|
+
${i.stdout}`))return i;const a=await c(["api","rate_limit"]);return a.status===0&&x(r,n,a.stdout)?D(i,t,n,r):i}u(W,"gh");function E(t,{env:e=process.env,account:n=p,file:r=g(),...s}={}){const o=$(t,n,r);if(o)return o;const c=u(l=>{const d=v("gh",l,{encoding:"utf8",...s,env:e});return{status:d.status??1,stdout:d.stdout||"",stderr:d.stderr||d.error?.message||""}},"run"),i=c(t);if(i.status===0||!O(`${i.stderr}
|
|
4
|
+
${i.stdout}`))return i;const a=c(["api","rate_limit"]);return a.status===0&&x(r,n,a.stdout)?D(i,t,n,r):i}u(E,"ghSync");export{p as PRIMARY,y as budgetOf,g as gateFile,W as gh,E as ghSync,b as limitWords,w as limitedUntil};
|
package/shared/owner-check.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
var h=Object.defineProperty;var r=(t,
|
|
2
|
-
`).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(
|
|
3
|
-
`).find(Boolean)
|
|
1
|
+
var h=Object.defineProperty;var r=(t,s)=>h(t,"name",{value:s,configurable:!0});import{execFile as m}from"node:child_process";import{gh as k}from"./gh.mjs";const b=/^[\w.-]+\/[\w.-]+$/,A=/^(@[\w.-]+\/)?[\w.][\w.-]*$/,S=/^[\w.-]+(\/[\w.-]+)*$/,j=/^[\w-]+(\.[\w-]+)*$/,u=r(t=>{throw new Error(t)},"bad"),d=r(t=>b.test(t||"")?t:u(`"${t??""}" isn't a GitHub repo such as owner/name`),"repoOf"),f=r((t,s)=>t&&!t.startsWith("-")?t:u(`${s} is missing`),"plain"),l={tag:{usage:"tag <owner/repo> <pattern>",args:r(([t,s,...e])=>e.length?u("tag takes a repo and one pattern"):[d(t),f(s,"the tag pattern")],"args"),words:r(([t,s])=>`a ${s} tag on ${t}`,"words"),async run([t,s],{exec:e}){const n=await e("git",["ls-remote","--tags",`https://github.com/${t}.git`]),a=O(s),i=[...new Set(n.split(`
|
|
2
|
+
`).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(p).at(-1);return i?{passed:!0,found:`tag ${i} is on ${t}`}:{passed:!1}}},npm:{usage:"npm <package> <version-or-range>",args:r(([t,s,...e])=>e.length?u("npm takes a package and one version or range"):[A.test(t||"")?t:u(`"${t??""}" isn't an npm package name`),f(s,"the version or range")],"args"),words:r(([t,s])=>`${t}@${s} on npm`,"words"),async run([t,s],{exec:e}){const n=(await e("npm",["view",`${t}@${s}`,"version","--json"]).catch(i=>{if(/No match found for version/.test(`${i.stdout??""}${i.stderr??""}`))return"";throw i})).trim(),o=(n?[JSON.parse(n)].flat():[]).sort(p).at(-1);return o?{passed:!0,found:`${t}@${o} is on npm`}:{passed:!1}}},run:{usage:"run <owner/repo> <workflow>",args:r(([t,...s])=>[d(t),f(s.join(" "),"the workflow")],"args"),words:r(([t,s])=>`a passing ${s} run on ${t}`,"words"),async run([t,s],{exec:e,since:n}){const a=await e("gh",["run","list","--repo",t,"--workflow",s,"--status","success","--limit","20","--json","createdAt,url"]),o=JSON.parse(a||"[]").filter(i=>String(i.createdAt)>=n).sort((i,c)=>String(i.createdAt).localeCompare(String(c.createdAt))).at(-1);return o?{passed:!0,found:`${s} passed on ${t} (${o.url})`}:{passed:!1}}},gh:{usage:"gh <owner/repo> <api-path> <field>=<value>",args([t,s,e,...n]){n.length&&u("gh takes a repo, an API path and one field=value"),(!S.test(s||"")||s.split("/").includes(".."))&&u(`"${s??""}" isn't an API path under the repo, such as actions/permissions`);const[a,...o]=String(e??"").split("=");return(!j.test(a)||!o.length)&&u(`"${e??""}" isn't a field=value, such as enabled=true`),[d(t),s,e]},words:r(([t,s,e])=>`${s} reads ${e} on ${t}`,"words"),async run([t,s,e],{exec:n}){const[a,...o]=e.split("="),i=o.join("="),c=a.split(".").reduce((g,w)=>g?.[w],JSON.parse(await n("gh",["api",`repos/${t}/${s}`])));return c!==void 0&&String(c)===i?{passed:!0,found:`${s} reads ${e} on ${t}`}:{passed:!1}}},url:{usage:"url <http(s) address> [<status> | <text>]",args([t,...s]){let e;try{e=new URL(t)}catch{u(`"${t??""}" isn't a web address`)}return["http:","https:"].includes(e.protocol)||u("url takes an http or https address"),s.length?[t,s.join(" ")]:[t]},words:r(([t,s])=>s===void 0||/^\d{3}$/.test(s)?`${t} answers ${s??200}`:`${t} shows "${s}"`,"words"),async run([t,s],{fetchImpl:e}){const n=s!==void 0&&/^\d{3}$/.test(s)?Number(s):200,a=await e(t,{redirect:"follow",signal:AbortSignal.timeout(2e4)}),o=await a.text();return a.status!==n?{passed:!1}:n===200&&s!==void 0&&!/^\d{3}$/.test(s)?o.includes(s)?{passed:!0,found:`${t} shows "${s}"`}:{passed:!1}:{passed:!0,found:`${t} answered ${n}`}}}},$=Object.values(l).map(t=>t.usage);function I(t){const[s,...e]=String(t??"").trim().split(/\s+/).filter(Boolean);s||u(`--check takes "<name> <args>": ${$.join(", ")}`);const n=Object.hasOwn(l,s)?l[s]:u(`--check has no check "${s}"; it takes ${$.join(", ")}`);try{return{name:s,args:n.args(e)}}catch(a){return u(`--check ${n.usage}: ${a.message}`)}}r(I,"parseCheck");const N=r(({name:t,args:s})=>[t,...s].join(" "),"checkText"),_=r(({name:t,args:s})=>l[t].words(s),"checkWords");function B({name:t,args:s},{since:e,exec:n=v,fetchImpl:a=fetch}={}){return l[t].run(s,{since:e,exec:n,fetchImpl:a})}r(B,"runCheck");async function v(t,s){if(t==="gh"){const e=await k(s,{timeout:6e4,maxBuffer:16777216});if(e.status===0)return e.stdout;throw Object.assign(new Error(`gh ${s[0]} failed: ${e.stderr.trim().split(`
|
|
3
|
+
`).find(Boolean)||`exit ${e.status}`}`),{stdout:e.stdout,stderr:e.stderr})}return new Promise((e,n)=>{m(t,s,{timeout:6e4,maxBuffer:16*1024*1024,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(a,o,i)=>{if(!a)return e(o);const c=String(i||"").trim().split(`
|
|
4
|
+
`).find(Boolean)?.replace(/^npm error /,"");n(Object.assign(new Error(`${t} ${s[0]} failed: ${c||a.message}`),{stdout:o,stderr:i}))})})}r(v,"execArgv");function O(t){return new RegExp(`^${t.split("").map(s=>s==="*"?".*":s==="?"?".":s.replace(/[.+^${}()|[\]\\]/g,"\\$&")).join("")}$`)}r(O,"globOf");function p(t,s){return t.localeCompare(s,"en",{numeric:!0})}r(p,"byVersion");export{$ as CHECK_USAGE,N as checkText,_ as checkWords,I as parseCheck,B as runCheck};
|