@jokerized/decksmith 0.4.0 → 0.4.1
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/dist/cli.js +48 -12
- package/dist/index.js +48 -12
- package/dist/mcp.js +20 -8
- package/dist/types/render/capture.d.ts +27 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8922,18 +8922,26 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
8922
8922
|
import { homedir as homedir2 } from "node:os";
|
|
8923
8923
|
import { join as join8 } from "node:path";
|
|
8924
8924
|
import { pathToFileURL } from "node:url";
|
|
8925
|
-
async function
|
|
8926
|
-
const
|
|
8927
|
-
if (
|
|
8925
|
+
async function resolveChrome(need = "open the deck with") {
|
|
8926
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
8927
|
+
if (env) return { path: env, source: "env" };
|
|
8928
8928
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
8929
|
-
const
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8929
|
+
const pick = async (cacheDir) => {
|
|
8930
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
8931
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
8932
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
8933
|
+
};
|
|
8934
|
+
const hf = await pick(join8(homedir2(), ".cache", "hyperframes", "chrome"));
|
|
8935
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
8936
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join8(homedir2(), ".cache", "puppeteer"));
|
|
8937
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
8933
8938
|
throw new Error(
|
|
8934
8939
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
8935
8940
|
);
|
|
8936
8941
|
}
|
|
8942
|
+
async function chromePath(need = "open the deck with") {
|
|
8943
|
+
return (await resolveChrome(need)).path;
|
|
8944
|
+
}
|
|
8937
8945
|
function runtimePath() {
|
|
8938
8946
|
return createRequire2(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
8939
8947
|
}
|
|
@@ -8950,12 +8958,24 @@ async function openDeck(dir, opts = {}) {
|
|
|
8950
8958
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
8951
8959
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
8952
8960
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
8961
|
+
const chrome2 = await resolveChrome();
|
|
8953
8962
|
const browser = await puppeteer.launch({
|
|
8954
|
-
executablePath:
|
|
8963
|
+
executablePath: chrome2.path,
|
|
8955
8964
|
headless: true,
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8965
|
+
args: [
|
|
8966
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
8967
|
+
// renderer's.
|
|
8968
|
+
"--force-device-scale-factor=1",
|
|
8969
|
+
"--hide-scrollbars",
|
|
8970
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
8971
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
8972
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
8973
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
8974
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
8975
|
+
"--use-gl=angle",
|
|
8976
|
+
"--use-angle=swiftshader",
|
|
8977
|
+
"--enable-unsafe-swiftshader"
|
|
8978
|
+
]
|
|
8959
8979
|
});
|
|
8960
8980
|
try {
|
|
8961
8981
|
const page = await browser.newPage();
|
|
@@ -8971,6 +8991,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
8971
8991
|
{ timeout }
|
|
8972
8992
|
);
|
|
8973
8993
|
await page.evaluate(() => document.fonts.ready);
|
|
8994
|
+
const canvas = await page.evaluate(() => {
|
|
8995
|
+
const el = document.querySelector("canvas");
|
|
8996
|
+
if (!el) return { present: false, gl: true };
|
|
8997
|
+
const probe2 = document.createElement("canvas");
|
|
8998
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
8999
|
+
return { present: true, gl };
|
|
9000
|
+
});
|
|
9001
|
+
if (canvas.present && !canvas.gl) {
|
|
9002
|
+
throw new Error(
|
|
9003
|
+
`this deck draws on a <canvas> and ${chrome2.path} cannot create a WebGL context, so the capture would be of an empty rectangle. Chrome came from ${chrome2.source}; the renderer uses hyperframes' own shell. Set HYPERFRAMES_BROWSER_PATH to the binary \`hyperframes render\` uses so the gate and the render are the same browser.`
|
|
9004
|
+
);
|
|
9005
|
+
}
|
|
8974
9006
|
const cdp = await page.createCDPSession();
|
|
8975
9007
|
return {
|
|
8976
9008
|
width,
|
|
@@ -10324,7 +10356,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
10324
10356
|
"--disable-background-networking",
|
|
10325
10357
|
"--disable-extensions",
|
|
10326
10358
|
"--no-default-browser-check",
|
|
10327
|
-
"--no-first-run"
|
|
10359
|
+
"--no-first-run",
|
|
10360
|
+
// The floor under interception — see the note above. Not a hardening
|
|
10361
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
10362
|
+
// happens to be that run.
|
|
10363
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
10328
10364
|
]
|
|
10329
10365
|
});
|
|
10330
10366
|
try {
|
package/dist/index.js
CHANGED
|
@@ -6973,18 +6973,26 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
6973
6973
|
import { homedir } from "node:os";
|
|
6974
6974
|
import { join as join2 } from "node:path";
|
|
6975
6975
|
import { pathToFileURL } from "node:url";
|
|
6976
|
-
async function
|
|
6977
|
-
const
|
|
6978
|
-
if (
|
|
6976
|
+
async function resolveChrome(need = "open the deck with") {
|
|
6977
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6978
|
+
if (env) return { path: env, source: "env" };
|
|
6979
6979
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6980
|
-
const
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6980
|
+
const pick = async (cacheDir) => {
|
|
6981
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
6982
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
6983
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
6984
|
+
};
|
|
6985
|
+
const hf = await pick(join2(homedir(), ".cache", "hyperframes", "chrome"));
|
|
6986
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
6987
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join2(homedir(), ".cache", "puppeteer"));
|
|
6988
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6984
6989
|
throw new Error(
|
|
6985
6990
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
6986
6991
|
);
|
|
6987
6992
|
}
|
|
6993
|
+
async function chromePath(need = "open the deck with") {
|
|
6994
|
+
return (await resolveChrome(need)).path;
|
|
6995
|
+
}
|
|
6988
6996
|
function runtimePath() {
|
|
6989
6997
|
return createRequire2(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
6990
6998
|
}
|
|
@@ -7001,12 +7009,24 @@ async function openDeck(dir, opts = {}) {
|
|
|
7001
7009
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
7002
7010
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
7003
7011
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
7012
|
+
const chrome2 = await resolveChrome();
|
|
7004
7013
|
const browser = await puppeteer.launch({
|
|
7005
|
-
executablePath:
|
|
7014
|
+
executablePath: chrome2.path,
|
|
7006
7015
|
headless: true,
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7016
|
+
args: [
|
|
7017
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
7018
|
+
// renderer's.
|
|
7019
|
+
"--force-device-scale-factor=1",
|
|
7020
|
+
"--hide-scrollbars",
|
|
7021
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
7022
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
7023
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
7024
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
7025
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
7026
|
+
"--use-gl=angle",
|
|
7027
|
+
"--use-angle=swiftshader",
|
|
7028
|
+
"--enable-unsafe-swiftshader"
|
|
7029
|
+
]
|
|
7010
7030
|
});
|
|
7011
7031
|
try {
|
|
7012
7032
|
const page = await browser.newPage();
|
|
@@ -7022,6 +7042,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
7022
7042
|
{ timeout }
|
|
7023
7043
|
);
|
|
7024
7044
|
await page.evaluate(() => document.fonts.ready);
|
|
7045
|
+
const canvas = await page.evaluate(() => {
|
|
7046
|
+
const el = document.querySelector("canvas");
|
|
7047
|
+
if (!el) return { present: false, gl: true };
|
|
7048
|
+
const probe2 = document.createElement("canvas");
|
|
7049
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
7050
|
+
return { present: true, gl };
|
|
7051
|
+
});
|
|
7052
|
+
if (canvas.present && !canvas.gl) {
|
|
7053
|
+
throw new Error(
|
|
7054
|
+
`this deck draws on a <canvas> and ${chrome2.path} cannot create a WebGL context, so the capture would be of an empty rectangle. Chrome came from ${chrome2.source}; the renderer uses hyperframes' own shell. Set HYPERFRAMES_BROWSER_PATH to the binary \`hyperframes render\` uses so the gate and the render are the same browser.`
|
|
7055
|
+
);
|
|
7056
|
+
}
|
|
7025
7057
|
const cdp = await page.createCDPSession();
|
|
7026
7058
|
return {
|
|
7027
7059
|
width,
|
|
@@ -8005,7 +8037,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8005
8037
|
"--disable-background-networking",
|
|
8006
8038
|
"--disable-extensions",
|
|
8007
8039
|
"--no-default-browser-check",
|
|
8008
|
-
"--no-first-run"
|
|
8040
|
+
"--no-first-run",
|
|
8041
|
+
// The floor under interception — see the note above. Not a hardening
|
|
8042
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
8043
|
+
// happens to be that run.
|
|
8044
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8009
8045
|
]
|
|
8010
8046
|
});
|
|
8011
8047
|
try {
|
package/dist/mcp.js
CHANGED
|
@@ -6992,18 +6992,26 @@ function message(err) {
|
|
|
6992
6992
|
// src/render/capture.ts
|
|
6993
6993
|
import { homedir } from "node:os";
|
|
6994
6994
|
import { join as join3 } from "node:path";
|
|
6995
|
-
async function
|
|
6996
|
-
const
|
|
6997
|
-
if (
|
|
6995
|
+
async function resolveChrome(need = "open the deck with") {
|
|
6996
|
+
const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6997
|
+
if (env2) return { path: env2, source: "env" };
|
|
6998
6998
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6999
|
-
const
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
6999
|
+
const pick = async (cacheDir) => {
|
|
7000
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
7001
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
7002
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
7003
|
+
};
|
|
7004
|
+
const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
|
|
7005
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
7006
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
|
|
7007
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
7003
7008
|
throw new Error(
|
|
7004
7009
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
7005
7010
|
);
|
|
7006
7011
|
}
|
|
7012
|
+
async function chromePath(need = "open the deck with") {
|
|
7013
|
+
return (await resolveChrome(need)).path;
|
|
7014
|
+
}
|
|
7007
7015
|
|
|
7008
7016
|
// src/source/assets.ts
|
|
7009
7017
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -7964,7 +7972,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
7964
7972
|
"--disable-background-networking",
|
|
7965
7973
|
"--disable-extensions",
|
|
7966
7974
|
"--no-default-browser-check",
|
|
7967
|
-
"--no-first-run"
|
|
7975
|
+
"--no-first-run",
|
|
7976
|
+
// The floor under interception — see the note above. Not a hardening
|
|
7977
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
7978
|
+
// happens to be that run.
|
|
7979
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
7968
7980
|
]
|
|
7969
7981
|
});
|
|
7970
7982
|
try {
|
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
* the renderer's own clip.
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
|
-
* The browser `render` already uses.
|
|
26
|
+
* The browser `render` already uses — which, until 2026-09-11, this did not find.
|
|
27
|
+
*
|
|
28
|
+
* IT PREFERRED PUPPETEER'S CACHE AND SAID THE TWO WERE INTERCHANGEABLE. They are
|
|
29
|
+
* not. The renderer runs hyperframes' own shell out of `~/.cache/hyperframes`
|
|
30
|
+
* (152 here); puppeteer's cache holds 145. For a screenshot of a static DOM the
|
|
31
|
+
* old comment was right and nothing noticed for months. For a WebGL canvas 145
|
|
32
|
+
* refuses a context and 152 grants one, so `frames` and the `fidelity` gate
|
|
33
|
+
* returned a clean, plausible, EMPTY frame — the seventh case in this repo of a
|
|
34
|
+
* green gate over wrong output, and the first found by opening a PNG that had
|
|
35
|
+
* nothing in it. See `.planning/2026-09-09-webgl-determinism-spike.md`.
|
|
27
36
|
*
|
|
28
37
|
* `@puppeteer/browsers` and `puppeteer-core` are DIRECT dependencies of this
|
|
29
38
|
* package. They arrive with hyperframes too — a hard dependency of any verb that
|
|
@@ -39,6 +48,23 @@
|
|
|
39
48
|
* was two copies until `frames` became the third caller the older of them said
|
|
40
49
|
* to promote it on.
|
|
41
50
|
*/
|
|
51
|
+
export interface ResolvedChrome {
|
|
52
|
+
path: string;
|
|
53
|
+
/** Where it came from, so a caller can say whether it matches the renderer. */
|
|
54
|
+
source: "env" | "hyperframes" | "puppeteer";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve, in the renderer's own order of preference.
|
|
58
|
+
*
|
|
59
|
+
* `HYPERFRAMES_BROWSER_PATH` first because that is the variable the renderer
|
|
60
|
+
* itself honours, so setting it makes the two provably the same binary. Then
|
|
61
|
+
* hyperframes' cache, which is where the renderer gets its shell when nobody has
|
|
62
|
+
* set anything. Puppeteer's cache is LAST and is a different browser: it is kept
|
|
63
|
+
* because a machine that has only that one should still be able to look at a
|
|
64
|
+
* DOM-only deck, not because the two agree.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveChrome(need?: string): Promise<ResolvedChrome>;
|
|
67
|
+
/** The path alone, for the callers that only ever wanted that. */
|
|
42
68
|
export declare function chromePath(need?: string): Promise<string>;
|
|
43
69
|
type Browser = Awaited<ReturnType<typeof import("puppeteer-core").launch>>;
|
|
44
70
|
type Page = Awaited<ReturnType<Browser["newPage"]>>;
|