@lovek4d/kickflip 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/try.mjs CHANGED
@@ -1,12 +1,13 @@
1
- var Oe=Object.defineProperty;var a=(o,d)=>Oe(o,"name",{value:d,configurable:!0});import k from"node:fs";import T from"node:path";import Jt from"node:os";import Tt from"node:http";import{execFile as xe,spawn as Ee}from"node:child_process";import{createInterface as zt}from"node:readline";import{promisify as Te}from"node:util";import{listBoards as J,readManifest as Q,readJSON as ft,saveJSON as Yt,appendEvent as je,GITHUB_STATE as _e,REPO_FETCH as Qt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Vt,mainRefOf as Ie}from"./board-repo.mjs";import{gitReachFault as jt,remoteFromInput as Ae,remoteGitEnv as Me}from"./repos.mjs";import{readiness as Pe}from"./readiness.mjs";import{tailscaleBin as Ce}from"../shared/tailscale.mjs";import{buildScriptError as Fe,buildScriptOf as qt}from"../board/build-script.mjs";import{appendStatus as Xt,recordedWorktree as Ne,servedRoundsOpen as Be,setItem as De}from"../board/store.mjs";import{BASE_KEY as U,baseWorktreeOf as Re}from"../board/prototype-base.mjs";import{cardPRs as He,hasPR as We,prKeyOf as Ge}from"../shared/card-prs.mjs";import{hubGate as Le}from"../board/on-host.mjs";import{pipeUpgrade as Ue,refuse as Ke,refuseUpgrade as Je}from"./front-door.mjs";import{shareOf as ze}from"./share.mjs";class A extends Error{static{a(this,"BuildError")}constructor(d,h=400){super(d),this.status=h}}const rt=Te(xe),M=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),Ye=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,V={first:8500,last:8599},_t=a(o=>/^\d+$/.test(String(o))&&Number(o)>=V.first&&Number(o)<=V.last,"isTryPort"),Qe=30*6e4,Zt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Ve=["x-frame-options"],qe=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([h])=>!Ve.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),yt={main:"main",[U]:"prototypes"},H=a(o=>!Object.hasOwn(yt,o),"isCard"),te=a((o,d)=>T.join(o,qt(d)),"scriptAt"),gt=a(o=>k.existsSync(o),"exists"),ee=a((o,d)=>T.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function It(o,d,h){const c=ee(o,d);return k.mkdirSync(T.dirname(c),{recursive:!0}),k.writeFileSync(c,h.message+`
2
- `,{mode:384}),k.chmodSync(c,384),c}a(It,"failureLog");const re=a(o=>new Promise(d=>{const h=Tt.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ie=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),se=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),At=a(o=>new Promise(d=>{const h=a(()=>{clearTimeout(c),clearInterval(y),d()},"finish");ie(o,"SIGTERM");let c=setTimeout(()=>{ie(o,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const y=setInterval(()=>{se(o)||h()},20)}),"endGroup");async function Xe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
3
- `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===o).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await ne(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}a(Xe,"groupMembers");async function ne(o,d){let h="";try{h=(await rt("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(u){h=u.stdout||""}const c=new Map(o.map(u=>[u.pid,u]));let y;for(const u of h.split(`
4
- `))u[0]==="p"?y=c.get(u.slice(1)):u[0]==="n"&&y&&(y.cwd=u.slice(1));for(const u of o){if(!u.cwd&&k.existsSync("/proc"))try{u.cwd=k.readlinkSync(`/proc/${u.pid}/cwd`)}catch{}u.cwd=u.cwd.replace(/ \(deleted\)$/,"")}}a(ne,"readCwds");async function Ze(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
5
- `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await ne(h,["-d","cwd","-Fpn"]),h.filter(c=>oe(c,o))}a(Ze,"processesIn");const tr=a((o,d)=>o===d||o.startsWith(d+T.sep),"insideDir"),oe=a((o,d)=>tr(o.cwd,d)||o.command.includes(d+T.sep)||o.command.endsWith(d),"inWorktree");async function ae(o,d,h){const c=await Ne(o.dir,d),[y,u]=d.split("/"),S=o.files.find(O=>O.slug===y)?.itemFile,g=c?.path||S?.items.find(O=>O.id===u)?.worktree;if(typeof g!="string"||!g||g.split(T.sep).includes(".."))return"";let m=c?.repo;const p=(o.github||S?.repo||"").toLowerCase();if(!m&&p){const O=[o.owner?.repo,o.dir,...Object.values(h?.sites||{}).map(N=>N.owner?.repo)];for(const N of new Set(O.filter(x=>typeof x=="string"&&T.isAbsolute(x))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(N,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await M(N,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!m||c?.path&&!T.isAbsolute(g))return"";try{const O=[T.join(k.realpathSync(Jt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&T.isAbsolute(m))try{O.push(T.join(k.realpathSync(m),".claude","worktrees"))}catch{}const N=T.resolve(m||"/",g),x=k.realpathSync(N);return O.some(z=>N.startsWith(z+T.sep)&&x.startsWith(z+T.sep))&&k.statSync(x).isDirectory()?x:""}catch{return""}}a(ae,"installWorktree");function er({now:o=Date.now,answers:d=re,lingers:h=re,log:c=console.log,waiting:y=a(async()=>null,"waiting"),event:u=je,gate:S=Le}={}){const g=new Map,m=new Map,p=new Map,O=new Set,N=a(()=>[...m.values(),...p.values()],"live"),x=new Set,z=new Set,it=new Map,st=new Map,bt=new Map,nt=new Map;let W=!1,G=null,X=null,pt=null;const ht=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if(g.has(t.dir))return g.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=ft(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},..._t(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},i={state:s,file:e,written:JSON.stringify(r)};g.set(t.dir,i),s.main_port&&st.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,l])=>l.state==="running");for(const[l]of n)z.add(D(t,l));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),P(t),i}a(C,"stateOf");function P(t){const e=g.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Yt(e.file,e.state),e.written=r)}a(P,"persist");function ce(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),P(t.board)}a(ce,"recordGroup");function le(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,P(t.board))}a(le,"forgetGroup");async function de(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,P(t);for(const s of r){if(!se(s.pgid))continue;let i=[];try{i=await Xe(s.pgid)}catch(n){c(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>oe(n,s.worktree))&&(await At(s.pgid),c(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await Mt(t,s.key,s.port))}}a(de,"sweepGroups");async function Mt(t,e,r){if(!r||!await h(r))return;const s=`Port ${r} still answers after the run was stopped.`;H(e)&&await Xt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>c(`build /${t.route} ${e}: ${i.message}`)),c(`build /${t.route} ${e}: ${s}`)}a(Mt,"checkQuiet");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),Pt=a((t,e)=>H(e)?t.cards[e]:t[yt[e]],"entryOf"),ue=a((t,e,r)=>{H(e)?t.cards[e]=r:t[yt[e]]=r},"setEntry"),Ct=a((t,e)=>{H(e)?delete t.cards[e]:t[yt[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=Pt(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,H(e)&&n?{at:ht()}:{}),P(t)}a(K,"update");function kt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=ft(T.join(r.dir,"data","try.json"),{}).main_port;_t(s)&&st.set(r.dir,Number(s))}}a(kt,"reserveMains");function fe(t){const{board:e,key:r}=t,s=D(e,r),i=r==="main"?st.get(e.dir):it.get(s);if(i)return i;const n=new Set([...st.values(),...it.values()]);let l=V.first;for(;n.has(l)&&l<=V.last;)l++;if(l>V.last)throw new Error(`No try ports available (${V.first}\u2013${V.last}). Stop a served card and retry.`);return r==="main"?(st.set(e.dir,l),C(e).state.main_port=l,P(e)):it.set(s,l),l}a(fe,"publicPort");const ot=a(async t=>{const e=Ce();if(!e)throw new Error("Tailscale isn't installed on this host");return rt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Ft(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Ft,"removePort");const Nt=a(t=>ze(ft(T.join(t.dir,"board.json"),{})),"sharedWith");async function Bt(t,e){const r=await S(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Nt(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Bt,"relayVerdict");function pe(){for(const t of N()){if(!t.shared?.size)continue;const e=Nt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(pe,"endUnshared");async function he(t){t.https=fe(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Tt.createServer(async(n,l)=>{const $=await Bt(n,t.board);if($!=="owner"&&$!=="shared"){Ke(n,l,$);return}if($==="shared"){const v={login:n.headers["tailscale-user-login"],res:l};i.add(v),l.on("close",()=>i.delete(v))}const b=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Zt.includes(v))),w=Tt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:b},v=>{l.writeHead(v.statusCode,t.key===U?qe(v.headers):v.headers),v.on("data",r),v.pipe(l),v.on("close",()=>{v.complete||l.destroy()})});l.on("close",()=>{l.writableFinished||w.destroy()}),w.on("error",()=>{l.headersSent?l.destroy():l.writeHead(502).end()}),n.pipe(w)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,l,$)=>{const b=await Bt(n,t.board);if(b!=="owner"){Je(l,b==="shared"?"view-only":b);return}s(Ue({req:n,socket:l,head:$,port:t.port,path:n.url,replace:Zt}))}),await new Promise((n,l)=>{t.relay.once("error",l),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await ot(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(he,"openPort");async function me(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!W&&!e&&await Ft(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||it.delete(D(t.board,t.key))}a(me,"closePort");async function we(){const t=N().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await ot(["serve","status","--json"])).stdout)}catch(r){c(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){c(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await ot(["serve","--bg",`--https=${r.https}`,s])}catch(n){c(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(we,"reserve");function at(t=Q()){if(pt)return pt;kt(t);for(const e of J(t))C(e);return pt=(async()=>{for(const e of J(t))await de(e).catch(r=>c(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await ot(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))_t(s)&&![...m.values()].some(i=>i.https===Number(s))&&await Ft(s)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),pt}a(at,"initialize");async function Dt(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!H(n))return;let l="",$=!1;try{for await(const v of zt({input:k.createReadStream(t.logFile),crlfDelay:1/0}))$||=Ye.test(v),v.trim()&&(l=v)}catch(v){l=`could not read the build log: ${v.message}`}const b=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(j=>j.install?.url))?"app":"web"))==="web"?"Serve":"Build",w=$?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||l;await Xt(i.dir,n,"run-failed",{kind:b,code:e??1,why:w})}a(Dt,"failure");function mt(t){return t.ending||(t.ending=At(t.child.pid)),t.ending}a(mt,"terminate");async function ct(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await mt(t),await t.done}a(ct,"halt");async function Z(t,e=!1){await ct(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||bt.delete(D(t.board,t.key))}a(Z,"stopRun");async function St(t,e,r,s={},i=""){if(!r)throw new A("This card has no valid worktree.");const n=te(r,t.build_target),l=Fe(n);if(l)throw new A(l);if(!k.realpathSync(n).startsWith(k.realpathSync(r)+T.sep))throw new A("Build script must be inside the worktree.");const $=await M(r,"rev-parse","HEAD");if(W)throw new A("The hub is stopping. Try again after it restarts.",503);const{state:b}=C(t),w=Pt(b,e),v={...H(e)?w:{},...s,sha:$,script:!0,state:"running",...H(e)?{at:ht(),head:$}:{},...e===U?{since:ht()}:{}};delete v.port,delete v.https;const j=ee(t,e);k.mkdirSync(T.dirname(j),{recursive:!0});const I=k.openSync(j,"w",384);k.fchmodSync(I,384),i&&k.writeSync(I,i);const _=Ee(n,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});ue(b,e,v);const E=D(t,e),f={child:_,board:t,key:e,worktree:r,sha:$,at:s.at,metadata:s,logFile:j,visited:o(),stopped:!1,port:null,ready:!1};ce(f),m.set(E,f),bt.set(E,$);let F,tt;const B=a(R=>{tt||=new Error(R),mt(f)},"fail");_.on("error",R=>{F=R}),_.stdout.on("data",R=>k.writeSync(I,R)),_.stderr.on("data",R=>k.writeSync(I,R));const lt=zt({input:_.stdout,crlfDelay:1/0});return lt.on("line",R=>{const dt=/^port (\d{1,5})$/.exec(R);if(!dt||f.port||f.stopped)return;const L=Number(dt[1]);if(L<1||L>65535){B("Build script printed an invalid port.");return}f.port=L,f.group.port=L,P(t),clearTimeout(f.deadline);let Y,Et=!1;const Ut=a(async()=>{if(!(Et||f.stopped||f.ready||_.exitCode!==null||_.signalCode!==null)){Et=!0;try{if(await d(L)){if(f.stopped||tt||_.exitCode!==null||_.signalCode!==null||(f.opening=he(f),await f.opening,f.stopped||tt||_.exitCode!==null||_.signalCode!==null))return;f.ready=!0,f.visited=o(),b.kind="web",K(t,e,{state:"ready",port:L,https:f.https}),clearInterval(f.poll),e==="main"&&await wt(t)}else o()>=Y&&B("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{Et=!1}}},"check"),Kt=a(()=>{f.stopped||(Y=o()+6e4,f.poll=setInterval(()=>Ut().catch(ut=>B(ut.message)),250),Ut().catch(ut=>B(ut.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===L?wt(t).then(Kt,ut=>B(ut.message)):Kt()}),f.deadline=setTimeout(()=>B("No port line within 60 minutes."),60*6e4),_.once("exit",()=>{mt(f)}),f.done=new Promise(R=>_.once("close",(dt,L)=>{clearTimeout(f.deadline),clearInterval(f.poll),lt.close(),k.closeSync(I),(async()=>{if(await mt(f),await me(f),le(f),W||await Mt(t,e,f.port),f.finished=!0,f.stopped)return;if(p.get(t.dir)===f){p.delete(t.dir),K(t,e,{serving:void 0});return}if(e==="main"&&!f.ready&&ye(t,f)&&!W){await ge(f);return}const Y=e===U&&!f.port?new Error('Build script exited without printing "port <n>".'):null;tt||F||dt!==0||f.port&&!f.ready||Y?await Dt(f,dt??1,L,tt||F||(f.port&&!f.ready?new Error("Build script exited before its port answered."):Y)):f.ready?(e==="main"?(delete b.main.state,delete b.main.port,delete b.main.https):Ct(b,e),P(t)):(b.kind="app",K(t,e,{state:"ready"}))})().catch(Y=>c(`build /${t.route} ${e}: ${Y.message}`)).finally(()=>{m.get(E)===f&&m.delete(E),R()})})),c(`build /${t.route} ${e}: started; log ${j}`),{status:"started"}}a(St,"launch");async function wt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ct(e))}a(wt,"retire");function ye(t,e){const r=p.get(t.dir);return!!r&&k.readFileSync(e.logFile,"utf8").includes(r.worktree)}a(ye,"refusedByOld");async function ge(t){const{board:e,worktree:r}=t;await wt(e),c(`try /${e.route} main: the new serve refused to run beside the old one; stopped the old serve and serving again in ${r}`);const{serving:s,...i}=t.metadata;try{await St(e,"main",r,i,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
6
- `)}catch(n){Rt(e,i,n)}}a(ge,"handOverFailed");function Rt(t,e,r){r.status!==503&&(C(t).state.main={...e,state:"failed"},It(t,"main",r),P(t))}a(Rt,"mainFailed");async function $e(t,e,r){const s=`try /${t.route} main:`;if(!O.has(t.dir)){O.add(t.dir);try{const i=await Ze(r);for(const n of new Set(i.map(l=>l.pgid))){await At(n);for(const l of i.filter($=>$.pgid===n))c(`${s} stopped pid ${l.pid} (pgid ${n}) in the old main worktree ${r}`)}}catch(i){c(`${s} could not stop the processes in the old main worktree ${r}: ${i.message}`)}}if(gt(r))try{await M(e,"worktree","remove","--force",r);return}catch(i){try{k.rmSync(r,{recursive:!0,force:!0}),c(`${s} git could not remove the old main worktree ${r}; removed the folder: ${(i.stderr||i.message).trim().split(`
7
- `).at(-1)}`),await M(e,"worktree","prune")}catch(n){c(`${s} could not remove the old main worktree ${r}: ${n.message}`)}}}a($e,"removeLegacyMain");async function Ht(t,e,r){const s=D(t,"main");if(x.has(s))throw new A("A build is already running for main.",409);x.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await Z(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const l=a(b=>T.join(Jt.homedir(),".claude","kickflip","try",`${t.route}-main${b}`),"worktreeOf"),$=l(n?.worktree===l("-a")?"-b":"-a");return k.mkdirSync(T.dirname($),{recursive:!0}),await $e(t,e,l("")),gt($)?await M($,"checkout","--detach","--force",r.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",$,r.sha)),await St(t,"main",$,r)}catch(i){throw Rt(t,r,i),i}finally{x.delete(s)}}a(Ht,"launchMain");async function Ot(t,{manifest:e=Q()}={}){if(await at(e),kt(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(w=>!["route","key"].includes(w))||typeof t.route!="string"||typeof t.key!="string")throw new A("Send only the board route and card key.");const{route:r,key:s}=t;if(H(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(w=>w==="."||w==="..")))throw new A("Card key must be <file>/<id>.");const i=J(e).find(w=>w.route===r);if(!i)throw new A(`/${r} is not a board.`,404);if(H(s)&&!i.files.some(({slug:w,itemFile:v})=>v.items.some(j=>`${w}/${j.id}`===s)))throw new A("There is no item with this card key.",404);const n=await y();if(n)throw new A(n,409);const l=D(i,s),$=m.get(l);if(x.has(l)||$&&!$.ready&&!$.finished)throw new A(`A build is already running for ${s==="main"?"main":s===U?"the prototype site":"this card"}.`,409);if(s==="main"){const w=C(i).state.main;if(w?.state==="running")throw new A("A build is already running for main.",409);if(w?.state!=="failed"||!w.sha)throw new A("Main has no failed build to retry.",409);const{sha:v,branch:j,slug:I,at:_,script:E}=w;return Ht(i,et(r,e).repo,{sha:v,branch:j,slug:I,at:_,script:E,...i.build_target?{target:i.build_target}:{}})}x.add(l);const b=z.delete(l);try{m.has(l)&&await Z(m.get(l),!0);const w=s===U?Wt(i):await ae(i,s,e);return await St(i,s,w)}catch(w){throw it.delete(l),b&&w.status!==503&&K(i,s,{state:void 0,sha:void 0}),w}finally{x.delete(l)}}a(Ot,"start");const Wt=a(t=>{const e=Re(t.route);return gt(e)&&k.statSync(e).isDirectory()?k.realpathSync(e):""},"baseWorktree");async function ve(t,e){const{state:r}=C(t),s=D(t,U),i=m.get(s),n=(await Be(t.dir)).length>0&&Wt(t);if(!n){i&&await Z(i),r.prototypes&&(r.prototypes=null,P(t));return}if(i?.ready&&!i.stopped&&!x.has(s)){const l=await M(n,"rev-parse","HEAD");if(l===r.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(i||x.has(s)||r.prototypes?.state==="failed"||await y())return;try{await Ot({route:t.route,key:U},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw r.prototypes={state:"failed"},It(t,U,l),P(t),l}}a(ve,"serveBase");function Gt(t,e,{manifest:r=Q()}={}){const s=J(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Gt,"lookup");async function xt({manifest:t=Q(),routes:e}={}){if(await at(t),kt(t),W)return G;if(G&&e){if(!X){const r={routes:new Set,manifest:t};r.done=G.catch(()=>{}).then(()=>(X=null,xt({manifest:r.manifest,routes:[...r.routes]}))),X=r}X.manifest=t;for(const r of e)X.routes.add(r);return X.done}return G||(G=(async()=>{await we();const r=J(t),s=ft(_e,{}).prs||{};for(const[i,n]of[...p])r.some(l=>l.dir===i)||(p.delete(i),await ct(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||H(i.key)&&i.ready&&o()-i.visited>=Qe){await Z(i);const{state:n}=C(i.board);Ct(n,i.key),P(i.board)}for(const i of r){if(W)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),l=et(i.route,t),$=l.branch||"main",b=Ie(l);for(const{slug:w,itemFile:v}of i.files)for(const j of v.items){const I=`${w}/${j.id}`,_=He(j,i.github||v.repo).map(E=>s[Ge(E)]);if(!(j.lane==="done"||_.length&&_.every(E=>["MERGED","CLOSED"].includes(E?.state))))try{const E=D(i,I),f=await ae(i,I,t);if(f){const B=await M(f,"rev-parse","HEAD");let lt=0;try{lt=Number(await M(f,"rev-list","--count",`${b}..HEAD`))}catch{}Object.assign(n.cards[I]||={},{head:B,ahead:lt,script:gt(te(f,i.build_target))}),P(i)}else if(!z.has(E))continue;const F=n.cards[I];if((z.has(E)||i.auto_build&&F.script&&F.ahead>0&&(F.sha||bt.get(E))!==F.head&&F.state!=="running")&&!x.has(E))try{await Ot({route:i.route,key:I},{manifest:t})}catch(B){if([409,503].includes(B.status))continue;throw K(i,I,{sha:F.head}),await Dt({board:i,key:I,logFile:It(i,I,B)},1),B}}catch(E){c(`try /${i.route} ${I}: ${E.message}`)}}await ve(i,t).catch(w=>c(`try /${i.route} prototypes: ${w.message}`));try{if(!l.repo||await y())continue;const w=await M(l.repo,"rev-parse",b);let v=!1;try{await M(l.repo,"cat-file","-e",`${w}:${qt(i.build_target)}`),v=!0}catch{}if(!v){const F=m.get(D(i,"main"));F&&await Z(F),await wt(i),n.main=null,P(i);continue}const j=n.main;if(j?.state==="running"||j?.sha===w&&(j.target||null)===(i.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Gt(i.route,"main",{manifest:t}))))continue;const I=D(i,"main");if(x.has(I))continue;const _=await M(l.repo,"log","-1","--format=%cI",w);let E=i.github;if(!E)try{E=Vt(await M(l.repo,"remote","get-url","origin"))}catch{}const f={sha:w,branch:$,slug:E||"",at:_,script:v,...i.build_target?{target:i.build_target}:{}};await Ht(i,l.repo,f)}catch(w){c(`try /${i.route} main: ${w.message}`)}}})().finally(()=>{G=null}),G)}a(xt,"tick");function Lt(t,e){if(nt.has(t.dir))return nt.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},P(t);let l,$;try{l=await(s.local?ir:rr)(s.repo,i)}catch(b){$=b.message}if(l===null?delete n.pull:n.pull={at:ht(),...$?{error:$}:{}},P(t),l===null)return null;if($)return c(`pull /${t.route}: ${$}`),{commits:0,error:$};if(l.commits){c(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await u(t,"main-pulled",{branch:i,...l})}catch(b){c(`pull /${t.route}: could not send main-pulled (${b.message})`)}}return{commits:l.commits}})().finally(()=>nt.delete(t.dir));return nt.set(t.dir,r),r}a(Lt,"pullMain");async function be({manifest:t=Q(),routes:e}={}){await at(t);const r=[];for(const s of J(t)){if(W)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Lt(s,t))?.commits&&r.push(s.route)}catch(i){c(`pull /${s.route} failed: ${i.message}`)}}return r}a(be,"pullMains");async function ke(t,{manifest:e=Q()}={}){if(await at(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new A("Send only the board route.");const r=J(e).find(n=>n.route===t.route);if(!r)throw new A(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Lt(r,e);if(!i)throw new A("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&xt({manifest:e,routes:[r.route]}).catch(n=>c(`try /${r.route} main: ${n.message}`)),i}a(ke,"pull");async function Se(){W=!0;const t=Promise.all([...m.values()].map(e=>e.ready?Z(e):ct(e)).concat([...p.values()].map(e=>ct(e))));await Promise.all([t,G,...nt.values()])}return a(Se,"stop"),{start:Ot,tick:xt,stop:Se,lookup:Gt,initialize:at,pull:ke,pullMains:be,endUnshared:pe}}a(er,"createTryManager");const $t=1e4,vt=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:$t,env:Me()})).stdout,"originGit");async function rr(o,d,{run:h=vt}={}){const c=a(async(...p)=>(await h(...p)).trim(),"git"),y=a(p=>c(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),u=`refs/remotes/origin/${d}`,S=await y(`refs/heads/${d}`)||await y(u);try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${u}`),await c(o,"checkout","--quiet","--force","-B",d,u),await c(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${$t/1e3} s`:jt(p.stderr,{repo:o}).error}`)}const g=await y(u),m=S&&S!==g?Number(await c(o,"rev-list","--count",`${S}..${g}`)):0;return{from:S||g,to:g,commits:m}}a(rr,"resetBranch");async function ir(o,d,{run:h=vt}={}){const c=a(async(...u)=>(await h(...u)).trim(),"git"),y=a(u=>{throw new Error(`can\u2019t pull: ${u}`)},"refuse");try{await c(o,"config","--get","remote.origin.url")}catch{return null}try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(u){y(u.killed?`git fetch timed out after ${$t/1e3} s`:jt(u.stderr,{repo:o}).error)}try{const u=await c(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),S=await c(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=a((O,N)=>c(o,"merge-base","--is-ancestor",O,N).then(()=>!0,x=>{if(x.code===1)return!1;throw x}),"isAncestor");if(u===S||await g(S,u))return{from:u,to:u,commits:0};await g(u,S)||y(`${d} has split from origin`);const m=Number(await c(o,"rev-list","--count",`${u}..${S}`)),p=(await c(o,"worktree","list","--porcelain")).split(`
1
+ var Me=Object.defineProperty;var o=(a,d)=>Me(a,"name",{value:d,configurable:!0});import O from"node:fs";import j from"node:path";import qt from"node:os";import Ft from"node:http";import{execFile as Ie,spawn as Pe}from"node:child_process";import{createInterface as Xt}from"node:readline";import{promisify as Be}from"node:util";import{listBoards as z,readManifest as q,readJSON as ot,saveJSON as Zt,appendEvent as Fe,GITHUB_STATE as Ne,REPO_FETCH as te}from"./boards.mjs";import{boardRepo as at,githubFromOrigin as ee,mainRefOf as Ce}from"./board-repo.mjs";import{gitReachFault as Nt,remoteFromInput as Re,remoteGitEnv as De}from"./repos.mjs";import{readiness as We}from"./readiness.mjs";import{tailscaleBin as He}from"../shared/tailscale.mjs";import{buildScriptError as Ge,buildScriptOf as re}from"../board/build-script.mjs";import{appendStatus as se,recordedWorktree as Le,servedRoundsOpen as Ke,setItem as Ue,settingsOf as Je}from"../board/store.mjs";import{readMachineState as Ye}from"../shared/machine-gate.mjs";import{BASE_KEY as Q,baseWorktreeOf as ze}from"../board/prototype-base.mjs";import{cardPRs as Qe,hasPR as Ve,prKeyOf as qe}from"../shared/card-prs.mjs";import{hubGate as Xe}from"../board/on-host.mjs";import{pipeUpgrade as Ze,refuse as tr,refuseUpgrade as er}from"./front-door.mjs";import{shareOf as rr}from"./share.mjs";class T extends Error{static{o(this,"BuildError")}constructor(d,h=400,c){super(d),this.status=h,c&&(this.ask=c)}}const ct=Be(Ie),M=o(async(a,...d)=>(await ct("git",["-C",a,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),sr=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,tt={first:8500,last:8599},Ct=o(a=>/^\d+$/.test(String(a))&&Number(a)>=tt.first&&Number(a)<=tt.last,"isTryPort"),Tt=3,Rt=10*6e4,ir="The host is busy, start this serve anyway?",ie=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],nr=["x-frame-options"],or=o(a=>{const d=Object.fromEntries(Object.entries(a).filter(([h])=>!nr.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),Et={main:"main",[Q]:"prototypes"},P=o(a=>!Object.hasOwn(Et,a),"isCard"),ne=o((a,d)=>j.join(a,re(d)),"scriptAt"),yt=o(a=>O.existsSync(a),"exists"),oe=o((a,d)=>j.join(a.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function jt(a,d,h){const c=oe(a,d);return O.mkdirSync(j.dirname(c),{recursive:!0}),O.writeFileSync(c,h.message+`
2
+ `,{mode:384}),O.chmodSync(c,384),c}o(jt,"failureLog");const ae=o(a=>new Promise(d=>{const h=Ft.get({host:"127.0.0.1",port:a,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ce=o((a,d)=>{try{process.kill(-a,d)}catch{}},"signalGroup"),le=o(a=>{try{return process.kill(-a,0),!0}catch{return!1}},"groupUp"),Dt=o(a=>new Promise(d=>{const h=o(()=>{clearTimeout(c),clearInterval(w),d()},"finish");ce(a,"SIGTERM");let c=setTimeout(()=>{ce(a,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const w=setInterval(()=>{le(a)||h()},20)}),"endGroup");async function ar(a){const{stdout:d}=await ct("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
3
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===a).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await de(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}o(ar,"groupMembers");async function de(a,d){let h="";try{h=(await ct("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(f){h=f.stdout||""}const c=new Map(a.map(f=>[f.pid,f]));let w;for(const f of h.split(`
4
+ `))f[0]==="p"?w=c.get(f.slice(1)):f[0]==="n"&&w&&(w.cwd=f.slice(1));for(const f of a){if(!f.cwd&&O.existsSync("/proc"))try{f.cwd=O.readlinkSync(`/proc/${f.pid}/cwd`)}catch{}f.cwd=f.cwd.replace(/ \(deleted\)$/,"")}}o(de,"readCwds");async function cr(a){const{stdout:d}=await ct("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
5
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await de(h,["-d","cwd","-Fpn"]),h.filter(c=>ue(c,a))}o(cr,"processesIn");const lr=o((a,d)=>a===d||a.startsWith(d+j.sep),"insideDir"),ue=o((a,d)=>lr(a.cwd,d)||a.command.includes(d+j.sep)||a.command.endsWith(d),"inWorktree");async function fe(a,d,h){const c=await Le(a.dir,d),[w,f]=d.split("/"),x=a.files.find($=>$.slug===w)?.itemFile,g=c?.path||x?.items.find($=>$.id===f)?.worktree;if(typeof g!="string"||!g||g.split(j.sep).includes(".."))return"";let S=c?.repo;const p=(a.github||x?.repo||"").toLowerCase();if(!S&&p){const $=[a.owner?.repo,a.dir,...Object.values(h?.sites||{}).map(H=>H.owner?.repo)];for(const H of new Set($.filter(D=>typeof D=="string"&&j.isAbsolute(D))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(H,"remote","get-url","origin"))?.[1].toLowerCase()===p){S=await M(H,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!S||c?.path&&!j.isAbsolute(g))return"";try{const $=[j.join(O.realpathSync(qt.homedir()),".claude","kickflip","worktrees")];if(typeof S=="string"&&j.isAbsolute(S))try{$.push(j.join(O.realpathSync(S),".claude","worktrees"))}catch{}const H=j.resolve(S||"/",g),D=O.realpathSync(H);return $.some(C=>H.startsWith(C+j.sep)&&D.startsWith(C+j.sep))&&O.statSync(D).isDirectory()?D:""}catch{return""}}o(fe,"installWorktree");function dr({now:a=Date.now,answers:d=ae,lingers:h=ae,log:c=console.log,waiting:w=o(async()=>null,"waiting"),event:f=Fe,gate:x=Xe,busy:g=o(()=>Ye().state==="busy","busy")}={}){const S=new Map,p=new Map,$=new Map,H=new Set,D=o(()=>[...p.values(),...$.values()],"live"),C=new Set,lt=new Set,dt=new Map,ut=new Map,gt=new Map,et=new Map,ft=new Map;let W=!1,U=null,rt=null,$t=null;const vt=o(()=>new Date(a()).toISOString(),"stamp");function B(t){if(S.has(t.dir))return S.get(t.dir);const e=j.join(t.dir,"data","try.json"),s=ot(e,{kind:null,main:null,cards:{}}),n={...Array.isArray(s.groups)&&s.groups.length?{groups:s.groups}:{},...Ct(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:n,file:e,written:JSON.stringify(s)};if(S.set(t.dir,r),n.main_port&&ut.set(t.dir,n.main_port),n.kind==="web")for(const l of Object.values(n.cards))l.state==="ready"&&(l.state="running",delete l.port,delete l.https);const i=Object.entries(n.cards).filter(([,l])=>l.state==="running");for(const[l]of i)lt.add(R(t,l));return n.kind==="web"?(n.cards=Object.fromEntries(Object.entries(n.cards).filter(([,l])=>l.state==="running"||l.killed)),n.main&&n.main.state!=="failed"&&(delete n.main.state,delete n.main.port,delete n.main.https)):n.main?.state==="running"&&(delete n.main.state,delete n.main.sha),n.main&&delete n.main.serving,s.prototypes?.state==="failed"&&(n.prototypes=s.prototypes),A(t),r}o(B,"stateOf");function A(t){const e=S.get(t.dir),s=JSON.stringify(e.state);s!==e.written&&(Zt(e.file,e.state),e.written=s)}o(A,"persist");function pe(t){const{state:e}=B(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),A(t.board)}o(pe,"recordGroup");function he(t){const{state:e}=B(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(s=>s!==t.group),e.groups.length||delete e.groups,A(t.board))}o(he,"forgetGroup");async function me(t){const{state:e}=B(t),s=e.groups||[];delete e.groups,A(t);for(const n of s){if(!le(n.pgid))continue;let r=[];try{r=await ar(n.pgid)}catch(i){c(`try startup sweep: could not read group ${n.pgid}: ${i.message}`)}r.some(i=>ue(i,n.worktree))&&(await Dt(n.pgid),c(`stopped leftover try /${t.route} ${n.key} (pgid ${n.pgid})`),await Wt(t,n.key,n.port))}}o(me,"sweepGroups");async function Wt(t,e,s){if(!s||!await h(s))return;const n=`Port ${s} still answers after the run was stopped.`;P(e)&&await se(t.dir,e,"run-failed",{kind:"Serve",code:1,why:n}).catch(r=>c(`build /${t.route} ${e}: ${r.message}`)),c(`build /${t.route} ${e}: ${n}`)}o(Wt,"checkQuiet");const R=o((t,e)=>`${t.dir}\0${e}`,"runKey"),Ht=o((t,e)=>P(e)?t.cards[e]:t[Et[e]],"entryOf"),we=o((t,e,s)=>{P(e)?t.cards[e]=s:t[Et[e]]=s},"setEntry"),pt=o((t,e)=>{P(e)?delete t.cards[e]:t[Et[e]]=null},"dropEntry");function J(t,e,s){const{state:n}=B(t),r=Ht(n,e);if(!r)return;const i=Object.hasOwn(s,"state")&&s.state!==r.state;Object.assign(r,s,P(e)&&i?{at:vt()}:{}),A(t)}o(J,"update");function Mt(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 n=ot(j.join(s.dir,"data","try.json"),{}).main_port;Ct(n)&&ut.set(s.dir,Number(n))}}o(Mt,"reserveMains");function ye(t){const{board:e,key:s}=t,n=R(e,s),r=s==="main"?ut.get(e.dir):dt.get(n);if(r)return r;const i=new Set([...ut.values(),...dt.values()]);let l=tt.first;for(;i.has(l)&&l<=tt.last;)l++;if(l>tt.last)throw new Error(`No try ports available (${tt.first}\u2013${tt.last}). Stop a served card and retry.`);return s==="main"?(ut.set(e.dir,l),B(e).state.main_port=l,A(e)):dt.set(n,l),l}o(ye,"publicPort");const ht=o(async t=>{const e=He();if(!e)throw new Error("Tailscale isn't installed on this host");return ct(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Gt(t){try{await ht(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}o(Gt,"removePort");const Lt=o(t=>rr(ot(j.join(t.dir,"board.json"),{})),"sharedWith");async function Kt(t,e){const s=await x(t),n=t.headers["tailscale-user-login"];return s!=="other"||!n||!Lt(e).includes(n)?s:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}o(Kt,"relayVerdict");function ge(){for(const t of D()){if(!t.shared?.size)continue;const e=Lt(t.board);for(const s of t.shared)e.includes(s.login)||(t.shared.delete(s),s.res.destroy())}}o(ge,"endUnshared");async function $e(t){t.https=ye(t);const e=t.sockets=new Set,s=o(r=>{e.add(r),r.on("close",()=>e.delete(r))},"track"),n=t.shared=new Set;if(t.relay=Ft.createServer(async(r,i)=>{const l=await Kt(r,t.board);if(l!=="owner"&&l!=="shared"){tr(r,i,l);return}if(l==="shared"){const E={login:r.headers["tailscale-user-login"],res:i};n.add(E),i.on("close",()=>n.delete(E))}const m=Object.fromEntries(Object.entries(r.headers).filter(([E])=>!ie.includes(E))),k=Ft.request({host:"127.0.0.1",port:t.port,method:r.method,path:r.url,headers:m},E=>{i.writeHead(E.statusCode,t.key===Q?or(E.headers):E.headers),E.pipe(i),E.on("close",()=>{E.complete||i.destroy()})});i.on("close",()=>{i.writableFinished||k.destroy()}),k.on("error",()=>{i.headersSent?i.destroy():i.writeHead(502).end()}),r.pipe(k)}),t.relay.on("connection",s),t.relay.on("upgrade",async(r,i,l)=>{const m=await Kt(r,t.board);if(m!=="owner"){er(i,m==="shared"?"view-only":m);return}s(Ze({req:r,socket:i,head:l,port:t.port,path:r.url,replace:ie}))}),await new Promise((r,i)=>{t.relay.once("error",i),t.relay.listen(0,"127.0.0.1",r)}),!t.stopped){t.exposed=!0;try{await ht(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(r){throw new Error(`Couldn't open try port ${t.https}: ${(r.stderr||r.message).trim()}`)}}}o($e,"openPort");async function ve(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...p.values()].some(s=>s!==t&&s.exposed&&s.https===t.https);if(t.exposed&&!W&&!e&&await Gt(t.https),t.relay){for(const s of t.sockets)s.destroy();await new Promise(s=>t.relay.close(s))}t.keepPort||dt.delete(R(t.board,t.key))}o(ve,"closePort");async function be(){const t=D().filter(s=>s.ready&&s.relay&&!s.closing);if(!t.length)return;let e;try{e=JSON.parse((await ht(["serve","status","--json"])).stdout)}catch(s){c(`try ports check: ${(s.stderr||s.message).trim()}`);return}for(const s of t){if(s.stopped)continue;const n=`http://127.0.0.1:${s.relay.address().port}`,r=Object.entries(e?.Web||{}).find(([i])=>i.endsWith(`:${s.https}`))?.[1];if(r?.Handlers?.["/"]?.Proxy!==n){c(`try port ${s.https}: its serve mapping was ${r?"changed":"gone"}; serving it again`);try{await ht(["serve","--bg",`--https=${s.https}`,n])}catch(i){c(`try port ${s.https}: could not serve it again: ${(i.stderr||i.message).trim()}`)}}}}o(be,"reserve");function st(t=q()){if($t)return $t;Mt(t);for(const e of z(t))B(e);return $t=(async()=>{for(const e of z(t))await me(e).catch(s=>c(`try startup sweep /${e.route}: ${s.message}`));try{const{stdout:e}=await ht(["serve","status","--json"]),s=JSON.parse(e);for(const n of Object.keys(s?.TCP||{}))Ct(n)&&![...p.values()].some(r=>r.https===Number(n))&&await Gt(n)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),$t}o(st,"initialize");async function bt(t,e,s,n){const{board:r,key:i}=t;if(J(r,i,{state:"failed",port:void 0,https:void 0}),!P(i))return;let l="",m=!1;try{for await(const y of Xt({input:O.createReadStream(t.logFile),crlfDelay:1/0}))m||=sr.test(y),y.trim()&&(l=y)}catch(y){l=`could not read the build log: ${y.message}`}const k=t.port||(B(r).state.kind||(r.files.some(({itemFile:y})=>y.items.some(v=>v.install?.url))?"app":"web"))==="web"?"Serve":"Build",E=m?"unlock the login keychain on the host":n?.message||s&&`stopped by ${s}`||l;await se(r.dir,i,"run-failed",{kind:k,code:e??1,why:E})}o(bt,"failure");function kt(t){return t.ending||(t.ending=Dt(t.child.pid)),t.ending}o(kt,"terminate");async function mt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await kt(t),await t.done}o(mt,"halt");function ke(t){const e=(et.get(t)||[]).filter(s=>a()-s<Rt);return et.set(t,e),e.length<Tt}o(ke,"restartsLeft");function Ut(t,e){if(p.has(e)||B(t).state.kind==="app")return null;const s=Je(ot(j.join(t.dir,"board.json"),{}),t).max_serves;return[...p.values()].filter(r=>r.board.dir===t.dir&&P(r.key)&&!r.stopped&&!r.finished).length>=s?{ask:"cap",cap:s}:g()?{ask:"busy"}:null}o(Ut,"servesHeld");async function V(t,e=!1){await mt(t,e),J(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||gt.delete(R(t.board,t.key))}o(V,"stopRun");async function St(t,e,s,n={},r=""){if(!s)throw new T("This card has no valid worktree.");const i=ne(s,t.build_target),l=Ge(i);if(l)throw new T(l);if(!O.realpathSync(i).startsWith(O.realpathSync(s)+j.sep))throw new T("Build script must be inside the worktree.");const m=await M(s,"rev-parse","HEAD");if(W)throw new T("The hub is stopping. Try again after it restarts.",503);const{state:k}=B(t),E=Ht(k,e),y={...P(e)?E:{},...n,sha:m,script:!0,state:"running",...P(e)?{at:vt(),head:m}:{},...e===Q?{since:vt()}:{}};delete y.port,delete y.https,delete y.killed;const v=oe(t,e);O.mkdirSync(j.dirname(v),{recursive:!0});const _=O.openSync(v,"w",384);O.fchmodSync(_,384),r&&O.writeSync(_,r);const b=Pe(i,[e,s],{cwd:s,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});we(k,e,y);const F=R(t,e),u={child:b,board:t,key:e,worktree:s,sha:m,at:n.at,metadata:n,logFile:v,stopped:!1,port:null,ready:!1};pe(u),p.set(F,u),gt.set(F,m);let G,N;const Y=o(I=>{N||=new Error(I),kt(u)},"fail");b.on("error",I=>{G=I}),b.stdout.on("data",I=>O.writeSync(_,I)),b.stderr.on("data",I=>O.writeSync(_,I));const Bt=Xt({input:b.stdout,crlfDelay:1/0});return Bt.on("line",I=>{const K=/^port (\d{1,5})$/.exec(I);if(!K||u.port||u.stopped)return;const L=Number(K[1]);if(L<1||L>65535){Y("Build script printed an invalid port.");return}u.port=L,u.group.port=L,A(t),clearTimeout(u.deadline);let Z,it=!1;const nt=o(async()=>{if(!(it||u.stopped||u.ready||b.exitCode!==null||b.signalCode!==null)){it=!0;try{if(await d(L)){if(u.stopped||N||b.exitCode!==null||b.signalCode!==null||(u.opening=$e(u),await u.opening,u.stopped||N||b.exitCode!==null||b.signalCode!==null))return;u.ready=!0,k.kind="web",J(t,e,{state:"ready",port:L,https:u.https}),clearInterval(u.poll),e==="main"&&await Ot(t)}else a()>=Z&&Y("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{it=!1}}},"check"),xt=o(()=>{u.stopped||(Z=a()+6e4,u.poll=setInterval(()=>nt().catch(wt=>Y(wt.message)),250),nt().catch(wt=>Y(wt.message)))},"probeFrom");e==="main"&&$.get(t.dir)?.port===L?Ot(t).then(xt,wt=>Y(wt.message)):xt()}),u.deadline=setTimeout(()=>Y("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{kt(u)}),u.done=new Promise(I=>b.once("close",(K,L)=>{clearTimeout(u.deadline),clearInterval(u.poll),Bt.close(),O.closeSync(_),(async()=>{await kt(u);const Z=P(e)&&u.ready&&!u.stopped&&!W&&ke(F);if(Z&&(u.keepPort=!0),await ve(u),he(u),W||await Wt(t,e,u.port),u.finished=!0,u.stopped)return;if($.get(t.dir)===u){$.delete(t.dir),J(t,e,{serving:void 0});return}if(e==="main"&&!u.ready&&Se(t,u)&&!W){await Oe(u);return}if(Z){if(!yt(s)){pt(k,e),A(t);return}const nt=[...et.get(F)||[],a()];et.set(F,nt),c(`build /${t.route} ${e}: the serve exited on its own (${L||`code ${K}`}); serving it again (${nt.length} of ${Tt})`),await St(t,e,s,{},`The serve exited on its own; serving it again (${nt.length} of ${Tt} in ${Rt/6e4} minutes).
6
+ `).catch(xt=>bt({board:t,key:e,logFile:jt(t,e,xt)},1));return}if(P(e)&&u.ready&&!u.stopped&&!W){et.delete(F),await bt(u,K??1,L,new Error(`The serve exited on its own ${Tt+1} times within ${Rt/6e4} minutes.`));return}const it=e===Q&&!u.port?new Error('Build script exited without printing "port <n>".'):null;N||G||K!==0||u.port&&!u.ready||it?await bt(u,K??1,L,N||G||(u.port&&!u.ready?new Error("Build script exited before its port answered."):it)):u.ready?(e==="main"?(delete k.main.state,delete k.main.port,delete k.main.https):pt(k,e),A(t)):(k.kind="app",J(t,e,{state:"ready"}))})().catch(Z=>c(`build /${t.route} ${e}: ${Z.message}`)).finally(()=>{p.get(F)===u&&p.delete(F),I()})})),c(`build /${t.route} ${e}: started; log ${v}`),{status:"started"}}o(St,"launch");async function Ot(t){const e=$.get(t.dir);e&&($.delete(t.dir),J(t,"main",{serving:void 0}),await mt(e))}o(Ot,"retire");function Se(t,e){const s=$.get(t.dir);return!!s&&O.readFileSync(e.logFile,"utf8").includes(s.worktree)}o(Se,"refusedByOld");async function Oe(t){const{board:e,worktree:s}=t;await Ot(e),c(`try /${e.route} main: the new serve refused to run beside the old one; stopped the old serve and serving again in ${s}`);const{serving:n,...r}=t.metadata;try{await St(e,"main",s,r,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
7
+ `)}catch(i){Jt(e,r,i)}}o(Oe,"handOverFailed");function Jt(t,e,s){s.status!==503&&(B(t).state.main={...e,state:"failed"},jt(t,"main",s),A(t))}o(Jt,"mainFailed");async function xe(t,e,s){const n=`try /${t.route} main:`;if(!H.has(t.dir)){H.add(t.dir);try{const r=await cr(s);for(const i of new Set(r.map(l=>l.pgid))){await Dt(i);for(const l of r.filter(m=>m.pgid===i))c(`${n} stopped pid ${l.pid} (pgid ${i}) in the old main worktree ${s}`)}}catch(r){c(`${n} could not stop the processes in the old main worktree ${s}: ${r.message}`)}}if(yt(s))try{await M(e,"worktree","remove","--force",s);return}catch(r){try{O.rmSync(s,{recursive:!0,force:!0}),c(`${n} git could not remove the old main worktree ${s}; removed the folder: ${(r.stderr||r.message).trim().split(`
8
+ `).at(-1)}`),await M(e,"worktree","prune")}catch(i){c(`${n} could not remove the old main worktree ${s}: ${i.message}`)}}}o(xe,"removeLegacyMain");async function Yt(t,e,s){const n=R(t,"main");if(C.has(n))throw new T("A build is already running for main.",409);C.add(n);try{const r=p.get(n);r?.ready&&!r.ending?(p.delete(n),$.set(t.dir,r)):r&&await V(r,!0);const i=$.get(t.dir);i&&(s={...s,serving:{sha:i.sha,at:i.at,https:i.https}});const l=o(k=>j.join(qt.homedir(),".claude","kickflip","try",`${t.route}-main${k}`),"worktreeOf"),m=l(i?.worktree===l("-a")?"-b":"-a");return O.mkdirSync(j.dirname(m),{recursive:!0}),await xe(t,e,l("")),yt(m)?await M(m,"checkout","--detach","--force",s.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",m,s.sha)),await St(t,"main",m,s)}catch(r){throw Jt(t,s,r),r}finally{C.delete(n)}}o(Yt,"launchMain");async function It(t,{manifest:e=q(),auto:s=!1}={}){if(await st(e),Mt(e),W)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(v=>!["route","key","anyway"].includes(v))||typeof t.route!="string"||typeof t.key!="string"||!["undefined","boolean"].includes(typeof t.anyway))throw new T("Send only the board route, card key and anyway.");const{route:n,key:r}=t;if(P(r)&&(!/^[\w.-]+\/[\w.-]+$/.test(r)||r.split("/").some(v=>v==="."||v==="..")))throw new T("Card key must be <file>/<id>.");const i=z(e).find(v=>v.route===n);if(!i)throw new T(`/${n} is not a board.`,404);if(P(r)&&!i.files.some(({slug:v,itemFile:_})=>_.items.some(b=>`${v}/${b.id}`===r)))throw new T("There is no item with this card key.",404);const l=await w();if(l)throw new T(l,409);const m=R(i,r),k=p.get(m);if(C.has(m)||k&&!k.ready&&!k.finished)throw new T(`A build is already running for ${r==="main"?"main":r===Q?"the prototype site":"this card"}.`,409);if(r==="main"){const v=B(i).state.main;if(v?.state==="running")throw new T("A build is already running for main.",409);if(v?.state!=="failed"||!v.sha)throw new T("Main has no failed build to retry.",409);const{sha:_,branch:b,slug:F,at:u,script:G}=v;return Yt(i,at(n,e).repo,{sha:_,branch:b,slug:F,at:u,script:G,...i.build_target?{target:i.build_target}:{}})}const E=P(r)&&!s&&!lt.has(m)?Ut(i,m):null;if(E?.ask==="cap")throw new T(`This board is at its cap of ${E.cap} parallel serves. Kill another serve, or raise Maximum parallel serves in Board settings.`,409,"cap");if(E?.ask==="busy"&&!t.anyway)throw new T(ir,409,"busy");C.add(m);const y=lt.delete(m);try{p.has(m)&&await V(p.get(m),!0);const v=r===Q?zt(i):await fe(i,r,e);return await St(i,r,v)}catch(v){throw dt.delete(m),y&&v.status!==503&&J(i,r,{state:void 0,sha:void 0}),v}finally{C.delete(m)}}o(It,"start");async function Te(t,{manifest:e=q()}={}){if(await st(e),W)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(i=>!["route","key"].includes(i))||typeof t.route!="string"||typeof t.key!="string"||!P(t.key))throw new T("Send only the board route and card key.");const s=z(e).find(i=>i.route===t.route);if(!s)throw new T(`/${t.route} is not a board.`,404);const n=R(s,t.key),r=p.get(n);if(!r||r.stopped||r.finished)throw new T("This card has no serve running.",409);return await V(r),et.delete(n),gt.set(n,r.sha),J(s,t.key,{sha:r.sha,killed:!0}),c(`build /${s.route} ${t.key}: killed`),{status:"stopped"}}o(Te,"kill");const zt=o(t=>{const e=ze(t.route);return yt(e)&&O.statSync(e).isDirectory()?O.realpathSync(e):""},"baseWorktree");async function Ee(t,e){const{state:s}=B(t),n=R(t,Q),r=p.get(n),i=(await Ke(t.dir)).length>0&&zt(t);if(!i){r&&await V(r),s.prototypes&&(s.prototypes=null,A(t));return}if(r?.ready&&!r.stopped&&!C.has(n)){const l=await M(i,"rev-parse","HEAD");if(l===s.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(r||C.has(n)||s.prototypes?.state==="failed"||await w())return;try{await It({route:t.route,key:Q},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw s.prototypes={state:"failed"},jt(t,Q,l),A(t),l}}o(Ee,"serveBase");function Qt(t,e,{manifest:s=q()}={}){const n=z(s).find(i=>i.route===t);if(!n)return null;const r=[...p.values()].find(i=>i.board.dir===n.dir&&i.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&i.ready&&!i.stopped&&i.child.exitCode===null&&i.child.signalCode===null);return r?{key:r.key,port:r.port}:null}o(Qt,"lookup");async function Pt({manifest:t=q(),routes:e}={}){if(await st(t),Mt(t),W)return U;if(U&&e){if(!rt){const s={routes:new Set,manifest:t};s.done=U.catch(()=>{}).then(()=>(rt=null,Pt({manifest:s.manifest,routes:[...s.routes]}))),rt=s}rt.manifest=t;for(const s of e)rt.routes.add(s);return rt.done}return U||(U=(async()=>{await be();const s=z(t),n=ot(Ne,{}).prs||{};for(const[r,i]of[...$])s.some(l=>l.dir===r)||($.delete(r),await mt(i));for(const r of[...p.values()])if(!s.some(i=>i.dir===r.board.dir)){await V(r);const{state:i}=B(r.board);pt(i,r.key),A(r.board)}for(const r of s){if(W)return;if(e&&!e.includes(r.route))continue;const{state:i}=B(r),l=at(r.route,t),m=l.branch||"main",k=Ce(l),E=new Set;for(const{slug:y,itemFile:v}of r.files)for(const _ of v.items){const b=`${y}/${_.id}`,F=Qe(_,r.github||v.repo).map(u=>n[qe(u)]);if(!(_.lane==="done"||F.length&&F.every(u=>["MERGED","CLOSED"].includes(u?.state))))try{const u=R(r,b),G=await fe(r,b,t);if(G){E.add(b);const I=await M(G,"rev-parse","HEAD");let K=0;try{K=Number(await M(G,"rev-list","--count",`${k}..HEAD`))}catch{}Object.assign(i.cards[b]||={},{head:I,ahead:K,script:yt(ne(G,r.build_target))}),A(r)}else continue;const N=i.cards[b],Y=lt.has(u);if((Y||r.auto_build&&N.script&&N.ahead>0&&(N.sha||gt.get(u))!==N.head&&N.state!=="running")&&!C.has(u)&&(Y||!Ut(r,u)))try{await It({route:r.route,key:b},{manifest:t,auto:!0})}catch(I){if([409,503].includes(I.status))continue;throw J(r,b,{sha:N.head}),await bt({board:r,key:b,logFile:jt(r,b,I)},1),I}}catch(u){c(`try /${r.route} ${b}: ${u.message}`)}}for(const y of[...p.values()])y.board.dir!==r.dir||!P(y.key)||E.has(y.key)||C.has(R(r,y.key))||(await V(y),pt(i,y.key));for(const y of Object.keys(i.cards))!E.has(y)&&lt.delete(R(r,y))&&pt(i,y);A(r),await Ee(r,t).catch(y=>c(`try /${r.route} prototypes: ${y.message}`));try{if(!l.repo||await w())continue;const y=await M(l.repo,"rev-parse",k);let v=!1;try{await M(l.repo,"cat-file","-e",`${y}:${re(r.build_target)}`),v=!0}catch{}if(!v){const N=p.get(R(r,"main"));N&&await V(N),await Ot(r),i.main=null,A(r);continue}const _=i.main;if(_?.state==="running"||_?.sha===y&&(_.target||null)===(r.build_target||null)&&(_.state==="failed"||_.state==="ready"&&(i.kind!=="web"||Qt(r.route,"main",{manifest:t}))))continue;const b=R(r,"main");if(C.has(b))continue;const F=await M(l.repo,"log","-1","--format=%cI",y);let u=r.github;if(!u)try{u=ee(await M(l.repo,"remote","get-url","origin"))}catch{}const G={sha:y,branch:m,slug:u||"",at:F,script:v,...r.build_target?{target:r.build_target}:{}};await Yt(r,l.repo,G)}catch(y){c(`try /${r.route} main: ${y.message}`)}}})().finally(()=>{U=null}),U)}o(Pt,"tick");function Vt(t,e){if(ft.has(t.dir))return ft.get(t.dir);const s=(async()=>{const n=at(t.route,e),r=n.branch||"main",{state:i}=B(t);i.pull={...i.pull,running:!0},A(t);let l,m;try{l=await(n.local?fr:ur)(n.repo,r)}catch(k){m=k.message}if(l===null?delete i.pull:i.pull={at:vt(),...m?{error:m}:{}},A(t),l===null)return null;if(m)return c(`pull /${t.route}: ${m}`),{commits:0,error:m};if(l.commits){c(`pull /${t.route}: ${r} ${n.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await f(t,"main-pulled",{branch:r,...l})}catch(k){c(`pull /${t.route}: could not send main-pulled (${k.message})`)}}return{commits:l.commits}})().finally(()=>ft.delete(t.dir));return ft.set(t.dir,s),s}o(Vt,"pullMain");async function je({manifest:t=q(),routes:e}={}){await st(t);const s=[];for(const n of z(t)){if(W)break;if(!(e&&!e.includes(n.route)))try{if(!at(n.route,t).repo)continue;(await Vt(n,t))?.commits&&s.push(n.route)}catch(r){c(`pull /${n.route} failed: ${r.message}`)}}return s}o(je,"pullMains");async function _e(t,{manifest:e=q()}={}){if(await st(e),W)throw new T("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(i=>i!=="route")||typeof t.route!="string")throw new T("Send only the board route.");const s=z(e).find(i=>i.route===t.route);if(!s)throw new T(`/${t.route} is not a board.`,404);const n=at(s.route,e),r=await Vt(s,e);if(!r)throw new T("This board\u2019s folder has no origin to pull from.",409);return(r.commits||!n.local)&&Pt({manifest:e,routes:[s.route]}).catch(i=>c(`try /${s.route} main: ${i.message}`)),r}o(_e,"pull");async function Ae(){W=!0;const t=Promise.all([...p.values()].map(e=>e.ready&&!P(e.key)?V(e):mt(e)).concat([...$.values()].map(e=>mt(e))));await Promise.all([t,U,...ft.values()])}return o(Ae,"stop"),{start:It,kill:Te,tick:Pt,stop:Ae,lookup:Qt,initialize:st,pull:_e,pullMains:je,endUnshared:ge}}o(dr,"createTryManager");const _t=1e4,At=o(async(a,...d)=>(await ct("git",["-C",a,...d],{encoding:"utf8",timeout:_t,env:De()})).stdout,"originGit");async function ur(a,d,{run:h=At}={}){const c=o(async(...p)=>(await h(...p)).trim(),"git"),w=o(p=>c(a,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),f=`refs/remotes/origin/${d}`,x=await w(`refs/heads/${d}`)||await w(f);try{await c(a,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${f}`),await c(a,"checkout","--quiet","--force","-B",d,f),await c(a,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${_t/1e3} s`:Nt(p.stderr,{repo:a}).error}`)}const g=await w(f),S=x&&x!==g?Number(await c(a,"rev-list","--count",`${x}..${g}`)):0;return{from:x||g,to:g,commits:S}}o(ur,"resetBranch");async function fr(a,d,{run:h=At}={}){const c=o(async(...f)=>(await h(...f)).trim(),"git"),w=o(f=>{throw new Error(`can\u2019t pull: ${f}`)},"refuse");try{await c(a,"config","--get","remote.origin.url")}catch{return null}try{await c(a,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(f){w(f.killed?`git fetch timed out after ${_t/1e3} s`:Nt(f.stderr,{repo:a}).error)}try{const f=await c(a,"rev-parse","--verify",`refs/heads/${d}^{commit}`),x=await c(a,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=o(($,H)=>c(a,"merge-base","--is-ancestor",$,H).then(()=>!0,D=>{if(D.code===1)return!1;throw D}),"isAncestor");if(f===x||await g(x,f))return{from:f,to:f,commits:0};await g(f,x)||w(`${d} has split from origin`);const S=Number(await c(a,"rev-list","--count",`${f}..${x}`)),p=(await c(a,"worktree","list","--porcelain")).split(`
8
9
 
9
- `).map(O=>O.split(`
10
- `)).find(O=>O.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&y("local changes"),await c(p,"merge","--ff-only","--quiet",S)):await c(o,"update-ref",`refs/heads/${d}`,S,u),{from:u,to:S,commits:m}}catch(u){if(u.message.startsWith("can\u2019t pull: "))throw u;y(String(u.stderr||u.message).trim().split(`
11
- `).at(-1))}}a(ir,"pullBranch");async function xr({manifest:o=Q(),log:d=console.log,routes:h}={}){const c=h?ft(Qt,{}):{};for(const y of J(o))if(!(h&&!h.includes(y.route)))try{const u=et(y.route,o);if(u.local||!u.repo)continue;const S=u.branch||"main",g=new Date().toISOString(),m=Date.now();try{await vt(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${S}:refs/remotes/origin/${S}`),c[y.route]={at:g,ok:!0}}catch(p){d(`fetch /${y.route} after ${Date.now()-m} ms: ${p.message}`);let O=y.github;if(!O&&!y.remote)try{O=Vt(await M(u.repo,"remote","get-url","origin"))}catch{}c[y.route]={at:g,ok:!1,...jt(p.killed?`git fetch timed out after ${$t/1e3} s`:p.stderr,{github:O||"",remote:y.remote,repo:u.repo})};continue}if(y.remote)try{await sr(y,u.repo,S,d)}catch(p){d(`pushed branches /${y.route}: ${p.message}`)}}catch(u){d(`fetch /${y.route} failed: ${u.message}`)}Yt(Qt,c)}a(xr,"fetchTryBranches");async function sr(o,d,h,c){const y=o.files.flatMap(({slug:g,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!We(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!y.length)return;const u=new Map((await vt(d,"ls-remote","--heads","origin")).split(`
12
- `).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),S=Ae(o.remote)?.host||"the remote";for(const g of y){let m=u.get(g.branch);if(!m)try{m=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(m)try{await M(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&u.has(g.branch)))try{await De(o.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:S}]:["branch-gone",{branch:g.branch,host:S}]})}catch(O){c(`pushed ${g.key}: ${O.message}`)}}}a(sr,"settlePushed");const q=er({waiting:a(()=>Pe.waiting("tailscale"),"waiting")}),Er=a((...o)=>q.start(...o),"startBuild"),Tr=a((...o)=>q.tick(...o),"tryTick"),jr=a((...o)=>q.pull(...o),"pullNow"),_r=a((...o)=>q.pullMains(...o),"pullMains"),Ir=a(()=>q.stop(),"stopTries"),Ar=a(()=>q.endUnshared(),"endUnsharedTries"),Mr=a((...o)=>q.initialize(...o),"initializeTries");export{A as BuildError,V as TRY_PORTS,er as createTryManager,Ar as endUnsharedTries,xr as fetchTryBranches,Mr as initializeTries,ae as installWorktree,_t as isTryPort,ir as pullBranch,_r as pullMains,jr as pullNow,rr as resetBranch,Er as startBuild,Ir as stopTries,Tr as tryTick};
10
+ `).map($=>$.split(`
11
+ `)).find($=>$.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&w("local changes"),await c(p,"merge","--ff-only","--quiet",x)):await c(a,"update-ref",`refs/heads/${d}`,x,f),{from:f,to:x,commits:S}}catch(f){if(f.message.startsWith("can\u2019t pull: "))throw f;w(String(f.stderr||f.message).trim().split(`
12
+ `).at(-1))}}o(fr,"pullBranch");async function Fr({manifest:a=q(),log:d=console.log,routes:h}={}){const c=h?ot(te,{}):{};for(const w of z(a))if(!(h&&!h.includes(w.route)))try{const f=at(w.route,a);if(f.local||!f.repo)continue;const x=f.branch||"main",g=new Date().toISOString(),S=Date.now();try{await At(f.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${x}:refs/remotes/origin/${x}`),c[w.route]={at:g,ok:!0}}catch(p){d(`fetch /${w.route} after ${Date.now()-S} ms: ${p.message}`);let $=w.github;if(!$&&!w.remote)try{$=ee(await M(f.repo,"remote","get-url","origin"))}catch{}c[w.route]={at:g,ok:!1,...Nt(p.killed?`git fetch timed out after ${_t/1e3} s`:p.stderr,{github:$||"",remote:w.remote,repo:f.repo})};continue}if(w.remote)try{await pr(w,f.repo,x,d)}catch(p){d(`pushed branches /${w.route}: ${p.message}`)}}catch(f){d(`fetch /${w.route} failed: ${f.message}`)}Zt(te,c)}o(Fr,"fetchTryBranches");async function pr(a,d,h,c){const w=a.files.flatMap(({slug:g,itemFile:S})=>S.items.filter(p=>p.pushed?.branch&&!Ve(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!w.length)return;const f=new Map((await At(d,"ls-remote","--heads","origin")).split(`
13
+ `).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),x=Re(a.remote)?.host||"the remote";for(const g of w){let S=f.get(g.branch);if(!S)try{S=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(S)try{await M(d,"merge-base","--is-ancestor",S,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&f.has(g.branch)))try{await Ue(a.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:x}]:["branch-gone",{branch:g.branch,host:x}]})}catch($){c(`pushed ${g.key}: ${$.message}`)}}}o(pr,"settlePushed");const X=dr({waiting:o(()=>We.waiting("tailscale"),"waiting")}),Nr=o((...a)=>X.start(...a),"startBuild"),Cr=o((...a)=>X.kill(...a),"killBuild"),Rr=o((...a)=>X.tick(...a),"tryTick"),Dr=o((...a)=>X.pull(...a),"pullNow"),Wr=o((...a)=>X.pullMains(...a),"pullMains"),Hr=o(()=>X.stop(),"stopTries"),Gr=o(()=>X.endUnshared(),"endUnsharedTries"),Lr=o((...a)=>X.initialize(...a),"initializeTries");export{ir as BUSY_ASK,T as BuildError,tt as TRY_PORTS,dr as createTryManager,Gr as endUnsharedTries,Fr as fetchTryBranches,Lr as initializeTries,fe as installWorktree,Ct as isTryPort,Cr as killBuild,fr as pullBranch,Wr as pullMains,Dr as pullNow,ur as resetBranch,Nr as startBuild,Hr as stopTries,Rr as tryTick};
package/hub/upgrade.mjs CHANGED
@@ -1 +1 @@
1
- var y=Object.defineProperty;var e=(n,a)=>y(n,"name",{value:a,configurable:!0});import p from"node:fs";import u from"node:path";const S="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",v=60*6e4,m=e(n=>String(n).split("-")[0].split(".").map(Number),"parts");function g(n,a){const[o,i]=[m(n),m(a)];for(let r=0;r<3;r++)if((o[r]||0)!==(i[r]||0))return(o[r]||0)>(i[r]||0);return!1}e(g,"newer");function _({root:n,fetch:a=globalThis.fetch,run:o,log:i=e(()=>{},"log"),pulled:r=e(()=>{},"pulled")}){const d=p.existsSync(u.join(n,".git")),l=e(()=>JSON.parse(p.readFileSync(u.join(n,"package.json"),"utf8")).version,"installed"),k=l();let c=!1;async function h(){if(d)return{state:"clone"};let s;try{const t=await a(S,{signal:AbortSignal.timeout(15e3)});if(!t.ok)throw new Error(`npm answered ${t.status}`);s=(await t.json()).version}catch(t){return i(`kickflip update check failed: ${t.message}`),{state:"failed",error:t.message}}return g(s,l())?(w(s),{state:"pulling",latest:s}):{state:"same",latest:s}}e(h,"check");function w(s){c||(c=!0,o().then(({ok:t})=>{if(!t)return;const f=l();g(f,k)?r():i(`update left v${f}; npm has ${s}`)},t=>i(`kickflip update failed: ${t.message}`)).finally(()=>{c=!1}))}return e(w,"pull"),{check:h}}e(_,"upgrader");export{v as CHECK_MS,S as LATEST_URL,g as newer,_ as upgrader};
1
+ var L=Object.defineProperty;var e=(i,s)=>L(i,"name",{value:s,configurable:!0});import w from"node:fs";import y from"node:path";const v="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",E=15*6e4,g=15*6e4,M=10*6e4,C=60*6e4,A=e(i=>String(i).split("-")[0].split(".").map(Number),"parts");function h(i,s){const[u,r]=[A(i),A(s)];for(let l=0;l<3;l++)if((u[l]||0)!==(r[l]||0))return(u[l]||0)>(r[l]||0);return!1}e(h,"newer");function D({root:i,fetch:s=globalThis.fetch,run:u,log:r=e(()=>{},"log"),pulled:l=e(()=>{},"pulled"),now:f=Date.now,awayFor:_=e(()=>g,"awayFor")}){const T=w.existsSync(y.join(i,".git")),a=e(()=>JSON.parse(w.readFileSync(y.join(i,"package.json"),"utf8")).version,"installed"),b=a();let p=!1,k=null,o=null,m={line:null,at:0};function j(){if(k===null||f()-k>=E)return S({hold:!0});if(o&&h(o,a())&&_()>=g)return x(o)}e(j,"tick");async function S({hold:d=!1}={}){if(T)return{state:"clone"};k=f();let t;try{const n=await s(v,{signal:AbortSignal.timeout(15e3)});if(!n.ok)throw new Error(`npm answered ${n.status}`);t=(await n.json()).version}catch(n){return r(`kickflip update check failed: ${n.message}`),{state:"failed",error:n.message}}const c=`kickflip update check: npm ${t}, running ${a()}`;return(c!==m.line||f()-m.at>=C)&&(r(c),m={line:c,at:f()}),h(t,a())?d&&_()<g?(o=t,{state:"waiting",latest:t}):(x(t),{state:"pulling",latest:t}):(o=null,{state:"same",latest:t})}e(S,"check");function x(d){if(p)return;p=!0;const t=new AbortController,c=setTimeout(()=>{r("kickflip update killed after 10 minutes"),t.abort()},M).unref();u({signal:t.signal}).then(({ok:n})=>{if(!n)return;const $=a();h($,b)?l():r(`kickflip update left v${$}; npm has ${d}`)},n=>r(`kickflip update failed: ${n.message}`)).finally(()=>{clearTimeout(c),p=!1})}return e(x,"pull"),{check:S,tick:j}}e(D,"upgrader");export{g as AWAY_MS,E as CHECK_MS,v as LATEST_URL,M as PULL_MS,h as newer,D as upgrader};
package/hub/views.mjs CHANGED
@@ -1 +1 @@
1
- var D=Object.defineProperty;var i=(e,t)=>D(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as L,FOREVER as w,durationEnd as V,endOf as K}from"./duration.mjs";import{siteIcon as F}from"./icons.mjs";import{codexStatus as M}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),H=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"),E=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 x=[{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:x.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?x.find(o=>o.value===e).description(A[n]):t.words,fix:t.fix}),"codexView"),A={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},q=[{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:A[e],choices:q.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:q.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:y=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:I=M({installed:!1}),awake:T={on:!1,unsupported:null}}){const g=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),P=O?` \xB7 runs the code in its files (${r.onDisk})`:"",z=i(s=>({route:s.route,title:s.title,icon:F(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:y.get(s.route)||[],restart:g.has(s.route)&&s.restart===!0}),"site"),R=[["Boards",o.filter(s=>g.has(s.route))],["Pages",o.filter(s=>!g.has(s.route))]].filter(([,s])=>s.length).map(([s,U])=>({label:s,sites:U.map(z)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${P}`,restart:!O},awake:T,groups:R,integrations:f,usage:n.usage,pause:n.pause,codex:j(h,I,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:E(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?H(u.length,"rule"):"None yet",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.",C="until you end it",N="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),_=i((e,t)=>L.map(([n,o])=>({value:n,label:o,until:$(V(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?K(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),y=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,C)}`,h=r&&`Paused ${$(r,e,N)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:_(e,N)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:E(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.${y} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:_(e,C)}: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:history","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,_ as durationChoices,de as flushView,re as hubView,H as plural,W as quietSummary,le as settingsView,Z as siteHealth};
1
+ var D=Object.defineProperty;var i=(e,t)=>D(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as L,FOREVER as w,durationEnd as V,endOf as K}from"./duration.mjs";import{siteIcon as F}from"./icons.mjs";import{codexStatus as M}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),H=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"),E=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 j(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(j,"siteHealth");const x=[{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")}],Z=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:x.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?x.find(o=>o.value===e).description(A[n]):t.words,fix:t.fix}),"codexView"),A={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},q=[{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:A[e],choices:q.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:q.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:y=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:I=M({installed:!1}),awake:T={on:!1,unsupported:null}}){const g=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),P=O?` \xB7 runs the code in its files (${r.onDisk})`:"",z=i(s=>({route:s.route,title:s.title,icon:F(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...j(s,l.get(s.route)),remembered:y.get(s.route)||[],restart:g.has(s.route)&&s.restart===!0}),"site"),R=[["Boards",o.filter(s=>g.has(s.route))],["Pages",o.filter(s=>!g.has(s.route))]].filter(([,s])=>s.length).map(([s,U])=>({label:s,sites:U.map(z)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${P}`,restart:!O},awake:T,groups:R,integrations:f,usage:n.usage,pause:n.pause,codex:Z(h,I,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:E(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?H(u.length,"rule"):"None yet",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.",C="until you end it",N="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),_=i((e,t)=>L.map(([n,o])=>({value:n,label:o,until:$(V(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?K(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),y=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,C)}`,h=r&&`Paused ${$(r,e,N)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:_(e,N)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:E(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.${y} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:_(e,C)}: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","emoji:read","im:history","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,_ as durationChoices,de as flushView,re as hubView,H as plural,W as quietSummary,le as settingsView,j as siteHealth};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {