@plune-ai/cli 0.20.0 → 0.22.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
@@ -2304,6 +2304,13 @@ var init_credentials = __esm({
2304
2304
  });
2305
2305
 
2306
2306
  // src/reporter-core/client.ts
2307
+ function parseRetryAfter(raw, now = Date.now()) {
2308
+ const text2 = raw?.trim();
2309
+ if (text2 === void 0 || text2 === "") return null;
2310
+ const seconds = /^\d+(\.\d+)?$/.test(text2) ? Number(text2) : (Date.parse(text2) - now) / 1e3;
2311
+ if (!Number.isFinite(seconds)) return null;
2312
+ return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
2313
+ }
2307
2314
  function nodeTransport(url, init, idleMs = IDLE_MS) {
2308
2315
  return new Promise((resolve9, reject) => {
2309
2316
  const target = new URL(url);
@@ -2371,12 +2378,6 @@ async function detailOf(res) {
2371
2378
  return { detail: "" };
2372
2379
  }
2373
2380
  }
2374
- function retryAfterMs2(res) {
2375
- const raw = res.headers.get("retry-after");
2376
- if (raw === null) return null;
2377
- const seconds = Number(raw.trim());
2378
- return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
2379
- }
2380
2381
  function classify(status) {
2381
2382
  if (status === 401 || status === 403) return "auth";
2382
2383
  if (status === 409) return "conflict";
@@ -2387,15 +2388,34 @@ function classify(status) {
2387
2388
  function createClient(opts) {
2388
2389
  const doFetch = opts.fetchImpl ?? nodeTransport;
2389
2390
  const wait = opts.wait ?? sleep;
2391
+ const uploadRetries = opts.uploadRetries === void 0 ? void 0 : Math.max(0, Math.floor(opts.uploadRetries));
2392
+ const random = opts.random ?? Math.random;
2393
+ const now = opts.now ?? Date.now;
2394
+ let pausedUntil = 0;
2395
+ const jittered = (ms) => Math.round(ms * (1 + random() * 0.5));
2396
+ async function backOff(attempt, said) {
2397
+ opts.onRetry?.();
2398
+ const ms = jittered(said ?? Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CAP_MS));
2399
+ const until = now() + ms;
2400
+ pausedUntil = Math.max(pausedUntil, until);
2401
+ await wait(ms);
2402
+ return until;
2403
+ }
2404
+ async function respectPause(mine) {
2405
+ const left = pausedUntil - now();
2406
+ if (pausedUntil > mine && left > 0) await wait(left + Math.round(random() * 250));
2407
+ }
2390
2408
  const base = opts.apiUrl.replace(/\/+$/, "");
2391
2409
  const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
2392
- async function send(method, path17, payload) {
2410
+ async function send(method, path17, payload, retries = DEFAULT_RETRIES) {
2393
2411
  let last = {
2394
2412
  kind: "unavailable",
2395
2413
  status: null,
2396
2414
  detail: "no attempt was made"
2397
2415
  };
2398
- for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
2416
+ let mine = 0;
2417
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
2418
+ await respectPause(mine);
2399
2419
  let res;
2400
2420
  try {
2401
2421
  res = await doFetch(`${base}${path17}`, {
@@ -2408,7 +2428,8 @@ function createClient(opts) {
2408
2428
  });
2409
2429
  } catch (err) {
2410
2430
  last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
2411
- if (attempt < RETRY_DELAYS_MS.length) await wait(RETRY_DELAYS_MS[attempt]);
2431
+ if (attempt >= Math.min(retries, DEFAULT_RETRIES)) break;
2432
+ mine = await backOff(attempt, null);
2412
2433
  continue;
2413
2434
  }
2414
2435
  if (res.ok) {
@@ -2423,25 +2444,28 @@ function createClient(opts) {
2423
2444
  const kind = classify(res.status);
2424
2445
  last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
2425
2446
  if (kind !== "unavailable") break;
2426
- if (res.status === 429 && retryAfterMs2(res) === null) {
2447
+ const said429 = parseRetryAfter(res.headers.get("retry-after"), now());
2448
+ if (res.status === 429 && said429 === null) {
2427
2449
  last.kind = "invalid";
2428
2450
  last.detail = "quota exceeded (no Retry-After)";
2429
2451
  if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
2430
2452
  break;
2431
2453
  }
2432
- if (attempt < RETRY_DELAYS_MS.length) {
2433
- await wait(retryAfterMs2(res) ?? RETRY_DELAYS_MS[attempt]);
2434
- }
2454
+ if (attempt >= (said429 === null ? Math.min(retries, DEFAULT_RETRIES) : retries)) break;
2455
+ mine = await backOff(attempt, said429);
2435
2456
  }
2436
2457
  return { ok: false, ...last };
2437
2458
  }
2438
2459
  return {
2439
- post: (path17, body) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }),
2440
- upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }),
2460
+ post: (path17, body, retries) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }, retries),
2461
+ upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
2441
2462
  del: (path17) => send("DELETE", path17),
2442
2463
  async put(url, headers, source) {
2464
+ const putRetries = uploadRetries ?? 3;
2443
2465
  let last = { ok: false, status: null, code: "network" };
2444
- for (let attempt = 0; attempt <= PUT_DELAYS_MS.length; attempt += 1) {
2466
+ let mine = 0;
2467
+ for (let attempt = 0; attempt <= putRetries; attempt += 1) {
2468
+ await respectPause(mine);
2445
2469
  try {
2446
2470
  let status;
2447
2471
  let retryAfter;
@@ -2456,21 +2480,21 @@ function createClient(opts) {
2456
2480
  if (status >= 200 && status <= 299) return { ok: true };
2457
2481
  last = { ok: false, status, code: `bucket_${status}` };
2458
2482
  if (status < 500 && status !== 429) return last;
2459
- const seconds = retryAfter === null ? NaN : Number(retryAfter.trim());
2460
- if (attempt < PUT_DELAYS_MS.length) {
2461
- await wait(Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : PUT_DELAYS_MS[attempt]);
2462
- }
2483
+ const said = parseRetryAfter(retryAfter, now());
2484
+ if (attempt >= (said === null ? Math.min(putRetries, 3) : putRetries)) return last;
2485
+ mine = await backOff(attempt, said);
2463
2486
  continue;
2464
2487
  } catch (err) {
2465
2488
  last = { ok: false, status: null, code: codeOfError(err) };
2466
2489
  }
2467
- if (attempt < PUT_DELAYS_MS.length) await wait(PUT_DELAYS_MS[attempt]);
2490
+ if (attempt >= Math.min(putRetries, 3)) return last;
2491
+ mine = await backOff(attempt, null);
2468
2492
  }
2469
2493
  return last;
2470
2494
  }
2471
2495
  };
2472
2496
  }
2473
- var import_node_fs2, import_node_stream, import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, PUT_DELAYS_MS, withoutLength, codeOfError, sleep;
2497
+ var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, IDLE_MS, withoutLength, codeOfError, sleep;
2474
2498
  var init_client = __esm({
2475
2499
  "src/reporter-core/client.ts"() {
2476
2500
  "use strict";
@@ -2479,9 +2503,11 @@ var init_client = __esm({
2479
2503
  import_node_stream = require("stream");
2480
2504
  import_node_http = require("http");
2481
2505
  import_node_https = require("https");
2482
- RETRY_DELAYS_MS = [300, 900];
2506
+ BACKOFF_BASE_MS = 300;
2507
+ BACKOFF_CAP_MS = 1e4;
2508
+ DEFAULT_RETRIES = 2;
2509
+ RETRY_AFTER_CAP_MS = 6e4;
2483
2510
  IDLE_MS = 3e5;
2484
- PUT_DELAYS_MS = [500, 1500, 4e3];
2485
2511
  withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
2486
2512
  codeOfError = (err) => {
2487
2513
  const code = err?.code;
@@ -2506,6 +2532,14 @@ function count(env, name) {
2506
2532
  const n = Number(raw);
2507
2533
  return Number.isInteger(n) && n > 0 ? n : void 0;
2508
2534
  }
2535
+ function bounded(env, name, range, warnings) {
2536
+ const raw = value(env, name);
2537
+ if (raw === void 0) return void 0;
2538
+ const n = Number(raw);
2539
+ if (/^\d+$/.test(raw) && n >= range.min && n <= range.max) return n;
2540
+ warnings.push(`${name}=${JSON.stringify(raw.slice(0, 40))} is not a whole number from ${range.min} to ${range.max} \u2014 using ${range.default}.`);
2541
+ return void 0;
2542
+ }
2509
2543
  function list(env, name) {
2510
2544
  const raw = value(env, name);
2511
2545
  if (raw === void 0) return void 0;
@@ -2518,6 +2552,9 @@ function readEnv(env = process.env) {
2518
2552
  const apiUrl = value(env, "PLUNE_API_URL");
2519
2553
  const fallbackPath = value(env, "PLUNE_FALLBACK");
2520
2554
  const batchSize = count(env, "PLUNE_BATCH_SIZE");
2555
+ const warnings = [];
2556
+ const uploadConcurrency = bounded(env, "PLUNE_UPLOAD_CONCURRENCY", UPLOAD_CONCURRENCY, warnings);
2557
+ const uploadRetries = bounded(env, "PLUNE_UPLOAD_RETRIES", UPLOAD_RETRIES, warnings);
2521
2558
  const title = value(env, "PLUNE_RUN_TITLE");
2522
2559
  const environment = value(env, "PLUNE_ENV");
2523
2560
  const labels = list(env, "PLUNE_LABELS");
@@ -2530,6 +2567,8 @@ function readEnv(env = process.env) {
2530
2567
  ...apiUrl !== void 0 ? { apiUrl } : {},
2531
2568
  ...fallbackPath !== void 0 ? { fallbackPath } : {},
2532
2569
  ...batchSize !== void 0 ? { batchSize } : {},
2570
+ ...uploadConcurrency !== void 0 ? { uploadConcurrency } : {},
2571
+ ...uploadRetries !== void 0 ? { uploadRetries } : {},
2533
2572
  ...title !== void 0 ? { title } : {},
2534
2573
  ...environment !== void 0 ? { environment } : {},
2535
2574
  ...labels !== void 0 ? { labels } : {},
@@ -2542,7 +2581,8 @@ function readEnv(env = process.env) {
2542
2581
  // reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
2543
2582
  // way this process must not be the one to call it finished.
2544
2583
  keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
2545
- ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
2584
+ ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
2585
+ warnings
2546
2586
  };
2547
2587
  }
2548
2588
  function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cwd = process.cwd()) {
@@ -2568,13 +2608,15 @@ function ciCommit(reported, env = process.env) {
2568
2608
  ...branch !== void 0 ? { branch } : {}
2569
2609
  };
2570
2610
  }
2571
- var path6, UNSUPPORTED_VARS, PROVIDERS;
2611
+ var path6, UNSUPPORTED_VARS, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
2572
2612
  var init_env2 = __esm({
2573
2613
  "src/reporter-core/env.ts"() {
2574
2614
  "use strict";
2575
2615
  init_cjs_shims();
2576
2616
  path6 = __toESM(require("path"), 1);
2577
2617
  UNSUPPORTED_VARS = ["PLUNE_GROUP"];
2618
+ UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 4 };
2619
+ UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
2578
2620
  PROVIDERS = [
2579
2621
  { is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
2580
2622
  { is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
@@ -2854,9 +2896,15 @@ async function startRun(passed, declared2) {
2854
2896
  const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
2855
2897
  const offerDiscovered = cfg.offerDiscovered ?? false;
2856
2898
  const token = cfg.token ?? loadToken() ?? "";
2899
+ const uploadsAtOnce = Math.min(Math.max(Math.floor(cfg.uploadConcurrency ?? UPLOAD_CONCURRENCY.default), UPLOAD_CONCURRENCY.min), UPLOAD_CONCURRENCY.max);
2900
+ const uploadRetries = Math.min(Math.max(Math.floor(cfg.uploadRetries ?? UPLOAD_RETRIES.default), UPLOAD_RETRIES.min), UPLOAD_RETRIES.max);
2901
+ let retried = 0;
2902
+ let uploadMs = 0;
2857
2903
  const client = createClient({
2858
2904
  apiUrl: resolveApiUrl(cfg.apiUrl),
2859
2905
  token,
2906
+ uploadRetries,
2907
+ onRetry: () => void (retried += 1),
2860
2908
  ...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
2861
2909
  });
2862
2910
  const stats = {
@@ -2907,6 +2955,7 @@ async function startRun(passed, declared2) {
2907
2955
  if (offline) {
2908
2956
  log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
2909
2957
  }
2958
+ for (const warning of env.warnings) log(`plune: ${warning}`);
2910
2959
  if (env.ignored.length > 0) {
2911
2960
  log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
2912
2961
  }
@@ -3036,7 +3085,7 @@ async function startRun(passed, declared2) {
3036
3085
  function ask(job, size) {
3037
3086
  return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
3038
3087
  files: [{ name: job.name, contentType: job.type, size }]
3039
- });
3088
+ }, uploadRetries);
3040
3089
  }
3041
3090
  function failed(job, why) {
3042
3091
  countsOf(job.kind).failed += 1;
@@ -3097,11 +3146,11 @@ async function startRun(passed, declared2) {
3097
3146
  if (fresh === void 0) return failedToBucket(job, "bad_answer");
3098
3147
  link = fresh;
3099
3148
  }
3100
- const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
3149
+ const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {}, uploadRetries);
3101
3150
  if (!done2.ok) return failedToBucket(job, codeOf2(done2));
3102
3151
  countsOf(job.kind).uploaded += 1;
3103
3152
  }
3104
- let free = UPLOADS_AT_ONCE;
3153
+ let free = uploadsAtOnce;
3105
3154
  const waiting = [];
3106
3155
  async function gated(work) {
3107
3156
  if (free === 0) await new Promise((resolve9) => waiting.push(resolve9));
@@ -3181,6 +3230,7 @@ async function startRun(passed, declared2) {
3181
3230
  }
3182
3231
  }
3183
3232
  }
3233
+ const started = Date.now();
3184
3234
  const byResult = /* @__PURE__ */ new Map();
3185
3235
  for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
3186
3236
  await Promise.all(
@@ -3193,6 +3243,7 @@ async function startRun(passed, declared2) {
3193
3243
  await Promise.all(work);
3194
3244
  })
3195
3245
  );
3246
+ uploadMs += Date.now() - started;
3196
3247
  }
3197
3248
  async function flush() {
3198
3249
  if (buffer.length === 0) return;
@@ -3285,6 +3336,13 @@ async function startRun(passed, declared2) {
3285
3336
  }
3286
3337
  if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
3287
3338
  if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
3339
+ const files = [stats.screenshots, stats.textFiles, stats.otherFiles];
3340
+ const total = (what) => files.reduce((n, counts) => n + counts[what], 0);
3341
+ if (total("uploaded") + total("skipped") + total("failed") > 0) {
3342
+ log(
3343
+ `plune: files \u2014 ${total("uploaded")} uploaded, ${total("skipped")} skipped, ${retried} retried, ${total("failed")} dropped, in ${(uploadMs / 1e3).toFixed(1)} s (${uploadsAtOnce} at once)`
3344
+ );
3345
+ }
3288
3346
  log(`plune: ${parts.join(" \xB7 ")}`);
3289
3347
  if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
3290
3348
  log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
@@ -3339,7 +3397,7 @@ async function startRun(passed, declared2) {
3339
3397
  }
3340
3398
  };
3341
3399
  }
3342
- var import_node_crypto2, import_node_fs4, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
3400
+ var import_node_crypto2, import_node_fs4, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
3343
3401
  var init_session = __esm({
3344
3402
  "src/reporter-core/session.ts"() {
3345
3403
  "use strict";
@@ -3357,7 +3415,7 @@ var init_session = __esm({
3357
3415
  RESOLVE_CHUNK = 500;
3358
3416
  BATCH_MAX = 500;
3359
3417
  DISCOVER_MAX = 500;
3360
- BATCH_DEFAULT = 100;
3418
+ BATCH_DEFAULT = 500;
3361
3419
  EXPECTED_MAX = 1e4;
3362
3420
  KEY_MAX = 1024;
3363
3421
  TITLE_MAX = 300;
@@ -3375,7 +3433,6 @@ var init_session = __esm({
3375
3433
  ".webp": "image/webp"
3376
3434
  };
3377
3435
  METADATA_TYPE2 = "application/plune.metadata+json";
3378
- UPLOADS_AT_ONCE = 4;
3379
3436
  NAME_MAX = 200;
3380
3437
  SKIPPED = {
3381
3438
  hidden: "its name starts with an underscore, which hides an attachment in Playwright's own reports too",
@@ -3644,160 +3701,631 @@ var init_run_lifecycle = __esm({
3644
3701
  }
3645
3702
  });
3646
3703
 
3647
- // src/reporter-core/result-key.ts
3648
- function resultKey(testId, retry) {
3649
- const plain = `${testId}#${retry}`;
3650
- return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
3704
+ // src/types/run-result-schema.ts
3705
+ function parseRunResult(value2) {
3706
+ return runResultSchema.parse(value2);
3651
3707
  }
3652
- var RESULT_KEY_MAX, HASH_CHARS;
3653
- var init_result_key = __esm({
3654
- "src/reporter-core/result-key.ts"() {
3708
+ var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
3709
+ var init_run_result_schema = __esm({
3710
+ "src/types/run-result-schema.ts"() {
3655
3711
  "use strict";
3656
3712
  init_cjs_shims();
3657
- init_hash();
3658
- RESULT_KEY_MAX = 200;
3659
- HASH_CHARS = 32;
3713
+ import_zod3 = require("zod");
3714
+ init_results();
3715
+ RUN_RESULT_SCHEMA_VERSION = 1;
3716
+ binaryVerdictSchema = import_zod3.z.object({
3717
+ question: import_zod3.z.string().min(1),
3718
+ passed: import_zod3.z.boolean()
3719
+ });
3720
+ assertionResultRecordSchema2 = import_zod3.z.object({
3721
+ type: import_zod3.z.string(),
3722
+ passed: import_zod3.z.boolean(),
3723
+ score: import_zod3.z.number().optional(),
3724
+ reason: import_zod3.z.string().optional(),
3725
+ binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
3726
+ });
3727
+ usageSchema = import_zod3.z.object({
3728
+ input_tokens: import_zod3.z.number(),
3729
+ output_tokens: import_zod3.z.number(),
3730
+ cost_usd: import_zod3.z.number()
3731
+ });
3732
+ runErrorSchema = import_zod3.z.object({
3733
+ code: import_zod3.z.string(),
3734
+ message: import_zod3.z.string()
3735
+ });
3736
+ rowResultSchema = import_zod3.z.object({
3737
+ vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
3738
+ output: import_zod3.z.string().nullable(),
3739
+ cached: import_zod3.z.boolean(),
3740
+ usage: usageSchema.optional(),
3741
+ latency_ms: import_zod3.z.number().optional(),
3742
+ error: runErrorSchema.optional(),
3743
+ assertions: import_zod3.z.array(assertionResultRecordSchema2),
3744
+ /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
3745
+ inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
3746
+ });
3747
+ summarySchema = import_zod3.z.object({
3748
+ total: import_zod3.z.number(),
3749
+ passed: import_zod3.z.number(),
3750
+ failed: import_zod3.z.number(),
3751
+ errored: import_zod3.z.number(),
3752
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
3753
+ blocked: import_zod3.z.number().optional(),
3754
+ skipped: import_zod3.z.number().optional(),
3755
+ cost_usd: import_zod3.z.number(),
3756
+ duration_ms: import_zod3.z.number()
3757
+ });
3758
+ evalResultSchema = import_zod3.z.object({
3759
+ id: import_zod3.z.string(),
3760
+ tags: import_zod3.z.array(import_zod3.z.string()),
3761
+ rows: import_zod3.z.array(rowResultSchema),
3762
+ passed: import_zod3.z.boolean()
3763
+ });
3764
+ runResultSchema = import_zod3.z.object({
3765
+ schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
3766
+ plune_version: import_zod3.z.string(),
3767
+ started_at: import_zod3.z.string(),
3768
+ finished_at: import_zod3.z.string(),
3769
+ config_hash: import_zod3.z.string(),
3770
+ summary: summarySchema,
3771
+ evals: import_zod3.z.array(evalResultSchema)
3772
+ });
3660
3773
  }
3661
3774
  });
3662
3775
 
3663
- // src/importers/attachments.ts
3664
- function reportFolder(report) {
3665
- const folder = path9.dirname(path9.resolve(report));
3776
+ // src/cli/commands/sync.ts
3777
+ var sync_exports = {};
3778
+ __export(sync_exports, {
3779
+ NotLoggedInError: () => NotLoggedInError,
3780
+ SyncFileError: () => SyncFileError,
3781
+ SyncHttpError: () => SyncHttpError,
3782
+ SyncNetworkError: () => SyncNetworkError,
3783
+ TokenRejectedError: () => TokenRejectedError,
3784
+ handleSync: () => handleSync,
3785
+ reportSyncFailure: () => reportSyncFailure
3786
+ });
3787
+ function readRun(cwd, file) {
3788
+ const target = file !== void 0 ? path9.resolve(cwd, file) : path9.join(cwd, ".plune", "last-run.json");
3789
+ let raw;
3666
3790
  try {
3667
- return fs8.realpathSync(folder);
3791
+ raw = fs8.readFileSync(target, "utf8");
3668
3792
  } catch {
3669
- return folder;
3793
+ throw new SyncFileError(
3794
+ file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
3795
+ );
3670
3796
  }
3671
- }
3672
- function reportFolders(report) {
3673
- return { lexical: path9.dirname(path9.resolve(report)), real: reportFolder(report) };
3674
- }
3675
- function within(folder, target) {
3676
- const rel = path9.relative(folder, target);
3677
- return rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
3678
- }
3679
- function resolveAttachment(folder, ref, lexical = folder) {
3680
- if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
3681
- const face = path9.resolve(lexical, ref);
3682
- if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
3683
- let real;
3797
+ let json;
3684
3798
  try {
3685
- real = fs8.realpathSync(path9.resolve(folder, ref));
3799
+ json = JSON.parse(raw);
3686
3800
  } catch {
3687
- return { why: "absent" };
3801
+ throw new SyncFileError(`Run file is not valid JSON: ${target}`);
3688
3802
  }
3689
- if (!within(folder, real)) return { why: "outside" };
3690
3803
  try {
3691
- return fs8.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
3804
+ return parseRunResult(json);
3692
3805
  } catch {
3693
- return { why: "absent" };
3806
+ throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
3694
3807
  }
3695
3808
  }
3696
- function markersOf(test) {
3697
- const refs = /* @__PURE__ */ new Set();
3698
- for (const stream of test.children) {
3699
- if (stream.name !== "system-out" && stream.name !== "system-err") continue;
3700
- for (const match of stream.text.matchAll(MARKER)) {
3701
- const ref = match[1]?.trim() ?? "";
3702
- if (ref !== "") refs.add(ref);
3703
- }
3809
+ async function safeErrorDetail(res) {
3810
+ try {
3811
+ const body = await res.json();
3812
+ return typeof body.error === "string" ? body.error : "";
3813
+ } catch {
3814
+ return "";
3704
3815
  }
3705
- return [...refs];
3706
3816
  }
3707
- function filesOfTest(test, folder, title, missing, options = {}) {
3708
- const files = [];
3709
- const all = markersOf(test);
3710
- if (all.length > MARKERS_PER_TEST) {
3711
- options.notes?.push(
3712
- `plune: ${JSON.stringify(printable(title))}: ${all.length - MARKERS_PER_TEST} more markers ignored (at most ${MARKERS_PER_TEST} per test)`
3713
- );
3817
+ async function handleSync(deps = {}) {
3818
+ const cwd = deps.cwd ?? process.cwd();
3819
+ const apiUrl = resolveApiUrl(deps.apiUrl);
3820
+ const token = (deps.loadToken ?? loadToken)();
3821
+ if (token === null) throw new NotLoggedInError();
3822
+ const run = readRun(cwd, deps.file);
3823
+ const doFetch = deps.fetchImpl ?? fetch;
3824
+ const endpoint = `${apiUrl}/v1/runs`;
3825
+ let res;
3826
+ try {
3827
+ res = await doFetch(endpoint, {
3828
+ method: "POST",
3829
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
3830
+ body: JSON.stringify(run)
3831
+ });
3832
+ } catch {
3833
+ throw new SyncNetworkError(apiUrl);
3714
3834
  }
3715
- for (const ref of all.slice(0, MARKERS_PER_TEST)) {
3716
- const found = resolveAttachment(folder, ref, options.lexical);
3717
- if (!("file" in found)) {
3718
- missing.push({ title, path: ref, ...options.resultKey !== void 0 ? { resultKey: options.resultKey } : {} });
3719
- continue;
3720
- }
3721
- const real = found.file;
3722
- if (!files.some((f) => f.path === real)) {
3723
- files.push({ name: printable(ref.split(/[\\/]/).pop() || path9.basename(real)), contentType: typeOfFile(real), path: real });
3724
- }
3835
+ if (!res.ok) {
3836
+ const detail = await safeErrorDetail(res);
3837
+ if (res.status === 401) throw new TokenRejectedError();
3838
+ throw new SyncHttpError(res.status, detail);
3725
3839
  }
3726
- return files;
3840
+ const body = await res.json().catch(() => ({}));
3841
+ const id = typeof body.id === "string" ? body.id : "";
3842
+ const result = { id, url: `${endpoint}/${id}` };
3843
+ if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
3844
+ return result;
3727
3845
  }
3728
- var fs8, path9, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
3729
- var init_attachments = __esm({
3730
- "src/importers/attachments.ts"() {
3731
- "use strict";
3732
- init_cjs_shims();
3733
- fs8 = __toESM(require("fs"), 1);
3734
- path9 = __toESM(require("path"), 1);
3735
- init_failure_detail();
3736
- MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
3737
- TYPES = {
3738
- ".png": "image/png",
3739
- ".jpg": "image/jpeg",
3740
- ".jpeg": "image/jpeg",
3741
- ".webp": "image/webp",
3742
- ".gif": "image/gif",
3743
- ".svg": "image/svg+xml",
3744
- ".json": "application/json",
3745
- ".txt": "text/plain",
3746
- ".log": "text/plain",
3747
- ".md": "text/plain",
3748
- ".csv": "text/csv",
3749
- ".html": "text/html",
3750
- ".htm": "text/html",
3751
- ".xml": "application/xml",
3752
- ".pdf": "application/pdf",
3753
- ".zip": "application/zip",
3754
- ".webm": "video/webm",
3755
- ".mp4": "video/mp4",
3756
- ".har": "application/json"
3757
- };
3758
- typeOfFile = (file) => TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream";
3759
- MARKERS_PER_TEST = 100;
3760
- }
3761
- });
3762
-
3763
- // src/importers/allure.ts
3764
- function isFolder(path17) {
3846
+ async function readVerdict(doFetch, url, token) {
3765
3847
  try {
3766
- return fs9.statSync(path17).isDirectory();
3848
+ const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
3849
+ const body = await res.json().catch(() => void 0);
3850
+ return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
3767
3851
  } catch {
3768
- return false;
3852
+ return void 0;
3769
3853
  }
3770
3854
  }
3771
- function load(file, maxBytes) {
3772
- let size;
3773
- try {
3774
- size = fs9.statSync(file).size;
3775
- } catch (err) {
3776
- throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
3777
- }
3778
- if (size > maxBytes) {
3779
- throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
3780
- }
3781
- let parsed;
3782
- try {
3783
- parsed = JSON.parse(fs9.readFileSync(file, "utf8"));
3784
- } catch (err) {
3785
- throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
3855
+ function reportSyncFailure(err, write) {
3856
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
3857
+ write(err.message + "\n");
3858
+ return 2;
3786
3859
  }
3787
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3788
- throw new AllureReportError(`${printable(file)} \u2014 not an Allure result: expected a JSON object.`);
3860
+ if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
3861
+ write(err.message + "\n");
3862
+ return 1;
3789
3863
  }
3790
- return parsed;
3791
- }
3792
- function listOf(v) {
3793
- const out = [];
3794
- if (!Array.isArray(v)) return out;
3795
- for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
3796
- return out;
3864
+ return null;
3797
3865
  }
3798
- function keyOf(r, name) {
3799
- const full = textOf(r.fullName);
3800
- if (name.endsWith("]") && full.includes("#")) return `${full.split("#")[0]}#${name}`;
3866
+ var fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
3867
+ var init_sync = __esm({
3868
+ "src/cli/commands/sync.ts"() {
3869
+ "use strict";
3870
+ init_cjs_shims();
3871
+ fs8 = __toESM(require("fs"), 1);
3872
+ path9 = __toESM(require("path"), 1);
3873
+ init_run_result_schema();
3874
+ init_api_url();
3875
+ init_credentials();
3876
+ init_gate();
3877
+ NotLoggedInError = class extends Error {
3878
+ constructor() {
3879
+ super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
3880
+ this.name = "NotLoggedInError";
3881
+ }
3882
+ };
3883
+ TokenRejectedError = class extends Error {
3884
+ constructor() {
3885
+ super(
3886
+ 'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
3887
+ );
3888
+ this.name = "TokenRejectedError";
3889
+ }
3890
+ };
3891
+ SyncFileError = class extends Error {
3892
+ constructor(message) {
3893
+ super(message);
3894
+ this.name = "SyncFileError";
3895
+ }
3896
+ };
3897
+ SyncNetworkError = class extends Error {
3898
+ constructor(apiUrl) {
3899
+ super(
3900
+ `Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
3901
+ );
3902
+ this.name = "SyncNetworkError";
3903
+ }
3904
+ };
3905
+ SyncHttpError = class extends Error {
3906
+ constructor(status, detail) {
3907
+ super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
3908
+ this.status = status;
3909
+ this.name = "SyncHttpError";
3910
+ }
3911
+ status;
3912
+ };
3913
+ }
3914
+ });
3915
+
3916
+ // src/cli/commands/plan.ts
3917
+ var plan_exports = {};
3918
+ __export(plan_exports, {
3919
+ RunRefError: () => RunRefError,
3920
+ formatGrep: () => formatGrep,
3921
+ grepFragment: () => grepFragment,
3922
+ handlePlanGrep: () => handlePlanGrep,
3923
+ handleRunGrep: () => handleRunGrep,
3924
+ runGrepSummary: () => runGrepSummary
3925
+ });
3926
+ function grepFragment(c) {
3927
+ const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
3928
+ const segments = pathTitle?.split("#") ?? [c.title];
3929
+ return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
3930
+ }
3931
+ function formatGrep(cases) {
3932
+ if (cases.length === 0) return "(?!)";
3933
+ return cases.map(grepFragment).join("|");
3934
+ }
3935
+ async function safeErrorDetail2(res) {
3936
+ try {
3937
+ const body = await res.json();
3938
+ return typeof body.error === "string" ? body.error : "";
3939
+ } catch {
3940
+ return "";
3941
+ }
3942
+ }
3943
+ function platformGet(deps) {
3944
+ const apiUrl = resolveApiUrl(deps.apiUrl);
3945
+ const token = (deps.loadToken ?? loadToken)();
3946
+ if (token === null) throw new NotLoggedInError();
3947
+ const doFetch = deps.fetchImpl ?? fetch;
3948
+ return async (path17) => {
3949
+ let res;
3950
+ try {
3951
+ res = await doFetch(`${apiUrl}${path17}`, { headers: { authorization: `Bearer ${token}` } });
3952
+ } catch {
3953
+ throw new SyncNetworkError(apiUrl);
3954
+ }
3955
+ if (!res.ok) {
3956
+ const detail = await safeErrorDetail2(res);
3957
+ if (res.status === 401) throw new TokenRejectedError();
3958
+ throw new SyncHttpError(res.status, detail);
3959
+ }
3960
+ return await res.json();
3961
+ };
3962
+ }
3963
+ async function handlePlanGrep(deps) {
3964
+ const get = platformGet(deps);
3965
+ const { cases } = await get(
3966
+ `/v1/plans/${encodeURIComponent(deps.planId)}/cases`
3967
+ );
3968
+ return { pattern: formatGrep(cases), cases: cases.length, keyed: keyedOf(cases) };
3969
+ }
3970
+ async function handleRunGrep(deps) {
3971
+ const byKey = RUN_KEY.test(deps.run);
3972
+ if (!byKey && !RUN_ID.test(deps.run)) {
3973
+ throw new RunRefError(
3974
+ `${JSON.stringify(deps.run)} is not a run \u2014 pass its id (a UUID, as in the run page's address) or its key (rk_ and ten hex, the PLUNE_RUN Plune shows).`,
3975
+ 2
3976
+ );
3977
+ }
3978
+ const get = platformGet(deps);
3979
+ let runId = deps.run;
3980
+ if (byKey) {
3981
+ const { runs } = await get(`/v1/runs?key=${deps.run}&limit=1`);
3982
+ const run = runs[0];
3983
+ if (run === void 0) {
3984
+ throw new RunRefError(
3985
+ `No run carries the key ${deps.run} \u2014 a key becomes a run's when the run is saved or launched in Plune.`,
3986
+ 1
3987
+ );
3988
+ }
3989
+ runId = run.id;
3990
+ }
3991
+ const { cases } = await get(
3992
+ `/v1/runs/${encodeURIComponent(runId)}/cases${deps.failed === true ? "?status=failed,broken" : ""}`
3993
+ );
3994
+ const automated = cases.filter((c) => c.type === "automated");
3995
+ return {
3996
+ pattern: formatGrep(automated),
3997
+ cases: automated.length,
3998
+ keyed: keyedOf(automated),
3999
+ skipped: cases.length - automated.length
4000
+ };
4001
+ }
4002
+ function runGrepSummary(out, failed) {
4003
+ if (out.cases === 0) {
4004
+ const none = failed ? "no automated case failed or broke in the run" : "the run holds no automated case";
4005
+ return `plune: ${none} (${out.skipped} manual left out) \u2014 the pattern matches nothing.
4006
+ `;
4007
+ }
4008
+ const which = failed ? "that failed or broke in the run" : "of the run";
4009
+ return `plune: ${out.cases} automated case(s) ${which}, ${out.keyed} with a test key, the rest by title; ${out.skipped} manual left out.
4010
+ `;
4011
+ }
4012
+ var escape, BOUNDARY, keyedOf, RUN_ID, RUN_KEY, RunRefError;
4013
+ var init_plan = __esm({
4014
+ "src/cli/commands/plan.ts"() {
4015
+ "use strict";
4016
+ init_cjs_shims();
4017
+ init_api_url();
4018
+ init_credentials();
4019
+ init_sync();
4020
+ escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4021
+ BOUNDARY = "[ >#]+";
4022
+ keyedOf = (cases) => cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length;
4023
+ RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4024
+ RUN_KEY = /^rk_[0-9a-f]{10}$/;
4025
+ RunRefError = class extends Error {
4026
+ constructor(message, exitCode) {
4027
+ super(message);
4028
+ this.exitCode = exitCode;
4029
+ this.name = "RunRefError";
4030
+ }
4031
+ exitCode;
4032
+ };
4033
+ }
4034
+ });
4035
+
4036
+ // src/cli/commands/cases.ts
4037
+ var cases_exports = {};
4038
+ __export(cases_exports, {
4039
+ DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
4040
+ DirtyFileError: () => DirtyFileError,
4041
+ formatReport: () => formatReport,
4042
+ handlePull: () => handlePull,
4043
+ handlePush: () => handlePush,
4044
+ reportCasesFailure: () => reportCasesFailure
4045
+ });
4046
+ function gitDirty(file) {
4047
+ try {
4048
+ const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
4049
+ cwd: path10.dirname(file),
4050
+ stdio: ["ignore", "pipe", "ignore"],
4051
+ encoding: "utf8"
4052
+ });
4053
+ return out.trim() !== "";
4054
+ } catch {
4055
+ return false;
4056
+ }
4057
+ }
4058
+ async function safeErrorDetail3(res) {
4059
+ try {
4060
+ const body = await res.json();
4061
+ return typeof body.error === "string" ? body.error : "";
4062
+ } catch {
4063
+ return "";
4064
+ }
4065
+ }
4066
+ function tokenOf(deps) {
4067
+ const token = (deps.loadToken ?? loadToken)();
4068
+ if (token === null) throw new NotLoggedInError();
4069
+ return token;
4070
+ }
4071
+ async function handlePull(deps = {}) {
4072
+ const cwd = deps.cwd ?? process.cwd();
4073
+ const apiUrl = resolveApiUrl(deps.apiUrl);
4074
+ const token = tokenOf(deps);
4075
+ const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
4076
+ if (fs9.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
4077
+ throw new DirtyFileError(path10.relative(cwd, file) || file);
4078
+ const doFetch = deps.fetchImpl ?? fetch;
4079
+ const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
4080
+ let res;
4081
+ try {
4082
+ res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
4083
+ } catch {
4084
+ throw new SyncNetworkError(apiUrl);
4085
+ }
4086
+ if (!res.ok) {
4087
+ const detail = await safeErrorDetail3(res);
4088
+ if (res.status === 401) throw new TokenRejectedError();
4089
+ throw new SyncHttpError(res.status, detail);
4090
+ }
4091
+ const text2 = await res.text();
4092
+ fs9.mkdirSync(path10.dirname(file), { recursive: true });
4093
+ fs9.writeFileSync(file, text2, "utf8");
4094
+ return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
4095
+ }
4096
+ async function handlePush(deps = {}) {
4097
+ const cwd = deps.cwd ?? process.cwd();
4098
+ const apiUrl = resolveApiUrl(deps.apiUrl);
4099
+ const token = tokenOf(deps);
4100
+ const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
4101
+ let text2;
4102
+ try {
4103
+ text2 = fs9.readFileSync(file, "utf8");
4104
+ } catch {
4105
+ throw new SyncFileError(
4106
+ `No document to push: ${file}. Run "plune pull" first (or pass a file).`
4107
+ );
4108
+ }
4109
+ if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
4110
+ const doFetch = deps.fetchImpl ?? fetch;
4111
+ let res;
4112
+ try {
4113
+ res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
4114
+ method: "POST",
4115
+ headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
4116
+ body: text2
4117
+ });
4118
+ } catch {
4119
+ throw new SyncNetworkError(apiUrl);
4120
+ }
4121
+ if (!res.ok) {
4122
+ const detail = await safeErrorDetail3(res);
4123
+ if (res.status === 401) throw new TokenRejectedError();
4124
+ throw new SyncHttpError(res.status, detail);
4125
+ }
4126
+ return await res.json();
4127
+ }
4128
+ function formatReport(report) {
4129
+ const verb = (past, future) => report.dryRun ? future : past;
4130
+ const lines = [
4131
+ `${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
4132
+ ];
4133
+ for (const r of report.refused)
4134
+ lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
4135
+ for (const w of report.warnings)
4136
+ lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
4137
+ return lines.join("\n") + "\n";
4138
+ }
4139
+ function reportCasesFailure(err, write) {
4140
+ if (err instanceof DirtyFileError) {
4141
+ write(err.message + "\n");
4142
+ return 3;
4143
+ }
4144
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
4145
+ write(err.message + "\n");
4146
+ return 2;
4147
+ }
4148
+ if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
4149
+ write(err.message + "\n");
4150
+ return 1;
4151
+ }
4152
+ return null;
4153
+ }
4154
+ var import_node_child_process2, fs9, path10, DEFAULT_CASES_FILE, DirtyFileError;
4155
+ var init_cases = __esm({
4156
+ "src/cli/commands/cases.ts"() {
4157
+ "use strict";
4158
+ init_cjs_shims();
4159
+ import_node_child_process2 = require("child_process");
4160
+ fs9 = __toESM(require("fs"), 1);
4161
+ path10 = __toESM(require("path"), 1);
4162
+ init_api_url();
4163
+ init_credentials();
4164
+ init_sync();
4165
+ DEFAULT_CASES_FILE = path10.join("plune", "cases.md");
4166
+ DirtyFileError = class extends Error {
4167
+ constructor(file) {
4168
+ super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
4169
+ this.name = "DirtyFileError";
4170
+ }
4171
+ };
4172
+ }
4173
+ });
4174
+
4175
+ // src/reporter-core/result-key.ts
4176
+ function resultKey(testId, retry) {
4177
+ const plain = `${testId}#${retry}`;
4178
+ return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
4179
+ }
4180
+ var RESULT_KEY_MAX, HASH_CHARS;
4181
+ var init_result_key = __esm({
4182
+ "src/reporter-core/result-key.ts"() {
4183
+ "use strict";
4184
+ init_cjs_shims();
4185
+ init_hash();
4186
+ RESULT_KEY_MAX = 200;
4187
+ HASH_CHARS = 32;
4188
+ }
4189
+ });
4190
+
4191
+ // src/importers/attachments.ts
4192
+ function reportFolder(report) {
4193
+ const folder = path11.dirname(path11.resolve(report));
4194
+ try {
4195
+ return fs10.realpathSync(folder);
4196
+ } catch {
4197
+ return folder;
4198
+ }
4199
+ }
4200
+ function reportFolders(report) {
4201
+ return { lexical: path11.dirname(path11.resolve(report)), real: reportFolder(report) };
4202
+ }
4203
+ function within(folder, target) {
4204
+ const rel = path11.relative(folder, target);
4205
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${path11.sep}`) && !path11.isAbsolute(rel);
4206
+ }
4207
+ function resolveAttachment(folder, ref, lexical = folder) {
4208
+ if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
4209
+ const face = path11.resolve(lexical, ref);
4210
+ if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
4211
+ let real;
4212
+ try {
4213
+ real = fs10.realpathSync(path11.resolve(folder, ref));
4214
+ } catch {
4215
+ return { why: "absent" };
4216
+ }
4217
+ if (!within(folder, real)) return { why: "outside" };
4218
+ try {
4219
+ return fs10.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
4220
+ } catch {
4221
+ return { why: "absent" };
4222
+ }
4223
+ }
4224
+ function markersOf(test) {
4225
+ const refs = /* @__PURE__ */ new Set();
4226
+ for (const stream of test.children) {
4227
+ if (stream.name !== "system-out" && stream.name !== "system-err") continue;
4228
+ for (const match of stream.text.matchAll(MARKER)) {
4229
+ const ref = match[1]?.trim() ?? "";
4230
+ if (ref !== "") refs.add(ref);
4231
+ }
4232
+ }
4233
+ return [...refs];
4234
+ }
4235
+ function filesOfTest(test, folder, title, missing, options = {}) {
4236
+ const files = [];
4237
+ const all = markersOf(test);
4238
+ if (all.length > MARKERS_PER_TEST) {
4239
+ options.notes?.push(
4240
+ `plune: ${JSON.stringify(printable(title))}: ${all.length - MARKERS_PER_TEST} more markers ignored (at most ${MARKERS_PER_TEST} per test)`
4241
+ );
4242
+ }
4243
+ for (const ref of all.slice(0, MARKERS_PER_TEST)) {
4244
+ const found = resolveAttachment(folder, ref, options.lexical);
4245
+ if (!("file" in found)) {
4246
+ missing.push({ title, path: ref, ...options.resultKey !== void 0 ? { resultKey: options.resultKey } : {} });
4247
+ continue;
4248
+ }
4249
+ const real = found.file;
4250
+ if (!files.some((f) => f.path === real)) {
4251
+ files.push({ name: printable(ref.split(/[\\/]/).pop() || path11.basename(real)), contentType: typeOfFile(real), path: real });
4252
+ }
4253
+ }
4254
+ return files;
4255
+ }
4256
+ var fs10, path11, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
4257
+ var init_attachments = __esm({
4258
+ "src/importers/attachments.ts"() {
4259
+ "use strict";
4260
+ init_cjs_shims();
4261
+ fs10 = __toESM(require("fs"), 1);
4262
+ path11 = __toESM(require("path"), 1);
4263
+ init_failure_detail();
4264
+ MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
4265
+ TYPES = {
4266
+ ".png": "image/png",
4267
+ ".jpg": "image/jpeg",
4268
+ ".jpeg": "image/jpeg",
4269
+ ".webp": "image/webp",
4270
+ ".gif": "image/gif",
4271
+ ".svg": "image/svg+xml",
4272
+ ".json": "application/json",
4273
+ ".txt": "text/plain",
4274
+ ".log": "text/plain",
4275
+ ".md": "text/plain",
4276
+ ".csv": "text/csv",
4277
+ ".html": "text/html",
4278
+ ".htm": "text/html",
4279
+ ".xml": "application/xml",
4280
+ ".pdf": "application/pdf",
4281
+ ".zip": "application/zip",
4282
+ ".webm": "video/webm",
4283
+ ".mp4": "video/mp4",
4284
+ ".har": "application/json"
4285
+ };
4286
+ typeOfFile = (file) => TYPES[path11.extname(file).toLowerCase()] ?? "application/octet-stream";
4287
+ MARKERS_PER_TEST = 100;
4288
+ }
4289
+ });
4290
+
4291
+ // src/importers/allure.ts
4292
+ function isFolder(path17) {
4293
+ try {
4294
+ return fs11.statSync(path17).isDirectory();
4295
+ } catch {
4296
+ return false;
4297
+ }
4298
+ }
4299
+ function load(file, maxBytes) {
4300
+ let size;
4301
+ try {
4302
+ size = fs11.statSync(file).size;
4303
+ } catch (err) {
4304
+ throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
4305
+ }
4306
+ if (size > maxBytes) {
4307
+ throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
4308
+ }
4309
+ let parsed;
4310
+ try {
4311
+ parsed = JSON.parse(fs11.readFileSync(file, "utf8"));
4312
+ } catch (err) {
4313
+ throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
4314
+ }
4315
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4316
+ throw new AllureReportError(`${printable(file)} \u2014 not an Allure result: expected a JSON object.`);
4317
+ }
4318
+ return parsed;
4319
+ }
4320
+ function listOf(v) {
4321
+ const out = [];
4322
+ if (!Array.isArray(v)) return out;
4323
+ for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
4324
+ return out;
4325
+ }
4326
+ function keyOf(r, name) {
4327
+ const full = textOf(r.fullName);
4328
+ if (name.endsWith("]") && full.includes("#")) return `${full.split("#")[0]}#${name}`;
3801
4329
  let base;
3802
4330
  if (full.includes(" \u203A ")) base = full.split(" \u203A ").join("#");
3803
4331
  else if (full.includes("#")) base = full;
@@ -3837,9 +4365,9 @@ function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
3837
4365
  `${printable(dir)} \u2014 no *-result.json files in it, so there is no Allure report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), or at a JUnit or Playwright JSON file.`
3838
4366
  );
3839
4367
  }
3840
- const { lexical, real: folder } = reportFolders(path10.join(dir, "_"));
3841
- const tests = resultNames.map((n) => load(path10.join(dir, n), maxBytes));
3842
- const containers = namesIn(dir, CONTAINER).map((n) => load(path10.join(dir, n), maxBytes));
4368
+ const { lexical, real: folder } = reportFolders(path12.join(dir, "_"));
4369
+ const tests = resultNames.map((n) => load(path12.join(dir, n), maxBytes));
4370
+ const containers = namesIn(dir, CONTAINER).map((n) => load(path12.join(dir, n), maxBytes));
3843
4371
  const direct = /* @__PURE__ */ new Map();
3844
4372
  for (const c of containers) {
3845
4373
  for (const child of Array.isArray(c.children) ? c.children : []) {
@@ -3907,7 +4435,7 @@ function pendingOf(r, retry, ctx) {
3907
4435
  }
3908
4436
  if (files.some((f) => f.path === found.file)) return;
3909
4437
  const type = textOf(att.type).trim();
3910
- const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path10.basename(found.file));
4438
+ const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path12.basename(found.file));
3911
4439
  files.push({ name: name2, contentType: type !== "" ? type : typeOfFile(found.file), path: found.file });
3912
4440
  };
3913
4441
  const holders = [];
@@ -3966,13 +4494,13 @@ function pendingOf(r, retry, ctx) {
3966
4494
  ...files.length > 0 ? { files } : {}
3967
4495
  };
3968
4496
  }
3969
- var fs9, path10, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
4497
+ var fs11, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
3970
4498
  var init_allure = __esm({
3971
4499
  "src/importers/allure.ts"() {
3972
4500
  "use strict";
3973
4501
  init_cjs_shims();
3974
- fs9 = __toESM(require("fs"), 1);
3975
- path10 = __toESM(require("path"), 1);
4502
+ fs11 = __toESM(require("fs"), 1);
4503
+ path12 = __toESM(require("path"), 1);
3976
4504
  init_failure_detail();
3977
4505
  init_result_key();
3978
4506
  init_attachments();
@@ -3989,7 +4517,7 @@ var init_allure = __esm({
3989
4517
  MAX_JSON_BYTES = 64 * 1024 * 1024;
3990
4518
  MAX_DEPTH = 256;
3991
4519
  MAX_TIME = 864e13;
3992
- namesIn = (dir, pattern) => fs9.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
4520
+ namesIn = (dir, pattern) => fs11.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
3993
4521
  timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
3994
4522
  textOf = (v) => typeof v === "string" ? v : "";
3995
4523
  labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
@@ -4463,7 +4991,7 @@ async function handleRunImport(options) {
4463
4991
  } else {
4464
4992
  let source;
4465
4993
  try {
4466
- source = fs10.readFileSync(options.file, "utf8");
4994
+ source = fs12.readFileSync(options.file, "utf8");
4467
4995
  } catch {
4468
4996
  throw new RunCommandError(`Cannot read ${options.file}.`);
4469
4997
  }
@@ -4487,6 +5015,7 @@ async function handleRunImport(options) {
4487
5015
  ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
4488
5016
  ...options.key !== void 0 ? { externalKey: options.key } : {},
4489
5017
  ...options.create === true ? { offerDiscovered: true } : {},
5018
+ ...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
4490
5019
  kind: "automated",
4491
5020
  // The CI run the report names, never this machine's: whoever imports may be another job (#790).
4492
5021
  // The commit and branch likewise, where the report names a commit; a report that names none —
@@ -4594,12 +5123,12 @@ function describe(result, apiUrl) {
4594
5123
  if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
4595
5124
  return lines;
4596
5125
  }
4597
- var fs10, NEXT;
5126
+ var fs12, NEXT;
4598
5127
  var init_run_import = __esm({
4599
5128
  "src/cli/commands/run-import.ts"() {
4600
5129
  "use strict";
4601
5130
  init_cjs_shims();
4602
- fs10 = __toESM(require("fs"), 1);
5131
+ fs12 = __toESM(require("fs"), 1);
4603
5132
  init_session();
4604
5133
  init_failure_detail();
4605
5134
  init_importers();
@@ -4629,10 +5158,10 @@ function isRunResult(v) {
4629
5158
  }
4630
5159
  function handleReport(opts = {}) {
4631
5160
  const cwd = opts.cwd ?? process.cwd();
4632
- const file = path11.join(cwd, ".plune", "last-run.json");
5161
+ const file = path13.join(cwd, ".plune", "last-run.json");
4633
5162
  let raw;
4634
5163
  try {
4635
- raw = fs11.readFileSync(file, "utf8");
5164
+ raw = fs13.readFileSync(file, "utf8");
4636
5165
  } catch {
4637
5166
  throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
4638
5167
  }
@@ -4647,13 +5176,13 @@ function handleReport(opts = {}) {
4647
5176
  }
4648
5177
  return parsed;
4649
5178
  }
4650
- var fs11, path11, ReportNotFoundError;
5179
+ var fs13, path13, ReportNotFoundError;
4651
5180
  var init_report = __esm({
4652
5181
  "src/cli/commands/report.ts"() {
4653
5182
  "use strict";
4654
5183
  init_cjs_shims();
4655
- fs11 = __toESM(require("fs"), 1);
4656
- path11 = __toESM(require("path"), 1);
5184
+ fs13 = __toESM(require("fs"), 1);
5185
+ path13 = __toESM(require("path"), 1);
4657
5186
  ReportNotFoundError = class extends Error {
4658
5187
  code = "NO_SAVED_RUN";
4659
5188
  constructor(message) {
@@ -4723,7 +5252,7 @@ function isRunResult2(v) {
4723
5252
  function loadRunResult(file, label) {
4724
5253
  let raw;
4725
5254
  try {
4726
- raw = fs12.readFileSync(file, "utf8");
5255
+ raw = fs14.readFileSync(file, "utf8");
4727
5256
  } catch {
4728
5257
  throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
4729
5258
  }
@@ -4743,12 +5272,12 @@ function handleDiff(opts) {
4743
5272
  const current = loadRunResult(opts.currentPath, "current");
4744
5273
  return diffRuns(baseline, current);
4745
5274
  }
4746
- var fs12, DiffInputError;
5275
+ var fs14, DiffInputError;
4747
5276
  var init_diff2 = __esm({
4748
5277
  "src/cli/commands/diff.ts"() {
4749
5278
  "use strict";
4750
5279
  init_cjs_shims();
4751
- fs12 = __toESM(require("fs"), 1);
5280
+ fs14 = __toESM(require("fs"), 1);
4752
5281
  init_diff();
4753
5282
  DiffInputError = class extends Error {
4754
5283
  code = "DIFF_INPUT";
@@ -4873,8 +5402,8 @@ async function runInitWizard(cwd) {
4873
5402
  process.exit(1);
4874
5403
  return;
4875
5404
  }
4876
- const outputPath = path12.join(cwd, "plune.yaml");
4877
- if (fs13.existsSync(outputPath)) {
5405
+ const outputPath = path14.join(cwd, "plune.yaml");
5406
+ if (fs15.existsSync(outputPath)) {
4878
5407
  const overwrite = await clack.confirm({
4879
5408
  message: "plune.yaml already exists. Overwrite?"
4880
5409
  });
@@ -4913,18 +5442,18 @@ async function runInitWizard(cwd) {
4913
5442
  `;
4914
5443
  const spin = clack.spinner();
4915
5444
  spin.start("Writing plune.yaml\u2026");
4916
- fs13.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
5445
+ fs15.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
4917
5446
  spin.stop("plune.yaml created!");
4918
5447
  clack.outro("plune.yaml is ready.");
4919
5448
  }
4920
- var clack, fs13, path12, import_yaml2;
5449
+ var clack, fs15, path14, import_yaml2;
4921
5450
  var init_wizard = __esm({
4922
5451
  "src/config/init/wizard.ts"() {
4923
5452
  "use strict";
4924
5453
  init_cjs_shims();
4925
5454
  clack = __toESM(require("@clack/prompts"), 1);
4926
- fs13 = __toESM(require("fs"), 1);
4927
- path12 = __toESM(require("path"), 1);
5455
+ fs15 = __toESM(require("fs"), 1);
5456
+ path14 = __toESM(require("path"), 1);
4928
5457
  import_yaml2 = require("yaml");
4929
5458
  init_errors();
4930
5459
  }
@@ -4932,21 +5461,21 @@ var init_wizard = __esm({
4932
5461
 
4933
5462
  // src/cli/templates/index.ts
4934
5463
  function writeTemplateFile(cwd, rel, content, force) {
4935
- const target = path13.join(cwd, rel);
4936
- if (!force && fs14.existsSync(target)) {
5464
+ const target = path15.join(cwd, rel);
5465
+ if (!force && fs16.existsSync(target)) {
4937
5466
  return "skipped";
4938
5467
  }
4939
- fs14.mkdirSync(path13.dirname(target), { recursive: true });
4940
- fs14.writeFileSync(target, content, "utf8");
5468
+ fs16.mkdirSync(path15.dirname(target), { recursive: true });
5469
+ fs16.writeFileSync(target, content, "utf8");
4941
5470
  return "written";
4942
5471
  }
4943
- var fs14, path13, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
5472
+ var fs16, path15, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
4944
5473
  var init_templates = __esm({
4945
5474
  "src/cli/templates/index.ts"() {
4946
5475
  "use strict";
4947
5476
  init_cjs_shims();
4948
- fs14 = __toESM(require("fs"), 1);
4949
- path13 = __toESM(require("path"), 1);
5477
+ fs16 = __toESM(require("fs"), 1);
5478
+ path15 = __toESM(require("path"), 1);
4950
5479
  ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
4951
5480
  # .env is gitignored; .env.example is safe to commit.
4952
5481
  # Plune reads whichever key matches your configured provider.
@@ -5032,7 +5561,7 @@ __export(login_exports, {
5032
5561
  EmptyTokenError: () => EmptyTokenError,
5033
5562
  LoginHttpError: () => LoginHttpError,
5034
5563
  LoginNetworkError: () => LoginNetworkError,
5035
- TokenRejectedError: () => TokenRejectedError,
5564
+ TokenRejectedError: () => TokenRejectedError2,
5036
5565
  handleLogin: () => handleLogin,
5037
5566
  reportLoginFailure: () => reportLoginFailure
5038
5567
  });
@@ -5051,12 +5580,12 @@ async function handleLogin(deps) {
5051
5580
  throw new LoginNetworkError(apiUrl);
5052
5581
  }
5053
5582
  await res.text().catch(() => "");
5054
- if (res.status === 401) throw new TokenRejectedError(apiUrl);
5583
+ if (res.status === 401) throw new TokenRejectedError2(apiUrl);
5055
5584
  if (!res.ok) throw new LoginHttpError(res.status);
5056
5585
  return { path: saveToken(token), verified: true };
5057
5586
  }
5058
5587
  function reportLoginFailure(err, write) {
5059
- if (err instanceof EmptyTokenError || err instanceof TokenRejectedError) {
5588
+ if (err instanceof EmptyTokenError || err instanceof TokenRejectedError2) {
5060
5589
  write(`${err.message}
5061
5590
  `);
5062
5591
  return 2;
@@ -5067,285 +5596,73 @@ function reportLoginFailure(err, write) {
5067
5596
  return 1;
5068
5597
  }
5069
5598
  return null;
5070
- }
5071
- var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
5072
- var init_login = __esm({
5073
- "src/cli/commands/login.ts"() {
5074
- "use strict";
5075
- init_cjs_shims();
5076
- init_api_url();
5077
- init_credentials();
5078
- EmptyTokenError = class extends Error {
5079
- constructor(apiUrl = resolveApiUrl()) {
5080
- super(
5081
- `No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
5082
- );
5083
- this.name = "EmptyTokenError";
5084
- }
5085
- };
5086
- TokenRejectedError = class extends Error {
5087
- constructor(apiUrl = resolveApiUrl()) {
5088
- super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
5089
- this.name = "TokenRejectedError";
5090
- }
5091
- };
5092
- LoginNetworkError = class extends Error {
5093
- constructor(apiUrl) {
5094
- super(
5095
- `Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
5096
- );
5097
- this.name = "LoginNetworkError";
5098
- }
5099
- };
5100
- LoginHttpError = class extends Error {
5101
- constructor(status) {
5102
- super(
5103
- `The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
5104
- );
5105
- this.name = "LoginHttpError";
5106
- }
5107
- };
5108
- }
5109
- });
5110
-
5111
- // src/cli/commands/logout.ts
5112
- var logout_exports = {};
5113
- __export(logout_exports, {
5114
- handleLogout: () => handleLogout
5115
- });
5116
- function handleLogout() {
5117
- return clearToken();
5118
- }
5119
- var init_logout = __esm({
5120
- "src/cli/commands/logout.ts"() {
5121
- "use strict";
5122
- init_cjs_shims();
5123
- init_credentials();
5124
- }
5125
- });
5126
-
5127
- // src/types/run-result-schema.ts
5128
- function parseRunResult(value2) {
5129
- return runResultSchema.parse(value2);
5130
- }
5131
- var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
5132
- var init_run_result_schema = __esm({
5133
- "src/types/run-result-schema.ts"() {
5134
- "use strict";
5135
- init_cjs_shims();
5136
- import_zod3 = require("zod");
5137
- init_results();
5138
- RUN_RESULT_SCHEMA_VERSION = 1;
5139
- binaryVerdictSchema = import_zod3.z.object({
5140
- question: import_zod3.z.string().min(1),
5141
- passed: import_zod3.z.boolean()
5142
- });
5143
- assertionResultRecordSchema2 = import_zod3.z.object({
5144
- type: import_zod3.z.string(),
5145
- passed: import_zod3.z.boolean(),
5146
- score: import_zod3.z.number().optional(),
5147
- reason: import_zod3.z.string().optional(),
5148
- binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
5149
- });
5150
- usageSchema = import_zod3.z.object({
5151
- input_tokens: import_zod3.z.number(),
5152
- output_tokens: import_zod3.z.number(),
5153
- cost_usd: import_zod3.z.number()
5154
- });
5155
- runErrorSchema = import_zod3.z.object({
5156
- code: import_zod3.z.string(),
5157
- message: import_zod3.z.string()
5158
- });
5159
- rowResultSchema = import_zod3.z.object({
5160
- vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
5161
- output: import_zod3.z.string().nullable(),
5162
- cached: import_zod3.z.boolean(),
5163
- usage: usageSchema.optional(),
5164
- latency_ms: import_zod3.z.number().optional(),
5165
- error: runErrorSchema.optional(),
5166
- assertions: import_zod3.z.array(assertionResultRecordSchema2),
5167
- /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
5168
- inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
5169
- });
5170
- summarySchema = import_zod3.z.object({
5171
- total: import_zod3.z.number(),
5172
- passed: import_zod3.z.number(),
5173
- failed: import_zod3.z.number(),
5174
- errored: import_zod3.z.number(),
5175
- /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
5176
- blocked: import_zod3.z.number().optional(),
5177
- skipped: import_zod3.z.number().optional(),
5178
- cost_usd: import_zod3.z.number(),
5179
- duration_ms: import_zod3.z.number()
5180
- });
5181
- evalResultSchema = import_zod3.z.object({
5182
- id: import_zod3.z.string(),
5183
- tags: import_zod3.z.array(import_zod3.z.string()),
5184
- rows: import_zod3.z.array(rowResultSchema),
5185
- passed: import_zod3.z.boolean()
5186
- });
5187
- runResultSchema = import_zod3.z.object({
5188
- schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
5189
- plune_version: import_zod3.z.string(),
5190
- started_at: import_zod3.z.string(),
5191
- finished_at: import_zod3.z.string(),
5192
- config_hash: import_zod3.z.string(),
5193
- summary: summarySchema,
5194
- evals: import_zod3.z.array(evalResultSchema)
5195
- });
5196
- }
5197
- });
5198
-
5199
- // src/cli/commands/sync.ts
5200
- var sync_exports = {};
5201
- __export(sync_exports, {
5202
- NotLoggedInError: () => NotLoggedInError,
5203
- SyncFileError: () => SyncFileError,
5204
- SyncHttpError: () => SyncHttpError,
5205
- SyncNetworkError: () => SyncNetworkError,
5206
- TokenRejectedError: () => TokenRejectedError2,
5207
- handleSync: () => handleSync,
5208
- reportSyncFailure: () => reportSyncFailure
5209
- });
5210
- function readRun(cwd, file) {
5211
- const target = file !== void 0 ? path14.resolve(cwd, file) : path14.join(cwd, ".plune", "last-run.json");
5212
- let raw;
5213
- try {
5214
- raw = fs15.readFileSync(target, "utf8");
5215
- } catch {
5216
- throw new SyncFileError(
5217
- file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
5218
- );
5219
- }
5220
- let json;
5221
- try {
5222
- json = JSON.parse(raw);
5223
- } catch {
5224
- throw new SyncFileError(`Run file is not valid JSON: ${target}`);
5225
- }
5226
- try {
5227
- return parseRunResult(json);
5228
- } catch {
5229
- throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
5230
- }
5231
- }
5232
- async function safeErrorDetail(res) {
5233
- try {
5234
- const body = await res.json();
5235
- return typeof body.error === "string" ? body.error : "";
5236
- } catch {
5237
- return "";
5238
- }
5239
- }
5240
- async function handleSync(deps = {}) {
5241
- const cwd = deps.cwd ?? process.cwd();
5242
- const apiUrl = resolveApiUrl(deps.apiUrl);
5243
- const token = (deps.loadToken ?? loadToken)();
5244
- if (token === null) throw new NotLoggedInError();
5245
- const run = readRun(cwd, deps.file);
5246
- const doFetch = deps.fetchImpl ?? fetch;
5247
- const endpoint = `${apiUrl}/v1/runs`;
5248
- let res;
5249
- try {
5250
- res = await doFetch(endpoint, {
5251
- method: "POST",
5252
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
5253
- body: JSON.stringify(run)
5254
- });
5255
- } catch {
5256
- throw new SyncNetworkError(apiUrl);
5257
- }
5258
- if (!res.ok) {
5259
- const detail = await safeErrorDetail(res);
5260
- if (res.status === 401) throw new TokenRejectedError2();
5261
- throw new SyncHttpError(res.status, detail);
5262
- }
5263
- const body = await res.json().catch(() => ({}));
5264
- const id = typeof body.id === "string" ? body.id : "";
5265
- const result = { id, url: `${endpoint}/${id}` };
5266
- if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
5267
- return result;
5268
- }
5269
- async function readVerdict(doFetch, url, token) {
5270
- try {
5271
- const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
5272
- const body = await res.json().catch(() => void 0);
5273
- return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
5274
- } catch {
5275
- return void 0;
5276
- }
5277
- }
5278
- function reportSyncFailure(err, write) {
5279
- if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
5280
- write(err.message + "\n");
5281
- return 2;
5282
- }
5283
- if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
5284
- write(err.message + "\n");
5285
- return 1;
5286
- }
5287
- return null;
5288
- }
5289
- var fs15, path14, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
5290
- var init_sync = __esm({
5291
- "src/cli/commands/sync.ts"() {
5292
- "use strict";
5293
- init_cjs_shims();
5294
- fs15 = __toESM(require("fs"), 1);
5295
- path14 = __toESM(require("path"), 1);
5296
- init_run_result_schema();
5297
- init_api_url();
5298
- init_credentials();
5299
- init_gate();
5300
- NotLoggedInError = class extends Error {
5301
- constructor() {
5302
- super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
5303
- this.name = "NotLoggedInError";
5304
- }
5305
- };
5306
- TokenRejectedError2 = class extends Error {
5307
- constructor() {
5599
+ }
5600
+ var EmptyTokenError, TokenRejectedError2, LoginNetworkError, LoginHttpError;
5601
+ var init_login = __esm({
5602
+ "src/cli/commands/login.ts"() {
5603
+ "use strict";
5604
+ init_cjs_shims();
5605
+ init_api_url();
5606
+ init_credentials();
5607
+ EmptyTokenError = class extends Error {
5608
+ constructor(apiUrl = resolveApiUrl()) {
5308
5609
  super(
5309
- 'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
5610
+ `No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
5310
5611
  );
5311
- this.name = "TokenRejectedError";
5612
+ this.name = "EmptyTokenError";
5312
5613
  }
5313
5614
  };
5314
- SyncFileError = class extends Error {
5315
- constructor(message) {
5316
- super(message);
5317
- this.name = "SyncFileError";
5615
+ TokenRejectedError2 = class extends Error {
5616
+ constructor(apiUrl = resolveApiUrl()) {
5617
+ super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
5618
+ this.name = "TokenRejectedError";
5318
5619
  }
5319
5620
  };
5320
- SyncNetworkError = class extends Error {
5621
+ LoginNetworkError = class extends Error {
5321
5622
  constructor(apiUrl) {
5322
5623
  super(
5323
- `Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
5624
+ `Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
5324
5625
  );
5325
- this.name = "SyncNetworkError";
5626
+ this.name = "LoginNetworkError";
5326
5627
  }
5327
5628
  };
5328
- SyncHttpError = class extends Error {
5329
- constructor(status, detail) {
5330
- super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
5331
- this.status = status;
5332
- this.name = "SyncHttpError";
5629
+ LoginHttpError = class extends Error {
5630
+ constructor(status) {
5631
+ super(
5632
+ `The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
5633
+ );
5634
+ this.name = "LoginHttpError";
5333
5635
  }
5334
- status;
5335
5636
  };
5336
5637
  }
5337
5638
  });
5338
5639
 
5640
+ // src/cli/commands/logout.ts
5641
+ var logout_exports = {};
5642
+ __export(logout_exports, {
5643
+ handleLogout: () => handleLogout
5644
+ });
5645
+ function handleLogout() {
5646
+ return clearToken();
5647
+ }
5648
+ var init_logout = __esm({
5649
+ "src/cli/commands/logout.ts"() {
5650
+ "use strict";
5651
+ init_cjs_shims();
5652
+ init_credentials();
5653
+ }
5654
+ });
5655
+
5339
5656
  // src/cli/commands/cairn-artifact.ts
5340
5657
  async function findLatestCairnRun(runsDir = "runs") {
5341
- const entries = await fs16.readdir(runsDir, { withFileTypes: true }).catch(() => null);
5658
+ const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
5342
5659
  if (entries === null) throw new NoCairnRunsError(runsDir);
5343
5660
  let newest;
5344
5661
  for (const entry of entries) {
5345
5662
  if (!entry.isDirectory()) continue;
5346
- const dir = path15.join(runsDir, entry.name);
5663
+ const dir = path16.join(runsDir, entry.name);
5347
5664
  try {
5348
- const report = await fs16.lstat(path15.join(dir, "report.json"));
5665
+ const report = await fs17.lstat(path16.join(dir, "report.json"));
5349
5666
  if (!report.isFile()) continue;
5350
5667
  if (newest === void 0 || report.mtimeMs > newest.at) {
5351
5668
  newest = { dir, at: report.mtimeMs };
@@ -5358,13 +5675,13 @@ async function findLatestCairnRun(runsDir = "runs") {
5358
5675
  }
5359
5676
  async function readJson(file) {
5360
5677
  try {
5361
- return JSON.parse(await fs16.readFile(file, "utf8"));
5678
+ return JSON.parse(await fs17.readFile(file, "utf8"));
5362
5679
  } catch {
5363
5680
  return void 0;
5364
5681
  }
5365
5682
  }
5366
5683
  async function defaultReadEvidence(dir) {
5367
- const raw = await readJson(path15.join(dir, "api-evidence.json"));
5684
+ const raw = await readJson(path16.join(dir, "api-evidence.json"));
5368
5685
  if (!Array.isArray(raw)) return void 0;
5369
5686
  return raw.map((r) => {
5370
5687
  const rec = asRecord(r);
@@ -5407,7 +5724,7 @@ function casesFromApi(report, verdicts) {
5407
5724
  });
5408
5725
  }
5409
5726
  async function readCairnRun(dir, deps = {}) {
5410
- const raw = await readJson(path15.join(dir, "report.json"));
5727
+ const raw = await readJson(path16.join(dir, "report.json"));
5411
5728
  if (raw === void 0) throw new CairnRunNotFoundError(dir);
5412
5729
  const report = asRecord(raw);
5413
5730
  if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
@@ -5437,20 +5754,20 @@ async function readCairnRun(dir, deps = {}) {
5437
5754
  return {
5438
5755
  schemaVersion: INGEST_SCHEMA_VERSION,
5439
5756
  run: {
5440
- runId: str(report["runId"], path15.basename(dir)),
5757
+ runId: str(report["runId"], path16.basename(dir)),
5441
5758
  mode,
5442
5759
  target: str(report["url"], "unknown")
5443
5760
  },
5444
5761
  cases
5445
5762
  };
5446
5763
  }
5447
- var fs16, path15, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
5764
+ var fs17, path16, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
5448
5765
  var init_cairn_artifact = __esm({
5449
5766
  "src/cli/commands/cairn-artifact.ts"() {
5450
5767
  "use strict";
5451
5768
  init_cjs_shims();
5452
- fs16 = __toESM(require("fs/promises"), 1);
5453
- path15 = __toESM(require("path"), 1);
5769
+ fs17 = __toESM(require("fs/promises"), 1);
5770
+ path16 = __toESM(require("path"), 1);
5454
5771
  SUPPORTED_ARTIFACT_VERSION = 1;
5455
5772
  INGEST_SCHEMA_VERSION = 1;
5456
5773
  NoCairnRunsError = class extends Error {
@@ -5505,7 +5822,7 @@ __export(ingest_exports, {
5505
5822
  handleIngest: () => handleIngest,
5506
5823
  reportIngestFailure: () => reportIngestFailure
5507
5824
  });
5508
- async function safeErrorDetail2(res) {
5825
+ async function safeErrorDetail4(res) {
5509
5826
  try {
5510
5827
  const body = await res.json();
5511
5828
  return typeof body.error === "string" ? body.error : "";
@@ -5532,7 +5849,7 @@ async function handleIngest(deps) {
5532
5849
  throw new IngestNetworkError(apiUrl);
5533
5850
  }
5534
5851
  if (!res.ok) {
5535
- const detail = await safeErrorDetail2(res);
5852
+ const detail = await safeErrorDetail4(res);
5536
5853
  if (res.status === 401) throw new IngestTokenRejectedError();
5537
5854
  throw new IngestHttpError(res.status, detail);
5538
5855
  }
@@ -5604,207 +5921,6 @@ var init_ingest = __esm({
5604
5921
  }
5605
5922
  });
5606
5923
 
5607
- // src/cli/commands/cases.ts
5608
- var cases_exports = {};
5609
- __export(cases_exports, {
5610
- DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
5611
- DirtyFileError: () => DirtyFileError,
5612
- formatReport: () => formatReport,
5613
- handlePull: () => handlePull,
5614
- handlePush: () => handlePush,
5615
- reportCasesFailure: () => reportCasesFailure
5616
- });
5617
- function gitDirty(file) {
5618
- try {
5619
- const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
5620
- cwd: path16.dirname(file),
5621
- stdio: ["ignore", "pipe", "ignore"],
5622
- encoding: "utf8"
5623
- });
5624
- return out.trim() !== "";
5625
- } catch {
5626
- return false;
5627
- }
5628
- }
5629
- async function safeErrorDetail3(res) {
5630
- try {
5631
- const body = await res.json();
5632
- return typeof body.error === "string" ? body.error : "";
5633
- } catch {
5634
- return "";
5635
- }
5636
- }
5637
- function tokenOf(deps) {
5638
- const token = (deps.loadToken ?? loadToken)();
5639
- if (token === null) throw new NotLoggedInError();
5640
- return token;
5641
- }
5642
- async function handlePull(deps = {}) {
5643
- const cwd = deps.cwd ?? process.cwd();
5644
- const apiUrl = resolveApiUrl(deps.apiUrl);
5645
- const token = tokenOf(deps);
5646
- const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
5647
- if (fs17.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
5648
- throw new DirtyFileError(path16.relative(cwd, file) || file);
5649
- const doFetch = deps.fetchImpl ?? fetch;
5650
- const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
5651
- let res;
5652
- try {
5653
- res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
5654
- } catch {
5655
- throw new SyncNetworkError(apiUrl);
5656
- }
5657
- if (!res.ok) {
5658
- const detail = await safeErrorDetail3(res);
5659
- if (res.status === 401) throw new TokenRejectedError2();
5660
- throw new SyncHttpError(res.status, detail);
5661
- }
5662
- const text2 = await res.text();
5663
- fs17.mkdirSync(path16.dirname(file), { recursive: true });
5664
- fs17.writeFileSync(file, text2, "utf8");
5665
- return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
5666
- }
5667
- async function handlePush(deps = {}) {
5668
- const cwd = deps.cwd ?? process.cwd();
5669
- const apiUrl = resolveApiUrl(deps.apiUrl);
5670
- const token = tokenOf(deps);
5671
- const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
5672
- let text2;
5673
- try {
5674
- text2 = fs17.readFileSync(file, "utf8");
5675
- } catch {
5676
- throw new SyncFileError(
5677
- `No document to push: ${file}. Run "plune pull" first (or pass a file).`
5678
- );
5679
- }
5680
- if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
5681
- const doFetch = deps.fetchImpl ?? fetch;
5682
- let res;
5683
- try {
5684
- res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
5685
- method: "POST",
5686
- headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
5687
- body: text2
5688
- });
5689
- } catch {
5690
- throw new SyncNetworkError(apiUrl);
5691
- }
5692
- if (!res.ok) {
5693
- const detail = await safeErrorDetail3(res);
5694
- if (res.status === 401) throw new TokenRejectedError2();
5695
- throw new SyncHttpError(res.status, detail);
5696
- }
5697
- return await res.json();
5698
- }
5699
- function formatReport(report) {
5700
- const verb = (past, future) => report.dryRun ? future : past;
5701
- const lines = [
5702
- `${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
5703
- ];
5704
- for (const r of report.refused)
5705
- lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
5706
- for (const w of report.warnings)
5707
- lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
5708
- return lines.join("\n") + "\n";
5709
- }
5710
- function reportCasesFailure(err, write) {
5711
- if (err instanceof DirtyFileError) {
5712
- write(err.message + "\n");
5713
- return 3;
5714
- }
5715
- if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
5716
- write(err.message + "\n");
5717
- return 2;
5718
- }
5719
- if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
5720
- write(err.message + "\n");
5721
- return 1;
5722
- }
5723
- return null;
5724
- }
5725
- var import_node_child_process2, fs17, path16, DEFAULT_CASES_FILE, DirtyFileError;
5726
- var init_cases = __esm({
5727
- "src/cli/commands/cases.ts"() {
5728
- "use strict";
5729
- init_cjs_shims();
5730
- import_node_child_process2 = require("child_process");
5731
- fs17 = __toESM(require("fs"), 1);
5732
- path16 = __toESM(require("path"), 1);
5733
- init_api_url();
5734
- init_credentials();
5735
- init_sync();
5736
- DEFAULT_CASES_FILE = path16.join("plune", "cases.md");
5737
- DirtyFileError = class extends Error {
5738
- constructor(file) {
5739
- super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
5740
- this.name = "DirtyFileError";
5741
- }
5742
- };
5743
- }
5744
- });
5745
-
5746
- // src/cli/commands/plan.ts
5747
- var plan_exports = {};
5748
- __export(plan_exports, {
5749
- formatGrep: () => formatGrep,
5750
- grepFragment: () => grepFragment,
5751
- handlePlanGrep: () => handlePlanGrep
5752
- });
5753
- function grepFragment(c) {
5754
- const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
5755
- const segments = pathTitle?.split("#") ?? [c.title];
5756
- return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
5757
- }
5758
- function formatGrep(cases) {
5759
- if (cases.length === 0) return "(?!)";
5760
- return cases.map(grepFragment).join("|");
5761
- }
5762
- async function safeErrorDetail4(res) {
5763
- try {
5764
- const body = await res.json();
5765
- return typeof body.error === "string" ? body.error : "";
5766
- } catch {
5767
- return "";
5768
- }
5769
- }
5770
- async function handlePlanGrep(deps) {
5771
- const apiUrl = resolveApiUrl(deps.apiUrl);
5772
- const token = (deps.loadToken ?? loadToken)();
5773
- if (token === null) throw new NotLoggedInError();
5774
- const doFetch = deps.fetchImpl ?? fetch;
5775
- let res;
5776
- try {
5777
- res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
5778
- headers: { authorization: `Bearer ${token}` }
5779
- });
5780
- } catch {
5781
- throw new SyncNetworkError(apiUrl);
5782
- }
5783
- if (!res.ok) {
5784
- const detail = await safeErrorDetail4(res);
5785
- if (res.status === 401) throw new TokenRejectedError2();
5786
- throw new SyncHttpError(res.status, detail);
5787
- }
5788
- const { cases } = await res.json();
5789
- return {
5790
- pattern: formatGrep(cases),
5791
- cases: cases.length,
5792
- keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
5793
- };
5794
- }
5795
- var escape, BOUNDARY;
5796
- var init_plan = __esm({
5797
- "src/cli/commands/plan.ts"() {
5798
- "use strict";
5799
- init_cjs_shims();
5800
- init_api_url();
5801
- init_credentials();
5802
- init_sync();
5803
- escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5804
- BOUNDARY = "[ >#]+";
5805
- }
5806
- });
5807
-
5808
5924
  // src/cli.ts
5809
5925
  var cli_exports = {};
5810
5926
  __export(cli_exports, {
@@ -6014,6 +6130,32 @@ function createProgram() {
6014
6130
  await failRunCommand(err, verboseOf(command));
6015
6131
  }
6016
6132
  });
6133
+ runCommand.command("grep").argument("<run>", "The run: its id, or its key (rk_\u2026) \u2014 the one PLUNE_RUN joins by").description(
6134
+ "Print the run's automated cases as one --grep pattern for the runner (GET /v1/runs/:id/cases)"
6135
+ ).option("--failed", "Only the cases whose latest result failed or broke \u2014 a muted failure does not count", false).action(async (run, options, command) => {
6136
+ const verbose = verboseOf(command);
6137
+ const { handleRunGrep: handleRunGrep2, runGrepSummary: runGrepSummary2, RunRefError: RunRefError2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
6138
+ const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
6139
+ try {
6140
+ const out = await handleRunGrep2({ run, failed: options.failed });
6141
+ process.stdout.write(out.pattern + "\n");
6142
+ process.stderr.write(runGrepSummary2(out, options.failed));
6143
+ } catch (err) {
6144
+ if (err instanceof RunRefError2) {
6145
+ process.stderr.write(`plune: ${err.message}
6146
+ `);
6147
+ process.exitCode = err.exitCode;
6148
+ return;
6149
+ }
6150
+ const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
6151
+ if (code !== null) {
6152
+ maybeStack(err, verbose);
6153
+ process.exitCode = code;
6154
+ return;
6155
+ }
6156
+ failUnexpected(err, verbose);
6157
+ }
6158
+ });
6017
6159
  runCommand.command("report").description("Send what the reporter could not \u2014 replays .plune/pending-results.jsonl").option("--file <path>", "The fallback file to replay").action(async (options, command) => {
6018
6160
  try {
6019
6161
  const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
@@ -6026,6 +6168,13 @@ function createProgram() {
6026
6168
  "--fail-on-missing-attachments",
6027
6169
  "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",
6028
6170
  false
6171
+ ).option(
6172
+ "--upload-concurrency <n>",
6173
+ "Files uploaded at once, 1 to 32 (default 4; PLUNE_UPLOAD_CONCURRENCY sets it too). A 429 is retried PLUNE_UPLOAD_RETRIES times (default 6)",
6174
+ (v) => {
6175
+ if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 32) throw new import_commander.InvalidArgumentError("a whole number from 1 to 32.");
6176
+ return Number(v);
6177
+ }
6029
6178
  ).option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
6030
6179
  async (file, options, command) => {
6031
6180
  const [
@@ -6051,6 +6200,7 @@ function createProgram() {
6051
6200
  ...format !== void 0 ? { format } : {},
6052
6201
  ...options.key !== void 0 ? { key: options.key } : {},
6053
6202
  ...options.create ? { create: true } : {},
6203
+ ...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
6054
6204
  ...options.failOnFailures ? { failOnFailures: true } : {}
6055
6205
  });
6056
6206
  if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;