@lovek4d/kickflip 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +15 -11
- package/SKILL.md +7 -7
- package/bin/doctor.mjs +5 -4
- package/bin/install.mjs +3 -3
- package/board/bin/board.mjs +26 -12
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +9 -9
- package/board/public/app-cards.js +23 -44
- package/board/public/app-lanes.js +4 -7
- package/board/public/app-threads.js +12 -8
- package/board/public/prototype-switcher.js +7 -2
- package/board/public/shell.js +4 -3
- package/board/public/sidebar.css +5 -0
- package/board/public/sidebar.js +3 -0
- package/board/public/style.css +11 -19
- package/board/public/ui-cards.js +21 -15
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-header.js +4 -6
- package/board/public/ui-hub.js +7 -4
- package/board/public/ui-order.js +1 -1
- package/board/public/ui-page-header.js +1 -1
- package/board/public/ui-sidebar.js +11 -5
- package/board/public/ui-snapshot.js +1 -1
- package/board/public/ui.js +3 -2
- package/board/public/view-hub.js +3 -1
- package/board/public/view-questions.js +1 -1
- package/board/server.mjs +20 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +27 -25
- package/board/transcript.mjs +2 -2
- package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
- package/hub/front-door.mjs +1 -1
- package/hub/hub.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/usage.mjs +3 -3
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -2
- package/shared/tailscale.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
package/board/transcript.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var m=Object.defineProperty;var s=(
|
|
2
|
-
`).reverse()){let
|
|
1
|
+
var m=Object.defineProperty;var s=(n,e)=>m(n,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:os";import l from"node:path";const d=256*1024;function p(n,e){const r=Math.min(e,d),o=Buffer.alloc(r),t=c.openSync(n,"r");try{c.readSync(t,o,0,r,e-r)}finally{c.closeSync(t)}for(const i of o.toString("utf8").split(`
|
|
2
|
+
`).reverse()){let a;try{a=JSON.parse(i).message?.content}catch{continue}if(Array.isArray(a))return a.at(-1)?.type==="tool_use"}return!1}s(p,"endsOnToolCall");function u(n,e){let r=n;try{r=c.realpathSync(n)}catch{}const o=l.join(e,".claude","projects",r.replace(/[/.]/g,"-"));return c.readdirSync(o).filter(t=>t.endsWith(".jsonl")).map(t=>({file:l.join(o,t),stat:c.statSync(l.join(o,t))})).reduce((t,i)=>t&&t.stat.mtimeMs>=i.stat.mtimeMs?t:i,void 0)}s(u,"newestTranscript");function j(n,e=f.homedir()){try{return u(n,e)?.stat.mtimeMs??null}catch{return null}}s(j,"transcriptMovedAt");function M(n,e=f.homedir()){try{const r=u(n,e);return r!==void 0&&p(r.file,r.stat.size)}catch{return!1}}s(M,"transcriptCommandRunning");export{M as transcriptCommandRunning,j as transcriptMovedAt};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// A PreToolUse hook on Edit|Write that refuses a hand edit of the
|
|
3
|
-
//
|
|
4
|
-
// because a headless session has nobody to ask, and the reason names
|
|
5
|
-
//
|
|
6
|
-
// (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md
|
|
2
|
+
// A PreToolUse hook on Edit|Write that refuses a hand edit of the files board.mjs owns on a board site: the site's
|
|
3
|
+
// board.json, every .json and .md under its data/, the wave file(s) board.json's `items` names, and the .md notes beside
|
|
4
|
+
// a ~/.claude/waves wave file. It denies, not asks, because a headless session has nobody to ask, and the reason names
|
|
5
|
+
// the board.mjs command that makes the change. Drafts (<site>/drafts/, or any folder outside data/) and Bash writes stay
|
|
6
|
+
// open (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md,
|
|
7
|
+
// docs/adr/2026-11-09-1100-the-board-owns-a-cards-notes-file.md).
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
@@ -14,13 +15,14 @@ process.stdout.on('error', () => {});
|
|
|
14
15
|
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
15
16
|
const inside = (dir, file) => !path.relative(dir, file).startsWith('..') && !path.isAbsolute(path.relative(dir, file));
|
|
16
17
|
|
|
17
|
-
// → the file's kind on this board ('wave', 'board' or 'data'), or null when it is not board-owned
|
|
18
|
+
// → the file's kind on this board ('wave', 'board', 'notes' or 'data'), or null when it is not board-owned
|
|
18
19
|
function ownedKind(board, file) {
|
|
19
20
|
const dir = real(board.dir);
|
|
20
21
|
let items = 'waves';
|
|
21
22
|
try { items = JSON.parse(fs.readFileSync(path.join(dir, 'board.json'), 'utf8')).items || 'waves'; } catch {}
|
|
22
23
|
if (file === path.join(dir, 'board.json')) return 'board';
|
|
23
24
|
if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return 'wave';
|
|
25
|
+
if (file.endsWith('.md') && (inside(path.join(dir, 'data'), file) || (items === 'waves' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file)))) return 'notes';
|
|
24
26
|
if (file.endsWith('.json') && inside(path.join(dir, 'data'), file)) return 'data';
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
@@ -28,6 +30,7 @@ function ownedKind(board, file) {
|
|
|
28
30
|
const HINTS = {
|
|
29
31
|
wave: 'A card changes with `board.mjs --site <route> item add|set <wave>/<id> …`',
|
|
30
32
|
board: 'Board settings change with `board.mjs --site <route> rules|reviewers …` or the settings page',
|
|
33
|
+
notes: 'A card\'s notes change with `board.mjs --site <route> item notes <wave>/<id> --heading "<heading>" --file <draft.md>`, the draft written outside data/',
|
|
31
34
|
data: 'Board data changes with a board.mjs command (`item set`, `thread`, `topic add`, `question add --file <draft>`, `reply`, `builder …`); its usage text lists them',
|
|
32
35
|
};
|
|
33
36
|
|
package/hub/front-door.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var u=Object.defineProperty;var a=(t,e)=>u(t,"name",{value:e,configurable:!0});import d from"node:fs";import g from"node:net";import c from"node:path";import{fileURLToPath as y}from"node:url";const f=c.join(c.dirname(y(import.meta.url)),".."),p={"view-only":{status:403,line:"You can look at this board, not change it.",hint:"Its owner shared it with you view only.",json:{error:"This board is shared with you to look at only."}},other:{status:403,line:"This kickflip belongs to someone else.",hint:"Ask its owner to share it with you.",json:{}},unread:{status:503,line:"Tailscale didn\u2019t answer, so this kickflip can\u2019t tell who you are.",hint:"Open it on the
|
|
1
|
+
var u=Object.defineProperty;var a=(t,e)=>u(t,"name",{value:e,configurable:!0});import d from"node:fs";import g from"node:net";import c from"node:path";import{fileURLToPath as y}from"node:url";const f=c.join(c.dirname(y(import.meta.url)),".."),p={"view-only":{status:403,line:"You can look at this board, not change it.",hint:"Its owner shared it with you view only.",json:{error:"This board is shared with you to look at only."}},other:{status:403,line:"This kickflip belongs to someone else.",hint:"Ask its owner to share it with you.",json:{}},unread:{status:503,line:"Tailscale didn\u2019t answer, so this kickflip can\u2019t tell who you are.",hint:"Open it on the host itself, or try again in a moment.",json:{error:"Tailscale didn\u2019t answer, so the hub can\u2019t tell who you are."}}},b=`html, body { margin: 0; height: 100%; }
|
|
2
2
|
body { display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; box-sizing: border-box; background: var(--bg); color: var(--text); text-align: center;
|
|
3
3
|
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif; }
|
|
4
4
|
.rf-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
|
package/hub/hub.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var Ft=Object.defineProperty;var l=(e,t)=>Ft(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Gt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as zt}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 Kt,removeSubscription as Vt,removeSubscriptionById as Zt,sendAll as Qt,flushQueue as Ge,queueStatus as ze,saveSettings as Xt,readSettings as ee,migrateSettings as Yt,setNotifications as qt,readCodexShare as We,setCodexShare as en,readClaudeModel as tn,setClaudeModel as nn,deliver as sn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as Je,answerOf as rn,expired as on,pendingFor as Ke,promptRecord as cn,pushOf as ln,updatePrompts as G,shown as un,pruneRemembered as ye,rememberRules as dn,rememberedAnswer as hn,rememberedFor as Ve}from"./prompts.mjs";import{hold as fn,heldUntil as pn,holdLine as mn,editQuiet as gn,checkEvery as Ze,nextCheckAt as wn,LOW_POINTS as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n,AWAKE_REFUSED as xn}from"./views.mjs";import{caffeinate as Tn,setKeepAwake as vn,awakeUntil as Qe,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as Se,listenState as Xe,RESUME_STATE as Ln,sendToBoards as Ye,checkListening as Cn,recordHubCode as An,hubCode as In,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Hn,linksOf as Un,hubLink as Bn,hubCodeState as Fn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Nn,applyRules as Gn,changed as nt,readHubRules as ke,writeHubRules as zn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Kn,startRoute as Vn,hubHealth as Zn}from"./home.mjs";import{checkSessions as Qn,compactFullContexts as Xn,startingBoards as Yn}from"./sessions.mjs";import{repoService as qn,repoLookupRoute as es}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ts,migrateBoardRepos as ns,migrateHubBoard as ss}from"./board-repo.mjs";import{requestBoard as as,retryBoard as rs,dismissBoard as os,clonePercent as is,settleStarts as cs}from"./new-board.mjs";import{boardClosure as ls,closeBoard as us,closedBoards as ds,reopenBoard as hs}from"./close-board.mjs";import{restartBoard as fs,restartable as ps}from"./restart-board.mjs";import{crossSite as ms,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as gs,colorsVersion as at,destOf as ws,fileVersion as bs,pageOf as rt,shellHtml as ys}from"./shell.mjs";import{hubStream as Ss}from"./stream.mjs";import{usageReader as ks,checkUsagePause as $s,contextReading as _s}from"./usage.mjs";import{readUsageGate as te,setPauseAt as xs,setKeepGoing as Ts,setHubPause as vs,boardPausedUntil as Os}from"../shared/usage-gate.mjs";import{checkMachine as js}from"../shared/machine-gate.mjs";import{readiness as Ps,readCodex as ne}from"./readiness.mjs";import{BuildError as Rs,startBuild as Es,stopTries as Ds,endUnsharedTries as Ls,tryTick as xe,initializeTries as Cs,fetchTryBranches as ot,pullNow as As,pullMains as Is}from"./try.mjs";import{serveFiles as Ms}from"./files.mjs";import{upgrader as Hs,CHECK_MS as Us}from"./upgrade.mjs";import{restarter as Bs}from"./self-restart.mjs";import{fallBackFromLinear as Fs,openBoardBuilders as Ns}from"../board/store.mjs";import{recordRefusal as Gs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as zs,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as it,shareReach as Vs,routeOfPath as Zs,sharedSidebar as Qs,shareTracker as Xs}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as Ys}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as qs,saveLinearKey as ea,savedClient as lt}from"./linear.mjs";import{SlackError as ta,slackStatus as na,removeSlackToken as sa,saveSlackToken as aa}from"./slack.mjs";import{sendReviews as ra}from"./review-send.mjs";const z=$.dirname(zt(import.meta.url)),oa=$.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"),W=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(W,"manifest.json"),ae=$.join(W,"logs"),ht=$.join(W,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[W,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=An();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 ns();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 ss();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=qn(),ia=ks(),re=Tn({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"),J=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function ca(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?Gt("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(ca,"killStale");function la(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: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(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
|
|
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
7
|
`).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l(ka,"runUpdate"),Ct.check(),setInterval(()=>Ct.check(),Us);const x=Ss({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 At=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)}">
|
package/hub/sessions.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function
|
|
2
|
-
${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!
|
|
1
|
+
var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Re(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Re,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Re(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Me=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Me}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),R=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let d=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],d=!0);continue}if(p.route!==c||h(n,p.route)||!R(i,p.route))continue;const u=`=${T(p.route)}:`,y=i(["send-keys","-t",u,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",u,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),d=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return d&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function M(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",d;try{d=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),d.stdout.destroy(),d.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{o+=l}),d.stderr.on("data",l=>{f+=l}),d.on("error",l=>{f+=l.message,c(1)}),d.on("close",l=>c(l??1)),d.on("exit",l=>setTimeout(()=>c(l??1),300)),d.unref()})}a(M,"runClaude");async function Ke(e,{cwd:s,run:t=M,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),o(new m(d.message,"failed"))}),f.on("close",d=>{i(),n({status:d??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${g.join(E,"bin","kickflip.mjs")} join ${L(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
|
|
2
|
+
${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!R(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy host. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=M,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=G(s,e);K(d,o);const c=g.resolve(t||d?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||d?.model,prompt:i}),u=await n(l,{cwd:c});if(u.status!==0){const w=(u.stderr||"").trim()||(u.stdout||"").trim();throw new m(w?`claude --bg failed (${u.status}): ${w.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(u.stdout);if(!y)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${u.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:y,confirmed:b,repo:Q(c),model:r||d?.model||null,name:p,argv:[C(),...l],output:u.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=M,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(R(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),d=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(d,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...d],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:f}){if(s)return{action:"none",entry:null};const d=new Date(f).toISOString(),c=e?{...e}:{down_since:d};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||f-Date.parse(c.resumed_at)>=Ue?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((d,c)=>he(d,{now:s.getTime(),running:c}),"listening"),resume:n=a((d,c)=>ze(d,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gateOf:f=a(d=>Se({now:s,dir:d.dir}),"gateOf")}={}){const d=t();if(!d)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of U(e)){const y=G(e,u.route),b=!!(y&&d.has(y.session)),w=i(u,d);let{action:j,entry:$}=Ze(h(c,u.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(u.route),counts:w?null:be(u),waiting:!w&&!b&&ge(u)!==null});if(j==="resume"&&f(u).state==="paused"){const I=h(c,u.route)?.mark||ke(u);Object.entries($e(u)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,u.route),re={listening:w,live:b,asked:r.has(u.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(u.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(u.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(u.route,$.mark),l.resumed.push(u.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${u.route}, or claude agents.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${u.title}'s session didn't come back`,body:I,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[u.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Me as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,M as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};
|
package/hub/try.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{mode:384}),
|
|
3
|
-
`)})}a(Et,"failure");function ot(t){return t.ending||(Kt(t.child,"SIGTERM"),t.ending=new Promise(e=>{const i=a(()=>{clearTimeout(s),clearInterval(r),e()},"finish");let s=setTimeout(()=>{Kt(t.child,"SIGKILL"),s=setTimeout(i,1e3)},4e3);const r=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{i()}},20)})),t.ending}a(ot,"terminate");async function _t(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await ot(t),await t.done}a(_t,"halt");async function Z(t,e=!1){await _t(t,e),X(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||M.delete(D(t.board,t.key))}a(Z,"stopRun");async function jt(t,e,i,s={}){if(!i)throw new x("This card has no valid worktree.");const r=Lt(i,t.build_target),n=fe(r);if(n)throw new x(n);if(!T.realpathSync(r).startsWith(T.realpathSync(i)+O.sep))throw new x("Build script must be inside the worktree.");const o=await C(i,"rev-parse","HEAD");if(W)throw new x("The hub is stopping. Try again after it restarts.",503);const{state:m}=N(t),S=kt(m,e),d={...F(e)?S:{},...s,sha:o,script:!0,state:"running",...F(e)?{at:ct(),head:o}:{}};delete d.port,delete d.https;const _=Wt(t,e);T.mkdirSync(O.dirname(_),{recursive:!0});const E=T.openSync(_,"w",384);T.fchmodSync(E,384);const $=ie(r,[e,i],{cwd:i,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});zt(m,e,d),A(t);const B=D(t,e),l={child:$,board:t,key:e,logFile:_,visited:c(),stopped:!1,port:null,ready:!1};u.set(B,l),M.set(B,o);let R,I;const q=a(P=>{I||=new Error(P),ot(l)},"fail");$.on("error",P=>{R=P}),$.stdout.on("data",P=>T.writeSync(E,P)),$.stderr.on("data",P=>T.writeSync(E,P));const L=Dt({input:$.stdout,crlfDelay:1/0});return L.on("line",P=>{const it=/^port (\d{1,5})$/.exec(P);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 At=a(async()=>{if(!(pt||l.stopped||l.ready||$.exitCode!==null||$.signalCode!==null)){pt=!0;try{if(await f(K)){if(l.stopped||I||$.exitCode!==null||$.signalCode!==null||(l.opening=Vt(l),await l.opening,l.stopped||I||$.exitCode!==null||$.signalCode!==null))return;l.ready=!0,l.visited=c(),m.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(()=>At().catch(ht=>q(ht.message)),250),At().catch(ht=>q(ht.message))}),l.deadline=setTimeout(()=>q("No port line within 60 minutes."),60*6e4),$.once("exit",()=>{ot(l)}),l.done=new Promise(P=>$.once("close",(it,K)=>{clearTimeout(l.deadline),clearInterval(l.poll),L.close(),T.closeSync(E),(async()=>{if(await ot(l),await Qt(l),l.finished=!0,l.stopped)return;const tt=e===J&&!l.port?new Error('Build script exited without printing "port <n>".'):null;I||R||it!==0||l.port&&!l.ready||tt?await Et(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 m.main.state,delete m.main.port,delete m.main.https):St(m,e),A(t)):(m.kind="app",X(t,e,{state:"ready"}))})().catch(tt=>w(`build /${t.route} ${e}: ${tt.message}`)).finally(()=>{u.get(B)===l&&u.delete(B),P()})})),w(`build /${t.route} ${e}: started; log ${_}`),{status:"started"}}a(jt,"launch");async function It(t,e,i){const s=D(t,"main");if(y.has(s))throw new x("A build is already running for main.",409);y.add(s);try{u.has(s)&&await Z(u.get(s),!0);const r=O.join(Nt.homedir(),".claude","kickflip","try",`${t.route}-main`);return T.mkdirSync(O.dirname(r),{recursive:!0}),gt(r)?await C(r,"checkout","--detach","--force",i.sha):(await C(e,"worktree","prune"),await C(e,"worktree","add","--detach",r,i.sha)),await jt(t,"main",r,i)}catch(r){throw r.status!==503&&(N(t).state.main={...i,state:"failed"},$t(t,"main",r),A(t)),r}finally{y.delete(s)}}a(It,"launchMain");async function ut(t,{manifest:e=z()}={}){if(await rt(e),lt(e),W)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new x("Send only the board route and card key.");const{route:i,key:s}=t;if(F(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(d=>d==="."||d==="..")))throw new x("Card key must be <wave>/<id>.");const r=U(e).find(d=>d.route===i);if(!r)throw new x(`/${i} is not a board.`,404);if(F(s)&&!r.waves.some(({slug:d,wave:_})=>_.items.some(E=>`${d}/${E.id}`===s)))throw new x("There is no item with this card key.",404);const n=await h();if(n)throw new x(n,409);const o=D(r,s),m=u.get(o);if(y.has(o)||m&&!m.ready&&!m.finished)throw new x(`A build is already running for ${s==="main"?"main":s===J?"the prototype site":"this card"}.`,409);if(s==="main"){const d=N(r).state.main;if(d?.state==="running")throw new x("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new x("Main has no failed build to retry.",409);const{sha:_,branch:E,slug:$,at:B,script:l}=d;return It(r,et(i,e).repo,{sha:_,branch:E,slug:$,at:B,script:l,...r.build_target?{target:r.build_target}:{}})}y.add(o);const S=g.delete(o);try{u.has(o)&&await Z(u.get(o),!0);const d=s===J?Ct(r):await Ut(r,s,e);return await jt(r,s,d)}catch(d){throw v.delete(o),S&&d.status!==503&&X(r,s,{state:void 0,sha:void 0}),d}finally{y.delete(o)}}a(ut,"start");const Ct=a(t=>{const e=ye(t.route);return gt(e)&&T.statSync(e).isDirectory()?T.realpathSync(e):""},"baseWorktree");async function Xt(t,e){const{state:i}=N(t),s=D(t,J),r=u.get(s);if(!((await me(t.dir)).length>0&&Ct(t))){r&&await Z(r),i.prototypes&&(i.prototypes=null,A(t));return}if(!(r||y.has(s)||i.prototypes?.state==="failed"||await h()))try{await ut({route:t.route,key:J},{manifest:e})}catch(o){if([409,503].includes(o.status))return;throw i.prototypes={state:"failed"},$t(t,J,o),A(t),o}}a(Xt,"serveBase");function Mt(t,e,{manifest:i=z()}={}){const s=U(i).find(n=>n.route===t);if(!s)return null;const r=[...u.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return r?{key:r.key,port:r.port}:null}a(Mt,"lookup");async function ft({manifest:t=z(),routes:e}={}){if(await rt(t),lt(t),W)return H;if(H&&e){if(!Q){const i={routes:new Set,manifest:t};i.done=H.catch(()=>{}).then(()=>(Q=null,ft({manifest:i.manifest,routes:[...i.routes]}))),Q=i}Q.manifest=t;for(const i of e)Q.routes.add(i);return Q.done}return H||(H=(async()=>{const i=U(t),s=st(oe,{}).prs||{};for(const r of[...u.values()])if(!i.some(n=>n.dir===r.board.dir)||F(r.key)&&r.ready&&c()-r.visited>=Ee){await Z(r);const{state:n}=N(r.board);St(n,r.key),A(r.board)}for(const r of i){if(W)return;if(e&&!e.includes(r.route))continue;const{state:n}=N(r),o=et(r.route,t),m=o.branch||"main",S=ae(o);for(const{slug:d,wave:_}of r.waves)for(const E of _.items){const $=`${d}/${E.id}`,B=ge(E,r.github||_.repo).map(l=>s[ve(l)]);if(!(E.lane==="done"||B.length&&B.every(l=>["MERGED","CLOSED"].includes(l?.state))))try{const l=D(r,$),R=await Ut(r,$,t);if(R){const L=await C(R,"rev-parse","HEAD");let P=0;try{P=Number(await C(R,"rev-list","--count",`${S}..HEAD`))}catch{}Object.assign(n.cards[$]||={},{head:L,ahead:P,script:gt(Lt(R,r.build_target))}),A(r)}else if(!g.has(l))continue;const I=n.cards[$];if((g.has(l)||r.auto_build&&I.script&&I.ahead>0&&(I.sha||M.get(l))!==I.head&&I.state!=="running")&&!y.has(l))try{await ut({route:r.route,key:$},{manifest:t})}catch(L){if([409,503].includes(L.status))continue;throw X(r,$,{sha:I.head}),await Et({board:r,key:$,logFile:$t(r,$,L)},1),L}}catch(l){w(`try /${r.route} ${$}: ${l.message}`)}}await Xt(r,t).catch(d=>w(`try /${r.route} prototypes: ${d.message}`));try{if(!o.repo||await h())continue;const d=await C(o.repo,"rev-parse",S);let _=!1;try{await C(o.repo,"cat-file","-e",`${d}:${Gt(r.build_target)}`),_=!0}catch{}if(!_){const I=u.get(D(r,"main"));I&&await Z(I),n.main=null,A(r);continue}const E=n.main;if(E?.state==="running"||E?.sha===d&&(E.target||null)===(r.build_target||null)&&(E.state==="failed"||E.state==="ready"&&(n.kind!=="web"||Mt(r.route,"main",{manifest:t}))))continue;const $=D(r,"main");if(y.has($))continue;const B=await C(o.repo,"log","-1","--format=%cI",d);let l=r.github;if(!l)try{l=Ft(await C(o.repo,"remote","get-url","origin"))}catch{}const R={sha:d,branch:m,slug:l||"",at:B,script:_,...r.build_target?{target:r.build_target}:{}};await It(r,o.repo,R)}catch(d){w(`try /${r.route} main: ${d.message}`)}}})().finally(()=>{H=null}),H)}a(ft,"tick");function Pt(t,e){if(G.has(t.dir))return G.get(t.dir);const i=(async()=>{const s=et(t.route,e),r=s.branch||"main",{state:n}=N(t);n.pull={...n.pull,running:!0},A(t);let o,m;try{o=await Ie(s.repo,r)}catch(S){m=S.message}if(o===null?delete n.pull:n.pull={at:ct(),...m?{error:m}:{}},A(t),o===null)return null;if(m)return w(`pull /${t.route}: ${m}`),{commits:0,error:m};if(o.commits){w(`pull /${t.route}: ${r} fast-forwarded ${o.commits} commit${o.commits===1?"":"s"} to ${o.to.slice(0,7)}`);try{await b(t,"main-pulled",{branch:r,...o})}catch(S){w(`pull /${t.route}: could not send main-pulled (${S.message})`)}}return{commits:o.commits}})().finally(()=>G.delete(t.dir));return G.set(t.dir,i),i}a(Pt,"pullMain");async function Zt({manifest:t=z(),routes:e}={}){await rt(t);const i=[];for(const s of U(t)){if(W)break;if(e&&!e.includes(s.route))continue;const r=et(s.route,t);!r.local||!r.repo||(await Pt(s,t))?.commits&&i.push(s.route)}return i}a(Zt,"pullMains");async function qt(t,{manifest:e=z()}={}){if(await rt(e),W)throw new x("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new x("Send only the board route.");const i=U(e).find(n=>n.route===t.route);if(!i)throw new x(`/${t.route} is not a board.`,404);const s=et(i.route,e);if(!s.local||!s.repo)throw new x("This board fetches its branch on every poll; only a local board pulls.",409);const r=await Pt(i,e);if(!r)throw new x("This board\u2019s folder has no origin to pull from.",409);return r.commits&&ft({manifest:e,routes:[i.route]}).catch(n=>w(`try /${i.route} main: ${n.message}`)),r}a(qt,"pull");async function te(){W=!0;const t=Promise.all([...u.values()].map(e=>e.ready?Z(e):_t(e)));await Promise.all([t,H,...G.values()])}return a(te,"stop"),{start:ut,tick:ft,stop:te,lookup:Mt,initialize:rt,pull:qt,pullMains:Zt,endUnshared:Yt}}a(je,"createTryManager");const vt=1e4,bt=a(async(c,...f)=>(await wt("git",["-C",c,...f],{encoding:"utf8",timeout:vt,env:le()})).stdout,"originGit");async function Ie(c,f,{run:w=bt}={}){const h=a(async(...p)=>(await w(...p)).trim(),"git"),b=a(p=>{throw new Error(`can\u2019t pull: ${p}`)},"refuse");try{await h(c,"config","--get","remote.origin.url")}catch{return null}try{await h(c,"fetch","--quiet","--no-tags","origin",`+refs/heads/${f}:refs/remotes/origin/${f}`)}catch(p){b(p.killed?`git fetch timed out after ${vt/1e3} s`:Ht(p.stderr,{repo:c}).error)}try{const p=await h(c,"rev-parse","--verify",`refs/heads/${f}^{commit}`),k=await h(c,"rev-parse","--verify",`refs/remotes/origin/${f}^{commit}`),u=a((v,j)=>h(c,"merge-base","--is-ancestor",v,j).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(p===k||await u(k,p))return{from:p,to:p,commits:0};await u(p,k)||b(`${f} has split from origin`);const y=Number(await h(c,"rev-list","--count",`${p}..${k}`)),g=(await h(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 Nt from"node:os";import mt from"node:http";import{execFile as ie,spawn as ne}from"node:child_process";import{createInterface as Dt}from"node:readline";import{promisify as oe}from"node:util";import{listBoards as U,readManifest as J,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 Ft,mainRefOf as le}from"./board-repo.mjs";import{gitReachFault as Ht,remoteFromInput as de,remoteGitEnv as ue}from"./repos.mjs";import{readiness as fe}from"./readiness.mjs";import{tailscaleBin as pe}from"../shared/tailscale.mjs";import{buildScriptError as he,buildScriptOf as Wt}from"../board/build-script.mjs";import{appendThread as me,recordedWorktree as we,servedRoundsOpen as ye,setItem as ge}from"../board/store.mjs";import{BASE_KEY as z,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),C=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"],ct={main:"main",[z]:"prototypes"},F=a(c=>!Object.hasOwn(ct,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 g=m?.repo;const $=(c.github||k?.repo||"").toLowerCase();if(!g&&$){const b=[c.owner?.repo,c.dir,...Object.values(h?.sites||{}).map(_=>_.owner?.repo)];for(const _ of new Set(b.filter(P=>typeof P=="string"&&T.isAbsolute(P))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await C(_,"remote","get-url","origin"))?.[1].toLowerCase()===$){g=await C(_,"rev-parse","--show-toplevel");break}}catch{}}if(!m?.path&&!g||m?.path&&!T.isAbsolute(u))return"";try{const b=[T.join(O.realpathSync(Nt.homedir()),".claude","kickflip","worktrees")];if(typeof g=="string"&&T.isAbsolute(g))try{b.push(T.join(O.realpathSync(g),".claude","worktrees"))}catch{}const _=T.resolve(g||"/",u),P=O.realpathSync(_);return b.some(W=>_.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,g=new Set,$=new Set,b=new Map,_=new Map,P=new Map,W=new Map;let G=!1,H=null,Q=null,ot=null;const lt=a(()=>new Date(c()).toISOString(),"stamp");function N(t){if(k.has(t.dir))return k.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=nt(e,{kind:null,main:null,cards:{}}),i={...yt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},s={state:i,file:e,written:JSON.stringify(r)};k.set(t.dir,s),i.main_port&&_.set(t.dir,i.main_port);const n=Object.entries(i.cards).filter(([,o])=>o.state==="running");for(const[o]of n)$.add(D(t,o));return i.kind==="web"?(i.cards=Object.fromEntries(n),i.main&&i.main.state!=="failed"&&(delete i.main.state,delete i.main.port,delete i.main.https)):i.main?.state==="running"&&(delete i.main.state,delete i.main.sha),r.prototypes?.state==="failed"&&(i.prototypes=r.prototypes),M(t),s}a(N,"stateOf");function M(t){const e=k.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Bt(e.file,e.state),e.written=r)}a(M,"persist");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),St=a((t,e)=>F(e)?t.cards[e]:t[ct[e]],"entryOf"),zt=a((t,e,r)=>{F(e)?t.cards[e]=r:t[ct[e]]=r},"setEntry"),kt=a((t,e)=>{F(e)?delete t.cards[e]:t[ct[e]]=null},"dropEntry");function X(t,e,r){const{state:i}=N(t),s=St(i,e);if(!s)return;const n=Object.hasOwn(r,"state")&&r.state!==s.state;Object.assign(s,r,F(e)&&n?{at:lt()}:{}),M(t)}a(X,"update");function dt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const i=nt(T.join(r.dir,"data","try.json"),{}).main_port;yt(i)&&_.set(r.dir,Number(i))}}a(dt,"reserveMains");function Yt(t){const{board:e,key:r}=t,i=D(e,r),s=r==="main"?_.get(e.dir):b.get(i);if(s)return s;const n=new Set([..._.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 r==="main"?(_.set(e.dir,o),N(e).state.main_port=o,M(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 r=await p(t),i=t.headers["tailscale-user-login"];return r!=="other"||!i||!Tt(e).includes(i)?r: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 r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(Vt,"endUnshared");async function Qt(t){t.https=Yt(t);const e=t.sockets=new Set,r=a(()=>{t.visited=c()},"visit"),i=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),s=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 y={login:n.headers["tailscale-user-login"],res:o};s.add(y),o.on("close",()=>s.delete(y))}const x=Object.fromEntries(Object.entries(n.headers).filter(([y])=>!Gt.includes(y))),d=mt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:x},y=>{o.writeHead(y.statusCode,y.headers),y.on("data",r),y.pipe(o),y.on("close",()=>{y.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=>{r(),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(D(t.board,t.key))}a(Xt,"closePort");async function Zt(){const t=[...u.values()].filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await rt(["serve","status","--json"])).stdout)}catch(r){h(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){const i=`http://127.0.0.1:${r.relay.address().port}`,s=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(s?.Handlers?.["/"]?.Proxy!==i){h(`try port ${r.https}: its serve mapping was ${s?"changed":"gone"}; serving it again`);try{await rt(["serve","--bg",`--https=${r.https}`,i])}catch(n){h(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(Zt,"reserve");function st(t=J()){if(ot)return ot;dt(t);for(const e of U(t))N(e);return ot=(async()=>{try{const{stdout:e}=await rt(["serve","status","--json"]),r=JSON.parse(e);for(const i of Object.keys(r?.TCP||{}))yt(i)&&![...u.values()].some(s=>s.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,r,i){const{board:s,key:n}=t;if(X(s,n,{state:"failed",port:void 0,https:void 0}),!F(n))return;const o=[];let w=!1;try{for await(const d of Dt({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||(N(s).state.kind||(s.waves.some(({wave:d})=>d.items.some(y=>y.install?.url))?"app":"web"))==="web"?"Serve":"Build";await me(s.dir,n,{text:[`${x} failed (exit ${e??1})`,i?.message,r&&`Stopped by ${r}.`,w&&"The login keychain must be unlocked on the host.",...o].filter(Boolean).join(`
|
|
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(`
|
|
4
4
|
|
|
5
|
-
`).map(
|
|
6
|
-
`)).find(
|
|
7
|
-
`).at(-1))}}a(
|
|
8
|
-
`).map(u=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(u)).filter(Boolean).map(u=>[u[2],u[1]])),k=
|
|
5
|
+
`).map(b=>b.split(`
|
|
6
|
+
`)).find(b=>b.includes(`branch refs/heads/${f}`))?.[0].slice(9);return $?(await m($,"status","--porcelain","--untracked-files=no")&&S("local changes"),await m($,"merge","--ff-only","--quiet",k)):await m(c,"update-ref",`refs/heads/${f}`,k,p),{from:p,to:k,commits:g}}catch(p){if(p.message.startsWith("can\u2019t pull: "))throw p;S(String(p.stderr||p.message).trim().split(`
|
|
7
|
+
`).at(-1))}}a(Pe,"pullBranch");async function tr({manifest:c=J(),log:f=console.log,routes:h}={}){const m=h?nt(Rt,{}):{};for(const S of U(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(),g=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($){f(`fetch /${S.route} after ${Date.now()-g} ms: ${$.message}`);let b=S.github;if(!b&&!S.remote)try{b=Ft(await C(p.repo,"remote","get-url","origin"))}catch{}m[S.route]={at:u,ok:!1,...Ht($.killed?`git fetch timed out after ${vt/1e3} s`:$.stderr,{github:b||"",remote:S.remote,repo:p.repo})};continue}if(S.remote)try{await Ae(S,p.repo,k,f)}catch($){f(`pushed branches /${S.route}: ${$.message}`)}}Bt(Rt,m)}a(tr,"fetchTryBranches");async function Ae(c,f,h,m){const S=c.waves.flatMap(({slug:u,wave:g})=>g.items.filter($=>$.pushed?.branch&&!be($)&&$.lane!=="done").map($=>({key:`${u}/${$.id}`,branch:$.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 g=p.get(u.branch);if(!g)try{g=await C(f,"rev-parse","--verify","--quiet",`refs/heads/${u.branch}^{commit}`)}catch{}let $=!1;if(g)try{await C(f,"merge-base","--is-ancestor",g,`refs/remotes/origin/${h}`),$=!0}catch{}if(!(!$&&p.has(u.branch)))try{await ge(c.dir,u.key,{done:!0,force:!0,text:$?`${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/usage.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`);
|
|
3
|
-
`),
|
|
1
|
+
var k=Object.defineProperty;var u=(e,t)=>k(e,"name",{value:t,configurable:!0});import d from"node:fs";import w from"node:os";import f from"node:path";import{audit as v,hubAudit as D}from"../shared/audit.mjs";import{FOREVER as M}from"./duration.mjs";const b=6,L=12,$=180,I={five_hour:5*36e5,seven_day:7*864e5},S=u(e=>{try{return JSON.parse(d.readFileSync(e,"utf8"))}catch{return null}},"readJson"),_=u(e=>e==null||e===""||!Number.isFinite(Number(e))?null:Number(e),"num"),y=u(e=>{if(e==null||e==="")return null;const n=typeof e=="number"||/^\d+(\.\d+)?$/.test(String(e))?Number(e)>1e12?Number(e):Number(e)*1e3:Date.parse(String(e));return Number.isFinite(new Date(n).getTime())?new Date(n).toISOString():null},"isoOf"),h=u((e,t)=>({used_percentage:_(e),resets_at:y(t)}),"win");function x(e,t,n){let s;try{s=d.openSync(e,"r");const r=d.fstatSync(s).size;for(const o of[256*1024,2*1024*1024]){const a=Math.max(0,r-o),c=Buffer.alloc(r-a);d.readSync(s,c,0,c.length,a);const m=c.toString("utf8").split(`
|
|
2
|
+
`);a>0&&m.shift();for(let i=m.length-1;i>=0;i--)if(m[i].includes(t))try{const l=n(JSON.parse(m[i]));if(l)return l}catch{}if(a===0)break}}catch{}finally{if(s!==void 0)try{d.closeSync(s)}catch{}}return null}u(x,"lastMatch");const z=u(e=>x(e,"rate_limits",t=>{const n=t?.payload?.rate_limits??t?.rate_limits;return n?.primary?{rl:n}:null}),"lastRateLimits"),O=u(e=>x(e,'"rate_limit_event"',t=>t?.type==="rate_limit_event"&&t.rate_limit_info?t.rate_limit_info:null),"lastRateLimitEvent");function A(e){const t=u(n=>{const s=e.unifiedWindows?.[n];if(!s||_(s.utilization)==null)return null;const r=s.status==="rejected"||e.status==="rejected"&&e.rateLimitType===n;return h(r?100:Math.round(_(s.utilization)*1e3)/10,s.resetsAt)},"window");return{five_hour:t("five_hour"),seven_day:t("seven_day")}}u(A,"eventWindows");function q(e,t=Date.now()){const n=O(e);if(n?.status!=="rejected")return null;const s=Object.values(n.unifiedWindows||{}).find(o=>o?.status==="rejected"),r=y(s?.resetsAt??n.resetsAt);return r&&Date.parse(r)>t?r:null}u(q,"runLimit");const R=/-(\d{4}-\d\d-\d\dT\d\d)-(\d\d)-(\d\d)-(\d{3})Z\.log$/;function E(e,t){let n;try{n=d.readdirSync(e)}catch{return[]}const s=[];for(const r of n){const o=r.match(R);if(o)try{const a=d.statSync(f.join(e,r));s.push({file:f.join(e,r),started:`${o[1]}:${o[2]}:${o[3]}.${o[4]}Z`,mtime:a.mtimeMs,size:a.size})}catch{}}return s.sort((r,o)=>o.mtime-r.mtime).slice(0,L).map(r=>{const o=`${r.size}:${r.mtime}`;let a=t.get(r.file);if(a?.key!==o){const c=r.size?O(r.file):null;a={key:o,claude:r.size>0&&F(r.file)==="{",windows:c?A(c):null},t.set(r.file,a)}return{...r,claude:a.claude,windows:a.windows}})}u(E,"runLogs");function F(e){let t;try{t=d.openSync(e,"r");const n=Buffer.alloc(1);return d.readSync(t,n,0,1,0),n.toString()}catch{return null}finally{if(t!==void 0)try{d.closeSync(t)}catch{}}}u(F,"firstByte");function C(e,t){const n=t.filter(o=>o.windows).map(o=>({at:new Date(o.mtime).toISOString(),...o.windows}));if(e&&typeof e=="object"&&!Array.isArray(e)&&n.push(e),!n.length)return null;const s=u(o=>{const a=n.map(i=>i[o]&&h(i[o].used_percentage,i[o].resets_at)).filter(i=>i?.used_percentage!=null),c=Math.max(...a.map(i=>i.resets_at?Date.parse(i.resets_at):-1/0));return a.filter(i=>i.resets_at?Math.abs(Date.parse(i.resets_at)-c)<=$*1e3:c===-1/0).reduce((i,l)=>!i||l.used_percentage>i.used_percentage?l:i,null)},"window");return{at:n.map(o=>y(o.at)).filter(Boolean).sort().at(-1)??null,five_hour:s("five_hour"),seven_day:s("seven_day")}}u(C,"claudeReading");function T(e,t){const n=e.filter(s=>s.claude).sort((s,r)=>r.started.localeCompare(s.started))[0];return n?{at:n.started,reported:!!n.windows||!!(t&&Date.parse(t)>Date.parse(n.started))}:null}u(T,"newestStart");function W(e){const t=[],n=u(s=>{try{return d.readdirSync(s).sort().reverse()}catch{return[]}},"kids");e:for(const s of n(e))for(const r of n(f.join(e,s)))for(const o of n(f.join(e,s,r))){const a=f.join(e,s,r,o);for(const c of n(a))if(/^rollout-.*\.jsonl$/.test(c))try{t.push({file:f.join(a,c),mtime:d.statSync(f.join(a,c)).mtimeMs})}catch{}if(t.length>=b*2)break e}return t.sort((s,r)=>r.mtime-s.mtime).slice(0,b)}u(W,"newestRollouts");function B(e){const t=[];for(const{file:i,mtime:l}of W(e)){const p=z(i);p&&t.push({...p,mtime:l,resets:_(p.rl.primary.resets_at)})}if(!t.length)return null;const n=Math.max(...t.map(i=>i.resets??-1/0)),s=t.filter(i=>i.resets!=null&&Math.abs(i.resets-n)<=$),r=u(i=>s.reduce((l,p)=>(_(p.rl[i]?.used_percent)??-1)>(_(l?.rl[i]?.used_percent)??-1)?p:l,null),"pick");if(!s.length)return null;const o=r("primary"),a=r("secondary"),c=s.reduce((i,l)=>l.mtime>i.mtime?l:i),m=u((i,l)=>i?h(i.rl[l].used_percent,i.rl[l].resets_at):null,"w");return{five_hour:m(o,"primary"),seven_day:m(a,"secondary"),at:new Date(c.mtime).toISOString()}}u(B,"scanCodex");function N(e,t){if(!e||typeof e!="object"||Array.isArray(e))return null;const n=y(e.at),s=u(r=>{const o=e[r];if(!o)return null;const a=h(o.used_percentage,o.resets_at);if(a.used_percentage==null)return null;const c=!n||t-Date.parse(n)>I[r]?"stale":a.resets_at&&t>=Date.parse(a.resets_at)?"reset":"live";return{...a,used_percentage:c==="reset"?0:a.used_percentage,state:c}},"window");return{at:n,five_hour:s("five_hour"),seven_day:s("seven_day")}}u(N,"readingOf");function H({home:e=w.homedir(),now:t=Date.now,cacheMs:n=6e4}={}){const s=f.join(e,".codex","sessions"),r=f.join(e,".claude","kickflip","logs"),o=new Map;let a=null,c=-1/0;return({codexShare:m="off"}={})=>{const i=t(),l=S(f.join(e,".claude","kickflip","usage.json"))?.claude,p=E(r,o),g=N(C(l,p),i),j=T(p,y(l?.at));return m==="off"?{claude:g,codex:null,start:j}:(i-c>=n&&(a=B(s),c=i),{claude:g,codex:N(a||{at:null},i),start:j})}}u(H,"usageReader");const J=95,K=u((e,t)=>e==="deaf"&&t?.percent>=J,"contextFull");function Q(e,{home:t=w.homedir()}={}){if(typeof e!="string"||!/^[\w-]+$/.test(e))return null;const n=S(f.join(t,".claude","kickflip","context",`${e}.json`));return _(n?.percent)==null||!n.at?null:{percent:_(n.percent),at:n.at}}u(Q,"contextReading");async function Y({gate:e,home:t=w.homedir(),resume:n}){const s=f.join(t,".claude","kickflip","usage-gate-state.json"),r=S(s);if(e.state==="paused"&&r?.state!=="paused"){const a=e.manual?`The owner paused the hub ${e.resets_at===M?"until they resume it":`until ${e.resets_at}`}.`:`Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage reached the ${e.pause_at}% line.`;v(D(t),{call:"usage-pause",result:"paused",reason:a,inputs:{gate:e,previous:r?.state??null}})}if(r?.state==="paused"&&e.state==="normal"){const a=r.manual?"The owner's pause of the hub ended, so every board was told.":`Claude's ${r.window==="five_hour"?"5-hour":"weekly"} usage is under the line again, so every board was told.`;v(D(t),{call:"usage-resume",result:"resumed",reason:a,inputs:{gate:e,previous:r.manual?"manual":r.window}}),await n(r.manual?"manual":r.window)}d.mkdirSync(f.dirname(s),{recursive:!0});const o=`${s}.${process.pid}.tmp`;d.writeFileSync(o,JSON.stringify(e)+`
|
|
3
|
+
`),d.renameSync(o,s)}u(Y,"checkUsagePause");export{J as CONTEXT_FULL,Y as checkUsagePause,K as contextFull,Q as contextReading,q as runLimit,H as usageReader};
|
package/package.json
CHANGED
package/shared/card-prs.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var i=Object.defineProperty;var
|
|
1
|
+
var i=Object.defineProperty;var n=(e,r)=>i(e,"name",{value:r,configurable:!0});const s=/^(?:([\w.-]+\/[\w.-]+)#)?([1-9]\d*)$/,o=n(e=>Array.isArray(e?.prs)?e.prs:Number.isInteger(e?.pr)?[{number:e.pr}]:[],"storedPRs"),l=n((e,r)=>o(e).map(t=>({repo:t.repo||r,number:t.number,...t.merged_at?{merged_at:t.merged_at}:{},...t.kept?{kept:t.kept}:{}})).filter(t=>t.repo),"cardPRs"),m=n(e=>o(e).length>0,"hasPR"),u=n(({repo:e,number:r})=>`${e}#${r}`,"prKeyOf"),d=n(({repo:e,number:r})=>`https://github.com/${e}/pull/${r}`,"prURL"),x=n((e,r)=>e.map(t=>t.repo&&t.repo!==r?u(t):`#${t.number}`).join(", "),"prNames");function g(e){const r=s.exec(String(e).trim());return r?{...r[1]?{repo:r[1]}:{},number:Number(r[2])}:null}n(g,"parsePRRef");const a=["MERGED","CLOSED"],c=n(e=>e.checks?.state==="fail"||!!e.failing?.length,"failing");function p(e){return e?.state?a.includes(e.state)?5:e.isDraft?1:c(e)?2:e.reviewDecision==="APPROVED"?4:3:0}n(p,"readiness");function R(e){if(!e.length)return-1;const r=e.map(p);return r.every(t=>t===5)?Math.max(0,e.findIndex(t=>t.state==="MERGED")):r.indexOf(Math.min(...r))}n(R,"leastReady");const h=n(e=>e.length>0&&e.every(r=>a.includes(r?.state))&&e.some(r=>r.state==="MERGED"),"allLanded");export{h as allLanded,l as cardPRs,m as hasPR,R as leastReady,g as parsePRRef,u as prKeyOf,x as prNames,d as prURL,o as storedPRs};
|
package/shared/machine-gate.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var k=Object.defineProperty;var s=(e,t)=>k(e,"name",{value:t,configurable:!0});import i from"node:fs";import c from"node:os";import _ from"node:path";import{execFileSync as
|
|
2
|
-
`),i.renameSync(x,m),$}s(z,"checkMachine");export{
|
|
1
|
+
var k=Object.defineProperty;var s=(e,t)=>k(e,"name",{value:t,configurable:!0});import i from"node:fs";import c from"node:os";import _ from"node:path";import{execFileSync as v}from"node:child_process";import{audit as d,hubAudit as y}from"./audit.mjs";const h=4,f=3,A=10*6e4,M=Object.freeze({state:"normal",reason:null}),D={1:"normal",2:"warn",4:"critical"},F=s(e=>D[e]??null,"pressureOf");function N(){let e=null;try{e=Number(v("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim())}catch{}return{load:c.loadavg()[0],cores:c.cpus().length,pressure:F(e)}}s(N,"readMachine");function R(e,{load:t,cores:r,pressure:o}){const a=(e==="busy"?f:h)*r;return o==="critical"||t>=a?"busy":"normal"}s(R,"machineState");const b=s((e=c.homedir())=>_.join(e,".claude","kickflip","machine-state.json"),"machineFile"),g=s(e=>{try{return JSON.parse(i.readFileSync(b(e),"utf8"))}catch{return null}},"readFile");function T(e=c.homedir()){const t=g(e);return t?.state==="busy"?{state:"busy",reason:t.reason}:M}s(T,"readMachineState");function z({home:e=c.homedir(),reading:t=N(),now:r=new Date}={}){const o=g(e),a=o?.state??"normal",n=R(a,t),O=n==="busy"?t.pressure==="critical"?"memory pressure critical":"load high":null,l={reading:t,previous:o?.state??null,hold:h,release:f},u=`1-minute load ${t.load.toFixed(1)}`,p=n!==a,S=!o?.logged_at||r.getTime()-Date.parse(o.logged_at)>=A;if(p&&n==="busy"){const E=t.pressure==="critical"?"memory pressure is critical":`${u} is at least ${h}\xD7 its ${t.cores} cores`;d(y(e),{call:"machine-busy",result:n,reason:`The host is busy: ${E}, so no new work starts.`,inputs:l},{now:r})}else p?d(y(e),{call:"machine-eased",result:n,reason:`Memory pressure is ${t.pressure??"unread"} and ${u} is under ${f}\xD7 its ${t.cores} cores, so new work starts again.`,inputs:l},{now:r}):S&&d(y(e),{call:"machine-reading",result:n,reason:`Memory pressure is ${t.pressure??"unread"} and ${u} on ${t.cores} cores.`,inputs:l},{now:r});const $={state:n,reason:O,at:r.toISOString(),reading:t,logged_at:p||S?r.toISOString():o.logged_at},m=b(e);i.mkdirSync(_.dirname(m),{recursive:!0});const x=`${m}.${process.pid}.tmp`;return i.writeFileSync(x,JSON.stringify($)+`
|
|
2
|
+
`),i.renameSync(x,m),$}s(z,"checkMachine");export{h as HOLD_LOAD,M as MACHINE_NORMAL,f as RELEASE_LOAD,z as checkMachine,b as machineFile,R as machineState,F as pressureOf,N as readMachine,T as readMachineState};
|
package/shared/tailscale.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=Object.defineProperty;var
|
|
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 e from"node:path";import{spawnSync as d}from"node:child_process";const f=e.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 r({env:t=process.env,platform:s=process.platform,apps:n=["/Applications",e.join(u.homedir(),"Applications")]}={}){const l=(t.PATH||"").split(e.delimiter).filter(Boolean).map(o=>e.join(o,"tailscale"));return s==="darwin"&&l.push(...n.map(o=>e.join(o,f))),[...new Set(l)].filter(m)}a(r,"tailscaleCandidates");function h(t){const s=d(t,["status","--json"],{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]});try{return JSON.parse(s.stdout).BackendState==="Running"}catch{return!1}}a(h,"daemonRunning");function y({running:t=h,...s}={}){const n=r(s);return n.length>1?n.find(t)??n[0]:n[0]??null}a(y,"tailscaleBin");function A(t,s={}){const n=r(s);return n.includes(t)?n.slice(0,n.indexOf(t)):[]}a(A,"tailscaleSkipped");const c=a((t=process.platform)=>t==="darwin"?"brew install --cask tailscale-app":"curl -fsSL https://tailscale.com/install.sh | sh","tailscaleInstallCommand"),C=a((t=process.platform)=>t==="darwin"?`install Tailscale from https://tailscale.com/download, or ${c(t)}`:c(t),"tailscaleInstallHint");export{h as daemonRunning,y as tailscaleBin,r as tailscaleCandidates,c as tailscaleInstallCommand,C as tailscaleInstallHint,A as tailscaleSkipped};
|