@tempo-ai/mcp 0.0.105 → 0.0.106
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/README.md +4 -3
- package/dist/bin.js +1 -1
- package/dist/{call-BE2JZROL.js → call-N3UTMHLY.js} +2 -2
- package/dist/{chunk-DBGXBZSQ.js → chunk-52IG2UKP.js} +4 -4
- package/dist/index.js +1 -1
- package/dist/{serve-E62XRLQQ.js → serve-7RSR6WI6.js} +1277 -196
- package/dist/serve-7RSR6WI6.js.map +1 -0
- package/package.json +1 -1
- package/dist/serve-E62XRLQQ.js.map +0 -1
- /package/dist/{call-BE2JZROL.js.map → call-N3UTMHLY.js.map} +0 -0
- /package/dist/{chunk-DBGXBZSQ.js.map → chunk-52IG2UKP.js.map} +0 -0
|
@@ -1342,8 +1342,8 @@ var require_utils = __commonJS({
|
|
|
1342
1342
|
}
|
|
1343
1343
|
return output;
|
|
1344
1344
|
};
|
|
1345
|
-
exports.basename = (
|
|
1346
|
-
const segs =
|
|
1345
|
+
exports.basename = (path62, { windows } = {}) => {
|
|
1346
|
+
const segs = path62.split(windows ? /[\\/]/ : "/");
|
|
1347
1347
|
const last2 = segs[segs.length - 1];
|
|
1348
1348
|
if (last2 === "") {
|
|
1349
1349
|
return segs[segs.length - 2];
|
|
@@ -3645,10 +3645,10 @@ var require_src2 = __commonJS({
|
|
|
3645
3645
|
var fs_1 = __require("fs");
|
|
3646
3646
|
var debug_1 = __importDefault(require_src());
|
|
3647
3647
|
var log = debug_1.default("@kwsites/file-exists");
|
|
3648
|
-
function check(
|
|
3649
|
-
log(`checking %s`,
|
|
3648
|
+
function check(path62, isFile, isDirectory) {
|
|
3649
|
+
log(`checking %s`, path62);
|
|
3650
3650
|
try {
|
|
3651
|
-
const stat11 = fs_1.statSync(
|
|
3651
|
+
const stat11 = fs_1.statSync(path62);
|
|
3652
3652
|
if (stat11.isFile() && isFile) {
|
|
3653
3653
|
log(`[OK] path represents a file`);
|
|
3654
3654
|
return true;
|
|
@@ -3668,8 +3668,8 @@ var require_src2 = __commonJS({
|
|
|
3668
3668
|
throw e;
|
|
3669
3669
|
}
|
|
3670
3670
|
}
|
|
3671
|
-
function exists3(
|
|
3672
|
-
return check(
|
|
3671
|
+
function exists3(path62, type = exports.READABLE) {
|
|
3672
|
+
return check(path62, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
|
|
3673
3673
|
}
|
|
3674
3674
|
exports.exists = exists3;
|
|
3675
3675
|
exports.FILE = 1;
|
|
@@ -5692,7 +5692,7 @@ var require_png = __commonJS({
|
|
|
5692
5692
|
var Parser = require_parser_async();
|
|
5693
5693
|
var Packer = require_packer_async();
|
|
5694
5694
|
var PNGSync = require_png_sync();
|
|
5695
|
-
var
|
|
5695
|
+
var PNG5 = exports.PNG = function(options) {
|
|
5696
5696
|
Stream.call(this);
|
|
5697
5697
|
options = options || {};
|
|
5698
5698
|
this.width = options.width | 0;
|
|
@@ -5721,9 +5721,9 @@ var require_png = __commonJS({
|
|
|
5721
5721
|
this._parser.on("close", this._handleClose.bind(this));
|
|
5722
5722
|
this._packer.on("error", this.emit.bind(this, "error"));
|
|
5723
5723
|
};
|
|
5724
|
-
util.inherits(
|
|
5725
|
-
|
|
5726
|
-
|
|
5724
|
+
util.inherits(PNG5, Stream);
|
|
5725
|
+
PNG5.sync = PNGSync;
|
|
5726
|
+
PNG5.prototype.pack = function() {
|
|
5727
5727
|
if (!this.data || !this.data.length) {
|
|
5728
5728
|
this.emit("error", "No data provided");
|
|
5729
5729
|
return this;
|
|
@@ -5735,7 +5735,7 @@ var require_png = __commonJS({
|
|
|
5735
5735
|
);
|
|
5736
5736
|
return this;
|
|
5737
5737
|
};
|
|
5738
|
-
|
|
5738
|
+
PNG5.prototype.parse = function(data, callback) {
|
|
5739
5739
|
if (callback) {
|
|
5740
5740
|
let onParsed, onError2;
|
|
5741
5741
|
onParsed = function(parsedData) {
|
|
@@ -5753,27 +5753,27 @@ var require_png = __commonJS({
|
|
|
5753
5753
|
this.end(data);
|
|
5754
5754
|
return this;
|
|
5755
5755
|
};
|
|
5756
|
-
|
|
5756
|
+
PNG5.prototype.write = function(data) {
|
|
5757
5757
|
this._parser.write(data);
|
|
5758
5758
|
return true;
|
|
5759
5759
|
};
|
|
5760
|
-
|
|
5760
|
+
PNG5.prototype.end = function(data) {
|
|
5761
5761
|
this._parser.end(data);
|
|
5762
5762
|
};
|
|
5763
|
-
|
|
5763
|
+
PNG5.prototype._metadata = function(metadata) {
|
|
5764
5764
|
this.width = metadata.width;
|
|
5765
5765
|
this.height = metadata.height;
|
|
5766
5766
|
this.emit("metadata", metadata);
|
|
5767
5767
|
};
|
|
5768
|
-
|
|
5768
|
+
PNG5.prototype._gamma = function(gamma) {
|
|
5769
5769
|
this.gamma = gamma;
|
|
5770
5770
|
};
|
|
5771
|
-
|
|
5771
|
+
PNG5.prototype._handleClose = function() {
|
|
5772
5772
|
if (!this._parser.writable && !this._packer.readable) {
|
|
5773
5773
|
this.emit("close");
|
|
5774
5774
|
}
|
|
5775
5775
|
};
|
|
5776
|
-
|
|
5776
|
+
PNG5.bitblt = function(src, dst, srcX, srcY, width, height, deltaX, deltaY) {
|
|
5777
5777
|
srcX |= 0;
|
|
5778
5778
|
srcY |= 0;
|
|
5779
5779
|
width |= 0;
|
|
@@ -5795,11 +5795,11 @@ var require_png = __commonJS({
|
|
|
5795
5795
|
);
|
|
5796
5796
|
}
|
|
5797
5797
|
};
|
|
5798
|
-
|
|
5799
|
-
|
|
5798
|
+
PNG5.prototype.bitblt = function(dst, srcX, srcY, width, height, deltaX, deltaY) {
|
|
5799
|
+
PNG5.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY);
|
|
5800
5800
|
return this;
|
|
5801
5801
|
};
|
|
5802
|
-
|
|
5802
|
+
PNG5.adjustGamma = function(src) {
|
|
5803
5803
|
if (src.gamma) {
|
|
5804
5804
|
for (let y = 0; y < src.height; y++) {
|
|
5805
5805
|
for (let x = 0; x < src.width; x++) {
|
|
@@ -5814,8 +5814,8 @@ var require_png = __commonJS({
|
|
|
5814
5814
|
src.gamma = 0;
|
|
5815
5815
|
}
|
|
5816
5816
|
};
|
|
5817
|
-
|
|
5818
|
-
|
|
5817
|
+
PNG5.prototype.adjustGamma = function() {
|
|
5818
|
+
PNG5.adjustGamma(this);
|
|
5819
5819
|
};
|
|
5820
5820
|
}
|
|
5821
5821
|
});
|
|
@@ -5986,7 +5986,7 @@ var TOOL_MANIFEST = [
|
|
|
5986
5986
|
// ── linear (4) ──────────────────────────────────────────────────────────
|
|
5987
5987
|
...read("linear", ["linear_search_issues", "linear_get_issue"]),
|
|
5988
5988
|
...write("linear", ["linear_update_issue", "linear_add_comment"]),
|
|
5989
|
-
// ── canvas (
|
|
5989
|
+
// ── canvas (30) ─────────────────────────────────────────────────────────
|
|
5990
5990
|
// File/workspace tools — work anywhere the repo checkout lives.
|
|
5991
5991
|
...read("canvas", [
|
|
5992
5992
|
"canvas_validate",
|
|
@@ -6011,7 +6011,7 @@ var TOOL_MANIFEST = [
|
|
|
6011
6011
|
// Visual tools — powered externally by the headless capture hooks
|
|
6012
6012
|
// (disposable devserver + Playwright); error actionably when Chromium
|
|
6013
6013
|
// is not installed.
|
|
6014
|
-
...read("canvas", ["canvas_screenshot"]),
|
|
6014
|
+
...read("canvas", ["canvas_screenshot", "canvas_verify_vrt"]),
|
|
6015
6015
|
...write("canvas", ["canvas_share"]),
|
|
6016
6016
|
// App-internal surface: devserver supervision is the CLI's own plumbing
|
|
6017
6017
|
// (spawned per capture), and the editing-lock bookends coordinate the
|
|
@@ -6443,8 +6443,8 @@ var STAGE_RANK_END = Number.MAX_SAFE_INTEGER;
|
|
|
6443
6443
|
// ../issues-core/share-url.ts
|
|
6444
6444
|
function buildIssueShareUrl(baseUrl, orgSlugOrId, humanId, titleSlug) {
|
|
6445
6445
|
const base = `/${encodeURIComponent(orgSlugOrId)}/issues/${encodeURIComponent(humanId)}`;
|
|
6446
|
-
const
|
|
6447
|
-
const u = new URL(
|
|
6446
|
+
const path62 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
|
|
6447
|
+
const u = new URL(path62, baseUrl);
|
|
6448
6448
|
return u.toString();
|
|
6449
6449
|
}
|
|
6450
6450
|
|
|
@@ -7916,19 +7916,19 @@ function normalizeRepoUrl(url) {
|
|
|
7916
7916
|
const sshMatch = cleaned.match(sshShorthand);
|
|
7917
7917
|
if (sshMatch) {
|
|
7918
7918
|
const host = sshMatch[1];
|
|
7919
|
-
const
|
|
7920
|
-
return finalize(host,
|
|
7919
|
+
const path62 = sshMatch[2];
|
|
7920
|
+
return finalize(host, path62);
|
|
7921
7921
|
}
|
|
7922
7922
|
const protocolMatch = cleaned.match(/^[\w+.-]+:\/\/(?:[^@]+@)?([\w.-]+)\/([\w./-]+)$/);
|
|
7923
7923
|
if (protocolMatch) {
|
|
7924
7924
|
const host = protocolMatch[1];
|
|
7925
|
-
const
|
|
7926
|
-
return finalize(host,
|
|
7925
|
+
const path62 = protocolMatch[2];
|
|
7926
|
+
return finalize(host, path62);
|
|
7927
7927
|
}
|
|
7928
7928
|
return null;
|
|
7929
7929
|
}
|
|
7930
|
-
function finalize(host,
|
|
7931
|
-
let cleaned =
|
|
7930
|
+
function finalize(host, path62) {
|
|
7931
|
+
let cleaned = path62.replace(/\.git$/, "");
|
|
7932
7932
|
cleaned = cleaned.replace(/^\/+|\/+$/g, "");
|
|
7933
7933
|
if (!cleaned) return null;
|
|
7934
7934
|
return `${host}/${cleaned}`.toLowerCase();
|
|
@@ -9528,8 +9528,8 @@ var api2 = anyApi2;
|
|
|
9528
9528
|
// ../doc-editor/src/client/docShareUrl.ts
|
|
9529
9529
|
function buildDocShareUrl(baseUrl, orgSlugOrId, docId, titleSlug) {
|
|
9530
9530
|
const base = `/${encodeURIComponent(orgSlugOrId)}/docs/${encodeURIComponent(docId)}`;
|
|
9531
|
-
const
|
|
9532
|
-
return new URL(
|
|
9531
|
+
const path62 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
|
|
9532
|
+
return new URL(path62, baseUrl).toString();
|
|
9533
9533
|
}
|
|
9534
9534
|
|
|
9535
9535
|
// ../doc-editor/mcp/tools/read.ts
|
|
@@ -16954,6 +16954,7 @@ Screenshot ONE representative storyboard you actually edited \u2014 not every st
|
|
|
16954
16954
|
- A \`canvas_not_open\` error means the user does not have any canvas open in the Design tab. Stop calling \`canvas_screenshot\`, tell the user to open the canvas, and move on \u2014 firing more screenshots will not change the outcome.
|
|
16955
16955
|
- Any typed error (\`storyboard_not_found\`, \`storyboard_empty\`, \`capture_timeout\`, \`capture_threw\`, \`canvas_mismatch\`, \`capture_unsupported\`) \u2014 one try per storyboard. Don't retry, don't broaden to other storyboards, don't fire a second wave. **One exception:** a route storyboard's \`capture_threw\` "could not be cold-captured" means the app dev server isn't running \u2014 call \`start_app_dev_server\` once, then a single retry is warranted (better, pre-flight per the route-storyboard rule above so it never fails).
|
|
16956
16956
|
- If you need to understand the failure, call \`canvas_debug({ canvas_slug, storyboard? })\` once and inspect the diagnostics instead of repeatedly calling \`canvas_screenshot\`.
|
|
16957
|
+
- \`canvas_verify_vrt\` adds its own typed errors, also one try each: \`storyboard_not_tested\` (add a literal \`vrt\` prop first), \`storyboard_paused\` (remove \`skip: true\` before verifying), \`storyboard_unsupported\` (route/HTML storyboards are not verified \u2014 component storyboards only), \`storyboard_invalid\` (parse error, or \`vrt.ignore\` masks \u226590% of the board), \`capture_incomplete\` / \`compare_failed\` (the batch or the pixel comparison broke \u2014 \`canvas_debug\` once), \`determinism_setup_failed\` (the browser could not install frozen clocks/timezone \u2014 an environment problem, not the storyboard's). An \`unstable\` verdict is NOT an error: fix the nondeterministic input and rerun.
|
|
16957
16958
|
|
|
16958
16959
|
Quality checklist:
|
|
16959
16960
|
|
|
@@ -16978,6 +16979,27 @@ For UI that already exists, measure instead of guessing:
|
|
|
16978
16979
|
|
|
16979
16980
|
---
|
|
16980
16981
|
|
|
16982
|
+
## 9.2 VRT stability (\`canvas_verify_vrt\`)
|
|
16983
|
+
|
|
16984
|
+
A storyboard carrying the literal \`vrt\` prop is a **tested** storyboard: the visual-review workflow captures it on every push and diffs it against a baseline. A tested storyboard that renders differently on each mount (live clocks, \`Math.random\`, unseeded fixtures, in-flight animation, network-dependent content) flakes the \`visual-review\` status for every PR after yours. So after adding or changing \`vrt\`, run
|
|
16985
|
+
|
|
16986
|
+
\`\`\`
|
|
16987
|
+
canvas_verify_vrt({ canvas_slug, storyboard })
|
|
16988
|
+
\`\`\`
|
|
16989
|
+
|
|
16990
|
+
once per tested storyboard you touched, and keep it in the same session as the edit.
|
|
16991
|
+
|
|
16992
|
+
What it does: one capture session, \`runs\` (default 3, max 10) fresh isolated mounts at full resolution, each compared with run 1 under the exact visual-review policy \u2014 VRT's frozen clocks, seeded randomness and motion controls on the render side; the same AA-tolerant pixel matcher, the storyboard's \`threshold\` (default 0.1), and its \`ignore\` masks on the compare side. Nothing is retained between runs except run 1 and the running diff. It measures rendered output only; it never reads or edits source.
|
|
16993
|
+
|
|
16994
|
+
Reading the result \u2014 only \`vrt_stable\` matters:
|
|
16995
|
+
|
|
16996
|
+
- \`vrt_stable: true\` \u2014 done; the baseline will hold.
|
|
16997
|
+
- \`vrt_stable: false\` \u2014 the response carries \`diff_bounds_device_px\` (union of every change, in device pixels), per-run \`comparisons\`, and a fuchsia **diff-mask image** (transparent = stable, fuchsia = changed in at least one run; masks wider than 1990px are area-resampled so a one-pixel flicker still shows, while the JSON bounds stay in original device pixels). Use the region to walk the storyboard's component dependency graph for the nondeterministic input, replace it with fixed storyboard props / fixtures / a seeded source, and rerun. Raise \`runs\` for a flake that only shows one time in five.
|
|
16998
|
+
|
|
16999
|
+
It accepts only ACTIVE tested component storyboards: no \`vrt\` prop, \`vrt={{ skip: true }}\`, route storyboards, and HTML storyboards are all refused with a typed error (codes in the stop-conditions list above). Like \`canvas_screenshot\`, it fails fast with \`capacity_exceeded\` when every managed devserver slot is pinned \u2014 continue and tell the user to rerun rather than looping.
|
|
17000
|
+
|
|
17001
|
+
---
|
|
17002
|
+
|
|
16981
17003
|
## 10. Constraints
|
|
16982
17004
|
|
|
16983
17005
|
- No UUIDs, no JSON canvas configs
|
|
@@ -17905,9 +17927,9 @@ function buildCanvasInstructions(canvasesDir, options) {
|
|
|
17905
17927
|
Before creating, modifying, restyling, or reviewing ANY UI, call \`asset_list\` FIRST and reuse results per \`usageInstructions\`. Do the same for design system, component library, or UI kit requests; use \`asset_list_libraries\` for a collection overview. To ADD or EDIT asset declarations, FIRST call get_guide({ topic: "tempo-assets-instructions" }) \u2014 \`defineAsset\` declares the TOP-LEVEL component consumers import (\`Select\`, never \`SelectTrigger\`).
|
|
17906
17928
|
|
|
17907
17929
|
`;
|
|
17908
|
-
return `Tempo
|
|
17930
|
+
return `Tempo canvases are code-first. Canvases live under the canvases root ${canvasesDir}/, whose storyboards render PRODUCTION components and routes \u2014 never mocks of existing UI.
|
|
17909
17931
|
|
|
17910
|
-
Before any canvas
|
|
17932
|
+
Before any canvas or tempo.config.json change, you MUST call get_guide({ topic: "tempo-canvas-instructions" }); it is authoritative.
|
|
17911
17933
|
|
|
17912
17934
|
${assetLibrarySection}## Hard invariants
|
|
17913
17935
|
|
|
@@ -17916,12 +17938,13 @@ ${assetLibrarySection}## Hard invariants
|
|
|
17916
17938
|
- Bookend every canvas session: start_working_on_canvas BEFORE the first mutating call (canvas_create included) \u2192 edits \u2192 canvas_screenshot on ONE edited storyboard \u2192 finish_working_on_canvas once. On screenshot failure: canvas_debug once, no retries.
|
|
17917
17939
|
- Write is blocked on *.canvas.tsx \u2014 canvas_create creates, Edit modifies.
|
|
17918
17940
|
- Route storyboards need the user's real app: set_app_dev_command once, start_app_dev_server before screenshots.
|
|
17941
|
+
- After adding/changing \`vrt\`, run canvas_verify_vrt; fix nondeterminism and rerun until \`vrt_stable\`.
|
|
17919
17942
|
- Replicating existing UI (live page, Browser tab, URL)? Pixel parity is the goal: capture its screenshot/HTML/styles via the CDP endpoint, loop canvas_screenshot against it \u2014 guide \xA79.1.
|
|
17920
17943
|
- Link new canvases to sibling issue/doc via link_items (branch pushed).`;
|
|
17921
17944
|
}
|
|
17922
17945
|
|
|
17923
17946
|
// ../canvas-mcp/tools.ts
|
|
17924
|
-
import
|
|
17947
|
+
import path55 from "path";
|
|
17925
17948
|
|
|
17926
17949
|
// ../asset-libraries/mcp/tools.ts
|
|
17927
17950
|
import { readFile as readFile4, writeFile } from "fs/promises";
|
|
@@ -19782,7 +19805,7 @@ function parseCanvasFile(filePath, source) {
|
|
|
19782
19805
|
"name",
|
|
19783
19806
|
objectStringBindings
|
|
19784
19807
|
);
|
|
19785
|
-
const
|
|
19808
|
+
const path62 = readStringAttribute(openingElement, "path");
|
|
19786
19809
|
const backgroundColor = readResolvableStringAttribute(
|
|
19787
19810
|
openingElement,
|
|
19788
19811
|
"backgroundColor",
|
|
@@ -19801,7 +19824,7 @@ function parseCanvasFile(filePath, source) {
|
|
|
19801
19824
|
}
|
|
19802
19825
|
return {
|
|
19803
19826
|
filePath,
|
|
19804
|
-
canvasPath:
|
|
19827
|
+
canvasPath: path62?.value,
|
|
19805
19828
|
pageName: name?.value ?? fallbackPageName(filePath),
|
|
19806
19829
|
backgroundColor: backgroundColor?.value,
|
|
19807
19830
|
canvasElementSpan: toSpan(canvasElement),
|
|
@@ -19809,8 +19832,8 @@ function parseCanvasFile(filePath, source) {
|
|
|
19809
19832
|
canvasChildrenInsertionOffset: typeof canvasElement?.closingElement?.start === "number" ? canvasElement.closingElement.start : void 0,
|
|
19810
19833
|
nameSpan: name?.valueSpan,
|
|
19811
19834
|
nameAttributeSpan: name?.attributeSpan,
|
|
19812
|
-
pathSpan:
|
|
19813
|
-
pathAttributeSpan:
|
|
19835
|
+
pathSpan: path62?.valueSpan,
|
|
19836
|
+
pathAttributeSpan: path62?.attributeSpan,
|
|
19814
19837
|
backgroundColorSpan: backgroundColor?.valueSpan,
|
|
19815
19838
|
backgroundColorAttributeSpan: backgroundColor?.attributeSpan,
|
|
19816
19839
|
imports: imports.all,
|
|
@@ -20678,7 +20701,7 @@ function adaptReactCanvasPage(filePath, source) {
|
|
|
20678
20701
|
...storyboard.route ? { route: storyboard.route } : {},
|
|
20679
20702
|
...storyboard.filepath ? { filepath: storyboard.filepath } : {},
|
|
20680
20703
|
...storyboard.src ? { src: storyboard.src } : {},
|
|
20681
|
-
...storyboard.vrt ? { vrt: true } : {},
|
|
20704
|
+
...storyboard.vrt ? { vrt: true, vrtConfig: storyboard.vrt } : {},
|
|
20682
20705
|
layoutSpans: toLegacyLayoutSpans(storyboard.layoutSpans),
|
|
20683
20706
|
...storyboard.errors.length > 0 ? {
|
|
20684
20707
|
error: storyboard.errors.map((error) => error.message).join("; ")
|
|
@@ -22474,7 +22497,7 @@ function createQueuedFileEditor() {
|
|
|
22474
22497
|
};
|
|
22475
22498
|
return runAtIndex(0);
|
|
22476
22499
|
}
|
|
22477
|
-
async function
|
|
22500
|
+
async function readFile27(absolutePath) {
|
|
22478
22501
|
return withFileLock(absolutePath, () => readFileUnlocked(absolutePath));
|
|
22479
22502
|
}
|
|
22480
22503
|
async function readBinaryFile(absolutePath) {
|
|
@@ -22560,7 +22583,7 @@ function createQueuedFileEditor() {
|
|
|
22560
22583
|
});
|
|
22561
22584
|
}
|
|
22562
22585
|
return {
|
|
22563
|
-
readFile:
|
|
22586
|
+
readFile: readFile27,
|
|
22564
22587
|
readBinaryFile,
|
|
22565
22588
|
readFileIfExists,
|
|
22566
22589
|
writeFile: writeFile11,
|
|
@@ -22572,7 +22595,7 @@ function createQueuedFileEditor() {
|
|
|
22572
22595
|
unlinkFile,
|
|
22573
22596
|
withFileLock,
|
|
22574
22597
|
withFileLocks,
|
|
22575
|
-
readWorkspaceFile: (workspacePath, relativePath) =>
|
|
22598
|
+
readWorkspaceFile: (workspacePath, relativePath) => readFile27(resolveWorkspaceRelativePath(workspacePath, relativePath)),
|
|
22576
22599
|
readWorkspaceFileIfExists: (workspacePath, relativePath) => readFileIfExists(resolveWorkspaceRelativePath(workspacePath, relativePath)),
|
|
22577
22600
|
writeWorkspaceFile: (workspacePath, relativePath, content, expectedMtime, nextMtime) => writeFile11(
|
|
22578
22601
|
resolveWorkspaceRelativePath(workspacePath, relativePath),
|
|
@@ -24580,8 +24603,8 @@ function extractImporterFromLine(line) {
|
|
|
24580
24603
|
}
|
|
24581
24604
|
return null;
|
|
24582
24605
|
}
|
|
24583
|
-
function normalizeImporterPath(
|
|
24584
|
-
return
|
|
24606
|
+
function normalizeImporterPath(path62) {
|
|
24607
|
+
return path62.replace(/^\.\//, "");
|
|
24585
24608
|
}
|
|
24586
24609
|
|
|
24587
24610
|
// ../tempo-devserver/port-listener.ts
|
|
@@ -26718,10 +26741,10 @@ async function scanTree(realRoot, ignorePaths = []) {
|
|
|
26718
26741
|
for (let i = 0; i < pendingStats.length; i += STAT_CONCURRENCY) {
|
|
26719
26742
|
const slice = pendingStats.slice(i, i + STAT_CONCURRENCY);
|
|
26720
26743
|
await Promise.all(
|
|
26721
|
-
slice.map(async (
|
|
26744
|
+
slice.map(async (path62) => {
|
|
26722
26745
|
try {
|
|
26723
|
-
const st = await lstat(
|
|
26724
|
-
scan.set(
|
|
26746
|
+
const st = await lstat(path62);
|
|
26747
|
+
scan.set(path62, {
|
|
26725
26748
|
mtimeMs: st.mtimeMs,
|
|
26726
26749
|
size: st.size,
|
|
26727
26750
|
isDir: false
|
|
@@ -26735,16 +26758,16 @@ async function scanTree(realRoot, ignorePaths = []) {
|
|
|
26735
26758
|
}
|
|
26736
26759
|
function diffScans(previous, next) {
|
|
26737
26760
|
const events = [];
|
|
26738
|
-
for (const [
|
|
26739
|
-
const before = previous.get(
|
|
26761
|
+
for (const [path62, now] of next) {
|
|
26762
|
+
const before = previous.get(path62);
|
|
26740
26763
|
if (!before) {
|
|
26741
|
-
events.push({ path:
|
|
26764
|
+
events.push({ path: path62, type: "create" });
|
|
26742
26765
|
} else if (!now.isDir && (before.mtimeMs !== now.mtimeMs || before.size !== now.size)) {
|
|
26743
|
-
events.push({ path:
|
|
26766
|
+
events.push({ path: path62, type: "update" });
|
|
26744
26767
|
}
|
|
26745
26768
|
}
|
|
26746
|
-
for (const
|
|
26747
|
-
if (!next.has(
|
|
26769
|
+
for (const path62 of previous.keys()) {
|
|
26770
|
+
if (!next.has(path62)) events.push({ path: path62, type: "delete" });
|
|
26748
26771
|
}
|
|
26749
26772
|
return events;
|
|
26750
26773
|
}
|
|
@@ -27229,7 +27252,7 @@ async function parcelEventToFileEvent(event, rootInfo) {
|
|
|
27229
27252
|
// ../tempo-sdk/package.json
|
|
27230
27253
|
var package_default = {
|
|
27231
27254
|
name: "tempo-sdk",
|
|
27232
|
-
version: "0.0.
|
|
27255
|
+
version: "0.0.38",
|
|
27233
27256
|
type: "module",
|
|
27234
27257
|
description: "Tempo SDK \u2014 Vite, Next.js, and Expo plugins for JSX annotation and shared page/storyboard types",
|
|
27235
27258
|
repository: {
|
|
@@ -33962,8 +33985,8 @@ function pathspec(...paths) {
|
|
|
33962
33985
|
cache.set(key, paths);
|
|
33963
33986
|
return key;
|
|
33964
33987
|
}
|
|
33965
|
-
function isPathSpec(
|
|
33966
|
-
return
|
|
33988
|
+
function isPathSpec(path62) {
|
|
33989
|
+
return path62 instanceof String && cache.has(path62);
|
|
33967
33990
|
}
|
|
33968
33991
|
function toPaths(pathSpec) {
|
|
33969
33992
|
return cache.get(pathSpec) || [];
|
|
@@ -34052,8 +34075,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
|
|
|
34052
34075
|
function forEachLineWithContent(input, callback) {
|
|
34053
34076
|
return toLinesWithContent(input, true).map((line) => callback(line));
|
|
34054
34077
|
}
|
|
34055
|
-
function folderExists(
|
|
34056
|
-
return (0, import_file_exists.exists)(
|
|
34078
|
+
function folderExists(path62) {
|
|
34079
|
+
return (0, import_file_exists.exists)(path62, import_file_exists.FOLDER);
|
|
34057
34080
|
}
|
|
34058
34081
|
function append(target, item) {
|
|
34059
34082
|
if (Array.isArray(target)) {
|
|
@@ -34457,8 +34480,8 @@ function checkIsRepoRootTask() {
|
|
|
34457
34480
|
commands,
|
|
34458
34481
|
format: "utf-8",
|
|
34459
34482
|
onError,
|
|
34460
|
-
parser(
|
|
34461
|
-
return /^\.(git)?$/.test(
|
|
34483
|
+
parser(path62) {
|
|
34484
|
+
return /^\.(git)?$/.test(path62.trim());
|
|
34462
34485
|
}
|
|
34463
34486
|
};
|
|
34464
34487
|
}
|
|
@@ -34892,11 +34915,11 @@ function parseGrep(grep) {
|
|
|
34892
34915
|
const paths = /* @__PURE__ */ new Set();
|
|
34893
34916
|
const results = {};
|
|
34894
34917
|
forEachLineWithContent(grep, (input) => {
|
|
34895
|
-
const [
|
|
34896
|
-
paths.add(
|
|
34897
|
-
(results[
|
|
34918
|
+
const [path62, line, preview] = input.split(NULL);
|
|
34919
|
+
paths.add(path62);
|
|
34920
|
+
(results[path62] = results[path62] || []).push({
|
|
34898
34921
|
line: asNumber(line),
|
|
34899
|
-
path:
|
|
34922
|
+
path: path62,
|
|
34900
34923
|
preview
|
|
34901
34924
|
});
|
|
34902
34925
|
});
|
|
@@ -35661,14 +35684,14 @@ var init_hash_object = __esm({
|
|
|
35661
35684
|
init_task();
|
|
35662
35685
|
}
|
|
35663
35686
|
});
|
|
35664
|
-
function parseInit(bare,
|
|
35687
|
+
function parseInit(bare, path62, text) {
|
|
35665
35688
|
const response = String(text).trim();
|
|
35666
35689
|
let result;
|
|
35667
35690
|
if (result = initResponseRegex.exec(response)) {
|
|
35668
|
-
return new InitSummary(bare,
|
|
35691
|
+
return new InitSummary(bare, path62, false, result[1]);
|
|
35669
35692
|
}
|
|
35670
35693
|
if (result = reInitResponseRegex.exec(response)) {
|
|
35671
|
-
return new InitSummary(bare,
|
|
35694
|
+
return new InitSummary(bare, path62, true, result[1]);
|
|
35672
35695
|
}
|
|
35673
35696
|
let gitDir = "";
|
|
35674
35697
|
const tokens = response.split(" ");
|
|
@@ -35679,7 +35702,7 @@ function parseInit(bare, path61, text) {
|
|
|
35679
35702
|
break;
|
|
35680
35703
|
}
|
|
35681
35704
|
}
|
|
35682
|
-
return new InitSummary(bare,
|
|
35705
|
+
return new InitSummary(bare, path62, /^re/i.test(response), gitDir);
|
|
35683
35706
|
}
|
|
35684
35707
|
var InitSummary;
|
|
35685
35708
|
var initResponseRegex;
|
|
@@ -35688,9 +35711,9 @@ var init_InitSummary = __esm({
|
|
|
35688
35711
|
"src/lib/responses/InitSummary.ts"() {
|
|
35689
35712
|
"use strict";
|
|
35690
35713
|
InitSummary = class {
|
|
35691
|
-
constructor(bare,
|
|
35714
|
+
constructor(bare, path62, existing, gitDir) {
|
|
35692
35715
|
this.bare = bare;
|
|
35693
|
-
this.path =
|
|
35716
|
+
this.path = path62;
|
|
35694
35717
|
this.existing = existing;
|
|
35695
35718
|
this.gitDir = gitDir;
|
|
35696
35719
|
}
|
|
@@ -35702,7 +35725,7 @@ var init_InitSummary = __esm({
|
|
|
35702
35725
|
function hasBareCommand(command) {
|
|
35703
35726
|
return command.includes(bareCommand);
|
|
35704
35727
|
}
|
|
35705
|
-
function initTask(bare = false,
|
|
35728
|
+
function initTask(bare = false, path62, customArgs) {
|
|
35706
35729
|
const commands = ["init", ...customArgs];
|
|
35707
35730
|
if (bare && !hasBareCommand(commands)) {
|
|
35708
35731
|
commands.splice(1, 0, bareCommand);
|
|
@@ -35711,7 +35734,7 @@ function initTask(bare = false, path61, customArgs) {
|
|
|
35711
35734
|
commands,
|
|
35712
35735
|
format: "utf-8",
|
|
35713
35736
|
parser(text) {
|
|
35714
|
-
return parseInit(commands.includes("--bare"),
|
|
35737
|
+
return parseInit(commands.includes("--bare"), path62, text);
|
|
35715
35738
|
}
|
|
35716
35739
|
};
|
|
35717
35740
|
}
|
|
@@ -36527,12 +36550,12 @@ var init_FileStatusSummary = __esm({
|
|
|
36527
36550
|
"use strict";
|
|
36528
36551
|
fromPathRegex = /^(.+)\0(.+)$/;
|
|
36529
36552
|
FileStatusSummary = class {
|
|
36530
|
-
constructor(
|
|
36531
|
-
this.path =
|
|
36553
|
+
constructor(path62, index, working_dir) {
|
|
36554
|
+
this.path = path62;
|
|
36532
36555
|
this.index = index;
|
|
36533
36556
|
this.working_dir = working_dir;
|
|
36534
36557
|
if (index === "R" || working_dir === "R") {
|
|
36535
|
-
const detail = fromPathRegex.exec(
|
|
36558
|
+
const detail = fromPathRegex.exec(path62) || [null, path62, path62];
|
|
36536
36559
|
this.from = detail[2] || "";
|
|
36537
36560
|
this.path = detail[1] || "";
|
|
36538
36561
|
}
|
|
@@ -36563,14 +36586,14 @@ function splitLine(result, lineStr) {
|
|
|
36563
36586
|
default:
|
|
36564
36587
|
return;
|
|
36565
36588
|
}
|
|
36566
|
-
function data(index, workingDir,
|
|
36589
|
+
function data(index, workingDir, path62) {
|
|
36567
36590
|
const raw = `${index}${workingDir}`;
|
|
36568
36591
|
const handler = parsers6.get(raw);
|
|
36569
36592
|
if (handler) {
|
|
36570
|
-
handler(result,
|
|
36593
|
+
handler(result, path62);
|
|
36571
36594
|
}
|
|
36572
36595
|
if (raw !== "##" && raw !== "!!") {
|
|
36573
|
-
result.files.push(new FileStatusSummary(
|
|
36596
|
+
result.files.push(new FileStatusSummary(path62, index, workingDir));
|
|
36574
36597
|
}
|
|
36575
36598
|
}
|
|
36576
36599
|
}
|
|
@@ -36879,9 +36902,9 @@ var init_simple_git_api = __esm({
|
|
|
36879
36902
|
next
|
|
36880
36903
|
);
|
|
36881
36904
|
}
|
|
36882
|
-
hashObject(
|
|
36905
|
+
hashObject(path62, write2) {
|
|
36883
36906
|
return this._runTask(
|
|
36884
|
-
hashObjectTask(
|
|
36907
|
+
hashObjectTask(path62, write2 === true),
|
|
36885
36908
|
trailingFunctionArgument(arguments)
|
|
36886
36909
|
);
|
|
36887
36910
|
}
|
|
@@ -37234,8 +37257,8 @@ var init_branch = __esm({
|
|
|
37234
37257
|
}
|
|
37235
37258
|
});
|
|
37236
37259
|
function toPath(input) {
|
|
37237
|
-
const
|
|
37238
|
-
return
|
|
37260
|
+
const path62 = input.trim().replace(/^["']|["']$/g, "");
|
|
37261
|
+
return path62 && normalize(path62);
|
|
37239
37262
|
}
|
|
37240
37263
|
var parseCheckIgnore;
|
|
37241
37264
|
var init_CheckIgnore = __esm({
|
|
@@ -37549,8 +37572,8 @@ __export(sub_module_exports, {
|
|
|
37549
37572
|
subModuleTask: () => subModuleTask,
|
|
37550
37573
|
updateSubModuleTask: () => updateSubModuleTask
|
|
37551
37574
|
});
|
|
37552
|
-
function addSubModuleTask(repo,
|
|
37553
|
-
return subModuleTask(["add", repo,
|
|
37575
|
+
function addSubModuleTask(repo, path62) {
|
|
37576
|
+
return subModuleTask(["add", repo, path62]);
|
|
37554
37577
|
}
|
|
37555
37578
|
function initSubModuleTask(customArgs) {
|
|
37556
37579
|
return subModuleTask(["init", ...customArgs]);
|
|
@@ -37883,8 +37906,8 @@ var require_git = __commonJS2({
|
|
|
37883
37906
|
}
|
|
37884
37907
|
return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
|
|
37885
37908
|
};
|
|
37886
|
-
Git2.prototype.submoduleAdd = function(repo,
|
|
37887
|
-
return this._runTask(addSubModuleTask2(repo,
|
|
37909
|
+
Git2.prototype.submoduleAdd = function(repo, path62, then) {
|
|
37910
|
+
return this._runTask(addSubModuleTask2(repo, path62), trailingFunctionArgument2(arguments));
|
|
37888
37911
|
};
|
|
37889
37912
|
Git2.prototype.submoduleUpdate = function(args, then) {
|
|
37890
37913
|
return this._runTask(
|
|
@@ -42412,17 +42435,17 @@ var APP_LAYOUT_CANDIDATES = [
|
|
|
42412
42435
|
"src/app/layout.js"
|
|
42413
42436
|
];
|
|
42414
42437
|
var PAGES_DIRECTORY_CANDIDATES = ["pages", "src/pages"];
|
|
42415
|
-
async function fileExists3(
|
|
42438
|
+
async function fileExists3(path62) {
|
|
42416
42439
|
try {
|
|
42417
|
-
await access11(
|
|
42440
|
+
await access11(path62);
|
|
42418
42441
|
return true;
|
|
42419
42442
|
} catch {
|
|
42420
42443
|
return false;
|
|
42421
42444
|
}
|
|
42422
42445
|
}
|
|
42423
|
-
async function directoryExists(
|
|
42446
|
+
async function directoryExists(path62) {
|
|
42424
42447
|
try {
|
|
42425
|
-
const stats = await stat6(
|
|
42448
|
+
const stats = await stat6(path62);
|
|
42426
42449
|
return stats.isDirectory();
|
|
42427
42450
|
} catch {
|
|
42428
42451
|
return false;
|
|
@@ -42559,9 +42582,9 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
|
42559
42582
|
]);
|
|
42560
42583
|
var DEFAULT_SCAN_ROOTS = ["apps", "packages"];
|
|
42561
42584
|
var MAX_SCAN_DEPTH = 8;
|
|
42562
|
-
async function pathExists9(
|
|
42585
|
+
async function pathExists9(path62) {
|
|
42563
42586
|
try {
|
|
42564
|
-
await access12(
|
|
42587
|
+
await access12(path62);
|
|
42565
42588
|
return true;
|
|
42566
42589
|
} catch {
|
|
42567
42590
|
return false;
|
|
@@ -42696,9 +42719,9 @@ var LOCK_FILE_PRIORITY = [
|
|
|
42696
42719
|
packageManager: PackageManager.NPM
|
|
42697
42720
|
}
|
|
42698
42721
|
];
|
|
42699
|
-
async function fileExists4(
|
|
42722
|
+
async function fileExists4(path62) {
|
|
42700
42723
|
try {
|
|
42701
|
-
await access13(
|
|
42724
|
+
await access13(path62);
|
|
42702
42725
|
return true;
|
|
42703
42726
|
} catch {
|
|
42704
42727
|
return false;
|
|
@@ -42749,9 +42772,9 @@ var VITE_CONFIG_FILES = [
|
|
|
42749
42772
|
"vite.config.mjs",
|
|
42750
42773
|
"vite.config.mts"
|
|
42751
42774
|
];
|
|
42752
|
-
async function fileExists5(
|
|
42775
|
+
async function fileExists5(path62) {
|
|
42753
42776
|
try {
|
|
42754
|
-
await access14(
|
|
42777
|
+
await access14(path62);
|
|
42755
42778
|
return true;
|
|
42756
42779
|
} catch {
|
|
42757
42780
|
return false;
|
|
@@ -45041,6 +45064,558 @@ async function runCanvasValidation(args, options) {
|
|
|
45041
45064
|
};
|
|
45042
45065
|
}
|
|
45043
45066
|
|
|
45067
|
+
// ../canvas-mcp/storyboard-stability.ts
|
|
45068
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
45069
|
+
import path54 from "path";
|
|
45070
|
+
|
|
45071
|
+
// ../../node_modules/.pnpm/pixelmatch@7.1.0/node_modules/pixelmatch/index.js
|
|
45072
|
+
function pixelmatch(img1, img2, output, width, height, options = {}) {
|
|
45073
|
+
const {
|
|
45074
|
+
threshold = 0.1,
|
|
45075
|
+
alpha = 0.1,
|
|
45076
|
+
aaColor = [255, 255, 0],
|
|
45077
|
+
diffColor = [255, 0, 0],
|
|
45078
|
+
includeAA,
|
|
45079
|
+
diffColorAlt,
|
|
45080
|
+
diffMask
|
|
45081
|
+
} = options;
|
|
45082
|
+
if (!isPixelData(img1) || !isPixelData(img2) || output && !isPixelData(output))
|
|
45083
|
+
throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");
|
|
45084
|
+
if (img1.length !== img2.length || output && output.length !== img1.length)
|
|
45085
|
+
throw new Error("Image sizes do not match.");
|
|
45086
|
+
if (img1.length !== width * height * 4) throw new Error("Image data size does not match width/height.");
|
|
45087
|
+
const len = width * height;
|
|
45088
|
+
const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
|
|
45089
|
+
const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
|
|
45090
|
+
let identical = true;
|
|
45091
|
+
for (let i = 0; i < len; i++) {
|
|
45092
|
+
if (a32[i] !== b32[i]) {
|
|
45093
|
+
identical = false;
|
|
45094
|
+
break;
|
|
45095
|
+
}
|
|
45096
|
+
}
|
|
45097
|
+
if (identical) {
|
|
45098
|
+
if (output && !diffMask) {
|
|
45099
|
+
for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, alpha, output);
|
|
45100
|
+
}
|
|
45101
|
+
return 0;
|
|
45102
|
+
}
|
|
45103
|
+
const maxDelta = 35215 * threshold * threshold;
|
|
45104
|
+
const [aaR, aaG, aaB] = aaColor;
|
|
45105
|
+
const [diffR, diffG, diffB] = diffColor;
|
|
45106
|
+
const [altR, altG, altB] = diffColorAlt || diffColor;
|
|
45107
|
+
let diff = 0;
|
|
45108
|
+
for (let y = 0; y < height; y++) {
|
|
45109
|
+
for (let x = 0; x < width; x++) {
|
|
45110
|
+
const i = y * width + x;
|
|
45111
|
+
const pos = i * 4;
|
|
45112
|
+
const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, false);
|
|
45113
|
+
if (Math.abs(delta) > maxDelta) {
|
|
45114
|
+
const isAA = antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32);
|
|
45115
|
+
if (!includeAA && isAA) {
|
|
45116
|
+
if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);
|
|
45117
|
+
} else {
|
|
45118
|
+
if (output) {
|
|
45119
|
+
if (delta < 0) {
|
|
45120
|
+
drawPixel(output, pos, altR, altG, altB);
|
|
45121
|
+
} else {
|
|
45122
|
+
drawPixel(output, pos, diffR, diffG, diffB);
|
|
45123
|
+
}
|
|
45124
|
+
}
|
|
45125
|
+
diff++;
|
|
45126
|
+
}
|
|
45127
|
+
} else if (output && !diffMask) {
|
|
45128
|
+
drawGrayPixel(img1, pos, alpha, output);
|
|
45129
|
+
}
|
|
45130
|
+
}
|
|
45131
|
+
}
|
|
45132
|
+
return diff;
|
|
45133
|
+
}
|
|
45134
|
+
function isPixelData(arr) {
|
|
45135
|
+
return ArrayBuffer.isView(arr) && arr.BYTES_PER_ELEMENT === 1;
|
|
45136
|
+
}
|
|
45137
|
+
function antialiased(img, x1, y1, width, height, a32, b32) {
|
|
45138
|
+
const x0 = Math.max(x1 - 1, 0);
|
|
45139
|
+
const y0 = Math.max(y1 - 1, 0);
|
|
45140
|
+
const x2 = Math.min(x1 + 1, width - 1);
|
|
45141
|
+
const y2 = Math.min(y1 + 1, height - 1);
|
|
45142
|
+
const pos = y1 * width + x1;
|
|
45143
|
+
let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
|
|
45144
|
+
let min = 0;
|
|
45145
|
+
let max = 0;
|
|
45146
|
+
let minX = 0;
|
|
45147
|
+
let minY = 0;
|
|
45148
|
+
let maxX = 0;
|
|
45149
|
+
let maxY = 0;
|
|
45150
|
+
for (let x = x0; x <= x2; x++) {
|
|
45151
|
+
for (let y = y0; y <= y2; y++) {
|
|
45152
|
+
if (x === x1 && y === y1) continue;
|
|
45153
|
+
const delta = colorDelta(img, img, pos * 4, (y * width + x) * 4, true);
|
|
45154
|
+
if (delta === 0) {
|
|
45155
|
+
zeroes++;
|
|
45156
|
+
if (zeroes > 2) return false;
|
|
45157
|
+
} else if (delta < min) {
|
|
45158
|
+
min = delta;
|
|
45159
|
+
minX = x;
|
|
45160
|
+
minY = y;
|
|
45161
|
+
} else if (delta > max) {
|
|
45162
|
+
max = delta;
|
|
45163
|
+
maxX = x;
|
|
45164
|
+
maxY = y;
|
|
45165
|
+
}
|
|
45166
|
+
}
|
|
45167
|
+
}
|
|
45168
|
+
if (min === 0 || max === 0) return false;
|
|
45169
|
+
return hasManySiblings(a32, minX, minY, width, height) && hasManySiblings(b32, minX, minY, width, height) || hasManySiblings(a32, maxX, maxY, width, height) && hasManySiblings(b32, maxX, maxY, width, height);
|
|
45170
|
+
}
|
|
45171
|
+
function hasManySiblings(img, x1, y1, width, height) {
|
|
45172
|
+
const x0 = Math.max(x1 - 1, 0);
|
|
45173
|
+
const y0 = Math.max(y1 - 1, 0);
|
|
45174
|
+
const x2 = Math.min(x1 + 1, width - 1);
|
|
45175
|
+
const y2 = Math.min(y1 + 1, height - 1);
|
|
45176
|
+
const val = img[y1 * width + x1];
|
|
45177
|
+
let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
|
|
45178
|
+
for (let x = x0; x <= x2; x++) {
|
|
45179
|
+
for (let y = y0; y <= y2; y++) {
|
|
45180
|
+
if (x === x1 && y === y1) continue;
|
|
45181
|
+
zeroes += +(val === img[y * width + x]);
|
|
45182
|
+
if (zeroes > 2) return true;
|
|
45183
|
+
}
|
|
45184
|
+
}
|
|
45185
|
+
return false;
|
|
45186
|
+
}
|
|
45187
|
+
function colorDelta(img1, img2, k2, m, yOnly) {
|
|
45188
|
+
const r1 = img1[k2];
|
|
45189
|
+
const g1 = img1[k2 + 1];
|
|
45190
|
+
const b1 = img1[k2 + 2];
|
|
45191
|
+
const a1 = img1[k2 + 3];
|
|
45192
|
+
const r2 = img2[m];
|
|
45193
|
+
const g2 = img2[m + 1];
|
|
45194
|
+
const b2 = img2[m + 2];
|
|
45195
|
+
const a2 = img2[m + 3];
|
|
45196
|
+
let dr = r1 - r2;
|
|
45197
|
+
let dg = g1 - g2;
|
|
45198
|
+
let db = b1 - b2;
|
|
45199
|
+
const da = a1 - a2;
|
|
45200
|
+
if (!dr && !dg && !db && !da) return 0;
|
|
45201
|
+
if (a1 < 255 || a2 < 255) {
|
|
45202
|
+
const rb = 48 + 159 * (k2 % 2);
|
|
45203
|
+
const gb = 48 + 159 * ((k2 / 1.618033988749895 | 0) % 2);
|
|
45204
|
+
const bb = 48 + 159 * ((k2 / 2.618033988749895 | 0) % 2);
|
|
45205
|
+
dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
|
|
45206
|
+
dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
|
|
45207
|
+
db = (b1 * a1 - b2 * a2 - bb * da) / 255;
|
|
45208
|
+
}
|
|
45209
|
+
const y = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
|
|
45210
|
+
if (yOnly) return y;
|
|
45211
|
+
const i = dr * 0.59597799 - dg * 0.2741761 - db * 0.32180189;
|
|
45212
|
+
const q = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
|
|
45213
|
+
const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
|
|
45214
|
+
return y > 0 ? -delta : delta;
|
|
45215
|
+
}
|
|
45216
|
+
function drawPixel(output, pos, r, g, b2) {
|
|
45217
|
+
output[pos + 0] = r;
|
|
45218
|
+
output[pos + 1] = g;
|
|
45219
|
+
output[pos + 2] = b2;
|
|
45220
|
+
output[pos + 3] = 255;
|
|
45221
|
+
}
|
|
45222
|
+
function drawGrayPixel(img, i, alpha, output) {
|
|
45223
|
+
const val = 255 + (img[i] * 0.29889531 + img[i + 1] * 0.58662247 + img[i + 2] * 0.11448223 - 255) * alpha * img[i + 3] / 255;
|
|
45224
|
+
drawPixel(output, i, val, val, val);
|
|
45225
|
+
}
|
|
45226
|
+
|
|
45227
|
+
// ../canvas-mcp/storyboard-stability.ts
|
|
45228
|
+
var import_pngjs = __toESM(require_png(), 1);
|
|
45229
|
+
var DEFAULT_VRT_PIXEL_THRESHOLD = 0.1;
|
|
45230
|
+
var MAX_STABILITY_DIFF_DIMENSION_PX = 1990;
|
|
45231
|
+
var StoryboardStabilityTargetError = class extends Error {
|
|
45232
|
+
constructor(code, message) {
|
|
45233
|
+
super(message);
|
|
45234
|
+
this.name = "StoryboardStabilityTargetError";
|
|
45235
|
+
this.code = code;
|
|
45236
|
+
}
|
|
45237
|
+
};
|
|
45238
|
+
function toPosix(value) {
|
|
45239
|
+
return value.replace(/\\/g, "/");
|
|
45240
|
+
}
|
|
45241
|
+
function absoluteCanvasesRoot(workspaceRoot, canvasesDir) {
|
|
45242
|
+
return path54.isAbsolute(canvasesDir) ? path54.resolve(canvasesDir) : path54.resolve(workspaceRoot, canvasesDir);
|
|
45243
|
+
}
|
|
45244
|
+
async function resolveCanvasFile(options) {
|
|
45245
|
+
const canvasesRoot = absoluteCanvasesRoot(
|
|
45246
|
+
options.workspaceRoot,
|
|
45247
|
+
options.canvasesDir
|
|
45248
|
+
);
|
|
45249
|
+
const identifier = options.canvasSlug;
|
|
45250
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
45251
|
+
const direct = await resolveCanvasFilePath(
|
|
45252
|
+
path54.join(canvasesRoot, identifier)
|
|
45253
|
+
);
|
|
45254
|
+
if (direct) resolved.add(direct);
|
|
45255
|
+
if (resolved.size === 0 && !identifier.includes("/")) {
|
|
45256
|
+
const files = await findPageFilesInPagesRoot(canvasesRoot);
|
|
45257
|
+
for (const file of files) {
|
|
45258
|
+
if (path54.basename(path54.dirname(file)) === identifier) resolved.add(file);
|
|
45259
|
+
}
|
|
45260
|
+
}
|
|
45261
|
+
if (resolved.size === 0) {
|
|
45262
|
+
throw new StoryboardStabilityTargetError(
|
|
45263
|
+
"canvas_not_found",
|
|
45264
|
+
`No canvas matching "${options.canvasSlug}" exists under ${toPosix(canvasesRoot)}.`
|
|
45265
|
+
);
|
|
45266
|
+
}
|
|
45267
|
+
if (resolved.size > 1) {
|
|
45268
|
+
const candidates = [...resolved].map((file) => toPosix(path54.relative(canvasesRoot, path54.dirname(file)))).sort();
|
|
45269
|
+
throw new StoryboardStabilityTargetError(
|
|
45270
|
+
"ambiguous_canvas_slug",
|
|
45271
|
+
`"${options.canvasSlug}" names ${candidates.length} canvases: ${candidates.join(", ")}. Pass the full canvases-root-relative path.`
|
|
45272
|
+
);
|
|
45273
|
+
}
|
|
45274
|
+
const canvasFile = resolved.values().next().value;
|
|
45275
|
+
if (!canvasFile) {
|
|
45276
|
+
throw new StoryboardStabilityTargetError(
|
|
45277
|
+
"canvas_not_found",
|
|
45278
|
+
`No canvas matching "${options.canvasSlug}" exists under ${toPosix(canvasesRoot)}.`
|
|
45279
|
+
);
|
|
45280
|
+
}
|
|
45281
|
+
return canvasFile;
|
|
45282
|
+
}
|
|
45283
|
+
async function resolveTestedStoryboard(options) {
|
|
45284
|
+
const canvasFile = await resolveCanvasFile(options);
|
|
45285
|
+
const source = await readFile24(canvasFile, "utf8");
|
|
45286
|
+
const parsed = parsePageSource(canvasFile, source);
|
|
45287
|
+
const storyboard = parsed.storyboards.find(
|
|
45288
|
+
(candidate) => candidate.storyboardKey === options.storyboard || candidate.exportName === options.storyboard
|
|
45289
|
+
);
|
|
45290
|
+
if (!storyboard) {
|
|
45291
|
+
const available = parsed.storyboards.map((candidate) => candidate.storyboardKey ?? candidate.exportName).join(", ");
|
|
45292
|
+
throw new StoryboardStabilityTargetError(
|
|
45293
|
+
"storyboard_not_found",
|
|
45294
|
+
`Storyboard "${options.storyboard}" was not found in ${toPosix(path54.relative(options.workspaceRoot, canvasFile))}.${available ? ` Available storyboards: ${available}.` : ""}`
|
|
45295
|
+
);
|
|
45296
|
+
}
|
|
45297
|
+
if (storyboard.error) {
|
|
45298
|
+
throw new StoryboardStabilityTargetError(
|
|
45299
|
+
"storyboard_invalid",
|
|
45300
|
+
`Storyboard "${options.storyboard}" cannot be verified: ${storyboard.error}`
|
|
45301
|
+
);
|
|
45302
|
+
}
|
|
45303
|
+
if (storyboard.isRoute || storyboard.isHtml) {
|
|
45304
|
+
throw new StoryboardStabilityTargetError(
|
|
45305
|
+
"storyboard_unsupported",
|
|
45306
|
+
`Storyboard "${options.storyboard}" is not a component storyboard; VRT stability verification currently supports component storyboards only.`
|
|
45307
|
+
);
|
|
45308
|
+
}
|
|
45309
|
+
if (!storyboard.vrt || !storyboard.vrtConfig) {
|
|
45310
|
+
throw new StoryboardStabilityTargetError(
|
|
45311
|
+
"storyboard_not_tested",
|
|
45312
|
+
`Storyboard "${options.storyboard}" is not VRT-tested. Add a literal vrt prop before verifying stability.`
|
|
45313
|
+
);
|
|
45314
|
+
}
|
|
45315
|
+
if (storyboard.vrtConfig.skip) {
|
|
45316
|
+
throw new StoryboardStabilityTargetError(
|
|
45317
|
+
"storyboard_paused",
|
|
45318
|
+
`Storyboard "${options.storyboard}" is paused with vrt={{ skip: true }}. Remove skip before verifying it as an active test.`
|
|
45319
|
+
);
|
|
45320
|
+
}
|
|
45321
|
+
const maskedArea = (storyboard.vrtConfig.ignore ?? []).reduce(
|
|
45322
|
+
(sum, region) => sum + region.w * region.h,
|
|
45323
|
+
0
|
|
45324
|
+
);
|
|
45325
|
+
if (maskedArea >= 0.9 * storyboard.layout.width * storyboard.layout.height) {
|
|
45326
|
+
throw new StoryboardStabilityTargetError(
|
|
45327
|
+
"storyboard_invalid",
|
|
45328
|
+
`Storyboard "${options.storyboard}" has vrt.ignore masking at least 90% of its area; VRT rejects this configuration.`
|
|
45329
|
+
);
|
|
45330
|
+
}
|
|
45331
|
+
return {
|
|
45332
|
+
canvasFile: toPosix(path54.relative(options.workspaceRoot, canvasFile)),
|
|
45333
|
+
canvasSlug: toPosix(
|
|
45334
|
+
path54.relative(
|
|
45335
|
+
absoluteCanvasesRoot(options.workspaceRoot, options.canvasesDir),
|
|
45336
|
+
path54.dirname(canvasFile)
|
|
45337
|
+
)
|
|
45338
|
+
),
|
|
45339
|
+
storyboardId: storyboard.storyboardKey ?? storyboard.exportName,
|
|
45340
|
+
layout: {
|
|
45341
|
+
width: storyboard.layout.width,
|
|
45342
|
+
height: storyboard.layout.height
|
|
45343
|
+
},
|
|
45344
|
+
vrt: storyboard.vrtConfig
|
|
45345
|
+
};
|
|
45346
|
+
}
|
|
45347
|
+
function maskRegion(image, region) {
|
|
45348
|
+
const x0 = Math.max(0, Math.floor(region.x));
|
|
45349
|
+
const y0 = Math.max(0, Math.floor(region.y));
|
|
45350
|
+
const x1 = Math.min(image.width, Math.ceil(region.x + region.w));
|
|
45351
|
+
const y1 = Math.min(image.height, Math.ceil(region.y + region.h));
|
|
45352
|
+
for (let y = y0; y < y1; y += 1) {
|
|
45353
|
+
for (let x = x0; x < x1; x += 1) {
|
|
45354
|
+
const offset = image.width * y + x << 2;
|
|
45355
|
+
image.data[offset] = 0;
|
|
45356
|
+
image.data[offset + 1] = 0;
|
|
45357
|
+
image.data[offset + 2] = 0;
|
|
45358
|
+
image.data[offset + 3] = 255;
|
|
45359
|
+
}
|
|
45360
|
+
}
|
|
45361
|
+
}
|
|
45362
|
+
function findDiffBounds(diff) {
|
|
45363
|
+
let minX = diff.width;
|
|
45364
|
+
let minY = diff.height;
|
|
45365
|
+
let maxX = -1;
|
|
45366
|
+
let maxY = -1;
|
|
45367
|
+
for (let y = 0; y < diff.height; y += 1) {
|
|
45368
|
+
for (let x = 0; x < diff.width; x += 1) {
|
|
45369
|
+
if (diff.data[(diff.width * y + x << 2) + 3] === 0) continue;
|
|
45370
|
+
minX = Math.min(minX, x);
|
|
45371
|
+
minY = Math.min(minY, y);
|
|
45372
|
+
maxX = Math.max(maxX, x);
|
|
45373
|
+
maxY = Math.max(maxY, y);
|
|
45374
|
+
}
|
|
45375
|
+
}
|
|
45376
|
+
return maxX < minX || maxY < minY ? null : {
|
|
45377
|
+
x: minX,
|
|
45378
|
+
y: minY,
|
|
45379
|
+
width: maxX - minX + 1,
|
|
45380
|
+
height: maxY - minY + 1
|
|
45381
|
+
};
|
|
45382
|
+
}
|
|
45383
|
+
function unionBounds(left, right) {
|
|
45384
|
+
if (!left) return right;
|
|
45385
|
+
if (!right) return left;
|
|
45386
|
+
const x = Math.min(left.x, right.x);
|
|
45387
|
+
const y = Math.min(left.y, right.y);
|
|
45388
|
+
const x1 = Math.max(left.x + left.width, right.x + right.width);
|
|
45389
|
+
const y1 = Math.max(left.y + left.height, right.y + right.height);
|
|
45390
|
+
return { x, y, width: x1 - x, height: y1 - y };
|
|
45391
|
+
}
|
|
45392
|
+
function decodeCapture(capture) {
|
|
45393
|
+
const image = import_pngjs.PNG.sync.read(Buffer.from(capture.pngBase64, "base64"));
|
|
45394
|
+
if (image.width !== capture.width || image.height !== capture.height) {
|
|
45395
|
+
throw new Error(
|
|
45396
|
+
`Capture metadata ${capture.width}x${capture.height} does not match decoded PNG ${image.width}x${image.height}.`
|
|
45397
|
+
);
|
|
45398
|
+
}
|
|
45399
|
+
return image;
|
|
45400
|
+
}
|
|
45401
|
+
function compareCaptureToBaseline(baseline, capture, run, options) {
|
|
45402
|
+
const candidate = decodeCapture(capture);
|
|
45403
|
+
if (candidate.width !== baseline.width || candidate.height !== baseline.height) {
|
|
45404
|
+
const width = Math.max(baseline.width, candidate.width);
|
|
45405
|
+
const height = Math.max(baseline.height, candidate.height);
|
|
45406
|
+
const diff2 = new import_pngjs.PNG({ width, height });
|
|
45407
|
+
for (let offset = 0; offset < diff2.data.length; offset += 4) {
|
|
45408
|
+
diff2.data[offset] = 217;
|
|
45409
|
+
diff2.data[offset + 1] = 70;
|
|
45410
|
+
diff2.data[offset + 2] = 239;
|
|
45411
|
+
diff2.data[offset + 3] = 255;
|
|
45412
|
+
}
|
|
45413
|
+
return {
|
|
45414
|
+
diff: diff2,
|
|
45415
|
+
comparison: {
|
|
45416
|
+
run,
|
|
45417
|
+
baselineRun: 1,
|
|
45418
|
+
diffPixels: candidate.width * candidate.height,
|
|
45419
|
+
diffRatio: 1,
|
|
45420
|
+
diffBounds: { x: 0, y: 0, width, height },
|
|
45421
|
+
dimensions: { width: candidate.width, height: candidate.height },
|
|
45422
|
+
dimensionChange: {
|
|
45423
|
+
baseline: { width: baseline.width, height: baseline.height },
|
|
45424
|
+
candidate: { width: candidate.width, height: candidate.height }
|
|
45425
|
+
}
|
|
45426
|
+
}
|
|
45427
|
+
};
|
|
45428
|
+
}
|
|
45429
|
+
const scaleX = baseline.width / options.layout.width;
|
|
45430
|
+
const scaleY = baseline.height / options.layout.height;
|
|
45431
|
+
for (const region of options.ignore) {
|
|
45432
|
+
const scaled = {
|
|
45433
|
+
x: region.x * scaleX,
|
|
45434
|
+
y: region.y * scaleY,
|
|
45435
|
+
w: region.w * scaleX,
|
|
45436
|
+
h: region.h * scaleY
|
|
45437
|
+
};
|
|
45438
|
+
maskRegion(candidate, scaled);
|
|
45439
|
+
}
|
|
45440
|
+
const diff = new import_pngjs.PNG({ width: baseline.width, height: baseline.height });
|
|
45441
|
+
const diffPixels = pixelmatch(
|
|
45442
|
+
baseline.data,
|
|
45443
|
+
candidate.data,
|
|
45444
|
+
diff.data,
|
|
45445
|
+
baseline.width,
|
|
45446
|
+
baseline.height,
|
|
45447
|
+
{
|
|
45448
|
+
threshold: options.threshold,
|
|
45449
|
+
includeAA: false,
|
|
45450
|
+
diffMask: true,
|
|
45451
|
+
diffColor: [217, 70, 239]
|
|
45452
|
+
}
|
|
45453
|
+
);
|
|
45454
|
+
return {
|
|
45455
|
+
diff,
|
|
45456
|
+
comparison: {
|
|
45457
|
+
run,
|
|
45458
|
+
baselineRun: 1,
|
|
45459
|
+
diffPixels,
|
|
45460
|
+
diffRatio: diffPixels / (baseline.width * baseline.height),
|
|
45461
|
+
diffBounds: findDiffBounds(diff),
|
|
45462
|
+
dimensions: { width: candidate.width, height: candidate.height }
|
|
45463
|
+
}
|
|
45464
|
+
};
|
|
45465
|
+
}
|
|
45466
|
+
function mergeDiffImage(current, next) {
|
|
45467
|
+
const width = Math.max(current?.width ?? 0, next.width);
|
|
45468
|
+
const height = Math.max(current?.height ?? 0, next.height);
|
|
45469
|
+
const merged = current && current.width === width && current.height === height ? current : new import_pngjs.PNG({ width, height });
|
|
45470
|
+
const merge = (source) => {
|
|
45471
|
+
for (let y = 0; y < source.height; y += 1) {
|
|
45472
|
+
for (let x = 0; x < source.width; x += 1) {
|
|
45473
|
+
const sourceOffset = source.width * y + x << 2;
|
|
45474
|
+
if (source.data[sourceOffset + 3] === 0) continue;
|
|
45475
|
+
const targetOffset = width * y + x << 2;
|
|
45476
|
+
merged.data[targetOffset] = 217;
|
|
45477
|
+
merged.data[targetOffset + 1] = 70;
|
|
45478
|
+
merged.data[targetOffset + 2] = 239;
|
|
45479
|
+
merged.data[targetOffset + 3] = 255;
|
|
45480
|
+
}
|
|
45481
|
+
}
|
|
45482
|
+
};
|
|
45483
|
+
if (current && merged !== current) merge(current);
|
|
45484
|
+
merge(next);
|
|
45485
|
+
return merged;
|
|
45486
|
+
}
|
|
45487
|
+
function downscaleDiffMaskToFit(image, maxDimension) {
|
|
45488
|
+
if (image.width <= maxDimension && image.height <= maxDimension) return image;
|
|
45489
|
+
const scale = Math.min(
|
|
45490
|
+
maxDimension / image.width,
|
|
45491
|
+
maxDimension / image.height
|
|
45492
|
+
);
|
|
45493
|
+
const width = Math.max(
|
|
45494
|
+
1,
|
|
45495
|
+
Math.min(maxDimension, Math.round(image.width * scale))
|
|
45496
|
+
);
|
|
45497
|
+
const height = Math.max(
|
|
45498
|
+
1,
|
|
45499
|
+
Math.min(maxDimension, Math.round(image.height * scale))
|
|
45500
|
+
);
|
|
45501
|
+
const output = new import_pngjs.PNG({ width, height });
|
|
45502
|
+
for (let y = 0; y < height; y += 1) {
|
|
45503
|
+
const sourceY0 = Math.floor(y * image.height / height);
|
|
45504
|
+
const sourceY1 = Math.max(
|
|
45505
|
+
sourceY0 + 1,
|
|
45506
|
+
Math.ceil((y + 1) * image.height / height)
|
|
45507
|
+
);
|
|
45508
|
+
for (let x = 0; x < width; x += 1) {
|
|
45509
|
+
const sourceX0 = Math.floor(x * image.width / width);
|
|
45510
|
+
const sourceX1 = Math.max(
|
|
45511
|
+
sourceX0 + 1,
|
|
45512
|
+
Math.ceil((x + 1) * image.width / width)
|
|
45513
|
+
);
|
|
45514
|
+
let changed = false;
|
|
45515
|
+
for (let sourceY = sourceY0; sourceY < sourceY1 && !changed; sourceY += 1) {
|
|
45516
|
+
for (let sourceX = sourceX0; sourceX < sourceX1; sourceX += 1) {
|
|
45517
|
+
const sourceOffset = image.width * sourceY + sourceX << 2;
|
|
45518
|
+
if (image.data[sourceOffset + 3] !== 0) {
|
|
45519
|
+
changed = true;
|
|
45520
|
+
break;
|
|
45521
|
+
}
|
|
45522
|
+
}
|
|
45523
|
+
}
|
|
45524
|
+
if (!changed) continue;
|
|
45525
|
+
const outputOffset = width * y + x << 2;
|
|
45526
|
+
output.data[outputOffset] = 217;
|
|
45527
|
+
output.data[outputOffset + 1] = 70;
|
|
45528
|
+
output.data[outputOffset + 2] = 239;
|
|
45529
|
+
output.data[outputOffset + 3] = 255;
|
|
45530
|
+
}
|
|
45531
|
+
}
|
|
45532
|
+
return output;
|
|
45533
|
+
}
|
|
45534
|
+
function createVrtStabilityAccumulator(options) {
|
|
45535
|
+
const threshold = options.threshold ?? DEFAULT_VRT_PIXEL_THRESHOLD;
|
|
45536
|
+
const compareOptions = {
|
|
45537
|
+
threshold,
|
|
45538
|
+
ignore: options.ignore ?? [],
|
|
45539
|
+
layout: options.layout
|
|
45540
|
+
};
|
|
45541
|
+
let baseline = null;
|
|
45542
|
+
let cumulativeDiff = null;
|
|
45543
|
+
let runs = 0;
|
|
45544
|
+
let retriedCaptures = 0;
|
|
45545
|
+
const comparisons = [];
|
|
45546
|
+
return {
|
|
45547
|
+
addCapture(capture) {
|
|
45548
|
+
runs += 1;
|
|
45549
|
+
if (capture.retried === true) retriedCaptures += 1;
|
|
45550
|
+
if (!baseline) {
|
|
45551
|
+
baseline = decodeCapture(capture);
|
|
45552
|
+
const scaleX = baseline.width / compareOptions.layout.width;
|
|
45553
|
+
const scaleY = baseline.height / compareOptions.layout.height;
|
|
45554
|
+
for (const region of compareOptions.ignore) {
|
|
45555
|
+
maskRegion(baseline, {
|
|
45556
|
+
x: region.x * scaleX,
|
|
45557
|
+
y: region.y * scaleY,
|
|
45558
|
+
w: region.w * scaleX,
|
|
45559
|
+
h: region.h * scaleY
|
|
45560
|
+
});
|
|
45561
|
+
}
|
|
45562
|
+
return;
|
|
45563
|
+
}
|
|
45564
|
+
const result = compareCaptureToBaseline(
|
|
45565
|
+
baseline,
|
|
45566
|
+
capture,
|
|
45567
|
+
runs,
|
|
45568
|
+
compareOptions
|
|
45569
|
+
);
|
|
45570
|
+
comparisons.push(result.comparison);
|
|
45571
|
+
if (result.comparison.diffPixels > 0) {
|
|
45572
|
+
cumulativeDiff = mergeDiffImage(cumulativeDiff, result.diff);
|
|
45573
|
+
}
|
|
45574
|
+
},
|
|
45575
|
+
get captureCount() {
|
|
45576
|
+
return runs;
|
|
45577
|
+
},
|
|
45578
|
+
summarize() {
|
|
45579
|
+
if (!baseline || runs < 2) {
|
|
45580
|
+
throw new Error(
|
|
45581
|
+
"VRT stability verification requires at least two captures."
|
|
45582
|
+
);
|
|
45583
|
+
}
|
|
45584
|
+
const changed = comparisons.filter(
|
|
45585
|
+
(comparison) => comparison.diffPixels > 0
|
|
45586
|
+
);
|
|
45587
|
+
const mergedDiff = cumulativeDiff;
|
|
45588
|
+
const outputDiff = mergedDiff ? downscaleDiffMaskToFit(mergedDiff, MAX_STABILITY_DIFF_DIMENSION_PX) : null;
|
|
45589
|
+
return {
|
|
45590
|
+
status: changed.length === 0 ? "stable" : "unstable",
|
|
45591
|
+
vrtStable: changed.length === 0,
|
|
45592
|
+
runs,
|
|
45593
|
+
retriedCaptures,
|
|
45594
|
+
threshold,
|
|
45595
|
+
ignoreRegions: compareOptions.ignore.length,
|
|
45596
|
+
changedRuns: changed.length,
|
|
45597
|
+
maxDiffPixels: Math.max(
|
|
45598
|
+
0,
|
|
45599
|
+
...comparisons.map((item) => item.diffPixels)
|
|
45600
|
+
),
|
|
45601
|
+
maxDiffRatio: Math.max(0, ...comparisons.map((item) => item.diffRatio)),
|
|
45602
|
+
diffBounds: comparisons.reduce(
|
|
45603
|
+
(bounds, comparison) => unionBounds(bounds, comparison.diffBounds),
|
|
45604
|
+
null
|
|
45605
|
+
),
|
|
45606
|
+
cumulativeDiff: outputDiff && mergedDiff ? {
|
|
45607
|
+
pngBase64: import_pngjs.PNG.sync.write(outputDiff).toString("base64"),
|
|
45608
|
+
width: outputDiff.width,
|
|
45609
|
+
height: outputDiff.height,
|
|
45610
|
+
sourceWidth: mergedDiff.width,
|
|
45611
|
+
sourceHeight: mergedDiff.height
|
|
45612
|
+
} : null,
|
|
45613
|
+
comparisons
|
|
45614
|
+
};
|
|
45615
|
+
}
|
|
45616
|
+
};
|
|
45617
|
+
}
|
|
45618
|
+
|
|
45044
45619
|
// ../canvas-mcp/tools.ts
|
|
45045
45620
|
function textResult2(text) {
|
|
45046
45621
|
return { content: [{ type: "text", text }] };
|
|
@@ -45052,12 +45627,12 @@ async function normalizeCanvasSlug(input, workspaceRoot) {
|
|
|
45052
45627
|
const value = input.trim().replace(/\\/g, "/").replace(/\/+$/, "").replace(/\/[^/]*\.canvas\.tsx$/i, "");
|
|
45053
45628
|
if (!value.includes("/")) return value;
|
|
45054
45629
|
const canvasesDir = resolveCanvasesDirSync(workspaceRoot);
|
|
45055
|
-
const absoluteCanvasesDir =
|
|
45056
|
-
const candidates =
|
|
45630
|
+
const absoluteCanvasesDir = path55.isAbsolute(canvasesDir) ? canvasesDir : path55.join(workspaceRoot, canvasesDir);
|
|
45631
|
+
const candidates = path55.isAbsolute(value) ? [value] : [path55.join(workspaceRoot, value), path55.join(absoluteCanvasesDir, value)];
|
|
45057
45632
|
for (const candidate of candidates) {
|
|
45058
45633
|
const dir = await resolveCanvasDirPath(candidate);
|
|
45059
45634
|
if (!dir) continue;
|
|
45060
|
-
const relative3 =
|
|
45635
|
+
const relative3 = path55.relative(absoluteCanvasesDir, dir).replace(/\\/g, "/");
|
|
45061
45636
|
return relative3 && !relative3.startsWith("..") ? relative3 : value;
|
|
45062
45637
|
}
|
|
45063
45638
|
return value;
|
|
@@ -45793,6 +46368,152 @@ function registerTools(server, context) {
|
|
|
45793
46368
|
signal: extra.signal
|
|
45794
46369
|
})
|
|
45795
46370
|
);
|
|
46371
|
+
const stabilityErrorResult = (code, message) => errorResult9(`canvas_verify_vrt failed [${code}]: ${message}`);
|
|
46372
|
+
server.tool(
|
|
46373
|
+
"canvas_verify_vrt",
|
|
46374
|
+
"Check that a VRT-tracked storyboard renders identically across fresh mounts so its baseline won't flake in CI. Args: canvas_slug (folder name, or root-relative path when the name isn't unique), storyboard (export name carrying a literal `vrt` prop; `skip: true` is rejected), runs (2\u201310, default 3). Compares each run to run 1 under the visual-review matcher, threshold, and ignore masks. Returns `vrt_stable`; when false, also changed-pixel bounds and a diff-mask image. Call after adding or changing `vrt`; fix the nondeterministic input and rerun. Read-only. Fails fast with capacity_exceeded; typed errors are not retryable.",
|
|
46375
|
+
{
|
|
46376
|
+
canvas_slug: screenshotSchema.canvas_slug,
|
|
46377
|
+
storyboard: z24.string().describe("Storyboard export name carrying the literal `vrt` prop."),
|
|
46378
|
+
runs: z24.number().int().min(2).max(10).default(3).describe("Number of captures to compare. Defaults to 3.")
|
|
46379
|
+
},
|
|
46380
|
+
async ({ canvas_slug, storyboard, runs }, extra) => {
|
|
46381
|
+
if (!context.canvasStoryboardStability)
|
|
46382
|
+
return unsupportedEnvironmentResult("canvasStoryboardStability");
|
|
46383
|
+
try {
|
|
46384
|
+
const slug = await normalizeCanvasSlug(
|
|
46385
|
+
canvas_slug,
|
|
46386
|
+
context.workspaceRoot
|
|
46387
|
+
);
|
|
46388
|
+
const target = await resolveTestedStoryboard({
|
|
46389
|
+
workspaceRoot: context.workspaceRoot,
|
|
46390
|
+
canvasesDir: context.canvasesDir,
|
|
46391
|
+
canvasSlug: slug,
|
|
46392
|
+
storyboard
|
|
46393
|
+
});
|
|
46394
|
+
if (context.canvasDevserver && !context.canvasStoryboardStability.managesDevserver) {
|
|
46395
|
+
try {
|
|
46396
|
+
await context.canvasDevserver.start();
|
|
46397
|
+
} catch (startError) {
|
|
46398
|
+
return errorResult9(
|
|
46399
|
+
`canvas_verify_vrt could not start the canvas devserver: ${startError instanceof Error ? startError.message : String(startError)}`
|
|
46400
|
+
);
|
|
46401
|
+
}
|
|
46402
|
+
}
|
|
46403
|
+
const accumulator = createVrtStabilityAccumulator({
|
|
46404
|
+
threshold: target.vrt.threshold,
|
|
46405
|
+
ignore: target.vrt.ignore,
|
|
46406
|
+
layout: target.layout
|
|
46407
|
+
});
|
|
46408
|
+
let comparisonError;
|
|
46409
|
+
const result = await context.canvasStoryboardStability.captureRuns(
|
|
46410
|
+
{
|
|
46411
|
+
canvasSlug: target.canvasSlug,
|
|
46412
|
+
canvasFile: target.canvasFile,
|
|
46413
|
+
storyboard: target.storyboardId,
|
|
46414
|
+
runs
|
|
46415
|
+
},
|
|
46416
|
+
{
|
|
46417
|
+
timeoutMs: 12e4,
|
|
46418
|
+
signal: extra.signal,
|
|
46419
|
+
onCapture: (capture) => {
|
|
46420
|
+
if (comparisonError) return;
|
|
46421
|
+
try {
|
|
46422
|
+
accumulator.addCapture(capture);
|
|
46423
|
+
} catch (error) {
|
|
46424
|
+
comparisonError = error;
|
|
46425
|
+
}
|
|
46426
|
+
}
|
|
46427
|
+
}
|
|
46428
|
+
);
|
|
46429
|
+
if (comparisonError) {
|
|
46430
|
+
return stabilityErrorResult(
|
|
46431
|
+
"compare_failed",
|
|
46432
|
+
comparisonError instanceof Error ? comparisonError.message : String(comparisonError)
|
|
46433
|
+
);
|
|
46434
|
+
}
|
|
46435
|
+
if (result.kind === "error") {
|
|
46436
|
+
return stabilityErrorResult(
|
|
46437
|
+
result.code,
|
|
46438
|
+
`${result.message}${result.availableStoryboards && result.availableStoryboards.length > 0 ? ` Available storyboards in the current canvas: ${result.availableStoryboards.join(", ")}.` : ""}`
|
|
46439
|
+
);
|
|
46440
|
+
}
|
|
46441
|
+
if (result.runs !== runs || accumulator.captureCount !== runs) {
|
|
46442
|
+
return stabilityErrorResult(
|
|
46443
|
+
"capture_incomplete",
|
|
46444
|
+
`requested ${runs} fresh captures but received ${accumulator.captureCount}.`
|
|
46445
|
+
);
|
|
46446
|
+
}
|
|
46447
|
+
let summary;
|
|
46448
|
+
try {
|
|
46449
|
+
summary = accumulator.summarize();
|
|
46450
|
+
} catch (error) {
|
|
46451
|
+
return stabilityErrorResult(
|
|
46452
|
+
"compare_failed",
|
|
46453
|
+
error instanceof Error ? error.message : String(error)
|
|
46454
|
+
);
|
|
46455
|
+
}
|
|
46456
|
+
const diagnosis = summary.status === "stable" ? `All ${runs} isolated renders are equivalent under the storyboard's VRT threshold and ignore masks.` : `${summary.changedRuns} of ${runs - 1} comparisons changed under the storyboard's VRT policy; the largest changed ${summary.maxDiffPixels} pixels (${(summary.maxDiffRatio * 100).toFixed(4)}%).`;
|
|
46457
|
+
const content = [];
|
|
46458
|
+
if (summary.cumulativeDiff) {
|
|
46459
|
+
content.push({
|
|
46460
|
+
type: "image",
|
|
46461
|
+
data: summary.cumulativeDiff.pngBase64,
|
|
46462
|
+
mimeType: "image/png"
|
|
46463
|
+
});
|
|
46464
|
+
}
|
|
46465
|
+
content.push({
|
|
46466
|
+
type: "text",
|
|
46467
|
+
text: JSON.stringify(
|
|
46468
|
+
{
|
|
46469
|
+
canvas_slug: target.canvasSlug,
|
|
46470
|
+
canvas_file: target.canvasFile,
|
|
46471
|
+
storyboard: target.storyboardId,
|
|
46472
|
+
vrt: target.vrt,
|
|
46473
|
+
status: summary.status,
|
|
46474
|
+
vrt_stable: summary.vrtStable,
|
|
46475
|
+
runs: summary.runs,
|
|
46476
|
+
retried_captures: summary.retriedCaptures,
|
|
46477
|
+
threshold_used: summary.threshold,
|
|
46478
|
+
ignore_regions: summary.ignoreRegions,
|
|
46479
|
+
changed_runs: summary.changedRuns,
|
|
46480
|
+
max_diff_pixels: summary.maxDiffPixels,
|
|
46481
|
+
max_diff_ratio: summary.maxDiffRatio,
|
|
46482
|
+
diff_bounds_device_px: summary.diffBounds,
|
|
46483
|
+
cumulative_diff_image: summary.cumulativeDiff ? {
|
|
46484
|
+
width: summary.cumulativeDiff.width,
|
|
46485
|
+
height: summary.cumulativeDiff.height,
|
|
46486
|
+
source_width: summary.cumulativeDiff.sourceWidth,
|
|
46487
|
+
source_height: summary.cumulativeDiff.sourceHeight,
|
|
46488
|
+
meaning: "Transparent pixels were stable; fuchsia pixels changed in at least one comparison to run 1. Large masks are area-resampled to fit within 1990px without dropping thin differences."
|
|
46489
|
+
} : null,
|
|
46490
|
+
comparisons: summary.comparisons.map((comparison) => ({
|
|
46491
|
+
run: comparison.run,
|
|
46492
|
+
baseline_run: comparison.baselineRun,
|
|
46493
|
+
diff_pixels: comparison.diffPixels,
|
|
46494
|
+
diff_ratio: comparison.diffRatio,
|
|
46495
|
+
diff_bounds_device_px: comparison.diffBounds,
|
|
46496
|
+
dimensions: comparison.dimensions,
|
|
46497
|
+
...comparison.dimensionChange ? { dimension_change: comparison.dimensionChange } : {}
|
|
46498
|
+
})),
|
|
46499
|
+
diagnosis,
|
|
46500
|
+
next_action: summary.status === "unstable" ? "Inspect the changed region and the storyboard's component dependency graph for nondeterministic render inputs. Replace them with fixed props, fixtures, or seeded inputs, then rerun." : "No deterministic-source change is needed for this storyboard under its current VRT policy."
|
|
46501
|
+
},
|
|
46502
|
+
null,
|
|
46503
|
+
2
|
|
46504
|
+
)
|
|
46505
|
+
});
|
|
46506
|
+
return { content };
|
|
46507
|
+
} catch (error) {
|
|
46508
|
+
if (error instanceof StoryboardStabilityTargetError) {
|
|
46509
|
+
return stabilityErrorResult(error.code, error.message);
|
|
46510
|
+
}
|
|
46511
|
+
return errorResult9(
|
|
46512
|
+
`canvas_verify_vrt failed: ${error instanceof Error ? error.message : String(error)}`
|
|
46513
|
+
);
|
|
46514
|
+
}
|
|
46515
|
+
}
|
|
46516
|
+
);
|
|
45796
46517
|
server.tool(
|
|
45797
46518
|
"canvas_share",
|
|
45798
46519
|
// The implementation lives in `tempo-client/electron/src/main/adapters/
|
|
@@ -45979,26 +46700,20 @@ function buildBackends(runtime, toolsets, workspace) {
|
|
|
45979
46700
|
}
|
|
45980
46701
|
|
|
45981
46702
|
// src/version.ts
|
|
45982
|
-
var CLI_VERSION = "0.0.
|
|
46703
|
+
var CLI_VERSION = "0.0.106";
|
|
45983
46704
|
|
|
45984
46705
|
// src/canvas-hooks.ts
|
|
45985
|
-
var
|
|
45986
|
-
import
|
|
46706
|
+
var import_pngjs4 = __toESM(require_png(), 1);
|
|
46707
|
+
import path61 from "path";
|
|
45987
46708
|
import { stat as stat10 } from "fs/promises";
|
|
45988
46709
|
import { gzipSync } from "zlib";
|
|
45989
46710
|
import { anyApi as anyApi9 } from "convex/server";
|
|
45990
46711
|
|
|
45991
46712
|
// ../canvas-capture-headless/src/capture.ts
|
|
45992
|
-
var
|
|
46713
|
+
var import_pngjs2 = __toESM(require_png(), 1);
|
|
45993
46714
|
import { createHash as createHash4 } from "crypto";
|
|
45994
46715
|
|
|
45995
|
-
//
|
|
45996
|
-
var CHROMIUM_LAUNCH_ARGS = [
|
|
45997
|
-
"--force-color-profile=srgb",
|
|
45998
|
-
"--force-device-scale-factor=2",
|
|
45999
|
-
"--hide-scrollbars",
|
|
46000
|
-
"--disable-lcd-text"
|
|
46001
|
-
];
|
|
46716
|
+
// ../../modules/frame-pool/capture-determinism.ts
|
|
46002
46717
|
var FROZEN_EPOCH_MS = Date.UTC(2026, 0, 1, 12, 0, 0);
|
|
46003
46718
|
function buildFreezeInitScript() {
|
|
46004
46719
|
return `(() => {
|
|
@@ -46018,7 +46733,6 @@ function buildFreezeInitScript() {
|
|
|
46018
46733
|
}
|
|
46019
46734
|
FrozenDate.parse = RealDate.parse;
|
|
46020
46735
|
FrozenDate.UTC = RealDate.UTC;
|
|
46021
|
-
// eslint-disable-next-line no-global-assign
|
|
46022
46736
|
Date = FrozenDate;
|
|
46023
46737
|
globalThis.Date = FrozenDate;
|
|
46024
46738
|
if (typeof performance !== "undefined") {
|
|
@@ -46026,11 +46740,9 @@ function buildFreezeInitScript() {
|
|
|
46026
46740
|
const realNow = performance.now.bind(performance);
|
|
46027
46741
|
performance.now = () => realNow() - origin;
|
|
46028
46742
|
}
|
|
46029
|
-
// Deterministic Math.random (mulberry32, fixed seed). Reusable capture
|
|
46030
|
-
// pages reset the seed before mounting each storyboard.
|
|
46031
46743
|
let seed;
|
|
46032
46744
|
globalThis.__tempoResetCaptureDeterminism = () => {
|
|
46033
|
-
seed = 0x74656d70;
|
|
46745
|
+
seed = 0x74656d70;
|
|
46034
46746
|
};
|
|
46035
46747
|
globalThis.__tempoResetCaptureDeterminism();
|
|
46036
46748
|
Math.random = () => {
|
|
@@ -46053,6 +46765,14 @@ var FREEZE_MOTION_CSS = `
|
|
|
46053
46765
|
caret-color: transparent !important;
|
|
46054
46766
|
}
|
|
46055
46767
|
`;
|
|
46768
|
+
|
|
46769
|
+
// ../canvas-capture-headless/src/determinism.ts
|
|
46770
|
+
var CHROMIUM_LAUNCH_ARGS = [
|
|
46771
|
+
"--force-color-profile=srgb",
|
|
46772
|
+
"--force-device-scale-factor=2",
|
|
46773
|
+
"--hide-scrollbars",
|
|
46774
|
+
"--disable-lcd-text"
|
|
46775
|
+
];
|
|
46056
46776
|
async function runSettleStep(name, work, onStep) {
|
|
46057
46777
|
const startedAt = Date.now();
|
|
46058
46778
|
try {
|
|
@@ -46143,19 +46863,19 @@ async function settlePage(page, targetSelector, options = {}) {
|
|
|
46143
46863
|
// ../canvas-capture-headless/src/ensure-chromium.ts
|
|
46144
46864
|
import { spawn as spawn7 } from "child_process";
|
|
46145
46865
|
import { createRequire as createRequire5 } from "module";
|
|
46146
|
-
import
|
|
46866
|
+
import path56 from "path";
|
|
46147
46867
|
var INSTALL_TIMEOUT_MS = 8 * 60 * 1e3;
|
|
46148
46868
|
function isMissingBrowserError(err) {
|
|
46149
46869
|
const message = err instanceof Error ? err.message : String(err);
|
|
46150
|
-
return message.includes("Executable doesn't exist") ||
|
|
46870
|
+
return message.includes("Executable doesn't exist") || /playwright install(?!-deps)/.test(message);
|
|
46151
46871
|
}
|
|
46152
46872
|
function autoInstallDisabled() {
|
|
46153
46873
|
return process.env.TEMPO_MCP_NO_AUTO_INSTALL === "1";
|
|
46154
46874
|
}
|
|
46155
46875
|
async function installChromium(log, spawnImpl = spawn7) {
|
|
46156
46876
|
const require3 = createRequire5(import.meta.url);
|
|
46157
|
-
const cliPath =
|
|
46158
|
-
|
|
46877
|
+
const cliPath = path56.join(
|
|
46878
|
+
path56.dirname(require3.resolve("playwright/package.json")),
|
|
46159
46879
|
"cli.js"
|
|
46160
46880
|
);
|
|
46161
46881
|
await new Promise((resolve2, reject) => {
|
|
@@ -46599,7 +47319,7 @@ function sha256Hex(bytes) {
|
|
|
46599
47319
|
function looksBlank(pngBytes) {
|
|
46600
47320
|
let png;
|
|
46601
47321
|
try {
|
|
46602
|
-
png =
|
|
47322
|
+
png = import_pngjs2.PNG.sync.read(Buffer.from(pngBytes));
|
|
46603
47323
|
} catch {
|
|
46604
47324
|
return true;
|
|
46605
47325
|
}
|
|
@@ -46612,6 +47332,86 @@ function looksBlank(pngBytes) {
|
|
|
46612
47332
|
}
|
|
46613
47333
|
return true;
|
|
46614
47334
|
}
|
|
47335
|
+
function isNavigationInterruptedError(message) {
|
|
47336
|
+
return /Execution context was destroyed|most likely because of a navigation|Cannot find context with specified id|[Ff]rame was detached|Navigation interrupted|is interrupted by another navigation|Inspected target navigated or closed|Cannot find object with id|context was destroyed/.test(
|
|
47337
|
+
message
|
|
47338
|
+
);
|
|
47339
|
+
}
|
|
47340
|
+
function isPageGoneError(message) {
|
|
47341
|
+
return /Target closed|Target page, context or browser has been closed|Page closed|Target crashed/.test(
|
|
47342
|
+
message
|
|
47343
|
+
);
|
|
47344
|
+
}
|
|
47345
|
+
function isBrowserGoneError(message) {
|
|
47346
|
+
return /Browser has been closed|Browser closed|browser has disconnected|Browser process crashed/.test(
|
|
47347
|
+
message
|
|
47348
|
+
);
|
|
47349
|
+
}
|
|
47350
|
+
var navigatedErrors = /* @__PURE__ */ new WeakSet();
|
|
47351
|
+
function markNavigated(error, navigated) {
|
|
47352
|
+
if (navigated && error !== null && typeof error === "object") {
|
|
47353
|
+
navigatedErrors.add(error);
|
|
47354
|
+
}
|
|
47355
|
+
}
|
|
47356
|
+
function readNavigatedMark(error) {
|
|
47357
|
+
return error !== null && typeof error === "object" && navigatedErrors.has(error);
|
|
47358
|
+
}
|
|
47359
|
+
function documentReplaced(start, now, expectedNavigations) {
|
|
47360
|
+
const ownSameDocument = now.sameDocumentIssued - start.sameDocumentIssued;
|
|
47361
|
+
return now.navigationRequests > start.navigationRequests + expectedNavigations || now.frameNavigations > start.frameNavigations + expectedNavigations + ownSameDocument;
|
|
47362
|
+
}
|
|
47363
|
+
var BOARD_ATTEMPT_LIMIT = 3;
|
|
47364
|
+
function decideBoardRetry(attempt, attemptIndex, attemptLimit = BOARD_ATTEMPT_LIMIT) {
|
|
47365
|
+
if (attemptIndex >= attemptLimit - 1) return "accept";
|
|
47366
|
+
if (attempt.status === "render-error") {
|
|
47367
|
+
const message = attempt.error ?? "";
|
|
47368
|
+
if (isBrowserGoneError(message)) return "accept";
|
|
47369
|
+
if (isPageGoneError(message)) return "retry-fresh-worker";
|
|
47370
|
+
if (attempt.navigated || isNavigationInterruptedError(message)) {
|
|
47371
|
+
return "retry";
|
|
47372
|
+
}
|
|
47373
|
+
return "accept";
|
|
47374
|
+
}
|
|
47375
|
+
return attempt.navigated ? "retry" : "accept";
|
|
47376
|
+
}
|
|
47377
|
+
function navigatedRejectionError(attempts) {
|
|
47378
|
+
return `document reloaded during every attempt (${attempts}); the capture never saw one stable document`;
|
|
47379
|
+
}
|
|
47380
|
+
function finalizeAcceptedAttempt(attempt, attempts) {
|
|
47381
|
+
const { result } = attempt;
|
|
47382
|
+
if (attempt.navigated && result.status !== "render-error") {
|
|
47383
|
+
return {
|
|
47384
|
+
canvasPath: result.canvasPath,
|
|
47385
|
+
storyboardId: result.storyboardId,
|
|
47386
|
+
status: "render-error",
|
|
47387
|
+
error: navigatedRejectionError(attempts),
|
|
47388
|
+
timings: result.timings
|
|
47389
|
+
};
|
|
47390
|
+
}
|
|
47391
|
+
if (result.status === "render-error" && attempts > 1) {
|
|
47392
|
+
return { ...result, error: `after ${attempts} attempts: ${result.error}` };
|
|
47393
|
+
}
|
|
47394
|
+
return result;
|
|
47395
|
+
}
|
|
47396
|
+
function reloadStormWindowFor(parallelPages) {
|
|
47397
|
+
return Math.max(8, 4 * Math.max(1, Math.floor(parallelPages)));
|
|
47398
|
+
}
|
|
47399
|
+
function isReloadStorm(recent, window2) {
|
|
47400
|
+
if (recent.length < window2) return false;
|
|
47401
|
+
return recent.slice(-window2).every(Boolean);
|
|
47402
|
+
}
|
|
47403
|
+
var CaptureReloadStormError = class extends Error {
|
|
47404
|
+
constructor() {
|
|
47405
|
+
super(...arguments);
|
|
47406
|
+
this.name = "CaptureReloadStormError";
|
|
47407
|
+
}
|
|
47408
|
+
};
|
|
47409
|
+
function describeRetryReason(attempt) {
|
|
47410
|
+
if (attempt.status === "render-error" && isPageGoneError(attempt.error ?? "")) {
|
|
47411
|
+
return "page closed mid-capture";
|
|
47412
|
+
}
|
|
47413
|
+
return "document reloaded mid-capture";
|
|
47414
|
+
}
|
|
46615
47415
|
var DEFAULT_VIEWPORT = { width: 1600, height: 1200 };
|
|
46616
47416
|
async function startCaptureSession(options) {
|
|
46617
47417
|
const log = options.log ?? (() => {
|
|
@@ -46633,6 +47433,7 @@ async function startCaptureSession(options) {
|
|
|
46633
47433
|
os: process.platform,
|
|
46634
47434
|
browserVersion: browser.version()
|
|
46635
47435
|
});
|
|
47436
|
+
const browserGoneError = (cause) => new Error(`capture browser is gone; cannot open a fresh page: ${cause}`);
|
|
46636
47437
|
const createCaptureWorker = async (capture) => {
|
|
46637
47438
|
const contextStart = Date.now();
|
|
46638
47439
|
const context = await browser.newContext({
|
|
@@ -46653,6 +47454,13 @@ async function startCaptureSession(options) {
|
|
|
46653
47454
|
contextMs: Date.now() - contextStart,
|
|
46654
47455
|
contextClaimed: false,
|
|
46655
47456
|
hasNavigated: false,
|
|
47457
|
+
counters: {
|
|
47458
|
+
navigationRequests: 0,
|
|
47459
|
+
frameNavigations: 0,
|
|
47460
|
+
sameDocumentIssued: 0
|
|
47461
|
+
},
|
|
47462
|
+
documentsLoaded: 0,
|
|
47463
|
+
navigationsInFlight: 0,
|
|
46656
47464
|
async dispose() {
|
|
46657
47465
|
await page.close().catch(() => {
|
|
46658
47466
|
});
|
|
@@ -46660,9 +47468,64 @@ async function startCaptureSession(options) {
|
|
|
46660
47468
|
});
|
|
46661
47469
|
}
|
|
46662
47470
|
};
|
|
47471
|
+
const isMainFrameNavigation = (request) => {
|
|
47472
|
+
try {
|
|
47473
|
+
return request.isNavigationRequest() && request.frame() === page.mainFrame();
|
|
47474
|
+
} catch {
|
|
47475
|
+
return false;
|
|
47476
|
+
}
|
|
47477
|
+
};
|
|
47478
|
+
page.on("request", (request) => {
|
|
47479
|
+
if (!isMainFrameNavigation(request)) return;
|
|
47480
|
+
if (request.redirectedFrom()) return;
|
|
47481
|
+
worker.counters.navigationRequests += 1;
|
|
47482
|
+
worker.navigationsInFlight += 1;
|
|
47483
|
+
});
|
|
47484
|
+
page.on("requestfailed", (request) => {
|
|
47485
|
+
if (!isMainFrameNavigation(request)) return;
|
|
47486
|
+
worker.navigationsInFlight = Math.max(0, worker.navigationsInFlight - 1);
|
|
47487
|
+
});
|
|
47488
|
+
page.on("framenavigated", (frame) => {
|
|
47489
|
+
if (frame === page.mainFrame()) worker.counters.frameNavigations += 1;
|
|
47490
|
+
});
|
|
47491
|
+
page.on("domcontentloaded", () => {
|
|
47492
|
+
worker.documentsLoaded += 1;
|
|
47493
|
+
worker.navigationsInFlight = 0;
|
|
47494
|
+
if (worker.hasNavigated) {
|
|
47495
|
+
page.addStyleTag({ content: FREEZE_MOTION_CSS }).catch(() => {
|
|
47496
|
+
});
|
|
47497
|
+
}
|
|
47498
|
+
});
|
|
46663
47499
|
options.timing?.recordBoard("context", worker.contextMs);
|
|
46664
47500
|
return worker;
|
|
46665
47501
|
};
|
|
47502
|
+
const sleep6 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
47503
|
+
const awaitDocumentQuiet = async (worker, timeoutMs) => {
|
|
47504
|
+
const quietMs = 500;
|
|
47505
|
+
const deadline = Date.now() + Math.min(timeoutMs, 1e4);
|
|
47506
|
+
const settleLoad = async () => {
|
|
47507
|
+
while (worker.navigationsInFlight > 0 && Date.now() < deadline) {
|
|
47508
|
+
await sleep6(50);
|
|
47509
|
+
}
|
|
47510
|
+
await worker.page.waitForLoadState("load", {
|
|
47511
|
+
timeout: Math.max(1, deadline - Date.now())
|
|
47512
|
+
}).catch(() => {
|
|
47513
|
+
});
|
|
47514
|
+
};
|
|
47515
|
+
await settleLoad();
|
|
47516
|
+
let requests = worker.counters.navigationRequests;
|
|
47517
|
+
let quietSince = Date.now();
|
|
47518
|
+
while (Date.now() < deadline) {
|
|
47519
|
+
await sleep6(100);
|
|
47520
|
+
if (worker.counters.navigationRequests !== requests || worker.navigationsInFlight > 0) {
|
|
47521
|
+
requests = worker.counters.navigationRequests;
|
|
47522
|
+
quietSince = Date.now();
|
|
47523
|
+
await settleLoad();
|
|
47524
|
+
} else if (Date.now() - quietSince >= quietMs) {
|
|
47525
|
+
return;
|
|
47526
|
+
}
|
|
47527
|
+
}
|
|
47528
|
+
};
|
|
46666
47529
|
const withSettledStoryboard = async (target, capture, timings, work, reusableWorker) => {
|
|
46667
47530
|
const renderId = storyboardRenderId(
|
|
46668
47531
|
target.hostCanvasPath,
|
|
@@ -46676,6 +47539,9 @@ async function startCaptureSession(options) {
|
|
|
46676
47539
|
timings.contextMs = worker.contextMs;
|
|
46677
47540
|
worker.contextClaimed = true;
|
|
46678
47541
|
}
|
|
47542
|
+
const countersAtStart = { ...worker.counters };
|
|
47543
|
+
const expectedNavigations = worker.hasNavigated ? 0 : 1;
|
|
47544
|
+
const navigatedSinceStart = () => documentReplaced(countersAtStart, worker.counters, expectedNavigations);
|
|
46679
47545
|
try {
|
|
46680
47546
|
const navStart = Date.now();
|
|
46681
47547
|
try {
|
|
@@ -46692,6 +47558,7 @@ async function startCaptureSession(options) {
|
|
|
46692
47558
|
history.replaceState(null, "", url);
|
|
46693
47559
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
46694
47560
|
}, idleUrl.toString());
|
|
47561
|
+
worker.counters.sameDocumentIssued += 1;
|
|
46695
47562
|
await page.waitForSelector(worker.currentSelector, {
|
|
46696
47563
|
state: "detached",
|
|
46697
47564
|
timeout: capture.settleTimeoutMs ?? options.settleTimeoutMs ?? 3e4
|
|
@@ -46708,6 +47575,7 @@ async function startCaptureSession(options) {
|
|
|
46708
47575
|
},
|
|
46709
47576
|
{ url: targetUrl, nextSelector: selector }
|
|
46710
47577
|
);
|
|
47578
|
+
worker.counters.sameDocumentIssued += 1;
|
|
46711
47579
|
}
|
|
46712
47580
|
worker.currentSelector = selector;
|
|
46713
47581
|
} finally {
|
|
@@ -46798,12 +47666,16 @@ async function startCaptureSession(options) {
|
|
|
46798
47666
|
profile.longTaskCount,
|
|
46799
47667
|
"count"
|
|
46800
47668
|
);
|
|
46801
|
-
|
|
47669
|
+
const value = await work(page, selector);
|
|
47670
|
+
return { value, navigated: navigatedSinceStart() };
|
|
47671
|
+
} catch (error) {
|
|
47672
|
+
markNavigated(error, navigatedSinceStart());
|
|
47673
|
+
throw error;
|
|
46802
47674
|
} finally {
|
|
46803
47675
|
if (ownsWorker) await worker.dispose();
|
|
46804
47676
|
}
|
|
46805
47677
|
};
|
|
46806
|
-
const
|
|
47678
|
+
const captureStoryboardArtifactsOnce = async (request) => {
|
|
46807
47679
|
const artifacts = request.artifacts ?? ["png"];
|
|
46808
47680
|
const timings = { contextMs: 0, navigateMs: 0, settleMs: 0 };
|
|
46809
47681
|
return withSettledStoryboard(
|
|
@@ -46817,7 +47689,7 @@ async function startCaptureSession(options) {
|
|
|
46817
47689
|
const pngBytes = new Uint8Array(
|
|
46818
47690
|
await page.locator(selector).screenshot({ type: "png", animations: "disabled" })
|
|
46819
47691
|
);
|
|
46820
|
-
const decoded =
|
|
47692
|
+
const decoded = import_pngjs2.PNG.sync.read(Buffer.from(pngBytes));
|
|
46821
47693
|
result.png = {
|
|
46822
47694
|
pngBytes,
|
|
46823
47695
|
width: decoded.width,
|
|
@@ -46857,6 +47729,31 @@ async function startCaptureSession(options) {
|
|
|
46857
47729
|
}
|
|
46858
47730
|
);
|
|
46859
47731
|
};
|
|
47732
|
+
const captureStoryboardArtifacts = async (request) => {
|
|
47733
|
+
const key = `${request.hostCanvasPath}#${request.storyboardId}`;
|
|
47734
|
+
for (let attemptIndex = 0; ; attemptIndex += 1) {
|
|
47735
|
+
const lastAttempt = attemptIndex >= BOARD_ATTEMPT_LIMIT - 1;
|
|
47736
|
+
const attempts = attemptIndex + 1;
|
|
47737
|
+
let outcome;
|
|
47738
|
+
try {
|
|
47739
|
+
outcome = await captureStoryboardArtifactsOnce(request);
|
|
47740
|
+
} catch (error) {
|
|
47741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47742
|
+
if (!browser.isConnected()) throw browserGoneError(message);
|
|
47743
|
+
const retryable = !isBrowserGoneError(message) && (readNavigatedMark(error) || isNavigationInterruptedError(message) || isPageGoneError(message));
|
|
47744
|
+
if (lastAttempt || !retryable) throw error;
|
|
47745
|
+
log(
|
|
47746
|
+
`[capture] ${key}: ${message.split("\n")[0]} (attempt ${attempts}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
|
|
47747
|
+
);
|
|
47748
|
+
continue;
|
|
47749
|
+
}
|
|
47750
|
+
if (!outcome.navigated) return outcome.value;
|
|
47751
|
+
if (lastAttempt) throw new Error(navigatedRejectionError(attempts));
|
|
47752
|
+
log(
|
|
47753
|
+
`[capture] ${key}: document reloaded mid-capture (attempt ${attempts}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
|
|
47754
|
+
);
|
|
47755
|
+
}
|
|
47756
|
+
};
|
|
46860
47757
|
const captureOne = async (board, worker) => {
|
|
46861
47758
|
const timings = {
|
|
46862
47759
|
contextMs: 0,
|
|
@@ -46865,7 +47762,7 @@ async function startCaptureSession(options) {
|
|
|
46865
47762
|
screenshotMs: 0
|
|
46866
47763
|
};
|
|
46867
47764
|
try {
|
|
46868
|
-
|
|
47765
|
+
const { value, navigated } = await withSettledStoryboard(
|
|
46869
47766
|
{
|
|
46870
47767
|
hostCanvasPath: board.hostCanvasPath,
|
|
46871
47768
|
storyboardId: board.storyboardId,
|
|
@@ -46903,14 +47800,21 @@ async function startCaptureSession(options) {
|
|
|
46903
47800
|
},
|
|
46904
47801
|
worker
|
|
46905
47802
|
);
|
|
47803
|
+
return { result: value, status: value.status, navigated };
|
|
46906
47804
|
} catch (error) {
|
|
46907
|
-
|
|
47805
|
+
const result = {
|
|
46908
47806
|
canvasPath: board.canvasPath,
|
|
46909
47807
|
storyboardId: board.storyboardId,
|
|
46910
47808
|
status: "render-error",
|
|
46911
47809
|
error: error instanceof Error ? error.message : String(error),
|
|
46912
47810
|
timings
|
|
46913
47811
|
};
|
|
47812
|
+
return {
|
|
47813
|
+
result,
|
|
47814
|
+
status: result.status,
|
|
47815
|
+
error: result.error,
|
|
47816
|
+
navigated: readNavigatedMark(error)
|
|
47817
|
+
};
|
|
46914
47818
|
}
|
|
46915
47819
|
};
|
|
46916
47820
|
return {
|
|
@@ -46919,33 +47823,90 @@ async function startCaptureSession(options) {
|
|
|
46919
47823
|
captureBoards: async (boards, captureOptions) => {
|
|
46920
47824
|
if (boards.length === 0) return [];
|
|
46921
47825
|
const label = captureOptions?.label ?? "capture";
|
|
47826
|
+
const settleTimeoutMs = options.settleTimeoutMs ?? 3e4;
|
|
46922
47827
|
const results = new Array(boards.length);
|
|
47828
|
+
const workerCount = Math.max(1, Math.min(parallelPages, boards.length));
|
|
47829
|
+
const stormWindow = reloadStormWindowFor(workerCount);
|
|
47830
|
+
const recentReloadRetries = [];
|
|
47831
|
+
let reloadRetriesTotal = 0;
|
|
47832
|
+
let aborted = false;
|
|
46923
47833
|
let nextIndex = 0;
|
|
46924
47834
|
let completed = 0;
|
|
46925
|
-
const workers = Array.from(
|
|
46926
|
-
|
|
46927
|
-
async () => {
|
|
46928
|
-
|
|
47835
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
47836
|
+
let worker = await createCaptureWorker({});
|
|
47837
|
+
const replaceWorker = async () => {
|
|
47838
|
+
await worker.dispose();
|
|
47839
|
+
if (!browser.isConnected()) {
|
|
47840
|
+
throw browserGoneError("browser disconnected");
|
|
47841
|
+
}
|
|
46929
47842
|
try {
|
|
46930
|
-
|
|
46931
|
-
|
|
46932
|
-
|
|
46933
|
-
|
|
46934
|
-
|
|
46935
|
-
|
|
46936
|
-
|
|
46937
|
-
|
|
47843
|
+
worker = await createCaptureWorker({});
|
|
47844
|
+
} catch (error) {
|
|
47845
|
+
throw browserGoneError(
|
|
47846
|
+
error instanceof Error ? error.message : String(error)
|
|
47847
|
+
);
|
|
47848
|
+
}
|
|
47849
|
+
};
|
|
47850
|
+
try {
|
|
47851
|
+
for (; ; ) {
|
|
47852
|
+
if (aborted) return;
|
|
47853
|
+
const index = nextIndex;
|
|
47854
|
+
nextIndex += 1;
|
|
47855
|
+
if (index >= boards.length) return;
|
|
47856
|
+
const board = boards[index];
|
|
47857
|
+
const key = `${board.canvasPath}#${board.storyboardId}`;
|
|
47858
|
+
let result;
|
|
47859
|
+
let reloadRetries = 0;
|
|
47860
|
+
for (let attemptIndex = 0; ; attemptIndex += 1) {
|
|
47861
|
+
const attempt = await captureOne(board, worker);
|
|
47862
|
+
const decision = decideBoardRetry(attempt, attemptIndex);
|
|
47863
|
+
if (decision === "accept") {
|
|
47864
|
+
result = finalizeAcceptedAttempt(attempt, attemptIndex + 1);
|
|
47865
|
+
if (attemptIndex > 0) {
|
|
47866
|
+
options.timing?.recordBoard(
|
|
47867
|
+
"capture.retries",
|
|
47868
|
+
attemptIndex,
|
|
47869
|
+
"count"
|
|
47870
|
+
);
|
|
47871
|
+
}
|
|
47872
|
+
break;
|
|
46938
47873
|
}
|
|
46939
|
-
|
|
46940
|
-
|
|
46941
|
-
|
|
47874
|
+
const reason = describeRetryReason(attempt);
|
|
47875
|
+
if (reason === "document reloaded mid-capture")
|
|
47876
|
+
reloadRetries += 1;
|
|
47877
|
+
log(
|
|
47878
|
+
`${label}: ${key}: ${reason} (attempt ${attemptIndex + 1}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
|
|
47879
|
+
);
|
|
47880
|
+
if (decision === "retry-fresh-worker") {
|
|
47881
|
+
await replaceWorker();
|
|
47882
|
+
} else {
|
|
47883
|
+
await awaitDocumentQuiet(worker, settleTimeoutMs);
|
|
46942
47884
|
}
|
|
46943
47885
|
}
|
|
46944
|
-
|
|
46945
|
-
|
|
47886
|
+
results[index] = result;
|
|
47887
|
+
completed += 1;
|
|
47888
|
+
if (completed % 10 === 0 || completed === boards.length) {
|
|
47889
|
+
log(`${label}: ${completed}/${boards.length}`);
|
|
47890
|
+
}
|
|
47891
|
+
reloadRetriesTotal += reloadRetries;
|
|
47892
|
+
recentReloadRetries.push(reloadRetries > 0);
|
|
47893
|
+
if (recentReloadRetries.length > stormWindow) {
|
|
47894
|
+
recentReloadRetries.shift();
|
|
47895
|
+
}
|
|
47896
|
+
if (isReloadStorm(recentReloadRetries, stormWindow)) {
|
|
47897
|
+
aborted = true;
|
|
47898
|
+
throw new CaptureReloadStormError(
|
|
47899
|
+
`capture aborted: the devserver reloaded during the last ${stormWindow} storyboards in a row (${reloadRetriesTotal} re-captures so far) \u2014 it is re-optimizing or restarting continuously; see the "devserver:" lines above`
|
|
47900
|
+
);
|
|
47901
|
+
}
|
|
47902
|
+
if (result.status === "render-error") {
|
|
47903
|
+
await replaceWorker();
|
|
47904
|
+
}
|
|
46946
47905
|
}
|
|
47906
|
+
} finally {
|
|
47907
|
+
await worker.dispose();
|
|
46947
47908
|
}
|
|
46948
|
-
);
|
|
47909
|
+
});
|
|
46949
47910
|
await Promise.all(workers);
|
|
46950
47911
|
return results;
|
|
46951
47912
|
},
|
|
@@ -46973,7 +47934,7 @@ async function startCaptureSession(options) {
|
|
|
46973
47934
|
}
|
|
46974
47935
|
|
|
46975
47936
|
// ../canvas-capture-headless/src/composite.ts
|
|
46976
|
-
var
|
|
47937
|
+
var import_pngjs3 = __toESM(require_png(), 1);
|
|
46977
47938
|
var DEFAULT_PADDING = 24;
|
|
46978
47939
|
function normalizeCompositePadding(value, contentWidth, contentHeight) {
|
|
46979
47940
|
if (value == null) return DEFAULT_PADDING;
|
|
@@ -47003,7 +47964,7 @@ function compositeStoryboardPngs(images, options = {}) {
|
|
|
47003
47964
|
throw new Error("Composite image inputs must have finite positions");
|
|
47004
47965
|
}
|
|
47005
47966
|
return {
|
|
47006
|
-
png:
|
|
47967
|
+
png: import_pngjs3.PNG.sync.read(Buffer.from(image.pngBytes)),
|
|
47007
47968
|
x: Math.round(image.x * scale),
|
|
47008
47969
|
y: Math.round(image.y * scale)
|
|
47009
47970
|
};
|
|
@@ -47027,7 +47988,7 @@ function compositeStoryboardPngs(images, options = {}) {
|
|
|
47027
47988
|
);
|
|
47028
47989
|
const width = contentWidth + padding * 2;
|
|
47029
47990
|
const height = contentHeight + padding * 2;
|
|
47030
|
-
const output = new
|
|
47991
|
+
const output = new import_pngjs3.PNG({ width, height });
|
|
47031
47992
|
if (options.background) {
|
|
47032
47993
|
const { r, g, b: b2 } = options.background;
|
|
47033
47994
|
const a = Math.round((options.background.a ?? 1) * 255);
|
|
@@ -47066,17 +48027,17 @@ function compositeStoryboardPngs(images, options = {}) {
|
|
|
47066
48027
|
}
|
|
47067
48028
|
}
|
|
47068
48029
|
return {
|
|
47069
|
-
pngBytes: new Uint8Array(
|
|
48030
|
+
pngBytes: new Uint8Array(import_pngjs3.PNG.sync.write(output)),
|
|
47070
48031
|
width,
|
|
47071
48032
|
height
|
|
47072
48033
|
};
|
|
47073
48034
|
}
|
|
47074
48035
|
|
|
47075
48036
|
// ../canvas-capture-headless/src/devserver.ts
|
|
47076
|
-
import { readFile as
|
|
47077
|
-
import
|
|
48037
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
48038
|
+
import path57 from "path";
|
|
47078
48039
|
async function readTempoDevserverConfig(tempoConfigPath) {
|
|
47079
|
-
const config = JSON.parse(await
|
|
48040
|
+
const config = JSON.parse(await readFile25(tempoConfigPath, "utf-8"));
|
|
47080
48041
|
if (!config.scripts?.install || !config.scripts?.start) {
|
|
47081
48042
|
throw new Error(
|
|
47082
48043
|
`tempo.config.json at ${tempoConfigPath} must declare scripts.install and scripts.start`
|
|
@@ -47092,9 +48053,9 @@ async function readTempoDevserverConfig(tempoConfigPath) {
|
|
|
47092
48053
|
};
|
|
47093
48054
|
}
|
|
47094
48055
|
function resolveTempoDir(options) {
|
|
47095
|
-
if (options.tempoConfigPath) return
|
|
48056
|
+
if (options.tempoConfigPath) return path57.dirname(options.tempoConfigPath);
|
|
47096
48057
|
if (options.tempoDir) return options.tempoDir;
|
|
47097
|
-
if (options.repoRoot) return
|
|
48058
|
+
if (options.repoRoot) return path57.join(options.repoRoot, "tempo");
|
|
47098
48059
|
throw new Error(
|
|
47099
48060
|
"startDevserverSession needs one of tempoDir, repoRoot, or tempoConfigPath"
|
|
47100
48061
|
);
|
|
@@ -47130,11 +48091,30 @@ async function awaitCanvasRegistration(options) {
|
|
|
47130
48091
|
await new Promise((resolve2) => setTimeout(resolve2, retryDelayMs));
|
|
47131
48092
|
}
|
|
47132
48093
|
}
|
|
48094
|
+
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
48095
|
+
function cleanDevserverLine(line) {
|
|
48096
|
+
return line.replace(ANSI_RE, "").trim();
|
|
48097
|
+
}
|
|
48098
|
+
function createDevserverLineSplitter() {
|
|
48099
|
+
let remainder = "";
|
|
48100
|
+
return {
|
|
48101
|
+
push(chunk) {
|
|
48102
|
+
const parts = (remainder + chunk).split(/\r?\n/);
|
|
48103
|
+
remainder = parts.pop() ?? "";
|
|
48104
|
+
return parts.map(cleanDevserverLine).filter((line) => line.length > 0);
|
|
48105
|
+
},
|
|
48106
|
+
flush() {
|
|
48107
|
+
const tail = cleanDevserverLine(remainder);
|
|
48108
|
+
remainder = "";
|
|
48109
|
+
return tail.length > 0 ? [tail] : [];
|
|
48110
|
+
}
|
|
48111
|
+
};
|
|
48112
|
+
}
|
|
47133
48113
|
async function startDevserverSession(options) {
|
|
47134
48114
|
const log = options.log ?? (() => {
|
|
47135
48115
|
});
|
|
47136
48116
|
const tempoDir = resolveTempoDir(options);
|
|
47137
|
-
const tempoConfigPath = options.tempoConfigPath ??
|
|
48117
|
+
const tempoConfigPath = options.tempoConfigPath ?? path57.join(tempoDir, "tempo.config.json");
|
|
47138
48118
|
const config = await readTempoDevserverConfig(tempoConfigPath);
|
|
47139
48119
|
const hostCanvasPaths = options.hostCanvasPaths ?? [];
|
|
47140
48120
|
const devServer = new TempoDevServer({
|
|
@@ -47143,18 +48123,36 @@ async function startDevserverSession(options) {
|
|
|
47143
48123
|
scripts: config.scripts
|
|
47144
48124
|
});
|
|
47145
48125
|
await devServer.initialize();
|
|
47146
|
-
const projectRoot =
|
|
48126
|
+
const projectRoot = path57.dirname(tempoDir);
|
|
47147
48127
|
for (const hostCanvasPath of hostCanvasPaths) {
|
|
47148
48128
|
await devServer.ensureCanvasRegistered(
|
|
47149
|
-
|
|
48129
|
+
path57.join(projectRoot, hostCanvasPath)
|
|
47150
48130
|
);
|
|
47151
48131
|
}
|
|
47152
48132
|
const handle = await devServer.startDevServer();
|
|
48133
|
+
const onOutput = options.onOutput;
|
|
48134
|
+
const outputLines = onOutput ? createDevserverLineSplitter() : null;
|
|
48135
|
+
const forwardOutput = (lines) => {
|
|
48136
|
+
if (!onOutput) return;
|
|
48137
|
+
for (const line of lines) {
|
|
48138
|
+
try {
|
|
48139
|
+
onOutput(line);
|
|
48140
|
+
} catch (error) {
|
|
48141
|
+
log(
|
|
48142
|
+
`devserver output listener threw: ${error instanceof Error ? error.message : String(error)}`
|
|
48143
|
+
);
|
|
48144
|
+
}
|
|
48145
|
+
}
|
|
48146
|
+
};
|
|
48147
|
+
if (outputLines) {
|
|
48148
|
+
handle.onOutput((data) => forwardOutput(outputLines.push(String(data))));
|
|
48149
|
+
}
|
|
47153
48150
|
const ready = await handle.waitUntilReady(options.readyTimeoutMs ?? 12e4);
|
|
47154
48151
|
if (!ready.ok || !ready.url) {
|
|
47155
48152
|
await handle.stop().catch(() => {
|
|
47156
48153
|
});
|
|
47157
48154
|
await devServer.dispose();
|
|
48155
|
+
forwardOutput(outputLines?.flush() ?? []);
|
|
47158
48156
|
throw new Error(
|
|
47159
48157
|
`tempobook devserver failed to become ready: ${ready.message ?? (ready.timedOut ? "timeout" : "unknown")}`
|
|
47160
48158
|
);
|
|
@@ -47164,12 +48162,14 @@ async function startDevserverSession(options) {
|
|
|
47164
48162
|
log(`devserver ready at ${hostUrl}`);
|
|
47165
48163
|
const registerCanvas = async (hostCanvasPath) => {
|
|
47166
48164
|
await devServer.ensureCanvasRegistered(
|
|
47167
|
-
|
|
48165
|
+
path57.join(projectRoot, hostCanvasPath)
|
|
47168
48166
|
);
|
|
47169
48167
|
await awaitCanvasRegistration({
|
|
47170
48168
|
origin,
|
|
47171
48169
|
hostCanvasPath,
|
|
47172
|
-
reRegister: () => devServer.ensureCanvasRegistered(
|
|
48170
|
+
reRegister: () => devServer.ensureCanvasRegistered(
|
|
48171
|
+
path57.join(projectRoot, hostCanvasPath)
|
|
48172
|
+
),
|
|
47173
48173
|
log
|
|
47174
48174
|
});
|
|
47175
48175
|
};
|
|
@@ -47177,7 +48177,9 @@ async function startDevserverSession(options) {
|
|
|
47177
48177
|
await awaitCanvasRegistration({
|
|
47178
48178
|
origin,
|
|
47179
48179
|
hostCanvasPath,
|
|
47180
|
-
reRegister: () => devServer.ensureCanvasRegistered(
|
|
48180
|
+
reRegister: () => devServer.ensureCanvasRegistered(
|
|
48181
|
+
path57.join(projectRoot, hostCanvasPath)
|
|
48182
|
+
),
|
|
47181
48183
|
log
|
|
47182
48184
|
});
|
|
47183
48185
|
}
|
|
@@ -47191,26 +48193,27 @@ async function startDevserverSession(options) {
|
|
|
47191
48193
|
await handle.stop().catch(() => {
|
|
47192
48194
|
});
|
|
47193
48195
|
await devServer.dispose();
|
|
48196
|
+
forwardOutput(outputLines?.flush() ?? []);
|
|
47194
48197
|
}
|
|
47195
48198
|
};
|
|
47196
48199
|
}
|
|
47197
48200
|
|
|
47198
48201
|
// ../canvas-capture-headless/src/enumerate.ts
|
|
47199
|
-
import { readFile as
|
|
47200
|
-
import
|
|
48202
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
48203
|
+
import path58 from "path";
|
|
47201
48204
|
function toPosixPath7(value) {
|
|
47202
|
-
return value.split(
|
|
48205
|
+
return value.split(path58.sep).join("/");
|
|
47203
48206
|
}
|
|
47204
48207
|
async function enumerateCanvasStoryboards(options) {
|
|
47205
|
-
const pagesDir = options.pagesDir ??
|
|
47206
|
-
const projectRoot =
|
|
48208
|
+
const pagesDir = options.pagesDir ?? path58.join(options.tempoDir, "designs");
|
|
48209
|
+
const projectRoot = path58.dirname(options.tempoDir);
|
|
47207
48210
|
const canvasFiles = await scanCanvasFiles(pagesDir);
|
|
47208
48211
|
const storyboards = [];
|
|
47209
48212
|
const structuralErrors = [];
|
|
47210
48213
|
const vrtConfigErrors = [];
|
|
47211
48214
|
let totalStoryboards = 0;
|
|
47212
48215
|
for (const filePath of canvasFiles) {
|
|
47213
|
-
const source = await
|
|
48216
|
+
const source = await readFile26(filePath, "utf-8");
|
|
47214
48217
|
const parsed = parseCanvasFile(filePath, source);
|
|
47215
48218
|
const structural = parsed.errors.filter(
|
|
47216
48219
|
(error) => error.kind === "structural"
|
|
@@ -47238,9 +48241,9 @@ async function enumerateCanvasStoryboards(options) {
|
|
|
47238
48241
|
continue;
|
|
47239
48242
|
}
|
|
47240
48243
|
const canvasPath = toPosixPath7(
|
|
47241
|
-
|
|
48244
|
+
path58.relative(options.tempoDir, path58.dirname(filePath))
|
|
47242
48245
|
);
|
|
47243
|
-
const hostCanvasPath = toPosixPath7(
|
|
48246
|
+
const hostCanvasPath = toPosixPath7(path58.relative(projectRoot, filePath));
|
|
47244
48247
|
for (const storyboard of parsed.storyboards) {
|
|
47245
48248
|
totalStoryboards += 1;
|
|
47246
48249
|
if (storyboard.kind !== "component") continue;
|
|
@@ -47288,7 +48291,7 @@ function describeStyleStarvation(assessment) {
|
|
|
47288
48291
|
import { createHash as createHash5 } from "crypto";
|
|
47289
48292
|
import fs6 from "fs/promises";
|
|
47290
48293
|
import os3 from "os";
|
|
47291
|
-
import
|
|
48294
|
+
import path59 from "path";
|
|
47292
48295
|
|
|
47293
48296
|
// ../workspaces/git-worktree/non-interactive-git.ts
|
|
47294
48297
|
var NON_INTERACTIVE_GIT_CONFIG = [
|
|
@@ -47409,7 +48412,7 @@ async function checkBranchOnRemote(repoPath, branch) {
|
|
|
47409
48412
|
|
|
47410
48413
|
// ../workspaces/git-worktree/canvas-scan-cache.ts
|
|
47411
48414
|
import fs7 from "fs/promises";
|
|
47412
|
-
import
|
|
48415
|
+
import path60 from "path";
|
|
47413
48416
|
var DEFAULT_CONCURRENCY = 6;
|
|
47414
48417
|
var DEFAULT_MAX_AGE_MS = 5 * 6e4;
|
|
47415
48418
|
var Semaphore = class {
|
|
@@ -47435,15 +48438,15 @@ var sharedSemaphore = new Semaphore(DEFAULT_CONCURRENCY);
|
|
|
47435
48438
|
|
|
47436
48439
|
// src/canvas-hooks.ts
|
|
47437
48440
|
var MAX_MCP_SCREENSHOT_DIMENSION_PX = 1990;
|
|
47438
|
-
function
|
|
47439
|
-
return value.split(
|
|
48441
|
+
function toPosix2(value) {
|
|
48442
|
+
return value.split(path61.sep).join("/");
|
|
47440
48443
|
}
|
|
47441
48444
|
function normalizeSlug(input) {
|
|
47442
48445
|
return input.trim().replace(/\\/g, "/").replace(/\/+$/, "").replace(/\/[^/]*\.canvas\.tsx$/i, "");
|
|
47443
48446
|
}
|
|
47444
48447
|
async function isCanvasFolder(dir) {
|
|
47445
48448
|
try {
|
|
47446
|
-
return (await stat10(
|
|
48449
|
+
return (await stat10(path61.join(dir, "index.canvas.tsx"))).isFile();
|
|
47447
48450
|
} catch {
|
|
47448
48451
|
return false;
|
|
47449
48452
|
}
|
|
@@ -47451,18 +48454,18 @@ async function isCanvasFolder(dir) {
|
|
|
47451
48454
|
async function resolveCanvas(workspace, canvasSlug) {
|
|
47452
48455
|
const workspaceRoot = workspace.workspaceRoot;
|
|
47453
48456
|
const canvasesDir = workspace.canvasesDir;
|
|
47454
|
-
const tempoDir =
|
|
48457
|
+
const tempoDir = path61.join(workspaceRoot, "tempo");
|
|
47455
48458
|
const enumeration = await enumerateCanvasStoryboards({
|
|
47456
48459
|
tempoDir,
|
|
47457
48460
|
pagesDir: canvasesDir
|
|
47458
48461
|
});
|
|
47459
48462
|
const byFolder = /* @__PURE__ */ new Map();
|
|
47460
48463
|
for (const storyboard of enumeration.storyboards) {
|
|
47461
|
-
const folder =
|
|
48464
|
+
const folder = path61.dirname(storyboard.filePath);
|
|
47462
48465
|
let canvas = byFolder.get(folder);
|
|
47463
48466
|
if (!canvas) {
|
|
47464
48467
|
canvas = {
|
|
47465
|
-
contentPath:
|
|
48468
|
+
contentPath: toPosix2(path61.relative(workspaceRoot, folder)),
|
|
47466
48469
|
hostCanvasPath: storyboard.hostCanvasPath,
|
|
47467
48470
|
storyboards: []
|
|
47468
48471
|
};
|
|
@@ -47472,15 +48475,15 @@ async function resolveCanvas(workspace, canvasSlug) {
|
|
|
47472
48475
|
}
|
|
47473
48476
|
const slug = normalizeSlug(canvasSlug);
|
|
47474
48477
|
if (!slug) return { kind: "not_found" };
|
|
47475
|
-
if (slug.includes("/") ||
|
|
48478
|
+
if (slug.includes("/") || path61.isAbsolute(canvasSlug)) {
|
|
47476
48479
|
for (const [folder, canvas] of byFolder) {
|
|
47477
48480
|
const candidates = [
|
|
47478
|
-
|
|
47479
|
-
|
|
47480
|
-
|
|
48481
|
+
toPosix2(path61.relative(canvasesDir, folder)),
|
|
48482
|
+
toPosix2(path61.relative(workspaceRoot, folder)),
|
|
48483
|
+
toPosix2(path61.relative(tempoDir, folder))
|
|
47481
48484
|
];
|
|
47482
|
-
if (
|
|
47483
|
-
if (
|
|
48485
|
+
if (path61.isAbsolute(canvasSlug)) {
|
|
48486
|
+
if (path61.resolve(canvasSlug) === folder) {
|
|
47484
48487
|
return { kind: "ok", canvas };
|
|
47485
48488
|
}
|
|
47486
48489
|
continue;
|
|
@@ -47489,29 +48492,29 @@ async function resolveCanvas(workspace, canvasSlug) {
|
|
|
47489
48492
|
}
|
|
47490
48493
|
} else {
|
|
47491
48494
|
const matches = [...byFolder.entries()].filter(
|
|
47492
|
-
([folder]) =>
|
|
48495
|
+
([folder]) => path61.basename(folder) === slug
|
|
47493
48496
|
);
|
|
47494
48497
|
if (matches.length > 1) {
|
|
47495
48498
|
return {
|
|
47496
48499
|
kind: "ambiguous",
|
|
47497
|
-
candidates: matches.map(([folder]) =>
|
|
48500
|
+
candidates: matches.map(([folder]) => toPosix2(path61.relative(canvasesDir, folder))).sort()
|
|
47498
48501
|
};
|
|
47499
48502
|
}
|
|
47500
48503
|
if (matches.length === 1) return { kind: "ok", canvas: matches[0][1] };
|
|
47501
48504
|
}
|
|
47502
|
-
const probeDirs =
|
|
47503
|
-
|
|
47504
|
-
|
|
47505
|
-
|
|
48505
|
+
const probeDirs = path61.isAbsolute(canvasSlug) ? [path61.resolve(canvasSlug)] : [
|
|
48506
|
+
path61.join(canvasesDir, slug),
|
|
48507
|
+
path61.join(workspaceRoot, slug),
|
|
48508
|
+
path61.join(tempoDir, slug)
|
|
47506
48509
|
];
|
|
47507
48510
|
for (const dir of probeDirs) {
|
|
47508
48511
|
if (await isCanvasFolder(dir)) {
|
|
47509
48512
|
return {
|
|
47510
48513
|
kind: "ok",
|
|
47511
48514
|
canvas: {
|
|
47512
|
-
contentPath:
|
|
47513
|
-
hostCanvasPath:
|
|
47514
|
-
|
|
48515
|
+
contentPath: toPosix2(path61.relative(workspaceRoot, dir)),
|
|
48516
|
+
hostCanvasPath: toPosix2(
|
|
48517
|
+
path61.relative(workspaceRoot, path61.join(dir, "index.canvas.tsx"))
|
|
47515
48518
|
),
|
|
47516
48519
|
storyboards: []
|
|
47517
48520
|
}
|
|
@@ -47554,7 +48557,7 @@ async function withCaptureSession(workspaceRoot, hostCanvasPath, settleTimeoutMs
|
|
|
47554
48557
|
}
|
|
47555
48558
|
}
|
|
47556
48559
|
function downscalePngToFit(pngBytes, maxDim) {
|
|
47557
|
-
const png =
|
|
48560
|
+
const png = import_pngjs4.PNG.sync.read(Buffer.from(pngBytes));
|
|
47558
48561
|
if (png.width <= maxDim && png.height <= maxDim) {
|
|
47559
48562
|
return {
|
|
47560
48563
|
pngBase64: Buffer.from(pngBytes).toString("base64"),
|
|
@@ -47568,7 +48571,7 @@ function downscalePngToFit(pngBytes, maxDim) {
|
|
|
47568
48571
|
maxDim,
|
|
47569
48572
|
Math.max(1, Math.round(png.height * ratio))
|
|
47570
48573
|
);
|
|
47571
|
-
const out = new
|
|
48574
|
+
const out = new import_pngjs4.PNG({ width: outWidth, height: outHeight });
|
|
47572
48575
|
for (let y = 0; y < outHeight; y += 1) {
|
|
47573
48576
|
const sy0 = Math.floor(y * png.height / outHeight);
|
|
47574
48577
|
const sy1 = Math.max(
|
|
@@ -47602,7 +48605,7 @@ function downscalePngToFit(pngBytes, maxDim) {
|
|
|
47602
48605
|
out.data[o + 3] = Math.round(a / count);
|
|
47603
48606
|
}
|
|
47604
48607
|
}
|
|
47605
|
-
const bytes =
|
|
48608
|
+
const bytes = import_pngjs4.PNG.sync.write(out);
|
|
47606
48609
|
return {
|
|
47607
48610
|
pngBase64: Buffer.from(bytes).toString("base64"),
|
|
47608
48611
|
width: outWidth,
|
|
@@ -47794,6 +48797,80 @@ function createHeadlessCanvasHooks(options) {
|
|
|
47794
48797
|
};
|
|
47795
48798
|
}
|
|
47796
48799
|
};
|
|
48800
|
+
const captureStoryboardStability = async (args, captureOptions) => {
|
|
48801
|
+
const resolution = await resolveCanvas(workspace, args.canvasSlug);
|
|
48802
|
+
if (resolution.kind === "ambiguous") {
|
|
48803
|
+
return {
|
|
48804
|
+
kind: "error",
|
|
48805
|
+
code: "ambiguous_canvas_slug",
|
|
48806
|
+
message: ambiguousCanvasSlugMessage(
|
|
48807
|
+
args.canvasSlug,
|
|
48808
|
+
resolution.candidates
|
|
48809
|
+
)
|
|
48810
|
+
};
|
|
48811
|
+
}
|
|
48812
|
+
if (resolution.kind === "not_found") {
|
|
48813
|
+
return {
|
|
48814
|
+
kind: "error",
|
|
48815
|
+
code: "canvas_not_open",
|
|
48816
|
+
message: `No canvas matching "${args.canvasSlug}" exists under ${workspace.canvasesDir}. Pass the canvas's folder name, or its path under the canvases root.`
|
|
48817
|
+
};
|
|
48818
|
+
}
|
|
48819
|
+
const canvas = resolution.canvas;
|
|
48820
|
+
const target = canvas.storyboards.find(
|
|
48821
|
+
(storyboard) => storyboard.storyboardId === args.storyboard
|
|
48822
|
+
);
|
|
48823
|
+
if (!target) {
|
|
48824
|
+
return {
|
|
48825
|
+
kind: "error",
|
|
48826
|
+
code: "storyboard_not_found",
|
|
48827
|
+
message: `Storyboard "${args.storyboard}" is not an export of canvas "${canvas.contentPath}".`,
|
|
48828
|
+
availableStoryboards: canvas.storyboards.map(
|
|
48829
|
+
(storyboard) => storyboard.storyboardId
|
|
48830
|
+
)
|
|
48831
|
+
};
|
|
48832
|
+
}
|
|
48833
|
+
try {
|
|
48834
|
+
const capturedRuns = await withCaptureSession(
|
|
48835
|
+
workspace.workspaceRoot,
|
|
48836
|
+
canvas.hostCanvasPath,
|
|
48837
|
+
captureOptions?.timeoutMs,
|
|
48838
|
+
async (session) => {
|
|
48839
|
+
let streamedRuns = 0;
|
|
48840
|
+
for (let run = 0; run < args.runs; run += 1) {
|
|
48841
|
+
if (captureOptions?.signal?.aborted) {
|
|
48842
|
+
throw captureOptions.signal.reason ?? new Error("Capture aborted");
|
|
48843
|
+
}
|
|
48844
|
+
const png = await session.captureStoryboard({
|
|
48845
|
+
hostCanvasPath: canvas.hostCanvasPath,
|
|
48846
|
+
storyboardId: target.storyboardId,
|
|
48847
|
+
width: target.layout.width,
|
|
48848
|
+
height: target.layout.height
|
|
48849
|
+
});
|
|
48850
|
+
if (looksBlank(png.pngBytes)) {
|
|
48851
|
+
throw new Error(
|
|
48852
|
+
`Storyboard "${target.storyboardId}" rendered an empty frame on run ${run + 1}/${args.runs}.`
|
|
48853
|
+
);
|
|
48854
|
+
}
|
|
48855
|
+
await captureOptions?.onCapture?.({
|
|
48856
|
+
pngBase64: Buffer.from(png.pngBytes).toString("base64"),
|
|
48857
|
+
width: png.width,
|
|
48858
|
+
height: png.height
|
|
48859
|
+
});
|
|
48860
|
+
streamedRuns += 1;
|
|
48861
|
+
}
|
|
48862
|
+
return streamedRuns;
|
|
48863
|
+
}
|
|
48864
|
+
);
|
|
48865
|
+
return { kind: "ok", runs: capturedRuns };
|
|
48866
|
+
} catch (error) {
|
|
48867
|
+
return {
|
|
48868
|
+
kind: "error",
|
|
48869
|
+
code: "capture_threw",
|
|
48870
|
+
message: isMissingPlaywright(error) ? PLAYWRIGHT_INSTALL_HINT : `Headless VRT stability capture failed: ${error instanceof Error ? error.message : String(error)}`
|
|
48871
|
+
};
|
|
48872
|
+
}
|
|
48873
|
+
};
|
|
47797
48874
|
const createShareLink = async (args) => {
|
|
47798
48875
|
const orgProjectId = options.getOrgProjectId();
|
|
47799
48876
|
if (!orgProjectId) {
|
|
@@ -48012,6 +49089,10 @@ function createHeadlessCanvasHooks(options) {
|
|
|
48012
49089
|
// canvas_screenshot tool must not try to lease one first.
|
|
48013
49090
|
managesDevserver: true
|
|
48014
49091
|
},
|
|
49092
|
+
canvasStoryboardStability: {
|
|
49093
|
+
captureRuns: captureStoryboardStability,
|
|
49094
|
+
managesDevserver: true
|
|
49095
|
+
},
|
|
48015
49096
|
canvasShare: { create: createShareLink }
|
|
48016
49097
|
};
|
|
48017
49098
|
}
|
|
@@ -48133,4 +49214,4 @@ export {
|
|
|
48133
49214
|
runServe,
|
|
48134
49215
|
scopeParamsFor
|
|
48135
49216
|
};
|
|
48136
|
-
//# sourceMappingURL=serve-
|
|
49217
|
+
//# sourceMappingURL=serve-7RSR6WI6.js.map
|