@reportforge/playwright-pdf 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.19.0] - 2026-07-11
7
+
8
+ ### Added
9
+
10
+ - **New `logLevel` option** (`'silent' | 'error' | 'warn' | 'info' | 'debug'`, default `'info'`) controls how chatty the reporter's `[reportforge]` console lines are. `'debug'` prints the same diagnostics as `RF_DEBUG=1` (useful in CI where env vars are awkward to set); `'silent'` suppresses everything, including error lines. `RF_DEBUG=1` always forces debug output, overriding the option, so "set `RF_DEBUG=1` and send the log" keeps working regardless of config.
11
+ - **`RF_DEBUG=1` now explains previously silent failure paths**: license cache reads/writes, cached-token rejection (including exactly why the reporter re-activated instead of serving from cache), device-id persistence, Chrome discovery, and the qpdf availability probe.
12
+
13
+ ### Changed
14
+
15
+ - A failed write of `~/.reportforge/device.json` now logs a warning, because it means this machine may register a new license seat on every run.
16
+ - The live watch URL is printed once instead of twice; the banner link line now carries the `[reportforge]` prefix so it stays greppable in CI logs.
17
+ - CLI output prefixes are consistent: the `demo` and `export-feedback` commands use the same `[reportforge]` prefix as the reporter.
18
+
19
+ ---
20
+
21
+ ## [0.18.0] - 2026-07-11
22
+
23
+ ### Changed
24
+
25
+ - **Node 20 is now the minimum supported version** (was Node 18). Node 18 reached end-of-life in April 2025 and no longer receives security patches, and dependencies have begun dropping support for it. If you are still on Node 18, upgrade to Node 20 or later before taking this release. CI now tests against Node 20, 22, and 24.
26
+ - `puppeteer-core` is now supported up to v25 (peer range widened to `>=21 <26`). Existing v21 to v24 installs keep working, so no action is needed unless you want to upgrade.
27
+ - Internal dependency updates: `sharp` 0.35 (which requires Node 20), and `@types/node` 26.
28
+
29
+ ---
30
+
6
31
  ## [0.17.0] - 2026-07-09
7
32
 
8
33
  ### Added
package/README.md CHANGED
@@ -189,6 +189,7 @@ All options are the second element of the reporter tuple.
189
189
  | `reportTitle` | `string` | `'Playwright Test Report'` | Custom title for the report cover page and running header. |
190
190
  | `projectName` | `string` | from `package.json` | Project or application name. Inferred from the `package.json` `name` field if absent. |
191
191
  | `open` | `boolean` | `false` | Open the generated PDF automatically after generation. For local use only; ignored in CI. |
192
+ | `logLevel` | `'silent' \| 'error' \| 'warn' \| 'info' \| 'debug'` | `'info'` | Console verbosity for `[reportforge]` log lines. `'silent'` suppresses everything including errors; `'debug'` adds diagnostics. The `RF_DEBUG=1` env var always forces debug, overriding this option. |
192
193
  | `puppeteerExecutablePath` | `string` | auto-detect | Full path to a Chrome or Chromium binary. Falls back to `PUPPETEER_EXECUTABLE_PATH` env, then system Chrome discovery. |
193
194
  | `serverUrl` | `string` | `'https://reportforge.org'` | Base URL for the ReportForge licensing server. Override only for self-hosting or local development. |
194
195
  | `compressionLevel` | `'auto' \| 'none' \| 'balanced' \| 'max'` | `'auto'` | Screenshot JPEG quality preset. `'auto'` picks based on failure volume; `'none'` keeps original PNGs; `'balanced'` uses JPEG q85; `'max'` uses JPEG q70. |
@@ -644,6 +645,18 @@ Set `RF_DEBUG=1` to log the full activation flow and error stack traces:
644
645
  RF_DEBUG=1 npx playwright test 2>&1 | grep '\[reportforge\]'
645
646
  ```
646
647
 
648
+ ### Controlling log output
649
+
650
+ The `logLevel` option sets how chatty the reporter's `[reportforge]` console lines are:
651
+
652
+ ```ts
653
+ ['@reportforge/playwright-pdf', { logLevel: 'warn' }] // warnings and errors only
654
+ ```
655
+
656
+ - `'debug'` prints the same diagnostics as `RF_DEBUG=1` — useful in CI where env vars are awkward to set.
657
+ - `'silent'` suppresses everything, **including error lines**: a failed PDF then surfaces only as a missing file, because the reporter never fails your test run.
658
+ - `RF_DEBUG=1` always forces debug output, overriding `logLevel` — it is the support escape hatch, so "set `RF_DEBUG=1` and send the log" works no matter what the config says.
659
+
647
660
  Common issues:
648
661
 
649
662
  - **`Chrome/Chromium not found`** → install Google Chrome Stable; don't use the Ubuntu `chromium-browser` snap.
@@ -677,6 +690,10 @@ Full documentation at [reportforge.org/docs](https://reportforge.org/docs):
677
690
 
678
691
  Questions and bug reports: email [support@reportforge.org](mailto:support@reportforge.org).
679
692
 
693
+ ## Security
694
+
695
+ Found a vulnerability? Email [support@reportforge.org](mailto:support@reportforge.org) with a subject starting with `[SECURITY]` — please don't open a public issue. Details, scope, and response timelines are in [SECURITY.md](./SECURITY.md); machine-readable contact data is at [reportforge.org/.well-known/security.txt](https://reportforge.org/.well-known/security.txt).
696
+
680
697
  ## License
681
698
 
682
- [Elastic License 2.0](./LICENSE), source-available. Read, copy, and modify the source freely. Three things the license does not allow: offering the software as a hosted service, bypassing or removing the license-key check, and stripping license or copyright notices. Generating PDF reports requires an active subscription.
699
+ [Elastic License 2.0](./LICENSE). The license does not allow offering the software as a hosted service, bypassing or removing the license-key check, or stripping license or copyright notices. Generating PDF reports requires an active subscription.
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ # Security Policy
2
+
3
+ ReportForge handles license keys, payment webhooks, and customer data. We take
4
+ vulnerability reports seriously and appreciate coordinated disclosure.
5
+
6
+ ## Reporting a vulnerability
7
+
8
+ Email **[support@reportforge.org](mailto:support@reportforge.org)** with the
9
+ subject line starting with `[SECURITY]`.
10
+
11
+ Please include:
12
+
13
+ - A description of the issue and where it lives (npm package
14
+ `@reportforge/playwright-pdf`, the `reportforge.org` service, or both).
15
+ - Steps to reproduce, or a proof of concept.
16
+ - The version(s) affected, if known.
17
+
18
+ Please do **not** open a public GitHub issue for security reports, and do not
19
+ test against production customer data — a trial subscription gives you a full
20
+ legitimate environment to test the license flow.
21
+
22
+ ## What to expect
23
+
24
+ - **Acknowledgement within 48 hours** of your report.
25
+ - A triage verdict (accepted / not a vulnerability / duplicate) within 7 days.
26
+ - If accepted: a fix or mitigation timeline, and credit in the release notes
27
+ if you want it. We ask for up to **90 days** of coordinated disclosure
28
+ before you publish details.
29
+ - We do not currently run a paid bounty program.
30
+
31
+ ## Scope
32
+
33
+ In scope:
34
+
35
+ - The npm package `@reportforge/playwright-pdf` (report generation, license
36
+ client, bundled assets).
37
+ - The `reportforge.org` service: license API, dashboard, checkout, webhooks.
38
+
39
+ Out of scope:
40
+
41
+ - Denial of service / volumetric attacks.
42
+ - Reports about the 25-machine licensing cap working as documented.
43
+ - Vulnerabilities exclusively in third-party services we integrate
44
+ (Razorpay, Clerk, Vercel, Resend) — report those upstream.
45
+
46
+ ## Supported versions
47
+
48
+ Only the latest published version of `@reportforge/playwright-pdf` receives
49
+ security fixes. Machine-readable contact data is published at
50
+ [`https://reportforge.org/.well-known/security.txt`](https://reportforge.org/.well-known/security.txt).
package/dist/cli/index.js CHANGED
@@ -42,24 +42,32 @@ var init_cjs_shims = __esm({
42
42
  });
43
43
 
44
44
  // src/utils/logger.ts
45
- var PREFIX, logger;
45
+ function isLevelEnabled(level) {
46
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
47
+ }
48
+ var PREFIX, LEVELS, threshold, logger;
46
49
  var init_logger = __esm({
47
50
  "src/utils/logger.ts"() {
48
51
  "use strict";
49
52
  init_cjs_shims();
50
53
  PREFIX = "[reportforge]";
54
+ LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
55
+ threshold = LEVELS.info;
51
56
  logger = {
52
57
  info(msg) {
58
+ if (!isLevelEnabled("info")) return;
53
59
  console.log(`${PREFIX} ${msg}`);
54
60
  },
55
61
  warn(msg) {
62
+ if (!isLevelEnabled("warn")) return;
56
63
  console.warn(`${PREFIX} WARN: ${msg}`);
57
64
  },
58
65
  error(msg) {
66
+ if (!isLevelEnabled("error")) return;
59
67
  console.error(`${PREFIX} ERROR: ${msg}`);
60
68
  },
61
69
  debug(msg, data) {
62
- if (!process.env.RF_DEBUG) return;
70
+ if (!isLevelEnabled("debug")) return;
63
71
  const suffix = data !== void 0 ? `
64
72
  ${JSON.stringify(data, null, 2)}` : "";
65
73
  console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
@@ -7257,8 +7265,7 @@ var init_BrowserManager = __esm({
7257
7265
  }
7258
7266
  async close() {
7259
7267
  if (this.browser) {
7260
- await this.browser.close().catch(() => {
7261
- });
7268
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
7262
7269
  this.browser = void 0;
7263
7270
  }
7264
7271
  }
@@ -7270,7 +7277,8 @@ var init_BrowserManager = __esm({
7270
7277
  const chromeFinder = require_lib2();
7271
7278
  const found = chromeFinder();
7272
7279
  if (found && import_fs3.default.existsSync(found)) return found;
7273
- } catch {
7280
+ } catch (err) {
7281
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
7274
7282
  }
7275
7283
  const linuxPaths = [
7276
7284
  "/usr/bin/google-chrome",
@@ -7288,12 +7296,14 @@ var init_BrowserManager = __esm({
7288
7296
  }).toString().trim().split("\n")[0];
7289
7297
  if (found && import_fs3.default.existsSync(found)) return found;
7290
7298
  } catch {
7299
+ logger.debug("chrome discovery: which lookup found nothing");
7291
7300
  }
7292
7301
  if (puppeteer.executablePath) {
7293
7302
  try {
7294
7303
  const bundled = puppeteer.executablePath();
7295
7304
  if (bundled && import_fs3.default.existsSync(bundled)) return bundled;
7296
- } catch {
7305
+ } catch (err) {
7306
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
7297
7307
  }
7298
7308
  }
7299
7309
  logger.warn(
@@ -7315,6 +7325,7 @@ var init_HtmlWriter = __esm({
7315
7325
  import_os = __toESM(require("os"));
7316
7326
  import_path2 = __toESM(require("path"));
7317
7327
  import_crypto = __toESM(require("crypto"));
7328
+ init_logger();
7318
7329
  HtmlWriter = class {
7319
7330
  async write(html) {
7320
7331
  const tmpDir = import_os.default.tmpdir();
@@ -7325,8 +7336,10 @@ var init_HtmlWriter = __esm({
7325
7336
  }
7326
7337
  async cleanup() {
7327
7338
  if (this.tempPath) {
7328
- await import_fs4.default.promises.unlink(this.tempPath).catch(() => {
7329
- });
7339
+ const tempPath = this.tempPath;
7340
+ await import_fs4.default.promises.unlink(tempPath).catch(
7341
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
7342
+ );
7330
7343
  this.tempPath = void 0;
7331
7344
  }
7332
7345
  }
@@ -7405,7 +7418,8 @@ var init_PdfEncryptor = __esm({
7405
7418
  try {
7406
7419
  (0, import_child_process2.execFileSync)("qpdf", ["--version"], { stdio: "pipe" });
7407
7420
  return true;
7408
- } catch {
7421
+ } catch (err) {
7422
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
7409
7423
  return false;
7410
7424
  }
7411
7425
  }
@@ -7638,7 +7652,8 @@ var init_PdfGenerator = __esm({
7638
7652
  if (CHARTJS_BUNDLE2) {
7639
7653
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
7640
7654
  }
7641
- } catch {
7655
+ } catch (err) {
7656
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
7642
7657
  }
7643
7658
  }
7644
7659
  /**
@@ -12085,6 +12100,7 @@ var init_schema = __esm({
12085
12100
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
12086
12101
  projectName: external_exports.string().optional(),
12087
12102
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
12103
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
12088
12104
  puppeteerExecutablePath: external_exports.string().optional(),
12089
12105
  serverUrl: external_exports.string().url().optional(),
12090
12106
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -12549,7 +12565,7 @@ function parseArgs() {
12549
12565
  const rawTemplate = templateArg?.slice("--template=".length);
12550
12566
  const rawOutput = outputArg?.slice("--output=".length);
12551
12567
  if (rawTemplate && !VALID_TEMPLATES.has(rawTemplate)) {
12552
- console.warn(`[ReportForge] Unknown template '${rawTemplate}', using 'detailed'. Valid: ${TEMPLATE_IDS.join(", ")}`);
12568
+ console.warn(`[reportforge] Unknown template '${rawTemplate}', using 'detailed'. Valid: ${TEMPLATE_IDS.join(", ")}`);
12553
12569
  }
12554
12570
  const template = rawTemplate && VALID_TEMPLATES.has(rawTemplate) ? rawTemplate : "detailed";
12555
12571
  const outputFile = rawOutput ?? import_path7.default.join("playwright-report", "demo-report.pdf");
@@ -12557,7 +12573,7 @@ function parseArgs() {
12557
12573
  }
12558
12574
  async function run() {
12559
12575
  const { template, outputFile } = parseArgs();
12560
- console.log("[ReportForge] Generating demo report...");
12576
+ console.log("[reportforge] Generating demo report...");
12561
12577
  console.log(` Template : ${template}`);
12562
12578
  console.log(` Output : ${outputFile}`);
12563
12579
  const reportData = buildDemoReportData(template);
@@ -12568,10 +12584,10 @@ async function run() {
12568
12584
  const paths = await generator.generateAll(reportData, options, DEMO_LICENSE_INFO2);
12569
12585
  if (paths.length > 0) {
12570
12586
  console.log(`
12571
- [ReportForge] Demo report written: ${paths[0]}`);
12587
+ [reportforge] Demo report written: ${paths[0]}`);
12572
12588
  console.log(" Open the PDF to preview exactly what ReportForge generates for your Playwright runs.");
12573
12589
  } else {
12574
- console.warn("\n[ReportForge] Warning: no PDF was written \u2014 check that Chrome is available.");
12590
+ console.warn("\n[reportforge] Warning: no PDF was written \u2014 check that Chrome is available.");
12575
12591
  process.exit(1);
12576
12592
  }
12577
12593
  }
@@ -12581,14 +12597,14 @@ async function runDemo() {
12581
12597
  } catch (err) {
12582
12598
  const msg = err instanceof Error ? err.message : String(err);
12583
12599
  if (/chrome|chromium|executable|browser|launcher/i.test(msg)) {
12584
- console.error("\n[ReportForge] Chrome not found.\n");
12600
+ console.error("\n[reportforge] Chrome not found.\n");
12585
12601
  console.error(" Fix: set PUPPETEER_EXECUTABLE_PATH, e.g.:");
12586
12602
  console.error(" PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable \\");
12587
12603
  console.error(" npx @reportforge/playwright-pdf demo\n");
12588
12604
  console.error(` Original error: ${msg}`);
12589
12605
  } else {
12590
12606
  console.error(`
12591
- [ReportForge] Demo generation failed: ${msg}`);
12607
+ [reportforge] Demo generation failed: ${msg}`);
12592
12608
  if (process.env.RF_DEBUG) console.error(err);
12593
12609
  }
12594
12610
  process.exit(1);
@@ -12648,10 +12664,10 @@ ${rows.join("\n")}
12648
12664
  function exportFeedbackCli() {
12649
12665
  const res = exportFeedback();
12650
12666
  if (res.rowCount === 0) {
12651
- process.stdout.write("No locally collected failures found (~/.reportforge/*/unclassified.csv).\n");
12667
+ process.stdout.write("[reportforge] No locally collected failures found (~/.reportforge/*/unclassified.csv).\n");
12652
12668
  } else {
12653
12669
  process.stdout.write(
12654
- `Saved ${res.rowCount} row(s) to ${res.outFile} - tokenized + redacted, LOCAL only.
12670
+ `[reportforge] Saved ${res.rowCount} row(s) to ${res.outFile} - tokenized + redacted, LOCAL only.
12655
12671
  Review it before sharing; there is no upload.
12656
12672
  `
12657
12673
  );
package/dist/index.d.mts CHANGED
@@ -91,6 +91,16 @@ interface ReporterOptions {
91
91
  * @default false
92
92
  */
93
93
  open?: boolean;
94
+ /**
95
+ * Console verbosity for reporter log lines (the `[reportforge]` prefix).
96
+ * `'silent'` suppresses everything, including error lines — a PDF failure
97
+ * then surfaces only as a missing file (the reporter never fails the test
98
+ * run). `'debug'` adds diagnostic detail. The `RF_DEBUG=1` environment
99
+ * variable always forces `'debug'`, overriding this option (support
100
+ * escape hatch).
101
+ * @default 'info'
102
+ */
103
+ logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
94
104
  /**
95
105
  * Full path to a Chrome/Chromium executable.
96
106
  * Falls back to PUPPETEER_EXECUTABLE_PATH env, then system Chrome discovery.
package/dist/index.d.ts CHANGED
@@ -91,6 +91,16 @@ interface ReporterOptions {
91
91
  * @default false
92
92
  */
93
93
  open?: boolean;
94
+ /**
95
+ * Console verbosity for reporter log lines (the `[reportforge]` prefix).
96
+ * `'silent'` suppresses everything, including error lines — a PDF failure
97
+ * then surfaces only as a missing file (the reporter never fails the test
98
+ * run). `'debug'` adds diagnostic detail. The `RF_DEBUG=1` environment
99
+ * variable always forces `'debug'`, overriding this option (support
100
+ * escape hatch).
101
+ * @default 'info'
102
+ */
103
+ logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
94
104
  /**
95
105
  * Full path to a Chrome/Chromium executable.
96
106
  * Falls back to PUPPETEER_EXECUTABLE_PATH env, then system Chrome discovery.
package/dist/index.js CHANGED
@@ -10532,6 +10532,7 @@ var ReporterOptionsSchema = external_exports.object({
10532
10532
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
10533
10533
  projectName: external_exports.string().optional(),
10534
10534
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
10535
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
10535
10536
  puppeteerExecutablePath: external_exports.string().optional(),
10536
10537
  serverUrl: external_exports.string().url().optional(),
10537
10538
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -10668,13 +10669,48 @@ var import_fs = __toESM(require("fs"));
10668
10669
  var import_path = __toESM(require("path"));
10669
10670
  var import_os = __toESM(require("os"));
10670
10671
  var import_crypto3 = require("crypto");
10672
+
10673
+ // src/utils/logger.ts
10674
+ init_cjs_shims();
10675
+ var PREFIX = "[reportforge]";
10676
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
10677
+ var threshold = LEVELS.info;
10678
+ function setLogLevel(level) {
10679
+ threshold = LEVELS[level];
10680
+ }
10681
+ function isLevelEnabled(level) {
10682
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
10683
+ }
10684
+ var logger = {
10685
+ info(msg) {
10686
+ if (!isLevelEnabled("info")) return;
10687
+ console.log(`${PREFIX} ${msg}`);
10688
+ },
10689
+ warn(msg) {
10690
+ if (!isLevelEnabled("warn")) return;
10691
+ console.warn(`${PREFIX} WARN: ${msg}`);
10692
+ },
10693
+ error(msg) {
10694
+ if (!isLevelEnabled("error")) return;
10695
+ console.error(`${PREFIX} ERROR: ${msg}`);
10696
+ },
10697
+ debug(msg, data) {
10698
+ if (!isLevelEnabled("debug")) return;
10699
+ const suffix = data !== void 0 ? `
10700
+ ${JSON.stringify(data, null, 2)}` : "";
10701
+ console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10702
+ }
10703
+ };
10704
+
10705
+ // src/license/DeviceId.ts
10671
10706
  var DEVICE_FILE = import_path.default.join(import_os.default.homedir(), ".reportforge", "device.json");
10672
10707
  function getStableDeviceId() {
10673
10708
  try {
10674
10709
  const raw = import_fs.default.readFileSync(DEVICE_FILE, "utf8");
10675
10710
  const parsed = JSON.parse(raw);
10676
10711
  if (parsed.id && /^[0-9a-f-]{36}$/i.test(parsed.id)) return parsed.id;
10677
- } catch {
10712
+ } catch (err) {
10713
+ logger.debug("device id: no readable device.json \u2014 creating new id", { error: err.message });
10678
10714
  }
10679
10715
  const id = (0, import_crypto3.randomUUID)();
10680
10716
  try {
@@ -10682,7 +10718,10 @@ function getStableDeviceId() {
10682
10718
  import_fs.default.writeFileSync(DEVICE_FILE, JSON.stringify({ id, createdAt: Date.now() }), {
10683
10719
  mode: 384
10684
10720
  });
10685
- } catch {
10721
+ } catch (err) {
10722
+ logger.warn(
10723
+ `Could not persist device id (~/.reportforge/device.json) \u2014 this machine may register a new seat on every run: ${err.message}`
10724
+ );
10686
10725
  }
10687
10726
  return id;
10688
10727
  }
@@ -10709,10 +10748,13 @@ function getMachineFingerprint() {
10709
10748
  label: `${ci.provider}/${ci.repo} (CI)`
10710
10749
  };
10711
10750
  }
10712
- const hostname = safe(() => import_os2.default.hostname()) ?? "unknown-host";
10751
+ const hostname = safe(() => import_os2.default.hostname());
10752
+ if (hostname === void 0) {
10753
+ logger.debug("fingerprint: os.hostname() failed \u2014 label falls back to unknown-host");
10754
+ }
10713
10755
  return {
10714
10756
  hash: sha256Hex(`dev:${getStableDeviceId()}`),
10715
- label: `${hostname} (dev)`
10757
+ label: `${hostname ?? "unknown-host"} (dev)`
10716
10758
  };
10717
10759
  }
10718
10760
  function detectCiRepo() {
@@ -10754,19 +10796,25 @@ function readCache() {
10754
10796
  if (f.endsWith(".tmp")) {
10755
10797
  try {
10756
10798
  import_fs2.default.unlinkSync(import_path2.default.join(CACHE_DIR, f));
10757
- } catch {
10799
+ } catch (err) {
10800
+ logger.debug("license cache: stale tmp cleanup failed", { file: f, error: err.message });
10758
10801
  }
10759
10802
  }
10760
10803
  }
10761
- } catch {
10804
+ } catch (err) {
10805
+ logger.debug("license cache: cache dir not readable (first run?)", { error: err.message });
10762
10806
  }
10763
10807
  try {
10764
10808
  if (!import_fs2.default.existsSync(CACHE_FILE)) return null;
10765
10809
  const raw = import_fs2.default.readFileSync(CACHE_FILE, "utf8");
10766
10810
  const parsed = JSON.parse(raw);
10767
- if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") return null;
10811
+ if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") {
10812
+ logger.debug("license cache: malformed entry \u2014 ignoring");
10813
+ return null;
10814
+ }
10768
10815
  return parsed;
10769
- } catch {
10816
+ } catch (err) {
10817
+ logger.debug("license cache: read failed \u2014 treating as no cache", { error: err.message });
10770
10818
  return null;
10771
10819
  }
10772
10820
  }
@@ -10776,13 +10824,15 @@ function writeCache(entry) {
10776
10824
  const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
10777
10825
  import_fs2.default.writeFileSync(tmp, JSON.stringify(entry, null, 2), { mode: 384 });
10778
10826
  import_fs2.default.renameSync(tmp, CACHE_FILE);
10779
- } catch {
10827
+ } catch (err) {
10828
+ logger.debug("license cache: write failed \u2014 will re-activate next run", { error: err.message });
10780
10829
  }
10781
10830
  }
10782
10831
  function clearCache() {
10783
10832
  try {
10784
10833
  import_fs2.default.unlinkSync(CACHE_FILE);
10785
- } catch {
10834
+ } catch (err) {
10835
+ logger.debug("license cache: clear failed", { error: err.message });
10786
10836
  }
10787
10837
  }
10788
10838
 
@@ -10801,27 +10851,6 @@ var import_node_crypto = require("crypto");
10801
10851
  init_cjs_shims();
10802
10852
  var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
10803
10853
 
10804
- // src/utils/logger.ts
10805
- init_cjs_shims();
10806
- var PREFIX = "[reportforge]";
10807
- var logger = {
10808
- info(msg) {
10809
- console.log(`${PREFIX} ${msg}`);
10810
- },
10811
- warn(msg) {
10812
- console.warn(`${PREFIX} WARN: ${msg}`);
10813
- },
10814
- error(msg) {
10815
- console.error(`${PREFIX} ERROR: ${msg}`);
10816
- },
10817
- debug(msg, data) {
10818
- if (!process.env.RF_DEBUG) return;
10819
- const suffix = data !== void 0 ? `
10820
- ${JSON.stringify(data, null, 2)}` : "";
10821
- console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10822
- }
10823
- };
10824
-
10825
10854
  // src/analysis/ModelStore.ts
10826
10855
  var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10827
10856
  var testDeps = null;
@@ -11092,9 +11121,16 @@ var LicenseClient = class {
11092
11121
  function verifyCache(verifier, cached, expectedFp) {
11093
11122
  try {
11094
11123
  const payload = verifier.verify(cached.jwt);
11095
- if (payload.fp !== expectedFp) return null;
11124
+ if (payload.fp !== expectedFp) {
11125
+ logger.debug("license cache rejected: fingerprint mismatch", {
11126
+ cachedFp: payload.fp.slice(0, 12),
11127
+ expectedFp: expectedFp.slice(0, 12)
11128
+ });
11129
+ return null;
11130
+ }
11096
11131
  return payload;
11097
- } catch {
11132
+ } catch (err) {
11133
+ logger.debug("license cache rejected: JWT verify failed", { error: err.message });
11098
11134
  return null;
11099
11135
  }
11100
11136
  }
@@ -11150,7 +11186,8 @@ function maybePrintUpdateNotice(licenseInfo, currentVersion) {
11150
11186
  logger.info(
11151
11187
  `Update available: ${currentVersion} -> ${shown}. Run: npm i -D @reportforge/playwright-pdf@latest`
11152
11188
  );
11153
- } catch {
11189
+ } catch (err) {
11190
+ logger.debug("update notice failed (ignored)", { error: err.message });
11154
11191
  }
11155
11192
  }
11156
11193
 
@@ -12847,8 +12884,7 @@ var BrowserManager = class {
12847
12884
  }
12848
12885
  async close() {
12849
12886
  if (this.browser) {
12850
- await this.browser.close().catch(() => {
12851
- });
12887
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
12852
12888
  this.browser = void 0;
12853
12889
  }
12854
12890
  }
@@ -12860,7 +12896,8 @@ var BrowserManager = class {
12860
12896
  const chromeFinder = require_lib2();
12861
12897
  const found = chromeFinder();
12862
12898
  if (found && import_fs6.default.existsSync(found)) return found;
12863
- } catch {
12899
+ } catch (err) {
12900
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
12864
12901
  }
12865
12902
  const linuxPaths = [
12866
12903
  "/usr/bin/google-chrome",
@@ -12878,12 +12915,14 @@ var BrowserManager = class {
12878
12915
  }).toString().trim().split("\n")[0];
12879
12916
  if (found && import_fs6.default.existsSync(found)) return found;
12880
12917
  } catch {
12918
+ logger.debug("chrome discovery: which lookup found nothing");
12881
12919
  }
12882
12920
  if (puppeteer.executablePath) {
12883
12921
  try {
12884
12922
  const bundled = puppeteer.executablePath();
12885
12923
  if (bundled && import_fs6.default.existsSync(bundled)) return bundled;
12886
- } catch {
12924
+ } catch (err) {
12925
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
12887
12926
  }
12888
12927
  }
12889
12928
  logger.warn(
@@ -12909,8 +12948,10 @@ var HtmlWriter = class {
12909
12948
  }
12910
12949
  async cleanup() {
12911
12950
  if (this.tempPath) {
12912
- await import_fs7.default.promises.unlink(this.tempPath).catch(() => {
12913
- });
12951
+ const tempPath = this.tempPath;
12952
+ await import_fs7.default.promises.unlink(tempPath).catch(
12953
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
12954
+ );
12914
12955
  this.tempPath = void 0;
12915
12956
  }
12916
12957
  }
@@ -12975,7 +13016,8 @@ var PdfEncryptor = class {
12975
13016
  try {
12976
13017
  (0, import_child_process3.execFileSync)("qpdf", ["--version"], { stdio: "pipe" });
12977
13018
  return true;
12978
- } catch {
13019
+ } catch (err) {
13020
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
12979
13021
  return false;
12980
13022
  }
12981
13023
  }
@@ -13132,7 +13174,8 @@ var PdfGenerator = class {
13132
13174
  if (CHARTJS_BUNDLE2) {
13133
13175
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
13134
13176
  }
13135
- } catch {
13177
+ } catch (err) {
13178
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
13136
13179
  }
13137
13180
  }
13138
13181
  /**
@@ -14048,7 +14091,7 @@ function buildWatchUrl(serverUrl, runId, token) {
14048
14091
  }
14049
14092
  function formatLiveBanner(url) {
14050
14093
  const label = "Live Tracker";
14051
- const link = `Watch live: ${url}`;
14094
+ const link = `[reportforge] Watch live: ${url}`;
14052
14095
  const width = Math.max(label.length, link.length);
14053
14096
  const rule = "\u2500".repeat(width + 2);
14054
14097
  return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
@@ -14154,8 +14197,9 @@ var PdfReporter = class {
14154
14197
  this.liveSteps = "failed";
14155
14198
  this.liveConsole = false;
14156
14199
  this.options = parseOptions(rawOptions);
14200
+ setLogLevel(this.options.logLevel);
14157
14201
  this.dataCollector = new DataCollector(this.options.capture);
14158
- this.version = true ? "0.17.0" : "0.x";
14202
+ this.version = true ? "0.19.0" : "0.x";
14159
14203
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14160
14204
  this.licenseClient = new LicenseClient({
14161
14205
  licenseKey: this.options.licenseKey,
@@ -14418,8 +14462,9 @@ var PdfReporter = class {
14418
14462
  );
14419
14463
  return;
14420
14464
  }
14421
- logger.info(`Live Tracker: ${watchUrl}`);
14422
- console.log(formatLiveBanner(watchUrl));
14465
+ if (isLevelEnabled("info")) {
14466
+ console.log(formatLiveBanner(watchUrl));
14467
+ }
14423
14468
  const isPrimaryShard = !shard || shard.current === 1;
14424
14469
  const notify = this.options.notify;
14425
14470
  if (isPrimaryShard && notify) {
package/dist/index.mjs CHANGED
@@ -10533,6 +10533,7 @@ var ReporterOptionsSchema = external_exports.object({
10533
10533
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
10534
10534
  projectName: external_exports.string().optional(),
10535
10535
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
10536
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
10536
10537
  puppeteerExecutablePath: external_exports.string().optional(),
10537
10538
  serverUrl: external_exports.string().url().optional(),
10538
10539
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -10669,13 +10670,48 @@ import fs from "fs";
10669
10670
  import path2 from "path";
10670
10671
  import os from "os";
10671
10672
  import { randomUUID } from "crypto";
10673
+
10674
+ // src/utils/logger.ts
10675
+ init_esm_shims();
10676
+ var PREFIX = "[reportforge]";
10677
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
10678
+ var threshold = LEVELS.info;
10679
+ function setLogLevel(level) {
10680
+ threshold = LEVELS[level];
10681
+ }
10682
+ function isLevelEnabled(level) {
10683
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
10684
+ }
10685
+ var logger = {
10686
+ info(msg) {
10687
+ if (!isLevelEnabled("info")) return;
10688
+ console.log(`${PREFIX} ${msg}`);
10689
+ },
10690
+ warn(msg) {
10691
+ if (!isLevelEnabled("warn")) return;
10692
+ console.warn(`${PREFIX} WARN: ${msg}`);
10693
+ },
10694
+ error(msg) {
10695
+ if (!isLevelEnabled("error")) return;
10696
+ console.error(`${PREFIX} ERROR: ${msg}`);
10697
+ },
10698
+ debug(msg, data) {
10699
+ if (!isLevelEnabled("debug")) return;
10700
+ const suffix = data !== void 0 ? `
10701
+ ${JSON.stringify(data, null, 2)}` : "";
10702
+ console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10703
+ }
10704
+ };
10705
+
10706
+ // src/license/DeviceId.ts
10672
10707
  var DEVICE_FILE = path2.join(os.homedir(), ".reportforge", "device.json");
10673
10708
  function getStableDeviceId() {
10674
10709
  try {
10675
10710
  const raw = fs.readFileSync(DEVICE_FILE, "utf8");
10676
10711
  const parsed = JSON.parse(raw);
10677
10712
  if (parsed.id && /^[0-9a-f-]{36}$/i.test(parsed.id)) return parsed.id;
10678
- } catch {
10713
+ } catch (err) {
10714
+ logger.debug("device id: no readable device.json \u2014 creating new id", { error: err.message });
10679
10715
  }
10680
10716
  const id = randomUUID();
10681
10717
  try {
@@ -10683,7 +10719,10 @@ function getStableDeviceId() {
10683
10719
  fs.writeFileSync(DEVICE_FILE, JSON.stringify({ id, createdAt: Date.now() }), {
10684
10720
  mode: 384
10685
10721
  });
10686
- } catch {
10722
+ } catch (err) {
10723
+ logger.warn(
10724
+ `Could not persist device id (~/.reportforge/device.json) \u2014 this machine may register a new seat on every run: ${err.message}`
10725
+ );
10687
10726
  }
10688
10727
  return id;
10689
10728
  }
@@ -10710,10 +10749,13 @@ function getMachineFingerprint() {
10710
10749
  label: `${ci.provider}/${ci.repo} (CI)`
10711
10750
  };
10712
10751
  }
10713
- const hostname = safe(() => os2.hostname()) ?? "unknown-host";
10752
+ const hostname = safe(() => os2.hostname());
10753
+ if (hostname === void 0) {
10754
+ logger.debug("fingerprint: os.hostname() failed \u2014 label falls back to unknown-host");
10755
+ }
10714
10756
  return {
10715
10757
  hash: sha256Hex(`dev:${getStableDeviceId()}`),
10716
- label: `${hostname} (dev)`
10758
+ label: `${hostname ?? "unknown-host"} (dev)`
10717
10759
  };
10718
10760
  }
10719
10761
  function detectCiRepo() {
@@ -10755,19 +10797,25 @@ function readCache() {
10755
10797
  if (f.endsWith(".tmp")) {
10756
10798
  try {
10757
10799
  fs2.unlinkSync(path3.join(CACHE_DIR, f));
10758
- } catch {
10800
+ } catch (err) {
10801
+ logger.debug("license cache: stale tmp cleanup failed", { file: f, error: err.message });
10759
10802
  }
10760
10803
  }
10761
10804
  }
10762
- } catch {
10805
+ } catch (err) {
10806
+ logger.debug("license cache: cache dir not readable (first run?)", { error: err.message });
10763
10807
  }
10764
10808
  try {
10765
10809
  if (!fs2.existsSync(CACHE_FILE)) return null;
10766
10810
  const raw = fs2.readFileSync(CACHE_FILE, "utf8");
10767
10811
  const parsed = JSON.parse(raw);
10768
- if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") return null;
10812
+ if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") {
10813
+ logger.debug("license cache: malformed entry \u2014 ignoring");
10814
+ return null;
10815
+ }
10769
10816
  return parsed;
10770
- } catch {
10817
+ } catch (err) {
10818
+ logger.debug("license cache: read failed \u2014 treating as no cache", { error: err.message });
10771
10819
  return null;
10772
10820
  }
10773
10821
  }
@@ -10777,13 +10825,15 @@ function writeCache(entry) {
10777
10825
  const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
10778
10826
  fs2.writeFileSync(tmp, JSON.stringify(entry, null, 2), { mode: 384 });
10779
10827
  fs2.renameSync(tmp, CACHE_FILE);
10780
- } catch {
10828
+ } catch (err) {
10829
+ logger.debug("license cache: write failed \u2014 will re-activate next run", { error: err.message });
10781
10830
  }
10782
10831
  }
10783
10832
  function clearCache() {
10784
10833
  try {
10785
10834
  fs2.unlinkSync(CACHE_FILE);
10786
- } catch {
10835
+ } catch (err) {
10836
+ logger.debug("license cache: clear failed", { error: err.message });
10787
10837
  }
10788
10838
  }
10789
10839
 
@@ -10802,27 +10852,6 @@ import { createPublicKey as createPublicKey2, verify as verify2 } from "crypto";
10802
10852
  init_esm_shims();
10803
10853
  var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
10804
10854
 
10805
- // src/utils/logger.ts
10806
- init_esm_shims();
10807
- var PREFIX = "[reportforge]";
10808
- var logger = {
10809
- info(msg) {
10810
- console.log(`${PREFIX} ${msg}`);
10811
- },
10812
- warn(msg) {
10813
- console.warn(`${PREFIX} WARN: ${msg}`);
10814
- },
10815
- error(msg) {
10816
- console.error(`${PREFIX} ERROR: ${msg}`);
10817
- },
10818
- debug(msg, data) {
10819
- if (!process.env.RF_DEBUG) return;
10820
- const suffix = data !== void 0 ? `
10821
- ${JSON.stringify(data, null, 2)}` : "";
10822
- console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10823
- }
10824
- };
10825
-
10826
10855
  // src/analysis/ModelStore.ts
10827
10856
  var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10828
10857
  var testDeps = null;
@@ -11093,9 +11122,16 @@ var LicenseClient = class {
11093
11122
  function verifyCache(verifier, cached, expectedFp) {
11094
11123
  try {
11095
11124
  const payload = verifier.verify(cached.jwt);
11096
- if (payload.fp !== expectedFp) return null;
11125
+ if (payload.fp !== expectedFp) {
11126
+ logger.debug("license cache rejected: fingerprint mismatch", {
11127
+ cachedFp: payload.fp.slice(0, 12),
11128
+ expectedFp: expectedFp.slice(0, 12)
11129
+ });
11130
+ return null;
11131
+ }
11097
11132
  return payload;
11098
- } catch {
11133
+ } catch (err) {
11134
+ logger.debug("license cache rejected: JWT verify failed", { error: err.message });
11099
11135
  return null;
11100
11136
  }
11101
11137
  }
@@ -11151,7 +11187,8 @@ function maybePrintUpdateNotice(licenseInfo, currentVersion) {
11151
11187
  logger.info(
11152
11188
  `Update available: ${currentVersion} -> ${shown}. Run: npm i -D @reportforge/playwright-pdf@latest`
11153
11189
  );
11154
- } catch {
11190
+ } catch (err) {
11191
+ logger.debug("update notice failed (ignored)", { error: err.message });
11155
11192
  }
11156
11193
  }
11157
11194
 
@@ -12848,8 +12885,7 @@ var BrowserManager = class {
12848
12885
  }
12849
12886
  async close() {
12850
12887
  if (this.browser) {
12851
- await this.browser.close().catch(() => {
12852
- });
12888
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
12853
12889
  this.browser = void 0;
12854
12890
  }
12855
12891
  }
@@ -12861,7 +12897,8 @@ var BrowserManager = class {
12861
12897
  const chromeFinder = require_lib2();
12862
12898
  const found = chromeFinder();
12863
12899
  if (found && fs7.existsSync(found)) return found;
12864
- } catch {
12900
+ } catch (err) {
12901
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
12865
12902
  }
12866
12903
  const linuxPaths = [
12867
12904
  "/usr/bin/google-chrome",
@@ -12879,12 +12916,14 @@ var BrowserManager = class {
12879
12916
  }).toString().trim().split("\n")[0];
12880
12917
  if (found && fs7.existsSync(found)) return found;
12881
12918
  } catch {
12919
+ logger.debug("chrome discovery: which lookup found nothing");
12882
12920
  }
12883
12921
  if (puppeteer.executablePath) {
12884
12922
  try {
12885
12923
  const bundled = puppeteer.executablePath();
12886
12924
  if (bundled && fs7.existsSync(bundled)) return bundled;
12887
- } catch {
12925
+ } catch (err) {
12926
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
12888
12927
  }
12889
12928
  }
12890
12929
  logger.warn(
@@ -12910,8 +12949,10 @@ var HtmlWriter = class {
12910
12949
  }
12911
12950
  async cleanup() {
12912
12951
  if (this.tempPath) {
12913
- await fs8.promises.unlink(this.tempPath).catch(() => {
12914
- });
12952
+ const tempPath = this.tempPath;
12953
+ await fs8.promises.unlink(tempPath).catch(
12954
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
12955
+ );
12915
12956
  this.tempPath = void 0;
12916
12957
  }
12917
12958
  }
@@ -12976,7 +13017,8 @@ var PdfEncryptor = class {
12976
13017
  try {
12977
13018
  execFileSync("qpdf", ["--version"], { stdio: "pipe" });
12978
13019
  return true;
12979
- } catch {
13020
+ } catch (err) {
13021
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
12980
13022
  return false;
12981
13023
  }
12982
13024
  }
@@ -13133,7 +13175,8 @@ var PdfGenerator = class {
13133
13175
  if (CHARTJS_BUNDLE2) {
13134
13176
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
13135
13177
  }
13136
- } catch {
13178
+ } catch (err) {
13179
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
13137
13180
  }
13138
13181
  }
13139
13182
  /**
@@ -14049,7 +14092,7 @@ function buildWatchUrl(serverUrl, runId, token) {
14049
14092
  }
14050
14093
  function formatLiveBanner(url) {
14051
14094
  const label = "Live Tracker";
14052
- const link = `Watch live: ${url}`;
14095
+ const link = `[reportforge] Watch live: ${url}`;
14053
14096
  const width = Math.max(label.length, link.length);
14054
14097
  const rule = "\u2500".repeat(width + 2);
14055
14098
  return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
@@ -14155,8 +14198,9 @@ var PdfReporter = class {
14155
14198
  this.liveSteps = "failed";
14156
14199
  this.liveConsole = false;
14157
14200
  this.options = parseOptions(rawOptions);
14201
+ setLogLevel(this.options.logLevel);
14158
14202
  this.dataCollector = new DataCollector(this.options.capture);
14159
- this.version = true ? "0.17.0" : "0.x";
14203
+ this.version = true ? "0.19.0" : "0.x";
14160
14204
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14161
14205
  this.licenseClient = new LicenseClient({
14162
14206
  licenseKey: this.options.licenseKey,
@@ -14419,8 +14463,9 @@ var PdfReporter = class {
14419
14463
  );
14420
14464
  return;
14421
14465
  }
14422
- logger.info(`Live Tracker: ${watchUrl}`);
14423
- console.log(formatLiveBanner(watchUrl));
14466
+ if (isLevelEnabled("info")) {
14467
+ console.log(formatLiveBanner(watchUrl));
14468
+ }
14424
14469
  const isPrimaryShard = !shard || shard.current === 1;
14425
14470
  const notify = this.options.notify;
14426
14471
  if (isPrimaryShard && notify) {
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
5
5
  "license": "Elastic-2.0",
6
6
  "author": "ReportForge",
7
7
  "homepage": "https://reportforge.org",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/muralidharan92/reportforge"
11
- },
12
8
  "bugs": {
13
- "url": "https://github.com/muralidharan92/reportforge/issues",
14
9
  "email": "support@reportforge.org"
15
10
  },
16
11
  "keywords": [
@@ -30,7 +25,11 @@
30
25
  "pdf-generator",
31
26
  "ci",
32
27
  "puppeteer",
33
- "html-to-pdf"
28
+ "html-to-pdf",
29
+ "reportforge",
30
+ "reportforge-playwright-pdf",
31
+ "report",
32
+ "forge"
34
33
  ],
35
34
  "main": "./dist/index.js",
36
35
  "module": "./dist/index.mjs",
@@ -53,7 +52,8 @@
53
52
  "ci-templates",
54
53
  "README.md",
55
54
  "CHANGELOG.md",
56
- "LICENSE"
55
+ "LICENSE",
56
+ "SECURITY.md"
57
57
  ],
58
58
  "scripts": {
59
59
  "prebuild": "npm run bundle-chartjs && npm run bundle-fonts && npm run bundle-pubkey && npm run bundle-model && npm run precompile-templates",
@@ -82,14 +82,14 @@
82
82
  },
83
83
  "peerDependencies": {
84
84
  "@playwright/test": ">=1.40.0",
85
- "puppeteer-core": ">=21.0.0 <25.0.0"
85
+ "puppeteer-core": ">=21.0.0 <26.0.0"
86
86
  },
87
87
  "dependencies": {
88
88
  "chrome-finder": "^1.0.7",
89
89
  "framer-motion": "^12.40.0",
90
90
  "glob": "^13.0.6",
91
91
  "handlebars": "^4.7.8",
92
- "sharp": "^0.34.5",
92
+ "sharp": "^0.35.3",
93
93
  "zod": "^3.23.8"
94
94
  },
95
95
  "devDependencies": {
@@ -98,20 +98,20 @@
98
98
  "@fontsource/source-serif-pro": "^5.2.5",
99
99
  "@playwright/test": "^1.49.0",
100
100
  "@types/handlebars": "^4.1.0",
101
- "@types/node": "^22.0.0",
101
+ "@types/node": "^26.1.1",
102
102
  "@typescript-eslint/eslint-plugin": "^8.0.0",
103
103
  "@typescript-eslint/parser": "^8.0.0",
104
- "@vitest/coverage-v8": "^2.1.0",
104
+ "@vitest/coverage-v8": "^4.1.10",
105
105
  "chart.js": "^4.4.0",
106
106
  "eslint": "^9.0.0",
107
107
  "prettier": "^3.3.0",
108
- "puppeteer-core": "^23.0.0",
108
+ "puppeteer-core": "^25.3.0",
109
109
  "tsup": "^8.3.0",
110
110
  "tsx": "^4.19.0",
111
- "typescript": "^5.6.0",
112
- "vitest": "^2.1.0"
111
+ "typescript": "^5.9.3",
112
+ "vitest": "^4.1.10"
113
113
  },
114
114
  "engines": {
115
- "node": ">=18.0.0"
115
+ "node": ">=20.0.0"
116
116
  }
117
117
  }