@lovek4d/kickflip 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +43 -43
- package/SKILL.md +12 -10
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +87 -87
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +1 -1
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +5 -6
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +37 -59
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +2 -2
- package/board/public/app.js +2 -2
- package/board/public/dialogs.js +1 -1
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +2 -0
- package/board/public/linear-key.js +1 -1
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +2 -2
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +20 -29
- package/board/public/ui-cards.js +19 -17
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +5 -6
- package/board/public/ui-questions.js +12 -10
- package/board/public/ui-threads.js +10 -7
- package/board/public/ui.js +11 -2
- package/board/public/view-hub-settings.js +28 -31
- package/board/public/view-hub.js +7 -9
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +3 -3
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -25
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
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
|
|
2
|
-
`).map(
|
|
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};
|
package/hub/owner-checks.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var $=Object.defineProperty;var m=(r,t)=>$(r,"name",{value:t,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as g,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const E=3;async function F(r,t,u){const p=t.ownerChecks||{},i={},f=new Set;for(const e of r)for(const{slug:d,itemFile:w}of e.files)for(const n of w.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||n.lane!=="done")continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,d,n.id,c.check]);if(f.has(o))continue;f.add(o);const s=`${d}/${n.id}`;let a;try{a=await y(g(c.check),{since:c.since})}catch(k){const h=(p[o]||0)+1;if(h<E){i[o]=h;continue}try{await b(e.dir,s,c.check,k.message)}catch(l){i[o]=h,u.push(`${e.route}: could not mark ${s}'s check broken (${l.message})`)}continue}if(a.passed)try{await O(e.dir,s,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${s} on its passing check (${k.message})`)}}t.ownerChecks=i}m(F,"runOwnerChecks");export{E as BROKEN_AFTER,F as runOwnerChecks};
|
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
var se=Object.defineProperty;var a=(c,
|
|
2
|
-
`,{mode:384}),O.chmodSync(m,384),m}a($t,"failureLog");const Ie=a(c=>new Promise(f=>{const h=mt.get({host:"127.0.0.1",port:c,path:"/",timeout:1e3},m=>{m.destroy(),f(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>f(!1))}),"probe"),Ut=a((c,f)=>{try{process.kill(-c.pid,f)}catch{}},"signalGroup");async function Jt(c,f,h){const m=await we(c.dir,f),[S,p]=f.split("/"),k=c.waves.find(b=>b.slug===S)?.wave,u=m?.path||k?.items.find(b=>b.id===p)?.worktree;if(typeof u!="string"||!u||u.split(T.sep).includes(".."))return"";let y=m?.repo;const g=(c.github||k?.repo||"").toLowerCase();if(!y&&g){const b=[c.owner?.repo,c.dir,...Object.values(h?.sites||{}).map(I=>I.owner?.repo)];for(const I of new Set(b.filter(P=>typeof P=="string"&&T.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await _(I,"remote","get-url","origin"))?.[1].toLowerCase()===g){y=await _(I,"rev-parse","--show-toplevel");break}}catch{}}if(!m?.path&&!y||m?.path&&!T.isAbsolute(u))return"";try{const b=[T.join(O.realpathSync(Mt.homedir()),".claude","kickflip","worktrees")];if(typeof y=="string"&&T.isAbsolute(y))try{b.push(T.join(O.realpathSync(y),".claude","worktrees"))}catch{}const I=T.resolve(y||"/",u),P=O.realpathSync(I);return b.some(W=>I.startsWith(W+T.sep)&&P.startsWith(W+T.sep))&&O.statSync(P).isDirectory()?P:""}catch{return""}}a(Jt,"installWorktree");function Ce({now:c=Date.now,answers:f=Ie,log:h=console.log,waiting:m=a(async()=>null,"waiting"),event:S=ae,gate:p=ke}={}){const k=new Map,u=new Map,y=new Set,g=new Set,b=new Map,I=new Map,P=new Map,W=new Map;let G=!1,F=null,Q=null,ot=null;const at=a(()=>new Date(c()).toISOString(),"stamp");function M(t){if(k.has(t.dir))return k.get(t.dir);const e=T.join(t.dir,"data","try.json"),s=nt(e,{kind:null,main:null,cards:{}}),i={...yt(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)g.add(N(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),D(t),r}a(M,"stateOf");function D(t){const e=k.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Bt(e.file,e.state),e.written=s)}a(D,"persist");const N=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>H(e)?t.cards[e]:t[lt[e]],"entryOf"),zt=a((t,e,s)=>{H(e)?t.cards[e]=s:t[lt[e]]=s},"setEntry"),kt=a((t,e)=>{H(e)?delete t.cards[e]:t[lt[e]]=null},"dropEntry");function X(t,e,s){const{state:i}=M(t),r=St(i,e);if(!r)return;const n=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,H(e)&&n?{at:at()}:{}),D(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(T.join(s.dir,"data","try.json"),{}).main_port;yt(i)&&I.set(s.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:s}=t,i=N(e,s),r=s==="main"?I.get(e.dir):b.get(i);if(r)return r;const n=new Set([...I.values(),...b.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),M(e).state.main_port=o,D(e)):b.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 wt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ot(t){try{await rt(["serve",`--https=${t}`,"off"])}catch(e){h(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ot,"removePort");const Tt=a(t=>xe(nt(T.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 u.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 $={login:n.headers["tailscale-user-login"],res:o};r.add($),o.on("close",()=>r.delete($))}const x=Object.fromEntries(Object.entries(n.headers).filter(([$])=>!Gt.includes($))),d=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:x},$=>{o.writeHead($.statusCode,$.headers),$.on("data",s),$.pipe(o),$.on("close",()=>{$.complete||o.destroy()})});o.on("close",()=>{o.writableFinished||d.destroy()}),d.on("error",()=>{o.headersSent?o.destroy():o.writeHead(502).end()}),n.pipe(d)}),t.relay.on("connection",n=>{s(),i(n)}),t.relay.on("upgrade",async(n,o,w)=>{const x=await Et(n,t.board);if(x!=="owner"){Ee(o,x==="shared"?"view-only":x);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||b.delete(N(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...u.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){h(`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){h(`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){h(`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))M(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||{}))yt(i)&&![...u.values()].some(r=>r.https===Number(i))&&await Ot(i)}catch(e){h(`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}),!H(n))return;const o=[];let w=!1;try{for await(const d of Nt({input:O.createReadStream(t.logFile),crlfDelay:1/0}))w||=je.test(d),o.push(d),o.length>5&&o.shift()}catch(d){o.push(`Could not read the build log: ${d.message}`)}const x=t.port||(M(r).state.kind||(r.waves.some(({wave:d})=>d.items.some($=>$.install?.url))?"app":"web"))==="web"?"Serve":"Build";await me(r.dir,n,{text:[`${x} failed (exit ${e??1})`,i?.message,s&&`Stopped by ${s}.`,w&&"The login keychain must be unlocked on the host.",...o].filter(Boolean).join(`
|
|
3
|
-
`)})}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 jt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ct(t),await t.done}a(jt,"halt");async function Z(t,e=!1){await jt(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(N(t.board,t.key))}a(Z,"stopRun");async function _t(t,e,s,i={}){if(!s)throw new E("This card has no valid worktree.");const r=Lt(s,t.build_target),n=he(r);if(n)throw new E(n);if(!O.realpathSync(r).startsWith(O.realpathSync(s)+T.sep))throw new E("Build script must be inside the worktree.");const o=await _(s,"rev-parse","HEAD");if(G)throw new E("The hub is stopping. Try again after it restarts.",503);const{state:w}=M(t),x=St(w,e),d={...H(e)?x:{},...i,sha:o,script:!0,state:"running",...H(e)?{at:at(),head:o}:{},...e===K?{since:at()}:{}};delete d.port,delete d.https;const $=Kt(t,e);O.mkdirSync(T.dirname($),{recursive:!0});const j=O.openSync($,"w",384);O.fchmodSync(j,384);const v=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,d),D(t);const B=N(t,e),l={child:v,board:t,key:e,logFile:$,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),P.set(B,o);let R,C;const q=a(A=>{C||=new Error(A),ct(l)},"fail");v.on("error",A=>{R=A}),v.stdout.on("data",A=>O.writeSync(j,A)),v.stderr.on("data",A=>O.writeSync(j,A));const L=Nt({input:v.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 Dt=a(async()=>{if(!(pt||l.stopped||l.ready||v.exitCode!==null||v.signalCode!==null)){pt=!0;try{if(await f(U)){if(l.stopped||C||v.exitCode!==null||v.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||C||v.exitCode!==null||v.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(()=>Dt().catch(ht=>q(ht.message)),250),Dt().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),v.once("exit",()=>{ct(l)}),l.done=new Promise(A=>v.once("close",(it,U)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),O.closeSync(j),(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||R||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,U,C||R||(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),D(t)):(w.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>h(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{u.get(B)===l&&u.delete(B),A()})})),h(`build /${t.route} ${e}: started; log ${$}`),{status:"started"}}a(_t,"launch");async function It(t,e,s){const i=N(t,"main");if(y.has(i))throw new E("A build is already running for main.",409);y.add(i);try{u.has(i)&&await Z(u.get(i),!0);const r=T.join(Mt.homedir(),".claude","kickflip","try",`${t.route}-main`);return O.mkdirSync(T.dirname(r),{recursive:!0}),gt(r)?await _(r,"checkout","--detach","--force",s.sha):(await _(e,"worktree","prune"),await _(e,"worktree","add","--detach",r,s.sha)),await _t(t,"main",r,s)}catch(r){throw r.status!==503&&(M(t).state.main={...s,state:"failed"},$t(t,"main",r),D(t)),r}finally{y.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await st(e),dt(e),G)throw new E("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new E("Send only the board route and card key.");const{route:s,key:i}=t;if(H(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(d=>d==="."||d==="..")))throw new E("Card key must be <wave>/<id>.");const r=J(e).find(d=>d.route===s);if(!r)throw new E(`/${s} is not a board.`,404);if(H(i)&&!r.waves.some(({slug:d,wave:$})=>$.items.some(j=>`${d}/${j.id}`===i)))throw new E("There is no item with this card key.",404);const n=await m();if(n)throw new E(n,409);const o=N(r,i),w=u.get(o);if(y.has(o)||w&&!w.ready&&!w.finished)throw new E(`A build is already running for ${i==="main"?"main":i===K?"the prototype site":"this card"}.`,409);if(i==="main"){const d=M(r).state.main;if(d?.state==="running")throw new E("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new E("Main has no failed build to retry.",409);const{sha:$,branch:j,slug:v,at:B,script:l}=d;return It(r,et(s,e).repo,{sha:$,branch:j,slug:v,at:B,script:l,...r.build_target?{target:r.build_target}:{}})}y.add(o);const x=g.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=i===K?Ct(r):await Jt(r,i,e);return await _t(r,i,d)}catch(d){throw b.delete(o),x&&d.status!==503&&X(r,i,{state:void 0,sha:void 0}),d}finally{y.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=$e(t.route);return gt(e)&&O.statSync(e).isDirectory()?O.realpathSync(e):""},"baseWorktree");async function qt(t,e){const{state:s}=M(t),i=N(t,K),r=u.get(i),n=(await ye(t.dir)).length>0&&Ct(t);if(!n){r&&await Z(r),s.prototypes&&(s.prototypes=null,D(t));return}if(r?.ready&&!r.stopped&&!y.has(i)){const o=await _(n,"rev-parse","HEAD");if(o===s.prototypes?.sha)return;h(`try /${t.route} prototypes: the base moved to ${o.slice(0,7)}; restarting its serve`)}else if(r||y.has(i)||s.prototypes?.state==="failed"||await m())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),D(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=[...u.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 F;if(F&&e){if(!Q){const s={routes:new Set,manifest:t};s.done=F.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 F||(F=(async()=>{await Zt();const s=J(t),i=nt(ce,{}).prs||{};for(const r of[...u.values()])if(!s.some(n=>n.dir===r.board.dir)||H(r.key)&&r.ready&&c()-r.visited>=_e){await Z(r);const{state:n}=M(r.board);kt(n,r.key),D(r.board)}for(const r of s){if(G)return;if(e&&!e.includes(r.route))continue;const{state:n}=M(r),o=et(r.route,t),w=o.branch||"main",x=le(o);for(const{slug:d,wave:$}of r.waves)for(const j of $.items){const v=`${d}/${j.id}`,B=ve(j,r.github||$.repo).map(l=>i[Se(l)]);if(!(j.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=N(r,v),R=await Jt(r,v,t);if(R){const L=await _(R,"rev-parse","HEAD");let A=0;try{A=Number(await _(R,"rev-list","--count",`${x}..HEAD`))}catch{}Object.assign(n.cards[v]||={},{head:L,ahead:A,script:gt(Lt(R,r.build_target))}),D(r)}else if(!g.has(l))continue;const C=n.cards[v];if((g.has(l)||r.auto_build&&C.script&&C.ahead>0&&(C.sha||P.get(l))!==C.head&&C.state!=="running")&&!y.has(l))try{await ut({route:r.route,key:v},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,v,{sha:C.head}),await xt({board:r,key:v,logFile:$t(r,v,L)},1),L}}catch(l){h(`try /${r.route} ${v}: ${l.message}`)}}await qt(r,t).catch(d=>h(`try /${r.route} prototypes: ${d.message}`));try{if(!o.repo||await m())continue;const d=await _(o.repo,"rev-parse",x);let $=!1;try{await _(o.repo,"cat-file","-e",`${d}:${Wt(r.build_target)}`),$=!0}catch{}if(!$){const C=u.get(N(r,"main"));C&&await Z(C),n.main=null,D(r);continue}const j=n.main;if(j?.state==="running"||j?.sha===d&&(j.target||null)===(r.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Pt(r.route,"main",{manifest:t}))))continue;const v=N(r,"main");if(y.has(v))continue;const B=await _(o.repo,"log","-1","--format=%cI",d);let l=r.github;if(!l)try{l=Ht(await _(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:w,slug:l||"",at:B,script:$,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,R)}catch(d){h(`try /${r.route} main: ${d.message}`)}}})().finally(()=>{F=null}),F)}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}=M(t);n.pull={...n.pull,running:!0},D(t);let o,w;try{o=await Pe(i.repo,r)}catch(x){w=x.message}if(o===null?delete n.pull:n.pull={at:at(),...w?{error:w}:{}},D(t),o===null)return null;if(w)return h(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){h(`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(x){h(`pull /${t.route}: could not send main-pulled (${x.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 E("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 E("Send only the board route.");const s=J(e).find(n=>n.route===t.route);if(!s)throw new E(`/${t.route} is not a board.`,404);const i=et(s.route,e);if(!i.local||!i.repo)throw new E("This board fetches its branch on every poll; only a local board pulls.",409);const r=await At(s,e);if(!r)throw new E("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[s.route]}).catch(n=>h(`try /${s.route} main: ${n.message}`)),r}a(ee,"pull");async function re(){G=!0;const t=Promise.all([...u.values()].map(e=>e.ready?Z(e):jt(e)));await Promise.all([t,F,...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 vt=1e4,bt=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:vt,env:ue()})).stdout,"originGit");async function Pe(c,f,{run:h=bt}={}){const m=a(async(...p)=>(await h(...p)).trim(),"git"),S=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await m(c,"config","--get","remote.origin.url")}catch{return null}try{await m(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){S(p.killed?`git fetch timed out after ${vt/1e3} s`:Ft(p.stderr,{repo:c}).error)}try{const p=await m(c,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await m(c,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=a((b,I)=>m(c,"merge-base","--is-ancestor",b,I).then(()=>!0,P=>{if(P.code===1)return!1;throw P}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||S(`${f} has split from origin`);const y=Number(await m(c,"rev-list","--count",`${p}..${k}`)),g=(await m(c,"worktree","list","--porcelain")).split(`
|
|
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(`
|
|
4
3
|
|
|
5
|
-
`).map(
|
|
6
|
-
`)).find(
|
|
7
|
-
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=z(),log:
|
|
8
|
-
`).map(
|
|
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};
|
package/hub/views.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
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};
|
package/package.json
CHANGED
package/shared/usage-gate.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var v=Object.defineProperty;var
|
|
1
|
+
var v=Object.defineProperty;var t=(e,s)=>v(e,"name",{value:s,configurable:!0});import U from"node:fs";import w from"node:os";import H from"node:path";import{usageReader as G}from"../hub/usage.mjs";import{readSettingsFile as _,writeSettingsFile as $,readCodexShare as T,readClaudeModel as I}from"../hub/push.mjs";import{FOREVER as O,durationEnd as z,storedEnd as J}from"../hub/duration.mjs";import{codexStatus as K,codexStatusReader as L,effectiveCodexShare as V}from"./codex.mjs";import{MACHINE_NORMAL as q,readMachineState as Q}from"./machine-gate.mjs";const X=t(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),c=t(e=>Number(e),"timeOf"),y=["five_hour","seven_day"],b=t((e,s)=>e===O||typeof e=="string"&&Date.parse(e)>c(s)?e:null,"pausedUntil");function Y(e,s=new Date){if(e==="off")return null;const o=z(new Date(c(s)),e);if(!o)throw Object.assign(new Error("A pause ends after now and within 30 days: <n>m, <n>h, <n>d, <n>w, forever or a date and time; or off."),{status:400});return J(o)}t(Y,"pauseEnd");const Z=t(e=>e===O?"until the owner resumes it":`until ${e}`,"pauseUntilWords");function ee({claude:e,codex:s,pauseAt:o=90,keepGoing:n,hubPause:a,boardPause:r,machine:u=q,start:m=null,now:d=Date.now()}={}){const p=X(o)?o:90,k=t(l=>p===100?null:y.find(i=>{const f=l?.[i];return f?.state==="live"&&Number.isFinite(f.used_percentage)&&f.used_percentage>=p&&(!f.resets_at||Date.parse(f.resets_at)>c(d))})||null,"past"),D=t((l,i)=>({window:i,used:i?l[i].used_percentage:null,resets_at:i?l[i].resets_at:null}),"details"),x=k(e),A=D(e,x),C=t((l,i,f)=>l?.window===i&&Math.abs(Date.parse(l.until)-Date.parse(f))<=3e5&&c(d)<Date.parse(l.until),"covers"),j=x&&!n?.near&&C(n,x,A.resets_at),h=x?null:te(e,p,d);h&&(h.going=!!n?.near&&C(n,h.window,h.resets_at),h.waiting=!h.going&&!!m&&!m.reported&&c(d)-Date.parse(m.at)<se);const M=k(s),R={past:!!M,...D(s,M)},g=b(a,d)?"hub":b(r,d)?"board":null;return g?{pause_at:p,state:"paused",manual:g,window:null,used:null,resets_at:b(g==="hub"?a:r,d),near:null,codex:R,machine:u}:{pause_at:p,state:x?j?"going":"paused":"normal",manual:null,...A,near:h,codex:R,machine:u}}t(ee,"usageGate");const se=10*6e4,E=t(e=>2*e-100,"nearFrom");function te(e,s,o){if(s===100)return null;const n=y.find(a=>{const r=e?.[a];return r?.state==="live"&&Number.isFinite(r.used_percentage)&&r.used_percentage>=E(s)&&(!r.resets_at||Date.parse(r.resets_at)>c(o))});return n?{window:n,used:e[n].used_percentage,resets_at:e[n].resets_at,from:E(s)}:null}t(te,"nearOf");const ne=t(e=>{try{return JSON.parse(U.readFileSync(H.join(e,"board.json"),"utf8"))||{}}catch{return{}}},"readBoardJson"),ae=t((e,s=Date.now())=>b(ne(e).paused_until,s),"boardPausedUntil"),N=new Map;function W({home:e=w.homedir(),now:s=Date.now(),dir:o=null}={}){let n=N.get(e);n||(n={now:c(s)},n.usage=G({home:e,now:t(()=>n.now,"now")}),n.codex=L({home:e,now:t(()=>n.now,"now")}),N.set(e,n)),n.now=c(s);const a=_(e);return ee({...n.usage({codexShare:T(n.codex(),e)}),pauseAt:a.pauseAt,keepGoing:a.keepGoing,hubPause:a.paused_until,boardPause:o?ae(o,s):null,machine:Q(e),now:s})}t(W,"readUsageGate");function be(e,{home:s=w.homedir(),now:o=new Date}={}){const n=Y(e,o),{paused_until:a,...r}=_(s);return $(n?{...r,paused_until:n}:r,s),n}t(be,"setHubPause");function ge(e,{home:s=w.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});$({..._(s),pauseAt:Number(e)},s)}t(ge,"setPauseAt");function $e(e,{home:s=w.homedir(),now:o=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=_(s);if(e==="on"){const a=W({home:s,now:o});if(a.state==="paused")n.keepGoing={window:a.window,until:a.resets_at};else if(a.near)n.keepGoing={window:a.near.window,until:a.near.resets_at,near:!0};else throw Object.assign(new Error("Nothing is paused."),{status:409})}else delete n.keepGoing;$(n,s)}t($e,"setKeepGoing");const Se=t(e=>{try{return["github-change","check-github","card-unblocked","overnight-triage","pr-review","owner-check-broken"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function ke({home:e=w.homedir(),now:s=Date.now(),card:o={}}={}){const n=K({home:e}),a={codex:n.reason??"usable",claudeModel:I(e),size:o.size??null,cardModel:o.model??null},r=t(m=>{const{model:d,reason:p}=oe(a.claudeModel,o);return{kind:"claude",model:d,reason:`${m} ${p}`,inputs:a}},"claude");if(!n.usable)return r(n.words);if(a.codexShare=V(_(e).codexShare,n),a.codexShare==="off")return r("Codex share is off.");const u=W({home:e,now:s});return Object.assign(a,{pauseAt:u.pause_at,codexUsage:u.codex}),u.codex.past?r(`Codex is at ${Math.round(u.codex.used)}%, past the ${u.pause_at}% line until ${u.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${a.codexShare} and Codex is under the ${u.pause_at}% line.`,inputs:a}}t(ke,"builderPick");const F={opus:"Opus",sonnet:"Sonnet"};function oe(e,s){return e!=="auto"?{model:e,reason:`Claude model is ${F[e]}.`}:s.model?{model:s.model,reason:`Claude model is Auto and the card asks for Claude ${F[s.model]}.`}:s.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}t(oe,"claudePick");const P={hub:["the hub","Pause hub in Hub settings"],board:["this board","Pause board in Board settings"]},re=t(e=>e.manual?`Work is paused: the owner paused ${P[e.manual][0]} ${Z(e.resets_at)}. ${P[e.manual][1]} resumes it.`:e.state==="paused"?de(e):e.machine.state==="busy"||!e.near?.waiting?B(e):ue(e),"pauseReason"),ue=t(e=>`Work is held: Claude's ${S(e.near.window)} usage is at ${Math.round(e.near.used)}%, near the ${e.pause_at}% line, so agents start one at a time, each once the last one has reported its usage. The owner can tap Keep going on the Hub page.`,"nearReason"),B=t(e=>`Work is held: the host is busy (${e.machine.reason}). It starts again when the host eases.`,"busyReason"),De=t(e=>e.state==="paused"||e.machine.state==="busy","holdsWork"),Ae=t(e=>`usage paused ${e.resets_at} ${e.manual?`manual ${e.manual}`:`${e.window} ${e.used}%`}`,"pausedLine"),de=t(e=>`Work is paused: Claude's ${S(e.window)} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"usagePauseReason"),ie=t((e,s)=>!!e.manual||e.machine.state==="busy"||(s==="codex"?e.codex.past:e.state==="paused"),"heldFor"),Ce=t((e,s)=>ie(e,s)||s!=="codex"&&!!e.near?.waiting,"launchHeld"),Me=t((e,s)=>!!e.manual||(s==="codex"?e.codex.past:e.state==="paused"),"pausesRunning"),Re=t(e=>e.manual?`${e.manual} paused`:"usage limit","pausedWords"),S=t(e=>e==="five_hour"?"5-hour":"weekly","windowWord"),Oe=t((e,s)=>s==="codex"&&!e.manual&&e.codex.past?`Codex is paused: its ${S(e.codex.window)} usage is at ${Math.round(e.codex.used)}%, past the ${e.pause_at}% line. Resumes at ${e.codex.resets_at}.`:s==="codex"&&!e.manual?B(e):re(e),"pauseReasonFor");export{ae as boardPausedUntil,ke as builderPick,ie as heldFor,Se as heldUsageEvent,De as holdsWork,Ce as launchHeld,Y as pauseEnd,re as pauseReason,Oe as pauseReasonFor,Z as pauseUntilWords,Ae as pausedLine,Re as pausedWords,Me as pausesRunning,W as readUsageGate,be as setHubPause,$e as setKeepGoing,ge as setPauseAt,ee as usageGate,X as validPauseAt};
|