@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 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 chromePath(need = "open the deck with") {
8926
- const explicit = process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
8927
- if (explicit) return explicit;
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 cacheDir = process.env.PUPPETEER_CACHE_DIR || join8(homedir2(), ".cache", "puppeteer");
8930
- const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
8931
- const found = installed2.find((b) => b.browser === "chrome-headless-shell") ?? installed2.find((b) => b.browser === "chrome");
8932
- if (found) return found.executablePath;
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: await chromePath(),
8963
+ executablePath: chrome2.path,
8955
8964
  headless: true,
8956
- // A retina host would otherwise hand back a 2x frame, whose clip is not the
8957
- // renderer's.
8958
- args: ["--force-device-scale-factor=1", "--hide-scrollbars"]
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 chromePath(need = "open the deck with") {
6977
- const explicit = process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
6978
- if (explicit) return explicit;
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 cacheDir = process.env.PUPPETEER_CACHE_DIR || join2(homedir(), ".cache", "puppeteer");
6981
- const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
6982
- const found = installed2.find((b) => b.browser === "chrome-headless-shell") ?? installed2.find((b) => b.browser === "chrome");
6983
- if (found) return found.executablePath;
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: await chromePath(),
7014
+ executablePath: chrome2.path,
7006
7015
  headless: true,
7007
- // A retina host would otherwise hand back a 2x frame, whose clip is not the
7008
- // renderer's.
7009
- args: ["--force-device-scale-factor=1", "--hide-scrollbars"]
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 chromePath(need = "open the deck with") {
6996
- const explicit = process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
6997
- if (explicit) return explicit;
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 cacheDir = process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer");
7000
- const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
7001
- const found = installed2.find((b) => b.browser === "chrome-headless-shell") ?? installed2.find((b) => b.browser === "chrome");
7002
- if (found) return found.executablePath;
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"]>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",