@lovek4d/kickflip 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,28 @@
1
- var Q=Object.defineProperty;var s=(t,e)=>Q(t,"name",{value:e,configurable:!0});import d from"node:fs";import p from"node:path";import T from"node:os";import{randomUUID as X}from"node:crypto";import{spawn as z,spawnSync as q}from"node:child_process";import{fileURLToPath as V}from"node:url";import{BoardError as Z,opensPRs as tt,settingsOf as et}from"./store.mjs";import{boardAt as nt,hubCode as rt,listBoards as ot,readJSON as it,readManifest as st,samePath as at}from"../hub/boards.mjs";import{boardRepo as ct}from"../hub/board-repo.mjs";import{cloneRoot as lt}from"../shared/repos.mjs";import{nodeBin as O,storedPath as x}from"../bin/install.mjs";import{hubService as ft}from"../shared/service.mjs";const dt=p.join(p.dirname(V(import.meta.url)),"..","bin","kickflip.mjs"),C=lt,L=s(()=>p.join(T.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),m=s(t=>{throw new Z(t)},"fail");function c(t,...e){const n=q("git",["-C",t,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:n.status===0,out:(n.stdout||"").trim(),err:(n.stderr||n.error?.message||"").trim().split(`
2
- `)[0]}}s(c,"git");const W=s((t,e)=>c(t,"rev-parse","--short",e).out,"short");function A(t,{repo:e=C(),base:n="main",remote:r=!1,busy:o=[]}={}){const i=p.resolve(e);c(i,"rev-parse","--git-dir").ok||m(`--repo ${i} is not a git repo`),c(i,"check-ref-format","--branch",t).ok||m(`--worktree: "${t}" can't be a branch name`),c(i,"check-ref-format","--branch",n).ok||m("--worktree: invalid base branch");const a=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(i,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${a}`),c(i,"rev-parse","--verify","--quiet",a).ok||m(`--worktree: ${i} has no ${n} branch`);const u=p.join(L(),t),$=d.existsSync(u)?ut(i,u,t,{ref:a,base:n,busy:o}):null;c(i,"rev-parse","--verify","--quiet",`refs/heads/${t}`).ok&&m(`--worktree: branch ${t} already exists in ${i}`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(i,"worktree","add","--quiet","-b",t,u,a);return h.ok||m(`--worktree: git worktree add failed: ${h.err}`),{path:x(u),branch:t,repo:x(i),remade:$}}s(A,"makeWorktree");function ut(t,e,n,{ref:r,base:o,busy:i}){const a=s(l=>m(`--worktree: ${e} ${l}; land or remove it first`),"refuse");i.includes(x(e))&&a("already exists and another open builder record uses it");const u=d.realpathSync(e);c(t,"worktree","list","--porcelain").out.split(`
1
+ var Q=Object.defineProperty;var a=(t,e)=>Q(t,"name",{value:e,configurable:!0});import u from"node:fs";import $ from"node:path";import R from"node:os";import{randomUUID as X}from"node:crypto";import{spawn as z,spawnSync as C}from"node:child_process";import{fileURLToPath as V}from"node:url";import{BoardError as Z,opensPRs as tt,settingsOf as et}from"./store.mjs";import{boardAt as nt,hubCode as rt,listBoards as ot,readJSON as it,readManifest as st,samePath as at}from"../hub/boards.mjs";import{boardRepo as ct}from"../hub/board-repo.mjs";import{cloneRoot as lt}from"../shared/repos.mjs";import{nodeBin as O,storedPath as j}from"../bin/install.mjs";import{hubService as ft}from"../shared/service.mjs";import{subjectFault as dt}from"./commit-message.mjs";const ut=$.join($.dirname(V(import.meta.url)),"..","bin","kickflip.mjs"),T=lt,L=a(()=>$.join(R.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),m=a(t=>{throw new Z(t)},"fail");function c(t,...e){const n=C("git",["-C",t,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:n.status===0,out:(n.stdout||"").trim(),err:(n.stderr||n.error?.message||"").trim().split(`
2
+ `)[0]}}a(c,"git");const W=a((t,e)=>c(t,"rev-parse","--short",e).out,"short");function A(t,{repo:e=T(),base:n="main",remote:r=!1,busy:o=[]}={}){const i=$.resolve(e);c(i,"rev-parse","--git-dir").ok||m(`--repo ${i} is not a git repo`),c(i,"check-ref-format","--branch",t).ok||m(`--worktree: "${t}" can't be a branch name`),c(i,"check-ref-format","--branch",n).ok||m("--worktree: invalid base branch");const s=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(i,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${s}`),c(i,"rev-parse","--verify","--quiet",s).ok||m(`--worktree: ${i} has no ${n} branch`);const d=$.join(L(),t),h=u.existsSync(d)?mt(i,d,t,{ref:s,base:n,busy:o}):null;c(i,"rev-parse","--verify","--quiet",`refs/heads/${t}`).ok&&m(`--worktree: branch ${t} already exists in ${i}`),u.mkdirSync($.dirname(d),{recursive:!0});const p=c(i,"worktree","add","--quiet","-b",t,d,s);return p.ok||m(`--worktree: git worktree add failed: ${p.err}`),{path:j(d),branch:t,repo:j(i),remade:h}}a(A,"makeWorktree");function mt(t,e,n,{ref:r,base:o,busy:i}){const s=a(l=>m(`--worktree: ${e} ${l}; land or remove it first`),"refuse");i.includes(j(e))&&s("already exists and another open builder record uses it");const d=u.realpathSync(e);c(t,"worktree","list","--porcelain").out.split(`
3
3
 
4
4
  `).some(l=>{const f=l.split(`
5
- `),v=f.find(b=>b.startsWith("worktree "))?.slice(9);try{return f.includes(`branch refs/heads/${n}`)&&d.realpathSync(v)===u}catch{return!1}})||a("already exists");const h=Number(c(t,"rev-list","--count",`${r}..refs/heads/${n}`).out),k=c(e,"status","--porcelain").out.split(`
6
- `).filter(Boolean).length,g=s((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...h?[`${g(h,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&a(`holds ${w.join(" and ")}`);const S=c(t,"worktree","remove","--force",e);S.ok||m(`--worktree: could not remove the leftover ${e}: ${S.err}`);const y=c(t,"branch","--quiet","-D",n);return y.ok||m(`--worktree: could not delete the leftover branch ${n}: ${y.err}`),`Remade the leftover worktree ${x(e)} from ${o}: it held no commits or changes.`}s(ut,"removeLeftover");function P(t,{repo:e,manifest:n=st()}={}){const r=t?nt(t,n):null;if(!r)return{repo:e??C(),base:"main",remote:!1};const o=ct(r.route,n),i=e??(o.repo||m(`/${r.route} has no folder recorded for its session; rejoin it from its repo folder`)),a=tt(o,et(it(p.join(r.dir,"board.json"),{}),o));return{repo:i,base:o.branch||"main",remote:!o.local||a}}s(P,"builderRepo");const Pt=s((t,e,{repo:n,busy:r}={})=>A(e,{...P(t,{repo:n}),busy:r}),"boardWorktree"),Jt=s((t,e,{repo:n,base:r,busy:o})=>A(`${t}-${e}`,{repo:n,base:r,busy:o}),"helperWorktree");function Bt(t,e){const n=[];if(c(t.path,"merge-base","--is-ancestor",e.branch,"HEAD").ok)n.push(`${e.branch} is already merged`);else{const o=c(t.path,"merge","--no-edit",e.branch);if(!o.ok){const i=c(t.path,"diff","--name-only","--diff-filter=U").out.split(`
7
- `).filter(Boolean);m(i.length?`merging ${e.branch} conflicts in ${i.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${o.err}`)}n.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(d.existsSync(e.path)){const o=c(t.path,"worktree","remove",e.path);o.ok||m(`merged, but could not remove ${e.path}: ${o.err}`),n.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&n.push(`branch deleted: ${e.branch}`),{lines:n}}s(Bt,"mergeHelper");function Kt(t,e,n){const{repo:r}=P(t),o=p.resolve(r);c(o,"check-ref-format","--branch",n).ok||m(`"${n}" can't be a branch name`);const i=`refs/remotes/origin/${n}`,a=c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`);a.ok||m(`could not fetch ${n} from origin: ${a.err}`);const u=p.join(L(),e);d.existsSync(u)&&m(`${x(u)} already exists; land or remove it first`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(o,"rev-parse","--verify","--quiet",`refs/heads/${n}`).ok?c(o,"worktree","add","--quiet",u,n):c(o,"worktree","add","--quiet","--track","-b",n,u,i);return h.ok||m(`git worktree add failed: ${h.err}`),{path:x(u),branch:n,repo:x(o),remade:null}}s(Kt,"prWorktree");function mt(t){const e=[],n=s(r=>{for(const o of d.readdirSync(r,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?n(p.join(r,o.name)):o.name.endsWith(".test.mjs")&&e.push(p.join(r,o.name)))},"walk");return n(t),e.sort()}s(mt,"testFiles");function J(t,{limit:e=900*1e3,testLimit:n=300*1e3}={}){const r=mt(t);if(!r.length)return Promise.resolve({ok:!0,pass:0,fail:0});const{NODE_TEST_CONTEXT:o,...i}=process.env,a=z(O(),["--test","--test-reporter=tap",`--test-timeout=${n}`,...r],{cwd:t,env:i,detached:!0});a.once("exit",()=>{try{process.kill(-a.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"&&g.code!=="EPERM")throw g}});let u="",$="";a.stdout.setEncoding("utf8").on("data",g=>{u+=g}),a.stderr.setEncoding("utf8").on("data",g=>{$+=g});let h=null;const k=setTimeout(()=>{h=q("ps",["-A","-o","pgid=,pid=,args="],{encoding:"utf8"}).stdout.split(`
8
- `).filter(w=>Number(w.trim().split(/\s+/)[0])===a.pid).map(w=>w.trim()),h.length&&process.kill(-a.pid,"SIGKILL");const g=s(()=>{a.stdout.destroy(),a.stderr.destroy()},"release");a.exitCode===null&&a.signalCode===null?a.once("exit",g):g()},e);return new Promise(g=>a.on("close",(w,S)=>{clearTimeout(k);const y=s(j=>Number(u.match(new RegExp(`^# ${j} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:w===0,pass:y("pass"),fail:y("fail")};if(l.ok){g(l);return}const f=p.join(T.homedir(),".claude","kickflip","logs");try{d.mkdirSync(f,{recursive:!0});const j=p.join(f,`land-${X()}.tap`);d.writeFileSync(j,u+$+(h?`
5
+ `),v=f.find(x=>x.startsWith("worktree "))?.slice(9);try{return f.includes(`branch refs/heads/${n}`)&&u.realpathSync(v)===d}catch{return!1}})||s("already exists");const p=Number(c(t,"rev-list","--count",`${r}..refs/heads/${n}`).out),k=c(e,"status","--porcelain").out.split(`
6
+ `).filter(Boolean).length,g=a((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...p?[`${g(p,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&s(`holds ${w.join(" and ")}`);const S=c(t,"worktree","remove","--force",e);S.ok||m(`--worktree: could not remove the leftover ${e}: ${S.err}`);const y=c(t,"branch","--quiet","-D",n);return y.ok||m(`--worktree: could not delete the leftover branch ${n}: ${y.err}`),`Remade the leftover worktree ${j(e)} from ${o}: it held no commits or changes.`}a(mt,"removeLeftover");function P(t,{repo:e,manifest:n=st()}={}){const r=t?nt(t,n):null;if(!r)return{repo:e??T(),base:"main",remote:!1};const o=ct(r.route,n),i=e??(o.repo||m(`/${r.route} has no folder recorded for its session; rejoin it from its repo folder`)),s=tt(o,et(it($.join(r.dir,"board.json"),{}),o));return{repo:i,base:o.branch||"main",remote:!o.local||s}}a(P,"builderRepo");const Bt=a((t,e,{repo:n,busy:r}={})=>A(e,{...P(t,{repo:n}),busy:r}),"boardWorktree"),Kt=a((t,e,{repo:n,base:r,busy:o})=>A(`${t}-${e}`,{repo:n,base:r,busy:o}),"helperWorktree");function Mt(t,e,n){const r=[];if(c(t.path,"merge-base","--is-ancestor",e.branch,"HEAD").ok)r.push(`${e.branch} is already merged`);else{const i=c(t.path,"merge","--no-edit","-m",`chore: merge the ${e.branch} helper branch
7
+
8
+ Refs: ${n}`,e.branch);if(!i.ok){const s=c(t.path,"diff","--name-only","--diff-filter=U").out.split(`
9
+ `).filter(Boolean);m(s.length?`merging ${e.branch} conflicts in ${s.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${i.err}`)}r.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(u.existsSync(e.path)){const i=c(t.path,"worktree","remove",e.path);i.ok||m(`merged, but could not remove ${e.path}: ${i.err}`),r.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&r.push(`branch deleted: ${e.branch}`),{lines:r}}a(Mt,"mergeHelper");function _t(t,e,n){const{repo:r}=P(t),o=$.resolve(r);c(o,"check-ref-format","--branch",n).ok||m(`"${n}" can't be a branch name`);const i=`refs/remotes/origin/${n}`,s=c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`);s.ok||m(`could not fetch ${n} from origin: ${s.err}`);const d=$.join(L(),e);u.existsSync(d)&&m(`${j(d)} already exists; land or remove it first`),u.mkdirSync($.dirname(d),{recursive:!0});const p=c(o,"rev-parse","--verify","--quiet",`refs/heads/${n}`).ok?c(o,"worktree","add","--quiet",d,n):c(o,"worktree","add","--quiet","--track","-b",n,d,i);return p.ok||m(`git worktree add failed: ${p.err}`),{path:j(d),branch:n,repo:j(o),remade:null}}a(_t,"prWorktree");function ht(t){const e=[],n=a(r=>{for(const o of u.readdirSync(r,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?n($.join(r,o.name)):o.name.endsWith(".test.mjs")&&e.push($.join(r,o.name)))},"walk");return n(t),e.sort()}a(ht,"testFiles");function J(t,{limit:e=900*1e3,testLimit:n=300*1e3}={}){const r=ht(t);if(!r.length)return Promise.resolve({ok:!0,pass:0,fail:0});const{NODE_TEST_CONTEXT:o,...i}=process.env,s=z(O(),["--test","--test-reporter=tap",`--test-timeout=${n}`,...r],{cwd:t,env:i,detached:!0});s.once("exit",()=>{try{process.kill(-s.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"&&g.code!=="EPERM")throw g}});let d="",h="";s.stdout.setEncoding("utf8").on("data",g=>{d+=g}),s.stderr.setEncoding("utf8").on("data",g=>{h+=g});let p=null;const k=setTimeout(()=>{p=C("ps",["-A","-o","pgid=,pid=,args="],{encoding:"utf8"}).stdout.split(`
10
+ `).filter(w=>Number(w.trim().split(/\s+/)[0])===s.pid).map(w=>w.trim()),p.length&&process.kill(-s.pid,"SIGKILL");const g=a(()=>{s.stdout.destroy(),s.stderr.destroy()},"release");s.exitCode===null&&s.signalCode===null?s.once("exit",g):g()},e);return new Promise(g=>s.on("close",(w,S)=>{clearTimeout(k);const y=a(b=>Number(d.match(new RegExp(`^# ${b} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:w===0,pass:y("pass"),fail:y("fail")};if(l.ok){g(l);return}const f=$.join(R.homedir(),".claude","kickflip","logs");try{u.mkdirSync(f,{recursive:!0});const b=$.join(f,`land-${X()}.tap`);u.writeFileSync(b,d+h+(p?`
9
11
  # processes still running at the cutoff
10
- ${h.join(`
12
+ ${p.join(`
11
13
  `)}
12
- `:""),{mode:384}),l.log=j}catch(j){l.logError=`Could not save the full test output: ${j.message}`}const v=h?.filter(j=>Number(j.split(/\s+/)[1])!==a.pid);h&&(l.hung={after:ht(e),files:r.filter(j=>v.some(G=>G.includes(j))).map(j=>p.relative(t,j))}),l.failures=pt(u);const b=$.trim().split(`
13
- `)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${b?`: ${b}`:""}`,g(l)}))}s(J,"nodeTests");const ht=s(t=>t%6e4?`${t/1e3} seconds`:`${t/6e4} minutes`,"duration");function pt(t){const e=t.split(`
14
- `),n=[],r=[],o=s(i=>i.startsWith("'")?i.slice(1,-1).replaceAll("''","'"):i.startsWith('"')?JSON.parse(i):i,"scalar");for(let i=0;i<e.length;i++){const a=/^(\s*)# Subtest: (.*)$/.exec(e[i]);if(a){const l=a[1].length;for(;n.length&&n.at(-1).indent>=l;)n.pop();n.push({indent:l,name:a[2]})}const u=/^(\s*)not ok \d+ - (.*)$/.exec(e[i]);if(!u||/ # (?:TODO|SKIP)\b/i.test(u[2]))continue;const $=u[1].length,h=[...n.filter(l=>l.indent<$).map(l=>l.name),u[2]].join(" > "),k=[];for(let l=i+1;l<e.length&&e[l].startsWith(" ".repeat($+2));l++)k.push(e[l]);const g=s(l=>k.find(f=>f.startsWith(`${" ".repeat($+2)}${l}: `))?.slice($+l.length+4),"field"),w=g("location"),S=g("error");let y=[];if(S&&/^[|>]/.test(S)){const l=k.findIndex(f=>f.startsWith(`${" ".repeat($+2)}error: `))+1;for(let f=l;f<k.length&&k[f].startsWith(" ".repeat($+4));f++)y.push(k[f].slice($+4))}else S&&(y=o(S).split(`
15
- `));if(y[0]==="test failed"){const l=[];let f=i-1;for(e[f]?.startsWith("# Subtest: ")&&f--;f>=0&&e[f].startsWith("# ")&&!e[f].startsWith("# Subtest: ");f--)l.unshift(e[f].slice(2));const v=l.findIndex(b=>/^\w*Error(?: \[[^\]]+\])?:/.test(b));v>=0&&(y=l.slice(v))}r.push(`${h}
14
+ `:""),{mode:384}),l.log=b}catch(b){l.logError=`Could not save the full test output: ${b.message}`}const v=p?.filter(b=>Number(b.split(/\s+/)[1])!==s.pid);p&&(l.hung={after:pt(e),files:r.filter(b=>v.some(G=>G.includes(b))).map(b=>$.relative(t,b))}),l.failures=$t(d);const x=h.trim().split(`
15
+ `)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${x?`: ${x}`:""}`,g(l)}))}a(J,"nodeTests");const pt=a(t=>t%6e4?`${t/1e3} seconds`:`${t/6e4} minutes`,"duration");function $t(t){const e=t.split(`
16
+ `),n=[],r=[],o=a(i=>i.startsWith("'")?i.slice(1,-1).replaceAll("''","'"):i.startsWith('"')?JSON.parse(i):i,"scalar");for(let i=0;i<e.length;i++){const s=/^(\s*)# Subtest: (.*)$/.exec(e[i]);if(s){const l=s[1].length;for(;n.length&&n.at(-1).indent>=l;)n.pop();n.push({indent:l,name:s[2]})}const d=/^(\s*)not ok \d+ - (.*)$/.exec(e[i]);if(!d||/ # (?:TODO|SKIP)\b/i.test(d[2]))continue;const h=d[1].length,p=[...n.filter(l=>l.indent<h).map(l=>l.name),d[2]].join(" > "),k=[];for(let l=i+1;l<e.length&&e[l].startsWith(" ".repeat(h+2));l++)k.push(e[l]);const g=a(l=>k.find(f=>f.startsWith(`${" ".repeat(h+2)}${l}: `))?.slice(h+l.length+4),"field"),w=g("location"),S=g("error");let y=[];if(S&&/^[|>]/.test(S)){const l=k.findIndex(f=>f.startsWith(`${" ".repeat(h+2)}error: `))+1;for(let f=l;f<k.length&&k[f].startsWith(" ".repeat(h+4));f++)y.push(k[f].slice(h+4))}else S&&(y=o(S).split(`
17
+ `));if(y[0]==="test failed"){const l=[];let f=i-1;for(e[f]?.startsWith("# Subtest: ")&&f--;f>=0&&e[f].startsWith("# ")&&!e[f].startsWith("# Subtest: ");f--)l.unshift(e[f].slice(2));const v=l.findIndex(x=>/^\w*Error(?: \[[^\]]+\])?:/.test(x));v>=0&&(y=l.slice(v))}r.push(`${p}
16
18
  ${w?o(w):"file not reported by test runner"}${y.length?`
17
19
  `+y.slice(0,3).map(l=>` ${l}`).join(`
18
- `):""}`)}return r}s(pt,"tapFailures");function $t(t,e){let n;if(process.env.KICKFLIP_RESTART_CMD){try{n=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(n)||!n.length||!n.every(o=>typeof o=="string"))&&m("KICKFLIP_RESTART_CMD must be a JSON array of strings"),n=[...n,t,...e?[e]:[]]}else{if(t==="hub")return ft().restart();n=[O(),dt,"restart",e]}const r=q(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
19
- `)[0]}s($t,"restartRunner");const N=s(()=>p.join(T.homedir(),".claude","kickflip"),"kickflipDir"),D=s(()=>p.join(N(),"landing"),"landing"),E=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"alive"),gt=5e3;async function B(t){const e=[];let n;try{for(const r of t)try{e.push({file:r,ino:d.statSync(r).ino,watcher:d.watch(r)})}catch{return}await new Promise(r=>{for(const{watcher:o}of e)o.on("change",r).on("error",r);n=setTimeout(r,gt),setImmediate(()=>setImmediate(()=>{for(const{file:o,ino:i}of e){let a=null;try{a=d.statSync(o).ino}catch{}a!==i&&r()}}))})}finally{clearTimeout(n);for(const{watcher:r}of e)r.close()}}s(B,"untilGone");const I=s(t=>p.join(N(),`${t}.queue`),"queueDir"),K=s(t=>Number(t.split("-")[2]),"ticketPid"),kt=s((t,e)=>{try{return JSON.parse(d.readFileSync(p.join(t,e),"utf8"))}catch{return{}}},"readTicket"),M=s((t,{first:e})=>`${e?`0${String(1e15-Date.parse(e)).padStart(16,"0")}`:"1"}-${t}`,"rank");function R(t){const e=I(t);let n=[];try{n=d.readdirSync(e).filter(r=>!r.endsWith(".tmp"))}catch{}return n.filter(r=>E(K(r))).map(r=>({ticket:r,...kt(e,r)})).sort((r,o)=>M(r.ticket,r)<M(o.ticket,o)?-1:1)}s(R,"queuedAsks");const _=s((t,e,n)=>{const r=p.join(t,`${e}.tmp`);d.writeFileSync(r,JSON.stringify(n)),d.renameSync(r,p.join(t,e))},"writeTicket");let yt=0;async function wt(t,e,n,{onWait:r,asked:o=Date.now(),site:i=null,key:a=null,first:u=null}={}){const $=p.join(N(),`${t}.lock`),h=I(t);d.mkdirSync(h,{recursive:!0});const k=`${String(o).padStart(15,"0")}-${String(yt++).padStart(6,"0")}-${process.pid}`;_(h,k,{card:e,site:i,key:a,first:u});let g=null;try{for(;;){const w=R(t),S=w.findIndex(v=>v.ticket===k),y=w.slice(0,S);let l=null;if(!y.length)try{d.writeFileSync($,JSON.stringify({card:e,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(v){if(v.code!=="EEXIST")throw v;try{l=JSON.parse(d.readFileSync($,"utf8"))}catch{}if(!(Number.isInteger(l?.pid)&&E(l.pid))){d.rmSync($,{force:!0});continue}}const f=l?.card??(y.length?`the ${t} ahead`:null);f&&f!==g&&(g=f,await r?.(f)),await B([y.length?p.join(h,y.at(-1).ticket):$,p.join(h,k)])}}finally{d.rmSync(p.join(h,k),{force:!0})}try{return await n()}finally{try{JSON.parse(d.readFileSync($,"utf8")).pid===process.pid&&d.rmSync($,{force:!0})}catch{}}}s(wt,"holdingQueuedLock");const St=s((t,e,n)=>wt("land",t,e,n),"holdingLandLock");function Mt(t,e,n){const r=I("land"),o=R("land").filter(i=>i.key===e&&i.site&&at(i.site,t));for(const{ticket:i,...a}of o)_(r,i,{...a,first:n});return o.length}s(Mt,"moveQueuedLand");function _t(){const t=vt();return[...t?[{card:t.card,running:!0,first:null}]:[],...R("land").map(({card:e,first:n})=>({card:e,running:!1,first:n??null}))]}s(_t,"landQueue");function vt(){try{const t=JSON.parse(d.readFileSync(p.join(N(),"land.lock"),"utf8"));return Number.isInteger(t?.pid)&&E(t.pid)?t:null}catch{return null}}s(vt,"landLock");async function Ht(t,e,{merge:n=!1}={}){const r=D();d.mkdirSync(r,{recursive:!0});const o=p.join(r,encodeURIComponent(t));let i=!1;for(;;){if(i&&!d.existsSync(o))return{joined:!0};try{d.writeFileSync(o,JSON.stringify({pid:process.pid,at:new Date().toISOString(),...n?{merge:n}:{}}),{flag:"wx"});break}catch(a){if(a.code!=="EEXIST")throw a;let u=null;try{u=JSON.parse(d.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(u?.pid)&&E(u.pid))){d.rmSync(o,{force:!0});continue}i=!0,await B([o])}}try{return await e()}finally{d.rmSync(o,{force:!0})}}s(Ht,"joiningLand");function Ut(t){try{const e=JSON.parse(d.readFileSync(p.join(D(),encodeURIComponent(t)),"utf8"));return Number.isInteger(e?.pid)&&E(e.pid)?e:null}catch{return null}}s(Ut,"runningLand");function Yt(t,e){const n=p.join(D(),encodeURIComponent(t));try{e.includes(JSON.parse(d.readFileSync(n,"utf8")).pid)&&d.rmSync(n,{force:!0})}catch{}let r=[];try{r=d.readdirSync(I("land"))}catch{}for(const o of r.filter(i=>e.includes(K(i))))d.rmSync(p.join(I("land"),o),{force:!0})}s(Yt,"clearStoppedLand");function jt(t,e,n){const o=c(t,"diff","--name-only","--no-renames","--diff-filter=A",e,n,"--","docs/adr").out.split(`
20
- `).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&m(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${p.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}s(jt,"refuseNumberedAdr");function xt(t,e,n,r){const o=c(t,"log","--format=%h",`${e}..${n}`).out.split(`
21
- `).filter(Boolean);for(const i of o){const a=c(t,"log","-1","--format=%s",i).out;r&&!a.startsWith(`${r}: `)&&m(`land: commit ${i} "${a}" doesn't start with "${r}: "; reword it and land again`),/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${a}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}s(xt,"refuseBadMessages");function Gt(t,e){const n=c(t,"fetch","--quiet","--no-tags","origin",`+refs/heads/${e}:refs/remotes/origin/${e}`);if(n.ok||m(`could not fetch ${e} from origin into ${t}: ${n.err}`),c(t,"symbolic-ref","--quiet","--short","HEAD").out!==e)return;const r=c(t,"merge","--quiet","--ff-only",`refs/remotes/origin/${e}`);r.ok||m(`${e} in ${t} doesn't fast-forward to origin's (${r.err}); bring it level with origin/${e} and land again`)}s(Gt,"syncBase");async function bt(t,{say:e=console.log,runTests:n=J,outcome:r}){const o=await n(t),i=o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[];o.hung&&m([`tests hung past ${o.hung.after} in ${o.hung.files.join(", ")||"the test runner"}; ${r}`,...i].join(`
20
+ `):""}`)}return r}a($t,"tapFailures");function gt(t,e){let n;if(process.env.KICKFLIP_RESTART_CMD){try{n=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(n)||!n.length||!n.every(o=>typeof o=="string"))&&m("KICKFLIP_RESTART_CMD must be a JSON array of strings"),n=[...n,t,...e?[e]:[]]}else{if(t==="hub")return ft().restart();n=[O(),ut,"restart",e]}const r=C(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
21
+ `)[0]}a(gt,"restartRunner");const N=a(()=>$.join(R.homedir(),".claude","kickflip"),"kickflipDir"),q=a(()=>$.join(N(),"landing"),"landing"),E=a(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"alive"),kt=5e3;async function B(t){const e=[];let n;try{for(const r of t)try{e.push({file:r,ino:u.statSync(r).ino,watcher:u.watch(r)})}catch{return}await new Promise(r=>{for(const{watcher:o}of e)o.on("change",r).on("error",r);n=setTimeout(r,kt),setImmediate(()=>setImmediate(()=>{for(const{file:o,ino:i}of e){let s=null;try{s=u.statSync(o).ino}catch{}s!==i&&r()}}))})}finally{clearTimeout(n);for(const{watcher:r}of e)r.close()}}a(B,"untilGone");const I=a(t=>$.join(N(),`${t}.queue`),"queueDir"),K=a(t=>Number(t.split("-")[2]),"ticketPid"),yt=a((t,e)=>{try{return JSON.parse(u.readFileSync($.join(t,e),"utf8"))}catch{return{}}},"readTicket"),M=a((t,{first:e})=>`${e?`0${String(1e15-Date.parse(e)).padStart(16,"0")}`:"1"}-${t}`,"rank");function D(t){const e=I(t);let n=[];try{n=u.readdirSync(e).filter(r=>!r.endsWith(".tmp"))}catch{}return n.filter(r=>E(K(r))).map(r=>({ticket:r,...yt(e,r)})).sort((r,o)=>M(r.ticket,r)<M(o.ticket,o)?-1:1)}a(D,"queuedAsks");const _=a((t,e,n)=>{const r=$.join(t,`${e}.tmp`);u.writeFileSync(r,JSON.stringify(n)),u.renameSync(r,$.join(t,e))},"writeTicket");let wt=0;async function St(t,e,n,{onWait:r,asked:o=Date.now(),site:i=null,key:s=null,first:d=null}={}){const h=$.join(N(),`${t}.lock`),p=I(t);u.mkdirSync(p,{recursive:!0});const k=`${String(o).padStart(15,"0")}-${String(wt++).padStart(6,"0")}-${process.pid}`;_(p,k,{card:e,site:i,key:s,first:d});let g=null;try{for(;;){const w=D(t),S=w.findIndex(v=>v.ticket===k),y=w.slice(0,S);let l=null;if(!y.length)try{u.writeFileSync(h,JSON.stringify({card:e,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(v){if(v.code!=="EEXIST")throw v;try{l=JSON.parse(u.readFileSync(h,"utf8"))}catch{}if(!(Number.isInteger(l?.pid)&&E(l.pid))){u.rmSync(h,{force:!0});continue}}const f=l?.card??(y.length?`the ${t} ahead`:null);f&&f!==g&&(g=f,await r?.(f)),await B([y.length?$.join(p,y.at(-1).ticket):h,$.join(p,k)])}}finally{u.rmSync($.join(p,k),{force:!0})}try{return await n()}finally{try{JSON.parse(u.readFileSync(h,"utf8")).pid===process.pid&&u.rmSync(h,{force:!0})}catch{}}}a(St,"holdingQueuedLock");const vt=a((t,e,n)=>St("land",t,e,n),"holdingLandLock");function Ht(t,e,n){const r=I("land"),o=D("land").filter(i=>i.key===e&&i.site&&at(i.site,t));for(const{ticket:i,...s}of o)_(r,i,{...s,first:n});return o.length}a(Ht,"moveQueuedLand");function Ut(){const t=bt();return[...t?[{card:t.card,running:!0,first:null}]:[],...D("land").map(({card:e,first:n})=>({card:e,running:!1,first:n??null}))]}a(Ut,"landQueue");function bt(){try{const t=JSON.parse(u.readFileSync($.join(N(),"land.lock"),"utf8"));return Number.isInteger(t?.pid)&&E(t.pid)?t:null}catch{return null}}a(bt,"landLock");async function Yt(t,e,{merge:n=!1}={}){const r=q();u.mkdirSync(r,{recursive:!0});const o=$.join(r,encodeURIComponent(t));let i=!1;for(;;){if(i&&!u.existsSync(o))return{joined:!0};try{u.writeFileSync(o,JSON.stringify({pid:process.pid,at:new Date().toISOString(),...n?{merge:n}:{}}),{flag:"wx"});break}catch(s){if(s.code!=="EEXIST")throw s;let d=null;try{d=JSON.parse(u.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(d?.pid)&&E(d.pid))){u.rmSync(o,{force:!0});continue}i=!0,await B([o])}}try{return await e()}finally{u.rmSync(o,{force:!0})}}a(Yt,"joiningLand");function Gt(t){try{const e=JSON.parse(u.readFileSync($.join(q(),encodeURIComponent(t)),"utf8"));return Number.isInteger(e?.pid)&&E(e.pid)?e:null}catch{return null}}a(Gt,"runningLand");function Qt(t,e){const n=$.join(q(),encodeURIComponent(t));try{e.includes(JSON.parse(u.readFileSync(n,"utf8")).pid)&&u.rmSync(n,{force:!0})}catch{}let r=[];try{r=u.readdirSync(I("land"))}catch{}for(const o of r.filter(i=>e.includes(K(i))))u.rmSync($.join(I("land"),o),{force:!0})}a(Qt,"clearStoppedLand");function jt(t,e,n){const o=c(t,"diff","--name-only","--no-renames","--diff-filter=A",e,n,"--","docs/adr").out.split(`
22
+ `).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&m(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${$.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}a(jt,"refuseNumberedAdr");function xt(t,e,n,r){const o=c(t,"log","--format=%h",`${e}..${n}`).out.split(`
23
+ `).filter(Boolean);for(const i of o){const s=c(t,"log","-1","--format=%s",i).out;/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${s}" still holds a "# Conflicts:" note in its message; amend it out and land again`);const d=r&&dt(s);d&&m(`land: commit ${i} "${s}" ${d}; reword it to the commit standard and land again`);const h=c(t,"log","-1","--format=%(trailers:key=Refs,valueonly,separator=%x00)",i).out.split("\0");r&&!h.includes(r)&&m(`land: commit ${i} "${s}" has no "Refs: ${r}" trailer; add it above Co-Authored-By and land again`)}}a(xt,"refuseBadMessages");function Xt(t,e){const n=c(t,"fetch","--quiet","--no-tags","origin",`+refs/heads/${e}:refs/remotes/origin/${e}`);if(n.ok||m(`could not fetch ${e} from origin into ${t}: ${n.err}`),c(t,"symbolic-ref","--quiet","--short","HEAD").out!==e)return;const r=c(t,"merge","--quiet","--ff-only",`refs/remotes/origin/${e}`);r.ok||m(`${e} in ${t} doesn't fast-forward to origin's (${r.err}); bring it level with origin/${e} and land again`)}a(Xt,"syncBase");async function Et(t,{say:e=console.log,runTests:n=J,outcome:r}){const o=await n(t),i=o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[];o.hung&&m([`tests hung past ${o.hung.after} in ${o.hung.files.join(", ")||"the test runner"}; ${r}`,...i].join(`
22
24
  `)),o.ok||m([`tests failed in ${t} (${o.fail} failing); ${r}`,...o.failures?.length?o.failures:[o.error||"test runner failed without test diagnostics"],...i].join(`
23
- `)),e(`tests passed: ${o.pass}`)}s(bt,"passTests");async function H({path:t,branch:e,repo:n,base:r,ref:o},{say:i=console.log,runTests:a=J}={}){if(d.existsSync(t)||m(`no worktree at ${t}`),U(n,r),c(t,"status","--porcelain").out&&m(`${t} has uncommitted changes; commit them there first`),Number(c(n,"rev-list","--count",`${r}..${e}`).out)===0&&m(`${e} has no commits beyond ${r}; nothing to land`),!c(n,"merge-base","--is-ancestor",r,e).ok){const h=c(t,"rebase","--quiet",r);if(!h.ok){const k=c(t,"diff","--name-only","--diff-filter=U").out.split(`
24
- `).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${h.err}`),m(`rebasing ${e} onto ${r} conflicts in ${k.join(", ")}; resolve it in ${t} and land again`)}i(`rebased ${e} onto ${r} (${W(n,r)})`)}const u=c(n,"log","--format=%h %s",`${r}..${e}`).out.split(`
25
- `).filter(h=>h&&!h.includes(" "));u.length&&m(`${e} has a commit with an empty subject (${u[0]}); reword it in ${t} and land again`),jt(n,r,e),xt(n,r,e,o);const $=c(n,"rev-parse",e).out;return c(n,"rev-parse","--verify","--quiet",F(e)).out===$?(i(`${e} was tested on ${r} as it is now; not testing it again`),$):(await bt(t,{say:i,runTests:a,outcome:"nothing landed"}),c(n,"update-ref",F(e),$),$)}s(H,"prepareLand");const F=s(t=>`refs/kickflip/tested/${t}`,"testedRef");function U(t,e){const n=c(t,"symbolic-ref","--quiet","--short","HEAD").out;n!==e&&m(`${t} is on ${n||"a detached HEAD"}, not ${e}; switch it to ${e} (git -C ${t} switch ${e}) and land again`)}s(U,"onBase");async function Qt(t,{hold:e=!1,onWait:n,sync:r=s(()=>{},"sync"),queue:o={},...i}={}){const{say:a=console.log}=i,u=Date.now();for(;;){const $=await H(t,i);if(e||await St(t.ref??t.branch,()=>(r(),Y(t,$,i)),{onWait:n,asked:u,...o}))return;a(`${t.base} moved while ${t.branch} was tested; testing it on ${t.base} again`)}}s(Qt,"landInTurn");async function Xt(t,{hold:e=!1,...n}={}){const r=await H(t,n);e||Y(t,r,n)||m(`${t.base} moved while ${t.branch} was tested; land again`)}s(Xt,"land");function Y({path:t,branch:e,repo:n,base:r},o,{say:i=console.log,restart:a=$t,boards:u=s(()=>ot(),"boards"),clone:$=C()}={}){if(U(n,r),c(n,"rev-parse",e).out!==o||!c(n,"merge-base","--is-ancestor",r,o).ok)return!1;const h=c(n,"diff","--name-only",r,e).out.split(`
26
- `).filter(Boolean),k=c(n,"merge","--quiet","--ff-only",o);k.ok||m(`could not fast-forward ${r} in ${n}: ${k.err}`),i(`${r} fast-forwarded to ${W(n,r)} in ${n}`);const g=[],w=d.realpathSync(n)===d.realpathSync($)?h.filter(f=>!/\.test\.mjs$/.test(f)):[],S=new Set(w.length?rt(p.join(n,"hub","hub.mjs")).files:[]);if(w.some(f=>f.startsWith("hub/")||S.has(f))){const f=a("hub");f?g.push(`the hub didn't restart (${f})`):i("hub restarted, with every site")}else if(w.some(f=>f.startsWith("board/")))for(const f of u()){const v=a("board",f.route);v?g.push(`/${f.route} didn't restart (${v})`):i(`board restarted: ${f.route}`)}const y=c(n,"worktree","remove",t);y.ok?i(`worktree removed: ${t}`):g.push(`could not remove ${t} (${y.err})`);const l=c(n,"branch","--quiet","-d",e);return l.ok?i(`branch deleted: ${e}`):g.push(`could not delete branch ${e} (${l.err})`),c(n,"update-ref","-d",F(e)),g.length&&m(`landed, but ${g.join("; ")}`),!0}s(Y,"landPrepared");export{Pt as boardWorktree,P as builderRepo,Yt as clearStoppedLand,c as git,Jt as helperWorktree,St as holdingLandLock,Ht as joiningLand,Xt as land,Qt as landInTurn,vt as landLock,Y as landPrepared,_t as landQueue,A as makeWorktree,Bt as mergeHelper,Mt as moveQueuedLand,J as nodeTests,bt as passTests,Kt as prWorktree,H as prepareLand,$t as restartRunner,Ut as runningLand,C as skillRepo,Gt as syncBase,L as worktreesDir};
25
+ `)),e(`tests passed: ${o.pass}`)}a(Et,"passTests");async function H({path:t,branch:e,repo:n,base:r,ref:o},{say:i=console.log,runTests:s=J}={}){if(u.existsSync(t)||m(`no worktree at ${t}`),U(n,r),c(t,"status","--porcelain").out&&m(`${t} has uncommitted changes; commit them there first`),Number(c(n,"rev-list","--count",`${r}..${e}`).out)===0&&m(`${e} has no commits beyond ${r}; nothing to land`),!c(n,"merge-base","--is-ancestor",r,e).ok){const p=c(t,"rebase","--quiet",r);if(!p.ok){const k=c(t,"diff","--name-only","--diff-filter=U").out.split(`
26
+ `).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${p.err}`),m(`rebasing ${e} onto ${r} conflicts in ${k.join(", ")}; resolve it in ${t} and land again`)}i(`rebased ${e} onto ${r} (${W(n,r)})`)}const d=c(n,"log","--format=%h %s",`${r}..${e}`).out.split(`
27
+ `).filter(p=>p&&!p.includes(" "));d.length&&m(`${e} has a commit with an empty subject (${d[0]}); reword it in ${t} and land again`),jt(n,r,e),xt(n,r,e,o);const h=c(n,"rev-parse",e).out;return c(n,"rev-parse","--verify","--quiet",F(e)).out===h?(i(`${e} was tested on ${r} as it is now; not testing it again`),h):(await Et(t,{say:i,runTests:s,outcome:"nothing landed"}),c(n,"update-ref",F(e),h),h)}a(H,"prepareLand");const F=a(t=>`refs/kickflip/tested/${t}`,"testedRef");function U(t,e){const n=c(t,"symbolic-ref","--quiet","--short","HEAD").out;n!==e&&m(`${t} is on ${n||"a detached HEAD"}, not ${e}; switch it to ${e} (git -C ${t} switch ${e}) and land again`)}a(U,"onBase");async function zt(t,{hold:e=!1,onWait:n,sync:r=a(()=>{},"sync"),queue:o={},...i}={}){const{say:s=console.log}=i,d=Date.now();for(;;){const h=await H(t,i);if(e||await vt(t.ref??t.branch,()=>(r(),Y(t,h,i)),{onWait:n,asked:d,...o}))return;s(`${t.base} moved while ${t.branch} was tested; testing it on ${t.base} again`)}}a(zt,"landInTurn");async function Vt(t,{hold:e=!1,...n}={}){const r=await H(t,n);e||Y(t,r,n)||m(`${t.base} moved while ${t.branch} was tested; land again`)}a(Vt,"land");function Y({path:t,branch:e,repo:n,base:r},o,{say:i=console.log,restart:s=gt,boards:d=a(()=>ot(),"boards"),clone:h=T()}={}){if(U(n,r),c(n,"rev-parse",e).out!==o||!c(n,"merge-base","--is-ancestor",r,o).ok)return!1;const p=c(n,"diff","--name-only",r,e).out.split(`
28
+ `).filter(Boolean),k=c(n,"merge","--quiet","--ff-only",o);k.ok||m(`could not fast-forward ${r} in ${n}: ${k.err}`),i(`${r} fast-forwarded to ${W(n,r)} in ${n}`);const g=[],w=u.realpathSync(n)===u.realpathSync(h)?p.filter(f=>!/\.test\.mjs$/.test(f)):[],S=new Set(w.length?rt($.join(n,"hub","hub.mjs")).files:[]);if(w.some(f=>f.startsWith("hub/")||S.has(f))){const f=s("hub");f?g.push(`the hub didn't restart (${f})`):i("hub restarted, with every site")}else if(w.some(f=>f.startsWith("board/")))for(const f of d()){const v=s("board",f.route);v?g.push(`/${f.route} didn't restart (${v})`):i(`board restarted: ${f.route}`)}const y=c(n,"worktree","remove",t);y.ok?i(`worktree removed: ${t}`):g.push(`could not remove ${t} (${y.err})`);const l=c(n,"branch","--quiet","-d",e);return l.ok?i(`branch deleted: ${e}`):g.push(`could not delete branch ${e} (${l.err})`),c(n,"update-ref","-d",F(e)),g.length&&m(`landed, but ${g.join("; ")}`),!0}a(Y,"landPrepared");export{Bt as boardWorktree,P as builderRepo,Qt as clearStoppedLand,c as git,Kt as helperWorktree,vt as holdingLandLock,Yt as joiningLand,Vt as land,zt as landInTurn,bt as landLock,Y as landPrepared,Ut as landQueue,A as makeWorktree,Mt as mergeHelper,Ht as moveQueuedLand,J as nodeTests,Et as passTests,_t as prWorktree,H as prepareLand,gt as restartRunner,Gt as runningLand,T as skillRepo,Xt as syncBase,L as worktreesDir};
@@ -7,7 +7,6 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { readUsageGate, pauseReason, pauseUntilWords } from '../../shared/usage-gate.mjs';
10
- import { FOREVER } from '../../hub/duration.mjs';
11
10
 
12
11
  function boardDirOf(session) {
13
12
  try {
@@ -27,8 +26,7 @@ try {
27
26
  } }));
28
27
  } else if (['SessionStart', 'UserPromptSubmit'].includes(input.hook_event_name)) {
29
28
  const why = gate.manual ? MANUAL[gate.manual] : `Claude's usage is past the pause line (${gate.pause_at}%)`;
30
- const answer = gate.resets_at === FOREVER ? '"paused until you resume"' : `"paused until <that time, in the owner's local time>"`;
31
- if (gate.state === 'paused') console.log(`${why}. Running steps finish; start no new work, builders or agents ${pauseUntilWords(gate.resets_at)}; answer the owner's messages briefly with ${answer}.`);
29
+ if (gate.state === 'paused') console.log(`${why}. Running steps finish; builders and agents are held ${pauseUntilWords(gate.resets_at)}; keep sorting, readying, screening and answering.`);
32
30
  else if (gate.state === 'going') console.log(`Keep going is on: work runs past the pause line until ${gate.resets_at}, spending usage.`);
33
31
  }
34
32
  } catch {}
package/hub/readiness.mjs CHANGED
@@ -1 +1 @@
1
- var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as A,tailscaleInstallCommand as D}from"../shared/tailscale.mjs";const H={slack:"Reviewer DMs and digests wait on this."},h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack",null],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const d=s((r,e)=>{switch(r){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=D(a)),n?{ready:!1,missing:n,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const E=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=y({home:t,env:i,now:a,cacheMs:d}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:E(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:A({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&P({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??H[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(F,"readinessReader");const G=y(),Y=F({probes:{codex:G}});export{G as readCodex,Y as readiness,F as readinessReader,k as readinessRows};
1
+ var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as R}from"../shared/gh.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as N}from"./linear.mjs";import{slackStatus as P}from"./slack.mjs";import{readSettings as B,subscriptions as I}from"./push.mjs";import{tailscaleState as A}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const O={slack:"Reviewer DMs and digests wait on this."},y=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack",null],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],E="Can't read replies in the review threads: add im:history to the Slack app, reinstall it and paste its new token.",L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const d=s((r,e)=>{switch(r){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key",...e.unheard?{note:E}:{}}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=H(a)),n?{ready:!1,missing:n,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return y.filter(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const F=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function G({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=h({home:t,env:i,now:a,cacheMs:d}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>N(t),"linear"),slack:s(()=>P(t),"slack"),tailscale:s(()=>A({run:F(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:B(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&R({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(y.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??O[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(G,"readinessReader");const U=h(),Z=G({probes:{codex:U}});export{U as readCodex,Z as readiness,G as readinessReader,k as readinessRows};
@@ -1 +1 @@
1
- var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub \u203A Integrations \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function L({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(L,"sendReviews");export{L as sendReviews};
1
+ var R=Object.defineProperty;var o=(t,r)=>R(t,"name",{value:r,configurable:!0});import _ from"node:fs";import N from"node:path";import{appendEvent as x,listBoards as D}from"./boards.mjs";import{savedSlack as H,slackStatus as M,refusalUnpushed as P,markRefusalPushed as T,missingScope as g,setHearing as k,hearingUnpushed as j,markHearingPushed as q,slackUserId as B}from"./slack.mjs";import{deliver as $}from"./push.mjs";import{reviewDue as I,queuePushes as U,postLink as A,postReply as E,recordReaction as W,postDigest as C,openDayThread as F,heardThreads as J,markHeard as K,boardPosts as L,ANSWER_MARK as O}from"../board/store.mjs";const v=o(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function z(t,r,p){await U(t.dir);const n=await I(t.dir,{now:p});if(n.waiting)return{sent:[],failed:[]};const h=[],c=[],s=o(async(e,a)=>{try{await a(),h.push(e)}catch(w){if(v(w))throw w;c.push(`${t.route}: ${e}: ${w.message}`)}},"attempt");for(const e of(n.digest?.reviewers||[]).filter(a=>a.owed))await s(`digest to ${e.name}`,async()=>{await C(t.dir,n.digest.date,e,{channel:e.sent?.channel||await r.openDM(e.slack_user),header:o(a=>`${t.title} digest: ${a} PR${a===1?"":"s"} pending your review`,"header"),post:o(a=>r.post(a),"post")})});const d=new Map,i=o((e,a)=>(d.has(e)||d.set(e,(async()=>F(t.dir,n.date,e,{channel:await a(),header:`${t.title} reviews`,post:o(w=>r.post(w),"post")}))()),d.get(e)),"dayThread");for(const e of n.links)await s(`link to ${e.reviewer} on ${e.pr}`,async()=>{const a=await i(e.reviewer,()=>r.openDM(e.slack_user));await A(t.dir,e,a,()=>r.post({channel:a.channel,thread_ts:a.ts,text:e.text}))});for(const e of n.replies)await s(`${e.id} to ${e.reviewer} on ${e.pr}`,async()=>{const a=e.thread_ts||(await i(e.reviewer,async()=>e.channel)).ts;await E(t.dir,e,()=>r.post({channel:e.channel,thread_ts:a,text:e.text}))});for(const e of n.reactions)await s(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await r.react({channel:e.channel,ts:e.ts,name:e.emoji}),await W(t.dir,e)});return{sent:h,failed:c}}o(z,"sendBoard");const G=o(t=>{try{return(JSON.parse(_.readFileSync(N.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function Q(t,r){return!P()||!t.some(G)?[]:(await $({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub \u203A Integrations \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:r}),T(),["pushed that Slack refused the token"])}o(Q,"pushRefusal");async function V(t,r,p){const n=[],h=[],c=B(),s=new Map;for(const i of t)try{for(const e of await J(i.dir,p))s.set(e.channel,[...s.get(e.channel)||[],{board:i,thread:e}])}catch(e){h.push(`${i.route}: reading its day threads: ${e.message}`)}const d=o(async(i,e)=>{try{await e()}catch(a){if(g(a))throw a;h.push(`${i}: ${a.message}`)}},"attempt");for(const[i,e]of s)await d(`hearing DM ${i}`,async()=>{const a=e.map(u=>u.thread.ts).sort((u,f)=>Number(u)-Number(f))[0],w=new Map((await r.history({channel:i,oldest:a})).map(u=>[u.ts,u.latest_reply]));k(!0);for(const{board:u,thread:f}of e){const m=f.heard||f.ts;Number(w.get(f.ts))>Number(m)&&await d(`${u.route}: hearing ${f.reviewer}'s ${f.date} thread`,async()=>{const S=(await r.replies({channel:i,ts:f.ts,oldest:m})).filter(l=>Number(l.ts)>Number(m)),b=await L(u.dir);for(const l of S){if(!l.subtype&&!l.bot_id&&!b.has(l.ts)&&!(l.user===c&&l.text?.startsWith(O))){const y=l.user===c?"owner":"reviewer";await x(u,"slack-question",{reviewer:f.reviewer,channel:i,thread_ts:f.ts,ts:l.ts,from:y,answer:y==="reviewer"||/@kickflip\b/i.test(l.text||"")}),n.push(`${u.route}: slack-question from ${y==="owner"?"the owner":f.reviewer}`)}await K(u.dir,f,l.ts)}})}});return{sent:n,failed:h}}o(V,"hearBoards");async function X(t,r,p){try{return await V(t,r,p)}catch(n){return g(n)?(k(!1),j()?(await $({title:"Slack can\u2019t read the review threads",body:"Reviewers\u2019 messages to the board wait unheard until Hub\u2019s Slack app has im:history: add it, reinstall the app and paste its new token in Hub \u203A Integrations \u203A Slack. Sends go on.",url:"/_hub/settings#slack",tag:"slack-unheard"},{kind:"fault",at:p}),q(),{sent:["pushed that Slack can't read the review threads"],failed:[]}):{sent:[],failed:[]}):{sent:[],failed:[`hearing the day threads: ${n.message}`]}}}o(X,"hear");async function ne({boards:t=D(),fetch:r,now:p=new Date}={}){const n=H({fetch:r}),h=[],c=[];if(!n)return{sent:h,failed:c};for(const s of t)try{const d=await z(s,n,p);h.push(...d.sent.map(i=>`${s.route}: ${i}`)),c.push(...d.failed)}catch(d){if(c.push(`${s.route}: ${d.message}`),v(d))break}if(!M().refused)try{const s=await X(t,n,p);h.push(...s.sent),c.push(...s.failed)}catch(s){c.push(`unheard push: ${s.message}`)}try{h.push(...await Q(t,p))}catch(s){c.push(`refused-token push: ${s.message}`)}return{sent:h,failed:c}}o(ne,"sendReviews");export{ne as sendReviews};
package/hub/slack.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var b=Object.defineProperty;var n=(r,e)=>b(r,"name",{value:e,configurable:!0});import u from"node:fs";import h from"node:path";import k from"node:os";import{withFileLock as v}from"./locks.mjs";import{readSecret as $,writeSecret as d}from"./secrets.mjs";const m=["self_dm","owner_slack_user","slack_domain","timezone"],p=n(()=>h.join(k.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(m.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function _(r){const e=p();u.mkdirSync(h.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
- `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub \u203A Integrations \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};
1
+ var O=Object.defineProperty;var n=(e,t)=>O(e,"name",{value:t,configurable:!0});import m from"node:fs";import p from"node:path";import w from"node:os";import{withFileLock as $}from"./locks.mjs";import{readSecret as D,writeSecret as d}from"./secrets.mjs";const f=["self_dm","owner_slack_user","slack_domain","timezone"],k=n(()=>p.join(w.homedir(),".claude","kickflip","slack.json"),"fileOf"),S=n(e=>{try{return JSON.parse(m.readFileSync(e,"utf8"))||{}}catch{return{}}},"read"),y=n(e=>Object.fromEntries(f.map(t=>[t,typeof e?.[t]=="string"?e[t]:""])),"identityOf");function g(e){const t=k();m.mkdirSync(p.dirname(t),{recursive:!0});const r=`${t}.${process.pid}.tmp`;return m.writeFileSync(r,JSON.stringify(e,null,2)+`
2
+ `,{mode:384}),m.chmodSync(r,384),m.renameSync(r,t),e}n(g,"save");function x(){const e=y(S(k()));if(f.some(r=>e[r]))return e;const t=y(S(p.join(w.homedir(),".claude","slack-bridge","state.json")).config);return f.some(r=>t[r])?g(t):e}n(x,"readSlack");async function N(e){for(const[t,r]of Object.entries(e)){if(!f.includes(t)||typeof r!="string")throw new Error(`Unknown Slack setting ${t}`);if(t==="self_dm"&&r&&!/^D[A-Z0-9]+$/.test(r))throw new Error("self-dm must be a DM channel id");if(t==="owner_slack_user"&&r&&!/^U[A-Z0-9]+$/.test(r))throw new Error("user must be a Slack user id");if(t==="slack_domain"&&r&&!/^[a-z0-9][a-z0-9-]*$/.test(r))throw new Error("domain must be a Slack workspace domain");if(t==="timezone"&&r)try{new Intl.DateTimeFormat("en",{timeZone:r})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return $(k(),()=>g({...x(),...e}))}n(N,"setSlack");function B(e=new Date){const t=x().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,r=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(e).map(({type:c,value:u})=>[c,u]));return{date:`${r.year}-${r.month}-${r.day}`,open:!["Sat","Sun"].includes(r.weekday)&&Number(r.hour)>=9&&Number(r.hour)<17}}n(B,"slackDay");class h extends Error{static{n(this,"SlackError")}constructor(t,r=502){super(t),this.status=r}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),I=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function v(e,{fetch:t=globalThis.fetch,checked:r=n(()=>{},"checked")}={}){async function c(a,i={}){let s;try{const o=await t(`${E()}/${a}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${e}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(s=await o.json().catch(()=>null),!s)throw new Error(`Slack answered ${o.status}`)}catch(o){throw new h(`Could not reach Slack: ${o.message}`)}if(I.includes(s.error))throw r(!1),new h("Slack refused the token.",400);if(r(!0),!s.ok)throw new h(`Slack: ${s.error}`);return s}return n(c,"call"),{async who(){const{team:a,team_id:i,user:s,user_id:o}=await c("auth.test");return{team:a,team_id:i,user:s,user_id:o}},async people(){const a=[];let i="";do{const s=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const o of s.members)!o.deleted&&!o.is_bot&&o.id!=="USLACKBOT"&&a.push({id:o.id,name:o.profile?.real_name||o.real_name||o.name});i=s.response_metadata?.next_cursor||""}while(i);return a.sort((s,o)=>s.name.localeCompare(o.name))},openDM:n(async a=>(await c("conversations.open",{users:a})).channel.id,"openDM"),post:n(async({channel:a,text:i,thread_ts:s})=>(await c("chat.postMessage",{channel:a,text:i,unfurl_links:!1,...s?{thread_ts:s}:{}})).ts,"post"),async react({channel:a,ts:i,name:s}){try{await c("reactions.add",{channel:a,timestamp:i,name:s})}catch(o){if(o.message!=="Slack: already_reacted")throw o}},history:n(({channel:a,oldest:i})=>u("conversations.history",{channel:a,oldest:i,inclusive:!0}),"history"),replies:n(({channel:a,ts:i,oldest:s})=>u("conversations.replies",{channel:a,ts:i,...s?{oldest:s}:{}}),"replies")};async function u(a,i){const s=[];let o="";do{const _=await c(a,{...i,limit:200,...o?{cursor:o}:{}});s.push(..._.messages),o=_.response_metadata?.next_cursor||""}while(o);return s}n(u,"pages")}n(v,"slackClient");const K=n(e=>e.message==="Slack: missing_scope","missingScope"),l=n(e=>{const t=D("slack",e);return typeof t?.token=="string"&&t.token?t:null},"savedToken");function b(e){const t=l(e);return t?{set:!0,...t.refused_at?{refused:!0}:{},...t.unheard_at?{unheard:!0}:{},...t.team?{team:t.team,user:t.user}:{}}:{set:!1}}n(b,"slackStatus");const L=n(()=>l()?.user_id,"slackUserId"),P="Slack needs Hub\u2019s Slack token; add it in Hub \u203A Integrations \u203A Slack first.";async function Z(e,{fetch:t}={}){const r=typeof e=="string"?e.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(r))throw new h("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let u=!1;const a=await v(r,{fetch:t,checked:n(i=>{u=!i},"checked")}).who().catch(i=>{if(!u)throw i;return null});return d("slack",a?{token:r,...a,saved_at:c}:{token:r,saved_at:c,refused_at:c}),b()}n(Z,"saveSlackToken");function H(){return d("slack",null),b()}n(H,"removeSlackToken");function T(e,t){const r=l();if(r?.token!==e||!!r.refused_at==!t)return;const{refused_at:c,refusal_pushed_at:u,...a}=r;d("slack",t?a:{...a,refused_at:new Date().toISOString()})}n(T,"markChecked");const J=n(()=>{const e=l();return!!(e?.refused_at&&!e.refusal_pushed_at)},"refusalUnpushed");function M(){const e=l();e?.refused_at&&d("slack",{...e,refusal_pushed_at:new Date().toISOString()})}n(M,"markRefusalPushed");function R(e){const t=l();if(!t||!!t.unheard_at==!e)return;const{unheard_at:r,unheard_pushed_at:c,...u}=t;d("slack",e?u:{...u,unheard_at:new Date().toISOString()})}n(R,"setHearing");const q=n(()=>{const e=l();return!!(e?.unheard_at&&!e.unheard_pushed_at)},"hearingUnpushed");function G(){const e=l();e?.unheard_at&&d("slack",{...e,unheard_pushed_at:new Date().toISOString()})}n(G,"markHearingPushed");function Q({fetch:e}={}){const t=l();return t?v(t.token,{fetch:e,checked:n(r=>T(t.token,r),"checked")}):null}n(Q,"savedSlack");export{P as SLACK_NEEDS_TOKEN,h as SlackError,q as hearingUnpushed,G as markHearingPushed,M as markRefusalPushed,K as missingScope,x as readSlack,J as refusalUnpushed,H as removeSlackToken,Z as saveSlackToken,Q as savedSlack,R as setHearing,N as setSlack,v as slackClient,B as slackDay,b as slackStatus,L as slackUserId};
package/hub/views.mjs CHANGED
@@ -1 +1 @@
1
- var L=Object.defineProperty;var i=(e,t)=>L(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as V,FOREVER as w,durationEnd as K,endOf as F}from"./duration.mjs";import{siteIcon as M}from"./icons.mjs";import{codexStatus as H}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),E=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),p=i(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),c(o,o)},"timeOfDay"),x=i((e,t)=>e.hold?v(e.hold,t):"pushes go out after each check","deviceLine"),W=i(e=>`${p(e.quiet.start)} \u2013 ${p(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),B="checks every minute while you're here, every 5 when away";function Z(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(u=>!u.ok),l=o.filter(u=>u.state==="broken"),r=t.find(u=>u.link==="events"),a=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",d=l.length?`degraded \xB7 ${a}`:o.length?`up \xB7 ${a} \xB7 ${o.length} settling`:`up \xB7 ${a} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:d,lines:o.map(u=>({link:u.link,detail:`${u.state==="broken"?"":"settling: "}${u.detail}`}))}}i(Z,"siteHealth");const A=[{value:"always",label:"Always on",description:i(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:i(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:i(e=>`${e} implements and reviews.`,"description")}],j=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:A.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?A.find(o=>o.value===e).description(q[n]):t.words,fix:t.fix}),"codexView"),q={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},C=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],J=i(e=>({model:e,builder:q[e],choices:C.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:C.find(t=>t.value===e).description}),"claudeView");function re({now:e,st:t,sidebar:n,rows:o,links:l=new Map,code:r={},host:a="",version:d,rules:u=[],remembered:g=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:T=H({installed:!1}),awake:P={on:!1,unsupported:null}}){const y=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),z=O?` \xB7 runs the code in its files (${r.onDisk})`:"",R=i(s=>({route:s.route,title:s.title,icon:M(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:g.get(s.route)||[],restart:y.has(s.route)&&s.restart===!0}),"site"),U=[["Boards",o.filter(s=>y.has(s.route))],["Pages",o.filter(s=>!y.has(s.route))]].filter(([,s])=>s.length).map(([s,D])=>({label:s,sites:D.map(R)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${z}`,restart:!O},awake:P,groups:U,integrations:f,usage:n.usage,pause:n.pause,codex:j(h,T,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:x(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?E(u.length,"rule"):"None yet",sites:E(o.length,"site"),host:a,version:d,total:n.total}}i(re,"hubView");const ae="That snooze ends in the past or more than 30 days on. Pick another time.",N="until you end it",_="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),I=i((e,t)=>V.map(([n,o])=>({value:n,label:o,until:$(K(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?F(e.snoozed_until,t):null,"snoozedUntil");function le({now:e,st:t,rules:n=[],linear:o={set:!1},slack:l={set:!1},pausedUntil:r=null}){const a=t.settings,d=a.notifications,u=Q(d,e),g=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,N)}`,h=r&&`Paused ${$(r,e,_)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:I(e,_)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:x(t,e),on:{on:d.on,sub:d.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${g} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:I(e,N)}:null,weekends:{on:d.weekends_off,sub:`Pushes wait from Friday ${p(a.quiet.start)} until Monday ${p(a.quiet.end)}. Urgent ones still go.`},quiet:ee(a,e),rules:n,linear:G(o),slack:Y(l)}}i(le,"settingsView");const X={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:write","reactions:write","users:read"]}}},b={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(X))}`,label:"a new Slack app"};function Y(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${b.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:b,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:b,summary:"No token"}}i(Y,"slackWords");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function G(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}i(G,"linearWords");function ee(e,t){const n=e.quiet,[o,l]=[p(n.start),p(n.end)],r=m(t,"tonight",e),a=[["Snooze tonight",`Until ${c(r,t)}${n.hold?`, then quiet hours hold pushes until ${c(m(r,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${c(m(t,"morning",e),t)}.`],["Snooze monday",`Until ${c(m(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${l}.`:"Off."],["Daily wake",`Every board wakes once at ${l}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:W(e),hint:B,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${l}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:a.map(([d,u])=>({title:d,sub:u}))}}i(ee,"quietView");function de(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}i(de,"flushView");export{B as QUIET_HINT,ae as SNOOZE_REFUSED,ue as capital,I as durationChoices,de as flushView,re as hubView,E as plural,W as quietSummary,le as settingsView,Z as siteHealth};
1
+ var D=Object.defineProperty;var i=(e,t)=>D(e,"name",{value:t,configurable:!0});import{clock as c,holdLine as v,snoozeEnd as m}from"./schedule.mjs";import{CHOICES as L,FOREVER as w,durationEnd as V,endOf as K}from"./duration.mjs";import{siteIcon as F}from"./icons.mjs";import{codexStatus as M}from"../shared/codex.mjs";const ue=i(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),H=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),p=i(e=>{const[t,n]=e.split(":").map(Number),o=new Date;return o.setHours(t,n,0,0),c(o,o)},"timeOfDay"),E=i((e,t)=>e.hold?v(e.hold,t):"pushes go out after each check","deviceLine"),W=i(e=>`${p(e.quiet.start)} \u2013 ${p(e.quiet.end)}${e.quiet.hold?" \xB7 pushes held":""}`,"quietSummary"),B="checks every minute while you're here, every 5 when away";function Z(e,t){const n={port:"down \xB7 not answering",files:"down \xB7 folder missing"}[e.serves]||"down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?n:e.state,lines:[]};if(!t)return{tone:"ok",word:"up",lines:[]};const o=t.filter(u=>!u.ok),l=o.filter(u=>u.state==="broken"),r=t.find(u=>u.link==="events"),a=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",d=l.length?`degraded \xB7 ${a}`:o.length?`up \xB7 ${a} \xB7 ${o.length} settling`:`up \xB7 ${a} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:d,lines:o.map(u=>({link:u.link,detail:`${u.state==="broken"?"":"settling: "}${u.detail}`}))}}i(Z,"siteHealth");const x=[{value:"always",label:"Always on",description:i(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:i(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:i(e=>`${e} implements and reviews.`,"description")}],j=i((e,t,n)=>({usable:t.usable,tag:t.usable?null:t.reason,share:e,choices:x.map(({value:o,label:l})=>({value:o,label:l,on:o===e})),description:t.usable?x.find(o=>o.value===e).description(A[n]):t.words,fix:t.fix}),"codexView"),A={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},q=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],J=i(e=>({model:e,builder:A[e],choices:q.map(({value:t,label:n})=>({value:t,label:n,on:t===e})),description:q.find(t=>t.value===e).description}),"claudeView");function re({now:e,st:t,sidebar:n,rows:o,links:l=new Map,code:r={},host:a="",version:d,rules:u=[],remembered:y=new Map,integrations:f=[],codexShare:h="off",claudeModel:S="auto",codex:I=M({installed:!1}),awake:T={on:!1,unsupported:null}}){const g=new Set(n.sites.filter(s=>s.board).map(s=>s.route)),O=!!(r.running&&r.running===r.onDisk),P=O?` \xB7 runs the code in its files (${r.onDisk})`:"",z=i(s=>({route:s.route,title:s.title,icon:F(s.route,s),href:`/${encodeURIComponent(s.route)}/`,...Z(s,l.get(s.route)),remembered:y.get(s.route)||[],restart:g.has(s.route)&&s.restart===!0}),"site"),R=[["Boards",o.filter(s=>g.has(s.route))],["Pages",o.filter(s=>!g.has(s.route))]].filter(([,s])=>s.length).map(([s,U])=>({label:s,sites:U.map(z)}));return{hub:{tone:n.hub.tone,word:n.hub.word,detail:`${n.hub.detail}${P}`,restart:!O},awake:T,groups:R,integrations:f,usage:n.usage,pause:n.pause,codex:j(h,I,S),claude:J(S),empty:o.length===0,notifications:{on:t.settings.notifications.on,text:n.notifications.text,device:E(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${v(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:u.length?H(u.length,"rule"):"None yet",host:a,version:d,total:n.total}}i(re,"hubView");const ae="That snooze ends in the past or more than 30 days on. Pick another time.",C="until you end it",N="until you resume",$=i((e,t,n)=>e===w?n:`until ${c(e,t)}`,"untilWords"),_=i((e,t)=>L.map(([n,o])=>({value:n,label:o,until:$(V(e,n),e,t)})),"durationChoices"),Q=i((e,t)=>e.on?K(e.snoozed_until,t):null,"snoozedUntil");function le({now:e,st:t,rules:n=[],linear:o={set:!1},slack:l={set:!1},pausedUntil:r=null}){const a=t.settings,d=a.notifications,u=Q(d,e),y=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",f=u&&`Snoozed ${$(u,e,C)}`,h=r&&`Paused ${$(r,e,N)}`;return{pause:{on:r?{title:h}:null,summary:r===w?"Paused \xB7 forever":h||"Off",choices:r?null:_(e,N)},snoozed:u?{title:f,summary:u===w?"Snoozed \xB7 forever":f}:null,device:E(t,e),on:{on:d.on,sub:d.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${y} Needs you and the badge still update.`},snooze:d.on&&!u?{choices:_(e,C)}:null,weekends:{on:d.weekends_off,sub:`Pushes wait from Friday ${p(a.quiet.start)} until Monday ${p(a.quiet.end)}. Urgent ones still go.`},quiet:ee(a,e),rules:n,linear:G(o),slack:Y(l)}}i(le,"settingsView");const X={display_information:{name:"Kickflip"},oauth_config:{scopes:{user:["chat:write","im:history","im:write","reactions:write","users:read"]}}},b={url:`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(X))}`,label:"a new Slack app"};function Y(e){const t=e.team?`${e.team} workspace`:"",n=`Make ${b.label}, install it to your workspace, then paste its User OAuth Token (xoxp-\u2026) here.`;return e.set?e.refused?{set:!0,refused:!0,title:t||"Slack token",down:"doesn\u2019t work",sub:`Slack refused the token. ${n}`,keys:b,summary:t?`Token doesn\u2019t work \xB7 ${t}`:"Token doesn\u2019t work"}:{set:!0,title:t,sub:`Token \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 \xB7 sends as ${e.user}`,summary:`Token set \xB7 ${t}`}:{set:!1,title:"No token",sub:n,keys:b,summary:"No token"}}i(Y,"slackWords");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function G(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}i(G,"linearWords");function ee(e,t){const n=e.quiet,[o,l]=[p(n.start),p(n.end)],r=m(t,"tonight",e),a=[["Snooze tonight",`Until ${c(r,t)}${n.hold?`, then quiet hours hold pushes until ${c(m(r,"morning",e),t)}`:""}.`],["Snooze morning",`Until ${c(m(t,"morning",e),t)}.`],["Snooze monday",`Until ${c(m(t,"monday",e),t)}.`],["Off on weekends",e.notifications.weekends_off?`Pushes wait from Friday ${o} until Monday ${l}.`:"Off."],["Daily wake",`Every board wakes once at ${l}, when quiet hours end.`]];return{start:n.start,end:n.end,summary:W(e),hint:B,hold:{on:n.hold,sub:n.hold?`Pushes queued after ${o} wait until ${l}. Urgent ones still go.`:"Off. Pushes go out all night."},reads:a.map(([d,u])=>({title:d,sub:u}))}}i(ee,"quietView");function de(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}i(de,"flushView");export{B as QUIET_HINT,ae as SNOOZE_REFUSED,ue as capital,_ as durationChoices,de as flushView,re as hubView,H as plural,W as quietSummary,le as settingsView,Z as siteHealth};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -1,5 +1,6 @@
1
- var p=Object.defineProperty;var t=(r,e)=>p(r,"name",{value:e,configurable:!0});import n from"node:fs";import l from"node:path";import{PR_VIEW_FIELDS as g}from"../hub/pr-report.mjs";const a=t((r,e,s,i)=>{const o=l.join(r,e,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(i))},"write"),y=t((r,e,s)=>a(r,"pr",e,s),"writePR"),x=t((r,e,s)=>a(r,"issue-comments",e,s),"writeIssueComments"),b=t((r,e,s)=>a(r,"review-comments",e,s),"writeReviewComments"),v=t((r,e,s)=>a(r,"reviews",e,s),"writeReviews"),_=t((r,e,s)=>a(r,"merge",e,s),"writeMerge"),S=t((r,e,s)=>a(r,"ready",e,s),"writeReady"),j=t((r,e,s)=>a(r,"open",e,s),"writeOpen"),R=t((r,e)=>a(r,"","user",e),"writeUser"),O=t((r,e,s={login:e})=>a(r,"github-users",e,s),"writeGithubUser"),q=t((r,e,s)=>a(r,"collaborators",e.replace("/","__"),s),"writeCollaborators"),N=t((r,e)=>a(r,"","accounts",e),"writeAccounts"),c=t((r,e)=>{try{return n.readFileSync(l.join(r,e),"utf8").trim().split(`
2
- `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(r=>c(r,"calls.jsonl"),"calls"),k=t((r,e)=>a(r,"","rate",e),"writeRate"),A=t((r,e)=>a(r,"","limit",e),"writeLimit"),E=t(r=>f(r).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,o])=>`${s}/${i}#${o}`)),"queried"),J=t(r=>c(r,"tokens.jsonl"),"tokens");function P({head:r="abc1234567",failing:e=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:r,statusCheckRollup:[...e.map(o=>({__typename:"CheckRun",name:o,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(o,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(P,"prView");function I(r){const e=l.join(r,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
1
+ var u=Object.defineProperty;var t=(r,e)=>u(r,"name",{value:e,configurable:!0});import n from"node:fs";import l from"node:path";import{PR_VIEW_FIELDS as g}from"../hub/pr-report.mjs";const o=t((r,e,s,i)=>{const a=l.join(r,e,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(a),{recursive:!0}),n.writeFileSync(a,JSON.stringify(i))},"write"),x=t((r,e,s)=>o(r,"pr",e,s),"writePR"),b=t((r,e,s)=>o(r,"issue-comments",e,s),"writeIssueComments"),v=t((r,e,s)=>o(r,"review-comments",e,s),"writeReviewComments"),S=t((r,e,s)=>o(r,"reviews",e,s),"writeReviews"),_=t((r,e,s)=>o(r,"merge",e,s),"writeMerge"),f={title:"feat: the work",body:"Checked it.",commits:[{messageBody:`Refs: A1
2
+ Co-Authored-By: Claude <noreply@anthropic.com>`}]},j=t((r,e,s)=>o(r,"ready",e,s),"writeReady"),R=t((r,e,s)=>o(r,"open",e,s),"writeOpen"),O=t((r,e)=>o(r,"","user",e),"writeUser"),N=t((r,e,s={login:e})=>o(r,"github-users",e,s),"writeGithubUser"),q=t((r,e,s)=>o(r,"collaborators",e.replace("/","__"),s),"writeCollaborators"),A=t((r,e)=>o(r,"","accounts",e),"writeAccounts"),c=t((r,e)=>{try{return n.readFileSync(l.join(r,e),"utf8").trim().split(`
3
+ `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),d=t(r=>c(r,"calls.jsonl"),"calls"),k=t((r,e)=>o(r,"","rate",e),"writeRate"),J=t((r,e)=>o(r,"","limit",e),"writeLimit"),E=t(r=>d(r).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,a])=>`${s}/${i}#${a}`)),"queried"),$=t(r=>c(r,"tokens.jsonl"),"tokens");function P({head:r="abc1234567",failing:e=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:r,statusCheckRollup:[...e.map(a=>({__typename:"CheckRun",name:a,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(a,p)=>({__typename:"CheckRun",name:`ci-${p}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(P,"prView");function I(r){const e=l.join(r,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
3
4
  const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
  const dir = ${JSON.stringify(r)};
@@ -43,7 +44,7 @@ if (args[0] === 'issue' && args.includes('--repo')) {
43
44
  else if (args[1] !== 'close') fail();
44
45
  process.exit(0);
45
46
  }
46
- if (args.length === 6 && args[0] === 'pr' && args[1] === 'merge' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--squash') {
47
+ if (args.length >= 6 && args[0] === 'pr' && args[1] === 'merge' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--squash') {
47
48
  let refused = null;
48
49
  try { refused = JSON.parse(fs.readFileSync(fixture('merge', args[4], args[2]), 'utf8')).error; } catch {}
49
50
  if (refused) { console.error(refused); process.exit(1); }
@@ -103,6 +104,10 @@ if (args[0] === 'pr' && args[1] === 'create' && args.includes('--repo')) {
103
104
  console.log('https://github.com/' + args[args.indexOf('--repo') + 1] + '/pull/' + (99 + n));
104
105
  process.exit(0);
105
106
  }
107
+ if (args.length === 7 && args[0] === 'pr' && args[1] === 'view' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--json' && args[6] === 'title,body,commits') {
108
+ console.log(${JSON.stringify(JSON.stringify(f))});
109
+ process.exit(0);
110
+ }
106
111
  let file;
107
112
  let list = false;
108
113
  if (args.length === 7 && args[0] === 'pr' && args[1] === 'view' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--json' && args[6] === ${JSON.stringify(g)}) file = fixture('pr', args[4], args[2]);
@@ -130,4 +135,4 @@ if (list) {
130
135
  try { if (JSON.parse(value).fail) fail(); } catch {}
131
136
  console.log(value);
132
137
  }
133
- `,{mode:493}),e}t(I,"fakeGh");export{f as calls,I as fakeGh,P as prView,E as queried,J as tokens,N as writeAccounts,q as writeCollaborators,O as writeGithubUser,x as writeIssueComments,A as writeLimit,_ as writeMerge,j as writeOpen,y as writePR,k as writeRate,S as writeReady,b as writeReviewComments,v as writeReviews,R as writeUser};
138
+ `,{mode:493}),e}t(I,"fakeGh");export{f as SQUASH_SOURCE,d as calls,I as fakeGh,P as prView,E as queried,$ as tokens,A as writeAccounts,q as writeCollaborators,N as writeGithubUser,b as writeIssueComments,J as writeLimit,_ as writeMerge,R as writeOpen,x as writePR,k as writeRate,j as writeReady,v as writeReviewComments,S as writeReviews,O as writeUser};