@lovek4d/kickflip 0.7.1 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CONTEXT.md +25 -21
  2. package/SKILL.md +13 -9
  3. package/bin/event-lines.mjs +1 -1
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +1 -1
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/bin/board.mjs +68 -50
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +8 -8
  11. package/board/hub-lines.mjs +1 -1
  12. package/board/onboarding.mjs +3 -3
  13. package/board/permissions.mjs +2 -2
  14. package/board/public/app-asks.js +2 -14
  15. package/board/public/app-banner.js +8 -3
  16. package/board/public/app-cards.js +7 -20
  17. package/board/public/app-core.js +2 -2
  18. package/board/public/app-lanes.js +7 -15
  19. package/board/public/app-reader.js +7 -3
  20. package/board/public/app-snapshot.js +1 -1
  21. package/board/public/app.js +1 -1
  22. package/board/public/attach.js +18 -2
  23. package/board/public/dialogs.js +10 -12
  24. package/board/public/hub.css +0 -3
  25. package/board/public/linear-key.js +1 -1
  26. package/board/public/sidebar.css +1 -1
  27. package/board/public/style.css +42 -36
  28. package/board/public/ui-cards.js +33 -27
  29. package/board/public/ui-enums.js +2 -2
  30. package/board/public/ui-links.js +0 -1
  31. package/board/public/ui-order.js +1 -1
  32. package/board/public/ui-questions.js +8 -6
  33. package/board/public/ui-sidebar.js +1 -1
  34. package/board/public/ui-threads.js +26 -22
  35. package/board/public/ui.js +15 -9
  36. package/board/public/view-hub-settings.js +25 -30
  37. package/board/public/view-hub.js +23 -5
  38. package/board/public/view-questions.js +1 -1
  39. package/board/public/view-settings.js +4 -4
  40. package/board/server.mjs +18 -18
  41. package/board/store.mjs +30 -27
  42. package/board/tickets.mjs +3 -3
  43. package/board/worktree.mjs +15 -15
  44. package/hub/board-repo.mjs +1 -1
  45. package/hub/github.mjs +3 -3
  46. package/hub/hub.mjs +11 -11
  47. package/hub/linear.mjs +1 -1
  48. package/hub/new-board.mjs +2 -2
  49. package/hub/pr-report.mjs +2 -2
  50. package/hub/readiness.mjs +1 -1
  51. package/hub/repos.mjs +3 -3
  52. package/hub/review-send.mjs +1 -1
  53. package/hub/schedule.mjs +1 -1
  54. package/hub/slack.mjs +1 -1
  55. package/hub/try.mjs +8 -6
  56. package/hub/views.mjs +1 -1
  57. package/package.json +1 -1
  58. package/shared/fake-gh.mjs +19 -4
  59. package/shared/gh.mjs +4 -0
  60. package/shared/owner-check.mjs +4 -3
  61. package/shared/service.mjs +12 -12
  62. package/starter/server.mjs +3 -3
package/hub/linear.mjs CHANGED
@@ -1 +1 @@
1
- var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
1
+ var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub \u203A Integrations \u203A Linear first, or pick GitHub issues or None.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
package/hub/new-board.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as B,listBoards as D,readManifest as h,withManifestLock as u,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as L,routeFromName as M,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as v,validateBranch as A,validateRemote as G}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as T}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as q,linearWorks as H,savedClient as K}from"./linear.mjs";const d=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function Y(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(c=>!["name","github","remote","branch","purpose","tickets"].includes(c)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=M(t);L.test(s)||d("Name must contain a letter or digit for its route.");const{github:r,remote:n}=U(e),f=A(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;n&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let o;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:c,...m}=a;o=typeof c=="string"?c.trim():"",(!o||o.length>80)&&d("Name the new Linear project."),a={...m,project:o}}return{route:s,title:t,github:r,remote:n,branch:f,tickets:$(a),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(Y,"details");function U(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:v(e.github)}:{remote:G(e.remote).remote}}i(U,"repoInput");const _=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),E="That board route already exists. Reopen it from Closed boards if it is closed.",V=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),W=i(({purpose:e})=>`--template board --items data/items.json${e?` --description ${V(e)}`:""}`,"joinFlags");function R(e){const t=String(e).split(`
2
- `).map(s=>s.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(R,"errorLine");async function N(e,t){await u(async()=>{const s=h(),r=j(s.starting,e);!r||j(s.sites,e)||t.phase&&t.phase===r.phase||(s.starting[e]=t.phase?{...r,phase:t.phase,phase_at:new Date().toISOString()}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(s))})}i(N,"recordStart");const I=i(({title:e,purpose:t,repo:s,github:r,remote:n,branch:f,tickets:a},o)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:n},branch:f,tickets:a,phase:o.cloned(r||n)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),O=new Map,se=i(e=>O.get(e),"clonePercent"),z=1e3;async function x(e,{purpose:t,repo:s,github:r,remote:n,branch:f},{run:a,live:o,repos:c,changed:m}){let g,p=-1/0;const y=i(l=>{O.set(e,l),!(Date.now()-p<z)&&(p=Date.now(),m())},"progress");try{try{await c.ensure(r||n,f,{progress:y})}finally{O.delete(e)}await N(e,{phase:"starting"}),m();const l=await F(e,{repo:s,prompt:W({purpose:t}),run:a,live:o,clones:c.base});g={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){g={error:R(l.message)}}try{await N(e,g)}catch(l){return{error:R(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(x,"start");async function ne(e,{run:t,live:s,repos:r=k(),changed:n=i(()=>{},"changed"),linear:f}={}){const a=Y(e),{route:o,github:c,remote:m,newProject:g}=a;let{tickets:p}=a;p.source==="linear"&&!f&&!H()&&d(q);const y=r.path(c||m),l=g===void 0?null:f||K();if(l){_(h(),o)&&d(E);try{p={...p,project:(await l.createProject(p.team,g)).id}}catch(b){d(b.message,b.status||502)}}return await u(async()=>{const b=h();_(b,o)&&d(E),b.starting={...b.starting,[o]:I({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:x(o,{...a,repo:y},{run:t,live:s,repos:r,changed:n})}}i(ne,"requestBoard");function S(e,t,s){const r=j(e.starting,t);return(!r||j(e.sites,t)&&!r.joined_at||!C(r))&&d(`Only a board that didn\u2019t start can be ${s}.`),r}i(S,"failedStart");async function ae(e,{run:t,live:s,stop:r,repos:n=k(),changed:f=i(()=>{},"changed")}={}){const a=S(h(),e,"retried");a.joined_at&&d("Its session joined; a board that is not listening yet can only be dismissed."),a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await T(a,{live:s,stop:r})}catch(c){d(`Couldn\u2019t stop its earlier session: ${c.message}`,502)}const o=await u(async()=>{const c=h(),m=S(c,e,"retried");return c.starting[e]=I({...m,repo:n.path(m.github||m.remote),purpose:m.purpose||""},n),await w(c),c.starting[e]});return{route:e,started:x(e,o,{run:t,live:s,repos:n,changed:f})}}i(ae,"retryBoard");async function ie(e){return u(async()=>{const t=h();return S(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}i(ie,"dismissBoard");async function oe({listening:e=B}={}){return Object.values(h().starting||{}).some(t=>t.joined_at)?u(async()=>{const t=h(),s=new Map(D(t).map(n=>[n.route,n])),r=Object.entries(t.starting||{}).filter(([n,f])=>f.joined_at&&(!s.has(n)||e(s.get(n)))).map(([n])=>n);if(!r.length)return[];for(const n of r)delete t.starting[n];return Object.keys(t.starting).length||delete t.starting,await w(t),r}):[]}i(oe,"settleStarts");export{se as clonePercent,ie as dismissBoard,U as repoInput,ne as requestBoard,ae as retryBoard,oe as settleStarts};
1
+ var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as B,listBoards as D,readManifest as h,withManifestLock as u,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as L,routeFromName as M,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as v,validateBranch as A,validateRemote as G}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as T}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as q,linearWorks as H,savedClient as K}from"./linear.mjs";const d=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function Y(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(c=>!["name","github","remote","branch","purpose","tickets"].includes(c)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=M(t);L.test(s)||d("Name must contain a letter or digit for its route.");const{github:r,remote:n}=U(e),f=A(e.branch);let a=e.tickets===void 0?{source:"none"}:e.tickets;n&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick None or Linear.");let o;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:c,...m}=a;o=typeof c=="string"?c.trim():"",(!o||o.length>80)&&d("Name the new Linear project."),a={...m,project:o}}return{route:s,title:t,github:r,remote:n,branch:f,tickets:$(a),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(Y,"details");function U(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:v(e.github)}:{remote:G(e.remote).remote}}i(U,"repoInput");const _=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),E="That board route already exists. Reopen it from Closed boards if it is closed.",V=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),W=i(({purpose:e})=>`--template board --items data/items.json${e?` --description ${V(e)}`:""}`,"joinFlags");function N(e){const t=String(e).split(`
2
+ `).map(s=>s.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(N,"errorLine");async function R(e,t){await u(async()=>{const s=h(),r=j(s.starting,e);!r||j(s.sites,e)||t.phase&&t.phase===r.phase||(s.starting[e]=t.phase?{...r,phase:t.phase,phase_at:new Date().toISOString()}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(s))})}i(R,"recordStart");const x=i(({title:e,purpose:t,repo:s,github:r,remote:n,branch:f,tickets:a},o)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:n},branch:f,tickets:a,phase:o.cloned(r||n)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),O=new Map,se=i(e=>O.get(e),"clonePercent"),z=1e3;async function I(e,{purpose:t,repo:s,github:r,remote:n,branch:f},{run:a,live:o,repos:c,changed:m}){let g,p=-1/0;const y=i(l=>{O.set(e,l),!(Date.now()-p<z)&&(p=Date.now(),m())},"progress");try{try{await c.ensure(r||n,f,{progress:y})}finally{O.delete(e)}await R(e,{phase:"starting"}),m();const l=await F(e,{repo:s,prompt:W({purpose:t}),run:a,live:o,clones:c.base});g={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){g={error:N(l.message)}}try{await R(e,g)}catch(l){return{error:N(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(I,"start");async function ne(e,{run:t,live:s,repos:r=k(),changed:n=i(()=>{},"changed"),linear:f}={}){const a=Y(e),{route:o,github:c,remote:m,newProject:g}=a;let{tickets:p}=a;p?.source==="linear"&&!f&&!H()&&d(q);const y=r.path(c||m),l=g===void 0?null:f||K();if(l){_(h(),o)&&d(E);try{p={...p,project:(await l.createProject(p.team,g)).id}}catch(b){d(b.message,b.status||502)}}return await u(async()=>{const b=h();_(b,o)&&d(E),b.starting={...b.starting,[o]:x({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:I(o,{...a,repo:y},{run:t,live:s,repos:r,changed:n})}}i(ne,"requestBoard");function S(e,t,s){const r=j(e.starting,t);return(!r||j(e.sites,t)&&!r.joined_at||!C(r))&&d(`Only a board that didn\u2019t start can be ${s}.`),r}i(S,"failedStart");async function ae(e,{run:t,live:s,stop:r,repos:n=k(),changed:f=i(()=>{},"changed")}={}){const a=S(h(),e,"retried");a.joined_at&&d("Its session joined; a board that is not listening yet can only be dismissed."),a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await T(a,{live:s,stop:r})}catch(c){d(`Couldn\u2019t stop its earlier session: ${c.message}`,502)}const o=await u(async()=>{const c=h(),m=S(c,e,"retried");return c.starting[e]=x({...m,repo:n.path(m.github||m.remote),purpose:m.purpose||""},n),await w(c),c.starting[e]});return{route:e,started:I(e,o,{run:t,live:s,repos:n,changed:f})}}i(ae,"retryBoard");async function ie(e){return u(async()=>{const t=h();return S(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}i(ie,"dismissBoard");async function oe({listening:e=B}={}){return Object.values(h().starting||{}).some(t=>t.joined_at)?u(async()=>{const t=h(),s=new Map(D(t).map(n=>[n.route,n])),r=Object.entries(t.starting||{}).filter(([n,f])=>f.joined_at&&(!s.has(n)||e(s.get(n)))).map(([n])=>n);if(!r.length)return[];for(const n of r)delete t.starting[n];return Object.keys(t.starting).length||delete t.starting,await w(t),r}):[]}i(oe,"settleStarts");export{se as clonePercent,ie as dismissBoard,U as repoInput,ne as requestBoard,ae as retryBoard,oe as settleStarts};
package/hub/pr-report.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var _=Object.defineProperty;var i=(t,n)=>_(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const E="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",g="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",w=i(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function p(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(r,u,m)=>{if(r)return o(new Error(w(m||r.message)));try{s(JSON.parse(u))}catch(l){o(new Error(w(`unreadable output (${l.message})`)))}})})}i(p,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(t){const n=process.env;if(!t)return n;const s=await new Promise(o=>h("gh",["auth","token","-u",t],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")}},(r,u)=>o(r?"":u.trim())));if(!s)throw new Error(`gh has no ${t} account; run gh auth login`);return{...n,GH_TOKEN:s}}i(y,"envFor");async function O({repo:t,pr:n,replyTo:s,words:o,login:r}){const u=await y(r),m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await p(["api",m,"-f",`body=${v}${o}
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}`],u)).html_url}catch(l){throw new Error(`${t}#${n}: ${l.message}`)}}i(O,"postToPR");async function A({repo:t,pr:n,head:s,body:o,login:r}){const u=await y(r);try{return(await p(["api",`repos/${t}/pulls/${n}/reviews`,"-f",`body=${o}`,"-f","event=COMMENT","-f",`commit_id=${s}`],u)).html_url}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}}i(A,"postReview");async function T(t){return(await p(["api","--paginate","--slurp",`repos/${t}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}i(T,"collaborators");async function I(t){try{return(await p(["api",`users/${encodeURIComponent(t)}`])).login}catch{return null}}i(I,"githubUser");async function H({repo:t,pr:n,since:s}){try{return(await p(["api","--paginate","--slurp",`repos/${t}/pulls/${n}/reviews`])).flat().filter(r=>Date.parse(r.submitted_at)>Date.parse(s)).map(r=>({login:r.user?.login??null,at:r.submitted_at}))}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}}i(H,"reviewsSince");function L({repo:t,pr:n}){return new Promise((s,o)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,g].filter(Boolean).join(":")},timeout:9e4},(r,u,m)=>r?o(new Error(w(m||r.message))):s())})}i(L,"markReady");const N=i(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function q({repo:t,pr:n,since:s}){let o;try{o=await p(["pr","view",String(n),"--repo",t,"--json",E])}catch(c){throw new Error(`${t}#${n}: ${c.message}`)}const r=i(c=>p(["api","--paginate","--slurp",`repos/${t}/${c}`]).then(d=>d.flat()).catch(()=>[]),"list"),[u,m,l]=await Promise.all([r(`issues/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/reviews`)]);return{view:o,issueComments:u,reviewComments:m,reviews:l}}i(q,"fetchPR");const $="author { __typename login }",S=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles 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 } } } } }`;function P(t,n=process.env){return new Promise((s,o)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(r,u,m)=>{let l=null;try{l=JSON.parse(u)}catch{}if(l?.data)return s(l);o(new Error(`GitHub query: ${w(r?m||r.message:"unreadable output")}`))})})}i(P,"graphql");const R=i(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user"),C="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function U(t,n=[]){const s=t.map(({repo:l,pr:c},d)=>{const[e,f]=l.split("/");return`pr${d}: repository(owner: ${JSON.stringify(e)}, name: ${JSON.stringify(f)}) { pullRequest(number: ${Number(c)}) { ${S} } }`});n.forEach((l,c)=>{const[d,e]=l.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(d)}, name: ${JSON.stringify(e)}) { ${C} }`)});const{data:o,errors:r=[]}=await P(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),u=Object.fromEntries(n.flatMap((l,c)=>o[`open${c}`]?.pullRequests?[[l,o[`open${c}`].pullRequests.nodes]]:[])),m=t.map(({repo:l,pr:c},d)=>{const e=o[`pr${d}`]?.pullRequest;if(!e)return{error:`${l}#${c}: ${r.find(a=>a.path?.[0]===`pr${d}`)?.message||"GitHub did not answer for it"}`};const f=i(a=>({id:a.databaseId??null,user:R(a.author),created_at:a.createdAt??null,body:a.body??null,html_url:a.url??null}),"comment");return{view:{state:e.state,isDraft:e.isDraft,reviewDecision:e.reviewDecision,mergeable:e.mergeable,mergeStateStatus:e.mergeStateStatus,headRefOid:e.headRefOid,statusCheckRollup:e.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,changedFiles:e.changedFiles??null,approved:(e.latestReviews?.nodes??[]).some(a=>a.state==="APPROVED"),commits:(e.recent?.nodes??[]).map(a=>({oid:a.commit.oid.slice(0,10),subject:a.commit.messageHeadline}))},issueComments:(e.comments?.nodes??[]).map(f),reviewComments:(e.reviewThreads?.nodes??[]).flatMap(a=>a.comments?.nodes??[]).map(a=>({...f(a),path:a.path??null,line:a.line??null,in_reply_to_id:a.replyTo?.databaseId??null})),reviews:(e.reviews?.nodes??[]).map(a=>({id:a.databaseId??null,user:R(a.author),submitted_at:a.submittedAt??null,body:a.body??null,state:a.state??null,html_url:a.url??null}))}});return{login:o.viewer?.login??null,rate:o.rateLimit??null,results:m,open:u}}i(U,"fetchPRs");function k({view:t,issueComments:n,reviewComments:s,reviews:o},{pr:r,since:u,me:m}){const l=i((e,f)=>({kind:f,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),c=[...n.map(e=>({...l(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...l(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...o.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>u).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,f)=>e.at<f.at?-1:e.at>f.at?1:0),d=t.statusCheckRollup??[];return{pr:Number(r),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:d.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:d.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:c,title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,files:t.changedFiles??null,approved:!!t.approved,commits:t.commits??[]}}i(k,"reportOf");async function F(t){let n;try{n=await p(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} 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(o=>o.path),requested:(s.reviewRequests||[]).map(o=>o.login).filter(Boolean)}))}i(F,"openPRs");async function B(t,n){let s;try{s=await p(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}return{state:s.state??null,files:(s.files||[]).map(o=>o.path)}}i(B,"prFiles");async function j({env:t}={}){const n=await p(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}i(j,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,E as PR_VIEW_FIELDS,N as checksPassed,T as collaborators,q as fetchPR,U as fetchPRs,j as githubLogin,I as githubUser,L as markReady,F as openPRs,A as postReview,O as postToPR,B as prFiles,k as reportOf,H as reviewsSince};
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 k=Object.defineProperty;var t=(s,n)=>k(s,"name",{value:n,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as S}from"./pr-report.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as P}from"./linear.mjs";import{slackStatus as T}from"./slack.mjs";import{readSettings as R,subscriptions as C}from"./push.mjs";import{tailscaleState as N}from"../bin/install.mjs";import{tailscaleBin as I,tailscaleInstallCommand as L}from"../shared/tailscale.mjs";const w=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],f=t((s,n,a)=>({settings:s,label:`Hub settings \u203A ${n}`,text:a}),"settingsFix");function x(s,{now:n=Date.now(),platform:a=process.platform}={}){const d=t((o,e)=>{switch(o){case"github":return 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",fix:f("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:f("linear","Linear","Add one in")};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",fix:f("slack","Slack","Paste a new one in")}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`}:{ready:!1,missing:"No Slack token set",fix:f("slack","Slack","Add one in")};case"tailscale":{let i,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(i=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(i="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(i="Tailscale isn't signed in",c="tailscale up"):(i="Tailscale isn't installed on the host",c=L(a)),i?{ready:!1,missing:i,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:i,kind:c,browser:m,added:p})=>({id:i,kind:c,browser:m,added:p}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:f("notifications","Notifications","Turn on notifications in")}}},"row");return w.filter(([o])=>Object.hasOwn(s,o)).map(([o,e,i])=>({id:o,name:e,...d(o,s[o]),waits:i}))}t(x,"readinessRows");const O=t(s=>(n,a)=>new Promise(d=>{$(n,a,{env:s,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function H({home:s=b.homedir(),env:n=process.env,now:a=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=h({home:s,env:n,now:a,cacheMs:d}),i={github:t(()=>S({env:n}).catch(()=>null),"github"),codex:e,linear:t(()=>P(s),"linear"),slack:t(()=>T(s),"slack"),tailscale:t(()=>N({run:O(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:I({env:n})}),"tailscale"),notifications:t(()=>({on:R(s).notifications.on,devices:C(s)}),"notifications"),...o},c=new Map;async function m(l,r){if(!["github","tailscale"].includes(l))return i[l]({fresh:r});const g=c.get(l);if(!r&&g&&a()-g.at<d)return g.value;const u=Promise.resolve().then(()=>i[l]());return c.set(l,{at:a(),value:u}),u}t(m,"check");const p=t(async({fresh:l=!1}={})=>x(Object.fromEntries(await Promise.all(w.map(async([r])=>[r,await m(r,l)]))),{now:a()}),"rows");async function y(l){const[r]=x({[l]:await m(l,!1)},{now:a()});return r.ready||!r.waits?null:r.waits.replace("this",`${r.name}: ${r.missing.replace(/\.$/,"")}`)}return t(y,"waiting"),{rows:p,waiting:y}}t(H,"readinessReader");const A=h(),q=H({probes:{codex:A}});export{A as readCodex,q as readiness,H as readinessReader,x as readinessRows};
1
+ var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,d;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,d="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",d="kickflip doctor --fix"):(n="Tailscale isn't signed in",d="tailscale up"):(n="Tailscale isn't installed on the host",d=H(a)),n?{ready:!1,missing:n,fix:{command:d}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:d,browser:f,added:m})=>({id:n,kind:d,browser:f,added:m}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...l(o,t[o]),waits:n}))}s(k,"readinessRows");const A=s(t=>(i,a)=>new Promise(l=>{$(i,a,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=y({home:t,env:i,now:a,cacheMs:l}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:A(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...o},d=new Map;async function f(r,c){if(!["github","tailscale"].includes(r))return n[r]({fresh:c});const p=r==="github"&&P({file:T(t),now:a()});if(p)return{limited:p};const g=d.get(r);if(!c&&g&&a()-g.at<l)return g.value;const u=Promise.resolve().then(()=>n[r]());return d.set(r,{at:a(),value:u}),u}s(f,"check");const m=s(async({fresh:r=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([c])=>[c,await f(c,r)]))),{now:a()}),"rows");async function w(r){const[c]=k({[r]:await f(r,!1)},{now:a()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:m,waiting:w}}s(F,"readinessReader");const E=y(),X=F({probes:{codex:E}});export{E as readCodex,X as readiness,F as readinessReader,k as readinessRows};
package/hub/repos.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var Y=Object.defineProperty;var i=(t,e)=>Y(t,"name",{value:e,configurable:!0});import v from"node:fs";import h from"node:path";import{spawn as Z}from"node:child_process";import{reposDir as A}from"../shared/repos.mjs";import{withFileLock as tt}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 et(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(et,"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 vt(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(vt,"repoLookupRoute");function z(t,e,{onStderr:s,...r}={}){return 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 $t(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($t,"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 St(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(St,"isClone");function st(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(st,"cloneMeter");const nt=/^(?:Cloning into |remote: (?:Enumerating|Counting|Compressing|Total) |Receiving objects:|Resolving deltas:|Updating files:)/;function ot(t,e){return(String(t||"").split(/\n\s*\n/).map(r=>r.split(/[\r\n]+/).map(o=>o.trim()).filter(o=>o&&!nt.test(o))).find(r=>r.length)?.at(-1)||e).replace(/(\w+:\/\/)[^/\s@]+@/g,"$1")}i(ot,"errorLine");const _=i((t,e)=>ot(t.stderr||t.stdout,e),"failure"),rt=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 rt(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):et(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"),it=i(t=>["-c","credential.helper=","-c","credential.https://github.com.helper=!gh auth git-credential",...t],"githubGitArgv"),at=i((t,e,s={})=>t("git",it(e),{...s,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"githubGit"),ct=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:ct()}),"remoteGit"),U=i(t=>$(t)?L:at,"gitFor"),lt=i(t=>$(t)?t:`https://github.com/${t}.git`,"cloneURL"),ut=/not found|authentication failed|could not read username|invalid username or password|permission denied|terminal prompts disabled|\b403\b/i;function ft(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&&ut.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(ft,"gitReachFault");const N=i((t,e)=>{const{error:s,hint:r}=ft(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 xt({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}),tt(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",lt(n),h.join(p,"repo")],a?{onStderr:st(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(xt,"repoService");export{st as cloneMeter,$t as defaultBranch,ft as gitReachFault,W as githubFromInput,et as githubFromOrigin,it as githubGitArgv,St as isClone,$ as isRemote,O as remoteFromInput,ct as remoteGitEnv,vt as repoLookupRoute,B as repoName,b as repoParts,x as repoPath,xt as repoService,z as runRepo,E as validateBranch,S as validateGithub,k as validateRemote};
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};
@@ -1 +1 @@
1
- var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub settings \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function N({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(N,"sendReviews");export{N as sendReviews};
1
+ var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub \u203A Integrations \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function L({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(L,"sendReviews");export{L as sendReviews};
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:n=null,backAt:o=null}){return o&&o>t?new Date(o):new Date(t.getTime()+v({present:e,remaining:n})*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:u}=_(t?.notifications);return u?{error:u}: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(s=>(n>s?["snoozed",n]:null)||D(s,e)&&["weekend",D(s,e)]||C(s,e)&&["overnight",y(s,e.quiet.end)],"at"),r=o(t);if(!r)return null;let u=r[1];for(let s=o(u),p=0;s&&p<8;s=o(u),p++)u=s[1];return{until:u,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,u)=>{r.tag&&n.set(r.tag,u)});const o=new Map;return t.filter((r,u)=>!r.tag||n.get(r.tag)===u).forEach(r=>{const u=I(r.url)??Symbol(r.id);o.set(u,[...o.get(u)||[],r])}),[...o].flatMap(([r,u])=>{if(u.length===1){const[l]=u;return[{title:l.title,body:l.body||"",url:l.url||"/",tag:l.tag||void 0,kind:l.kind,members:[l.id]}]}const s=e[r];if(s&&!s.count)return[];const p=r==="_hub"?"Hub":r,g=u.map(l=>l.title),z=s?`${s.count} need${s.count===1?"s":""} you on ${p}`:`${u.length} updates on ${p}`,E=s?s.summary:[...g.slice(0,2),...g.length>2?[`and ${g.length-2} more`]:[]].join(" \xB7 "),M=u.every(l=>l.kind==="action")?"action":u.find(l=>l.kind!=="action").kind;return[{title:z,body:E,url:`/${r}/`,tag:`summary:${r}`,kind:M,members:u.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};
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/slack.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
  var b=Object.defineProperty;var n=(r,e)=>b(r,"name",{value:e,configurable:!0});import u from"node:fs";import h from"node:path";import k from"node:os";import{withFileLock as v}from"./locks.mjs";import{readSecret as $,writeSecret as d}from"./secrets.mjs";const m=["self_dm","owner_slack_user","slack_domain","timezone"],p=n(()=>h.join(k.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(m.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function _(r){const e=p();u.mkdirSync(h.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
- `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub settings \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};
2
+ `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub \u203A Integrations \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};
package/hub/try.mjs CHANGED
@@ -1,7 +1,9 @@
1
- var se=Object.defineProperty;var a=(c,u)=>se(c,"name",{value:u,configurable:!0});import T from"node:fs";import E from"node:path";import Nt from"node:os";import mt from"node:http";import{execFile as ie,spawn as ne}from"node:child_process";import{createInterface as Dt}from"node:readline";import{promisify as oe}from"node:util";import{listBoards as J,readManifest as z,readJSON as nt,saveJSON as Rt,appendEvent as ae,GITHUB_STATE as ce,REPO_FETCH as Bt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Ft,mainRefOf as le}from"./board-repo.mjs";import{gitReachFault as Ht,remoteFromInput as de,remoteGitEnv as ue}from"./repos.mjs";import{readiness as fe}from"./readiness.mjs";import{tailscaleBin as pe}from"../shared/tailscale.mjs";import{buildScriptError as he,buildScriptOf as Wt}from"../board/build-script.mjs";import{appendStatus as me,recordedWorktree as ye,servedRoundsOpen as we,setItem as ge}from"../board/store.mjs";import{BASE_KEY as K,baseWorktreeOf as $e}from"../board/prototype-base.mjs";import{cardPRs as be,hasPR as ve,prKeyOf as Se}from"../shared/card-prs.mjs";import{hubGate as ke}from"../board/on-host.mjs";import{pipeUpgrade as Oe,refuse as Te,refuseUpgrade as Ee}from"./front-door.mjs";import{shareOf as xe}from"./share.mjs";class x extends Error{static{a(this,"BuildError")}constructor(u,m=400){super(u),this.status=m}}const yt=oe(ie),j=a(async(c,...u)=>(await yt("git",["-C",c,...u],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),_e=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,Y={first:8500,last:8599},wt=a(c=>/^\d+$/.test(String(c))&&Number(c)>=Y.first&&Number(c)<=Y.last,"isTryPort"),je=30*6e4,Gt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],lt={main:"main",[K]:"prototypes"},F=a(c=>!Object.hasOwn(lt,c),"isCard"),Lt=a((c,u)=>E.join(c,Wt(u)),"scriptAt"),gt=a(c=>T.existsSync(c),"exists"),Kt=a((c,u)=>E.join(c.dir,"data","try",`${u.split("/").at(-1)}.log`),"logFileOf");function $t(c,u,m){const y=Kt(c,u);return T.mkdirSync(E.dirname(y),{recursive:!0}),T.writeFileSync(y,m.message+`
2
- `,{mode:384}),T.chmodSync(y,384),y}a($t,"failureLog");const Ie=a(c=>new Promise(u=>{const m=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},y=>{y.destroy(),u(!0)});m.on("timeout",()=>m.destroy()),m.on("error",()=>u(!1))}),"probe"),Ut=a((c,u)=>{try{process.kill(-c.pid,u)}catch{}},"signalGroup");async function Jt(c,u,m){const y=await ye(c.dir,u),[S,p]=u.split("/"),k=c.files.find(v=>v.slug===S)?.itemFile,d=y?.path||k?.items.find(v=>v.id===p)?.worktree;if(typeof d!="string"||!d||d.split(E.sep).includes(".."))return"";let g=y?.repo;const $=(c.github||k?.repo||"").toLowerCase();if(!g&&$){const v=[c.owner?.repo,c.dir,...Object.values(m?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(v.filter(P=>typeof P=="string"&&E.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await j(I,"remote","get-url","origin"))?.[1].toLowerCase()===$){g=await j(I,"rev-parse","--show-toplevel");break}}catch{}}if(!y?.path&&!g||y?.path&&!E.isAbsolute(d))return"";try{const v=[E.join(T.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&E.isAbsolute(g))try{v.push(E.join(T.realpathSync(g),".claude","worktrees"))}catch{}const I=E.resolve(g||"/",d),P=T.realpathSync(I);return v.some(W=>I.startsWith(W+E.sep)&&P.startsWith(W+E.sep))&&T.statSync(P).isDirectory()?P:""}catch{return""}}a(Jt,"installWorktree");function Ce({now:c=Date.now,answers:u=Ie,log:m=console.log,waiting:y=a(async()=>null,"waiting"),event:S=ae,gate:p=ke}={}){const k=new Map,d=new Map,g=new Set,$=new Set,v=new Map,I=new Map,P=new Map,W=new Map;let G=!1,H=null,Q=null,ot=null;const at=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=E.join(t.dir,"data","try.json"),s=nt(e,{kind:null,main:null,cards:{}}),i={...wt(s.main_port)?{main_port:s.main_port}:{},kind:s.kind||null,main:s.main||null,cards:{...s.cards},...s.pull?{pull:{...s.pull,running:void 0}}:{}},r={state:i,file:e,written:JSON.stringify(s)};k.set(t.dir,r),i.main_port&&I.set(t.dir,i.main_port);const n=Object.entries(i.cards).filter(([,o])=>o.state==="running");for(const[o]of n)$.add(D(t,o));return i.kind==="web"?(i.cards=Object.fromEntries(n),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),s.prototypes?.state==="failed"&&(i.prototypes=s.prototypes),M(t),r}a(N,"stateOf");function M(t){const e=k.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Rt(e.file,e.state),e.written=s)}a(M,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>F(e)?t.cards[e]:t[lt[e]],"entryOf"),zt=a((t,e,s)=>{F(e)?t.cards[e]=s:t[lt[e]]=s},"setEntry"),kt=a((t,e)=>{F(e)?delete t.cards[e]:t[lt[e]]=null},"dropEntry");function X(t,e,s){const{state:i}=N(t),r=St(i,e);if(!r)return;const n=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,F(e)&&n?{at:at()}:{}),M(t)}a(X,"update");function dt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(s=>s.site)];for(const s of e){if(!s?.dir)continue;const i=nt(E.join(s.dir,"data","try.json"),{}).main_port;wt(i)&&I.set(s.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:s}=t,i=D(e,s),r=s==="main"?I.get(e.dir):v.get(i);if(r)return r;const n=new Set([...I.values(),...v.values()]);let o=Y.first;for(;n.has(o)&&o<=Y.last;)o++;if(o>Y.last)throw new Error(`No try ports available (${Y.first}\u2013${Y.last}). Stop a served card and retry.`);return s==="main"?(I.set(e.dir,o),N(e).state.main_port=o,M(e)):v.set(i,o),o}a(Yt,"publicPort");const rt=a(async t=>{const e=pe();if(!e)throw new Error("Tailscale isn't installed on this host");return yt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ot(t){try{await rt(["serve",`--https=${t}`,"off"])}catch(e){m(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ot,"removePort");const Tt=a(t=>xe(nt(E.join(t.dir,"board.json"),{})),"sharedWith");async function Et(t,e){const s=await p(t),i=t.headers["tailscale-user-login"];return s!=="other"||!i||!Tt(e).includes(i)?s:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Et,"relayVerdict");function Vt(){for(const t of d.values()){if(!t.shared?.size)continue;const e=Tt(t.board);for(const s of t.shared)e.includes(s.login)||(t.shared.delete(s),s.res.destroy())}}a(Vt,"endUnshared");async function Qt(t){t.https=Yt(t);const e=t.sockets=new Set,s=a(()=>{t.visited=c()},"visit"),i=a(n=>{e.add(n),n.on("data",s),n.on("close",()=>e.delete(n))},"track"),r=t.shared=new Set;if(t.relay=mt.createServer(async(n,o)=>{const w=await Et(n,t.board);if(w!=="owner"&&w!=="shared"){Te(n,o,w);return}if(w==="shared"){const h={login:n.headers["tailscale-user-login"],res:o};r.add(h),o.on("close",()=>r.delete(h))}const _=Object.fromEntries(Object.entries(n.headers).filter(([h])=>!Gt.includes(h))),f=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:_},h=>{o.writeHead(h.statusCode,h.headers),h.on("data",s),h.pipe(o),h.on("close",()=>{h.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||f.destroy()}),f.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(f)}),t.relay.on("connection",n=>{s(),i(n)}),t.relay.on("upgrade",async(n,o,w)=>{const _=await Et(n,t.board);if(_!=="owner"){Ee(o,_==="shared"?"view-only":_);return}i(Oe({req:n,socket:o,head:w,port:t.port,path:n.url,replace:Gt}))}),await new Promise((n,o)=>{t.relay.once("error",o),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await rt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(Qt,"openPort");async function Xt(t){if(t.closing=!0,await t.opening?.catch(()=>{}),t.exposed&&!G&&await Ot(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||v.delete(D(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...d.values()].filter(s=>s.ready&&s.relay&&!s.closing);if(!t.length)return;let e;try{e=JSON.parse((await rt(["serve","status","--json"])).stdout)}catch(s){m(`try ports check: ${(s.stderr||s.message).trim()}`);return}for(const s of t){const i=`http://127.0.0.1:${s.relay.address().port}`,r=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${s.https}`))?.[1];if(r?.Handlers?.["/"]?.Proxy!==i){m(`try port ${s.https}: its serve mapping was ${r?"changed":"gone"}; serving it again`);try{await rt(["serve","--bg",`--https=${s.https}`,i])}catch(n){m(`try port ${s.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(Zt,"reserve");function st(t=z()){if(ot)return ot;dt(t);for(const e of J(t))N(e);return ot=(async()=>{try{const{stdout:e}=await rt(["serve","status","--json"]),s=JSON.parse(e);for(const i of Object.keys(s?.TCP||{}))wt(i)&&![...d.values()].some(r=>r.https===Number(i))&&await Ot(i)}catch(e){m(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ot}a(st,"initialize");async function xt(t,e,s,i){const{board:r,key:n}=t;if(X(r,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;let o="",w=!1;try{for await(const h of Dt({input:T.createReadStream(t.logFile),crlfDelay:1/0}))w||=_e.test(h),h.trim()&&(o=h)}catch(h){o=`could not read the build log: ${h.message}`}const _=t.port||(N(r).state.kind||(r.files.some(({itemFile:h})=>h.items.some(O=>O.install?.url))?"app":"web"))==="web"?"Serve":"Build",f=w?"unlock the login keychain on the host":i?.message||s&&`stopped by ${s}`||o;await me(r.dir,n,"run-failed",{kind:_,code:e??1,why:f})}a(xt,"failure");function ct(t){return t.ending||(Ut(t.child,"SIGTERM"),t.ending=new Promise(e=>{const s=a(()=>{clearTimeout(i),clearInterval(r),e()},"finish");let i=setTimeout(()=>{Ut(t.child,"SIGKILL"),i=setTimeout(s,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{s()}},20)})),t.ending}a(ct,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ct(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||P.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,s,i={}){if(!s)throw new x("This card has no valid worktree.");const r=Lt(s,t.build_target),n=he(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(s)+E.sep))throw new x("Build script must be inside the worktree.");const o=await j(s,"rev-parse","HEAD");if(G)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:w}=N(t),_=St(w,e),f={...F(e)?_:{},...i,sha:o,script:!0,state:"running",...F(e)?{at:at(),head:o}:{},...e===K?{since:at()}:{}};delete f.port,delete f.https;const h=Kt(t,e);T.mkdirSync(E.dirname(h),{recursive:!0});const O=T.openSync(h,"w",384);T.fchmodSync(O,384);const b=ne(r,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(w,e,f),M(t);const R=D(t,e),l={child:b,board:t,key:e,logFile:h,visited:c(),stopped:!1,port:null,ready:!1};d.set(R,l),P.set(R,o);let B,C;const q=a(A=>{C||=new Error(A),ct(l)},"fail");b.on("error",A=>{B=A}),b.stdout.on("data",A=>T.writeSync(O,A)),b.stderr.on("data",A=>T.writeSync(O,A));const L=Dt({input:b.stdout,crlfDelay:1/0});return L.on("line",A=>{const it=/^port (\d{1,5})$/.exec(A);if(!it||l.port||l.stopped)return;const U=Number(it[1]);if(U<1||U>65535){q("Build script printed an invalid port.");return}l.port=U,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const Mt=a(async()=>{if(!(pt||l.stopped||l.ready||b.exitCode!==null||b.signalCode!==null)){pt=!0;try{if(await u(U)){if(l.stopped||C||b.exitCode!==null||b.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||C||b.exitCode!==null||b.signalCode!==null))return;l.ready=!0,l.visited=c(),w.kind="web",X(t,e,{state:"ready",port:U,https:l.https}),clearInterval(l.poll)}else c()>=tt&&q("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{pt=!1}}},"check");l.poll=setInterval(()=>Mt().catch(ht=>q(ht.message)),250),Mt().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{ct(l)}),l.done=new Promise(A=>b.once("close",(it,U)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(O),(async()=>{if(await ct(l),await Xt(l),l.finished=!0,l.stopped)return;const tt=e===K&&!l.port?new Error('Build script exited without printing "port <n>".'):null;C||B||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,U,C||B||(l.port&&!l.ready?new Error("Build script exited before its port answered."):tt)):l.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):kt(w,e),M(t)):(w.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>m(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{d.get(R)===l&&d.delete(R),A()})})),m(`build /${t.route} ${e}: started; log ${h}`),{status:"started"}}a(jt,"launch");async function It(t,e,s){const i=D(t,"main");if(g.has(i))throw new x("A build is already running for main.",409);g.add(i);try{d.has(i)&&await Z(d.get(i),!0);const r=E.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(E.dirname(r),{recursive:!0}),gt(r)?await j(r,"checkout","--detach","--force",s.sha):(await j(e,"worktree","prune"),await j(e,"worktree","add","--detach",r,s.sha)),await jt(t,"main",r,s)}catch(r){throw r.status!==503&&(N(t).state.main={...s,state:"failed"},$t(t,"main",r),M(t)),r}finally{g.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await st(e),dt(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(f=>!["route","key"].includes(f))||typeof t.route!="string"||typeof t.key!="string")throw new x("Send only the board route and card key.");const{route:s,key:i}=t;if(F(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(f=>f==="."||f==="..")))throw new x("Card key must be <file>/<id>.");const r=J(e).find(f=>f.route===s);if(!r)throw new x(`/${s} is not a board.`,404);if(F(i)&&!r.files.some(({slug:f,itemFile:h})=>h.items.some(O=>`${f}/${O.id}`===i)))throw new x("There is no item with this card key.",404);const n=await y();if(n)throw new x(n,409);const o=D(r,i),w=d.get(o);if(g.has(o)||w&&!w.ready&&!w.finished)throw new x(`A build is already running for ${i==="main"?"main":i===K?"the prototype site":"this card"}.`,409);if(i==="main"){const f=N(r).state.main;if(f?.state==="running")throw new x("A build is already running for main.",409);if(f?.state!=="failed"||!f.sha)throw new x("Main has no failed build to retry.",409);const{sha:h,branch:O,slug:b,at:R,script:l}=f;return It(r,et(s,e).repo,{sha:h,branch:O,slug:b,at:R,script:l,...r.build_target?{target:r.build_target}:{}})}g.add(o);const _=$.delete(o);try{d.has(o)&&await Z(d.get(o),!0);const f=i===K?Ct(r):await Jt(r,i,e);return await jt(r,i,f)}catch(f){throw v.delete(o),_&&f.status!==503&&X(r,i,{state:void 0,sha:void 0}),f}finally{g.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=$e(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function qt(t,e){const{state:s}=N(t),i=D(t,K),r=d.get(i),n=(await we(t.dir)).length>0&&Ct(t);if(!n){r&&await Z(r),s.prototypes&&(s.prototypes=null,M(t));return}if(r?.ready&&!r.stopped&&!g.has(i)){const o=await j(n,"rev-parse","HEAD");if(o===s.prototypes?.sha)return;m(`try /${t.route} prototypes: the base moved to ${o.slice(0,7)}; restarting its serve`)}else if(r||g.has(i)||s.prototypes?.state==="failed"||await y())return;try{await ut({route:t.route,key:K},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw s.prototypes={state:"failed"},$t(t,K,o),M(t),o}}a(qt,"serveBase");function Pt(t,e,{manifest:s=z()}={}){const i=J(s).find(n=>n.route===t);if(!i)return null;const r=[...d.values()].find(n=>n.board.dir===i.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Pt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await st(t),dt(t),G)return H;if(H&&e){if(!Q){const s={routes:new Set,manifest:t};s.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:s.manifest,routes:[...s.routes]}))),Q=s}Q.manifest=t;for(const s of e)Q.routes.add(s);return Q.done}return H||(H=(async()=>{await Zt();const s=J(t),i=nt(ce,{}).prs||{};for(const r of[...d.values()])if(!s.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=je){await Z(r);const{state:n}=N(r.board);kt(n,r.key),M(r.board)}for(const r of s){if(G)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),w=o.branch||"main",_=le(o);for(const{slug:f,itemFile:h}of r.files)for(const O of h.items){const b=`${f}/${O.id}`,R=be(O,r.github||h.repo).map(l=>i[Se(l)]);if(!(O.lane==="done"||R.length&&R.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,b),B=await Jt(r,b,t);if(B){const L=await j(B,"rev-parse","HEAD");let A=0;try{A=Number(await j(B,"rev-list","--count",`${_}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:L,ahead:A,script:gt(Lt(B,r.build_target))}),M(r)}else if(!$.has(l))continue;const C=n.cards[b];if(($.has(l)||r.auto_build&&C.script&&C.ahead>0&&(C.sha||P.get(l))!==C.head&&C.state!=="running")&&!g.has(l))try{await ut({route:r.route,key:b},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,b,{sha:C.head}),await xt({board:r,key:b,logFile:$t(r,b,L)},1),L}}catch(l){m(`try /${r.route} ${b}: ${l.message}`)}}await qt(r,t).catch(f=>m(`try /${r.route} prototypes: ${f.message}`));try{if(!o.repo||await y())continue;const f=await j(o.repo,"rev-parse",_);let h=!1;try{await j(o.repo,"cat-file","-e",`${f}:${Wt(r.build_target)}`),h=!0}catch{}if(!h){const C=d.get(D(r,"main"));C&&await Z(C),n.main=null,M(r);continue}const O=n.main;if(O?.state==="running"||O?.sha===f&&(O.target||null)===(r.build_target||null)&&(O.state==="failed"||O.state==="ready"&&(n.kind!=="web"||Pt(r.route,"main",{manifest:t}))))continue;const b=D(r,"main");if(g.has(b))continue;const R=await j(o.repo,"log","-1","--format=%cI",f);let l=r.github;if(!l)try{l=Ft(await j(o.repo,"remote","get-url","origin"))}catch{}const B={sha:f,branch:w,slug:l||"",at:R,script:h,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,B)}catch(f){m(`try /${r.route} main: ${f.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function At(t,e){if(W.has(t.dir))return W.get(t.dir);const s=(async()=>{const i=et(t.route,e),r=i.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},M(t);let o,w;try{o=await Pe(i.repo,r)}catch(_){w=_.message}if(o===null?delete n.pull:n.pull={at:at(),...w?{error:w}:{}},M(t),o===null)return null;if(w)return m(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){m(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await S(t,"main-pulled",{branch:r,...o})}catch(_){m(`pull /${t.route}: could not send main-pulled (${_.message})`)}}return{commits:o.commits}})().finally(()=>W.delete(t.dir));return W.set(t.dir,s),s}a(At,"pullMain");async function te({manifest:t=z(),routes:e}={}){await st(t);const s=[];for(const i of J(t)){if(G)break;if(e&&!e.includes(i.route))continue;const r=et(i.route,t);!r.local||!r.repo||(await At(i,t))?.commits&&s.push(i.route)}return s}a(te,"pullMains");async function ee(t,{manifest:e=z()}={}){if(await st(e),G)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const s=J(e).find(n=>n.route===t.route);if(!s)throw new x(`/${t.route} is not a board.`,404);const i=et(s.route,e);if(!i.local||!i.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await At(s,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[s.route]}).catch(n=>m(`try /${s.route} main: ${n.message}`)),r}a(ee,"pull");async function re(){G=!0;const t=Promise.all([...d.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...W.values()])}return a(re,"stop"),{start:ut,tick:ft,stop:re,lookup:Pt,initialize:st,pull:ee,pullMains:te,endUnshared:Vt}}a(Ce,"createTryManager");const bt=1e4,vt=a(async(c,...u)=>(await yt("git",["-C",c,...u],{encoding:"utf8",timeout:bt,env:ue()})).stdout,"originGit");async function Pe(c,u,{run:m=vt}={}){const y=a(async(...p)=>(await m(...p)).trim(),"git"),S=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await y(c,"config","--get","remote.origin.url")}catch{return null}try{await y(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${u}:refs/remotes/origin/${u}`)}catch(p){S(p.killed?`git fetch timed out after ${bt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await y(c,"rev-parse","--verify",`refs/heads/${u}^{commit}`),k=await y(c,"rev-parse","--verify",`refs/remotes/origin/${u}^{commit}`),d=a((v,I)=>y(c,"merge-base","--is-ancestor",v,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await d(k,p))return{from:p,to:p,commits:0};await d(p,k)||S(`${u} has split from origin`);const g=Number(await y(c,"rev-list","--count",`${p}..${k}`)),$=(await y(c,"worktree","list","--porcelain")).split(`
1
+ var we=Object.defineProperty;var a=(o,d)=>we(o,"name",{value:d,configurable:!0});import j from"node:fs";import O from"node:path";import Gt from"node:os";import Ot from"node:http";import{execFile as ye,spawn as ge}from"node:child_process";import{createInterface as Lt}from"node:readline";import{promisify as $e}from"node:util";import{listBoards as z,readManifest as V,readJSON as lt,saveJSON as Ut,appendEvent as ve,GITHUB_STATE as be,REPO_FETCH as Kt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Jt,mainRefOf as ke}from"./board-repo.mjs";import{gitReachFault as Et,remoteFromInput as Se,remoteGitEnv as Oe}from"./repos.mjs";import{readiness as Ee}from"./readiness.mjs";import{tailscaleBin as Te}from"../shared/tailscale.mjs";import{buildScriptError as xe,buildScriptOf as zt}from"../board/build-script.mjs";import{appendStatus as Yt,recordedWorktree as je,servedRoundsOpen as _e,setItem as Ie}from"../board/store.mjs";import{BASE_KEY as L,baseWorktreeOf as Ae}from"../board/prototype-base.mjs";import{cardPRs as Me,hasPR as Pe,prKeyOf as Ce}from"../shared/card-prs.mjs";import{hubGate as Ne}from"../board/on-host.mjs";import{pipeUpgrade as De,refuse as Fe,refuseUpgrade as Re}from"./front-door.mjs";import{shareOf as Be}from"./share.mjs";class _ extends Error{static{a(this,"BuildError")}constructor(d,y=400){super(d),this.status=y}}const dt=$e(ye),I=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),He=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,q={first:8500,last:8599},Tt=a(o=>/^\d+$/.test(String(o))&&Number(o)>=q.first&&Number(o)<=q.last,"isTryPort"),We=30*6e4,Qt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Ge=["x-frame-options"],Le=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([y])=>!Ge.includes(y)));if(typeof d["content-security-policy"]=="string"){const y=d["content-security-policy"].split(";").filter(l=>!/^\s*frame-ancestors\b/i.test(l)).join(";").trim();y?d["content-security-policy"]=y:delete d["content-security-policy"]}return d},"withoutFraming"),ht={main:"main",[L]:"prototypes"},R=a(o=>!Object.hasOwn(ht,o),"isCard"),Vt=a((o,d)=>O.join(o,zt(d)),"scriptAt"),mt=a(o=>j.existsSync(o),"exists"),qt=a((o,d)=>O.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function xt(o,d,y){const l=qt(o,d);return j.mkdirSync(O.dirname(l),{recursive:!0}),j.writeFileSync(l,y.message+`
2
+ `,{mode:384}),j.chmodSync(l,384),l}a(xt,"failureLog");const Xt=a(o=>new Promise(d=>{const y=Ot.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},l=>{l.destroy(),d(!0)});y.on("timeout",()=>y.destroy()),y.on("error",()=>d(!1))}),"probe"),Zt=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),te=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),ee=a(o=>new Promise(d=>{const y=a(()=>{clearTimeout(l),clearInterval(g),d()},"finish");Zt(o,"SIGTERM");let l=setTimeout(()=>{Zt(o,"SIGKILL"),l=setTimeout(y,1e3)},4e3);const g=setInterval(()=>{te(o)||y()},20)}),"endGroup");async function Ue(o){const{stdout:d}=await dt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),y=d.split(`
3
+ `).map(l=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(l)).filter(l=>l&&Number(l[2])===o).map(l=>({pid:l[1],command:l[3],cwd:""}));if(!y.length)return y;try{const{stdout:l}=await dt("lsof",["-a","-d","cwd","-Fpn","-p",y.map(f=>f.pid).join(",")],{encoding:"utf8",timeout:5e3});let g;for(const f of l.split(`
4
+ `))f[0]==="p"?g=y.find(k=>k.pid===f.slice(1)):f[0]==="n"&&g&&(g.cwd=f.slice(1))}catch{}return y}a(Ue,"groupMembers");const Ke=a((o,d)=>o===d||o.startsWith(d+O.sep),"insideDir"),Je=a((o,d)=>Ke(o.cwd,d)||o.command.includes(d+O.sep)||o.command.endsWith(d),"inWorktree");async function re(o,d,y){const l=await je(o.dir,d),[g,f]=d.split("/"),k=o.files.find(S=>S.slug===g)?.itemFile,$=l?.path||k?.items.find(S=>S.id===f)?.worktree;if(typeof $!="string"||!$||$.split(O.sep).includes(".."))return"";let m=l?.repo;const p=(o.github||k?.repo||"").toLowerCase();if(!m&&p){const S=[o.owner?.repo,o.dir,...Object.values(y?.sites||{}).map(E=>E.owner?.repo)];for(const E of new Set(S.filter(M=>typeof M=="string"&&O.isAbsolute(M))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await I(E,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await I(E,"rev-parse","--show-toplevel");break}}catch{}}if(!l?.path&&!m||l?.path&&!O.isAbsolute($))return"";try{const S=[O.join(j.realpathSync(Gt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&O.isAbsolute(m))try{S.push(O.join(j.realpathSync(m),".claude","worktrees"))}catch{}const E=O.resolve(m||"/",$),M=j.realpathSync(E);return S.some(U=>E.startsWith(U+O.sep)&&M.startsWith(U+O.sep))&&j.statSync(M).isDirectory()?M:""}catch{return""}}a(re,"installWorktree");function ze({now:o=Date.now,answers:d=Xt,lingers:y=Xt,log:l=console.log,waiting:g=a(async()=>null,"waiting"),event:f=ve,gate:k=Ne}={}){const $=new Map,m=new Map,p=new Map,S=a(()=>[...m.values(),...p.values()],"live"),E=new Set,M=new Set,U=new Map,rt=new Map,gt=new Map,it=new Map;let H=!1,W=null,Z=null,ut=null;const ft=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if($.has(t.dir))return $.get(t.dir);const e=O.join(t.dir,"data","try.json"),r=lt(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},...Tt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},i={state:s,file:e,written:JSON.stringify(r)};$.set(t.dir,i),s.main_port&&rt.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,c])=>c.state==="running");for(const[c]of n)M.add(D(t,c));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),A(t),i}a(C,"stateOf");function A(t){const e=$.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Ut(e.file,e.state),e.written=r)}a(A,"persist");function ie(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),A(t.board)}a(ie,"recordGroup");function se(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,A(t.board))}a(se,"forgetGroup");async function ne(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,A(t);for(const s of r){if(!te(s.pgid))continue;let i=[];try{i=await Ue(s.pgid)}catch(n){l(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>Je(n,s.worktree))&&(await ee(s.pgid),l(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await jt(t,s.key,s.port))}}a(ne,"sweepGroups");async function jt(t,e,r){if(!r||!await y(r))return;const s=`Port ${r} still answers after the run was stopped.`;R(e)&&await Yt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>l(`build /${t.route} ${e}: ${i.message}`)),l(`build /${t.route} ${e}: ${s}`)}a(jt,"checkQuiet");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),_t=a((t,e)=>R(e)?t.cards[e]:t[ht[e]],"entryOf"),oe=a((t,e,r)=>{R(e)?t.cards[e]=r:t[ht[e]]=r},"setEntry"),It=a((t,e)=>{R(e)?delete t.cards[e]:t[ht[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=_t(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,R(e)&&n?{at:ft()}:{}),A(t)}a(K,"update");function $t(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=lt(O.join(r.dir,"data","try.json"),{}).main_port;Tt(s)&&rt.set(r.dir,Number(s))}}a($t,"reserveMains");function ae(t){const{board:e,key:r}=t,s=D(e,r),i=r==="main"?rt.get(e.dir):U.get(s);if(i)return i;const n=new Set([...rt.values(),...U.values()]);let c=q.first;for(;n.has(c)&&c<=q.last;)c++;if(c>q.last)throw new Error(`No try ports available (${q.first}\u2013${q.last}). Stop a served card and retry.`);return r==="main"?(rt.set(e.dir,c),C(e).state.main_port=c,A(e)):U.set(s,c),c}a(ae,"publicPort");const st=a(async t=>{const e=Te();if(!e)throw new Error("Tailscale isn't installed on this host");return dt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function At(t){try{await st(["serve",`--https=${t}`,"off"])}catch(e){l(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(At,"removePort");const Mt=a(t=>Be(lt(O.join(t.dir,"board.json"),{})),"sharedWith");async function Pt(t,e){const r=await k(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Mt(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Pt,"relayVerdict");function ce(){for(const t of S()){if(!t.shared?.size)continue;const e=Mt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(ce,"endUnshared");async function le(t){t.https=ae(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Ot.createServer(async(n,c)=>{const w=await Pt(n,t.board);if(w!=="owner"&&w!=="shared"){Fe(n,c,w);return}if(w==="shared"){const v={login:n.headers["tailscale-user-login"],res:c};i.add(v),c.on("close",()=>i.delete(v))}const T=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Qt.includes(v))),h=Ot.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:T},v=>{c.writeHead(v.statusCode,t.key===L?Le(v.headers):v.headers),v.on("data",r),v.pipe(c),v.on("close",()=>{v.complete||c.destroy()})});c.on("close",()=>{c.writableFinished||h.destroy()}),h.on("error",()=>{c.headersSent?c.destroy():c.writeHead(502).end()}),n.pipe(h)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,c,w)=>{const T=await Pt(n,t.board);if(T!=="owner"){Re(c,T==="shared"?"view-only":T);return}s(De({req:n,socket:c,head:w,port:t.port,path:n.url,replace:Qt}))}),await new Promise((n,c)=>{t.relay.once("error",c),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await st(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(le,"openPort");async function de(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!H&&!e&&await At(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||U.delete(D(t.board,t.key))}a(de,"closePort");async function ue(){const t=S().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await st(["serve","status","--json"])).stdout)}catch(r){l(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){l(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await st(["serve","--bg",`--https=${r.https}`,s])}catch(n){l(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(ue,"reserve");function nt(t=V()){if(ut)return ut;$t(t);for(const e of z(t))C(e);return ut=(async()=>{for(const e of z(t))await ne(e).catch(r=>l(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await st(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))Tt(s)&&![...m.values()].some(i=>i.https===Number(s))&&await At(s)}catch(e){l(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ut}a(nt,"initialize");async function Ct(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!R(n))return;let c="",w=!1;try{for await(const v of Lt({input:j.createReadStream(t.logFile),crlfDelay:1/0}))w||=He.test(v),v.trim()&&(c=v)}catch(v){c=`could not read the build log: ${v.message}`}const T=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(x=>x.install?.url))?"app":"web"))==="web"?"Serve":"Build",h=w?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||c;await Yt(i.dir,n,"run-failed",{kind:T,code:e??1,why:h})}a(Ct,"failure");function pt(t){return t.ending||(t.ending=ee(t.child.pid)),t.ending}a(pt,"terminate");async function ot(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await pt(t),await t.done}a(ot,"halt");async function tt(t,e=!1){await ot(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||gt.delete(D(t.board,t.key))}a(tt,"stopRun");async function Nt(t,e,r,s={}){if(!r)throw new _("This card has no valid worktree.");const i=Vt(r,t.build_target),n=xe(i);if(n)throw new _(n);if(!j.realpathSync(i).startsWith(j.realpathSync(r)+O.sep))throw new _("Build script must be inside the worktree.");const c=await I(r,"rev-parse","HEAD");if(H)throw new _("The hub is stopping. Try again after it restarts.",503);const{state:w}=C(t),T=_t(w,e),h={...R(e)?T:{},...s,sha:c,script:!0,state:"running",...R(e)?{at:ft(),head:c}:{},...e===L?{since:ft()}:{}};delete h.port,delete h.https;const v=qt(t,e);j.mkdirSync(O.dirname(v),{recursive:!0});const x=j.openSync(v,"w",384);j.fchmodSync(x,384);const b=ge(i,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});oe(w,e,h);const F=D(t,e),u={child:b,board:t,key:e,worktree:r,sha:c,at:s.at,logFile:v,visited:o(),stopped:!1,port:null,ready:!1};ie(u),m.set(F,u),gt.set(F,c);let B,P;const Y=a(N=>{P||=new Error(N),pt(u)},"fail");b.on("error",N=>{B=N}),b.stdout.on("data",N=>j.writeSync(x,N)),b.stderr.on("data",N=>j.writeSync(x,N));const J=Lt({input:b.stdout,crlfDelay:1/0});return J.on("line",N=>{const at=/^port (\d{1,5})$/.exec(N);if(!at||u.port||u.stopped)return;const G=Number(at[1]);if(G<1||G>65535){Y("Build script printed an invalid port.");return}u.port=G,u.group.port=G,A(t),clearTimeout(u.deadline);let Q,St=!1;const Ht=a(async()=>{if(!(St||u.stopped||u.ready||b.exitCode!==null||b.signalCode!==null)){St=!0;try{if(await d(G)){if(u.stopped||P||b.exitCode!==null||b.signalCode!==null||(u.opening=le(u),await u.opening,u.stopped||P||b.exitCode!==null||b.signalCode!==null))return;u.ready=!0,u.visited=o(),w.kind="web",K(t,e,{state:"ready",port:G,https:u.https}),clearInterval(u.poll),e==="main"&&await vt(t)}else o()>=Q&&Y("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{St=!1}}},"check"),Wt=a(()=>{u.stopped||(Q=o()+6e4,u.poll=setInterval(()=>Ht().catch(ct=>Y(ct.message)),250),Ht().catch(ct=>Y(ct.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===G?vt(t).then(Wt,ct=>Y(ct.message)):Wt()}),u.deadline=setTimeout(()=>Y("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{pt(u)}),u.done=new Promise(N=>b.once("close",(at,G)=>{clearTimeout(u.deadline),clearInterval(u.poll),J.close(),j.closeSync(x),(async()=>{if(await pt(u),await de(u),se(u),H||await jt(t,e,u.port),u.finished=!0,u.stopped)return;if(p.get(t.dir)===u){p.delete(t.dir),K(t,e,{serving:void 0});return}const Q=e===L&&!u.port?new Error('Build script exited without printing "port <n>".'):null;P||B||at!==0||u.port&&!u.ready||Q?await Ct(u,at??1,G,P||B||(u.port&&!u.ready?new Error("Build script exited before its port answered."):Q)):u.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):It(w,e),A(t)):(w.kind="app",K(t,e,{state:"ready"}))})().catch(Q=>l(`build /${t.route} ${e}: ${Q.message}`)).finally(()=>{m.get(F)===u&&m.delete(F),N()})})),l(`build /${t.route} ${e}: started; log ${v}`),{status:"started"}}a(Nt,"launch");async function vt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ot(e))}a(vt,"retire");async function Dt(t,e,r){const s=D(t,"main");if(E.has(s))throw new _("A build is already running for main.",409);E.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await tt(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const c=a(T=>O.join(Gt.homedir(),".claude","kickflip","try",`${t.route}-main${T}`),"worktreeOf"),w=c(n?.worktree===c("-a")?"-b":"-a");return j.mkdirSync(O.dirname(w),{recursive:!0}),mt(c(""))&&await I(e,"worktree","remove","--force",c("")),mt(w)?await I(w,"checkout","--detach","--force",r.sha):(await I(e,"worktree","prune"),await I(e,"worktree","add","--detach",w,r.sha)),await Nt(t,"main",w,r)}catch(i){throw i.status!==503&&(C(t).state.main={...r,state:"failed"},xt(t,"main",i),A(t)),i}finally{E.delete(s)}}a(Dt,"launchMain");async function bt(t,{manifest:e=V()}={}){if(await nt(e),$t(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(h=>!["route","key"].includes(h))||typeof t.route!="string"||typeof t.key!="string")throw new _("Send only the board route and card key.");const{route:r,key:s}=t;if(R(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(h=>h==="."||h==="..")))throw new _("Card key must be <file>/<id>.");const i=z(e).find(h=>h.route===r);if(!i)throw new _(`/${r} is not a board.`,404);if(R(s)&&!i.files.some(({slug:h,itemFile:v})=>v.items.some(x=>`${h}/${x.id}`===s)))throw new _("There is no item with this card key.",404);const n=await g();if(n)throw new _(n,409);const c=D(i,s),w=m.get(c);if(E.has(c)||w&&!w.ready&&!w.finished)throw new _(`A build is already running for ${s==="main"?"main":s===L?"the prototype site":"this card"}.`,409);if(s==="main"){const h=C(i).state.main;if(h?.state==="running")throw new _("A build is already running for main.",409);if(h?.state!=="failed"||!h.sha)throw new _("Main has no failed build to retry.",409);const{sha:v,branch:x,slug:b,at:F,script:u}=h;return Dt(i,et(r,e).repo,{sha:v,branch:x,slug:b,at:F,script:u,...i.build_target?{target:i.build_target}:{}})}E.add(c);const T=M.delete(c);try{m.has(c)&&await tt(m.get(c),!0);const h=s===L?Ft(i):await re(i,s,e);return await Nt(i,s,h)}catch(h){throw U.delete(c),T&&h.status!==503&&K(i,s,{state:void 0,sha:void 0}),h}finally{E.delete(c)}}a(bt,"start");const Ft=a(t=>{const e=Ae(t.route);return mt(e)&&j.statSync(e).isDirectory()?j.realpathSync(e):""},"baseWorktree");async function fe(t,e){const{state:r}=C(t),s=D(t,L),i=m.get(s),n=(await _e(t.dir)).length>0&&Ft(t);if(!n){i&&await tt(i),r.prototypes&&(r.prototypes=null,A(t));return}if(i?.ready&&!i.stopped&&!E.has(s)){const c=await I(n,"rev-parse","HEAD");if(c===r.prototypes?.sha)return;l(`try /${t.route} prototypes: the base moved to ${c.slice(0,7)}; restarting its serve`)}else if(i||E.has(s)||r.prototypes?.state==="failed"||await g())return;try{await bt({route:t.route,key:L},{manifest:e})}catch(c){if([409,503].includes(c.status))return;throw r.prototypes={state:"failed"},xt(t,L,c),A(t),c}}a(fe,"serveBase");function Rt(t,e,{manifest:r=V()}={}){const s=z(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Rt,"lookup");async function kt({manifest:t=V(),routes:e}={}){if(await nt(t),$t(t),H)return W;if(W&&e){if(!Z){const r={routes:new Set,manifest:t};r.done=W.catch(()=>{}).then(()=>(Z=null,kt({manifest:r.manifest,routes:[...r.routes]}))),Z=r}Z.manifest=t;for(const r of e)Z.routes.add(r);return Z.done}return W||(W=(async()=>{await ue();const r=z(t),s=lt(be,{}).prs||{};for(const[i,n]of[...p])r.some(c=>c.dir===i)||(p.delete(i),await ot(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||R(i.key)&&i.ready&&o()-i.visited>=We){await tt(i);const{state:n}=C(i.board);It(n,i.key),A(i.board)}for(const i of r){if(H)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),c=et(i.route,t),w=c.branch||"main",T=ke(c);for(const{slug:h,itemFile:v}of i.files)for(const x of v.items){const b=`${h}/${x.id}`,F=Me(x,i.github||v.repo).map(u=>s[Ce(u)]);if(!(x.lane==="done"||F.length&&F.every(u=>["MERGED","CLOSED"].includes(u?.state))))try{const u=D(i,b),B=await re(i,b,t);if(B){const J=await I(B,"rev-parse","HEAD");let N=0;try{N=Number(await I(B,"rev-list","--count",`${T}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:J,ahead:N,script:mt(Vt(B,i.build_target))}),A(i)}else if(!M.has(u))continue;const P=n.cards[b];if((M.has(u)||i.auto_build&&P.script&&P.ahead>0&&(P.sha||gt.get(u))!==P.head&&P.state!=="running")&&!E.has(u))try{await bt({route:i.route,key:b},{manifest:t})}catch(J){if([409,503].includes(J.status))continue;throw K(i,b,{sha:P.head}),await Ct({board:i,key:b,logFile:xt(i,b,J)},1),J}}catch(u){l(`try /${i.route} ${b}: ${u.message}`)}}await fe(i,t).catch(h=>l(`try /${i.route} prototypes: ${h.message}`));try{if(!c.repo||await g())continue;const h=await I(c.repo,"rev-parse",T);let v=!1;try{await I(c.repo,"cat-file","-e",`${h}:${zt(i.build_target)}`),v=!0}catch{}if(!v){const P=m.get(D(i,"main"));P&&await tt(P),await vt(i),n.main=null,A(i);continue}const x=n.main;if(x?.state==="running"||x?.sha===h&&(x.target||null)===(i.build_target||null)&&(x.state==="failed"||x.state==="ready"&&(n.kind!=="web"||Rt(i.route,"main",{manifest:t}))))continue;const b=D(i,"main");if(E.has(b))continue;const F=await I(c.repo,"log","-1","--format=%cI",h);let u=i.github;if(!u)try{u=Jt(await I(c.repo,"remote","get-url","origin"))}catch{}const B={sha:h,branch:w,slug:u||"",at:F,script:v,...i.build_target?{target:i.build_target}:{}};await Dt(i,c.repo,B)}catch(h){l(`try /${i.route} main: ${h.message}`)}}})().finally(()=>{W=null}),W)}a(kt,"tick");function Bt(t,e){if(it.has(t.dir))return it.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},A(t);let c,w;try{c=await(s.local?Qe:Ye)(s.repo,i)}catch(T){w=T.message}if(c===null?delete n.pull:n.pull={at:ft(),...w?{error:w}:{}},A(t),c===null)return null;if(w)return l(`pull /${t.route}: ${w}`),{commits:0,error:w};if(c.commits){l(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${c.commits} commit${c.commits===1?"":"s"} to ${c.to.slice(0,7)}`);try{await f(t,"main-pulled",{branch:i,...c})}catch(T){l(`pull /${t.route}: could not send main-pulled (${T.message})`)}}return{commits:c.commits}})().finally(()=>it.delete(t.dir));return it.set(t.dir,r),r}a(Bt,"pullMain");async function pe({manifest:t=V(),routes:e}={}){await nt(t);const r=[];for(const s of z(t)){if(H)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Bt(s,t))?.commits&&r.push(s.route)}catch(i){l(`pull /${s.route} failed: ${i.message}`)}}return r}a(pe,"pullMains");async function he(t,{manifest:e=V()}={}){if(await nt(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new _("Send only the board route.");const r=z(e).find(n=>n.route===t.route);if(!r)throw new _(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Bt(r,e);if(!i)throw new _("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&kt({manifest:e,routes:[r.route]}).catch(n=>l(`try /${r.route} main: ${n.message}`)),i}a(he,"pull");async function me(){H=!0;const t=Promise.all([...m.values()].map(e=>e.ready?tt(e):ot(e)).concat([...p.values()].map(e=>ot(e))));await Promise.all([t,W,...it.values()])}return a(me,"stop"),{start:bt,tick:kt,stop:me,lookup:Rt,initialize:nt,pull:he,pullMains:pe,endUnshared:ce}}a(ze,"createTryManager");const wt=1e4,yt=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:wt,env:Oe()})).stdout,"originGit");async function Ye(o,d,{run:y=yt}={}){const l=a(async(...p)=>(await y(...p)).trim(),"git"),g=a(p=>l(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),f=`refs/remotes/origin/${d}`,k=await g(`refs/heads/${d}`)||await g(f);try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${f}`),await l(o,"checkout","--quiet","--force","-B",d,f),await l(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${wt/1e3} s`:Et(p.stderr,{repo:o}).error}`)}const $=await g(f),m=k&&k!==$?Number(await l(o,"rev-list","--count",`${k}..${$}`)):0;return{from:k||$,to:$,commits:m}}a(Ye,"resetBranch");async function Qe(o,d,{run:y=yt}={}){const l=a(async(...f)=>(await y(...f)).trim(),"git"),g=a(f=>{throw new Error(`can\u2019t pull: ${f}`)},"refuse");try{await l(o,"config","--get","remote.origin.url")}catch{return null}try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(f){g(f.killed?`git fetch timed out after ${wt/1e3} s`:Et(f.stderr,{repo:o}).error)}try{const f=await l(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),k=await l(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),$=a((S,E)=>l(o,"merge-base","--is-ancestor",S,E).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(f===k||await $(k,f))return{from:f,to:f,commits:0};await $(f,k)||g(`${d} has split from origin`);const m=Number(await l(o,"rev-list","--count",`${f}..${k}`)),p=(await l(o,"worktree","list","--porcelain")).split(`
3
5
 
4
- `).map(v=>v.split(`
5
- `)).find(v=>v.includes(`branch refs/heads/${u}`))?.[0].slice(9);return $?(await y($,"status","--porcelain","--untracked-files=no")&&S("local changes"),await y($,"merge","--ff-only","--quiet",k)):await y(c,"update-ref",`refs/heads/${u}`,k,p),{from:p,to:k,commits:g}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;S(String(p.stderr||p.message).trim().split(`
6
- `).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=z(),log:u=console.log,routes:m}={}){const y=m?nt(Bt,{}):{};for(const S of J(c)){if(m&&!m.includes(S.route))continue;const p=et(S.route,c);if(p.local||!p.repo)continue;const k=p.branch||"main",d=new Date().toISOString(),g=Date.now();try{await vt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),y[S.route]={at:d,ok:!0}}catch($){u(`fetch /${S.route} after ${Date.now()-g} ms: ${$.message}`);let v=S.github;if(!v&&!S.remote)try{v=Ft(await j(p.repo,"remote","get-url","origin"))}catch{}y[S.route]={at:d,ok:!1,...Ht($.killed?`git fetch timed out after ${bt/1e3} s`:$.stderr,{github:v||"",remote:S.remote,repo:p.repo})};continue}if(S.remote)try{await Ae(S,p.repo,k,u)}catch($){u(`pushed branches /${S.route}: ${$.message}`)}}Rt(Bt,y)}a(tr,"fetchTryBranches");async function Ae(c,u,m,y){const S=c.files.flatMap(({slug:d,itemFile:g})=>g.items.filter($=>$.pushed?.branch&&!ve($)&&$.lane!=="done").map($=>({key:`${d}/${$.id}`,branch:$.pushed.branch})));if(!S.length)return;const p=new Map((await vt(u,"ls-remote","--heads","origin")).split(`
7
- `).map(d=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(d)).filter(Boolean).map(d=>[d[2],d[1]])),k=de(c.remote)?.host||"the remote";for(const d of S){let g=p.get(d.branch);if(!g)try{g=await j(u,"rev-parse","--verify","--quiet",`refs/heads/${d.branch}^{commit}`)}catch{}let $=!1;if(g)try{await j(u,"merge-base","--is-ancestor",g,`refs/remotes/origin/${m}`),$=!0}catch{}if(!(!$&&p.has(d.branch)))try{await ge(c.dir,d.key,{done:!0,force:!0,landed:$,closing:$?["branch-merged",{branch:d.branch,into:m,host:k}]:["branch-gone",{branch:d.branch,host:k}]})}catch(v){y(`pushed ${d.key}: ${v.message}`)}}}a(Ae,"settlePushed");const V=Ce({waiting:a(()=>fe.waiting("tailscale"),"waiting")}),er=a((...c)=>V.start(...c),"startBuild"),rr=a((...c)=>V.tick(...c),"tryTick"),sr=a((...c)=>V.pull(...c),"pullNow"),ir=a((...c)=>V.pullMains(...c),"pullMains"),nr=a(()=>V.stop(),"stopTries"),or=a(()=>V.endUnshared(),"endUnsharedTries"),ar=a((...c)=>V.initialize(...c),"initializeTries");export{x as BuildError,Y as TRY_PORTS,Ce as createTryManager,or as endUnsharedTries,tr as fetchTryBranches,ar as initializeTries,Jt as installWorktree,wt as isTryPort,Pe as pullBranch,ir as pullMains,sr as pullNow,er as startBuild,nr as stopTries,rr as tryTick};
6
+ `).map(S=>S.split(`
7
+ `)).find(S=>S.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await l(p,"status","--porcelain","--untracked-files=no")&&g("local changes"),await l(p,"merge","--ff-only","--quiet",k)):await l(o,"update-ref",`refs/heads/${d}`,k,f),{from:f,to:k,commits:m}}catch(f){if(f.message.startsWith("can\u2019t pull: "))throw f;g(String(f.stderr||f.message).trim().split(`
8
+ `).at(-1))}}a(Qe,"pullBranch");async function yr({manifest:o=V(),log:d=console.log,routes:y}={}){const l=y?lt(Kt,{}):{};for(const g of z(o))if(!(y&&!y.includes(g.route)))try{const f=et(g.route,o);if(f.local||!f.repo)continue;const k=f.branch||"main",$=new Date().toISOString(),m=Date.now();try{await yt(f.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),l[g.route]={at:$,ok:!0}}catch(p){d(`fetch /${g.route} after ${Date.now()-m} ms: ${p.message}`);let S=g.github;if(!S&&!g.remote)try{S=Jt(await I(f.repo,"remote","get-url","origin"))}catch{}l[g.route]={at:$,ok:!1,...Et(p.killed?`git fetch timed out after ${wt/1e3} s`:p.stderr,{github:S||"",remote:g.remote,repo:f.repo})};continue}if(g.remote)try{await Ve(g,f.repo,k,d)}catch(p){d(`pushed branches /${g.route}: ${p.message}`)}}catch(f){d(`fetch /${g.route} failed: ${f.message}`)}Ut(Kt,l)}a(yr,"fetchTryBranches");async function Ve(o,d,y,l){const g=o.files.flatMap(({slug:$,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!Pe(p)&&p.lane!=="done").map(p=>({key:`${$}/${p.id}`,branch:p.pushed.branch})));if(!g.length)return;const f=new Map((await yt(d,"ls-remote","--heads","origin")).split(`
9
+ `).map($=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec($)).filter(Boolean).map($=>[$[2],$[1]])),k=Se(o.remote)?.host||"the remote";for(const $ of g){let m=f.get($.branch);if(!m)try{m=await I(d,"rev-parse","--verify","--quiet",`refs/heads/${$.branch}^{commit}`)}catch{}let p=!1;if(m)try{await I(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${y}`),p=!0}catch{}if(!(!p&&f.has($.branch)))try{await Ie(o.dir,$.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:$.branch,into:y,host:k}]:["branch-gone",{branch:$.branch,host:k}]})}catch(S){l(`pushed ${$.key}: ${S.message}`)}}}a(Ve,"settlePushed");const X=ze({waiting:a(()=>Ee.waiting("tailscale"),"waiting")}),gr=a((...o)=>X.start(...o),"startBuild"),$r=a((...o)=>X.tick(...o),"tryTick"),vr=a((...o)=>X.pull(...o),"pullNow"),br=a((...o)=>X.pullMains(...o),"pullMains"),kr=a(()=>X.stop(),"stopTries"),Sr=a(()=>X.endUnshared(),"endUnsharedTries"),Or=a((...o)=>X.initialize(...o),"initializeTries");export{_ as BuildError,q as TRY_PORTS,ze as createTryManager,Sr as endUnsharedTries,yr as fetchTryBranches,Or as initializeTries,re as installWorktree,Tt as isTryPort,Qe as pullBranch,br as pullMains,vr as pullNow,Ye as resetBranch,gr as startBuild,kr as stopTries,$r as tryTick};
package/hub/views.mjs CHANGED
@@ -1 +1 @@
1
- var L=Object.defineProperty;var i=(e,t)=>L(e,"name",{value:t,configurable:!0});import{clock as p,holdLine as v,snoozeEnd as k}from"./schedule.mjs";import{CHOICES as V,FOREVER as $,durationEnd as K,endOf as F}from"./duration.mjs";import{siteIcon as M}from"./icons.mjs";import{codexStatus as H}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),E=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),f=i(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),p(o,o)},"timeOfDay"),x=i((e,t)=>e.hold?v(e.hold,t):"pushes go out after each check","deviceLine"),W=i(e=>`${f(e.quiet.start)} \u2013 ${f(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),B="checks every minute while you're here, every 5 when away";function Z(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(u=>!u.ok),l=o.filter(u=>u.state==="broken"),r=t.find(u=>u.link==="events"),a=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",d=l.length?`degraded \xB7 ${a}`:o.length?`up \xB7 ${a} \xB7 ${o.length} settling`:`up \xB7 ${a} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:d,lines:o.map(u=>({link:u.link,detail:`${u.state==="broken"?"":"settling: "}${u.detail}`}))}}i(Z,"siteHealth");const A=[{value:"always",label:"Always on",description:i(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:i(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:i(e=>`${e} implements and reviews.`,"description")}],j=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:A.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?A.find(o=>o.value===e).description(q[n]):t.words,fix:t.fix}),"codexView"),q={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},C=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],J=i(e=>({model:e,builder:q[e],choices:C.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:C.find(t=>t.value===e).description}),"claudeView");function re({now:e,st:t,sidebar:n,rows:o,links:l=new Map,code:r={},host:a="",version:d,rules:u=[],remembered:c=new Map,integrations:g=[],codexShare:m="off",claudeModel:h="auto",codex:T=H({installed:!1}),awake:P=null}){const w=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),z=O?` \xB7 runs the code in its files (${r.onDisk})`:"",R=i(s=>({route:s.route,title:s.title,icon:M(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:c.get(s.route)||[],restart:w.has(s.route)&&s.restart===!0}),"site"),U=[["Boards",o.filter(s=>w.has(s.route))],["Pages",o.filter(s=>!w.has(s.route))]].filter(([,s])=>s.length).map(([s,D])=>({label:s,sites:D.map(R)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${z}`,restart:!O},awake:!!P,groups:U,integrations:g,usage:n.usage,pause:n.pause,codex:j(m,T,h),claude:J(h),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:x(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?E(u.length,"rule"):"None yet",sites:E(o.length,"site"),host:a,version:d,total:n.total}}i(re,"hubView");const ae="That snooze ends in the past or more than 30 days on. Pick another time.",N="until you end it",_="until you resume",b=i((e,t,n)=>e===$?n:`until ${p(e,t)}`,"untilWords"),I=i((e,t)=>V.map(([n,o])=>({value:n,label:o,until:b(K(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?F(e.snoozed_until,t):null,"snoozedUntil");function le({now:e,st:t,rules:n=[],linear:o={set:!1},slack:l={set:!1},awake:r={on:!1,unsupported:null},pausedUntil:a=null}){const d=t.settings,u=d.notifications,c=Q(u,e),g=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",m=c&&`Snoozed ${b(c,e,N)}`,h=a&&`Paused ${b(a,e,_)}`;return{pause:{on:a?{title:h}:null,summary:a===$?"Paused \xB7 forever":h||"Off",choices:a?null:I(e,_)},snoozed:c?{title:m,summary:c===$?"Snoozed \xB7 forever":m}:null,device:x(t,e),on:{on:u.on,sub:u.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${g} Needs you and the badge still update.`},snooze:u.on&&!c?{choices:I(e,N)}:null,awake:{unsupported:r.unsupported,on:r.on,summary:r.unsupported||(r.on?"On":"Off")},weekends:{on:u.weekends_off,sub:`Pushes wait from Friday ${f(d.quiet.start)} until Monday ${f(d.quiet.end)}. Urgent ones still go.`},quiet:ee(d,e),rules:n,linear:G(o),slack:Y(l)}}i(le,"settingsView");const X={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:write","reactions:write","users:read"]}}},S={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(X))}`,label:"a new Slack app"};function Y(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${S.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:S,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:S,summary:"No token"}}i(Y,"slackWords");const y={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function G(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${y.label} and paste it here.`,keys:y,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${y.label}, then paste it here.`,keys:y,summary:"No key"}}i(G,"linearWords");function ee(e,t){const n=e.quiet,[o,l]=[f(n.start),f(n.end)],r=k(t,"tonight",e),a=[["Snooze tonight",`Until ${p(r,t)}${n.hold?`, then quiet hours hold pushes until ${p(k(r,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${p(k(t,"morning",e),t)}.`],["Snooze monday",`Until ${p(k(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${l}.`:"Off."],["Daily wake",`Every board wakes once at ${l}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:W(e),hint:B,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${l}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:a.map(([d,u])=>({title:d,sub:u}))}}i(ee,"quietView");function de(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}i(de,"flushView");export{B as QUIET_HINT,ae as SNOOZE_REFUSED,ue as capital,I as durationChoices,de as flushView,re as hubView,E as plural,W as quietSummary,le as settingsView,Z as siteHealth};
1
+ var L=Object.defineProperty;var i=(e,t)=>L(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as V,FOREVER as w,durationEnd as K,endOf as F}from"./duration.mjs";import{siteIcon as M}from"./icons.mjs";import{codexStatus as H}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),E=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),p=i(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),c(o,o)},"timeOfDay"),x=i((e,t)=>e.hold?v(e.hold,t):"pushes go out after each check","deviceLine"),W=i(e=>`${p(e.quiet.start)} \u2013 ${p(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),B="checks every minute while you're here, every 5 when away";function Z(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(u=>!u.ok),l=o.filter(u=>u.state==="broken"),r=t.find(u=>u.link==="events"),a=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",d=l.length?`degraded \xB7 ${a}`:o.length?`up \xB7 ${a} \xB7 ${o.length} settling`:`up \xB7 ${a} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:d,lines:o.map(u=>({link:u.link,detail:`${u.state==="broken"?"":"settling: "}${u.detail}`}))}}i(Z,"siteHealth");const A=[{value:"always",label:"Always on",description:i(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:i(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:i(e=>`${e} implements and reviews.`,"description")}],j=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:A.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?A.find(o=>o.value===e).description(q[n]):t.words,fix:t.fix}),"codexView"),q={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},C=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],J=i(e=>({model:e,builder:q[e],choices:C.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:C.find(t=>t.value===e).description}),"claudeView");function re({now:e,st:t,sidebar:n,rows:o,links:l=new Map,code:r={},host:a="",version:d,rules:u=[],remembered:g=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:T=H({installed:!1}),awake:P={on:!1,unsupported:null}}){const y=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),z=O?` \xB7 runs the code in its files (${r.onDisk})`:"",R=i(s=>({route:s.route,title:s.title,icon:M(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:g.get(s.route)||[],restart:y.has(s.route)&&s.restart===!0}),"site"),U=[["Boards",o.filter(s=>y.has(s.route))],["Pages",o.filter(s=>!y.has(s.route))]].filter(([,s])=>s.length).map(([s,D])=>({label:s,sites:D.map(R)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${z}`,restart:!O},awake:P,groups:U,integrations:f,usage:n.usage,pause:n.pause,codex:j(h,T,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:x(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?E(u.length,"rule"):"None yet",sites:E(o.length,"site"),host:a,version:d,total:n.total}}i(re,"hubView");const ae="That snooze ends in the past or more than 30 days on. Pick another time.",N="until you end it",_="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),I=i((e,t)=>V.map(([n,o])=>({value:n,label:o,until:$(K(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?F(e.snoozed_until,t):null,"snoozedUntil");function le({now:e,st:t,rules:n=[],linear:o={set:!1},slack:l={set:!1},pausedUntil:r=null}){const a=t.settings,d=a.notifications,u=Q(d,e),g=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,N)}`,h=r&&`Paused ${$(r,e,_)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:I(e,_)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:x(t,e),on:{on:d.on,sub:d.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${g} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:I(e,N)}:null,weekends:{on:d.weekends_off,sub:`Pushes wait from Friday ${p(a.quiet.start)} until Monday ${p(a.quiet.end)}. Urgent ones still go.`},quiet:ee(a,e),rules:n,linear:G(o),slack:Y(l)}}i(le,"settingsView");const X={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:write","reactions:write","users:read"]}}},b={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(X))}`,label:"a new Slack app"};function Y(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${b.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:b,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:b,summary:"No token"}}i(Y,"slackWords");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function G(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}i(G,"linearWords");function ee(e,t){const n=e.quiet,[o,l]=[p(n.start),p(n.end)],r=m(t,"tonight",e),a=[["Snooze tonight",`Until ${c(r,t)}${n.hold?`, then quiet hours hold pushes until ${c(m(r,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${c(m(t,"morning",e),t)}.`],["Snooze monday",`Until ${c(m(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${l}.`:"Off."],["Daily wake",`Every board wakes once at ${l}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:W(e),hint:B,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${l}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:a.map(([d,u])=>({title:d,sub:u}))}}i(ee,"quietView");function de(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}i(de,"flushView");export{B as QUIET_HINT,ae as SNOOZE_REFUSED,ue as capital,I as durationChoices,de as flushView,re as hubView,E as plural,W as quietSummary,le as settingsView,Z as siteHealth};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.7.1",
3
+ "version": "0.7.4",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {