@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.
- package/CONTEXT.md +6 -6
- package/SKILL.md +7 -7
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +25 -20
- package/board/domain.mjs +7 -7
- package/board/public/app-cards.js +31 -8
- package/board/public/app.js +22 -2
- package/board/public/dialogs.js +2 -2
- package/board/public/new-task.js +2 -2
- package/board/public/prototype-kit.js +1 -0
- package/board/public/prototype.html +6 -0
- package/board/public/style.css +7 -2
- package/board/public/ui-cards.js +11 -3
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui.js +10 -2
- package/board/public/view-questions.js +8 -4
- package/board/public/view-settings.js +9 -3
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +17 -17
- package/board/store.mjs +32 -32
- package/hub/hub.mjs +11 -11
- package/hub/presence.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +4 -2
- package/hub/try.mjs +12 -11
- package/hub/upgrade.mjs +1 -1
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
package/board/public/ui-cards.js
CHANGED
|
@@ -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
|
package/board/public/ui-enums.js
CHANGED
|
@@ -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']),
|
package/board/public/ui.js
CHANGED
|
@@ -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,
|
|
625
|
-
// Maximum parallel planners (docs/adr/2026-11-04-2359-every-card-arriving-in-todo-is-grilled-by-the-board.md)
|
|
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.'));
|
package/board/render-check.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0];let
|
|
3
|
-
`)[0]||
|
|
4
|
-
`)[0]}`):
|
|
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:
|
|
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
|
|
9
|
-
${
|
|
10
|
-
`)}`)}finally{
|
|
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};
|