@piwitests/reporter 0.36.0 → 0.39.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/dist/cli/index.js CHANGED
@@ -422,6 +422,7 @@ function formatGateResult(result) {
422
422
  );
423
423
  }
424
424
  for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
425
+ for (const warning of result.warnings ?? []) lines.push(` \u26A0 ${warning}`);
425
426
  lines.push(` ${facts.runUrl}`);
426
427
  return lines.join("\n");
427
428
  }
@@ -456,6 +457,9 @@ Policy (at least one is required):
456
457
  --fail-on-flaky Fail when this run contains any flaky test
457
458
  --require-selection <key> Fail when a test the named selection matches did not run or failed
458
459
 
460
+ Warn-only (reported, never fails the build):
461
+ --max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
462
+
459
463
  Other:
460
464
  --json Print the raw result as JSON instead of a summary
461
465
  -h, --help Show this help
@@ -477,9 +481,9 @@ function readCount(argv, name) {
477
481
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
478
482
  return Math.floor(n);
479
483
  }
480
- function readRunIdFromFile(path11) {
484
+ function readRunIdFromFile(path13) {
481
485
  try {
482
- const parsed = JSON.parse(fs3.readFileSync(path11, "utf-8"));
486
+ const parsed = JSON.parse(fs3.readFileSync(path13, "utf-8"));
483
487
  const runId = Number(parsed.runId);
484
488
  return Number.isFinite(runId) && runId > 0 ? runId : null;
485
489
  } catch {
@@ -510,7 +514,13 @@ function parseGateArgs(argv, env) {
510
514
  failOnFlaky: argv.includes("--fail-on-flaky"),
511
515
  requireSelection: readOption2(argv, "--require-selection")
512
516
  };
513
- return { serverUrl, apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null, runId, policy };
517
+ return {
518
+ serverUrl,
519
+ apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
520
+ runId,
521
+ policy,
522
+ maxUncoveredChanges: readCount(argv, "--max-uncovered-changes")
523
+ };
514
524
  }
515
525
  async function requestGate(args) {
516
526
  const headers = { "Content-Type": "application/json" };
@@ -518,7 +528,7 @@ async function requestGate(args) {
518
528
  const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
519
529
  method: "POST",
520
530
  headers,
521
- body: JSON.stringify(args.policy)
531
+ body: JSON.stringify({ ...args.policy, maxUncoveredChanges: args.maxUncoveredChanges })
522
532
  });
523
533
  if (!res.ok) {
524
534
  const body = await res.json().catch(() => ({}));
@@ -778,7 +788,8 @@ var WORKFLOW_SKILLS = [
778
788
  "investigate-failure",
779
789
  "apply-locator-healing",
780
790
  "stabilize-flaky-tests",
781
- "run-the-right-tests"
791
+ "run-the-right-tests",
792
+ "write-the-missing-test"
782
793
  ];
783
794
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
784
795
  var DEFAULT_SKILLS_DIR = path7.join(".claude", "skills");
@@ -1675,8 +1686,403 @@ async function runRun(argv, env = process.env) {
1675
1686
  return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
1676
1687
  }
1677
1688
 
1678
- // src/cli/index.ts
1689
+ // src/cli/probe.ts
1690
+ var import_node_child_process4 = require("child_process");
1691
+ var import_node_module3 = require("module");
1692
+ var fs10 = __toESM(require("fs"));
1693
+ var os2 = __toESM(require("os"));
1694
+ var path12 = __toESM(require("path"));
1695
+
1696
+ // src/internal/config/desktop.ts
1697
+ var fs9 = __toESM(require("fs"));
1698
+ var os = __toESM(require("os"));
1699
+ var path11 = __toESM(require("path"));
1700
+
1701
+ // src/internal/config/env.ts
1702
+ var DEFAULTS = {
1703
+ projectName: "default-project",
1704
+ uploadTraces: true,
1705
+ uploadReport: true,
1706
+ liveFileUploads: true,
1707
+ collectScmInfo: true,
1708
+ collectCiInfo: true,
1709
+ collectPerformanceMetrics: true,
1710
+ captureLocators: true,
1711
+ capturePageState: true,
1712
+ // Off by default: the page inventory reads control and link names from every
1713
+ // visited page, so it stays opt-in until a project turns it on.
1714
+ capturePageInventory: false,
1715
+ captureServerTraces: true,
1716
+ sampleAriaOnPass: true,
1717
+ uploadManifest: true,
1718
+ defaultCapture: true,
1719
+ streaming: true,
1720
+ streamingBatchSize: 5,
1721
+ streamingBatchDelay: 2e3,
1722
+ maxStreamBufferBytes: 100 * 1024 * 1024,
1723
+ failOnFlakyTests: false,
1724
+ username: null,
1725
+ password: null,
1726
+ apiKey: null,
1727
+ verbose: false
1728
+ };
1729
+ var PIWI_ENV_KEYS = {
1730
+ serverUrl: "PIWI_DASHBOARD_URL",
1731
+ projectName: "PIWI_PROJECT_NAME",
1732
+ verbose: "PIWI_VERBOSE",
1733
+ apiKey: "PIWI_API_KEY",
1734
+ username: "PIWI_USERNAME",
1735
+ password: "PIWI_PASSWORD",
1736
+ environment: "PIWI_ENVIRONMENT",
1737
+ label: "PIWI_LABEL",
1738
+ runLabel: "PIWI_RUN_LABEL",
1739
+ keep: "PIWI_KEEP",
1740
+ streaming: "PIWI_STREAMING",
1741
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
1742
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
1743
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
1744
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
1745
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
1746
+ uploadTraces: "PIWI_UPLOAD_TRACES",
1747
+ uploadReport: "PIWI_UPLOAD_REPORT",
1748
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
1749
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
1750
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
1751
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
1752
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
1753
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
1754
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
1755
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
1756
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
1757
+ outputFile: "PIWI_OUTPUT_FILE",
1758
+ aiMode: "PIWI_AI",
1759
+ aiDir: "PIWI_AI_DIR",
1760
+ aiOnMiss: "PIWI_AI_ON_MISS",
1761
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
1762
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
1763
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
1764
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
1765
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
1766
+ };
1767
+ var PIWI_PROBE_ENV = {
1768
+ flag: "PIWI_PROBE",
1769
+ plan: "PIWI_PROBE_PLAN",
1770
+ results: "PIWI_PROBE_RESULTS",
1771
+ secret: "PIWI_PROBE_SECRET"
1772
+ };
1773
+ var ENV_FALLBACK_SPECS = [
1774
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
1775
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
1776
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
1777
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
1778
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
1779
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
1780
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
1781
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
1782
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
1783
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
1784
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
1785
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
1786
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
1787
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
1788
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
1789
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
1790
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
1791
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
1792
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
1793
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
1794
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
1795
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
1796
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
1797
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
1798
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
1799
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1800
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1801
+ ];
1802
+
1803
+ // src/cli/probe.ts
1804
+ var EXIT_OK4 = 0;
1805
+ var EXIT_ERROR4 = 2;
1679
1806
  var USAGE6 = `
1807
+ piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
1808
+
1809
+ Usage:
1810
+ npx @piwitests/reporter probe [options] [-- <playwright args>]
1811
+
1812
+ Options:
1813
+ --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL)
1814
+ --api-key <key> Reporter API key (or PIWI_API_KEY)
1815
+ --project <name|id> Project name or id (or PIWI_PROJECT_NAME)
1816
+ --budget <n> Max pairs to probe this run (default the server's)
1817
+
1818
+ Everything after \`--\` is passed to \`playwright test\`.
1819
+ `.trim();
1820
+ function readOption6(argv, name) {
1821
+ const i = argv.indexOf(name);
1822
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
1823
+ }
1824
+ function escapeRegex(s) {
1825
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1826
+ }
1827
+ function resolvePlaywrightCli2() {
1828
+ const require2 = (0, import_node_module3.createRequire)(path12.join(process.cwd(), "noop.js"));
1829
+ for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
1830
+ try {
1831
+ return require2.resolve(id);
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ return null;
1836
+ }
1837
+ function spawnPlaywright2(playwrightArgs, env) {
1838
+ const cli = resolvePlaywrightCli2();
1839
+ const child = cli ? (0, import_node_child_process4.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process4.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs], {
1840
+ stdio: "inherit",
1841
+ env
1842
+ });
1843
+ return new Promise((resolve6) => {
1844
+ child.on("error", (err) => {
1845
+ console.error(`piwi probe: could not start Playwright \u2014 ${err.message}`);
1846
+ resolve6(EXIT_ERROR4);
1847
+ });
1848
+ child.on("exit", (code) => resolve6(code ?? EXIT_ERROR4));
1849
+ });
1850
+ }
1851
+ async function runProbe(argv) {
1852
+ const sep3 = argv.indexOf("--");
1853
+ const own = sep3 < 0 ? argv : argv.slice(0, sep3);
1854
+ const playwrightArgs = sep3 < 0 ? [] : argv.slice(sep3 + 1);
1855
+ if (own.includes("-h") || own.includes("--help")) {
1856
+ console.log(USAGE6);
1857
+ return EXIT_OK4;
1858
+ }
1859
+ const env = process.env;
1860
+ const serverUrl = (readOption6(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
1861
+ if (!serverUrl) {
1862
+ console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1863
+ return EXIT_ERROR4;
1864
+ }
1865
+ const apiKey = readOption6(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
1866
+ const project = readOption6(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
1867
+ const budget = readOption6(own, "--budget");
1868
+ let projectId;
1869
+ try {
1870
+ projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
1871
+ } catch (e) {
1872
+ console.error(`piwi probe: ${e.message}`);
1873
+ return EXIT_ERROR4;
1874
+ }
1875
+ const authHeaders2 = apiKey ? { "X-API-Key": apiKey } : {};
1876
+ const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
1877
+ let plan;
1878
+ try {
1879
+ const res = await fetch(planUrl, { headers: authHeaders2 });
1880
+ if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
1881
+ plan = await res.json();
1882
+ } catch (e) {
1883
+ console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
1884
+ return EXIT_ERROR4;
1885
+ }
1886
+ if (!plan.items || plan.items.length === 0) {
1887
+ console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
1888
+ return EXIT_OK4;
1889
+ }
1890
+ console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
1891
+ const dir = fs10.mkdtempSync(path12.join(os2.tmpdir(), "piwi-probe-"));
1892
+ const planFile = path12.join(dir, "plan.json");
1893
+ const resultsFile = path12.join(dir, "results.jsonl");
1894
+ const runOutputFile = path12.join(dir, "run.json");
1895
+ fs10.writeFileSync(planFile, JSON.stringify(plan));
1896
+ fs10.writeFileSync(resultsFile, "");
1897
+ const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex).join("|");
1898
+ const pwArgs = [...playwrightArgs];
1899
+ if (grep && !playwrightArgs.includes("--grep") && !playwrightArgs.includes("-g")) pwArgs.push("--grep", grep);
1900
+ const childEnv = {
1901
+ ...env,
1902
+ [PIWI_PROBE_ENV.flag]: "1",
1903
+ [PIWI_PROBE_ENV.plan]: planFile,
1904
+ [PIWI_PROBE_ENV.results]: resultsFile,
1905
+ // The reporter creates the probe run and knows its server-assigned id; have
1906
+ // it write the run output here so the results POST can name the run.
1907
+ [PIWI_ENV_KEYS.outputFile]: runOutputFile
1908
+ };
1909
+ await spawnPlaywright2(pwArgs, childEnv);
1910
+ const lines = fs10.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
1911
+ const results = [];
1912
+ for (const line of lines) {
1913
+ try {
1914
+ results.push(JSON.parse(line));
1915
+ } catch {
1916
+ }
1917
+ }
1918
+ if (results.length === 0) {
1919
+ console.error("piwi probe: no probe outcomes were recorded (are the Piwi capture fixtures active?).");
1920
+ return EXIT_OK4;
1921
+ }
1922
+ let runId = null;
1923
+ try {
1924
+ const out = JSON.parse(fs10.readFileSync(runOutputFile, "utf8"));
1925
+ if (typeof out.runId === "number") runId = out.runId;
1926
+ } catch {
1927
+ }
1928
+ try {
1929
+ const res = await fetch(`${serverUrl}/api/projects/${projectId}/probes/results`, {
1930
+ method: "POST",
1931
+ headers: { "Content-Type": "application/json", ...authHeaders2 },
1932
+ body: JSON.stringify({ runId, results })
1933
+ });
1934
+ if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
1935
+ const body = await res.json();
1936
+ const noticed = results.filter((r) => r.outcome === "noticed").length;
1937
+ const notNoticed = results.filter((r) => r.outcome === "not-noticed").length;
1938
+ console.error(
1939
+ `piwi probe: recorded ${body.recorded ?? results.length} outcome(s) \u2014 ${noticed} noticed, ${notNoticed} not noticed.`
1940
+ );
1941
+ } catch (e) {
1942
+ console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
1943
+ return EXIT_ERROR4;
1944
+ }
1945
+ return EXIT_OK4;
1946
+ }
1947
+
1948
+ // src/cli/quality-report.ts
1949
+ var fs11 = __toESM(require("fs"));
1950
+ var EXIT_OK5 = 0;
1951
+ var EXIT_VERDICT = 1;
1952
+ var EXIT_ERROR5 = 2;
1953
+ var FORMATS = ["md", "json", "html", "pdf", "csv"];
1954
+ var DASHBOARDS = ["executive", "engineering", "overview"];
1955
+ var USAGE7 = `
1956
+ piwi report \u2014 print a quality report from the dashboard
1957
+
1958
+ Usage:
1959
+ npx @piwitests/reporter report [options]
1960
+
1961
+ Connection:
1962
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
1963
+ --api-key <key> API key (env PIWI_API_KEY)
1964
+
1965
+ What to report:
1966
+ --project <names|ids> Comma-separated projects (default: every project you can see)
1967
+ --period <period> 7d, 30d (default), last-month, this-quarter, 2026-08-01..2026-08-31, \u2026
1968
+ --compare <mode> previous (default), year, none
1969
+ --dashboard <name> executive (default), engineering, overview
1970
+ --branch <names> Comma-separated branches (default: each project's default branch)
1971
+ --environment <names> Comma-separated environments
1972
+ --selection <key> Only the tests of this selection
1973
+ --lang <code> Report language, such as en or fr (default: the dashboard's default)
1974
+
1975
+ Output:
1976
+ --format <fmt> md (default), json, html, pdf, csv
1977
+ --output <file> Write to a file instead of stdout (required for pdf)
1978
+ --fail-on <tone> Exit 1 when the verdict is bad, or mixed or bad (--fail-on mixed)
1979
+ -h, --help Show this help
1980
+
1981
+ Exit codes: 0 written, 1 written and the verdict is at or below --fail-on, 2 no report.
1982
+ `.trim();
1983
+ function readOption7(argv, name) {
1984
+ const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
1985
+ if (withEquals) return withEquals.slice(name.length + 1);
1986
+ const index = argv.indexOf(name);
1987
+ if (index === -1) return void 0;
1988
+ const value = argv[index + 1];
1989
+ return value && !value.startsWith("--") ? value : void 0;
1990
+ }
1991
+ function normalizePeriod(raw) {
1992
+ const days = raw.trim().match(/^(\d+)d$/);
1993
+ return days ? `last-${days[1]}d` : raw.trim();
1994
+ }
1995
+ function parseReportArgs(argv, env) {
1996
+ const serverUrl = (readOption7(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
1997
+ if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1998
+ const format = (readOption7(argv, "--format") ?? "md").toLowerCase();
1999
+ if (!FORMATS.includes(format)) {
2000
+ throw new Error(`--format must be one of ${FORMATS.join(", ")}, got "${format}"`);
2001
+ }
2002
+ const dashboard = readOption7(argv, "--dashboard") ?? "executive";
2003
+ if (!DASHBOARDS.includes(dashboard)) {
2004
+ throw new Error(`--dashboard must be one of ${DASHBOARDS.join(", ")}, got "${dashboard}"`);
2005
+ }
2006
+ const lang = readOption7(argv, "--lang") ?? null;
2007
+ if (lang !== null && !/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i.test(lang)) {
2008
+ throw new Error(`--lang must be a language code such as en or fr, got "${lang}"`);
2009
+ }
2010
+ const failOn = readOption7(argv, "--fail-on") ?? null;
2011
+ if (failOn !== null && failOn !== "bad" && failOn !== "mixed") {
2012
+ throw new Error(`--fail-on must be bad or mixed, got "${failOn}"`);
2013
+ }
2014
+ const output = readOption7(argv, "--output") ?? null;
2015
+ if (format === "pdf" && !output) throw new Error("A PDF needs --output <file>");
2016
+ return {
2017
+ serverUrl,
2018
+ apiKey: readOption7(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
2019
+ projects: (readOption7(argv, "--project") ?? "").split(",").map((p) => p.trim()).filter(Boolean),
2020
+ period: normalizePeriod(readOption7(argv, "--period") ?? "30d"),
2021
+ compare: readOption7(argv, "--compare") ?? null,
2022
+ dashboard,
2023
+ branches: readOption7(argv, "--branch") ?? null,
2024
+ environments: readOption7(argv, "--environment") ?? null,
2025
+ selection: readOption7(argv, "--selection") ?? null,
2026
+ lang,
2027
+ format,
2028
+ output,
2029
+ failOn
2030
+ };
2031
+ }
2032
+ function reportQuery(args, projectIds, format) {
2033
+ const q = new URLSearchParams({ dashboard: args.dashboard, format, period: args.period });
2034
+ if (projectIds.length > 0) q.set("projects", projectIds.join(","));
2035
+ if (args.compare) q.set("compare", args.compare);
2036
+ if (args.branches) q.set("branches", args.branches);
2037
+ if (args.environments) q.set("environments", args.environments);
2038
+ if (args.selection) q.set("sel", args.selection);
2039
+ if (args.lang) q.set("lang", args.lang);
2040
+ return q;
2041
+ }
2042
+ function failsOn(tone, failOn) {
2043
+ if (!failOn) return false;
2044
+ return failOn === "mixed" ? tone !== "good" : tone === "bad";
2045
+ }
2046
+ async function fetchReport(args, query) {
2047
+ const res = await fetch(`${args.serverUrl}/api/reports/preview?${query}`, {
2048
+ headers: args.apiKey ? { "X-API-Key": args.apiKey } : {}
2049
+ });
2050
+ if (!res.ok) {
2051
+ const body = await res.json().catch(() => ({}));
2052
+ throw new Error(body.message || `Dashboard returned ${res.status}`);
2053
+ }
2054
+ return res;
2055
+ }
2056
+ async function runQualityReport(argv) {
2057
+ if (argv.includes("-h") || argv.includes("--help")) {
2058
+ console.log(USAGE7);
2059
+ return EXIT_OK5;
2060
+ }
2061
+ try {
2062
+ const args = parseReportArgs(argv, process.env);
2063
+ const projectIds = [];
2064
+ for (const project of args.projects) {
2065
+ projectIds.push(await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project, key: "" }));
2066
+ }
2067
+ const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
2068
+ const bytes = new Uint8Array(await res.arrayBuffer());
2069
+ if (args.output) fs11.writeFileSync(args.output, bytes);
2070
+ else process.stdout.write(bytes);
2071
+ if (!args.failOn) return EXIT_OK5;
2072
+ const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();
2073
+ if (failsOn(bundle.verdict.tone, args.failOn)) {
2074
+ console.error(`piwi report: the verdict is ${bundle.verdict.tone}`);
2075
+ return EXIT_VERDICT;
2076
+ }
2077
+ return EXIT_OK5;
2078
+ } catch (e) {
2079
+ console.error(`piwi report: ${e.message}`);
2080
+ return EXIT_ERROR5;
2081
+ }
2082
+ }
2083
+
2084
+ // src/cli/index.ts
2085
+ var USAGE8 = `
1680
2086
  piwi \u2014 companion commands for the Piwi Dashboard reporter
1681
2087
 
1682
2088
  Usage:
@@ -1686,8 +2092,10 @@ Commands:
1686
2092
  init Wire a Playwright project up to a Piwi Dashboard
1687
2093
  skills Install the Piwi agent skills into this project
1688
2094
  gate Fail a CI job on the dashboard's analysis of a run
2095
+ report Print a quality report (Markdown, PDF, HTML, CSV or JSON)
1689
2096
  select Print the Playwright args for a saved test selection
1690
2097
  run Run a saved test selection with playwright test
2098
+ probe Run the dashboard's probe plan and record what the suite noticed
1691
2099
  ai Manage committed natural-language AI-step artifacts
1692
2100
 
1693
2101
  Run \`npx @piwitests/reporter <command> --help\` for a command's options.
@@ -1703,21 +2111,25 @@ async function main() {
1703
2111
  return runSkills(rest, findTemplatesDir(__dirname));
1704
2112
  case "gate":
1705
2113
  return runGate(rest);
2114
+ case "report":
2115
+ return runQualityReport(rest);
1706
2116
  case "select":
1707
2117
  return runSelect(rest);
1708
2118
  case "run":
1709
2119
  return runRun(rest);
2120
+ case "probe":
2121
+ return runProbe(rest);
1710
2122
  case "ai":
1711
2123
  return runAi(rest);
1712
2124
  case void 0:
1713
2125
  case "-h":
1714
2126
  case "--help":
1715
- console.log(USAGE6);
2127
+ console.log(USAGE8);
1716
2128
  return 0;
1717
2129
  default:
1718
2130
  console.error(`piwi: unknown command "${command}"
1719
2131
  `);
1720
- console.error(USAGE6);
2132
+ console.error(USAGE8);
1721
2133
  return 2;
1722
2134
  }
1723
2135
  }