@lovek4d/kickflip 0.6.1 → 0.7.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/CONTEXT.md +29 -17
- package/SKILL.md +12 -8
- package/bin/event-lines.mjs +1 -0
- package/bin/kickflip.mjs +7 -7
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/bin/board.mjs +46 -32
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -9
- package/board/onboarding.mjs +1 -1
- package/board/prototype-page.mjs +1 -1
- package/board/public/app-arch.js +2 -2
- package/board/public/app-banner.js +1 -1
- package/board/public/app-cards.js +13 -4
- package/board/public/app-lanes.js +2 -2
- package/board/public/app-needs.js +107 -26
- package/board/public/app-reader.js +52 -42
- package/board/public/app-threads.js +2 -2
- package/board/public/app.html +1 -1
- package/board/public/app.js +22 -12
- package/board/public/progress-review.js +4 -4
- package/board/public/prototype-kit.js +25 -11
- package/board/public/prototype-switcher.js +47 -32
- package/board/public/prototype.html +7 -4
- package/board/public/router.js +34 -6
- package/board/public/shell.js +19 -13
- package/board/public/sidebar.css +2 -2
- package/board/public/style.css +58 -11
- package/board/public/ui-cards.js +44 -27
- package/board/public/ui-enums.js +10 -1
- package/board/public/ui-links.js +1 -1
- package/board/public/ui-order.js +8 -5
- package/board/public/ui-progress-bar.js +6 -7
- package/board/public/ui-questions.js +18 -3
- package/board/public/ui-sidebar.js +3 -2
- package/board/public/ui-threads.js +10 -12
- package/board/public/ui.js +4 -2
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -2
- package/board/public/view-settings.js +60 -28
- package/board/server.mjs +19 -20
- package/board/stop.mjs +1 -1
- package/board/store.mjs +25 -25
- package/board/tickets.mjs +1 -1
- package/board/transcript.mjs +4 -2
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +6 -6
- package/hub/try.mjs +6 -6
- package/hub/upgrade.mjs +1 -1
- package/package.json +1 -1
- package/shared/tailscale.mjs +1 -1
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const u=g(e,t);if(!u)return null;if(e.source==="internal")return{id:`${x(e)}-${u}`,url:`#
|
|
1
|
+
var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const u=g(e,t);if(!u)return null;if(e.source==="internal")return{id:`${x(e)}-${u}`,url:`#selectedCard=${encodeURIComponent(r)}`,source:"internal"};if(e.source==="github")return n?{id:`#${u}`,url:`https://github.com/${n}/issues/${u}`,source:"github"}:null;const l=`${e.team}-${u}`;return{id:l,url:`https://linear.app/${e.workspace}/issue/${l}`,source:"linear"}}o(C,"ticketBadge");async function j(e,t){if(!e.account)return t;const n=await t("gh",["auth","token","-u",e.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${e.account} account; run gh auth login`);const r={...process.env,GH_TOKEN:n.stdout.trim()};return(u,l)=>t(u,l,{env:r})}o(j,"issueRunner");const T=o(async e=>{try{return await e()}catch(t){i(t.message)}},"linearCall");async function E(e,{ticket:t,title:n,body:r="",repo:u,next:l},{run:a=$,linearFetch:b}={}){if(e.source==="internal")return t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.prefix}-<number>.`),{ticket:t??`${e.prefix}-${l}`,created:t===void 0};if(e.source==="linear"){t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.team}-<number>.`);const c=k({fetch:b});if(c||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub settings \u203A Linear, or pick another ticket source."),t===void 0)return{ticket:(await T(()=>c.createIssue({team:e.team,project:e.project,title:n,description:r}))).identifier,created:!0};const h=await T(()=>c.issue(t));return h||i(`Linear has no issue ${t}.`),h.team!==e.team&&i(`${t} is in Linear's ${h.team} team, not ${e.team}.`),{ticket:h.identifier,created:!1}}O(u);const s=t===void 0?null:y(t);t!==void 0&&!s&&i("GitHub ticket must be an issue number such as #41.");const p=s?["issue","view",String(s),"--repo",u,"--json","number,url"]:["issue","create","--repo",u,"--title",n,"--body",r],m=await(await j(e,a))("gh",p);m.status!==0&&i((m.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
2
|
`)[0]);let f;if(s){let c;try{c=JSON.parse(m.stdout)}catch{i("GitHub returned an invalid issue.")}c.number!==s&&i("GitHub returned a different issue."),f=c.url}else f=m.stdout?.trim();const d=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(f||"");return(!d||d[1].toLowerCase()!==u.toLowerCase()||s&&Number(d[2])!==s||!y(d[2]))&&i("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${d[2]}`,created:!s}}o(E,"resolveTicket");const Z=o(e=>e.startsWith("#")?`GH-${e.slice(1)}`:e,"cardIdOf");function R(e,t){const n=e.source==="github"?String(t??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(t??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(r=>g(e,r)))]}o(R,"titleTickets");function z({start:e,done:t,handover:n}){return t?"done":e?"started":n?"review":null}o(z,"ticketTarget");const S={started:"In Progress",review:"In Review",done:"Done"};async function _(e,t,n,{repo:r,run:u=$,linearFetch:l}={}){const a=t.ticket;if(!e||!n||!a||!g(e,a)||e.source==="internal"||e.source==="github"&&n!=="done")return null;if(e.source==="github"&&!r)return`ticket ${a}: not closed, the board has no GitHub repo`;const b=o(s=>`ticket ${a}: could not move to ${S[n]} (${String(s.message||s).split(`
|
|
3
3
|
`)[0]})`,"why");try{if(e.source==="linear"){const f=k({fetch:l});if(!f)return`ticket ${a}: not moved, Hub has no Linear key`;if(!t.ticket_created){const c=await f.issue(a);if(!c)return`ticket ${a}: not moved, Linear has no such issue`;if(c.assignee!==f.saved.viewer?.id)return`ticket ${a}: left as it is, not assigned to you`}const d=await f.moveIssue(a,n);return d?`ticket ${a}: moved to ${d}`:null}const s=a.slice(1),p=await j(e,u);if(!t.ticket_created){const f=await p("gh",["issue","view",s,"--repo",r,"--json","state,assignees"]);if(f.status!==0)throw new Error(f.stderr||"gh issue view failed");const d=await p("gh",["api","user","--jq",".login"]);if(d.status!==0)throw new Error(d.stderr||"gh api user failed");const c=JSON.parse(f.stdout);if(c.state!=="OPEN")return null;if(!(c.assignees||[]).some(h=>h.login?.toLowerCase()===d.stdout.trim().toLowerCase()))return`ticket ${a}: left as it is, not assigned to you`}const m=await p("gh",["issue","close",s,"--repo",r]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${a}: closed`}catch(s){return b(s)}}o(_,"moveTicket");export{Z as cardIdOf,_ as moveTicket,E as resolveTicket,C as ticketBadge,g as ticketIn,N as ticketSource,z as ticketTarget,R as titleTickets};
|
package/board/transcript.mjs
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).reverse()){let
|
|
1
|
+
var h=Object.defineProperty;var s=(t,n)=>h(t,"name",{value:n,configurable:!0});import a from"node:fs";import d from"node:os";import u from"node:path";const b=256*1024;function x(t,n){const e=Math.min(n,b),o=Buffer.alloc(e),r=a.openSync(t,"r");try{a.readSync(r,o,0,e,n-e)}finally{a.closeSync(r)}for(const i of o.toString("utf8").split(`
|
|
2
|
+
`).reverse()){let l;try{l=JSON.parse(i).message?.content}catch{continue}if(Array.isArray(l))return l.at(-1)?.type==="tool_use"}return!1}s(x,"endsOnToolCall");function y(t,n){let e=t;try{e=a.realpathSync(t)}catch{}const o=u.join(n,".claude","projects",e.replace(/[/.]/g,"-"));return a.readdirSync(o).filter(r=>r.endsWith(".jsonl")).map(r=>({file:u.join(o,r),stat:a.statSync(u.join(o,r))})).reduce((r,i)=>r&&r.stat.mtimeMs>=i.stat.mtimeMs?r:i,void 0)}s(y,"newestTranscript");function A(t,n=d.homedir()){try{return y(t,n)?.stat.mtimeMs??null}catch{return null}}s(A,"transcriptMovedAt");function C(t,n=d.homedir()){try{const e=y(t,n);return e!==void 0&&x(e.file,e.stat.size)}catch{return!1}}s(C,"transcriptCommandRunning");const p=4e3,m=s(t=>t.length>p?`${t.slice(0,p)}\u2026`:t,"cut"),g=s(t=>typeof t=="string"?t:t.map(n=>n.text??"").join(`
|
|
3
|
+
`),"textOf"),S=s(t=>t.command??t.file_path??t.pattern??t.url??t.description??JSON.stringify(t),"callOf"),O=s(t=>{try{const n=JSON.parse(t);return typeof n?.type=="string"?n:null}catch{return null}},"eventOf");function w(t){if(t.type==="system")return t.subtype==="init"?[{kind:"sys",label:"start",body:`${t.model} in ${t.cwd}`}]:[];if(t.type==="result")return[{kind:"text end",label:t.is_error?"stopped":"done",body:t.result??t.subtype}];if(t.type!=="assistant"&&t.type!=="user")return[];const n=t.message.content;return typeof n=="string"?[{kind:"text",body:n}]:n.flatMap(e=>e.type==="text"?[{kind:"text",body:e.text}]:e.type==="tool_use"?[{kind:"tool",id:e.id,label:e.name.toLowerCase(),body:S(e.input)}]:e.type==="tool_result"?[{kind:"out",for:e.tool_use_id,body:m(g(e.content))}]:[])}s(w,"rowsOf");function _(t,n){const e=t.lastIndexOf(10)+1,o=t.subarray(0,e).toString("utf8").split(`
|
|
4
|
+
`).slice(0,-1),r=[];let i=!1,l=!1;for(const f of o){const c=O(f);if(!c){r.push({kind:"out",body:m(f)});continue}i=!0,l||=c.type==="result",r.push(...w(c))}return{rows:r,next:n+e,stream:i,finished:l}}s(_,"transcriptChunk");async function R(t,n){const e=await a.promises.open(t,"r");try{const{size:o}=await e.stat(),r=Buffer.alloc(Math.max(0,o-n)),{bytesRead:i}=await e.read(r,0,r.length,n);return{..._(r.subarray(0,i),n),size:o}}finally{await e.close()}}s(R,"readRunTranscript");export{R as readRunTranscript,_ as transcriptChunk,C as transcriptCommandRunning,A as transcriptMovedAt};
|
package/hub/home.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var g=Object.defineProperty;var a=(t
|
|
1
|
+
var g=Object.defineProperty;var a=(e,t)=>g(e,"name",{value:t,configurable:!0});import{siteIcon as w}from"./icons.mjs";import{contextFull as f}from"./usage.mjs";const d=a(e=>Number.isInteger(e)&&e>0?e:0,"count");function A(e){const{items:t,working:i,summary:n,lanes:r,building:o}=e||{};if(!Array.isArray(t))return null;const l=[...new Set(t.map(u=>String(u?.kind||"").trim()).filter(Boolean))],s=r&&typeof r=="object"&&["review","progress","todo"].every(u=>r[u]===void 0||Number.isInteger(r[u])&&r[u]>=0);return{count:t.length,working:d(i),summary:typeof n=="string"&&n.trim()?n.trim():t.length?`${t.length} waiting${l.length?`: ${l.join(", ")}`:""}`:"",lanes:s?{review:d(r.review),progress:d(r.progress),todo:d(r.todo)}:null,building:o===!0}}a(A,"readNeeds");function h({state:e,working:t,building:i,listen:n}={}){return e==="down"?{tone:"down",word:"down"}:n==="full"?{tone:"down",word:"context full"}:n==="deaf"?{tone:"down",word:"not listening"}:e==="starting"?{tone:"starting",word:"starting"}:e==="stopped"?{tone:"stopped",word:"stopped"}:t?{tone:"working",word:"working"}:n==="waking"?{tone:"asleep",word:"waking\u2026"}:n==="asleep"?{tone:"asleep",word:"asleep"}:i?{tone:"building",word:"implementing"}:null}a(h,"siteStatus");function $(e){return e?[["review","review"],["progress","doing"],["todo","todo"]].filter(([t])=>e[t]>0).map(([t,i])=>({lane:t,text:`${e[t]} ${i}`})):null}a($,"laneChips");const c=a(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc");function m(e,t,i){const n=h({state:e,...t,listen:i}),r=e==="up"?$(t?.lanes):null,o=t?.count?[{lane:"needs",text:`${t.count} need you`},...r||[]]:r,l=n?`<span class="dot t-${n.tone}${n.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${n.tone}">${c(n.word)}</span>`:"",s=o===null?"":o.length?o.map(u=>`<span class="lc lc-${u.lane}">${c(u.text)}</span>`).join(""):"<span>nothing open</span>";return l||s?`${l}${s?`<span class="lcs">${s}</span>`:""}`:""}a(m,"statusLine");function v(e,t,i,n=new Map,r=new Map){return e.map(o=>{const l=t.get(o.route)??null,s=i.has(o.route)?n.get(o.route)??"listening":void 0,u=r.get(o.route)??null;return{route:o.route,title:o.title,board:i.has(o.route),state:o.state,count:l?.count||0,changedAt:o.changedAt||0,created:o.created||null,icon:w(o.route,o),sub:m(o.state,l,f(s,u)?"full":s),ctx:u}})}a(v,"sidebarSites");function S(e){return e.filter(t=>t.board).sort((t,i)=>i.count-t.count||i.changedAt-t.changedAt||t.route.localeCompare(i.route))[0]?.route??null}a(S,"startRoute");const x="runs older code than its files; restart it";function j(e,t=new Map,i=null){const n=e.filter(s=>s.state==="down").length,r=e.reduce((s,u)=>s+(t.get(u.route)||[]).filter(p=>p.state==="broken").length,0),o=e.filter(s=>s.state==="up").length,l=i?.ok===!1;return{tone:n?"down":r||l?"wait":"ok",word:n?`${n} site${n===1?"":"s"} down`:r||l?"degraded":"healthy",detail:`${o} of ${e.length} site${e.length===1?"":"s"} up${r?` \xB7 ${r} link${r===1?"":"s"} failing`:""}${l?` \xB7 ${x}`:""}`}}a(j,"hubHealth");export{x as STALE_CODE,j as hubHealth,$ as laneChips,A as readNeeds,v as sidebarSites,h as siteStatus,S as startRoute,m as statusLine};
|
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Nt=Object.defineProperty;var l=(e,t)=>Nt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Gt from"node:net";import{spawn as Be,execFileSync as zt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as Kt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Vt,removeSubscription as Zt,removeSubscriptionById as Qt,sendAll as Xt,flushQueue as Ge,queueStatus as ze,saveSettings as Yt,readSettings as ee,migrateSettings as qt,setNotifications as en,readCodexShare as Ke,setCodexShare as tn,readClaudeModel as nn,setClaudeModel as sn,deliver as an}from"./push.mjs";import{ownerTracker as rn}from"./presence.mjs";import{POLL_MS as We,answerOf as on,expired as cn,pendingFor as Je,promptRecord as ln,pushOf as un,updatePrompts as G,shown as dn,pruneRemembered as ye,rememberRules as hn,rememberedAnswer as fn,rememberedFor as Ve}from"./prompts.mjs";import{hold as pn,heldUntil as mn,holdLine as gn,editQuiet as wn,checkEvery as Ze,nextCheckAt as bn,LOW_POINTS as yn}from"./schedule.mjs";import{capital as Sn,hubView as kn,settingsView as $n,flushView as _n,SNOOZE_REFUSED as xn,AWAKE_REFUSED as Tn}from"./views.mjs";import{caffeinate as vn,setKeepAwake as On,awakeUntil as Qe,awakeUnsupported as jn}from"./awake.mjs";import{endOf as Pn}from"./duration.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Xe,RESUME_STATE as Cn,sendToBoards as Ye,checkListening as An,recordHubCode as In,hubCode as Mn,runningHubCode as Hn,boardCode as qe,recordLinkFaults as Un,linksOf as Bn,hubLink as Fn,hubCodeState as Nn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as nt,readHubRules as ke,writeHubRules as Kn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ws,colorsVersion as at,destOf as bs,fileVersion as ys,pageOf as rt,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as te,setPauseAt as Ts,setKeepGoing as vs,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as ne}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as xe,initializeTries as As,fetchTryBranches as ot,pullNow as Is,pullMains as Ms}from"./try.mjs";import{serveFiles as Hs}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Fs}from"./self-restart.mjs";import{fallBackFromLinear as Ns,openBoardBuilders as Gs}from"../board/store.mjs";import{recordRefusal as zs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Vs,sharedRoutes as it,shareReach as Zs,routeOfPath as Qs,sharedSidebar as Xs,shareTracker as Ys}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as qs}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as ea,saveLinearKey as ta,savedClient as lt}from"./linear.mjs";import{SlackError as na,slackStatus as sa,removeSlackToken as aa,saveSlackToken as ra}from"./slack.mjs";import{sendReviews as oa}from"./review-send.mjs";const z=$.dirname(Kt(import.meta.url)),ia=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),K=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(K,"manifest.json"),ae=$.join(K,"logs"),ht=$.join(K,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[K,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=In();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ss();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=es(),ca=$s(),re=vn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),W=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function la(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?zt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(la,"killStale");function ua(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
4
|
`),n.code=Se(t)?qe(t):null;let h;try{h=Be(i,a,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
|
|
5
|
-
`),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(la,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ua=3e4;async function da(e){const t=Date.now()+ua;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(da,"serverAnswers");async function gt(){if(ie)return;wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...J.keys()])U(R(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ca(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Ns(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&la(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=Bs({root:ve(),files:In().files,restart:l(()=>ct("hub"),"restart"),log:c}),ha=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Yt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const fa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=an(),yt=l(()=>y.startedAt?wn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(St,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Pn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Is({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ra({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Cn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Hn(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=pn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let kt=null;function pa(){kt||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{kt=null})}l(pa,"runWakes");const Ce=new Set,A={running:null};function $t(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Qn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Xn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l($t,"runSessionCheck"),await Cs(g());const ma=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=on(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(_t,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function xt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Gs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(xt,"noteRefusal");let Q=null,Tt=null;async function he(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,he())):(Q=(async()=>$s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{js()}catch(e){c("machine check failed:",e.message)}F.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=yt().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),cs({listening:l(e=>Dn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),re.apply(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function vt(){if(ie)return;ie=!0,clearInterval(ha),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ds().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,jt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(jt,"newestMtime");function ga(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=jt(t.dir);return Ot.set(e,{at:s,checked:Date.now()}),s}l(ga,"changedAt");const Pt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Pt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ps(n),changedAt:ga(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),pe=new Map,Rt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ba,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Rt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Et,"waitingCounts");const Dt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Ln,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Rt)&&(p={at:Date.now(),listen:a,links:Un(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(ya,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Et(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=ya(a),u=Fn(),_=new Map(a.map(o=>[o.route,_s(o.owner?.session)]).filter(([,o])=>o)),k=Yn(i,t.getTime(),is,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>Os(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Kn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=fn(t,O);return{data:{code:u.running??null,version:ut,total:Dt(s),start:Vn(w),sites:w,hub:Zn(w,p,Bn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ia({codexShare:We(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Lt(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Lt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return ys({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(Sa,"shellPage");const Ct=Hs({root:ve(),run:ka,log:c,pulled:l(()=>wt.restartNow(),"pulled")});function ka(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
5
|
+
`),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(ua,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Gt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const J=new Set,da=3e4;async function ha(e){const t=Date.now()+da;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(ha,"serverAnswers");async function gt(){if(ie)return;wt&&bt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...W.keys()])U(R(e,t))!=="port"&&W.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&W.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&la(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=wt&&s?.child&&s.code&&!s.stopping&&!J.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Gs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ua(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Fs({root:ve(),files:Mn().files,restart:l(()=>ct("hub"),"restart"),log:c}),fa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=l(async()=>{try{for(const e of await Ns())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const pa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+pa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=rn(),St=l(()=>y.startedAt?bn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(kt,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Rn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Ms({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await oa({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=mn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let $t=null;function ma(){$t||=En({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}l(ma,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||J.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l(_t,"runSessionCheck"),await As(g());const ga=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=cn(n,{now:Date.now(),aliveSince:ga}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(xt,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function Tt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await zs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(Tt,"noteRefusal");let Q=null,vt=null;async function he(){return Q?vt??=Q.catch(()=>{}).then(()=>(vt=null,he())):(Q=(async()=>_s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}F.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ma(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(fa),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,Pt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(Pt,"newestMtime");function wa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(wa,"changedAt");const Rt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Rt(n.dir)?"up":"down":i==="port"?W.has(t)?W.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:wa(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),pe=new Map,Et=2e4,ba=1500;async function ya(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ba)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ya,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Et)&&pe.set(i.route,{at:Date.now(),needs:ya(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Je(null).length,"badgeTotal"),Ie=new Map;function Sa(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:a,links:Bn(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(Sa,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Dt(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=Sa(a),u=Nn(),_=new Map(a.map(o=>[o.route,xs(o.owner?.session)]).filter(([,o])=>o)),k=qn(i,t.getTime(),cs,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>js(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Vn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=pn(t,O);return{data:{code:u.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Fn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ca({codexShare:Ke(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Sn(gn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Ct(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Ct,"sendText");async function ka(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(ka,"shellPage");const At=Us({root:ve(),run:$a,log:c,pulled:l(()=>bt.restartNow(),"pulled")});function $a(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
|
|
6
6
|
`);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[$.join(ve(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const a=b.readFileSync(e,"utf8").trim().split(`
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l(
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l($a,"runUpdate"),At.check(),setInterval(()=>At.check(),Bs);const x=ks({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const _a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function v(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(v,"readJSONBody");const It=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
8
|
<title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),xa=l(()=>({running:!!y.running,nextAt:yt(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<bn}),"pollState");async function ge(e){await _t();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...Sn({now:t,st:ze(t),poll:xa(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Ps.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(ne()),claudeModel:tn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(un),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:On()},n=te({now:e}),s=n.manual==="hub"?jn(n.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:se(),slack:na(),awake:t,pausedUntil:s}),closed:ds()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ta(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Mn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=_a(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await ls(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await hs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await us(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,K.has(r))return a(409,{error:"That board is already restarting."});K.add(r),await A.running;const o=await fs(r,{serverUp:da,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{K.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ys().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),St(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,$t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return es(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ts(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await as(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await rs(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await os(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await As(d,{manifest:g()})):a(202,await Es(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Rs)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(oa),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Ks(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=gs(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===bs(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Lt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Kt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Vt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Qt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await qt(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:_n}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:xn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vs(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{en(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{nn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?xs(r.get("value")):Ts(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(qs(),c("linear: key removed"),await bt()):c(`linear: key ${(await ea(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(sa(),c("slack: token removed")):c(`slack: token ${(await aa(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ta))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Xt(gn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=Gn(ke(),r.rules,r.base??[]),nt(o.changes)&&zn(o.rules)}catch(f){if(!(f instanceof Nn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,$n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=hn(f,r);if(q)return{answer:q};const be=cn(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await xt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),sn(ln(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await xt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ta,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Je,Je));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=rn(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),dn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(va,"promptRoute");const Oa=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(It,"downWhy");function Mt(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(ja(t,n))}l(Mt,"sendDown");function ja(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=It(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),xa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ta=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<yn}),"pollState");async function ge(e){await xt();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...kn({now:t,st:ze(t),poll:Ta(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Rs.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ke(ne()),claudeModel:nn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Je(null).map(dn),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:jn()},n=te({now:e}),s=n.manual==="hub"?Pn(n.resets_at,e):null;return{...$n({now:e,st:ze(e),rules:ke(),linear:se(),slack:sa(),awake:t,pausedUntil:s}),closed:hs()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function va(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Hn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=xa(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await us(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await fs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,J.has(r))return a(409,{error:"That board is already restarting."});J.add(r),await A.running;const o=await ps(r,{serverUp:ha,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{J.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:qs().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),kt(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,_t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ns(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await is(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await Is(d,{manifest:g()})):a(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",_a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(ia),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Vs(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=ws(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===ys(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Vt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Qt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Xt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await en(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:xn}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{On(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:Tn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{Os(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{tn(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{sn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?Ts(r.get("value")):vs(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(ea(),c("linear: key removed"),await yt()):c(`linear: key ${(await ta(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(aa(),c("slack: token removed")):c(`slack: token ${(await ra(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof na))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Yt(wn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=zn(ke(),r.rules,r.base??[]),nt(o.changes)&&Kn(o.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,_n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=fn(f,r);if(q)return{answer:q};const be=ln(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await Tt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),an(un(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await Tt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?Oa(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(va,"hubRoute");async function Oa(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||We,We));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=on(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),hn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(Oa,"promptRoute");const ja=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function Mt(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(Mt,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":Mt(t,n)}),e.end(Pa(t,n))}l(Ht,"sendDown");function Pa(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=Mt(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
11
|
<h1>${N(t.title||e)} isn't answering</h1>
|
|
12
12
|
<p class="lede">${N(u)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${ja({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}l(
|
|
16
|
+
</main></body></html>`}l(Pa,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Ys({file:$.join(K,"share-seen.json")}),we=new Set;function Ut(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Cs()}l(Ut,"endUnshared"),setInterval(Ut,2e3).unref();async function Bt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Zs({method:e.method,pathname:i,routes:a}):"other",h=Qs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Xs(_,a,u?.name||u?.login),"sidebar")}}}l(Bt,"access");const Ft=Ue.createServer((e,t)=>{Bt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ra(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ra(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);ka(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){va(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${It("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Rt(p.dir)){Ht(t,a,p);return}Hs(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ut),S.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,a,p)}),e.pipe(S)}l(Ra,"serve"),Ft.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Bt(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;Ks({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ft.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
|
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var se=Object.defineProperty;var a=(c,f)=>se(c,"name",{value:f,configurable:!0});import O from"node:fs";import T from"node:path";import
|
|
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
|
|
3
|
-
`)})}a(xt,"failure");function at(t){return t.ending||(Ut(t.child,"SIGTERM"),t.ending=new Promise(e=>{const r=a(()=>{clearTimeout(i),clearInterval(s),e()},"finish");let i=setTimeout(()=>{Ut(t.child,"SIGKILL"),i=setTimeout(r,1e3)},4e3);const s=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{r()}},20)})),t.ending}a(at,"terminate");async function jt(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await at(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(D(t.board,t.key))}a(Z,"stopRun");async function _t(t,e,r,i={}){if(!r)throw new E("This card has no valid worktree.");const s=Lt(r,t.build_target),n=he(s);if(n)throw new E(n);if(!O.realpathSync(s).startsWith(O.realpathSync(r)+T.sep))throw new E("Build script must be inside the worktree.");const o=await C(r,"rev-parse","HEAD");if(G)throw new E("The hub is stopping. Try again after it restarts.",503);const{state:w}=N(t),x=St(w,e),d={...F(e)?x:{},...i,sha:o,script:!0,state:"running",...F(e)?{at:lt(),head:o}:{}};delete d.port,delete d.https;const y=Kt(t,e);O.mkdirSync(T.dirname(y),{recursive:!0});const j=O.openSync(y,"w",384);O.fchmodSync(j,384);const v=ne(s,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(w,e,d),M(t);const B=D(t,e),l={child:v,board:t,key:e,logFile:y,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),P.set(B,o);let R,I;const q=a(A=>{I||=new Error(A),at(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=Dt({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 K=Number(it[1]);if(K<1||K>65535){q("Build script printed an invalid port.");return}l.port=K,clearTimeout(l.deadline);const tt=c()+6e4;let pt=!1;const Mt=a(async()=>{if(!(pt||l.stopped||l.ready||v.exitCode!==null||v.signalCode!==null)){pt=!0;try{if(await f(K)){if(l.stopped||I||v.exitCode!==null||v.signalCode!==null||(l.opening=Qt(l),await l.opening,l.stopped||I||v.exitCode!==null||v.signalCode!==null))return;l.ready=!0,l.visited=c(),w.kind="web",X(t,e,{state:"ready",port:K,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),v.once("exit",()=>{at(l)}),l.done=new Promise(A=>v.once("close",(it,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),O.closeSync(j),(async()=>{if(await at(l),await Xt(l),l.finished=!0,l.stopped)return;const tt=e===z&&!l.port?new Error('Build script exited without printing "port <n>".'):null;I||R||it!==0||l.port&&!l.ready||tt?await xt(l,it??1,K,I||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),M(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 ${y}`),{status:"started"}}a(_t,"launch");async function It(t,e,r){const i=D(t,"main");if(g.has(i))throw new E("A build is already running for main.",409);g.add(i);try{u.has(i)&&await Z(u.get(i),!0);const s=T.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return O.mkdirSync(T.dirname(s),{recursive:!0}),gt(s)?await C(s,"checkout","--detach","--force",r.sha):(await C(e,"worktree","prune"),await C(e,"worktree","add","--detach",s,r.sha)),await _t(t,"main",s,r)}catch(s){throw s.status!==503&&(N(t).state.main={...r,state:"failed"},$t(t,"main",s),M(t)),s}finally{g.delete(i)}}a(It,"launchMain");async function ut(t,{manifest:e=J()}={}){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:r,key:i}=t;if(F(i)&&(!/^[\w.-]+\/[\w.-]+$/.test(i)||i.split("/").some(d=>d==="."||d==="..")))throw new E("Card key must be <wave>/<id>.");const s=U(e).find(d=>d.route===r);if(!s)throw new E(`/${r} is not a board.`,404);if(F(i)&&!s.waves.some(({slug:d,wave:y})=>y.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=D(s,i),w=u.get(o);if(g.has(o)||w&&!w.ready&&!w.finished)throw new E(`A build is already running for ${i==="main"?"main":i===z?"the prototype site":"this card"}.`,409);if(i==="main"){const d=N(s).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:y,branch:j,slug:v,at:B,script:l}=d;return It(s,et(r,e).repo,{sha:y,branch:j,slug:v,at:B,script:l,...s.build_target?{target:s.build_target}:{}})}g.add(o);const x=$.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=i===z?Ct(s):await Jt(s,i,e);return await _t(s,i,d)}catch(d){throw b.delete(o),x&&d.status!==503&&X(s,i,{state:void 0,sha:void 0}),d}finally{g.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:r}=N(t),i=D(t,z),s=u.get(i);if(!((await ye(t.dir)).length>0&&Ct(t))){s&&await Z(s),r.prototypes&&(r.prototypes=null,M(t));return}if(!(s||g.has(i)||r.prototypes?.state==="failed"||await m()))try{await ut({route:t.route,key:z},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw r.prototypes={state:"failed"},$t(t,z,o),M(t),o}}a(qt,"serveBase");function Pt(t,e,{manifest:r=J()}={}){const i=U(r).find(n=>n.route===t);if(!i)return null;const s=[...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 s?{key:s.key,port:s.port}:null}a(Pt,"lookup");async function ft({manifest:t=J(),routes:e}={}){if(await st(t),dt(t),G)return H;if(H&&e){if(!Q){const r={routes:new Set,manifest:t};r.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:r.manifest,routes:[...r.routes]}))),Q=r}Q.manifest=t;for(const r of e)Q.routes.add(r);return Q.done}return H||(H=(async()=>{await Zt();const r=U(t),i=nt(ce,{}).prs||{};for(const s of[...u.values()])if(!r.some(n=>n.dir===s.board.dir)||F(s.key)&&s.ready&&c()-s.visited>=_e){await Z(s);const{state:n}=N(s.board);kt(n,s.key),M(s.board)}for(const s of r){if(G)return;if(e&&!e.includes(s.route))continue;const{state:n}=N(s),o=et(s.route,t),w=o.branch||"main",x=le(o);for(const{slug:d,wave:y}of s.waves)for(const j of y.items){const v=`${d}/${j.id}`,B=ve(j,s.github||y.repo).map(l=>i[Se(l)]);if(!(j.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(s,v),R=await Jt(s,v,t);if(R){const L=await C(R,"rev-parse","HEAD");let A=0;try{A=Number(await C(R,"rev-list","--count",`${x}..HEAD`))}catch{}Object.assign(n.cards[v]||={},{head:L,ahead:A,script:gt(Lt(R,s.build_target))}),M(s)}else if(!$.has(l))continue;const I=n.cards[v];if(($.has(l)||s.auto_build&&I.script&&I.ahead>0&&(I.sha||P.get(l))!==I.head&&I.state!=="running")&&!g.has(l))try{await ut({route:s.route,key:v},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(s,v,{sha:I.head}),await xt({board:s,key:v,logFile:$t(s,v,L)},1),L}}catch(l){h(`try /${s.route} ${v}: ${l.message}`)}}await qt(s,t).catch(d=>h(`try /${s.route} prototypes: ${d.message}`));try{if(!o.repo||await m())continue;const d=await C(o.repo,"rev-parse",x);let y=!1;try{await C(o.repo,"cat-file","-e",`${d}:${Wt(s.build_target)}`),y=!0}catch{}if(!y){const I=u.get(D(s,"main"));I&&await Z(I),n.main=null,M(s);continue}const j=n.main;if(j?.state==="running"||j?.sha===d&&(j.target||null)===(s.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Pt(s.route,"main",{manifest:t}))))continue;const v=D(s,"main");if(g.has(v))continue;const B=await C(o.repo,"log","-1","--format=%cI",d);let l=s.github;if(!l)try{l=Ft(await C(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:w,slug:l||"",at:B,script:y,...s.build_target?{target:s.build_target}:{}};await It(s,o.repo,R)}catch(d){h(`try /${s.route} main: ${d.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function At(t,e){if(W.has(t.dir))return W.get(t.dir);const r=(async()=>{const i=et(t.route,e),s=i.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},M(t);let o,w;try{o=await Pe(i.repo,s)}catch(x){w=x.message}if(o===null?delete n.pull:n.pull={at:lt(),...w?{error:w}:{}},M(t),o===null)return null;if(w)return h(`pull /${t.route}: ${w}`),{commits:0,error:w};if(o.commits){h(`pull /${t.route}: ${s} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await S(t,"main-pulled",{branch:s,...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,r),r}a(At,"pullMain");async function te({manifest:t=J(),routes:e}={}){await st(t);const r=[];for(const i of U(t)){if(G)break;if(e&&!e.includes(i.route))continue;const s=et(i.route,t);!s.local||!s.repo||(await At(i,t))?.commits&&r.push(i.route)}return r}a(te,"pullMains");async function ee(t,{manifest:e=J()}={}){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 r=U(e).find(n=>n.route===t.route);if(!r)throw new E(`/${t.route} is not a board.`,404);const i=et(r.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 s=await At(r,e);if(!s)throw new E("This board\u2019s folder has no origin to pull from.",409);return s.commits&&ft({manifest:e,routes:[r.route]}).catch(n=>h(`try /${r.route} main: ${n.message}`)),s}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,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 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`:Ht(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,_)=>m(c,"merge-base","--is-ancestor",b,_).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 g=Number(await m(c,"rev-list","--count",`${p}..${k}`)),$=(await m(c,"worktree","list","--porcelain")).split(`
|
|
1
|
+
var se=Object.defineProperty;var a=(c,f)=>se(c,"name",{value:f,configurable:!0});import O from"node:fs";import T from"node:path";import Mt from"node:os";import mt from"node:http";import{execFile as ie,spawn as ne}from"node:child_process";import{createInterface as Nt}from"node:readline";import{promisify as oe}from"node:util";import{listBoards as J,readManifest as z,readJSON as nt,saveJSON as Bt,appendEvent as ae,GITHUB_STATE as ce,REPO_FETCH as Rt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Ht,mainRefOf as le}from"./board-repo.mjs";import{gitReachFault as Ft,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{appendThread as me,recordedWorktree as we,servedRoundsOpen as ye,setItem as ge}from"../board/store.mjs";import{BASE_KEY as K,baseWorktreeOf as $e}from"../board/prototype-base.mjs";import{cardPRs as ve,hasPR as be,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 E extends Error{static{a(this,"BuildError")}constructor(f,h=400){super(f),this.status=h}}const wt=oe(ie),_=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),je=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,Y={first:8500,last:8599},yt=a(c=>/^\d+$/.test(String(c))&&Number(c)>=Y.first&&Number(c)<=Y.last,"isTryPort"),_e=30*6e4,Gt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],lt={main:"main",[K]:"prototypes"},H=a(c=>!Object.hasOwn(lt,c),"isCard"),Lt=a((c,f)=>T.join(c,Wt(f)),"scriptAt"),gt=a(c=>O.existsSync(c),"exists"),Kt=a((c,f)=>T.join(c.dir,"data","try",`${f.split("/").at(-1)}.log`),"logFileOf");function $t(c,f,h){const m=Kt(c,f);return O.mkdirSync(T.dirname(m),{recursive:!0}),O.writeFileSync(m,h.message+`
|
|
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(`
|
|
4
4
|
|
|
5
5
|
`).map(b=>b.split(`
|
|
6
|
-
`)).find(b=>b.includes(`branch refs/heads/${f}`))?.[0].slice(9);return
|
|
7
|
-
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=
|
|
8
|
-
`).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=de(c.remote)?.host||"the remote";for(const u of S){let
|
|
6
|
+
`)).find(b=>b.includes(`branch refs/heads/${f}`))?.[0].slice(9);return g?(await m(g,"status","--porcelain","--untracked-files=no")&&S("local changes"),await m(g,"merge","--ff-only","--quiet",k)):await m(c,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:y}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;S(String(p.stderr||p.message).trim().split(`
|
|
7
|
+
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=z(),log:f=console.log,routes:h}={}){const m=h?nt(Rt,{}):{};for(const S of J(c)){if(h&&!h.includes(S.route))continue;const p=et(S.route,c);if(p.local||!p.repo)continue;const k=p.branch||"main",u=new Date().toISOString(),y=Date.now();try{await bt(p.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),m[S.route]={at:u,ok:!0}}catch(g){f(`fetch /${S.route} after ${Date.now()-y} ms: ${g.message}`);let b=S.github;if(!b&&!S.remote)try{b=Ht(await _(p.repo,"remote","get-url","origin"))}catch{}m[S.route]={at:u,ok:!1,...Ft(g.killed?`git fetch timed out after ${vt/1e3} s`:g.stderr,{github:b||"",remote:S.remote,repo:p.repo})};continue}if(S.remote)try{await Ae(S,p.repo,k,f)}catch(g){f(`pushed branches /${S.route}: ${g.message}`)}}Bt(Rt,m)}a(tr,"fetchTryBranches");async function Ae(c,f,h,m){const S=c.waves.flatMap(({slug:u,wave:y})=>y.items.filter(g=>g.pushed?.branch&&!be(g)&&g.lane!=="done").map(g=>({key:`${u}/${g.id}`,branch:g.pushed.branch})));if(!S.length)return;const p=new Map((await bt(f,"ls-remote","--heads","origin")).split(`
|
|
8
|
+
`).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=de(c.remote)?.host||"the remote";for(const u of S){let y=p.get(u.branch);if(!y)try{y=await _(f,"rev-parse","--verify","--quiet",`refs/heads/${u.branch}^{commit}`)}catch{}let g=!1;if(y)try{await _(f,"merge-base","--is-ancestor",y,`refs/remotes/origin/${h}`),g=!0}catch{}if(!(!g&&p.has(u.branch)))try{await ge(c.dir,u.key,{done:!0,force:!0,landed:g,text:g?`${u.branch} is merged into ${h} on ${k}, so this card is Done.`:`${u.branch} is gone from ${k}, so this card is Done.`})}catch(b){m(`pushed ${u.key}: ${b.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{E as BuildError,Y as TRY_PORTS,Ce as createTryManager,or as endUnsharedTries,tr as fetchTryBranches,ar as initializeTries,Jt as installWorktree,yt as isTryPort,Pe as pullBranch,ir as pullMains,sr as pullNow,er as startBuild,nr as stopTries,rr as tryTick};
|
package/hub/upgrade.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var y=Object.defineProperty;var e=(n,s)=>y(n,"name",{value:s,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,s){const[a,i]=[m(n),m(s)];for(let r=0;r<3;r++)if((a[r]||0)!==(i[r]||0))return(a[r]||0)>(i[r]||0);return!1}e(g,"newer");function _({root:n,fetch:s=globalThis.fetch,run:a,log:i=e(()=>{},"log"),pulled:r=e(()=>{},"pulled")}){const k=p.existsSync(u.join(n,".git")),l=e(()=>JSON.parse(p.readFileSync(u.join(n,"package.json"),"utf8")).version,"installed"),d=l();let c=!1;async function h(){if(k)return;let o;try{const t=await s(S,{signal:AbortSignal.timeout(15e3)});if(!t.ok)throw new Error(`npm answered ${t.status}`);o=(await t.json()).version}catch(t){i(`kickflip update check failed: ${t.message}`);return}g(o,l())&&w(o)}e(h,"check");function w(o){c||(c=!0,a().then(({ok:t})=>{if(!t)return;const f=l();g(f,d)?r():i(`update left v${f}; npm has ${o}`)},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};
|
package/package.json
CHANGED
package/shared/tailscale.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=Object.defineProperty;var a=(t,s)=>p(t,"name",{value:s,configurable:!0});import i from"node:fs";import u from"node:os";import
|
|
1
|
+
var p=Object.defineProperty;var a=(t,s)=>p(t,"name",{value:s,configurable:!0});import i from"node:fs";import u from"node:os";import o from"node:path";import{spawnSync as f}from"node:child_process";const d=o.join("Tailscale.app","Contents","MacOS","Tailscale"),m=a(t=>{try{return i.statSync(t).isFile()&&(i.accessSync(t,i.constants.X_OK),!0)}catch{return!1}},"isExecutable");function l({env:t=process.env,platform:s=process.platform,apps:n=["/Applications",o.join(u.homedir(),"Applications")]}={}){if(t.KICKFLIP_TAILSCALE)return[t.KICKFLIP_TAILSCALE];const r=(t.PATH||"").split(o.delimiter).filter(Boolean).map(e=>o.join(e,"tailscale"));return s==="darwin"&&r.push(...n.map(e=>o.join(e,d))),[...new Set(r)].filter(m)}a(l,"tailscaleCandidates");function S(t){const s=f(t,["status","--json"],{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]});try{return JSON.parse(s.stdout).BackendState==="Running"}catch{return!1}}a(S,"daemonRunning");function L({running:t=S,...s}={}){const n=l(s);return n.length>1?n.find(t)??n[0]:n[0]??null}a(L,"tailscaleBin");function w(t,s={}){const n=l(s);return n.includes(t)?n.slice(0,n.indexOf(t)):[]}a(w,"tailscaleSkipped");const c=a((t=process.platform)=>t==="darwin"?"brew install --cask tailscale-app":"curl -fsSL https://tailscale.com/install.sh | sh","tailscaleInstallCommand"),g=a((t=process.platform)=>t==="darwin"?`install Tailscale from https://tailscale.com/download, or ${c(t)}`:c(t),"tailscaleInstallHint");export{S as daemonRunning,L as tailscaleBin,l as tailscaleCandidates,c as tailscaleInstallCommand,g as tailscaleInstallHint,w as tailscaleSkipped};
|