@plune-ai/cli 0.24.0 → 0.26.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/README.md +78 -7
- package/dist/cli.cjs +501 -61
- package/dist/reporter-core.cjs +126 -12
- package/dist/reporter-core.d.cts +22 -3
- package/dist/reporter-core.d.ts +22 -3
- package/dist/reporter-core.js +128 -14
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_retry = __esm({
|
|
|
512
512
|
"use strict";
|
|
513
513
|
init_cjs_shims();
|
|
514
514
|
init_errors2();
|
|
515
|
-
defaultSleep = (ms) => new Promise((
|
|
515
|
+
defaultSleep = (ms) => new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -555,22 +555,22 @@ var init_prices = __esm({
|
|
|
555
555
|
});
|
|
556
556
|
|
|
557
557
|
// src/providers/cost.ts
|
|
558
|
-
function priceFrom(price,
|
|
559
|
-
return
|
|
558
|
+
function priceFrom(price, usage2) {
|
|
559
|
+
return usage2.input_tokens / 1e3 * price.input_per_1k_usd + usage2.output_tokens / 1e3 * price.output_per_1k_usd;
|
|
560
560
|
}
|
|
561
|
-
function resolveCost(
|
|
561
|
+
function resolveCost(usage2, model, reportedCostUsd, overrides, deps = {}) {
|
|
562
562
|
const table = deps.table ?? PRICE_TABLE;
|
|
563
563
|
const warn = deps.warn ?? defaultWarn;
|
|
564
564
|
const override = overrides?.[model];
|
|
565
565
|
if (override !== void 0) {
|
|
566
|
-
return priceFrom(override,
|
|
566
|
+
return priceFrom(override, usage2);
|
|
567
567
|
}
|
|
568
568
|
if (reportedCostUsd !== void 0) {
|
|
569
569
|
return reportedCostUsd;
|
|
570
570
|
}
|
|
571
571
|
const tablePrice = table[model];
|
|
572
572
|
if (tablePrice !== void 0) {
|
|
573
|
-
return priceFrom(tablePrice,
|
|
573
|
+
return priceFrom(tablePrice, usage2);
|
|
574
574
|
}
|
|
575
575
|
warn(
|
|
576
576
|
`plune: no price for model "${model}" \u2014 reporting cost_usd=0. Set pricing["${model}"] in plune.yaml to track its cost.`
|
|
@@ -628,8 +628,8 @@ function makeAnthropicProvider(config, env, pricing) {
|
|
|
628
628
|
throw normalizeProviderError(err, apiKey, ENV_VAR);
|
|
629
629
|
}
|
|
630
630
|
},
|
|
631
|
-
estimateCost(
|
|
632
|
-
return { cost_usd: resolveCost(
|
|
631
|
+
estimateCost(usage2, reportedCostUsd) {
|
|
632
|
+
return { cost_usd: resolveCost(usage2, config.model, reportedCostUsd, pricing) };
|
|
633
633
|
}
|
|
634
634
|
};
|
|
635
635
|
}
|
|
@@ -692,8 +692,8 @@ function makeOpenAiCompatibleProvider(opts) {
|
|
|
692
692
|
throw normalizeProviderError(err, apiKey, apiKeyEnv);
|
|
693
693
|
}
|
|
694
694
|
},
|
|
695
|
-
estimateCost(
|
|
696
|
-
return { cost_usd: resolveCost(
|
|
695
|
+
estimateCost(usage2, reportedCostUsd) {
|
|
696
|
+
return { cost_usd: resolveCost(usage2, config.model, reportedCostUsd, pricing) };
|
|
697
697
|
}
|
|
698
698
|
};
|
|
699
699
|
}
|
|
@@ -796,8 +796,8 @@ function makeEstimateProvider(config, pricing) {
|
|
|
796
796
|
complete: () => Promise.reject(
|
|
797
797
|
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
798
798
|
),
|
|
799
|
-
estimateCost: (
|
|
800
|
-
cost_usd: resolveCost(
|
|
799
|
+
estimateCost: (usage2, reportedCostUsd) => ({
|
|
800
|
+
cost_usd: resolveCost(usage2, config.model, reportedCostUsd, pricing)
|
|
801
801
|
})
|
|
802
802
|
};
|
|
803
803
|
}
|
|
@@ -1716,8 +1716,8 @@ async function runRow(p) {
|
|
|
1716
1716
|
const maxTokens = p.providerConfig.max_tokens ?? DEFAULT_MAX_TOKENS2;
|
|
1717
1717
|
if (p.dryRun) {
|
|
1718
1718
|
const est = estimateUsage(prompt5, maxTokens);
|
|
1719
|
-
const
|
|
1720
|
-
return { vars: p.row.vars, output: null, cached: false, usage:
|
|
1719
|
+
const usage3 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
|
|
1720
|
+
return { vars: p.row.vars, output: null, cached: false, usage: usage3, assertions: [] };
|
|
1721
1721
|
}
|
|
1722
1722
|
const key = cacheKey({
|
|
1723
1723
|
provider: p.providerConfig.type,
|
|
@@ -1727,13 +1727,13 @@ async function runRow(p) {
|
|
|
1727
1727
|
prompt_resolved: prompt5
|
|
1728
1728
|
});
|
|
1729
1729
|
let output;
|
|
1730
|
-
let
|
|
1730
|
+
let usage2;
|
|
1731
1731
|
let cached = false;
|
|
1732
1732
|
const hit = p.noCache ? void 0 : p.cache.get(key);
|
|
1733
1733
|
if (hit !== void 0) {
|
|
1734
1734
|
output = hit.output;
|
|
1735
1735
|
cached = true;
|
|
1736
|
-
|
|
1736
|
+
usage2 = {
|
|
1737
1737
|
input_tokens: hit.usage.input_tokens,
|
|
1738
1738
|
output_tokens: hit.usage.output_tokens,
|
|
1739
1739
|
cost_usd: 0
|
|
@@ -1748,7 +1748,7 @@ async function runRow(p) {
|
|
|
1748
1748
|
prompt_resolved: prompt5
|
|
1749
1749
|
});
|
|
1750
1750
|
output = res.output;
|
|
1751
|
-
|
|
1751
|
+
usage2 = {
|
|
1752
1752
|
input_tokens: res.usage.input_tokens,
|
|
1753
1753
|
output_tokens: res.usage.output_tokens,
|
|
1754
1754
|
// Prefer the provider-reported actual cost (res.cost_usd) when present; resolveCost still
|
|
@@ -1794,7 +1794,7 @@ async function runRow(p) {
|
|
|
1794
1794
|
break;
|
|
1795
1795
|
}
|
|
1796
1796
|
}
|
|
1797
|
-
const finalUsage = { ...
|
|
1797
|
+
const finalUsage = { ...usage2, cost_usd: usage2.cost_usd + judgeCost };
|
|
1798
1798
|
return {
|
|
1799
1799
|
vars: p.row.vars,
|
|
1800
1800
|
output,
|
|
@@ -2312,17 +2312,18 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
2312
2312
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
2313
2313
|
}
|
|
2314
2314
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2315
|
-
return new Promise((
|
|
2315
|
+
return new Promise((resolve10, reject) => {
|
|
2316
2316
|
const target = new URL(url);
|
|
2317
2317
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2318
|
-
const
|
|
2318
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
2319
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
2319
2320
|
const chunks = [];
|
|
2320
2321
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
2321
2322
|
res.on("error", reject);
|
|
2322
2323
|
res.on("end", () => {
|
|
2323
2324
|
const status = res.statusCode ?? 0;
|
|
2324
2325
|
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2325
|
-
|
|
2326
|
+
resolve10({
|
|
2326
2327
|
ok: status >= 200 && status <= 299,
|
|
2327
2328
|
status,
|
|
2328
2329
|
headers: {
|
|
@@ -2341,12 +2342,12 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2341
2342
|
});
|
|
2342
2343
|
}
|
|
2343
2344
|
function nodePut(url, headers, source, idleMs) {
|
|
2344
|
-
return new Promise((
|
|
2345
|
+
return new Promise((resolve10, reject) => {
|
|
2345
2346
|
const target = new URL(url);
|
|
2346
2347
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2347
2348
|
const length = "body" in source ? source.body.length : source.size;
|
|
2348
2349
|
let answered;
|
|
2349
|
-
const settle = (err) => answered !== void 0 ?
|
|
2350
|
+
const settle = (err) => answered !== void 0 ? resolve10(answered) : reject(err);
|
|
2350
2351
|
const req = send(
|
|
2351
2352
|
target,
|
|
2352
2353
|
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
@@ -2355,7 +2356,7 @@ function nodePut(url, headers, source, idleMs) {
|
|
|
2355
2356
|
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
2356
2357
|
res.on("error", settle);
|
|
2357
2358
|
res.on("close", () => {
|
|
2358
|
-
|
|
2359
|
+
resolve10(answered);
|
|
2359
2360
|
req.destroy();
|
|
2360
2361
|
});
|
|
2361
2362
|
res.resume();
|
|
@@ -2370,14 +2371,25 @@ function nodePut(url, headers, source, idleMs) {
|
|
|
2370
2371
|
async function detailOf(res) {
|
|
2371
2372
|
try {
|
|
2372
2373
|
const body = await res.json();
|
|
2374
|
+
const q = body.quota;
|
|
2373
2375
|
return {
|
|
2376
|
+
...typeof body.shardsExpected === "number" && Number.isInteger(body.shardsExpected) ? { shardsExpected: body.shardsExpected } : {},
|
|
2374
2377
|
detail: typeof body.error === "string" ? body.error : "",
|
|
2378
|
+
...typeof q === "object" && q !== null && !Array.isArray(q) ? { quota: q } : {},
|
|
2375
2379
|
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
2376
2380
|
};
|
|
2377
2381
|
} catch {
|
|
2378
2382
|
return { detail: "" };
|
|
2379
2383
|
}
|
|
2380
2384
|
}
|
|
2385
|
+
function describeQuota(q) {
|
|
2386
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
2387
|
+
const kind = typeof q["kind"] === "string" ? q["kind"].slice(0, 60) : "limit";
|
|
2388
|
+
const [used, limit] = [num(q["used"]), num(q["limit"])];
|
|
2389
|
+
const parts = [used !== void 0 && limit !== void 0 ? `${kind}: ${used} of ${limit} used` : kind];
|
|
2390
|
+
if (typeof q["resetsAt"] === "string" && q["resetsAt"] !== "") parts.push(`resets ${q["resetsAt"].slice(0, 40)}`);
|
|
2391
|
+
return parts.join(", ");
|
|
2392
|
+
}
|
|
2381
2393
|
function classify(status) {
|
|
2382
2394
|
if (status === 401 || status === 403) return "auth";
|
|
2383
2395
|
if (status === 409) return "conflict";
|
|
@@ -2442,12 +2454,12 @@ function createClient(opts) {
|
|
|
2442
2454
|
}
|
|
2443
2455
|
const said = await detailOf(res);
|
|
2444
2456
|
const kind = classify(res.status);
|
|
2445
|
-
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
2457
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {}, ...said.shardsExpected !== void 0 ? { shardsExpected: said.shardsExpected } : {} };
|
|
2446
2458
|
if (kind !== "unavailable") break;
|
|
2447
2459
|
const said429 = parseRetryAfter(res.headers.get("retry-after"), now());
|
|
2448
|
-
if (res.status === 429 && said429 === null) {
|
|
2460
|
+
if (res.status === 429 && (said.quota !== void 0 || said429 === null)) {
|
|
2449
2461
|
last.kind = "invalid";
|
|
2450
|
-
last.detail =
|
|
2462
|
+
last.detail = `quota exceeded (${said.quota === void 0 ? "no Retry-After" : scrub(describeQuota(said.quota))})`;
|
|
2451
2463
|
if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
|
|
2452
2464
|
break;
|
|
2453
2465
|
}
|
|
@@ -2460,6 +2472,7 @@ function createClient(opts) {
|
|
|
2460
2472
|
post: (path17, body, retries) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2461
2473
|
upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2462
2474
|
del: (path17) => send("DELETE", path17),
|
|
2475
|
+
get: (path17) => send("GET", path17),
|
|
2463
2476
|
async put(url, headers, source) {
|
|
2464
2477
|
const putRetries = uploadRetries ?? 3;
|
|
2465
2478
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2494,7 +2507,7 @@ function createClient(opts) {
|
|
|
2494
2507
|
}
|
|
2495
2508
|
};
|
|
2496
2509
|
}
|
|
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;
|
|
2510
|
+
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, POOLS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2498
2511
|
var init_client = __esm({
|
|
2499
2512
|
"src/reporter-core/client.ts"() {
|
|
2500
2513
|
"use strict";
|
|
@@ -2507,6 +2520,7 @@ var init_client = __esm({
|
|
|
2507
2520
|
BACKOFF_CAP_MS = 1e4;
|
|
2508
2521
|
DEFAULT_RETRIES = 2;
|
|
2509
2522
|
RETRY_AFTER_CAP_MS = 6e4;
|
|
2523
|
+
POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
|
|
2510
2524
|
IDLE_MS = 3e5;
|
|
2511
2525
|
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2512
2526
|
codeOfError = (err) => {
|
|
@@ -2518,6 +2532,16 @@ var init_client = __esm({
|
|
|
2518
2532
|
});
|
|
2519
2533
|
|
|
2520
2534
|
// src/reporter-core/env.ts
|
|
2535
|
+
function shardOf(env) {
|
|
2536
|
+
const raw = value(env, "PLUNE_SHARD");
|
|
2537
|
+
if (raw === void 0) return {};
|
|
2538
|
+
const m = /^(\d{1,4})\/(\d{1,4})$/.exec(raw);
|
|
2539
|
+
const [k, n] = m ? [Number(m[1]), Number(m[2])] : [0, 0];
|
|
2540
|
+
if (k >= 1 && k <= n && n <= SHARDS_MAX) return { shard: { index: k, total: n } };
|
|
2541
|
+
return {
|
|
2542
|
+
error: `PLUNE_SHARD=${JSON.stringify(raw.slice(0, 40))} is not "k/N" with 1 <= k <= N <= ${SHARDS_MAX} (for example 2/4) - nothing was sent.`
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2521
2545
|
function value(env, name) {
|
|
2522
2546
|
const raw = env[name]?.trim();
|
|
2523
2547
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -2560,6 +2584,7 @@ function readEnv(env = process.env) {
|
|
|
2560
2584
|
const labels = list(env, "PLUNE_LABELS");
|
|
2561
2585
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2562
2586
|
const full = flag(env, "PLUNE_FULL_RUN");
|
|
2587
|
+
const { shard, error: shardError } = shardOf(env);
|
|
2563
2588
|
return {
|
|
2564
2589
|
config: {
|
|
2565
2590
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -2580,7 +2605,11 @@ function readEnv(env = process.env) {
|
|
|
2580
2605
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2581
2606
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
2582
2607
|
// way this process must not be the one to call it finished.
|
|
2583
|
-
|
|
2608
|
+
// `PLUNE_SHARD` implies it: the run is joined by the shared key and this process does not close it
|
|
2609
|
+
// by itself; `leaveOpen` reports the shard instead.
|
|
2610
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
2611
|
+
...shard !== void 0 ? { shard } : {},
|
|
2612
|
+
...shardError !== void 0 ? { shardError } : {},
|
|
2584
2613
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
2585
2614
|
warnings
|
|
2586
2615
|
};
|
|
@@ -2608,14 +2637,15 @@ function ciCommit(reported, env = process.env) {
|
|
|
2608
2637
|
...branch !== void 0 ? { branch } : {}
|
|
2609
2638
|
};
|
|
2610
2639
|
}
|
|
2611
|
-
var path6, UNSUPPORTED_VARS, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2640
|
+
var path6, UNSUPPORTED_VARS, SHARDS_MAX, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2612
2641
|
var init_env2 = __esm({
|
|
2613
2642
|
"src/reporter-core/env.ts"() {
|
|
2614
2643
|
"use strict";
|
|
2615
2644
|
init_cjs_shims();
|
|
2616
2645
|
path6 = __toESM(require("path"), 1);
|
|
2617
2646
|
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2618
|
-
|
|
2647
|
+
SHARDS_MAX = 1e3;
|
|
2648
|
+
UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
2619
2649
|
UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
2620
2650
|
PROVIDERS = [
|
|
2621
2651
|
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
@@ -2914,6 +2944,7 @@ function warnIfDropped(sent, run, log) {
|
|
|
2914
2944
|
}
|
|
2915
2945
|
async function startRun(passed, declared2) {
|
|
2916
2946
|
const env = readEnv();
|
|
2947
|
+
if (env.shardError !== void 0) throw new Error(env.shardError);
|
|
2917
2948
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2918
2949
|
const cfg = { ...env.config, ...stated };
|
|
2919
2950
|
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
@@ -2955,6 +2986,9 @@ async function startRun(passed, declared2) {
|
|
|
2955
2986
|
const resolved = /* @__PURE__ */ new Map();
|
|
2956
2987
|
let buffer = [];
|
|
2957
2988
|
const discoveries = /* @__PURE__ */ new Map();
|
|
2989
|
+
const held = /* @__PURE__ */ new Map();
|
|
2990
|
+
let createdWithResult = 0;
|
|
2991
|
+
let createdUndelivered = 0;
|
|
2958
2992
|
let runId = null;
|
|
2959
2993
|
let joined = false;
|
|
2960
2994
|
let offline = token === "";
|
|
@@ -3083,6 +3117,7 @@ async function startRun(passed, declared2) {
|
|
|
3083
3117
|
specRef: result.specRef,
|
|
3084
3118
|
rawStatus: result.rawStatus
|
|
3085
3119
|
});
|
|
3120
|
+
held.set(result.keys[0].value, result);
|
|
3086
3121
|
}
|
|
3087
3122
|
async function offer() {
|
|
3088
3123
|
if (discoveries.size === 0 || offline || runId === null) return;
|
|
@@ -3101,7 +3136,24 @@ async function startRun(passed, declared2) {
|
|
|
3101
3136
|
return;
|
|
3102
3137
|
}
|
|
3103
3138
|
stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
|
|
3104
|
-
|
|
3139
|
+
const made = out.body.results.filter((r) => r?.outcome === "created");
|
|
3140
|
+
stats.created += made.length;
|
|
3141
|
+
const submissions = [];
|
|
3142
|
+
const filesByKey = /* @__PURE__ */ new Map();
|
|
3143
|
+
for (const r of made) {
|
|
3144
|
+
const pending = typeof r.id === "string" && r.id !== "" ? held.get(r.key?.value) : void 0;
|
|
3145
|
+
if (pending === void 0) continue;
|
|
3146
|
+
held.delete(r.key.value);
|
|
3147
|
+
const { keys: _keys, files, ...rest } = pending;
|
|
3148
|
+
submissions.push({ ...rest, testCaseId: r.id });
|
|
3149
|
+
if (files !== void 0) filesByKey.set(pending.resultKey, files);
|
|
3150
|
+
}
|
|
3151
|
+
if (submissions.length > 0) {
|
|
3152
|
+
const before = stats.deferred;
|
|
3153
|
+
await send(submissions, filesByKey);
|
|
3154
|
+
createdWithResult += submissions.length;
|
|
3155
|
+
createdUndelivered += stats.deferred - before;
|
|
3156
|
+
}
|
|
3105
3157
|
}
|
|
3106
3158
|
}
|
|
3107
3159
|
function skip(kind, name, why) {
|
|
@@ -3154,8 +3206,8 @@ async function startRun(passed, declared2) {
|
|
|
3154
3206
|
return skip(kind, job.name, "missing");
|
|
3155
3207
|
}
|
|
3156
3208
|
}
|
|
3157
|
-
const
|
|
3158
|
-
if (
|
|
3209
|
+
const refusal2 = kind === "text" ? textRefusal(bytes) : void 0;
|
|
3210
|
+
if (refusal2 !== void 0) return skip(kind, job.name, refusal2);
|
|
3159
3211
|
requests += 1;
|
|
3160
3212
|
const out = await client.upload(
|
|
3161
3213
|
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
@@ -3187,7 +3239,7 @@ async function startRun(passed, declared2) {
|
|
|
3187
3239
|
let free = uploadsAtOnce;
|
|
3188
3240
|
const waiting = [];
|
|
3189
3241
|
async function gated(work) {
|
|
3190
|
-
if (free === 0) await new Promise((
|
|
3242
|
+
if (free === 0) await new Promise((resolve10) => waiting.push(resolve10));
|
|
3191
3243
|
else free -= 1;
|
|
3192
3244
|
try {
|
|
3193
3245
|
return await work();
|
|
@@ -3439,9 +3491,9 @@ async function startRun(passed, declared2) {
|
|
|
3439
3491
|
skip(kind, job.name, "missing");
|
|
3440
3492
|
continue;
|
|
3441
3493
|
}
|
|
3442
|
-
const
|
|
3443
|
-
if (
|
|
3444
|
-
skip(kind, job.name,
|
|
3494
|
+
const refusal2 = kind === "text" ? textRefusal(data) : void 0;
|
|
3495
|
+
if (refusal2 !== void 0) {
|
|
3496
|
+
skip(kind, job.name, refusal2);
|
|
3445
3497
|
continue;
|
|
3446
3498
|
}
|
|
3447
3499
|
body.push({ resultId: job.id, name: job.name, contentType: job.type, data: data.toString("base64") });
|
|
@@ -3508,7 +3560,29 @@ async function startRun(passed, declared2) {
|
|
|
3508
3560
|
else await uploadBatched(jobs);
|
|
3509
3561
|
uploadMs += Date.now() - started;
|
|
3510
3562
|
}
|
|
3563
|
+
const sending = /* @__PURE__ */ new Set();
|
|
3564
|
+
let sendError;
|
|
3565
|
+
let answered = false;
|
|
3566
|
+
async function dispatch(work) {
|
|
3567
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
3568
|
+
const task = work().catch((err) => {
|
|
3569
|
+
sendError ??= err;
|
|
3570
|
+
}).finally(() => sending.delete(task));
|
|
3571
|
+
sending.add(task);
|
|
3572
|
+
}
|
|
3573
|
+
async function settle() {
|
|
3574
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
3575
|
+
if (sendError !== void 0) {
|
|
3576
|
+
const err = sendError;
|
|
3577
|
+
sendError = void 0;
|
|
3578
|
+
throw err;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3511
3581
|
async function flush() {
|
|
3582
|
+
await flushBuffer();
|
|
3583
|
+
await settle();
|
|
3584
|
+
}
|
|
3585
|
+
async function flushBuffer() {
|
|
3512
3586
|
if (buffer.length === 0) return;
|
|
3513
3587
|
const pending = buffer;
|
|
3514
3588
|
buffer = [];
|
|
@@ -3516,6 +3590,9 @@ async function startRun(passed, declared2) {
|
|
|
3516
3590
|
defer(pending);
|
|
3517
3591
|
return;
|
|
3518
3592
|
}
|
|
3593
|
+
await dispatch(() => deliver(pending));
|
|
3594
|
+
}
|
|
3595
|
+
async function deliver(pending) {
|
|
3519
3596
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
3520
3597
|
const submissions = [];
|
|
3521
3598
|
const unanswered = [];
|
|
@@ -3538,6 +3615,9 @@ async function startRun(passed, declared2) {
|
|
|
3538
3615
|
progress();
|
|
3539
3616
|
return;
|
|
3540
3617
|
}
|
|
3618
|
+
await send(submissions, filesByKey);
|
|
3619
|
+
}
|
|
3620
|
+
async function send(submissions, filesByKey) {
|
|
3541
3621
|
for (const part of packed(submissions)) {
|
|
3542
3622
|
if (offline) {
|
|
3543
3623
|
defer(part);
|
|
@@ -3553,6 +3633,7 @@ async function startRun(passed, declared2) {
|
|
|
3553
3633
|
for (const k of told) stats[k] += counts[k];
|
|
3554
3634
|
progress();
|
|
3555
3635
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
3636
|
+
answered = true;
|
|
3556
3637
|
continue;
|
|
3557
3638
|
}
|
|
3558
3639
|
if (out.ok) {
|
|
@@ -3581,7 +3662,10 @@ async function startRun(passed, declared2) {
|
|
|
3581
3662
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
3582
3663
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
3583
3664
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
3584
|
-
if (stats.created > 0)
|
|
3665
|
+
if (stats.created > 0) {
|
|
3666
|
+
const recorded = createdWithResult === stats.created && createdUndelivered === 0;
|
|
3667
|
+
parts.push(`${stats.created} added as cases (trusted source)${recorded ? " and their results recorded" : ""}`);
|
|
3668
|
+
}
|
|
3585
3669
|
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
3586
3670
|
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
3587
3671
|
if (bucketSeen) {
|
|
@@ -3611,6 +3695,7 @@ async function startRun(passed, declared2) {
|
|
|
3611
3695
|
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
3612
3696
|
}
|
|
3613
3697
|
}
|
|
3698
|
+
const shardLines = [];
|
|
3614
3699
|
return {
|
|
3615
3700
|
get runId() {
|
|
3616
3701
|
return runId;
|
|
@@ -3623,7 +3708,7 @@ async function startRun(passed, declared2) {
|
|
|
3623
3708
|
},
|
|
3624
3709
|
async add(result) {
|
|
3625
3710
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
3626
|
-
if (buffer.length >= batchSize) await
|
|
3711
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
3627
3712
|
},
|
|
3628
3713
|
flush,
|
|
3629
3714
|
async finish(reason) {
|
|
@@ -3648,19 +3733,47 @@ async function startRun(passed, declared2) {
|
|
|
3648
3733
|
summarise();
|
|
3649
3734
|
return frozen;
|
|
3650
3735
|
},
|
|
3651
|
-
|
|
3652
|
-
|
|
3736
|
+
shardLines,
|
|
3737
|
+
async leaveOpen(opts) {
|
|
3738
|
+
if (done) return void 0;
|
|
3653
3739
|
done = true;
|
|
3654
3740
|
await flush();
|
|
3655
3741
|
await offer();
|
|
3656
|
-
|
|
3742
|
+
let frozen;
|
|
3743
|
+
const after = [];
|
|
3744
|
+
const shard = env.shard;
|
|
3745
|
+
let counted = false;
|
|
3746
|
+
let mismatch;
|
|
3747
|
+
if (runId !== null && shard !== void 0 && !offline) {
|
|
3748
|
+
const out = await client.post(`/v1/runs/${runId}/events`, { event: "finish", shardIndex: shard.index, shardsExpected: shard.total });
|
|
3749
|
+
if (out.ok && typeof out.body?.shardsReported === "number") {
|
|
3750
|
+
counted = true;
|
|
3751
|
+
frozen = out.body.summary ?? void 0;
|
|
3752
|
+
const n = out.body.shardsExpected ?? shard.total;
|
|
3753
|
+
if (out.body.run?.statusEvent === "finished") after.push("plune: run closed");
|
|
3754
|
+
after.push(`plune: shard ${shard.index} of ${shard.total} done \u2014 ${out.body.shardsReported} of ${n} shards reported`);
|
|
3755
|
+
} else if (out.ok || out.kind === "invalid") {
|
|
3756
|
+
log('plune: the platform does not support shard auto-close yet \u2014 the run is left open for "plune run finish".');
|
|
3757
|
+
} else if (out.code === "shards_mismatch") {
|
|
3758
|
+
mismatch = new Error(
|
|
3759
|
+
`PLUNE_SHARD=${shard.index}/${shard.total} says ${shard.total} shards, but run ${runId} already expects ${out.shardsExpected ?? "a different number of"} \u2014 every shard of one run must use the same N. The results are uploaded; the run is left open.`
|
|
3760
|
+
);
|
|
3761
|
+
} else {
|
|
3762
|
+
log(`plune: could not report shard ${shard.index} of ${shard.total} to run ${runId} (${out.detail || out.kind}) \u2014 the results are uploaded; close the run with "plune run finish ${runId}".`);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
if (runId !== null && !counted) {
|
|
3657
3766
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
3658
3767
|
}
|
|
3659
3768
|
summarise();
|
|
3769
|
+
shardLines.push(...after);
|
|
3770
|
+
if (opts?.holdShardLines !== true) for (const line of after) log(line);
|
|
3771
|
+
if (mismatch !== void 0) throw mismatch;
|
|
3772
|
+
return frozen;
|
|
3660
3773
|
}
|
|
3661
3774
|
};
|
|
3662
3775
|
}
|
|
3663
|
-
var import_node_crypto3, import_node_fs5, 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;
|
|
3776
|
+
var import_node_crypto3, import_node_fs5, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, SEND_CONCURRENCY, 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;
|
|
3664
3777
|
var init_session = __esm({
|
|
3665
3778
|
"src/reporter-core/session.ts"() {
|
|
3666
3779
|
"use strict";
|
|
@@ -3680,6 +3793,7 @@ var init_session = __esm({
|
|
|
3680
3793
|
BATCH_MAX = 500;
|
|
3681
3794
|
DISCOVER_MAX = 500;
|
|
3682
3795
|
BATCH_DEFAULT = 500;
|
|
3796
|
+
SEND_CONCURRENCY = 4;
|
|
3683
3797
|
EXPECTED_MAX = 1e4;
|
|
3684
3798
|
KEY_MAX = 1024;
|
|
3685
3799
|
TITLE_MAX = 300;
|
|
@@ -3843,14 +3957,14 @@ async function handleRunExec(options) {
|
|
|
3843
3957
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
3844
3958
|
const run = await handleRunStart(options);
|
|
3845
3959
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
3846
|
-
const code = await new Promise((
|
|
3960
|
+
const code = await new Promise((resolve10) => {
|
|
3847
3961
|
const child = spawnImpl(command, args, {
|
|
3848
3962
|
stdio: "inherit",
|
|
3849
3963
|
shell: process.platform === "win32",
|
|
3850
3964
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
3851
3965
|
});
|
|
3852
|
-
child.on("close", (exitCode) =>
|
|
3853
|
-
child.on("error", () =>
|
|
3966
|
+
child.on("close", (exitCode) => resolve10(exitCode ?? 1));
|
|
3967
|
+
child.on("error", () => resolve10(1));
|
|
3854
3968
|
});
|
|
3855
3969
|
return handleRunFinish(run.id, options, code);
|
|
3856
3970
|
}
|
|
@@ -5302,7 +5416,7 @@ async function handleRunImport(options) {
|
|
|
5302
5416
|
);
|
|
5303
5417
|
for (const result2 of results) await session.add(result2);
|
|
5304
5418
|
let frozen;
|
|
5305
|
-
if (env.keepOpen) await session.leaveOpen();
|
|
5419
|
+
if (env.keepOpen) frozen = await session.leaveOpen({ holdShardLines: true });
|
|
5306
5420
|
else frozen = await session.finish();
|
|
5307
5421
|
const stats = session.stats;
|
|
5308
5422
|
const result = {
|
|
@@ -5333,13 +5447,14 @@ async function handleRunImport(options) {
|
|
|
5333
5447
|
};
|
|
5334
5448
|
for (const line of describe(result, apiUrl)) write(line);
|
|
5335
5449
|
if (options.failOnFailures === true) {
|
|
5336
|
-
if (env.keepOpen) {
|
|
5450
|
+
if (env.keepOpen && frozen === void 0) {
|
|
5337
5451
|
write("plune: the run is left open for the jobs still reporting, so there is no verdict yet \u2014 gate on the step that finishes it.");
|
|
5338
5452
|
}
|
|
5339
5453
|
const gate = gateExit(frozen);
|
|
5340
5454
|
write(gate.line);
|
|
5341
5455
|
result.gate = gate.code;
|
|
5342
5456
|
}
|
|
5457
|
+
for (const line of session.shardLines) write(line);
|
|
5343
5458
|
return result;
|
|
5344
5459
|
}
|
|
5345
5460
|
function runUrl2(apiUrl, id) {
|
|
@@ -6195,6 +6310,304 @@ var init_ingest = __esm({
|
|
|
6195
6310
|
}
|
|
6196
6311
|
});
|
|
6197
6312
|
|
|
6313
|
+
// src/cli/commands/requirements-import.ts
|
|
6314
|
+
var requirements_import_exports = {};
|
|
6315
|
+
__export(requirements_import_exports, {
|
|
6316
|
+
MAX_MARKDOWN_BYTES: () => MAX_MARKDOWN_BYTES,
|
|
6317
|
+
RequirementsImportError: () => RequirementsImportError,
|
|
6318
|
+
buildImportBody: () => buildImportBody,
|
|
6319
|
+
formatDiscover: () => formatDiscover,
|
|
6320
|
+
formatReport: () => formatReport2,
|
|
6321
|
+
handleRequirementsImport: () => handleRequirementsImport,
|
|
6322
|
+
parseMap: () => parseMap,
|
|
6323
|
+
parseTableSpec: () => parseTableSpec,
|
|
6324
|
+
reportRequirementsImportFailure: () => reportRequirementsImportFailure
|
|
6325
|
+
});
|
|
6326
|
+
function parseMap(flag2, raw) {
|
|
6327
|
+
const out = {};
|
|
6328
|
+
for (const pair of raw.split(",")) {
|
|
6329
|
+
if (pair.trim() === "") continue;
|
|
6330
|
+
const at = pair.indexOf("=");
|
|
6331
|
+
const from = at < 0 ? "" : pair.slice(0, at).trim();
|
|
6332
|
+
const to = at < 0 ? "" : pair.slice(at + 1).trim();
|
|
6333
|
+
if (from === "" || to === "") usage(`${flag2}: "${pair.trim()}" is not Value=Value. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6334
|
+
out[from] = to;
|
|
6335
|
+
}
|
|
6336
|
+
if (Object.keys(out).length === 0) usage(`${flag2} is empty. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6337
|
+
return out;
|
|
6338
|
+
}
|
|
6339
|
+
function parseTableSpec(spec) {
|
|
6340
|
+
const at = spec.indexOf(":");
|
|
6341
|
+
const index = (at < 0 ? spec : spec.slice(0, at)).trim();
|
|
6342
|
+
if (!/^\d+$/.test(index)) usage(`--table "${spec}": "${index}" is not a table number. ${SPEC_HELP}`);
|
|
6343
|
+
const t = { table: Number(index) };
|
|
6344
|
+
for (const pair of (at < 0 ? "" : spec.slice(at + 1)).split(",")) {
|
|
6345
|
+
if (pair.trim() === "") continue;
|
|
6346
|
+
const eq = pair.indexOf("=");
|
|
6347
|
+
const name = (eq < 0 ? pair : pair.slice(0, eq)).trim().toLowerCase();
|
|
6348
|
+
const value2 = eq < 0 ? "" : pair.slice(eq + 1).trim();
|
|
6349
|
+
const field = TABLE_SPEC_KEYS[name];
|
|
6350
|
+
if (field === void 0) return usage(`--table "${spec}": "${name}" is not a column name. Use ${Object.keys(TABLE_SPEC_KEYS).join(", ")}.`);
|
|
6351
|
+
if (value2 === "") usage(`--table "${spec}": "${name}" has no value. ${SPEC_HELP}`);
|
|
6352
|
+
if (field in t) usage(`--table "${spec}": "${name}" is given twice.`);
|
|
6353
|
+
if (field === "fixedCategory") oneOf("fixed-category", value2, ["functional", "non-functional", "other"]);
|
|
6354
|
+
if (field === "fixedType") oneOf("fixed-type", value2, ["user_story", "feature", "epic"]);
|
|
6355
|
+
t[field] = value2;
|
|
6356
|
+
}
|
|
6357
|
+
if (t["titleColumn"] !== void 0 && t["textColumn"] !== void 0) usage(`--table "${spec}": give title= or text=, not both.`);
|
|
6358
|
+
if (t["titleColumn"] === void 0 && t["textColumn"] === void 0) usage(`--table "${spec}": each table needs title=<header> or text=<header>.`);
|
|
6359
|
+
return t;
|
|
6360
|
+
}
|
|
6361
|
+
function buildImportBody(f) {
|
|
6362
|
+
const tables = f.table === void 0 ? [] : Array.isArray(f.table) ? f.table : [f.table];
|
|
6363
|
+
const columnFlags = COLUMN_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6364
|
+
const sectionFlags = SECTION_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6365
|
+
const anyMapping = tables.length > 0 || f.sections !== void 0 || columnFlags.length > 0 || sectionFlags.length > 0 || f.mapPriority !== void 0 || f.mapCategory !== void 0 || f.type !== void 0;
|
|
6366
|
+
if (!anyMapping) return null;
|
|
6367
|
+
if (tables.length > 0 && f.sections !== void 0) usage("Give --table or --sections, not both: one import reads one table or one section.");
|
|
6368
|
+
if (tables.length === 0 && f.sections === void 0) {
|
|
6369
|
+
usage("The mapping flags need --table <n> (a table) or --sections <heading|n> (a section). Run without flags to see which the file has.");
|
|
6370
|
+
}
|
|
6371
|
+
const body = {};
|
|
6372
|
+
if (f.mapPriority !== void 0) body["priorityMap"] = parseMap("--map-priority", f.mapPriority);
|
|
6373
|
+
if (f.type !== void 0) body["defaultType"] = oneOf("--type", f.type, ["user_story", "feature", "epic"]);
|
|
6374
|
+
if (tables.length > 1 || tables.some((t) => t.includes(":"))) {
|
|
6375
|
+
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6376
|
+
if (columnFlags.length > 0) {
|
|
6377
|
+
usage(`${columnFlags.map(([, n]) => n).join(", ")} cannot be used with --table "<n>:...": put the column inside each --table, e.g. --table "3:key=ID,text=Requirement".`);
|
|
6378
|
+
}
|
|
6379
|
+
const specs = tables.map(parseTableSpec);
|
|
6380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6381
|
+
for (const t of specs) {
|
|
6382
|
+
if (seen.has(t["table"])) usage(`Table ${String(t["table"])} is given twice in --table.`);
|
|
6383
|
+
seen.add(t["table"]);
|
|
6384
|
+
}
|
|
6385
|
+
body["mode"] = "table";
|
|
6386
|
+
const categoryMap = f.mapCategory !== void 0 ? parseMap("--map-category", f.mapCategory) : void 0;
|
|
6387
|
+
body["tables"] = specs.map((t) => categoryMap === void 0 ? t : { ...t, categoryMap });
|
|
6388
|
+
} else if (tables.length === 1) {
|
|
6389
|
+
const only = tables[0];
|
|
6390
|
+
if (!/^\d+$/.test(only)) usage(`--table is the number of the table in the list (0, 1, ...), not "${only}".`);
|
|
6391
|
+
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6392
|
+
if (f.titleColumn !== void 0 && f.textColumn !== void 0) usage("Give --title-column or --text-column, not both.");
|
|
6393
|
+
if (f.titleColumn === void 0 && f.textColumn === void 0) usage("--table needs --title-column <header> or --text-column <header>.");
|
|
6394
|
+
body["mode"] = "table";
|
|
6395
|
+
body["table"] = Number(only);
|
|
6396
|
+
for (const [k, , key] of columnFlags) body[key] = f[k];
|
|
6397
|
+
if (f.mapCategory !== void 0) body["categoryMap"] = parseMap("--map-category", f.mapCategory);
|
|
6398
|
+
} else {
|
|
6399
|
+
if (columnFlags.length > 0) usage(`${columnFlags.map(([, n]) => n).join(", ")} ${columnFlags.length > 1 ? "are" : "is"} for --table, not --sections.`);
|
|
6400
|
+
if (f.mapCategory !== void 0) usage("--map-category is for --table, not --sections.");
|
|
6401
|
+
body["mode"] = "sections";
|
|
6402
|
+
const section = (f.sections ?? "").trim();
|
|
6403
|
+
if (section === "") usage("--sections needs a heading or its number in the list.");
|
|
6404
|
+
body["section"] = /^\d+$/.test(section) ? Number(section) : section;
|
|
6405
|
+
if (f.unit !== void 0) body["unit"] = oneOf("--unit", f.unit, ["bullets", "subheadings"]);
|
|
6406
|
+
if (f.keyPrefix !== void 0) body["keyPrefix"] = f.keyPrefix;
|
|
6407
|
+
if (f.priorityFrom !== void 0) body["priorityFrom"] = oneOf("--priority-from", f.priorityFrom, ["subheading"]);
|
|
6408
|
+
}
|
|
6409
|
+
return body;
|
|
6410
|
+
}
|
|
6411
|
+
function readMarkdown(cwd, file) {
|
|
6412
|
+
const target = (0, import_node_path4.resolve)(cwd, file);
|
|
6413
|
+
let size;
|
|
6414
|
+
try {
|
|
6415
|
+
const st = (0, import_node_fs7.statSync)(target);
|
|
6416
|
+
if (!st.isFile()) return usage(`${target} is not a file.`);
|
|
6417
|
+
size = st.size;
|
|
6418
|
+
} catch {
|
|
6419
|
+
return usage(`File not found: ${target}`);
|
|
6420
|
+
}
|
|
6421
|
+
if (size > MAX_MARKDOWN_BYTES) {
|
|
6422
|
+
return usage(`${target} is ${size} bytes; the platform reads at most ${MAX_MARKDOWN_BYTES} bytes (1 MiB). Nothing was sent. Split the file.`);
|
|
6423
|
+
}
|
|
6424
|
+
let text2;
|
|
6425
|
+
try {
|
|
6426
|
+
text2 = new TextDecoder("utf-8", { fatal: true }).decode((0, import_node_fs7.readFileSync)(target));
|
|
6427
|
+
} catch {
|
|
6428
|
+
return usage(`${target} is not valid UTF-8. Save it as UTF-8 and try again.`);
|
|
6429
|
+
}
|
|
6430
|
+
text2 = text2.replace(/^/, "");
|
|
6431
|
+
if (text2.trim() === "") usage(`${target} is empty.`);
|
|
6432
|
+
return text2;
|
|
6433
|
+
}
|
|
6434
|
+
function refusal(out, apiUrl, projectId) {
|
|
6435
|
+
const d = out.detail;
|
|
6436
|
+
const said = d === "" ? "" : ` (${d})`;
|
|
6437
|
+
switch (out.status) {
|
|
6438
|
+
case null:
|
|
6439
|
+
return new RequirementsImportError(`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`, 1);
|
|
6440
|
+
case 401:
|
|
6441
|
+
return new RequirementsImportError(`Your API token was rejected (revoked or invalid). ${whereToGetAToken(apiUrl)} Then run "plune login".`, 2);
|
|
6442
|
+
case 403:
|
|
6443
|
+
return new RequirementsImportError(`The platform refused: ${d || "read-only member"}. Importing needs an owner or a member of the project.`, 1);
|
|
6444
|
+
case 404:
|
|
6445
|
+
return /^project '.*' not found$/.test(d) ? new RequirementsImportError(`Project ${projectId} was not found for this token. A token only works in the project it was minted in; pass --project <id> or use a token from that project.`, 1) : new RequirementsImportError(SUPPORT, 1);
|
|
6446
|
+
case 409:
|
|
6447
|
+
return new RequirementsImportError(`A requirement key was taken by another change while the import ran. Nothing was written; run it again${said}.`, 1);
|
|
6448
|
+
case 413:
|
|
6449
|
+
return new RequirementsImportError(`The platform refused the file as too large${said}. Nothing was written.`, 1);
|
|
6450
|
+
case 400:
|
|
6451
|
+
return new RequirementsImportError(`The platform refused the mapping${said}.`, 1);
|
|
6452
|
+
default:
|
|
6453
|
+
return new RequirementsImportError(`The platform answered HTTP ${out.status}${said}. Nothing was written.`, 1);
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
async function resolveProject(client, explicit, apiUrl) {
|
|
6457
|
+
const given = (explicit ?? process.env["PLUNE_PROJECT"])?.trim();
|
|
6458
|
+
if (given !== void 0 && given !== "") return given;
|
|
6459
|
+
const out = await client.get("/v1/projects");
|
|
6460
|
+
if (!out.ok) throw refusal(out, apiUrl, "");
|
|
6461
|
+
const projects = out.body.projects ?? [];
|
|
6462
|
+
if (projects.length === 1 && projects[0] !== void 0) return projects[0].id;
|
|
6463
|
+
const list2 = projects.map((p) => `${p.id}${p.name ? ` (${p.name})` : ""}`).join(", ");
|
|
6464
|
+
return usage(
|
|
6465
|
+
projects.length === 0 ? "No project found for this token. Pass --project <id>." : `This account is in ${projects.length} projects (${list2}). A token only works in the project it was minted in: pass --project <id> (or set PLUNE_PROJECT).`
|
|
6466
|
+
);
|
|
6467
|
+
}
|
|
6468
|
+
async function handleRequirementsImport(deps) {
|
|
6469
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
6470
|
+
const mapping = buildImportBody(deps);
|
|
6471
|
+
const markdown = readMarkdown(deps.cwd ?? process.cwd(), deps.file);
|
|
6472
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
6473
|
+
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
6474
|
+
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
6475
|
+
const projectId = await resolveProject(client, deps.project, apiUrl);
|
|
6476
|
+
const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
|
|
6477
|
+
if (mapping === null) {
|
|
6478
|
+
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
6479
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId);
|
|
6480
|
+
return { step: "discover", result: out2.body };
|
|
6481
|
+
}
|
|
6482
|
+
const out = await client.post(base, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
6483
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId);
|
|
6484
|
+
return { step: "import", report: out.body };
|
|
6485
|
+
}
|
|
6486
|
+
function formatDiscover(file, r) {
|
|
6487
|
+
const out = [`${file}: ${plural(r.tables.length, "table")}, ${plural(r.sections.length, "section")}`];
|
|
6488
|
+
if (r.frontmatter.keys.length > 0) out.push(`Frontmatter keys: ${r.frontmatter.keys.join(", ")}`);
|
|
6489
|
+
out.push("", "Tables");
|
|
6490
|
+
if (r.tables.length === 0) out.push(" (none)");
|
|
6491
|
+
for (const t of r.tables) {
|
|
6492
|
+
const mark = t.unreadable !== void 0 ? ` cannot be read: ${t.unreadable}` : t.looksLikeRequirements ? " <- looks like requirements" : "";
|
|
6493
|
+
out.push(` #${t.index} line ${t.line} ${t.heading === null ? "(no heading)" : `"${flat(t.heading)}"`} ${cut2(t.headers.map(flat).join(" | "), 100)} ${plural(t.rowCount, "row")}${mark}`);
|
|
6494
|
+
}
|
|
6495
|
+
out.push("", "Sections");
|
|
6496
|
+
if (r.sections.length === 0) out.push(" (none)");
|
|
6497
|
+
for (const s of r.sections) {
|
|
6498
|
+
out.push(` #${s.index} line ${s.line} ${"#".repeat(s.level)} ${flat(s.heading)} ${plural(s.bulletCount, "bullet")}, ${plural(s.subheadingCount, "sub-heading")}`);
|
|
6499
|
+
}
|
|
6500
|
+
if (r.truncated) out.push("", "The file has more headings or tables than are listed here.");
|
|
6501
|
+
out.push(
|
|
6502
|
+
"",
|
|
6503
|
+
"Pick one and map it (a dry run, nothing is written):",
|
|
6504
|
+
` plune requirements import ${file} --table <n> --key-column <header> --title-column <header>`,
|
|
6505
|
+
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings`,
|
|
6506
|
+
"Several tables in one run, each with its own columns:",
|
|
6507
|
+
` plune requirements import ${file} --table "<n>:key=<header>,text=<header>,fixed-category=functional" --table "<m>:key=<header>,text=<header>"`
|
|
6508
|
+
);
|
|
6509
|
+
return out.join("\n") + "\n";
|
|
6510
|
+
}
|
|
6511
|
+
function formatReport2(report) {
|
|
6512
|
+
const s = report.summary;
|
|
6513
|
+
const out = [`create ${s.create} \xB7 update ${s.update} \xB7 unchanged ${s.unchanged} \xB7 skip ${s.skip}`];
|
|
6514
|
+
if (report.emptyTemplate === true) {
|
|
6515
|
+
const said = report.message !== void 0 && !/empty template/i.test(report.message) ? ` ${flat(report.message)}` : "";
|
|
6516
|
+
out.push("", `this file is an empty template: every row is a placeholder, nothing to import.${said}`);
|
|
6517
|
+
}
|
|
6518
|
+
for (const action of ["create", "update", "skip", "unchanged"]) {
|
|
6519
|
+
const rows = report.items.filter((i) => i.action === action);
|
|
6520
|
+
if (rows.length === 0) continue;
|
|
6521
|
+
out.push("", `${action} (${rows.length})`);
|
|
6522
|
+
for (const i of rows.slice(0, SHOWN)) {
|
|
6523
|
+
const changes = i.changes !== void 0 && i.changes.length > 0 ? `; changes: ${i.changes.join(", ")}` : "";
|
|
6524
|
+
const notes = i.notes !== void 0 && i.notes.length > 0 ? `; note: ${i.notes.join("; ")}` : "";
|
|
6525
|
+
const from = i.table === void 0 ? "" : `table #${i.table}${i.tableHeading ? ` "${cut2(flat(i.tableHeading), 40)}"` : ""} `;
|
|
6526
|
+
out.push(` ${from}line ${i.line} ${i.key ?? "-"} ${cut2(flat(i.title), 80)} - ${flat(i.reason)}${changes}${notes}`);
|
|
6527
|
+
}
|
|
6528
|
+
if (rows.length > SHOWN) out.push(` ... and ${rows.length - SHOWN} more (--json lists every row)`);
|
|
6529
|
+
}
|
|
6530
|
+
for (const [label, values] of [["Priority values", report.values.priority], ["Category values", report.values.category]]) {
|
|
6531
|
+
if (values.length === 0) continue;
|
|
6532
|
+
out.push("", label);
|
|
6533
|
+
for (const v of values.slice(0, SHOWN)) out.push(` ${v.value} x${v.count} -> ${v.mappedTo ?? "(no mapping: kept as written)"}`);
|
|
6534
|
+
}
|
|
6535
|
+
const n = report.notImported;
|
|
6536
|
+
if (n.tables.length + n.sections.length + n.frontmatterKeys.length > 0) {
|
|
6537
|
+
out.push("", "Not imported");
|
|
6538
|
+
for (const t of n.tables) out.push(` table #${t.index} ${t.heading === null ? "" : `"${flat(t.heading)}" `}(${cut2(t.headers.map(flat).join(" | "), 80)}; ${plural(t.rowCount, "row")})`);
|
|
6539
|
+
for (const x of n.sections) out.push(` section #${x.index} "${flat(x.heading)}" (${plural(x.bulletCount, "bullet")})`);
|
|
6540
|
+
if (n.frontmatterKeys.length > 0) out.push(` frontmatter keys: ${n.frontmatterKeys.join(", ")}`);
|
|
6541
|
+
}
|
|
6542
|
+
out.push("", report.applied ? `applied \u2014 ${plural(s.create, "requirement")} created, ${s.update} updated.` : "dry run \u2014 nothing was written; add --yes to apply");
|
|
6543
|
+
return out.join("\n") + "\n";
|
|
6544
|
+
}
|
|
6545
|
+
function reportRequirementsImportFailure(err, write) {
|
|
6546
|
+
if (!(err instanceof RequirementsImportError)) return null;
|
|
6547
|
+
write(err.message + "\n");
|
|
6548
|
+
return err.exitCode;
|
|
6549
|
+
}
|
|
6550
|
+
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, TABLE_SPEC_KEYS, SPEC_HELP, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6551
|
+
var init_requirements_import = __esm({
|
|
6552
|
+
"src/cli/commands/requirements-import.ts"() {
|
|
6553
|
+
"use strict";
|
|
6554
|
+
init_cjs_shims();
|
|
6555
|
+
import_node_fs7 = require("fs");
|
|
6556
|
+
import_node_path4 = require("path");
|
|
6557
|
+
init_client();
|
|
6558
|
+
init_api_url();
|
|
6559
|
+
init_credentials();
|
|
6560
|
+
MAX_MARKDOWN_BYTES = 1048576;
|
|
6561
|
+
RequirementsImportError = class extends Error {
|
|
6562
|
+
constructor(message, exitCode) {
|
|
6563
|
+
super(message);
|
|
6564
|
+
this.exitCode = exitCode;
|
|
6565
|
+
this.name = "RequirementsImportError";
|
|
6566
|
+
}
|
|
6567
|
+
exitCode;
|
|
6568
|
+
};
|
|
6569
|
+
usage = (message) => {
|
|
6570
|
+
throw new RequirementsImportError(message, 2);
|
|
6571
|
+
};
|
|
6572
|
+
COLUMN_FLAGS = [
|
|
6573
|
+
["keyColumn", "--key-column", "keyColumn"],
|
|
6574
|
+
["titleColumn", "--title-column", "titleColumn"],
|
|
6575
|
+
["textColumn", "--text-column", "textColumn"],
|
|
6576
|
+
["descriptionColumn", "--description-column", "descriptionColumn"],
|
|
6577
|
+
["priorityColumn", "--priority-column", "priorityColumn"],
|
|
6578
|
+
["categoryColumn", "--category-column", "categoryColumn"],
|
|
6579
|
+
["typeColumn", "--type-column", "typeColumn"],
|
|
6580
|
+
["statusColumn", "--status-column", "statusColumn"],
|
|
6581
|
+
["criteriaColumn", "--criteria-column", "acceptanceCriteriaColumn"]
|
|
6582
|
+
];
|
|
6583
|
+
SECTION_FLAGS = [
|
|
6584
|
+
["unit", "--unit"],
|
|
6585
|
+
["keyPrefix", "--key-prefix"],
|
|
6586
|
+
["priorityFrom", "--priority-from"]
|
|
6587
|
+
];
|
|
6588
|
+
oneOf = (flag2, value2, allowed) => allowed.includes(value2) ? value2 : usage(`${flag2} must be ${allowed.join(" or ")}, not "${value2}".`);
|
|
6589
|
+
TABLE_SPEC_KEYS = {
|
|
6590
|
+
key: "keyColumn",
|
|
6591
|
+
title: "titleColumn",
|
|
6592
|
+
text: "textColumn",
|
|
6593
|
+
description: "descriptionColumn",
|
|
6594
|
+
priority: "priorityColumn",
|
|
6595
|
+
category: "categoryColumn",
|
|
6596
|
+
type: "typeColumn",
|
|
6597
|
+
status: "statusColumn",
|
|
6598
|
+
criteria: "acceptanceCriteriaColumn",
|
|
6599
|
+
"fixed-category": "fixedCategory",
|
|
6600
|
+
"fixed-type": "fixedType"
|
|
6601
|
+
};
|
|
6602
|
+
SPEC_HELP = 'Write it like --table "3:key=ID,text=Requirement,fixed-category=functional".';
|
|
6603
|
+
SUPPORT = "This platform does not support requirements import yet. Nothing was sent to a project; try a newer Plune platform.";
|
|
6604
|
+
SHOWN = 50;
|
|
6605
|
+
flat = (s) => s.replace(/\s+/g, " ").trim();
|
|
6606
|
+
cut2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
6607
|
+
plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
6608
|
+
}
|
|
6609
|
+
});
|
|
6610
|
+
|
|
6198
6611
|
// src/cli.ts
|
|
6199
6612
|
var cli_exports = {};
|
|
6200
6613
|
__export(cli_exports, {
|
|
@@ -6204,11 +6617,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
6204
6617
|
init_cjs_shims();
|
|
6205
6618
|
init_api_url();
|
|
6206
6619
|
var import_commander = require("commander");
|
|
6207
|
-
var
|
|
6208
|
-
var
|
|
6620
|
+
var import_node_fs8 = require("fs");
|
|
6621
|
+
var import_node_path5 = require("path");
|
|
6209
6622
|
var import_node_url = require("url");
|
|
6210
|
-
var __dirname = (0,
|
|
6211
|
-
var pkg = JSON.parse((0,
|
|
6623
|
+
var __dirname = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
6624
|
+
var pkg = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
6212
6625
|
function toFormat(s) {
|
|
6213
6626
|
return s === "json" || s === "markdown" ? s : "console";
|
|
6214
6627
|
}
|
|
@@ -6217,7 +6630,7 @@ function shouldColor(outputPath) {
|
|
|
6217
6630
|
}
|
|
6218
6631
|
function writeReport(text2, outputPath) {
|
|
6219
6632
|
if (outputPath !== void 0) {
|
|
6220
|
-
(0,
|
|
6633
|
+
(0, import_node_fs8.writeFileSync)(outputPath, text2);
|
|
6221
6634
|
} else {
|
|
6222
6635
|
process.stdout.write(text2 + "\n");
|
|
6223
6636
|
}
|
|
@@ -6262,7 +6675,7 @@ function createProgram() {
|
|
|
6262
6675
|
program.hook("preAction", async (_program, action) => {
|
|
6263
6676
|
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
6264
6677
|
const { config } = action.optsWithGlobals();
|
|
6265
|
-
if (config !== void 0) loadEnv2((0,
|
|
6678
|
+
if (config !== void 0) loadEnv2((0, import_node_path5.dirname)((0, import_node_path5.resolve)(config)));
|
|
6266
6679
|
loadEnv2(process.cwd());
|
|
6267
6680
|
});
|
|
6268
6681
|
const runCommand = program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
|
|
@@ -6444,7 +6857,7 @@ function createProgram() {
|
|
|
6444
6857
|
false
|
|
6445
6858
|
).option(
|
|
6446
6859
|
"--upload-concurrency <n>",
|
|
6447
|
-
"File uploads in flight at once, 1 to 32 (default
|
|
6860
|
+
"File uploads in flight at once, 1 to 32 (default 16; PLUNE_UPLOAD_CONCURRENCY sets it too). Files are announced and confirmed up to 100 to a request; a 429 is retried PLUNE_UPLOAD_RETRIES times (default 6)",
|
|
6448
6861
|
(v) => {
|
|
6449
6862
|
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 32) throw new import_commander.InvalidArgumentError("a whole number from 1 to 32.");
|
|
6450
6863
|
return Number(v);
|
|
@@ -6495,7 +6908,7 @@ function createProgram() {
|
|
|
6495
6908
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
6496
6909
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
6497
6910
|
]);
|
|
6498
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
6911
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(globals.config)) : process.cwd();
|
|
6499
6912
|
try {
|
|
6500
6913
|
const result = handleReport2({ cwd });
|
|
6501
6914
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -6592,7 +7005,7 @@ function createProgram() {
|
|
|
6592
7005
|
const globals = command.optsWithGlobals();
|
|
6593
7006
|
const verbose = globals.verbose === true;
|
|
6594
7007
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
6595
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
7008
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(globals.config)) : process.cwd();
|
|
6596
7009
|
try {
|
|
6597
7010
|
const { id, url, gate } = await handleSync2({
|
|
6598
7011
|
cwd,
|
|
@@ -6663,13 +7076,13 @@ function createProgram() {
|
|
|
6663
7076
|
"Send a Markdown document of test cases back to Plune (POST /v1/test-cases/markdown)"
|
|
6664
7077
|
).option("--dry-run", "Show what would change and write nothing", false).action(async (file, options, command) => {
|
|
6665
7078
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
6666
|
-
const { handlePush: handlePush2, formatReport:
|
|
7079
|
+
const { handlePush: handlePush2, formatReport: formatReport3, reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
6667
7080
|
try {
|
|
6668
7081
|
const report = await handlePush2({
|
|
6669
7082
|
...file !== void 0 ? { file } : {},
|
|
6670
7083
|
dryRun: options.dryRun
|
|
6671
7084
|
});
|
|
6672
|
-
process.stdout.write(
|
|
7085
|
+
process.stdout.write(formatReport3(report));
|
|
6673
7086
|
if (report.refused.length > 0) process.exitCode = 4;
|
|
6674
7087
|
} catch (err) {
|
|
6675
7088
|
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
@@ -6681,6 +7094,33 @@ function createProgram() {
|
|
|
6681
7094
|
failUnexpected(err, verbose);
|
|
6682
7095
|
}
|
|
6683
7096
|
});
|
|
7097
|
+
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7098
|
+
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7099
|
+
"Import requirements from a Markdown file. With no mapping flags it lists the tables and sections found; with them it is a dry run until --yes"
|
|
7100
|
+
).option(
|
|
7101
|
+
"--table <n|spec>",
|
|
7102
|
+
'Read the table with this number (from the list); repeat as --table "<n>:key=ID,text=Requirement,fixed-category=functional" for several tables',
|
|
7103
|
+
(value2, previous = []) => [...previous, value2]
|
|
7104
|
+
).option("--key-column <header>", "Table: the column holding the requirement key").option("--title-column <header>", "Table: the column holding the title").option("--text-column <header>", "Table: one text column; the title is its first line (not with --title-column)").option("--description-column <header>", "Table: the description column").option("--priority-column <header>", "Table: the priority column").option("--category-column <header>", "Table: the category column").option("--type-column <header>", "Table: the type column").option("--status-column <header>", "Table: the status column (draft, active or done)").option("--criteria-column <header>", "Table: the acceptance criteria column").option("--map-priority <pairs>", 'Rename priority values, e.g. "Critical=Must-have,High=Should-have"').option("--map-category <pairs>", 'Table: rename category values, e.g. "Quality=non-functional"').option("--sections <heading|n>", "Read the section with this heading or number (from the list)").option("--unit <unit>", "Sections: bullets or subheadings (default: sub-headings if it has any)").option("--key-prefix <text>", "Sections: text a bullet or sub-heading starts with that carries its key").option("--priority-from <source>", "Sections: subheading \u2014 bullets take their sub-heading as priority").option("--type <type>", "What a row without a type becomes: user_story, feature or epic").option("--project <id>", "The project (default: PLUNE_PROJECT, else the only project of the account)").option("--yes", "Write it. Without it the import is a dry run", false).option("--json", "Print the platform\u2019s answer as JSON", false).action(async (file, options, command) => {
|
|
7105
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
7106
|
+
const { handleRequirementsImport: handleRequirementsImport2, formatDiscover: formatDiscover2, formatReport: formatReport3, reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7107
|
+
try {
|
|
7108
|
+
const res = await handleRequirementsImport2({ file, ...options });
|
|
7109
|
+
if (options.json === true) {
|
|
7110
|
+
process.stdout.write(JSON.stringify(res.step === "discover" ? res.result : res.report, null, 2) + "\n");
|
|
7111
|
+
} else {
|
|
7112
|
+
process.stdout.write(res.step === "discover" ? formatDiscover2(file, res.result) : formatReport3(res.report));
|
|
7113
|
+
}
|
|
7114
|
+
} catch (err) {
|
|
7115
|
+
const code = reportRequirementsImportFailure2(err, (s) => process.stderr.write(s));
|
|
7116
|
+
if (code !== null) {
|
|
7117
|
+
maybeStack(err, verbose);
|
|
7118
|
+
process.exitCode = code;
|
|
7119
|
+
return;
|
|
7120
|
+
}
|
|
7121
|
+
failUnexpected(err, verbose);
|
|
7122
|
+
}
|
|
7123
|
+
});
|
|
6684
7124
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
6685
7125
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
6686
7126
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
@@ -6716,7 +7156,7 @@ function isMainModule() {
|
|
|
6716
7156
|
const entry = process.argv[1];
|
|
6717
7157
|
if (entry === void 0) return false;
|
|
6718
7158
|
try {
|
|
6719
|
-
return (0,
|
|
7159
|
+
return (0, import_node_fs8.realpathSync)(entry) === (0, import_node_fs8.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
6720
7160
|
} catch {
|
|
6721
7161
|
return false;
|
|
6722
7162
|
}
|