@lovek4d/kickflip 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -125,9 +125,14 @@
125
125
  return `<${tag} class="install tone-${s.tone}"${attrs}>${s.html}</${tag}>`;
126
126
  }
127
127
  // A card's Serve / Build, only while the board's Auto-build is off and its branch has changes
128
- // (docs/adr/2026-11-02-1806-only-the-owner-or-auto-build-starts-a-card-run.md).
128
+ // (docs/adr/2026-11-02-1806-only-the-owner-or-auto-build-starts-a-card-run.md). A website card's serve that is running
129
+ // or up has Kill in its place, Auto serve on or off
130
+ // (docs/adr/2026-11-12-0112-a-card-serve-stays-up-while-its-worktree-does.md).
129
131
  function tryButton(it) {
130
132
  const t = tryOf(it);
133
+ if (it.kind === 'work' && t?.kind === 'web' && ['running', 'ready'].includes(t.state)) {
134
+ return `<button type="button" data-try-kill="${esc(it.key)}"${BoardUI.pending.has(`${it.key}:try`) ? BoardUI.BUSY : ''}>Kill</button>`;
135
+ }
131
136
  if (it.kind !== 'work' || !t?.script || t.autoBuild || !t.changes) return '';
132
137
  const words = tryWords(t.kind);
133
138
  const reason = t.state === 'running' ? words.busy : t.state === 'ready' && !t.stale ? words.current : '';
@@ -278,13 +283,16 @@
278
283
  // phase only as its progress bar, and a card In Review for an owner wait other than the merge has none. Ready reads
279
284
  // only on a card that can start (domain.mjs ready), so a blocked one shows what blocks it instead.
280
285
  const PHASE_LOOK = {
281
- new: ['new', 'quiet'], grilling: ['grilling', 'info'], prototyping: ['prototyping', 'info'], staging: ['staging', 'notice'], ready: ['ready', 'success'],
286
+ new: ['new', 'quiet'], discussing: ['discussing', 'info'], grilling: ['grilling', 'info'], prototyping: ['prototyping', 'info'], staging: ['staging', 'notice'], ready: ['ready', 'success'],
282
287
  draft: ['draft', 'quiet'], reviewers: ['pending review', 'notice'], merge: ['pending merge', 'notice'], stale: ['stale', 'quiet'],
283
288
  merged: ['merged', 'merged'], closed: ['closed', 'quiet'],
284
289
  };
290
+ // docs/adr/2026-11-12-0500-a-new-task-is-a-conversation.md: a discussing card says whose turn it is, blue for the
291
+ // owner's, orange while the orchestrator owes the reply.
292
+ const TURN_LOOK = { owner: ['discussing · your turn', 'info'], claude: ["discussing · Claude's turn", 'notice'] };
285
293
  function phaseWord(it) {
286
294
  if (it.kind !== 'work' || it.lane === 'progress' || !it.phase || (it.phase === 'ready' && !it.ready)) return '';
287
- const [word, tone] = PHASE_LOOK[it.phase];
295
+ const [word, tone] = it.phase === 'discussing' ? TURN_LOOK[it.turn] : PHASE_LOOK[it.phase];
288
296
  return `<span class="card-phase ph-${tone}">${word}</span>`;
289
297
  }
290
298
  // docs/adr/2026-11-09-1300-a-card-pause-stops-its-builder-at-its-next-stopping-point.md
@@ -11,7 +11,7 @@
11
11
  // In Progress's are the five build steps its progress bar draws; a review's "reviewers" and "merge" read "pending review"
12
12
  // and "pending merge" on the card (ui-cards.js PHASE_LOOK).
13
13
  const PHASES = Object.freeze({
14
- todo: Object.freeze(['new', 'grilling', 'prototyping', 'staging', 'ready']),
14
+ todo: Object.freeze(['new', 'discussing', 'grilling', 'prototyping', 'staging', 'ready']),
15
15
  progress: Object.freeze(['reading', 'building', 'testing', 'checking', 'landing']),
16
16
  review: Object.freeze(['draft', 'reviewers', 'merge', 'stale']),
17
17
  done: Object.freeze(['merged', 'closed']),
@@ -106,6 +106,7 @@
106
106
  'ask-link': mini('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
107
107
  move: mini('<path d="M3 7h14"/><path d="m13 3 4 4-4 4"/><path d="M21 17H7"/><path d="m11 13-4 4 4 4"/>'),
108
108
  'Implementation requested': mini('<path d="m8 6-6 6 6 6"/><path d="m16 6 6 6-6 6"/>'),
109
+ 'Marked as discussing': mini('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
109
110
  'Marked as staging': mini('<path d="m12 2 10 5-10 5L2 7z"/><path d="m2 12 10 5 10-5"/><path d="m2 17 10 5 10-5"/>'),
110
111
  'Marked as ready': mini('<path d="M4 22V4"/><path d="M4 4h13l-2 4 2 4H4"/>'),
111
112
  Held: mini('<path d="M9 5v14"/><path d="M15 5v14"/>'),
@@ -406,11 +407,18 @@
406
407
  // A view's place when it has no copy to show: the kickflip mark and down's two lines, as the no-access page has them.
407
408
  const downLanding = (down) => `<div class="down-landing" data-down="${down.state}"><img class="down-landing-mark" src="/_hub/kickflip.png" width="70" height="60" alt="">
408
409
  <p class="down-landing-line">${esc(down.line)}</p><p class="down-landing-hint">${esc(down.hint)}</p></div>`;
410
+ // The landing for a board that loaded but threw while drawing (docs/adr/2026-11-12-0400-a-board-that-throws-while-drawing-shows-a-landing-with-the-error.md):
411
+ // the error's message, its stack behind Show details, Reload and Copy error (app.js wires them).
412
+ const drawLanding = (err) => `<div class="down-landing" data-down="draw"><img class="down-landing-mark" src="/_hub/kickflip.png" width="70" height="60" alt="">
413
+ <p class="down-landing-line">This board couldn't draw</p><p class="down-landing-hint">The board loaded, but it threw while drawing. Your data is fine.</p>
414
+ <p class="down-landing-hint"><b>${esc(err?.name || 'Error')}:</b> ${esc(err?.message ?? String(err))}</p>
415
+ <div class="down-landing-buttons"><button type="button" data-draw="reload">Reload</button> <button type="button" data-draw="copy">Copy error</button> <button type="button" data-draw="more">Show details</button></div>
416
+ <pre class="down-landing-stack" hidden>${esc(err?.stack || '')}</pre></div>`;
409
417
  // The line over a side page painted from its copy while the board can't be reached.
410
418
  const copyNote = (what, savedAt) => `<p class="note copy-note">Copy of ${esc(what)} as of ${since(savedAt)}. Read only.</p>`;
411
419
  // The buttons that send, which a board out of reach holds (style.css .offline): every one greyed and stopped, keeping its
412
420
  // label and size. Reading controls, folds, links, Edit and drafts stay live.
413
- const SENDS = `form button[type="submit"], .actions button, button[data-confirm], button[data-deck-pick], button[data-review-answer], button[data-prompt-send], button[data-prompt-choice], button[data-quick], button[data-card-bump], button[data-bump], button[data-withdraw], button[data-grill], button[data-proto], button[data-visual], button[data-plan], button[data-main-pull], button[data-arch-run], button[data-try], button[data-reopen]`;
421
+ const SENDS = `form button[type="submit"], .actions button, button[data-confirm], button[data-deck-pick], button[data-review-answer], button[data-prompt-send], button[data-prompt-choice], button[data-quick], button[data-card-bump], button[data-bump], button[data-withdraw], button[data-grill], button[data-proto], button[data-visual], button[data-plan], button[data-main-pull], button[data-arch-run], button[data-try], button[data-try-kill], button[data-reopen]`;
414
422
  // While held(), a tap on one of root's sending buttons, and any form's submit there, Enter's included, goes nowhere, and onHeld
415
423
  // tells the tap it is waiting. on is the view's B.on.
416
424
  function holdSends(on, root, held, onHeld) {
@@ -464,7 +472,7 @@
464
472
 
465
473
  globalThis.BoardUI = {
466
474
  LINK_ARROW, escText, onBackdrop,
467
- esc, record, time, ago, lasted, since, clock, etaLine, cardTime, replyBody, setHTML, closeDetails, BUSY, waitOn, pending, fetchJSON, WAITING, RESTARTING, restartNotice, downOf, downLanding, copyNote, SENDS, holdSends,
475
+ esc, record, time, ago, lasted, since, clock, etaLine, cardTime, replyBody, setHTML, closeDetails, BUSY, waitOn, pending, fetchJSON, WAITING, RESTARTING, restartNotice, downOf, downLanding, drawLanding, copyNote, SENDS, holdSends,
468
476
  attachments, attachButton, dropZone, sendButton, keyedSend, pendingFiles, protoButton, grillButton, visualButton, mark, statusMark, MARK_ICONS, withdrawButton, editButton, cardBumpButton, workingLine, thinkingLine, isPaused, PAUSED, BADGE_TONES, badge,
469
477
  };
470
478
  })();
@@ -51,6 +51,8 @@
51
51
  let topics = [];
52
52
  let topicId = null;
53
53
  let soloId = null;
54
+ // Counts the owner's own topic picks (a pill, or the shell opening a topic), which a Send in flight compares against.
55
+ let picks = 0;
54
56
  let shown = '';
55
57
  let sentLine = '';
56
58
  // The shown topic's questions this page has drawn unsent, which stay in place once sent or resolved until you leave
@@ -157,15 +159,17 @@
157
159
  // The topic a tap on this page acts for, the pills before it and its name, which moveOn reads once it is recorded.
158
160
  function leaving() {
159
161
  const from = soloId ? topicOf(soloId)?.id : topicId;
160
- return { from, pills: topicRow(topics, from).pills, name: toastName(topics.find((t) => t.id === from) || { title: '' }) };
162
+ return { from, picks, pills: topicRow(topics, from).pills, name: toastName(topics.find((t) => t.id === from) || { title: '' }) };
161
163
  }
162
164
  // After a recorded tap: a load that started before it went out would read its question as still open.
163
165
  // `footer` is a line or, for a Send, a function of the reloaded board giving the whole line.
164
166
  // The tap's POST answers with the board as it left it, which lands here in place of a reload.
165
- async function moveOnOnceLoaded({ from, pills }, what, out, footer = what) {
167
+ async function moveOnOnceLoaded({ from, pills, picks: at }, what, out, footer = what) {
166
168
  await loading;
167
169
  if (!B.alive()) return;
168
170
  if (!land(out.board, false)) await load(false);
171
+ // Tapped to another topic meanwhile: the page stays there and only the toast says what was recorded.
172
+ if (picks !== at) { B.toast(what); return; }
169
173
  moveOn(from, pills, what, typeof footer === 'function' ? footer() : footer, typeof footer !== 'function');
170
174
  }
171
175
 
@@ -386,7 +390,7 @@
386
390
  const b = ev.target.closest && ev.target.closest('[data-topic]');
387
391
  if (b) {
388
392
  // Embedded in the desktop pane, one click also focuses the topic's card on the board; a phone only picks the topic.
389
- soloId = null; setTopic(b.dataset.topic); sentLine = ''; shown = '';
393
+ picks++; soloId = null; setTopic(b.dataset.topic); sentLine = ''; shown = '';
390
394
  const first = topics.find((t) => t.id === topicId)?.questions[0];
391
395
  if (first) setHash(first);
392
396
  ctx.onTopic?.(topicId);
@@ -468,7 +472,7 @@
468
472
  if (ctx.embedded) B.openTopic = (id) => {
469
473
  if (!loaded) return false;
470
474
  const topic = topics.find((t) => t.id === id);
471
- if (topic) { soloId = null; setTopic(topic.id); } else pick(id || '');
475
+ if (topic) { picks++; soloId = null; setTopic(topic.id); } else pick(id || '');
472
476
  ctx.onTopic?.(topicId);
473
477
  sentLine = '';
474
478
  shown = '';
@@ -49,6 +49,10 @@
49
49
  <span class="muted small sub">Grills and prototypes running at once. A card that lands in Todo past it waits its turn.</span></span>
50
50
  <input type="number" class="count" id="maxPlanners" min="1" max="99" step="1" inputmode="numeric" disabled></label>
51
51
  <p class="muted small" id="maxPlannersStatus" role="status"></p>
52
+ <label class="who" for="maxServes"><span><span>Maximum parallel serves</span>
53
+ <span class="muted small sub">Each open card worktree keeps one serve up. Installs aren’t counted. A busy host asks before it starts another.</span></span>
54
+ <input type="number" class="count" id="maxServes" min="1" max="99" step="1" inputmode="numeric" disabled></label>
55
+ <p class="muted small" id="maxServesStatus" role="status"></p>
52
56
  <label class="who" for="autoBuild"><span><span>Auto serve</span>
53
57
  <span class="muted small sub" id="autoBuildSub">${BoardUI.esc(BoardUI.tryWords('web').autoSub)}</span></span>
54
58
  <input type="checkbox" class="toggle" id="autoBuild" disabled></label>
@@ -508,7 +512,7 @@
508
512
  showBuildTargets();
509
513
  $('#autoPull').checked = d.settings?.auto_pull === true;
510
514
  $('#autoPull').disabled = false;
511
- for (const [id, key] of [['maxImplementers', 'max_implementers'], ['maxPlanners', 'max_planners']]) {
515
+ for (const [id, key] of [['maxImplementers', 'max_implementers'], ['maxPlanners', 'max_planners'], ['maxServes', 'max_serves']]) {
512
516
  $(`#${id}`).value = savedCaps[key] = String(d.settings?.[key]);
513
517
  $(`#${id}`).disabled = false;
514
518
  }
@@ -621,8 +625,9 @@
621
625
  });
622
626
  autoToggle('autoPull', 'auto_pull', (on) => (on ? 'Saved. Claude grills each card that lands in Todo and starts ready ones while an implementer slot is free.' : 'Saved. Todo cards wait for you.'));
623
627
 
624
- // docs/adr/2026-11-04-0057-auto-implement-fills-in-progress-up-to-a-cap.md: Maximum parallel implementers, and
625
- // Maximum parallel planners (docs/adr/2026-11-04-2359-every-card-arriving-in-todo-is-grilled-by-the-board.md), save on
628
+ // docs/adr/2026-11-04-0057-auto-implement-fills-in-progress-up-to-a-cap.md: Maximum parallel implementers,
629
+ // Maximum parallel planners (docs/adr/2026-11-04-2359-every-card-arriving-in-todo-is-grilled-by-the-board.md) and
630
+ // Maximum parallel serves (docs/adr/2026-11-12-0112-a-card-serve-stays-up-while-its-worktree-does.md), save on
626
631
  // change, waiting on the server like a toggle; a number outside 1 to 99 or a refusal puts the saved one back.
627
632
  const capInput = (id, key, saidOf) => B.on($(`#${id}`), 'change', async () => {
628
633
  const input = $(`#${id}`);
@@ -642,6 +647,7 @@
642
647
  });
643
648
  capInput('maxImplementers', 'max_implementers', (n) => `Saved. Up to ${n === 1 ? '1 agent builds' : `${n} agents build`} at once.`);
644
649
  capInput('maxPlanners', 'max_planners', (n) => `Saved. Up to ${n === 1 ? '1 grill or prototype runs' : `${n} grills or prototypes run`} at once.`);
650
+ capInput('maxServes', 'max_serves', (n) => `Saved. Up to ${n === 1 ? '1 serve stays' : `${n} serves stay`} up at once.`);
645
651
  autoToggle('autoReview', 'auto_review', (on) => (on ? 'Saved. When idle, Claude reviews incoming PRs that touch this board’s work.' : 'Saved. Claude leaves incoming PRs alone.'));
646
652
  autoToggle('idleNotification', 'idle_notification', (on) => (on ? 'Saved. One push once the board has been idle for 15 minutes.' : 'Saved. No push when the board goes idle.'));
647
653
  autoToggle('autoTriage', 'auto_triage', (on) => (on ? 'Saved. Once a night, when cards wait on your answers, Claude triages them.' : 'Saved. Questions and Todo are left as they are.'));
@@ -1,10 +1,10 @@
1
- var A=Object.defineProperty;var l=(t,e)=>A(t,"name",{value:e,configurable:!0});import g from"node:fs";import k from"node:fs/promises";import B from"node:http";import P from"node:os";import m from"node:path";import{spawn as R}from"node:child_process";import{setTimeout as b}from"node:timers/promises";import{fileURLToPath as D}from"node:url";import{createInterface as O}from"node:readline";import{BoardError as w,readPrototypes as F}from"./store.mjs";import{prototypePage as G}from"./prototype-page.mjs";import{BASE_KEY as U,baseWorktreeOf as K}from"./prototype-base.mjs";import{buildScriptError as W,buildScriptOf as C}from"./build-script.mjs";import{boardAt as q}from"../hub/boards.mjs";const S=m.dirname(D(import.meta.url)),_=[390,1280],j=process.env.CHROME||"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",T={".js":"text/javascript",".css":"text/css",".html":"text/html",".json":"application/json",".png":"image/png",".svg":"image/svg+xml"},N=3e3,J=300,Y=12e4,M="(document.getElementById('stage')?.innerHTML || '').trim()",V="(document.body?.innerText || '').trim()";function z(t,e){const n=[m.join(t,"public"),m.join(S,"public"),m.join(S,"..","starter","public"),m.join(S,"..","shared")];return B.createServer(async(u,r)=>{let o=decodeURIComponent(new URL(u.url,"http://x").pathname);const d=/^\/_hub\/(icon-(?:180|192|512)\.png)$/.exec(o);if(d){r.writeHead(200,{"content-type":T[".png"]}).end(await k.readFile(m.join(S,"..","hub",d[1])));return}if(["/_hub/kickflip.png","/_hub/hub.css","/_hub/colors.css"].includes(o)&&(o=o.slice(5)),o===`/prototype-${e.topic}.html`){r.writeHead(200,{"content-type":T[".html"]}).end(await G(t,e));return}for(const c of n){const i=m.resolve(c,`.${o}`);if(i.startsWith(c+m.sep)&&g.existsSync(i)&&g.statSync(i).isFile()){r.writeHead(200,{"content-type":T[m.extname(i)]||"application/octet-stream"}).end(await k.readFile(i));return}}r.writeHead(o==="/favicon.ico"?204:404).end()})}l(z,"serve");async function H(t){if(!g.existsSync(j))throw new w(`the render check needs Chrome; none at ${j}, set CHROME to its path`);const e=R(j,["--headless=new","--remote-debugging-port=0",`--user-data-dir=${t}`,"--no-first-run","about:blank"],{stdio:"ignore",env:{...process.env,HOME:P.userInfo().homedir}}),n=m.join(t,"DevToolsActivePort");for(let a=0;a<100&&!g.existsSync(n);a++)await b(100);if(!g.existsSync(n))throw e.kill(),new w("the render check could not start Chrome");const u=g.readFileSync(n,"utf8").split(`
2
- `)[0];let r;for(let a=0;a<100&&!r;a++)r=(await fetch(`http://127.0.0.1:${u}/json`).then(p=>p.json(),()=>[])).find(p=>p.type==="page"),r||await b(100);if(!r)throw e.kill(),new w("the render check could not start Chrome");const o=new WebSocket(r.webSocketDebuggerUrl);try{await new Promise((a,p)=>{o.onopen=a,o.onerror=p})}catch{throw e.kill(),new w("the render check could not start Chrome")}let d=0;const c=new Map,i=[];return o.onmessage=({data:a})=>{const p=JSON.parse(a);p.id?c.get(p.id)?.(p):i.forEach(f=>f(p))},{cdp:l((a,p={})=>new Promise((f,$)=>{const y=++d;c.set(y,v=>v.error?$(new Error(v.error.message)):f(v.result)),o.send(JSON.stringify({id:y,method:a,params:p}))}),"cdp"),on:l(a=>(i.push(a),()=>i.splice(i.indexOf(a),1)),"on"),close:l(async()=>{o.close();const a=new Promise(p=>e.once("exit",p));e.kill(),await a},"close")}}l(H,"launchOnce");async function Q(t){try{return await H(t)}catch(e){if(!(e instanceof w)||!/could not start Chrome/.test(e.message))throw e;return await k.rm(t,{recursive:!0,force:!0}),await k.mkdir(t),H(t)}}l(Q,"launch");const X=l(t=>t.exception?.description?.split(`
3
- `)[0]||t.text,"described");async function I({cdp:t,on:e},n,u,r=M){const o=[],d=e(s=>{s.method==="Runtime.exceptionThrown"?o.push(X(s.params.exceptionDetails)):s.method==="Runtime.consoleAPICalled"&&s.params.type==="error"?o.push(`console error: ${s.params.args.map(h=>h.value??h.description).join(" ").split(`
4
- `)[0]}`):s.method==="Log.entryAdded"&&s.params.entry.level==="error"&&s.params.entry.source!=="javascript"&&!/\/favicon\.ico$/.test(s.params.entry.url||"")&&o.push(`${s.params.entry.text}${s.params.entry.url?` ${s.params.entry.url}`:""}`)});await t("Emulation.setDeviceMetricsOverride",{width:u,height:900,deviceScaleFactor:1,mobile:u<600}),await t("Page.navigate",{url:n});const c=l(async()=>(await t("Runtime.evaluate",{expression:r,returnByValue:!0})).result.value,"stage");let i="";for(let s=0;s<N&&!i;s+=100)await b(100),i=await c();return await b(J),d(),i||o.push(r===M?"renders an empty stage":"renders an empty page"),[...new Set(o)]}l(I,"checkTake");async function Z(t){const e=q(t);if(!e)throw new w(`${t} is not a board in the hub's manifest, so it has no base prototype worktree to check`);const n=K(e.route),u=m.join(n,C(e.build_target)),r=W(u);if(r)throw new w(`the base prototype worktree can't serve the round: ${r} (${u})`);const o=R(u,[U,n],{cwd:n,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:e.route,HOST:"127.0.0.1"}}),d=[],c=l(f=>{d.push(f),d.length>5&&d.shift()},"keep");O({input:o.stderr}).on("line",c);const i=new Promise(f=>o.once("exit",f)),s=l(async()=>{try{process.kill(-o.pid,"SIGTERM")}catch{return}if(!await Promise.race([i.then(()=>!0),b(4e3).then(()=>!1)])){try{process.kill(-o.pid,"SIGKILL")}catch{}await i}},"stop"),h=await new Promise(f=>{const $=O({input:o.stdout}),y=l(x=>{clearTimeout(v),$.close(),f(x)},"done"),v=setTimeout(()=>y(null),Y);$.on("line",x=>{const E=/^port (\d{1,5})$/.exec(x);E?y(Number(E[1])):c(x)}),i.then(()=>y(null))}),a=l(()=>fetch(`http://127.0.0.1:${h}/`,{signal:AbortSignal.timeout(1e3)}).then(()=>!0,()=>!1),"answers");let p=!!h;for(let f=0;p&&!await a();f+=250)f>=6e4?p=!1:await b(250);if(!p)throw await s(),new w(`the base prototype worktree's ${C(e.build_target)} didn't serve on a port, so the round's question is not published:
1
+ var L=Object.defineProperty;var u=(n,e)=>L(n,"name",{value:e,configurable:!0});import g from"node:fs";import k from"node:fs/promises";import B from"node:http";import O from"node:os";import m from"node:path";import{spawn as P}from"node:child_process";import{setTimeout as $}from"node:timers/promises";import{fileURLToPath as D}from"node:url";import{createInterface as C}from"node:readline";import{BoardError as w,readPrototypes as F}from"./store.mjs";import{prototypePage as G}from"./prototype-page.mjs";import{BASE_KEY as N,baseWorktreeOf as U}from"./prototype-base.mjs";import{buildScriptError as W,buildScriptOf as _}from"./build-script.mjs";import{boardAt as q}from"../hub/boards.mjs";const j=m.dirname(D(import.meta.url)),b=[390,1280],E=process.env.CHROME||"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",R={".js":"text/javascript",".css":"text/css",".html":"text/html",".json":"application/json",".png":"image/png",".svg":"image/svg+xml"},K=3e3,J=300,V=12e4,A="(document.getElementById('stage')?.innerHTML || '').trim()",Y="[...document.querySelectorAll('#stage *')].map((e) => getComputedStyle(e).display !== 'none' ? null : (e.getAttribute('class') || '').trim().split(/\\s+/).filter(Boolean).map((c) => '.' + c).join('') || e.localName)",z="(document.body?.innerText || '').trim()";function Q(n,e){const o=[m.join(n,"public"),m.join(j,"public"),m.join(j,"..","starter","public"),m.join(j,"..","shared")];return B.createServer(async(p,t)=>{let r=decodeURIComponent(new URL(p.url,"http://x").pathname);const d=/^\/_hub\/(icon-(?:180|192|512)\.png)$/.exec(r);if(d){t.writeHead(200,{"content-type":R[".png"]}).end(await k.readFile(m.join(j,"..","hub",d[1])));return}if(["/_hub/kickflip.png","/_hub/hub.css","/_hub/colors.css"].includes(r)&&(r=r.slice(5)),r===`/prototype-${e.topic}.html`){t.writeHead(200,{"content-type":R[".html"]}).end(await G(n,e));return}for(const c of o){const s=m.resolve(c,`.${r}`);if(s.startsWith(c+m.sep)&&g.existsSync(s)&&g.statSync(s).isFile()){t.writeHead(200,{"content-type":R[m.extname(s)]||"application/octet-stream"}).end(await k.readFile(s));return}}t.writeHead(r==="/favicon.ico"?204:404).end()})}u(Q,"serve");async function M(n){if(!g.existsSync(E))throw new w(`the render check needs Chrome; none at ${E}, set CHROME to its path`);const e=P(E,["--headless=new","--remote-debugging-port=0",`--user-data-dir=${n}`,"--no-first-run","about:blank"],{stdio:"ignore",env:{...process.env,HOME:O.userInfo().homedir}}),o=m.join(n,"DevToolsActivePort");for(let a=0;a<100&&!g.existsSync(o);a++)await $(100);if(!g.existsSync(o))throw e.kill(),new w("the render check could not start Chrome");const p=g.readFileSync(o,"utf8").split(`
2
+ `)[0];let t;for(let a=0;a<100&&!t;a++)t=(await fetch(`http://127.0.0.1:${p}/json`).then(l=>l.json(),()=>[])).find(l=>l.type==="page"),t||await $(100);if(!t)throw e.kill(),new w("the render check could not start Chrome");const r=new WebSocket(t.webSocketDebuggerUrl);try{await new Promise((a,l)=>{r.onopen=a,r.onerror=l})}catch{throw e.kill(),new w("the render check could not start Chrome")}let d=0;const c=new Map,s=[];return r.onmessage=({data:a})=>{const l=JSON.parse(a);l.id?c.get(l.id)?.(l):s.forEach(f=>f(l))},{cdp:u((a,l={})=>new Promise((f,x)=>{const y=++d;c.set(y,v=>v.error?x(new Error(v.error.message)):f(v.result)),r.send(JSON.stringify({id:y,method:a,params:l}))}),"cdp"),on:u(a=>(s.push(a),()=>s.splice(s.indexOf(a),1)),"on"),close:u(async()=>{r.close();const a=new Promise(l=>e.once("exit",l));e.kill(),await a},"close")}}u(M,"launchOnce");async function X(n){try{return await M(n)}catch(e){if(!(e instanceof w)||!/could not start Chrome/.test(e.message))throw e;return await k.rm(n,{recursive:!0,force:!0}),await k.mkdir(n),M(n)}}u(X,"launch");const Z=u(n=>n.exception?.description?.split(`
3
+ `)[0]||n.text,"described");async function H({cdp:n,on:e},o,p,t=A){const r=[],d=e(i=>{i.method==="Runtime.exceptionThrown"?r.push(Z(i.params.exceptionDetails)):i.method==="Runtime.consoleAPICalled"&&i.params.type==="error"?r.push(`console error: ${i.params.args.map(h=>h.value??h.description).join(" ").split(`
4
+ `)[0]}`):i.method==="Log.entryAdded"&&i.params.entry.level==="error"&&i.params.entry.source!=="javascript"&&!/\/favicon\.ico$/.test(i.params.entry.url||"")&&r.push(`${i.params.entry.text}${i.params.entry.url?` ${i.params.entry.url}`:""}`)});await n("Emulation.setDeviceMetricsOverride",{width:p,height:900,deviceScaleFactor:1,mobile:p<600}),await n("Page.navigate",{url:o});const c=u(async()=>(await n("Runtime.evaluate",{expression:t,returnByValue:!0})).result.value,"stage");let s="";for(let i=0;i<K&&!s;i+=100)await $(100),s=await c();return await $(J),d(),s||r.push(t===A?"renders an empty stage":"renders an empty page"),[...new Set(r)]}u(H,"checkTake");function ee(n){const[e,o]=n,p=[];for(let t=0;t<Math.min(e.length,o.length);t++)e[t]&&!o[t]&&p.push(`${e[t]} is hidden at ${b[0]}px and shown at ${b[1]}px`),o[t]&&!e[t]&&p.push(`${o[t]} is hidden at ${b[1]}px and shown at ${b[0]}px`);return[...new Set(p)]}u(ee,"hiddenAtOneWidth");async function te(n){const e=q(n);if(!e)throw new w(`${n} is not a board in the hub's manifest, so it has no base prototype worktree to check`);const o=U(e.route),p=m.join(o,_(e.build_target)),t=W(p);if(t)throw new w(`the base prototype worktree can't serve the round: ${t} (${p})`);const r=P(p,[N,o],{cwd:o,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:e.route,HOST:"127.0.0.1"}}),d=[],c=u(f=>{d.push(f),d.length>5&&d.shift()},"keep");C({input:r.stderr}).on("line",c);const s=new Promise(f=>r.once("exit",f)),i=u(async()=>{try{process.kill(-r.pid,"SIGTERM")}catch{return}if(!await Promise.race([s.then(()=>!0),$(4e3).then(()=>!1)])){try{process.kill(-r.pid,"SIGKILL")}catch{}await s}},"stop"),h=await new Promise(f=>{const x=C({input:r.stdout}),y=u(S=>{clearTimeout(v),x.close(),f(S)},"done"),v=setTimeout(()=>y(null),V);x.on("line",S=>{const T=/^port (\d{1,5})$/.exec(S);T?y(Number(T[1])):c(S)}),s.then(()=>y(null))}),a=u(()=>fetch(`http://127.0.0.1:${h}/`,{signal:AbortSignal.timeout(1e3)}).then(()=>!0,()=>!1),"answers");let l=!!h;for(let f=0;l&&!await a();f+=250)f>=6e4?l=!1:await $(250);if(!l)throw await i(),new w(`the base prototype worktree's ${_(e.build_target)} didn't serve on a port, so the round's question is not published:
5
5
  ${d.join(`
6
- `)}`);return{origin:`http://127.0.0.1:${h}`,dir:n,stop:s}}l(Z,"serveBase");async function L(t){const e=await k.mkdtemp(m.join(P.tmpdir(),"render-check-"));let n;try{return n=await Q(e),await n.cdp("Runtime.enable"),await n.cdp("Log.enable"),await t(n)}finally{await n?.close(),await k.rm(e,{recursive:!0,force:!0,maxRetries:30,retryDelay:200})}}l(L,"withChrome");async function ee(t,e){const n=await Z(t);try{const u=e.takes[0].key,r=(e.states||[]).map(c=>c.key),o=[...e.takes.map(c=>[c.key,r[0]??null]),...r.slice(1).map(c=>[u,c])],d=await L(async c=>{const i=[];for(const[s,h]of o){const a=new URL(e.served,n.origin);a.searchParams.set("variant",s),h&&a.searchParams.set("state",h);for(const p of _)for(const f of await I(c,a.href,p,V))i.push(`take ${s}${h?`, state ${h}`:""} at ${p}px: ${f}`)}return i});if(d.length)throw new w(`prototype ${e.topic} does not render, so its question is not published; fix its page in ${n.dir} and ask again:
6
+ `)}`);return{origin:`http://127.0.0.1:${h}`,dir:o,stop:i}}u(te,"serveBase");async function I(n){const e=await k.mkdtemp(m.join(O.tmpdir(),"render-check-"));let o;try{return o=await X(e),await o.cdp("Runtime.enable"),await o.cdp("Log.enable"),await n(o)}finally{await o?.close(),await k.rm(e,{recursive:!0,force:!0,maxRetries:30,retryDelay:200})}}u(I,"withChrome");async function ne(n,e){const o=await te(n);try{const p=e.takes[0].key,t=(e.states||[]).map(c=>c.key),r=[...e.takes.map(c=>[c.key,t[0]??null]),...t.slice(1).map(c=>[p,c])],d=await I(async c=>{const s=[];for(const[i,h]of r){const a=new URL(e.served,o.origin);a.searchParams.set("variant",i),h&&a.searchParams.set("state",h);for(const l of b)for(const f of await H(c,a.href,l,z))s.push(`take ${i}${h?`, state ${h}`:""} at ${l}px: ${f}`)}return s});if(d.length)throw new w(`prototype ${e.topic} does not render, so its question is not published; fix its page in ${o.dir} and ask again:
7
7
  ${d.join(`
8
- `)}`)}finally{await n.stop()}}l(ee,"checkServedRender");async function we(t,e){const n=(await F(t)).find(r=>r.topic===e);if(n.served)return ee(t,n);const u=z(t,n);await new Promise(r=>u.listen(0,"127.0.0.1",r));try{const r=`http://127.0.0.1:${u.address().port}/prototype-${e}.html`,o=await L(async d=>{const c=[];for(const i of n.takes)for(const s of _)for(const h of await I(d,`${r}?variant=${i.key}`,s))c.push(`take ${i.key} at ${s}px: ${h}`);return c});if(o.length)throw new w(`prototype ${e} does not render, so its question is not published; fix the take files and ask again:
9
- ${o.join(`
10
- `)}`)}finally{u.close()}}l(we,"checkTakesRender");export{_ as WIDTHS,we as checkTakesRender};
8
+ `)}`)}finally{await o.stop()}}u(ne,"checkServedRender");async function ge(n,e){const o=(await F(n)).find(t=>t.topic===e);if(o.served)return ne(n,o);const p=Q(n,o);await new Promise(t=>p.listen(0,"127.0.0.1",t));try{const t=`http://127.0.0.1:${p.address().port}/prototype-${e}.html`,r=await I(async d=>{const c=[];for(const s of o.takes){const i=[];for(const h of b){for(const a of await H(d,`${t}?variant=${s.key}`,h))c.push(`take ${s.key} at ${h}px: ${a}`);i.push((await d.cdp("Runtime.evaluate",{expression:Y,returnByValue:!0})).result.value||[])}for(const h of ee(i))c.push(`take ${s.key}: ${h}`)}return c});if(r.length)throw new w(`prototype ${e} does not render, so its question is not published; fix the take files and ask again:
9
+ ${r.join(`
10
+ `)}`)}finally{p.close()}}u(ge,"checkTakesRender");export{b as WIDTHS,ge as checkTakesRender};