@plune-ai/cli 0.32.2 → 0.33.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.cjs CHANGED
@@ -70,6 +70,21 @@ var init_api_url = __esm({
70
70
  }
71
71
  });
72
72
 
73
+ // src/importers/min-score.ts
74
+ function parseMinScore(value2, previous = {}) {
75
+ const match = /^([A-Za-z0-9_-]+)=(\d+(?:\.\d+)?)$/.exec(value2.trim());
76
+ if (match === null) throw new Error(`"${value2}" is not category=score (for example performance=90).`);
77
+ const score = Number(match[2]);
78
+ if (score > 100) throw new Error(`"${value2}" \u2014 a Lighthouse score is 0 to 100.`);
79
+ return { ...previous, [match[1]]: score };
80
+ }
81
+ var init_min_score = __esm({
82
+ "src/importers/min-score.ts"() {
83
+ "use strict";
84
+ init_cjs_shims();
85
+ }
86
+ });
87
+
73
88
  // src/cli/env.ts
74
89
  var env_exports = {};
75
90
  __export(env_exports, {
@@ -558,24 +573,24 @@ var init_prices = __esm({
558
573
  function priceFrom(price, usage3) {
559
574
  return usage3.input_tokens / 1e3 * price.input_per_1k_usd + usage3.output_tokens / 1e3 * price.output_per_1k_usd;
560
575
  }
561
- function resolveCost(usage3, model, reportedCostUsd, overrides, deps = {}) {
576
+ function resolveCostEntry(usage3, model, reportedCostUsd, overrides, deps = {}) {
562
577
  const table = deps.table ?? PRICE_TABLE;
563
578
  const warn = deps.warn ?? defaultWarn;
564
579
  const override = overrides?.[model];
565
580
  if (override !== void 0) {
566
- return priceFrom(override, usage3);
581
+ return { cost_usd: priceFrom(override, usage3), priced: true };
567
582
  }
568
583
  if (reportedCostUsd !== void 0) {
569
- return reportedCostUsd;
584
+ return { cost_usd: reportedCostUsd, priced: true };
570
585
  }
571
586
  const tablePrice = table[model];
572
587
  if (tablePrice !== void 0) {
573
- return priceFrom(tablePrice, usage3);
588
+ return { cost_usd: priceFrom(tablePrice, usage3), priced: true };
574
589
  }
575
590
  warn(
576
591
  `plune: no price for model "${model}" \u2014 reporting cost_usd=0. Set pricing["${model}"] in plune.yaml to track its cost.`
577
592
  );
578
- return 0;
593
+ return { cost_usd: 0, priced: false };
579
594
  }
580
595
  var defaultWarn;
581
596
  var init_cost = __esm({
@@ -630,7 +645,7 @@ function makeAnthropicProvider(config, env, pricing) {
630
645
  }
631
646
  },
632
647
  estimateCost(usage3, reportedCostUsd) {
633
- return { cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing) };
648
+ return resolveCostEntry(usage3, config.model, reportedCostUsd, pricing);
634
649
  }
635
650
  };
636
651
  }
@@ -697,7 +712,7 @@ function makeOpenAiCompatibleProvider(opts) {
697
712
  }
698
713
  },
699
714
  estimateCost(usage3, reportedCostUsd) {
700
- return { cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing) };
715
+ return resolveCostEntry(usage3, config.model, reportedCostUsd, pricing);
701
716
  }
702
717
  };
703
718
  }
@@ -800,9 +815,7 @@ function makeEstimateProvider(config, pricing) {
800
815
  complete: () => Promise.reject(
801
816
  new Error("The --dry-run provider only estimates cost; it never calls a model.")
802
817
  ),
803
- estimateCost: (usage3, reportedCostUsd) => ({
804
- cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing)
805
- })
818
+ estimateCost: (usage3, reportedCostUsd) => resolveCostEntry(usage3, config.model, reportedCostUsd, pricing)
806
819
  };
807
820
  }
808
821
  var init_estimate = __esm({
@@ -1774,6 +1787,12 @@ function toRunError(err) {
1774
1787
  function estimateUsage(prompt5, maxTokens) {
1775
1788
  return { input_tokens: Math.ceil(prompt5.length / 4), output_tokens: maxTokens };
1776
1789
  }
1790
+ function pricedField(priced) {
1791
+ return priced === void 0 ? {} : { priced };
1792
+ }
1793
+ function costFields(c) {
1794
+ return { cost_usd: c.cost_usd, ...pricedField(c.priced) };
1795
+ }
1777
1796
  async function runRow(p) {
1778
1797
  const t0 = p.now();
1779
1798
  const prompt5 = resolvePrompt(p.template, p.row.vars);
@@ -1781,7 +1800,8 @@ async function runRow(p) {
1781
1800
  const maxTokens = p.providerConfig.max_tokens ?? DEFAULT_MAX_TOKENS2;
1782
1801
  if (p.dryRun) {
1783
1802
  const est = estimateUsage(prompt5, maxTokens);
1784
- const usage4 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
1803
+ const cost = p.provider.estimateCost(est);
1804
+ const usage4 = { ...est, ...costFields(cost) };
1785
1805
  return { vars: p.row.vars, output: null, cached: false, usage: usage4, assertions: [] };
1786
1806
  }
1787
1807
  const key = cacheKey(
@@ -1819,7 +1839,9 @@ async function runRow(p) {
1819
1839
  usage3 = {
1820
1840
  input_tokens: hit.usage.input_tokens,
1821
1841
  output_tokens: hit.usage.output_tokens,
1822
- cost_usd: 0
1842
+ cost_usd: 0,
1843
+ // A hit costs nothing, but whether that 0 is a known price depends on the model.
1844
+ ...pricedField(p.provider.estimateCost(hit.usage).priced)
1823
1845
  };
1824
1846
  cacheSaved = {
1825
1847
  input_tokens: hit.usage.input_tokens,
@@ -1836,12 +1858,13 @@ async function runRow(p) {
1836
1858
  prompt_resolved: prompt5
1837
1859
  });
1838
1860
  output = res.output;
1861
+ const cost = p.provider.estimateCost(res.usage, res.cost_usd);
1839
1862
  usage3 = {
1840
1863
  input_tokens: res.usage.input_tokens,
1841
1864
  output_tokens: res.usage.output_tokens,
1842
1865
  // Prefer the provider-reported actual cost (res.cost_usd) when present; resolveCost still
1843
1866
  // lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
1844
- cost_usd: p.provider.estimateCost(res.usage, res.cost_usd).cost_usd
1867
+ ...costFields(cost)
1845
1868
  };
1846
1869
  if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
1847
1870
  p.cache.set(key, res);
@@ -1858,8 +1881,11 @@ async function runRow(p) {
1858
1881
  }
1859
1882
  }
1860
1883
  let judgeCost = 0;
1884
+ let judgeUnpriced = false;
1861
1885
  const judge = buildJudge(p.provider, p.providerConfig, (u) => {
1862
- judgeCost += p.provider.estimateCost(u).cost_usd;
1886
+ const c = p.provider.estimateCost(u);
1887
+ judgeCost += c.cost_usd;
1888
+ if (c.priced === false) judgeUnpriced = true;
1863
1889
  });
1864
1890
  const records = [];
1865
1891
  let assertionError;
@@ -1884,7 +1910,12 @@ async function runRow(p) {
1884
1910
  break;
1885
1911
  }
1886
1912
  }
1887
- const finalUsage = { ...usage3, cost_usd: usage3.cost_usd + judgeCost };
1913
+ const finalUsage = {
1914
+ ...usage3,
1915
+ cost_usd: usage3.cost_usd + judgeCost,
1916
+ // A row is priced only when every call in it was; the field stays absent if the provider never said.
1917
+ ...judgeUnpriced ? { priced: false } : {}
1918
+ };
1888
1919
  return {
1889
1920
  vars: p.row.vars,
1890
1921
  output,
@@ -2565,7 +2596,7 @@ function createClient(opts) {
2565
2596
  }
2566
2597
  const base = opts.apiUrl.replace(/\/+$/, "");
2567
2598
  const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
2568
- async function send(method, path19, payload, retries = DEFAULT_RETRIES) {
2599
+ async function send(method, path21, payload, retries = DEFAULT_RETRIES) {
2569
2600
  let last = {
2570
2601
  kind: "unavailable",
2571
2602
  status: null,
@@ -2576,7 +2607,7 @@ function createClient(opts) {
2576
2607
  await respectPause(mine);
2577
2608
  let res;
2578
2609
  try {
2579
- res = await doFetch(`${base}${path19}`, {
2610
+ res = await doFetch(`${base}${path21}`, {
2580
2611
  method,
2581
2612
  headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
2582
2613
  // A DELETE carries none. Sending `"undefined"` as a body is what a naive
@@ -2615,10 +2646,10 @@ function createClient(opts) {
2615
2646
  return { ok: false, ...last };
2616
2647
  }
2617
2648
  return {
2618
- post: (path19, body, retries) => send("POST", path19, { body: JSON.stringify(body), type: "application/json" }, retries),
2619
- upload: (path19, bytes, contentType) => send("POST", path19, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
2620
- del: (path19) => send("DELETE", path19),
2621
- get: (path19) => send("GET", path19),
2649
+ post: (path21, body, retries) => send("POST", path21, { body: JSON.stringify(body), type: "application/json" }, retries),
2650
+ upload: (path21, bytes, contentType) => send("POST", path21, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
2651
+ del: (path21) => send("DELETE", path21),
2652
+ get: (path21) => send("GET", path21),
2622
2653
  async put(url, headers, source) {
2623
2654
  const putRetries = uploadRetries ?? 3;
2624
2655
  let last = { ok: false, status: null, code: "network" };
@@ -2899,12 +2930,12 @@ function machineless(raw, repoRoot, home) {
2899
2930
  if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
2900
2931
  return text2;
2901
2932
  }
2902
- function pathIn(path19) {
2903
- const parts = path19.split(/[\\/]+/);
2933
+ function pathIn(path21) {
2934
+ const parts = path21.split(/[\\/]+/);
2904
2935
  while (parts.length > 1 && parts.at(-1) === "") parts.pop();
2905
2936
  if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
2906
2937
  const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
2907
- const escaped = /^\/(?!\/)/.test(path19) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
2938
+ const escaped = /^\/(?!\/)/.test(path21) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
2908
2939
  const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
2909
2940
  return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
2910
2941
  }
@@ -2950,12 +2981,12 @@ function placeOf(error, testFile) {
2950
2981
  }
2951
2982
  function fileOf(raw) {
2952
2983
  if (!raw.startsWith("file://")) return raw;
2953
- let path19 = raw.slice("file://".length);
2984
+ let path21 = raw.slice("file://".length);
2954
2985
  try {
2955
- path19 = decodeURIComponent(path19);
2986
+ path21 = decodeURIComponent(path21);
2956
2987
  } catch {
2957
2988
  }
2958
- return /^\/[A-Za-z]:\//.test(path19) ? path19.slice(1) : path19;
2989
+ return /^\/[A-Za-z]:\//.test(path21) ? path21.slice(1) : path21;
2959
2990
  }
2960
2991
  function fromRoot(place, root) {
2961
2992
  if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
@@ -2995,14 +3026,14 @@ var init_failure_detail = __esm({
2995
3026
  FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
2996
3027
  FRAME_MAX = 8192;
2997
3028
  firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
2998
- normal = (path19) => import_node_path2.posix.normalize(path19.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
3029
+ normal = (path21) => import_node_path2.posix.normalize(path21.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
2999
3030
  }
3000
3031
  });
3001
3032
 
3002
3033
  // src/reporter-core/files-batch.ts
3003
- async function sha256File(path19) {
3034
+ async function sha256File(path21) {
3004
3035
  const hash = (0, import_node_crypto2.createHash)("sha256");
3005
- for await (const part of (0, import_node_fs4.createReadStream)(path19)) hash.update(part);
3036
+ for await (const part of (0, import_node_fs4.createReadStream)(path21)) hash.update(part);
3006
3037
  return hash.digest("hex");
3007
3038
  }
3008
3039
  var import_node_crypto2, import_node_fs4, FILES_BATCH_MAX, DIRECT_BATCH_BYTES, BATCH_PIPELINES, sha256Bytes, BUCKET_CODES;
@@ -4302,7 +4333,9 @@ var init_run_result_schema = __esm({
4302
4333
  usageSchema = import_zod3.z.object({
4303
4334
  input_tokens: import_zod3.z.number(),
4304
4335
  output_tokens: import_zod3.z.number(),
4305
- cost_usd: import_zod3.z.number()
4336
+ cost_usd: import_zod3.z.number(),
4337
+ /** cli#151. Optional and additive — a RunResult written before it validates unchanged. */
4338
+ priced: import_zod3.z.boolean().optional()
4306
4339
  });
4307
4340
  runErrorSchema = import_zod3.z.object({
4308
4341
  code: import_zod3.z.string(),
@@ -4633,10 +4666,10 @@ function platformGet(deps) {
4633
4666
  const token = (deps.loadToken ?? loadToken)();
4634
4667
  if (token === null) throw new NotLoggedInError();
4635
4668
  const doFetch = deps.fetchImpl ?? fetch;
4636
- return async (path19) => {
4669
+ return async (path21) => {
4637
4670
  let res;
4638
4671
  try {
4639
- res = await doFetch(`${apiUrl}${path19}`, { headers: { authorization: `Bearer ${token}` } });
4672
+ res = await doFetch(`${apiUrl}${path21}`, { headers: { authorization: `Bearer ${token}` } });
4640
4673
  } catch {
4641
4674
  throw new SyncNetworkError(apiUrl);
4642
4675
  }
@@ -4976,10 +5009,24 @@ var init_attachments = __esm({
4976
5009
  }
4977
5010
  });
4978
5011
 
5012
+ // src/importers/read-file.ts
5013
+ function readReportFile(file) {
5014
+ const text2 = fs11.readFileSync(file, "utf8");
5015
+ return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
5016
+ }
5017
+ var fs11;
5018
+ var init_read_file = __esm({
5019
+ "src/importers/read-file.ts"() {
5020
+ "use strict";
5021
+ init_cjs_shims();
5022
+ fs11 = __toESM(require("fs"), 1);
5023
+ }
5024
+ });
5025
+
4979
5026
  // src/importers/allure.ts
4980
- function isFolder(path19) {
5027
+ function isFolder(path21) {
4981
5028
  try {
4982
- return fs11.statSync(path19).isDirectory();
5029
+ return fs12.statSync(path21).isDirectory();
4983
5030
  } catch {
4984
5031
  return false;
4985
5032
  }
@@ -4987,7 +5034,7 @@ function isFolder(path19) {
4987
5034
  function load(file, maxBytes) {
4988
5035
  let size;
4989
5036
  try {
4990
- size = fs11.statSync(file).size;
5037
+ size = fs12.statSync(file).size;
4991
5038
  } catch (err) {
4992
5039
  throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
4993
5040
  }
@@ -4996,7 +5043,7 @@ function load(file, maxBytes) {
4996
5043
  }
4997
5044
  let parsed;
4998
5045
  try {
4999
- parsed = JSON.parse(fs11.readFileSync(file, "utf8"));
5046
+ parsed = JSON.parse(readReportFile(file));
5000
5047
  } catch (err) {
5001
5048
  throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
5002
5049
  }
@@ -5182,16 +5229,17 @@ function pendingOf(r, retry, ctx) {
5182
5229
  ...files.length > 0 ? { files } : {}
5183
5230
  };
5184
5231
  }
5185
- var fs11, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
5232
+ var fs12, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
5186
5233
  var init_allure = __esm({
5187
5234
  "src/importers/allure.ts"() {
5188
5235
  "use strict";
5189
5236
  init_cjs_shims();
5190
- fs11 = __toESM(require("fs"), 1);
5237
+ fs12 = __toESM(require("fs"), 1);
5191
5238
  path12 = __toESM(require("path"), 1);
5192
5239
  init_failure_detail();
5193
5240
  init_result_key();
5194
5241
  init_attachments();
5242
+ init_read_file();
5195
5243
  SOURCE = "allure";
5196
5244
  TOKEN = /@P([A-Za-z0-9_-]+)/;
5197
5245
  AllureReportError = class extends Error {
@@ -5205,7 +5253,7 @@ var init_allure = __esm({
5205
5253
  MAX_JSON_BYTES = 64 * 1024 * 1024;
5206
5254
  MAX_DEPTH = 256;
5207
5255
  MAX_TIME = 864e13;
5208
- namesIn = (dir, pattern) => fs11.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
5256
+ namesIn = (dir, pattern) => fs12.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
5209
5257
  timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
5210
5258
  textOf = (v) => typeof v === "string" ? v : "";
5211
5259
  labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
@@ -5492,10 +5540,10 @@ function attemptOf(result, file, ctx) {
5492
5540
  ...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
5493
5541
  })),
5494
5542
  steps: steps(result.steps),
5495
- attachments: (result.attachments ?? []).map(({ name, contentType, path: path19, body }) => ({
5543
+ attachments: (result.attachments ?? []).map(({ name, contentType, path: path21, body }) => ({
5496
5544
  name: name ?? "",
5497
5545
  ...contentType !== void 0 ? { contentType } : {},
5498
- ...path19 !== void 0 ? { path: path19 } : {},
5546
+ ...path21 !== void 0 ? { path: path21 } : {},
5499
5547
  // Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
5500
5548
  // bodies is megabytes of base64 that nothing here will read.
5501
5549
  ...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
@@ -5612,6 +5660,193 @@ var init_playwright_json = __esm({
5612
5660
  }
5613
5661
  });
5614
5662
 
5663
+ // src/importers/k6.ts
5664
+ function isObject(value2) {
5665
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5666
+ }
5667
+ function isK6Summary(parsed) {
5668
+ if (!isObject(parsed) || !isObject(parsed["metrics"])) return false;
5669
+ if (isObject(parsed["root_group"])) return true;
5670
+ return Object.values(parsed["metrics"]).some((m) => isObject(m) && (typeof m["type"] === "string" || isObject(m["thresholds"])));
5671
+ }
5672
+ function passedOf(verdict) {
5673
+ if (isObject(verdict) && typeof verdict["ok"] === "boolean") return verdict["ok"];
5674
+ if (typeof verdict === "boolean") return !verdict;
5675
+ return void 0;
5676
+ }
5677
+ function measuredOf(metric) {
5678
+ const values = isObject(metric.values) ? metric.values : metric;
5679
+ const unit = metric.contains === "time" ? " ms" : "";
5680
+ return Object.entries(values).filter(([name, v]) => name !== "thresholds" && typeof v === "number").map(([name, v]) => `${name}=${Math.round(v * 100) / 100}${unit}`).join(", ");
5681
+ }
5682
+ function readK6(source, file) {
5683
+ let parsed;
5684
+ try {
5685
+ parsed = JSON.parse(source);
5686
+ } catch (err) {
5687
+ throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
5688
+ }
5689
+ if (!isObject(parsed) || !isObject(parsed["metrics"])) {
5690
+ throw new JsonReportError(file, 'no "metrics" object \u2014 is this a k6 summary (k6 run --summary-export, or the data handleSummary receives)?');
5691
+ }
5692
+ const specRef = path13.basename(file);
5693
+ const out = [];
5694
+ for (const [name, raw] of Object.entries(parsed["metrics"])) {
5695
+ if (!isObject(raw) || !isObject(raw["thresholds"])) continue;
5696
+ const metric = raw;
5697
+ const measured = measuredOf(metric);
5698
+ for (const [expression, verdict] of Object.entries(metric.thresholds)) {
5699
+ const passed = passedOf(verdict);
5700
+ const status = passed === true ? "passed" : "failed";
5701
+ const said = passed === void 0 ? "no verdict in the summary" : status;
5702
+ const value2 = `${SOURCE4}#${specRef}#${name}#${expression}`;
5703
+ out.push({
5704
+ resultKey: resultKey(value2, 0),
5705
+ keys: [{ kind: "path-title", value: value2 }],
5706
+ source: SOURCE4,
5707
+ rawStatus: status,
5708
+ title: `${name} ${expression}`,
5709
+ specRef,
5710
+ errorContext: `k6 threshold "${expression}" on ${name}: ${said}.${measured !== "" ? ` Measured: ${measured}.` : ""}`
5711
+ });
5712
+ }
5713
+ }
5714
+ if (out.length === 0) {
5715
+ throw new JsonReportError(
5716
+ file,
5717
+ "k6 defined no thresholds, so there is nothing to pass or fail. Add thresholds to the script's options (for example http_req_duration: ['p(95)<500']) and run it again."
5718
+ );
5719
+ }
5720
+ return out;
5721
+ }
5722
+ var path13, SOURCE4;
5723
+ var init_k6 = __esm({
5724
+ "src/importers/k6.ts"() {
5725
+ "use strict";
5726
+ init_cjs_shims();
5727
+ path13 = __toESM(require("path"), 1);
5728
+ init_failure_detail();
5729
+ init_result_key();
5730
+ init_playwright_json();
5731
+ SOURCE4 = "k6";
5732
+ }
5733
+ });
5734
+
5735
+ // src/importers/lighthouse.ts
5736
+ function isObject2(value2) {
5737
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5738
+ }
5739
+ function isLighthouseReport(parsed) {
5740
+ return isObject2(parsed) && typeof parsed["lighthouseVersion"] === "string" && isObject2(parsed["categories"]);
5741
+ }
5742
+ function idOf(key, category) {
5743
+ return typeof category.id === "string" && category.id !== "" ? category.id : key;
5744
+ }
5745
+ function executionOf2(report) {
5746
+ const started = typeof report["fetchTime"] === "string" ? new Date(report["fetchTime"]) : void 0;
5747
+ if (started === void 0 || Number.isNaN(started.getTime())) return void 0;
5748
+ const timing = report["timing"];
5749
+ const total = isObject2(timing) && typeof timing["total"] === "number" && timing["total"] >= 0 ? Math.round(timing["total"]) : 0;
5750
+ return {
5751
+ startedAt: started.toISOString(),
5752
+ finishedAt: new Date(started.getTime() + total).toISOString(),
5753
+ durationMs: total,
5754
+ retry: 0
5755
+ };
5756
+ }
5757
+ function runtimeErrorOf(report) {
5758
+ const err = report["runtimeError"];
5759
+ if (!isObject2(err)) return void 0;
5760
+ const code = typeof err["code"] === "string" ? err["code"] : void 0;
5761
+ if (code === "NO_ERROR") return void 0;
5762
+ const message = typeof err["message"] === "string" ? err["message"] : void 0;
5763
+ if (code === void 0 && message === void 0) return void 0;
5764
+ return `Lighthouse stopped with ${code ?? "a runtime error"}${message !== void 0 ? `: ${message}` : ""}`;
5765
+ }
5766
+ function readLighthouse(source, file, minScores = {}) {
5767
+ let parsed;
5768
+ try {
5769
+ parsed = JSON.parse(source);
5770
+ } catch (err) {
5771
+ throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
5772
+ }
5773
+ if (!isLighthouseReport(parsed)) {
5774
+ throw new JsonReportError(file, 'no "lighthouseVersion" and "categories" \u2014 is this a Lighthouse JSON report (lighthouse <url> --output=json)?');
5775
+ }
5776
+ const report = parsed;
5777
+ const categories = report["categories"];
5778
+ const url = ["requestedUrl", "finalDisplayedUrl", "finalUrl"].map((k) => report[k]).find((u) => typeof u === "string" && u !== "");
5779
+ const settings = report["configSettings"];
5780
+ const formFactor = isObject2(settings) && typeof settings["formFactor"] === "string" && settings["formFactor"] !== "" ? settings["formFactor"] : void 0;
5781
+ const runtimeError = runtimeErrorOf(report);
5782
+ const execution = executionOf2(report);
5783
+ const specRef = path14.basename(file);
5784
+ const known = Object.entries(categories).filter(([, raw]) => isObject2(raw)).map(([key, raw]) => idOf(key, raw));
5785
+ if (known.length > 0) {
5786
+ for (const [id, score] of Object.entries(minScores)) {
5787
+ if (!known.includes(id)) {
5788
+ throw new JsonReportError(
5789
+ file,
5790
+ `--min-score ${printable(id)}=${score} names a category the report does not have; it has ${known.map(printable).join(", ")}.`
5791
+ );
5792
+ }
5793
+ }
5794
+ }
5795
+ const out = [];
5796
+ const result = (id, passed, message) => {
5797
+ const value2 = `${SOURCE5}#${url ?? ""}#${formFactor !== void 0 ? `${formFactor}#` : ""}${id}`;
5798
+ const where = [url, formFactor].filter((part) => part !== void 0);
5799
+ out.push({
5800
+ resultKey: resultKey(value2, 0),
5801
+ keys: [{ kind: "path-title", value: value2 }],
5802
+ source: SOURCE5,
5803
+ rawStatus: passed ? "passed" : "failed",
5804
+ title: where.length > 0 ? `${id} (${where.join(", ")})` : id,
5805
+ specRef,
5806
+ ...execution !== void 0 ? { execution } : {},
5807
+ errorContext: message
5808
+ });
5809
+ };
5810
+ for (const [key, raw] of Object.entries(categories)) {
5811
+ if (!isObject2(raw)) continue;
5812
+ const category = raw;
5813
+ const id = idOf(key, category);
5814
+ const name = typeof category.title === "string" && category.title !== "" ? category.title : id;
5815
+ const bar = (Object.hasOwn(minScores, id) ? minScores[id] : void 0) ?? DEFAULT_MIN_SCORE;
5816
+ let passed = false;
5817
+ let message;
5818
+ if (runtimeError !== void 0) {
5819
+ message = runtimeError;
5820
+ } else if (typeof category.score !== "number") {
5821
+ message = `Lighthouse could not compute a ${name} score, so it cannot pass.`;
5822
+ } else {
5823
+ const shown = Math.round(category.score * 100);
5824
+ passed = shown >= bar;
5825
+ message = `Lighthouse ${name} score: ${shown}, ${passed ? "at least" : "below"} the ${bar} needed to pass.`;
5826
+ }
5827
+ result(id, passed, message);
5828
+ }
5829
+ if (out.length === 0 && runtimeError !== void 0) result("run", false, runtimeError);
5830
+ if (out.length === 0) {
5831
+ throw new JsonReportError(file, "the report has no categories, so there is nothing to pass or fail.");
5832
+ }
5833
+ return out;
5834
+ }
5835
+ var path14, SOURCE5, DEFAULT_MIN_SCORE;
5836
+ var init_lighthouse = __esm({
5837
+ "src/importers/lighthouse.ts"() {
5838
+ "use strict";
5839
+ init_cjs_shims();
5840
+ path14 = __toESM(require("path"), 1);
5841
+ init_failure_detail();
5842
+ init_result_key();
5843
+ init_playwright_json();
5844
+ init_min_score();
5845
+ SOURCE5 = "lighthouse";
5846
+ DEFAULT_MIN_SCORE = 90;
5847
+ }
5848
+ });
5849
+
5615
5850
  // src/importers/index.ts
5616
5851
  var importers_exports = {};
5617
5852
  __export(importers_exports, {
@@ -5623,19 +5858,40 @@ __export(importers_exports, {
5623
5858
  detectFormat: () => detectFormat,
5624
5859
  isFolder: () => isFolder,
5625
5860
  readFolder: () => readFolder,
5626
- readReport: () => readReport
5861
+ readReport: () => readReport,
5862
+ readReportFile: () => readReportFile
5627
5863
  });
5628
5864
  function detectFormat(source, file) {
5629
5865
  if (looksLikeJUnit(source)) return "junit";
5866
+ const perf = perfFormatOf(source);
5867
+ if (perf !== void 0) return perf;
5630
5868
  if (looksLikePlaywrightJson(source)) return "playwright-json";
5631
5869
  throw new UnknownFormatError(
5632
5870
  file,
5633
- 'no <testsuite> and no Playwright "suites" \u2014 this is not a report we can read.'
5871
+ 'no <testsuite>, no Playwright "suites", no k6 "metrics" and no Lighthouse "lighthouseVersion" \u2014 this is not a report we can read.'
5634
5872
  );
5635
5873
  }
5636
- function readFolder(dir, format) {
5637
- if (format === "playwright-json") {
5638
- throw new UnknownFormatError(dir, "this is a folder, and only allure-results or JUnit XML files are read from one, not playwright-json.");
5874
+ function perfFormatOf(source) {
5875
+ if (!/^\s*\{/.test(source) || !(source.includes('"lighthouseVersion"') || source.includes('"metrics"'))) return void 0;
5876
+ let parsed;
5877
+ try {
5878
+ parsed = JSON.parse(source);
5879
+ } catch {
5880
+ return void 0;
5881
+ }
5882
+ if (isLighthouseReport(parsed)) return "lighthouse";
5883
+ if (isK6Summary(parsed)) return "k6";
5884
+ return void 0;
5885
+ }
5886
+ function readFolder(dir, format, options = {}) {
5887
+ if (format === "playwright-json" || format === "k6" || format === "lighthouse") {
5888
+ throw new UnknownFormatError(dir, `this is a folder, and only allure-results or JUnit XML files are read from one, not ${format}.`);
5889
+ }
5890
+ if (options.minScores !== void 0 && Object.keys(options.minScores).length > 0) {
5891
+ throw new JsonReportError(
5892
+ dir,
5893
+ "--min-score is for Lighthouse reports, and this is a folder: there is no score here to hold to it. Drop --min-score for this folder."
5894
+ );
5639
5895
  }
5640
5896
  const missing = [];
5641
5897
  const notes = [];
@@ -5646,10 +5902,10 @@ function readFolder(dir, format) {
5646
5902
  const skipped = [];
5647
5903
  let reports = 0;
5648
5904
  for (const name of namesIn(dir, /\.xml$/)) {
5649
- const file = path13.join(dir, name);
5905
+ const file = path15.join(dir, name);
5650
5906
  let source;
5651
5907
  try {
5652
- source = fs12.readFileSync(file, "utf8");
5908
+ source = readReportFile(file);
5653
5909
  } catch {
5654
5910
  throw new RunCommandError(`Cannot read ${printable(file)}.`);
5655
5911
  }
@@ -5669,30 +5925,43 @@ function readFolder(dir, format) {
5669
5925
  }
5670
5926
  return { format: "junit", results, missing, notes };
5671
5927
  }
5672
- function readReport(source, file, format) {
5928
+ function readReport(source, file, format, options = {}) {
5673
5929
  const chosen2 = format ?? detectFormat(source, file);
5674
5930
  const missing = [];
5675
5931
  if (chosen2 === "allure") throw new UnknownFormatError(file, "allure-results is a folder, and this is a file.");
5676
5932
  const notes = [];
5933
+ if (chosen2 !== "lighthouse" && options.minScores !== void 0 && Object.keys(options.minScores).length > 0) {
5934
+ throw new JsonReportError(
5935
+ file,
5936
+ `--min-score is for Lighthouse reports, and this is a ${chosen2} report: there is no score here to hold to it. Drop --min-score for this file.`
5937
+ );
5938
+ }
5677
5939
  if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
5940
+ if (chosen2 === "k6") return { format: chosen2, results: readK6(source, file), missing, notes };
5941
+ if (chosen2 === "lighthouse") {
5942
+ return { format: chosen2, results: readLighthouse(source, file, options.minScores), missing, notes };
5943
+ }
5678
5944
  return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
5679
5945
  }
5680
- var fs12, path13, IMPORT_FORMATS, UnknownFormatError;
5946
+ var path15, IMPORT_FORMATS, UnknownFormatError;
5681
5947
  var init_importers = __esm({
5682
5948
  "src/importers/index.ts"() {
5683
5949
  "use strict";
5684
5950
  init_cjs_shims();
5685
- fs12 = __toESM(require("fs"), 1);
5686
- path13 = __toESM(require("path"), 1);
5951
+ path15 = __toESM(require("path"), 1);
5687
5952
  init_run_lifecycle();
5688
5953
  init_failure_detail();
5689
5954
  init_allure();
5690
5955
  init_junit();
5691
5956
  init_playwright_json();
5957
+ init_k6();
5958
+ init_lighthouse();
5959
+ init_read_file();
5692
5960
  init_xml();
5693
5961
  init_playwright_json();
5694
5962
  init_allure();
5695
- IMPORT_FORMATS = ["junit", "playwright-json", "allure"];
5963
+ init_read_file();
5964
+ IMPORT_FORMATS = ["junit", "playwright-json", "allure", "k6", "lighthouse"];
5696
5965
  UnknownFormatError = class extends Error {
5697
5966
  constructor(file, detail) {
5698
5967
  super(`${file} \u2014 ${detail} Say which with --format ${IMPORT_FORMATS.join(" | ")}.`);
@@ -5728,15 +5997,19 @@ async function handleRunImport(options) {
5728
5997
  `));
5729
5998
  let read;
5730
5999
  if (isFolder(options.file)) {
5731
- read = readFolder(options.file, options.format);
6000
+ read = readFolder(options.file, options.format, {
6001
+ ...options.minScores !== void 0 ? { minScores: options.minScores } : {}
6002
+ });
5732
6003
  } else {
5733
6004
  let source;
5734
6005
  try {
5735
- source = fs13.readFileSync(options.file, "utf8");
6006
+ source = readReportFile(options.file);
5736
6007
  } catch {
5737
6008
  throw new RunCommandError(`Cannot read ${printable(options.file)}.`);
5738
6009
  }
5739
- read = readReport(source, options.file, options.format);
6010
+ read = readReport(source, options.file, options.format, {
6011
+ ...options.minScores !== void 0 ? { minScores: options.minScores } : {}
6012
+ });
5740
6013
  }
5741
6014
  const { format, results, missing, notes, ciUrl, ci } = read;
5742
6015
  if (results.length === 0) {
@@ -5885,12 +6158,11 @@ function describe(result, apiUrl) {
5885
6158
  if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
5886
6159
  return lines;
5887
6160
  }
5888
- var fs13, ImportTimeoutError, NEXT;
6161
+ var ImportTimeoutError, NEXT;
5889
6162
  var init_run_import = __esm({
5890
6163
  "src/cli/commands/run-import.ts"() {
5891
6164
  "use strict";
5892
6165
  init_cjs_shims();
5893
- fs13 = __toESM(require("fs"), 1);
5894
6166
  init_session();
5895
6167
  init_failure_detail();
5896
6168
  init_importers();
@@ -5922,10 +6194,10 @@ function isRunResult(v) {
5922
6194
  }
5923
6195
  function handleReport(opts = {}) {
5924
6196
  const cwd = opts.cwd ?? process.cwd();
5925
- const file = path14.join(cwd, ".plune", "last-run.json");
6197
+ const file = path16.join(cwd, ".plune", "last-run.json");
5926
6198
  let raw;
5927
6199
  try {
5928
- raw = fs14.readFileSync(file, "utf8");
6200
+ raw = fs13.readFileSync(file, "utf8");
5929
6201
  } catch {
5930
6202
  throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
5931
6203
  }
@@ -5940,13 +6212,13 @@ function handleReport(opts = {}) {
5940
6212
  }
5941
6213
  return parsed;
5942
6214
  }
5943
- var fs14, path14, ReportNotFoundError;
6215
+ var fs13, path16, ReportNotFoundError;
5944
6216
  var init_report = __esm({
5945
6217
  "src/cli/commands/report.ts"() {
5946
6218
  "use strict";
5947
6219
  init_cjs_shims();
5948
- fs14 = __toESM(require("fs"), 1);
5949
- path14 = __toESM(require("path"), 1);
6220
+ fs13 = __toESM(require("fs"), 1);
6221
+ path16 = __toESM(require("path"), 1);
5950
6222
  ReportNotFoundError = class extends Error {
5951
6223
  code = "NO_SAVED_RUN";
5952
6224
  constructor(message) {
@@ -6016,7 +6288,7 @@ function isRunResult2(v) {
6016
6288
  function loadRunResult(file, label) {
6017
6289
  let raw;
6018
6290
  try {
6019
- raw = fs15.readFileSync(file, "utf8");
6291
+ raw = fs14.readFileSync(file, "utf8");
6020
6292
  } catch {
6021
6293
  throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
6022
6294
  }
@@ -6036,12 +6308,12 @@ function handleDiff(opts) {
6036
6308
  const current = loadRunResult(opts.currentPath, "current");
6037
6309
  return diffRuns(baseline, current);
6038
6310
  }
6039
- var fs15, DiffInputError;
6311
+ var fs14, DiffInputError;
6040
6312
  var init_diff2 = __esm({
6041
6313
  "src/cli/commands/diff.ts"() {
6042
6314
  "use strict";
6043
6315
  init_cjs_shims();
6044
- fs15 = __toESM(require("fs"), 1);
6316
+ fs14 = __toESM(require("fs"), 1);
6045
6317
  init_diff();
6046
6318
  DiffInputError = class extends Error {
6047
6319
  code = "DIFF_INPUT";
@@ -6166,8 +6438,8 @@ async function runInitWizard(cwd) {
6166
6438
  process.exit(1);
6167
6439
  return;
6168
6440
  }
6169
- const outputPath = path15.join(cwd, "plune.yaml");
6170
- if (fs16.existsSync(outputPath)) {
6441
+ const outputPath = path17.join(cwd, "plune.yaml");
6442
+ if (fs15.existsSync(outputPath)) {
6171
6443
  const overwrite = await clack.confirm({
6172
6444
  message: "plune.yaml already exists. Overwrite?"
6173
6445
  });
@@ -6206,18 +6478,18 @@ async function runInitWizard(cwd) {
6206
6478
  `;
6207
6479
  const spin = clack.spinner();
6208
6480
  spin.start("Writing plune.yaml\u2026");
6209
- fs16.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
6481
+ fs15.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
6210
6482
  spin.stop("plune.yaml created!");
6211
6483
  clack.outro("plune.yaml is ready.");
6212
6484
  }
6213
- var clack, fs16, path15, import_yaml2;
6485
+ var clack, fs15, path17, import_yaml2;
6214
6486
  var init_wizard = __esm({
6215
6487
  "src/config/init/wizard.ts"() {
6216
6488
  "use strict";
6217
6489
  init_cjs_shims();
6218
6490
  clack = __toESM(require("@clack/prompts"), 1);
6219
- fs16 = __toESM(require("fs"), 1);
6220
- path15 = __toESM(require("path"), 1);
6491
+ fs15 = __toESM(require("fs"), 1);
6492
+ path17 = __toESM(require("path"), 1);
6221
6493
  import_yaml2 = require("yaml");
6222
6494
  init_errors();
6223
6495
  }
@@ -6225,21 +6497,21 @@ var init_wizard = __esm({
6225
6497
 
6226
6498
  // src/cli/templates/index.ts
6227
6499
  function writeTemplateFile(cwd, rel, content, force) {
6228
- const target = path16.join(cwd, rel);
6229
- if (!force && fs17.existsSync(target)) {
6500
+ const target = path18.join(cwd, rel);
6501
+ if (!force && fs16.existsSync(target)) {
6230
6502
  return "skipped";
6231
6503
  }
6232
- fs17.mkdirSync(path16.dirname(target), { recursive: true });
6233
- fs17.writeFileSync(target, content, "utf8");
6504
+ fs16.mkdirSync(path18.dirname(target), { recursive: true });
6505
+ fs16.writeFileSync(target, content, "utf8");
6234
6506
  return "written";
6235
6507
  }
6236
- var fs17, path16, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
6508
+ var fs16, path18, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
6237
6509
  var init_templates = __esm({
6238
6510
  "src/cli/templates/index.ts"() {
6239
6511
  "use strict";
6240
6512
  init_cjs_shims();
6241
- fs17 = __toESM(require("fs"), 1);
6242
- path16 = __toESM(require("path"), 1);
6513
+ fs16 = __toESM(require("fs"), 1);
6514
+ path18 = __toESM(require("path"), 1);
6243
6515
  ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
6244
6516
  # .env is gitignored; .env.example is safe to commit.
6245
6517
  # Plune reads whichever key matches your configured provider.
@@ -6419,14 +6691,14 @@ var init_logout = __esm({
6419
6691
 
6420
6692
  // src/cli/commands/cairn-artifact.ts
6421
6693
  async function findLatestCairnRun(runsDir = "runs") {
6422
- const entries = await fs18.readdir(runsDir, { withFileTypes: true }).catch(() => null);
6694
+ const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
6423
6695
  if (entries === null) throw new NoCairnRunsError(runsDir);
6424
6696
  let newest;
6425
6697
  for (const entry of entries) {
6426
6698
  if (!entry.isDirectory()) continue;
6427
- const dir = path17.join(runsDir, entry.name);
6699
+ const dir = path19.join(runsDir, entry.name);
6428
6700
  try {
6429
- const report = await fs18.lstat(path17.join(dir, "report.json"));
6701
+ const report = await fs17.lstat(path19.join(dir, "report.json"));
6430
6702
  if (!report.isFile()) continue;
6431
6703
  if (newest === void 0 || report.mtimeMs > newest.at) {
6432
6704
  newest = { dir, at: report.mtimeMs };
@@ -6439,13 +6711,13 @@ async function findLatestCairnRun(runsDir = "runs") {
6439
6711
  }
6440
6712
  async function readJson(file) {
6441
6713
  try {
6442
- return JSON.parse(await fs18.readFile(file, "utf8"));
6714
+ return JSON.parse(await fs17.readFile(file, "utf8"));
6443
6715
  } catch {
6444
6716
  return void 0;
6445
6717
  }
6446
6718
  }
6447
6719
  async function defaultReadEvidence(dir) {
6448
- const raw = await readJson(path17.join(dir, "api-evidence.json"));
6720
+ const raw = await readJson(path19.join(dir, "api-evidence.json"));
6449
6721
  if (!Array.isArray(raw)) return void 0;
6450
6722
  return raw.map((r) => {
6451
6723
  const rec = asRecord(r);
@@ -6488,7 +6760,7 @@ function casesFromApi(report, verdicts) {
6488
6760
  });
6489
6761
  }
6490
6762
  async function readCairnRun(dir, deps = {}) {
6491
- const raw = await readJson(path17.join(dir, "report.json"));
6763
+ const raw = await readJson(path19.join(dir, "report.json"));
6492
6764
  if (raw === void 0) throw new CairnRunNotFoundError(dir);
6493
6765
  const report = asRecord(raw);
6494
6766
  if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
@@ -6518,20 +6790,20 @@ async function readCairnRun(dir, deps = {}) {
6518
6790
  return {
6519
6791
  schemaVersion: INGEST_SCHEMA_VERSION,
6520
6792
  run: {
6521
- runId: str(report["runId"], path17.basename(dir)),
6793
+ runId: str(report["runId"], path19.basename(dir)),
6522
6794
  mode,
6523
6795
  target: str(report["url"], "unknown")
6524
6796
  },
6525
6797
  cases
6526
6798
  };
6527
6799
  }
6528
- var fs18, path17, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
6800
+ var fs17, path19, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
6529
6801
  var init_cairn_artifact = __esm({
6530
6802
  "src/cli/commands/cairn-artifact.ts"() {
6531
6803
  "use strict";
6532
6804
  init_cjs_shims();
6533
- fs18 = __toESM(require("fs/promises"), 1);
6534
- path17 = __toESM(require("path"), 1);
6805
+ fs17 = __toESM(require("fs/promises"), 1);
6806
+ path19 = __toESM(require("path"), 1);
6535
6807
  SUPPORTED_ARTIFACT_VERSION = 1;
6536
6808
  INGEST_SCHEMA_VERSION = 1;
6537
6809
  NoCairnRunsError = class extends Error {
@@ -6603,7 +6875,7 @@ async function handleIngest(deps) {
6603
6875
  const doFetch = deps.fetchImpl ?? fetch;
6604
6876
  const endpoint = `${apiUrl}/v1/ingest/cairn`;
6605
6877
  const job = new JobReporter(doFetch, apiUrl, token, "ingest");
6606
- await job.open(path18.basename(path18.resolve(dir)), payload.cases.length);
6878
+ await job.open(path20.basename(path20.resolve(dir)), payload.cases.length);
6607
6879
  try {
6608
6880
  const result = await upload();
6609
6881
  await job.uploaded({ subjectId: result.runId || void 0 });
@@ -6662,12 +6934,12 @@ function formatIngestResult(r) {
6662
6934
  return `${lines.join("\n")}
6663
6935
  `;
6664
6936
  }
6665
- var path18, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
6937
+ var path20, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
6666
6938
  var init_ingest = __esm({
6667
6939
  "src/cli/commands/ingest.ts"() {
6668
6940
  "use strict";
6669
6941
  init_cjs_shims();
6670
- path18 = __toESM(require("path"), 1);
6942
+ path20 = __toESM(require("path"), 1);
6671
6943
  init_api_url();
6672
6944
  init_credentials();
6673
6945
  init_cairn_artifact();
@@ -7346,6 +7618,7 @@ function parseDuration(text2) {
7346
7618
  }
7347
7619
 
7348
7620
  // src/cli.ts
7621
+ init_min_score();
7349
7622
  var __dirname = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
7350
7623
  var pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf-8"));
7351
7624
  function toFormat(s) {
@@ -7617,10 +7890,20 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
7617
7890
  });
7618
7891
  runCommand.command("import").argument(
7619
7892
  "<file>",
7620
- "A JUnit XML or Playwright JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
7893
+ "A JUnit XML or Playwright JSON report, a k6 summary or Lighthouse JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
7621
7894
  ).description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option(
7622
7895
  "--format <fmt>",
7623
- "junit | playwright-json | allure; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
7896
+ "junit | playwright-json | allure | k6 | lighthouse; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
7897
+ ).option(
7898
+ "--min-score <category=score>",
7899
+ "Lighthouse: the score (0-100) a category needs to pass, e.g. performance=90; repeat it per category. A category not named needs 90",
7900
+ (v, previous) => {
7901
+ try {
7902
+ return parseMinScore(v, previous);
7903
+ } catch (err) {
7904
+ throw new import_commander.InvalidArgumentError(err.message);
7905
+ }
7906
+ }
7624
7907
  ).option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--create", "Offer tests nothing matched to the review queue", false).option(
7625
7908
  "--fail-on-missing-attachments",
7626
7909
  "Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
@@ -7664,6 +7947,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
7664
7947
  ...format !== void 0 ? { format } : {},
7665
7948
  ...options.key !== void 0 ? { key: options.key } : {},
7666
7949
  ...options.name !== void 0 ? { name: options.name } : {},
7950
+ ...options.minScore !== void 0 ? { minScores: options.minScore } : {},
7667
7951
  ...options.create ? { create: true } : {},
7668
7952
  ...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
7669
7953
  ...options.timeout !== void 0 ? { timeoutSec: options.timeout } : {},
@@ -7751,10 +8035,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
7751
8035
  const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
7752
8036
  try {
7753
8037
  const token = options.token ?? await readTokenFromStdin();
7754
- const { path: path19, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
8038
+ const { path: path21, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
7755
8039
  process.stdout.write(
7756
- verified ? `Logged in. Token checked against the API and saved to ${path19}
7757
- ` : `Token saved to ${path19} WITHOUT being checked (--skip-verify).
8040
+ verified ? `Logged in. Token checked against the API and saved to ${path21}
8041
+ ` : `Token saved to ${path21} WITHOUT being checked (--skip-verify).
7758
8042
  `
7759
8043
  );
7760
8044
  } catch (err) {
@@ -7771,9 +8055,9 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
7771
8055
  const verbose = command.optsWithGlobals().verbose === true;
7772
8056
  const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
7773
8057
  try {
7774
- const { removed, path: path19 } = handleLogout2();
8058
+ const { removed, path: path21 } = handleLogout2();
7775
8059
  process.stdout.write(
7776
- removed ? `Logged out. Removed ${path19}
8060
+ removed ? `Logged out. Removed ${path21}
7777
8061
  ` : "Not logged in \u2014 nothing to remove.\n"
7778
8062
  );
7779
8063
  } catch (err) {