@lovek4d/kickflip 0.7.2 → 0.7.7
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-needs.js +2 -2
- package/board/public/app-snapshot.js +1 -1
- package/board/public/style.css +17 -17
- package/board/public/ui-cards.js +30 -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 +6 -7
- 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 +11 -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 A,tailscaleInstallCommand as D}from"../shared/tailscale.mjs";const H={slack:"Reviewer DMs and digests wait on this."},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",null],["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 d=s((r,e)=>{switch(r){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,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=D(a)),n?{ready:!1,missing:n,fix:{command:c}}:{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:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{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(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const E=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=y({home:t,env:i,now:a,cacheMs:d}),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:E(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:A({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&P({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??H[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(F,"readinessReader");const G=y(),Y=F({probes:{codex:G}});export{G as readCodex,Y 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,12 @@
|
|
|
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 Se=Object.defineProperty;var a=(o,d)=>Se(o,"name",{value:d,configurable:!0});import S from"node:fs";import T from"node:path";import Jt from"node:os";import Tt from"node:http";import{execFile as Oe,spawn as xe}from"node:child_process";import{createInterface as zt}from"node:readline";import{promisify as Ee}from"node:util";import{listBoards as J,readManifest as Q,readJSON as ft,saveJSON as Yt,appendEvent as Te,GITHUB_STATE as je,REPO_FETCH as Qt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Vt,mainRefOf as _e}from"./board-repo.mjs";import{gitReachFault as jt,remoteFromInput as Ie,remoteGitEnv as Ae}from"./repos.mjs";import{readiness as Me}from"./readiness.mjs";import{tailscaleBin as Pe}from"../shared/tailscale.mjs";import{buildScriptError as Ce,buildScriptOf as qt}from"../board/build-script.mjs";import{appendStatus as Xt,recordedWorktree as Ne,servedRoundsOpen as Fe,setItem as De}from"../board/store.mjs";import{BASE_KEY as U,baseWorktreeOf as Be}from"../board/prototype-base.mjs";import{cardPRs as Re,hasPR as He,prKeyOf as We}from"../shared/card-prs.mjs";import{hubGate as Ge}from"../board/on-host.mjs";import{pipeUpgrade as Le,refuse as Ue,refuseUpgrade as Ke}from"./front-door.mjs";import{shareOf as Je}from"./share.mjs";class A extends Error{static{a(this,"BuildError")}constructor(d,h=400){super(d),this.status=h}}const rt=Ee(Oe),M=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),ze=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,V={first:8500,last:8599},_t=a(o=>/^\d+$/.test(String(o))&&Number(o)>=V.first&&Number(o)<=V.last,"isTryPort"),Ye=30*6e4,Zt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Qe=["x-frame-options"],Ve=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([h])=>!Qe.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),yt={main:"main",[U]:"prototypes"},H=a(o=>!Object.hasOwn(yt,o),"isCard"),te=a((o,d)=>T.join(o,qt(d)),"scriptAt"),gt=a(o=>S.existsSync(o),"exists"),ee=a((o,d)=>T.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function It(o,d,h){const c=ee(o,d);return S.mkdirSync(T.dirname(c),{recursive:!0}),S.writeFileSync(c,h.message+`
|
|
2
|
+
`,{mode:384}),S.chmodSync(c,384),c}a(It,"failureLog");const re=a(o=>new Promise(d=>{const h=Tt.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ie=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),se=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),At=a(o=>new Promise(d=>{const h=a(()=>{clearTimeout(c),clearInterval(y),d()},"finish");ie(o,"SIGTERM");let c=setTimeout(()=>{ie(o,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const y=setInterval(()=>{se(o)||h()},20)}),"endGroup");async function qe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
|
|
3
|
+
`).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===o).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await ne(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}a(qe,"groupMembers");async function ne(o,d){let h="";try{h=(await rt("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(u){h=u.stdout||""}const c=new Map(o.map(u=>[u.pid,u]));let y;for(const u of h.split(`
|
|
4
|
+
`))u[0]==="p"?y=c.get(u.slice(1)):u[0]==="n"&&y&&(y.cwd=u.slice(1));for(const u of o){if(!u.cwd&&S.existsSync("/proc"))try{u.cwd=S.readlinkSync(`/proc/${u.pid}/cwd`)}catch{}u.cwd=u.cwd.replace(/ \(deleted\)$/,"")}}a(ne,"readCwds");async function Xe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
|
|
5
|
+
`).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await ne(h,["-d","cwd","-Fpn"]),h.filter(c=>oe(c,o))}a(Xe,"processesIn");const Ze=a((o,d)=>o===d||o.startsWith(d+T.sep),"insideDir"),oe=a((o,d)=>Ze(o.cwd,d)||o.command.includes(d+T.sep)||o.command.endsWith(d),"inWorktree");async function ae(o,d,h){const c=await Ne(o.dir,d),[y,u]=d.split("/"),k=o.files.find(O=>O.slug===y)?.itemFile,g=c?.path||k?.items.find(O=>O.id===u)?.worktree;if(typeof g!="string"||!g||g.split(T.sep).includes(".."))return"";let m=c?.repo;const p=(o.github||k?.repo||"").toLowerCase();if(!m&&p){const O=[o.owner?.repo,o.dir,...Object.values(h?.sites||{}).map(F=>F.owner?.repo)];for(const F of new Set(O.filter(x=>typeof x=="string"&&T.isAbsolute(x))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(F,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await M(F,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!m||c?.path&&!T.isAbsolute(g))return"";try{const O=[T.join(S.realpathSync(Jt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&T.isAbsolute(m))try{O.push(T.join(S.realpathSync(m),".claude","worktrees"))}catch{}const F=T.resolve(m||"/",g),x=S.realpathSync(F);return O.some(z=>F.startsWith(z+T.sep)&&x.startsWith(z+T.sep))&&S.statSync(x).isDirectory()?x:""}catch{return""}}a(ae,"installWorktree");function tr({now:o=Date.now,answers:d=re,lingers:h=re,log:c=console.log,waiting:y=a(async()=>null,"waiting"),event:u=Te,gate:k=Ge}={}){const g=new Map,m=new Map,p=new Map,O=new Set,F=a(()=>[...m.values(),...p.values()],"live"),x=new Set,z=new Set,it=new Map,st=new Map,bt=new Map,nt=new Map;let W=!1,G=null,X=null,pt=null;const ht=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if(g.has(t.dir))return g.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=ft(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},..._t(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)};g.set(t.dir,i),s.main_port&&st.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,l])=>l.state==="running");for(const[l]of n)z.add(B(t,l));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),P(t),i}a(C,"stateOf");function P(t){const e=g.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Yt(e.file,e.state),e.written=r)}a(P,"persist");function ce(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),P(t.board)}a(ce,"recordGroup");function le(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,P(t.board))}a(le,"forgetGroup");async function de(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,P(t);for(const s of r){if(!se(s.pgid))continue;let i=[];try{i=await qe(s.pgid)}catch(n){c(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>oe(n,s.worktree))&&(await At(s.pgid),c(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await Mt(t,s.key,s.port))}}a(de,"sweepGroups");async function Mt(t,e,r){if(!r||!await h(r))return;const s=`Port ${r} still answers after the run was stopped.`;H(e)&&await Xt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>c(`build /${t.route} ${e}: ${i.message}`)),c(`build /${t.route} ${e}: ${s}`)}a(Mt,"checkQuiet");const B=a((t,e)=>`${t.dir}\0${e}`,"runKey"),Pt=a((t,e)=>H(e)?t.cards[e]:t[yt[e]],"entryOf"),ue=a((t,e,r)=>{H(e)?t.cards[e]=r:t[yt[e]]=r},"setEntry"),Ct=a((t,e)=>{H(e)?delete t.cards[e]:t[yt[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=Pt(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,H(e)&&n?{at:ht()}:{}),P(t)}a(K,"update");function kt(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=ft(T.join(r.dir,"data","try.json"),{}).main_port;_t(s)&&st.set(r.dir,Number(s))}}a(kt,"reserveMains");function fe(t){const{board:e,key:r}=t,s=B(e,r),i=r==="main"?st.get(e.dir):it.get(s);if(i)return i;const n=new Set([...st.values(),...it.values()]);let l=V.first;for(;n.has(l)&&l<=V.last;)l++;if(l>V.last)throw new Error(`No try ports available (${V.first}\u2013${V.last}). Stop a served card and retry.`);return r==="main"?(st.set(e.dir,l),C(e).state.main_port=l,P(e)):it.set(s,l),l}a(fe,"publicPort");const ot=a(async t=>{const e=Pe();if(!e)throw new Error("Tailscale isn't installed on this host");return rt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Nt(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Nt,"removePort");const Ft=a(t=>Je(ft(T.join(t.dir,"board.json"),{})),"sharedWith");async function Dt(t,e){const r=await k(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Ft(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Dt,"relayVerdict");function pe(){for(const t of F()){if(!t.shared?.size)continue;const e=Ft(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(pe,"endUnshared");async function he(t){t.https=fe(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=Tt.createServer(async(n,l)=>{const $=await Dt(n,t.board);if($!=="owner"&&$!=="shared"){Ue(n,l,$);return}if($==="shared"){const v={login:n.headers["tailscale-user-login"],res:l};i.add(v),l.on("close",()=>i.delete(v))}const b=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Zt.includes(v))),w=Tt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:b},v=>{l.writeHead(v.statusCode,t.key===U?Ve(v.headers):v.headers),v.on("data",r),v.pipe(l),v.on("close",()=>{v.complete||l.destroy()})});l.on("close",()=>{l.writableFinished||w.destroy()}),w.on("error",()=>{l.headersSent?l.destroy():l.writeHead(502).end()}),n.pipe(w)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,l,$)=>{const b=await Dt(n,t.board);if(b!=="owner"){Ke(l,b==="shared"?"view-only":b);return}s(Le({req:n,socket:l,head:$,port:t.port,path:n.url,replace:Zt}))}),await new Promise((n,l)=>{t.relay.once("error",l),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await ot(["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(he,"openPort");async function me(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&&!W&&!e&&await Nt(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||it.delete(B(t.board,t.key))}a(me,"closePort");async function we(){const t=F().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await ot(["serve","status","--json"])).stdout)}catch(r){c(`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){c(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await ot(["serve","--bg",`--https=${r.https}`,s])}catch(n){c(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(we,"reserve");function at(t=Q()){if(pt)return pt;kt(t);for(const e of J(t))C(e);return pt=(async()=>{for(const e of J(t))await de(e).catch(r=>c(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await ot(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))_t(s)&&![...m.values()].some(i=>i.https===Number(s))&&await Nt(s)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),pt}a(at,"initialize");async function Bt(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!H(n))return;let l="",$=!1;try{for await(const v of zt({input:S.createReadStream(t.logFile),crlfDelay:1/0}))$||=ze.test(v),v.trim()&&(l=v)}catch(v){l=`could not read the build log: ${v.message}`}const b=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(j=>j.install?.url))?"app":"web"))==="web"?"Serve":"Build",w=$?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||l;await Xt(i.dir,n,"run-failed",{kind:b,code:e??1,why:w})}a(Bt,"failure");function mt(t){return t.ending||(t.ending=At(t.child.pid)),t.ending}a(mt,"terminate");async function ct(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await mt(t),await t.done}a(ct,"halt");async function Z(t,e=!1){await ct(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||bt.delete(B(t.board,t.key))}a(Z,"stopRun");async function St(t,e,r,s={},i=""){if(!r)throw new A("This card has no valid worktree.");const n=te(r,t.build_target),l=Ce(n);if(l)throw new A(l);if(!S.realpathSync(n).startsWith(S.realpathSync(r)+T.sep))throw new A("Build script must be inside the worktree.");const $=await M(r,"rev-parse","HEAD");if(W)throw new A("The hub is stopping. Try again after it restarts.",503);const{state:b}=C(t),w=Pt(b,e),v={...H(e)?w:{},...s,sha:$,script:!0,state:"running",...H(e)?{at:ht(),head:$}:{},...e===U?{since:ht()}:{}};delete v.port,delete v.https;const j=ee(t,e);S.mkdirSync(T.dirname(j),{recursive:!0});const I=S.openSync(j,"w",384);S.fchmodSync(I,384),i&&S.writeSync(I,i);const _=xe(n,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});ue(b,e,v);const E=B(t,e),f={child:_,board:t,key:e,worktree:r,sha:$,at:s.at,metadata:s,logFile:j,visited:o(),stopped:!1,port:null,ready:!1};ce(f),m.set(E,f),bt.set(E,$);let N,tt;const D=a(R=>{tt||=new Error(R),mt(f)},"fail");_.on("error",R=>{N=R}),_.stdout.on("data",R=>S.writeSync(I,R)),_.stderr.on("data",R=>S.writeSync(I,R));const lt=zt({input:_.stdout,crlfDelay:1/0});return lt.on("line",R=>{const dt=/^port (\d{1,5})$/.exec(R);if(!dt||f.port||f.stopped)return;const L=Number(dt[1]);if(L<1||L>65535){D("Build script printed an invalid port.");return}f.port=L,f.group.port=L,P(t),clearTimeout(f.deadline);let Y,Et=!1;const Ut=a(async()=>{if(!(Et||f.stopped||f.ready||_.exitCode!==null||_.signalCode!==null)){Et=!0;try{if(await d(L)){if(f.stopped||tt||_.exitCode!==null||_.signalCode!==null||(f.opening=he(f),await f.opening,f.stopped||tt||_.exitCode!==null||_.signalCode!==null))return;f.ready=!0,f.visited=o(),b.kind="web",K(t,e,{state:"ready",port:L,https:f.https}),clearInterval(f.poll),e==="main"&&await wt(t)}else o()>=Y&&D("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{Et=!1}}},"check"),Kt=a(()=>{f.stopped||(Y=o()+6e4,f.poll=setInterval(()=>Ut().catch(ut=>D(ut.message)),250),Ut().catch(ut=>D(ut.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===L?wt(t).then(Kt,ut=>D(ut.message)):Kt()}),f.deadline=setTimeout(()=>D("No port line within 60 minutes."),60*6e4),_.once("exit",()=>{mt(f)}),f.done=new Promise(R=>_.once("close",(dt,L)=>{clearTimeout(f.deadline),clearInterval(f.poll),lt.close(),S.closeSync(I),(async()=>{if(await mt(f),await me(f),le(f),W||await Mt(t,e,f.port),f.finished=!0,f.stopped)return;if(p.get(t.dir)===f){p.delete(t.dir),K(t,e,{serving:void 0});return}if(e==="main"&&!f.ready&&p.has(t.dir)&&!W){await ye(f);return}const Y=e===U&&!f.port?new Error('Build script exited without printing "port <n>".'):null;tt||N||dt!==0||f.port&&!f.ready||Y?await Bt(f,dt??1,L,tt||N||(f.port&&!f.ready?new Error("Build script exited before its port answered."):Y)):f.ready?(e==="main"?(delete b.main.state,delete b.main.port,delete b.main.https):Ct(b,e),P(t)):(b.kind="app",K(t,e,{state:"ready"}))})().catch(Y=>c(`build /${t.route} ${e}: ${Y.message}`)).finally(()=>{m.get(E)===f&&m.delete(E),R()})})),c(`build /${t.route} ${e}: started; log ${j}`),{status:"started"}}a(St,"launch");async function wt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ct(e))}a(wt,"retire");async function ye(t){const{board:e,worktree:r}=t;await wt(e),c(`try /${e.route} main: the new serve exited before answering while the old one served; stopped the old serve and serving again in ${r}`);const{serving:s,...i}=t.metadata;try{await St(e,"main",r,i,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
|
|
6
|
+
`)}catch(n){Rt(e,i,n)}}a(ye,"handOverFailed");function Rt(t,e,r){r.status!==503&&(C(t).state.main={...e,state:"failed"},It(t,"main",r),P(t))}a(Rt,"mainFailed");async function ge(t,e,r){const s=`try /${t.route} main:`;if(!O.has(t.dir)){O.add(t.dir);try{const i=await Xe(r);for(const n of new Set(i.map(l=>l.pgid))){await At(n);for(const l of i.filter($=>$.pgid===n))c(`${s} stopped pid ${l.pid} (pgid ${n}) in the old main worktree ${r}`)}}catch(i){c(`${s} could not stop the processes in the old main worktree ${r}: ${i.message}`)}}if(gt(r))try{await M(e,"worktree","remove","--force",r);return}catch(i){try{S.rmSync(r,{recursive:!0,force:!0}),c(`${s} git could not remove the old main worktree ${r}; removed the folder: ${(i.stderr||i.message).trim().split(`
|
|
7
|
+
`).at(-1)}`),await M(e,"worktree","prune")}catch(n){c(`${s} could not remove the old main worktree ${r}: ${n.message}`)}}}a(ge,"removeLegacyMain");async function Ht(t,e,r){const s=B(t,"main");if(x.has(s))throw new A("A build is already running for main.",409);x.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await Z(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const l=a(b=>T.join(Jt.homedir(),".claude","kickflip","try",`${t.route}-main${b}`),"worktreeOf"),$=l(n?.worktree===l("-a")?"-b":"-a");return S.mkdirSync(T.dirname($),{recursive:!0}),await ge(t,e,l("")),gt($)?await M($,"checkout","--detach","--force",r.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",$,r.sha)),await St(t,"main",$,r)}catch(i){throw Rt(t,r,i),i}finally{x.delete(s)}}a(Ht,"launchMain");async function Ot(t,{manifest:e=Q()}={}){if(await at(e),kt(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(w=>!["route","key"].includes(w))||typeof t.route!="string"||typeof t.key!="string")throw new A("Send only the board route and card key.");const{route:r,key:s}=t;if(H(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(w=>w==="."||w==="..")))throw new A("Card key must be <file>/<id>.");const i=J(e).find(w=>w.route===r);if(!i)throw new A(`/${r} is not a board.`,404);if(H(s)&&!i.files.some(({slug:w,itemFile:v})=>v.items.some(j=>`${w}/${j.id}`===s)))throw new A("There is no item with this card key.",404);const n=await y();if(n)throw new A(n,409);const l=B(i,s),$=m.get(l);if(x.has(l)||$&&!$.ready&&!$.finished)throw new A(`A build is already running for ${s==="main"?"main":s===U?"the prototype site":"this card"}.`,409);if(s==="main"){const w=C(i).state.main;if(w?.state==="running")throw new A("A build is already running for main.",409);if(w?.state!=="failed"||!w.sha)throw new A("Main has no failed build to retry.",409);const{sha:v,branch:j,slug:I,at:_,script:E}=w;return Ht(i,et(r,e).repo,{sha:v,branch:j,slug:I,at:_,script:E,...i.build_target?{target:i.build_target}:{}})}x.add(l);const b=z.delete(l);try{m.has(l)&&await Z(m.get(l),!0);const w=s===U?Wt(i):await ae(i,s,e);return await St(i,s,w)}catch(w){throw it.delete(l),b&&w.status!==503&&K(i,s,{state:void 0,sha:void 0}),w}finally{x.delete(l)}}a(Ot,"start");const Wt=a(t=>{const e=Be(t.route);return gt(e)&&S.statSync(e).isDirectory()?S.realpathSync(e):""},"baseWorktree");async function $e(t,e){const{state:r}=C(t),s=B(t,U),i=m.get(s),n=(await Fe(t.dir)).length>0&&Wt(t);if(!n){i&&await Z(i),r.prototypes&&(r.prototypes=null,P(t));return}if(i?.ready&&!i.stopped&&!x.has(s)){const l=await M(n,"rev-parse","HEAD");if(l===r.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(i||x.has(s)||r.prototypes?.state==="failed"||await y())return;try{await Ot({route:t.route,key:U},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw r.prototypes={state:"failed"},It(t,U,l),P(t),l}}a($e,"serveBase");function Gt(t,e,{manifest:r=Q()}={}){const s=J(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(Gt,"lookup");async function xt({manifest:t=Q(),routes:e}={}){if(await at(t),kt(t),W)return G;if(G&&e){if(!X){const r={routes:new Set,manifest:t};r.done=G.catch(()=>{}).then(()=>(X=null,xt({manifest:r.manifest,routes:[...r.routes]}))),X=r}X.manifest=t;for(const r of e)X.routes.add(r);return X.done}return G||(G=(async()=>{await we();const r=J(t),s=ft(je,{}).prs||{};for(const[i,n]of[...p])r.some(l=>l.dir===i)||(p.delete(i),await ct(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||H(i.key)&&i.ready&&o()-i.visited>=Ye){await Z(i);const{state:n}=C(i.board);Ct(n,i.key),P(i.board)}for(const i of r){if(W)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),l=et(i.route,t),$=l.branch||"main",b=_e(l);for(const{slug:w,itemFile:v}of i.files)for(const j of v.items){const I=`${w}/${j.id}`,_=Re(j,i.github||v.repo).map(E=>s[We(E)]);if(!(j.lane==="done"||_.length&&_.every(E=>["MERGED","CLOSED"].includes(E?.state))))try{const E=B(i,I),f=await ae(i,I,t);if(f){const D=await M(f,"rev-parse","HEAD");let lt=0;try{lt=Number(await M(f,"rev-list","--count",`${b}..HEAD`))}catch{}Object.assign(n.cards[I]||={},{head:D,ahead:lt,script:gt(te(f,i.build_target))}),P(i)}else if(!z.has(E))continue;const N=n.cards[I];if((z.has(E)||i.auto_build&&N.script&&N.ahead>0&&(N.sha||bt.get(E))!==N.head&&N.state!=="running")&&!x.has(E))try{await Ot({route:i.route,key:I},{manifest:t})}catch(D){if([409,503].includes(D.status))continue;throw K(i,I,{sha:N.head}),await Bt({board:i,key:I,logFile:It(i,I,D)},1),D}}catch(E){c(`try /${i.route} ${I}: ${E.message}`)}}await $e(i,t).catch(w=>c(`try /${i.route} prototypes: ${w.message}`));try{if(!l.repo||await y())continue;const w=await M(l.repo,"rev-parse",b);let v=!1;try{await M(l.repo,"cat-file","-e",`${w}:${qt(i.build_target)}`),v=!0}catch{}if(!v){const N=m.get(B(i,"main"));N&&await Z(N),await wt(i),n.main=null,P(i);continue}const j=n.main;if(j?.state==="running"||j?.sha===w&&(j.target||null)===(i.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Gt(i.route,"main",{manifest:t}))))continue;const I=B(i,"main");if(x.has(I))continue;const _=await M(l.repo,"log","-1","--format=%cI",w);let E=i.github;if(!E)try{E=Vt(await M(l.repo,"remote","get-url","origin"))}catch{}const f={sha:w,branch:$,slug:E||"",at:_,script:v,...i.build_target?{target:i.build_target}:{}};await Ht(i,l.repo,f)}catch(w){c(`try /${i.route} main: ${w.message}`)}}})().finally(()=>{G=null}),G)}a(xt,"tick");function Lt(t,e){if(nt.has(t.dir))return nt.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},P(t);let l,$;try{l=await(s.local?rr:er)(s.repo,i)}catch(b){$=b.message}if(l===null?delete n.pull:n.pull={at:ht(),...$?{error:$}:{}},P(t),l===null)return null;if($)return c(`pull /${t.route}: ${$}`),{commits:0,error:$};if(l.commits){c(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await u(t,"main-pulled",{branch:i,...l})}catch(b){c(`pull /${t.route}: could not send main-pulled (${b.message})`)}}return{commits:l.commits}})().finally(()=>nt.delete(t.dir));return nt.set(t.dir,r),r}a(Lt,"pullMain");async function ve({manifest:t=Q(),routes:e}={}){await at(t);const r=[];for(const s of J(t)){if(W)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Lt(s,t))?.commits&&r.push(s.route)}catch(i){c(`pull /${s.route} failed: ${i.message}`)}}return r}a(ve,"pullMains");async function be(t,{manifest:e=Q()}={}){if(await at(e),W)throw new A("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 A("Send only the board route.");const r=J(e).find(n=>n.route===t.route);if(!r)throw new A(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Lt(r,e);if(!i)throw new A("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&xt({manifest:e,routes:[r.route]}).catch(n=>c(`try /${r.route} main: ${n.message}`)),i}a(be,"pull");async function ke(){W=!0;const t=Promise.all([...m.values()].map(e=>e.ready?Z(e):ct(e)).concat([...p.values()].map(e=>ct(e))));await Promise.all([t,G,...nt.values()])}return a(ke,"stop"),{start:Ot,tick:xt,stop:ke,lookup:Gt,initialize:at,pull:be,pullMains:ve,endUnshared:pe}}a(tr,"createTryManager");const $t=1e4,vt=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:$t,env:Ae()})).stdout,"originGit");async function er(o,d,{run:h=vt}={}){const c=a(async(...p)=>(await h(...p)).trim(),"git"),y=a(p=>c(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),u=`refs/remotes/origin/${d}`,k=await y(`refs/heads/${d}`)||await y(u);try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${u}`),await c(o,"checkout","--quiet","--force","-B",d,u),await c(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${$t/1e3} s`:jt(p.stderr,{repo:o}).error}`)}const g=await y(u),m=k&&k!==g?Number(await c(o,"rev-list","--count",`${k}..${g}`)):0;return{from:k||g,to:g,commits:m}}a(er,"resetBranch");async function rr(o,d,{run:h=vt}={}){const c=a(async(...u)=>(await h(...u)).trim(),"git"),y=a(u=>{throw new Error(`can\u2019t pull: ${u}`)},"refuse");try{await c(o,"config","--get","remote.origin.url")}catch{return null}try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(u){y(u.killed?`git fetch timed out after ${$t/1e3} s`:jt(u.stderr,{repo:o}).error)}try{const u=await c(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),k=await c(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=a((O,F)=>c(o,"merge-base","--is-ancestor",O,F).then(()=>!0,x=>{if(x.code===1)return!1;throw x}),"isAncestor");if(u===k||await g(k,u))return{from:u,to:u,commits:0};await g(u,k)||y(`${d} has split from origin`);const m=Number(await c(o,"rev-list","--count",`${u}..${k}`)),p=(await c(o,"worktree","list","--porcelain")).split(`
|
|
3
8
|
|
|
4
|
-
`).map(
|
|
5
|
-
`)).find(
|
|
6
|
-
`).at(-1))}}a(
|
|
7
|
-
`).map(
|
|
9
|
+
`).map(O=>O.split(`
|
|
10
|
+
`)).find(O=>O.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&y("local changes"),await c(p,"merge","--ff-only","--quiet",k)):await c(o,"update-ref",`refs/heads/${d}`,k,u),{from:u,to:k,commits:m}}catch(u){if(u.message.startsWith("can\u2019t pull: "))throw u;y(String(u.stderr||u.message).trim().split(`
|
|
11
|
+
`).at(-1))}}a(rr,"pullBranch");async function Or({manifest:o=Q(),log:d=console.log,routes:h}={}){const c=h?ft(Qt,{}):{};for(const y of J(o))if(!(h&&!h.includes(y.route)))try{const u=et(y.route,o);if(u.local||!u.repo)continue;const k=u.branch||"main",g=new Date().toISOString(),m=Date.now();try{await vt(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),c[y.route]={at:g,ok:!0}}catch(p){d(`fetch /${y.route} after ${Date.now()-m} ms: ${p.message}`);let O=y.github;if(!O&&!y.remote)try{O=Vt(await M(u.repo,"remote","get-url","origin"))}catch{}c[y.route]={at:g,ok:!1,...jt(p.killed?`git fetch timed out after ${$t/1e3} s`:p.stderr,{github:O||"",remote:y.remote,repo:u.repo})};continue}if(y.remote)try{await ir(y,u.repo,k,d)}catch(p){d(`pushed branches /${y.route}: ${p.message}`)}}catch(u){d(`fetch /${y.route} failed: ${u.message}`)}Yt(Qt,c)}a(Or,"fetchTryBranches");async function ir(o,d,h,c){const y=o.files.flatMap(({slug:g,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!He(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!y.length)return;const u=new Map((await vt(d,"ls-remote","--heads","origin")).split(`
|
|
12
|
+
`).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),k=Ie(o.remote)?.host||"the remote";for(const g of y){let m=u.get(g.branch);if(!m)try{m=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(m)try{await M(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&u.has(g.branch)))try{await De(o.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:k}]:["branch-gone",{branch:g.branch,host:k}]})}catch(O){c(`pushed ${g.key}: ${O.message}`)}}}a(ir,"settlePushed");const q=tr({waiting:a(()=>Me.waiting("tailscale"),"waiting")}),xr=a((...o)=>q.start(...o),"startBuild"),Er=a((...o)=>q.tick(...o),"tryTick"),Tr=a((...o)=>q.pull(...o),"pullNow"),jr=a((...o)=>q.pullMains(...o),"pullMains"),_r=a(()=>q.stop(),"stopTries"),Ir=a(()=>q.endUnshared(),"endUnsharedTries"),Ar=a((...o)=>q.initialize(...o),"initializeTries");export{A as BuildError,V as TRY_PORTS,tr as createTryManager,Ir as endUnsharedTries,Or as fetchTryBranches,Ar as initializeTries,ae as installWorktree,_t as isTryPort,rr as pullBranch,jr as pullMains,Tr as pullNow,er as resetBranch,xr as startBuild,_r as stopTries,Er 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};
|