@lovek4d/kickflip 0.4.0 → 0.5.3

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.
Files changed (90) hide show
  1. package/CONTEXT.md +69 -48
  2. package/README.md +1 -1
  3. package/SKILL.md +22 -20
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +27 -18
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/agents.mjs +2 -0
  9. package/board/bin/board.mjs +108 -39
  10. package/board/domain.mjs +7 -7
  11. package/board/finish.mjs +4 -3
  12. package/board/headless.mjs +5 -3
  13. package/board/merge3.mjs +1 -0
  14. package/board/on-host.mjs +1 -1
  15. package/board/permissions.mjs +2 -2
  16. package/board/public/app-asks.js +9 -92
  17. package/board/public/app-banner.js +7 -13
  18. package/board/public/app-cards.js +70 -37
  19. package/board/public/app-core.js +24 -25
  20. package/board/public/app-header.js +2 -1
  21. package/board/public/app-lanes.js +55 -23
  22. package/board/public/app-needs.js +129 -4
  23. package/board/public/app-reader.js +174 -0
  24. package/board/public/app-snapshot.js +6 -5
  25. package/board/public/app-threads.js +83 -58
  26. package/board/public/app.html +2 -0
  27. package/board/public/app.js +66 -24
  28. package/board/public/attach.js +2 -2
  29. package/board/public/dialogs.js +9 -39
  30. package/board/public/hub-kit.js +4 -2
  31. package/board/public/hub.css +1 -0
  32. package/board/public/linear-key.js +46 -35
  33. package/board/public/new-task.js +100 -0
  34. package/board/public/prototype-kit.js +28 -3
  35. package/board/public/prototype-switcher.js +42 -107
  36. package/board/public/shell.js +7 -1
  37. package/board/public/sidebar.css +1 -0
  38. package/board/public/sidebar.js +44 -5
  39. package/board/public/style.css +194 -35
  40. package/board/public/ui-cards.js +80 -44
  41. package/board/public/ui-enums.js +1 -1
  42. package/board/public/ui-header.js +2 -2
  43. package/board/public/ui-order.js +12 -10
  44. package/board/public/ui-phase-bars.js +4 -4
  45. package/board/public/ui-questions.js +63 -47
  46. package/board/public/ui-sidebar.js +27 -15
  47. package/board/public/ui-threads.js +27 -14
  48. package/board/public/ui.js +76 -14
  49. package/board/public/view-frame.js +1 -0
  50. package/board/public/view-hub-settings.js +11 -10
  51. package/board/public/view-hub.js +17 -3
  52. package/board/public/view-questions.js +174 -202
  53. package/board/public/view-settings.js +208 -70
  54. package/board/server.mjs +19 -18
  55. package/board/store.mjs +20 -13
  56. package/board/transcript.mjs +2 -0
  57. package/board/worktree.mjs +24 -20
  58. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  59. package/hooks-plugin/hooks/hooks.json +9 -0
  60. package/hub/boards.mjs +4 -4
  61. package/hub/front-door.mjs +20 -0
  62. package/hub/github.mjs +3 -3
  63. package/hub/hub.mjs +11 -14
  64. package/hub/linear.mjs +1 -2
  65. package/hub/live.mjs +2 -2
  66. package/hub/locks.mjs +1 -1
  67. package/hub/new-board.mjs +2 -2
  68. package/hub/owner-checks.mjs +1 -0
  69. package/hub/pr-report.mjs +2 -2
  70. package/hub/presence.mjs +1 -1
  71. package/hub/push.mjs +2 -2
  72. package/hub/readiness.mjs +1 -2
  73. package/hub/review-send.mjs +1 -0
  74. package/hub/secrets.mjs +2 -0
  75. package/hub/self-restart.mjs +2 -0
  76. package/hub/sessions.mjs +2 -2
  77. package/hub/share.mjs +1 -0
  78. package/hub/slack.mjs +2 -2
  79. package/hub/stream.mjs +10 -13
  80. package/hub/try.mjs +7 -7
  81. package/hub/upgrade.mjs +1 -1
  82. package/hub/usage.mjs +3 -3
  83. package/hub/views.mjs +1 -1
  84. package/package.json +2 -1
  85. package/shared/audit.mjs +4 -0
  86. package/shared/fake-gh.mjs +17 -4
  87. package/shared/owner-check.mjs +3 -0
  88. package/shared/service.mjs +4 -4
  89. package/shared/tailscale.mjs +1 -0
  90. package/shared/usage-gate.mjs +1 -1
@@ -1,8 +1,8 @@
1
- var p=Object.defineProperty;var t=(e,r)=>p(e,"name",{value:r,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((e,r,s,i)=>{const a=l.join(e,r,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(a),{recursive:!0}),n.writeFileSync(a,JSON.stringify(i))},"write"),w=t((e,r,s)=>o(e,"pr",r,s),"writePR"),x=t((e,r,s)=>o(e,"issue-comments",r,s),"writeIssueComments"),v=t((e,r,s)=>o(e,"review-comments",r,s),"writeReviewComments"),b=t((e,r,s)=>o(e,"reviews",r,s),"writeReviews"),S=t((e,r,s)=>o(e,"merge",r,s),"writeMerge"),_=t((e,r,s)=>o(e,"ready",r,s),"writeReady"),j=t((e,r)=>o(e,"","user",r),"writeUser"),R=t((e,r)=>o(e,"","accounts",r),"writeAccounts"),c=t((e,r)=>{try{return n.readFileSync(l.join(e,r),"utf8").trim().split(`
2
- `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(e=>c(e,"calls.jsonl"),"calls"),O=t((e,r)=>o(e,"","rate",r),"writeRate"),N=t(e=>f(e).filter(r=>r[0]==="api"&&r[1]==="graphql").map(r=>[...r[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,a])=>`${s}/${i}#${a}`)),"queried"),k=t(e=>c(e,"tokens.jsonl"),"tokens");function E({head:e="abc1234567",failing:r=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:e,statusCheckRollup:[...r.map(a=>({__typename:"CheckRun",name:a,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(a,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(E,"prView");function J(e){const r=l.join(e,"bin");return n.mkdirSync(r,{recursive:!0}),n.writeFileSync(l.join(r,"gh"),`#!${process.execPath}
1
+ var p=Object.defineProperty;var t=(s,e)=>p(s,"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((s,e,r,i)=>{const a=l.join(s,e,`${String(r).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(a),{recursive:!0}),n.writeFileSync(a,JSON.stringify(i))},"write"),y=t((s,e,r)=>o(s,"pr",e,r),"writePR"),v=t((s,e,r)=>o(s,"issue-comments",e,r),"writeIssueComments"),x=t((s,e,r)=>o(s,"review-comments",e,r),"writeReviewComments"),b=t((s,e,r)=>o(s,"reviews",e,r),"writeReviews"),_=t((s,e,r)=>o(s,"merge",e,r),"writeMerge"),j=t((s,e,r)=>o(s,"ready",e,r),"writeReady"),S=t((s,e,r)=>o(s,"open",e,r),"writeOpen"),R=t((s,e)=>o(s,"","user",e),"writeUser"),O=t((s,e,r={login:e})=>o(s,"github-users",e,r),"writeGithubUser"),N=t((s,e,r)=>o(s,"collaborators",e.replace("/","__"),r),"writeCollaborators"),k=t((s,e)=>o(s,"","accounts",e),"writeAccounts"),c=t((s,e)=>{try{return n.readFileSync(l.join(s,e),"utf8").trim().split(`
2
+ `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(s=>c(s,"calls.jsonl"),"calls"),$=t((s,e)=>o(s,"","rate",e),"writeRate"),q=t(s=>f(s).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,r,i,a])=>`${r}/${i}#${a}`)),"queried"),E=t(s=>c(s,"tokens.jsonl"),"tokens");function J({head:s="abc1234567",failing:e=[],pending:r=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:s,statusCheckRollup:[...e.map(a=>({__typename:"CheckRun",name:a,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:r},(a,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(J,"prView");function A(s){const e=l.join(s,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const dir = ${JSON.stringify(e)};
5
+ const dir = ${JSON.stringify(s)};
6
6
  const args = process.argv.slice(2);
7
7
  fs.appendFileSync(path.join(dir, 'calls.jsonl'), JSON.stringify(args) + '\\n');
8
8
  fs.appendFileSync(path.join(dir, 'tokens.jsonl'), JSON.stringify({ args, token: process.env.GH_TOKEN || null }) + '\\n');
@@ -46,6 +46,11 @@ if (args.length === 4 && args[0] === 'api' && args[2] === '-f' && args[3].starts
46
46
  console.log(JSON.stringify({ html_url: 'https://github.com/' + args[1].slice('repos/'.length) }));
47
47
  process.exit(0);
48
48
  }
49
+ // A Comment review posted with gh api repos/<repo>/pulls/<n>/reviews -f body=\u2026 -f event=\u2026 -f commit_id=\u2026: answers its html_url.
50
+ if (args.length === 8 && args[0] === 'api' && args[2] === '-f' && args[4] === '-f' && args[6] === '-f' && /^repos\\/[^/]+\\/[^/]+\\/pulls\\/\\d+\\/reviews$/.test(args[1])) {
51
+ console.log(JSON.stringify({ html_url: 'https://github.com/' + args[1].slice('repos/'.length).replace('/pulls/', '/pull/').replace(/\\/reviews$/, '') + '#pullrequestreview-1' }));
52
+ process.exit(0);
53
+ }
49
54
  // The hub's one GraphQL query (hub/pr-report.mjs fetchPRs), answered from the same fixtures as the REST calls: each
50
55
  // aliased PR from its pr, issue-comments, review-comments and reviews fixtures, the viewer from user.json and the rate
51
56
  // limit from rate.json. A PR without a fixture, or {fail: true}, comes back null with an error, as GitHub answers.
@@ -58,6 +63,9 @@ if (args.length === 4 && args[0] === 'api' && args[1] === 'graphql' && args[2] =
58
63
  const list = (folder, repo, n) => { const v = read(fixture(folder, repo, n), []); return Array.isArray(v) ? v : []; };
59
64
  const data = { viewer: user ? { login: user.login } : null, rateLimit: rate };
60
65
  const errors = [];
66
+ for (const [, alias, owner, name] of args[3].matchAll(/(open\\d+): repository\\(owner: "([^"]+)", name: "([^"]+)"\\) \\{ pullRequests\\(/g)) {
67
+ data[alias] = { pullRequests: { nodes: read(path.join(dir, 'open', owner + '__' + name + '.json'), []) } };
68
+ }
61
69
  for (const [, alias, owner, name, n] of args[3].matchAll(/(\\w+): repository\\(owner: "([^"]+)", name: "([^"]+)"\\) \\{ pullRequest\\(number: (\\d+)\\)/g)) {
62
70
  const repo = owner + '/' + name;
63
71
  const view = read(fixture('pr', repo, n), null);
@@ -84,6 +92,11 @@ let file;
84
92
  let list = false;
85
93
  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]);
86
94
  else if (args.length === 2 && args[0] === 'api' && args[1] === 'user') file = path.join(dir, 'user.json');
95
+ else if (args.length === 2 && args[0] === 'api' && /^users\\/[\\w-]+$/.test(args[1])) file = path.join(dir, 'github-users', args[1].slice(6) + '.json');
96
+ else if (args.length === 4 && args[0] === 'api' && args[1] === '--paginate' && args[2] === '--slurp' && /^repos\\/[^/]+\\/[^/]+\\/collaborators$/.test(args[3])) {
97
+ file = path.join(dir, 'collaborators', args[3].split('/').slice(1, 3).join('__') + '.json');
98
+ list = true;
99
+ }
87
100
  else if (args.length === 4 && args[0] === 'api' && args[1] === '--paginate' && args[2] === '--slurp') {
88
101
  const match = /^repos\\/([^/]+\\/[^/]+)\\/(issues|pulls)\\/(\\d+)\\/(comments\\?since=.+|reviews)$/.exec(args[3]);
89
102
  if (!match || (match[2] === 'issues' && match[4] === 'reviews')) process.exit(1);
@@ -102,4 +115,4 @@ if (list) {
102
115
  try { if (JSON.parse(value).fail) fail(); } catch {}
103
116
  console.log(value);
104
117
  }
105
- `,{mode:493}),r}t(J,"fakeGh");export{f as calls,J as fakeGh,E as prView,N as queried,k as tokens,R as writeAccounts,x as writeIssueComments,S as writeMerge,w as writePR,O as writeRate,_ as writeReady,v as writeReviewComments,b as writeReviews,j as writeUser};
118
+ `,{mode:493}),e}t(A,"fakeGh");export{f as calls,A as fakeGh,J as prView,q as queried,E as tokens,k as writeAccounts,N as writeCollaborators,O as writeGithubUser,v as writeIssueComments,_ as writeMerge,S as writeOpen,y as writePR,$ as writeRate,j as writeReady,x as writeReviewComments,b as writeReviews,R as writeUser};
@@ -0,0 +1,3 @@
1
+ var h=Object.defineProperty;var r=(t,e)=>h(t,"name",{value:e,configurable:!0});import{execFile as m}from"node:child_process";const k=/^[\w.-]+\/[\w.-]+$/,A=/^(@[\w.-]+\/)?[\w.][\w.-]*$/,S=/^[\w.-]+(\/[\w.-]+)*$/,b=/^[\w-]+(\.[\w-]+)*$/,u=r(t=>{throw new Error(t)},"bad"),d=r(t=>k.test(t||"")?t:u(`"${t??""}" isn't a GitHub repo such as owner/name`),"repoOf"),p=r((t,e)=>t&&!t.startsWith("-")?t:u(`${e} is missing`),"plain"),l={tag:{usage:"tag <owner/repo> <pattern>",args:r(([t,e,...s])=>s.length?u("tag takes a repo and one pattern"):[d(t),p(e,"the tag pattern")],"args"),words:r(([t,e])=>`a ${e} tag on ${t}`,"words"),async run([t,e],{exec:s}){const n=await s("git",["ls-remote","--tags",`https://github.com/${t}.git`]),a=j(e),i=[...new Set(n.split(`
2
+ `).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(g).at(-1);return i?{passed:!0,found:`tag ${i} is on ${t}`}:{passed:!1}}},npm:{usage:"npm <package> <version-or-range>",args:r(([t,e,...s])=>s.length?u("npm takes a package and one version or range"):[A.test(t||"")?t:u(`"${t??""}" isn't an npm package name`),p(e,"the version or range")],"args"),words:r(([t,e])=>`${t}@${e} on npm`,"words"),async run([t,e],{exec:s}){const n=(await s("npm",["view",`${t}@${e}`,"version","--json"]).catch(i=>{if(/No match found for version/.test(`${i.stdout??""}${i.stderr??""}`))return"";throw i})).trim(),o=(n?[JSON.parse(n)].flat():[]).sort(g).at(-1);return o?{passed:!0,found:`${t}@${o} is on npm`}:{passed:!1}}},run:{usage:"run <owner/repo> <workflow>",args:r(([t,...e])=>[d(t),p(e.join(" "),"the workflow")],"args"),words:r(([t,e])=>`a passing ${e} run on ${t}`,"words"),async run([t,e],{exec:s,since:n}){const a=await s("gh",["run","list","--repo",t,"--workflow",e,"--status","success","--limit","20","--json","createdAt,url"]),o=JSON.parse(a||"[]").filter(i=>String(i.createdAt)>=n).sort((i,c)=>String(i.createdAt).localeCompare(String(c.createdAt))).at(-1);return o?{passed:!0,found:`${e} passed on ${t} (${o.url})`}:{passed:!1}}},gh:{usage:"gh <owner/repo> <api-path> <field>=<value>",args([t,e,s,...n]){n.length&&u("gh takes a repo, an API path and one field=value"),(!S.test(e||"")||e.split("/").includes(".."))&&u(`"${e??""}" isn't an API path under the repo, such as actions/permissions`);const[a,...o]=String(s??"").split("=");return(!b.test(a)||!o.length)&&u(`"${s??""}" isn't a field=value, such as enabled=true`),[d(t),e,s]},words:r(([t,e,s])=>`${e} reads ${s} on ${t}`,"words"),async run([t,e,s],{exec:n}){const[a,...o]=s.split("="),i=o.join("="),c=a.split(".").reduce((f,w)=>f?.[w],JSON.parse(await n("gh",["api",`repos/${t}/${e}`])));return c!==void 0&&String(c)===i?{passed:!0,found:`${e} reads ${s} on ${t}`}:{passed:!1}}},url:{usage:"url <http(s) address> [<status> | <text>]",args([t,...e]){let s;try{s=new URL(t)}catch{u(`"${t??""}" isn't a web address`)}return["http:","https:"].includes(s.protocol)||u("url takes an http or https address"),e.length?[t,e.join(" ")]:[t]},words:r(([t,e])=>e===void 0||/^\d{3}$/.test(e)?`${t} answers ${e??200}`:`${t} shows "${e}"`,"words"),async run([t,e],{fetchImpl:s}){const n=e!==void 0&&/^\d{3}$/.test(e)?Number(e):200,a=await s(t,{redirect:"follow",signal:AbortSignal.timeout(2e4)}),o=await a.text();return a.status!==n?{passed:!1}:n===200&&e!==void 0&&!/^\d{3}$/.test(e)?o.includes(e)?{passed:!0,found:`${t} shows "${e}"`}:{passed:!1}:{passed:!0,found:`${t} answered ${n}`}}}},$=Object.values(l).map(t=>t.usage);function E(t){const[e,...s]=String(t??"").trim().split(/\s+/).filter(Boolean);e||u(`--check takes "<name> <args>": ${$.join(", ")}`);const n=Object.hasOwn(l,e)?l[e]:u(`--check has no check "${e}"; it takes ${$.join(", ")}`);try{return{name:e,args:n.args(s)}}catch(a){return u(`--check ${n.usage}: ${a.message}`)}}r(E,"parseCheck");const C=r(({name:t,args:e})=>[t,...e].join(" "),"checkText"),I=r(({name:t,args:e})=>l[t].words(e),"checkWords");function N({name:t,args:e},{since:s,exec:n=v,fetchImpl:a=fetch}={}){return l[t].run(e,{since:s,exec:n,fetchImpl:a})}r(N,"runCheck");function v(t,e){return new Promise((s,n)=>{m(t,e,{timeout:6e4,maxBuffer:16*1024*1024,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(a,o,i)=>{if(!a)return s(o);const c=String(i||"").trim().split(`
3
+ `).find(Boolean)?.replace(/^npm error /,"");n(Object.assign(new Error(`${t} ${e[0]} failed: ${c||a.message}`),{stdout:o,stderr:i}))})})}r(v,"execArgv");function j(t){return new RegExp(`^${t.split("").map(e=>e==="*"?".*":e==="?"?".":e.replace(/[.+^${}()|[\]\\]/g,"\\$&")).join("")}$`)}r(j,"globOf");function g(t,e){return t.localeCompare(e,"en",{numeric:!0})}r(g,"byVersion");export{$ as CHECK_USAGE,C as checkText,I as checkWords,E as parseCheck,N as runCheck};
@@ -1,5 +1,5 @@
1
- var S=Object.defineProperty;var n=(i,u)=>S(i,"name",{value:u,configurable:!0});import p from"node:fs";import v from"node:os";import $ from"node:path";import{spawnSync as A}from"node:child_process";const d="local.claude.kickflip-hub",m="kickflip-hub.service";function E(i,u,{env:g=process.env,timeout:o=1e4}={}){const r=A(i,u,{encoding:"utf8",env:g,timeout:o});return{status:r.status??1,stdout:r.stdout||"",error:(r.error?.message||r.stderr||`exit ${r.status}`).trim().split(`
2
- `)[0]}}n(E,"runCommand");function D({platform:i=process.platform,home:u=v.homedir(),uid:g=process.getuid?.(),env:o=process.env,run:r=E}={}){const a=n((t,s,c={})=>{try{return r(t,s,{env:o,...c})}catch(l){return{status:1,stdout:"",error:l.message}}},"exec");if(i==="darwin")return L({home:u,uid:g,exec:a});if(i==="linux")return T({home:u,uid:g,exec:a});const e=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${i}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(e)},"install"),restart:n(()=>e,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:e}),"status"),notes:n(()=>[],"notes")}}n(D,"hubService");const b=12e4;function L({home:i,uid:u,exec:g}){const o=$.join(i,"Library","LaunchAgents"),r=`gui/${u}`,a=$.join(o,`${d}.plist`),e=n((t,s)=>g("launchctl",t,s),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${r}/${d}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${d}`,definition({node:t,entry:s,state:c,port:l,log:h}){const y=n(w=>String(w).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
1
+ var S=Object.defineProperty;var n=(i,u)=>S(i,"name",{value:u,configurable:!0});import p from"node:fs";import v from"node:os";import $ from"node:path";import{spawnSync as A}from"node:child_process";const d="local.claude.kickflip-hub",m="kickflip-hub.service";function T(i,u,{env:g=process.env,timeout:o=1e4}={}){const r=A(i,u,{encoding:"utf8",env:g,timeout:o});return{status:r.status??1,stdout:r.stdout||"",error:(r.error?.message||r.stderr||`exit ${r.status}`).trim().split(`
2
+ `)[0]}}n(T,"runCommand");function D({platform:i=process.platform,home:u=v.homedir(),uid:g=process.getuid?.(),env:o=process.env,run:r=T}={}){const a=n((t,s,c={})=>{try{return r(t,s,{env:o,...c})}catch(l){return{status:1,stdout:"",error:l.message}}},"exec");if(i==="darwin")return E({home:u,uid:g,exec:a});if(i==="linux")return L({home:u,uid:g,exec:a});const e=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${i}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(e)},"install"),restart:n(()=>e,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:e}),"status"),notes:n(()=>[],"notes")}}n(D,"hubService");const b=12e4;function E({home:i,uid:u,exec:g}){const o=$.join(i,"Library","LaunchAgents"),r=`gui/${u}`,a=$.join(o,`${d}.plist`),e=n((t,s)=>g("launchctl",t,s),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${r}/${d}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${d}`,definition({node:t,entry:s,state:c,port:l,log:h}){const y=n(w=>String(w).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
3
3
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
4
  <plist version="1.0">
5
5
  <dict>
@@ -15,7 +15,7 @@ var S=Object.defineProperty;var n=(i,u)=>S(i,"name",{value:u,configurable:!0});i
15
15
  <key>StandardErrorPath</key><string>${y(h)}</string>
16
16
  </dict>
17
17
  </plist>
18
- `},async install(t,{wait:s=n(l=>new Promise(h=>setTimeout(h,l)),"wait"),dryRun:c=!1}={}){if(c)return[`would write ${a} and start ${r}/${d}`];p.mkdirSync(o,{recursive:!0}),p.writeFileSync(a,t),e(["bootout",`${r}/${d}`]),await s(500);const l=e(["bootstrap",r,a]);if(l.status===0)return[`started ${r}/${d} from ${a}`];throw new Error(l.error)},restart(){const t=e(["kickstart","-k",`${r}/${d}`],{timeout:b});return t.status===0?null:t.error},remove(){e(["bootout",`${r}/${d}`]),p.rmSync(a,{force:!0})},status(){const t=e(["print",`${r}/${d}`]),s=p.existsSync(a),c=s&&t.status===0&&/\bstate = running\b/.test(t.stdout||"");return{installed:s,running:c,detail:c?"running":s?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(L,"launchd");const k=n(i=>String(i).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),f=n(i=>`"${k(i).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function T({home:i,uid:u,exec:g}){const o=$.join(i,".config","systemd","user",m),r=n((e,t)=>g("systemctl",["--user",...e],t),"systemctl"),a=n(e=>{const t=r(e);if(t.status!==0)throw new Error(`systemctl --user ${e.join(" ")}: ${t.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:m,restartCommand:`systemctl --user restart ${m}`,definition({node:e,entry:t,state:s,port:c,log:l}){return`[Unit]
18
+ `},async install(t,{wait:s=n(l=>new Promise(h=>setTimeout(h,l)),"wait"),dryRun:c=!1}={}){if(c)return[`would write ${a} and start ${r}/${d}`];p.mkdirSync(o,{recursive:!0}),p.writeFileSync(a,t),e(["bootout",`${r}/${d}`]),await s(500);const l=e(["bootstrap",r,a]);if(l.status===0)return[`started ${r}/${d} from ${a}`];throw new Error(l.error)},restart(){const t=e(["kickstart","-k",`${r}/${d}`],{timeout:b});return t.status===0?null:t.error},remove(){e(["bootout",`${r}/${d}`]),p.rmSync(a,{force:!0})},status(){const t=e(["print",`${r}/${d}`]),s=p.existsSync(a),c=s&&t.status===0&&/\bstate = running\b/.test(t.stdout||"");return{installed:s,running:c,detail:c?"running":s?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(E,"launchd");const k=n(i=>String(i).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),f=n(i=>`"${k(i).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function L({home:i,uid:u,exec:g}){const o=$.join(i,".config","systemd","user",m),r=n((e,t)=>g("systemctl",["--user",...e],t),"systemctl"),a=n(e=>{const t=r(e);if(t.status!==0)throw new Error(`systemctl --user ${e.join(" ")}: ${t.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:m,restartCommand:`systemctl --user restart ${m}`,definition({node:e,entry:t,state:s,port:c,log:l}){return`[Unit]
19
19
  Description=kickflip hub
20
20
 
21
21
  [Service]
@@ -30,4 +30,4 @@ StandardError=append:${k(l)}
30
30
 
31
31
  [Install]
32
32
  WantedBy=default.target
33
- `},async install(e,{dryRun:t=!1}={}){return t?[`would write ${o}, enable and start ${m}`]:(p.mkdirSync($.dirname(o),{recursive:!0}),p.writeFileSync(o,e),a(["daemon-reload"]),a(["enable",m]),a(["restart",m]),[`enabled and started ${m} from ${o}`])},restart(){const e=r(["restart",m],{timeout:b});return e.status===0?null:e.error},remove(){r(["disable","--now",m]),p.rmSync(o,{force:!0}),r(["daemon-reload"])},status(){const e=r(["is-active",m]),t=p.existsSync(o),s=t&&e.status===0&&(e.stdout||"").trim()==="active";return{installed:t,running:s,detail:s?"running":t?"not running":"not installed"}},notes(){const e=g("loginctl",["show-user",String(u),"--property=Linger"]),t=e.status===0&&/^Linger=yes$/m.test(e.stdout||"");return[{item:"hub without a login",state:t?"ok":"optional",detail:t?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"At the host always reads away, since presence comes from macOS ioreg, so pushes reach every device"}]}}}n(T,"systemd");export{d as LOGIN_LABEL,m as UNIT,D as hubService,E as runCommand};
33
+ `},async install(e,{dryRun:t=!1}={}){return t?[`would write ${o}, enable and start ${m}`]:(p.mkdirSync($.dirname(o),{recursive:!0}),p.writeFileSync(o,e),a(["daemon-reload"]),a(["enable",m]),a(["restart",m]),[`enabled and started ${m} from ${o}`])},restart(){const e=r(["restart",m],{timeout:b});return e.status===0?null:e.error},remove(){r(["disable","--now",m]),p.rmSync(o,{force:!0}),r(["daemon-reload"])},status(){const e=r(["is-active",m]),t=p.existsSync(o),s=t&&e.status===0&&(e.stdout||"").trim()==="active";return{installed:t,running:s,detail:s?"running":t?"not running":"not installed"}},notes(){const e=g("loginctl",["show-user",String(u),"--property=Linger"]),t=e.status===0&&/^Linger=yes$/m.test(e.stdout||"");return[{item:"hub without a login",state:t?"ok":"optional",detail:t?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"The owner reads away from the host, since presence comes from macOS ioreg, so only a page tap makes the hub check every minute"}]}}}n(L,"systemd");export{d as LOGIN_LABEL,m as UNIT,D as hubService,T as runCommand};
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var s=(t,l)=>p(t,"name",{value:l,configurable:!0});import n from"node:fs";import f from"node:os";import a from"node:path";const m=a.join("Tailscale.app","Contents","MacOS","Tailscale"),r=s(t=>{try{return n.statSync(t).isFile()&&(n.accessSync(t,n.constants.X_OK),!0)}catch{return!1}},"isExecutable");function j({env:t=process.env,platform:l=process.platform,apps:e=["/Applications",a.join(f.homedir(),"Applications")]}={}){for(const i of(t.PATH||"").split(a.delimiter).filter(Boolean)){const o=a.join(i,"tailscale");if(r(o))return o}return l!=="darwin"?null:e.map(i=>a.join(i,m)).find(r)??null}s(j,"tailscaleBin");const c=s((t=process.platform)=>t==="darwin"?"brew install --cask tailscale-app":"curl -fsSL https://tailscale.com/install.sh | sh","tailscaleInstallCommand"),x=s((t=process.platform)=>t==="darwin"?`install Tailscale from https://tailscale.com/download, or ${c(t)}`:c(t),"tailscaleInstallHint");export{j as tailscaleBin,c as tailscaleInstallCommand,x as tailscaleInstallHint};
@@ -1 +1 @@
1
- var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as x,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as M,effectiveCodexShare as O}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),h=o(e=>Number(e),"timeOf"),G=["five_hour","seven_day"];function v({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const d=E(s)?s:90,r=o(f=>d===100?null:G.find(i=>{const c=f?.[i];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=d&&(!c.resets_at||Date.parse(c.resets_at)>h(a))})||null,"past"),l=o((f,i)=>({window:i,used:i?f[i].used_percentage:null,resets_at:i?f[i].resets_at:null}),"details"),u=r(e),p=l(e,u),C=u&&n?.window===u&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:d,state:u?C?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(v,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(t)},s.usage=$({home:e,now:o(()=>s.now,"now")}),s.codex=M({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=h(t);const n=x(e);return v({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(k,"readUsageGate");function U(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...x(t),pauseAt:Number(e)},t)}o(U,"setPauseAt");function W(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=x(t);if(e==="on"){const a=k({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(W,"setKeepGoing");const z=o(e=>{try{return["github-change","check-github","card-unblocked","card-pull","card-plan","overnight-triage","pr-review"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function B({home:e=g.homedir(),now:t=Date.now(),card:s={}}={}){const n=o(l=>{const{model:u,reason:p}=P(D(e),s);return{kind:"claude",model:u,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const d=O(x(e).codexShare,a);if(d==="off")return n("Codex share is off.");const r=k({home:e,now:t});return r.codex.past?n(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${d} and Codex is under the ${r.pause_at}% line.`}}o(B,"builderPick");const b={opus:"Opus",sonnet:"Sonnet"};function P(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${b[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${b[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(P,"claudePick");const H=o(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{B as builderPick,z as heldUsageEvent,H as pauseReason,k as readUsageGate,W as setKeepGoing,U as setPauseAt,v as usageGate,E as validPauseAt};
1
+ var C=Object.defineProperty;var o=(e,t)=>C(e,"name",{value:t,configurable:!0});import f from"node:os";import{usageReader as M}from"../hub/usage.mjs";import{readSettingsFile as g,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as O,effectiveCodexShare as R}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),h=o(e=>Number(e),"timeOf"),F=["five_hour","seven_day"];function G({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const d=E(s)?s:90,r=o(x=>d===100?null:F.find(u=>{const c=x?.[u];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=d&&(!c.resets_at||Date.parse(c.resets_at)>h(a))})||null,"past"),l=o((x,u)=>({window:u,used:u?x[u].used_percentage:null,resets_at:u?x[u].resets_at:null}),"details"),i=r(e),p=l(e,i),S=i&&n?.window===i&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:d,state:i?S?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(G,"usageGate");const _=new Map;function b({home:e=f.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(t)},s.usage=M({home:e,now:o(()=>s.now,"now")}),s.codex=O({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=h(t);const n=g(e);return G({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(b,"readUsageGate");function K(e,{home:t=f.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...g(t),pauseAt:Number(e)},t)}o(K,"setPauseAt");function B(e,{home:t=f.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=g(t);if(e==="on"){const a=b({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(B,"setKeepGoing");const H=o(e=>{try{return["github-change","check-github","card-unblocked","overnight-triage","pr-review","owner-check-broken"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function I({home:e=f.homedir(),now:t=Date.now(),card:s={}}={}){const n=N({home:e}),a={codex:n.reason??"usable",claudeModel:D(e),size:s.size??null,cardModel:s.model??null},d=o(l=>{const{model:i,reason:p}=j(a.claudeModel,s);return{kind:"claude",model:i,reason:`${l} ${p}`,inputs:a}},"claude");if(!n.usable)return d(n.words);if(a.codexShare=R(g(e).codexShare,n),a.codexShare==="off")return d("Codex share is off.");const r=b({home:e,now:t});return Object.assign(a,{pauseAt:r.pause_at,codexUsage:r.codex}),r.codex.past?d(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${a.codexShare} and Codex is under the ${r.pause_at}% line.`,inputs:a}}o(I,"builderPick");const k={opus:"Opus",sonnet:"Sonnet"};function j(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${k[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${k[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(j,"claudePick");const v=o(e=>`Work is paused: Claude's ${$(e.window)} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason"),J=o((e,t)=>t==="codex"?e.codex.past:e.state==="paused","heldFor"),$=o(e=>e==="five_hour"?"5-hour":"weekly","windowWord"),T=o((e,t)=>t==="codex"?`Codex is paused: its ${$(e.codex.window)} usage is at ${Math.round(e.codex.used)}%, past the ${e.pause_at}% line. Resumes at ${e.codex.resets_at}.`:v(e),"pauseReasonFor");export{I as builderPick,J as heldFor,H as heldUsageEvent,v as pauseReason,T as pauseReasonFor,b as readUsageGate,B as setKeepGoing,K as setPauseAt,G as usageGate,E as validPauseAt};