@plune-ai/cli 0.23.0 → 0.25.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 +68 -8
- package/dist/cli.cjs +418 -61
- package/dist/index.cjs +9 -2
- package/dist/index.js +9 -2
- package/dist/reporter-core.cjs +95 -10
- package/dist/reporter-core.d.cts +22 -3
- package/dist/reporter-core.d.ts +22 -3
- package/dist/reporter-core.js +95 -10
- 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,7 +2312,7 @@ 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
2318
|
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
@@ -2322,7 +2322,7 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2322
2322
|
res.on("end", () => {
|
|
2323
2323
|
const status = res.statusCode ?? 0;
|
|
2324
2324
|
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2325
|
-
|
|
2325
|
+
resolve10({
|
|
2326
2326
|
ok: status >= 200 && status <= 299,
|
|
2327
2327
|
status,
|
|
2328
2328
|
headers: {
|
|
@@ -2341,12 +2341,12 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2341
2341
|
});
|
|
2342
2342
|
}
|
|
2343
2343
|
function nodePut(url, headers, source, idleMs) {
|
|
2344
|
-
return new Promise((
|
|
2344
|
+
return new Promise((resolve10, reject) => {
|
|
2345
2345
|
const target = new URL(url);
|
|
2346
2346
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2347
2347
|
const length = "body" in source ? source.body.length : source.size;
|
|
2348
2348
|
let answered;
|
|
2349
|
-
const settle = (err) => answered !== void 0 ?
|
|
2349
|
+
const settle = (err) => answered !== void 0 ? resolve10(answered) : reject(err);
|
|
2350
2350
|
const req = send(
|
|
2351
2351
|
target,
|
|
2352
2352
|
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
@@ -2355,7 +2355,7 @@ function nodePut(url, headers, source, idleMs) {
|
|
|
2355
2355
|
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
2356
2356
|
res.on("error", settle);
|
|
2357
2357
|
res.on("close", () => {
|
|
2358
|
-
|
|
2358
|
+
resolve10(answered);
|
|
2359
2359
|
req.destroy();
|
|
2360
2360
|
});
|
|
2361
2361
|
res.resume();
|
|
@@ -2370,14 +2370,25 @@ function nodePut(url, headers, source, idleMs) {
|
|
|
2370
2370
|
async function detailOf(res) {
|
|
2371
2371
|
try {
|
|
2372
2372
|
const body = await res.json();
|
|
2373
|
+
const q = body.quota;
|
|
2373
2374
|
return {
|
|
2375
|
+
...typeof body.shardsExpected === "number" && Number.isInteger(body.shardsExpected) ? { shardsExpected: body.shardsExpected } : {},
|
|
2374
2376
|
detail: typeof body.error === "string" ? body.error : "",
|
|
2377
|
+
...typeof q === "object" && q !== null && !Array.isArray(q) ? { quota: q } : {},
|
|
2375
2378
|
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
2376
2379
|
};
|
|
2377
2380
|
} catch {
|
|
2378
2381
|
return { detail: "" };
|
|
2379
2382
|
}
|
|
2380
2383
|
}
|
|
2384
|
+
function describeQuota(q) {
|
|
2385
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
2386
|
+
const kind = typeof q["kind"] === "string" ? q["kind"].slice(0, 60) : "limit";
|
|
2387
|
+
const [used, limit] = [num(q["used"]), num(q["limit"])];
|
|
2388
|
+
const parts = [used !== void 0 && limit !== void 0 ? `${kind}: ${used} of ${limit} used` : kind];
|
|
2389
|
+
if (typeof q["resetsAt"] === "string" && q["resetsAt"] !== "") parts.push(`resets ${q["resetsAt"].slice(0, 40)}`);
|
|
2390
|
+
return parts.join(", ");
|
|
2391
|
+
}
|
|
2381
2392
|
function classify(status) {
|
|
2382
2393
|
if (status === 401 || status === 403) return "auth";
|
|
2383
2394
|
if (status === 409) return "conflict";
|
|
@@ -2442,12 +2453,12 @@ function createClient(opts) {
|
|
|
2442
2453
|
}
|
|
2443
2454
|
const said = await detailOf(res);
|
|
2444
2455
|
const kind = classify(res.status);
|
|
2445
|
-
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
2456
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {}, ...said.shardsExpected !== void 0 ? { shardsExpected: said.shardsExpected } : {} };
|
|
2446
2457
|
if (kind !== "unavailable") break;
|
|
2447
2458
|
const said429 = parseRetryAfter(res.headers.get("retry-after"), now());
|
|
2448
|
-
if (res.status === 429 && said429 === null) {
|
|
2459
|
+
if (res.status === 429 && (said.quota !== void 0 || said429 === null)) {
|
|
2449
2460
|
last.kind = "invalid";
|
|
2450
|
-
last.detail =
|
|
2461
|
+
last.detail = `quota exceeded (${said.quota === void 0 ? "no Retry-After" : scrub(describeQuota(said.quota))})`;
|
|
2451
2462
|
if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
|
|
2452
2463
|
break;
|
|
2453
2464
|
}
|
|
@@ -2460,6 +2471,7 @@ function createClient(opts) {
|
|
|
2460
2471
|
post: (path17, body, retries) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2461
2472
|
upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2462
2473
|
del: (path17) => send("DELETE", path17),
|
|
2474
|
+
get: (path17) => send("GET", path17),
|
|
2463
2475
|
async put(url, headers, source) {
|
|
2464
2476
|
const putRetries = uploadRetries ?? 3;
|
|
2465
2477
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2518,6 +2530,16 @@ var init_client = __esm({
|
|
|
2518
2530
|
});
|
|
2519
2531
|
|
|
2520
2532
|
// src/reporter-core/env.ts
|
|
2533
|
+
function shardOf(env) {
|
|
2534
|
+
const raw = value(env, "PLUNE_SHARD");
|
|
2535
|
+
if (raw === void 0) return {};
|
|
2536
|
+
const m = /^(\d{1,4})\/(\d{1,4})$/.exec(raw);
|
|
2537
|
+
const [k, n] = m ? [Number(m[1]), Number(m[2])] : [0, 0];
|
|
2538
|
+
if (k >= 1 && k <= n && n <= SHARDS_MAX) return { shard: { index: k, total: n } };
|
|
2539
|
+
return {
|
|
2540
|
+
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.`
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2521
2543
|
function value(env, name) {
|
|
2522
2544
|
const raw = env[name]?.trim();
|
|
2523
2545
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -2560,6 +2582,7 @@ function readEnv(env = process.env) {
|
|
|
2560
2582
|
const labels = list(env, "PLUNE_LABELS");
|
|
2561
2583
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2562
2584
|
const full = flag(env, "PLUNE_FULL_RUN");
|
|
2585
|
+
const { shard, error: shardError } = shardOf(env);
|
|
2563
2586
|
return {
|
|
2564
2587
|
config: {
|
|
2565
2588
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -2580,7 +2603,11 @@ function readEnv(env = process.env) {
|
|
|
2580
2603
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2581
2604
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
2582
2605
|
// way this process must not be the one to call it finished.
|
|
2583
|
-
|
|
2606
|
+
// `PLUNE_SHARD` implies it: the run is joined by the shared key and this process does not close it
|
|
2607
|
+
// by itself; `leaveOpen` reports the shard instead.
|
|
2608
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
2609
|
+
...shard !== void 0 ? { shard } : {},
|
|
2610
|
+
...shardError !== void 0 ? { shardError } : {},
|
|
2584
2611
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
2585
2612
|
warnings
|
|
2586
2613
|
};
|
|
@@ -2608,14 +2635,15 @@ function ciCommit(reported, env = process.env) {
|
|
|
2608
2635
|
...branch !== void 0 ? { branch } : {}
|
|
2609
2636
|
};
|
|
2610
2637
|
}
|
|
2611
|
-
var path6, UNSUPPORTED_VARS, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2638
|
+
var path6, UNSUPPORTED_VARS, SHARDS_MAX, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2612
2639
|
var init_env2 = __esm({
|
|
2613
2640
|
"src/reporter-core/env.ts"() {
|
|
2614
2641
|
"use strict";
|
|
2615
2642
|
init_cjs_shims();
|
|
2616
2643
|
path6 = __toESM(require("path"), 1);
|
|
2617
2644
|
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2618
|
-
|
|
2645
|
+
SHARDS_MAX = 1e3;
|
|
2646
|
+
UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
2619
2647
|
UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
2620
2648
|
PROVIDERS = [
|
|
2621
2649
|
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
@@ -2914,6 +2942,7 @@ function warnIfDropped(sent, run, log) {
|
|
|
2914
2942
|
}
|
|
2915
2943
|
async function startRun(passed, declared2) {
|
|
2916
2944
|
const env = readEnv();
|
|
2945
|
+
if (env.shardError !== void 0) throw new Error(env.shardError);
|
|
2917
2946
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2918
2947
|
const cfg = { ...env.config, ...stated };
|
|
2919
2948
|
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
@@ -2955,6 +2984,9 @@ async function startRun(passed, declared2) {
|
|
|
2955
2984
|
const resolved = /* @__PURE__ */ new Map();
|
|
2956
2985
|
let buffer = [];
|
|
2957
2986
|
const discoveries = /* @__PURE__ */ new Map();
|
|
2987
|
+
const held = /* @__PURE__ */ new Map();
|
|
2988
|
+
let createdWithResult = 0;
|
|
2989
|
+
let createdUndelivered = 0;
|
|
2958
2990
|
let runId = null;
|
|
2959
2991
|
let joined = false;
|
|
2960
2992
|
let offline = token === "";
|
|
@@ -3083,6 +3115,7 @@ async function startRun(passed, declared2) {
|
|
|
3083
3115
|
specRef: result.specRef,
|
|
3084
3116
|
rawStatus: result.rawStatus
|
|
3085
3117
|
});
|
|
3118
|
+
held.set(result.keys[0].value, result);
|
|
3086
3119
|
}
|
|
3087
3120
|
async function offer() {
|
|
3088
3121
|
if (discoveries.size === 0 || offline || runId === null) return;
|
|
@@ -3101,7 +3134,24 @@ async function startRun(passed, declared2) {
|
|
|
3101
3134
|
return;
|
|
3102
3135
|
}
|
|
3103
3136
|
stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
|
|
3104
|
-
|
|
3137
|
+
const made = out.body.results.filter((r) => r?.outcome === "created");
|
|
3138
|
+
stats.created += made.length;
|
|
3139
|
+
const submissions = [];
|
|
3140
|
+
const filesByKey = /* @__PURE__ */ new Map();
|
|
3141
|
+
for (const r of made) {
|
|
3142
|
+
const pending = typeof r.id === "string" && r.id !== "" ? held.get(r.key?.value) : void 0;
|
|
3143
|
+
if (pending === void 0) continue;
|
|
3144
|
+
held.delete(r.key.value);
|
|
3145
|
+
const { keys: _keys, files, ...rest } = pending;
|
|
3146
|
+
submissions.push({ ...rest, testCaseId: r.id });
|
|
3147
|
+
if (files !== void 0) filesByKey.set(pending.resultKey, files);
|
|
3148
|
+
}
|
|
3149
|
+
if (submissions.length > 0) {
|
|
3150
|
+
const before = stats.deferred;
|
|
3151
|
+
await send(submissions, filesByKey);
|
|
3152
|
+
createdWithResult += submissions.length;
|
|
3153
|
+
createdUndelivered += stats.deferred - before;
|
|
3154
|
+
}
|
|
3105
3155
|
}
|
|
3106
3156
|
}
|
|
3107
3157
|
function skip(kind, name, why) {
|
|
@@ -3154,8 +3204,8 @@ async function startRun(passed, declared2) {
|
|
|
3154
3204
|
return skip(kind, job.name, "missing");
|
|
3155
3205
|
}
|
|
3156
3206
|
}
|
|
3157
|
-
const
|
|
3158
|
-
if (
|
|
3207
|
+
const refusal2 = kind === "text" ? textRefusal(bytes) : void 0;
|
|
3208
|
+
if (refusal2 !== void 0) return skip(kind, job.name, refusal2);
|
|
3159
3209
|
requests += 1;
|
|
3160
3210
|
const out = await client.upload(
|
|
3161
3211
|
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
@@ -3187,7 +3237,7 @@ async function startRun(passed, declared2) {
|
|
|
3187
3237
|
let free = uploadsAtOnce;
|
|
3188
3238
|
const waiting = [];
|
|
3189
3239
|
async function gated(work) {
|
|
3190
|
-
if (free === 0) await new Promise((
|
|
3240
|
+
if (free === 0) await new Promise((resolve10) => waiting.push(resolve10));
|
|
3191
3241
|
else free -= 1;
|
|
3192
3242
|
try {
|
|
3193
3243
|
return await work();
|
|
@@ -3439,9 +3489,9 @@ async function startRun(passed, declared2) {
|
|
|
3439
3489
|
skip(kind, job.name, "missing");
|
|
3440
3490
|
continue;
|
|
3441
3491
|
}
|
|
3442
|
-
const
|
|
3443
|
-
if (
|
|
3444
|
-
skip(kind, job.name,
|
|
3492
|
+
const refusal2 = kind === "text" ? textRefusal(data) : void 0;
|
|
3493
|
+
if (refusal2 !== void 0) {
|
|
3494
|
+
skip(kind, job.name, refusal2);
|
|
3445
3495
|
continue;
|
|
3446
3496
|
}
|
|
3447
3497
|
body.push({ resultId: job.id, name: job.name, contentType: job.type, data: data.toString("base64") });
|
|
@@ -3538,6 +3588,9 @@ async function startRun(passed, declared2) {
|
|
|
3538
3588
|
progress();
|
|
3539
3589
|
return;
|
|
3540
3590
|
}
|
|
3591
|
+
await send(submissions, filesByKey);
|
|
3592
|
+
}
|
|
3593
|
+
async function send(submissions, filesByKey) {
|
|
3541
3594
|
for (const part of packed(submissions)) {
|
|
3542
3595
|
if (offline) {
|
|
3543
3596
|
defer(part);
|
|
@@ -3581,7 +3634,10 @@ async function startRun(passed, declared2) {
|
|
|
3581
3634
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
3582
3635
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
3583
3636
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
3584
|
-
if (stats.created > 0)
|
|
3637
|
+
if (stats.created > 0) {
|
|
3638
|
+
const recorded = createdWithResult === stats.created && createdUndelivered === 0;
|
|
3639
|
+
parts.push(`${stats.created} added as cases (trusted source)${recorded ? " and their results recorded" : ""}`);
|
|
3640
|
+
}
|
|
3585
3641
|
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
3586
3642
|
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
3587
3643
|
if (bucketSeen) {
|
|
@@ -3611,6 +3667,7 @@ async function startRun(passed, declared2) {
|
|
|
3611
3667
|
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
3612
3668
|
}
|
|
3613
3669
|
}
|
|
3670
|
+
const shardLines = [];
|
|
3614
3671
|
return {
|
|
3615
3672
|
get runId() {
|
|
3616
3673
|
return runId;
|
|
@@ -3648,15 +3705,43 @@ async function startRun(passed, declared2) {
|
|
|
3648
3705
|
summarise();
|
|
3649
3706
|
return frozen;
|
|
3650
3707
|
},
|
|
3651
|
-
|
|
3652
|
-
|
|
3708
|
+
shardLines,
|
|
3709
|
+
async leaveOpen(opts) {
|
|
3710
|
+
if (done) return void 0;
|
|
3653
3711
|
done = true;
|
|
3654
3712
|
await flush();
|
|
3655
3713
|
await offer();
|
|
3656
|
-
|
|
3714
|
+
let frozen;
|
|
3715
|
+
const after = [];
|
|
3716
|
+
const shard = env.shard;
|
|
3717
|
+
let counted = false;
|
|
3718
|
+
let mismatch;
|
|
3719
|
+
if (runId !== null && shard !== void 0 && !offline) {
|
|
3720
|
+
const out = await client.post(`/v1/runs/${runId}/events`, { event: "finish", shardIndex: shard.index, shardsExpected: shard.total });
|
|
3721
|
+
if (out.ok && typeof out.body?.shardsReported === "number") {
|
|
3722
|
+
counted = true;
|
|
3723
|
+
frozen = out.body.summary ?? void 0;
|
|
3724
|
+
const n = out.body.shardsExpected ?? shard.total;
|
|
3725
|
+
if (out.body.run?.statusEvent === "finished") after.push("plune: run closed");
|
|
3726
|
+
after.push(`plune: shard ${shard.index} of ${shard.total} done \u2014 ${out.body.shardsReported} of ${n} shards reported`);
|
|
3727
|
+
} else if (out.ok || out.kind === "invalid") {
|
|
3728
|
+
log('plune: the platform does not support shard auto-close yet \u2014 the run is left open for "plune run finish".');
|
|
3729
|
+
} else if (out.code === "shards_mismatch") {
|
|
3730
|
+
mismatch = new Error(
|
|
3731
|
+
`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.`
|
|
3732
|
+
);
|
|
3733
|
+
} else {
|
|
3734
|
+
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}".`);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
if (runId !== null && !counted) {
|
|
3657
3738
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
3658
3739
|
}
|
|
3659
3740
|
summarise();
|
|
3741
|
+
shardLines.push(...after);
|
|
3742
|
+
if (opts?.holdShardLines !== true) for (const line of after) log(line);
|
|
3743
|
+
if (mismatch !== void 0) throw mismatch;
|
|
3744
|
+
return frozen;
|
|
3660
3745
|
}
|
|
3661
3746
|
};
|
|
3662
3747
|
}
|
|
@@ -3843,14 +3928,14 @@ async function handleRunExec(options) {
|
|
|
3843
3928
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
3844
3929
|
const run = await handleRunStart(options);
|
|
3845
3930
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
3846
|
-
const code = await new Promise((
|
|
3931
|
+
const code = await new Promise((resolve10) => {
|
|
3847
3932
|
const child = spawnImpl(command, args, {
|
|
3848
3933
|
stdio: "inherit",
|
|
3849
3934
|
shell: process.platform === "win32",
|
|
3850
3935
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
3851
3936
|
});
|
|
3852
|
-
child.on("close", (exitCode) =>
|
|
3853
|
-
child.on("error", () =>
|
|
3937
|
+
child.on("close", (exitCode) => resolve10(exitCode ?? 1));
|
|
3938
|
+
child.on("error", () => resolve10(1));
|
|
3854
3939
|
});
|
|
3855
3940
|
return handleRunFinish(run.id, options, code);
|
|
3856
3941
|
}
|
|
@@ -4086,12 +4171,18 @@ async function handleSync(deps = {}) {
|
|
|
4086
4171
|
const run = readRun(cwd, deps.file);
|
|
4087
4172
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4088
4173
|
const endpoint = `${apiUrl}/v1/runs`;
|
|
4174
|
+
const payload = JSON.stringify(run);
|
|
4175
|
+
const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
|
|
4089
4176
|
let res;
|
|
4090
4177
|
try {
|
|
4091
4178
|
res = await doFetch(endpoint, {
|
|
4092
4179
|
method: "POST",
|
|
4093
|
-
headers: {
|
|
4094
|
-
|
|
4180
|
+
headers: {
|
|
4181
|
+
"content-type": "application/json",
|
|
4182
|
+
authorization: `Bearer ${token}`,
|
|
4183
|
+
"idempotency-key": idempotencyKey
|
|
4184
|
+
},
|
|
4185
|
+
body: payload
|
|
4095
4186
|
});
|
|
4096
4187
|
} catch {
|
|
4097
4188
|
throw new SyncNetworkError(apiUrl);
|
|
@@ -4127,11 +4218,12 @@ function reportSyncFailure(err, write) {
|
|
|
4127
4218
|
}
|
|
4128
4219
|
return null;
|
|
4129
4220
|
}
|
|
4130
|
-
var fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4221
|
+
var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4131
4222
|
var init_sync = __esm({
|
|
4132
4223
|
"src/cli/commands/sync.ts"() {
|
|
4133
4224
|
"use strict";
|
|
4134
4225
|
init_cjs_shims();
|
|
4226
|
+
import_node_crypto4 = require("crypto");
|
|
4135
4227
|
fs8 = __toESM(require("fs"), 1);
|
|
4136
4228
|
path9 = __toESM(require("path"), 1);
|
|
4137
4229
|
init_run_result_schema();
|
|
@@ -5295,7 +5387,7 @@ async function handleRunImport(options) {
|
|
|
5295
5387
|
);
|
|
5296
5388
|
for (const result2 of results) await session.add(result2);
|
|
5297
5389
|
let frozen;
|
|
5298
|
-
if (env.keepOpen) await session.leaveOpen();
|
|
5390
|
+
if (env.keepOpen) frozen = await session.leaveOpen({ holdShardLines: true });
|
|
5299
5391
|
else frozen = await session.finish();
|
|
5300
5392
|
const stats = session.stats;
|
|
5301
5393
|
const result = {
|
|
@@ -5326,13 +5418,14 @@ async function handleRunImport(options) {
|
|
|
5326
5418
|
};
|
|
5327
5419
|
for (const line of describe(result, apiUrl)) write(line);
|
|
5328
5420
|
if (options.failOnFailures === true) {
|
|
5329
|
-
if (env.keepOpen) {
|
|
5421
|
+
if (env.keepOpen && frozen === void 0) {
|
|
5330
5422
|
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.");
|
|
5331
5423
|
}
|
|
5332
5424
|
const gate = gateExit(frozen);
|
|
5333
5425
|
write(gate.line);
|
|
5334
5426
|
result.gate = gate.code;
|
|
5335
5427
|
}
|
|
5428
|
+
for (const line of session.shardLines) write(line);
|
|
5336
5429
|
return result;
|
|
5337
5430
|
}
|
|
5338
5431
|
function runUrl2(apiUrl, id) {
|
|
@@ -5363,7 +5456,10 @@ function describe(result, apiUrl) {
|
|
|
5363
5456
|
);
|
|
5364
5457
|
}
|
|
5365
5458
|
if (result.offered > 0) {
|
|
5366
|
-
|
|
5459
|
+
const source = result.format === "playwright-json" ? "playwright" : result.format;
|
|
5460
|
+
lines.push(
|
|
5461
|
+
`${result.offered} unknown test(s) offered to the review queue. They wait there for a decision and do not count on the run until approved. To skip review for this source, add \`${source}\` under Settings \u2192 Trusted sources.`
|
|
5462
|
+
);
|
|
5367
5463
|
} else if (result.unresolved > 0 && result.created === 0 && !result.offering) {
|
|
5368
5464
|
lines.push("Nothing was offered to the review queue \u2014 pass --create to propose the unmatched tests.");
|
|
5369
5465
|
} else if (result.unresolved > 0 && result.created === 0 && result.unoffered === 0) {
|
|
@@ -6185,6 +6281,244 @@ var init_ingest = __esm({
|
|
|
6185
6281
|
}
|
|
6186
6282
|
});
|
|
6187
6283
|
|
|
6284
|
+
// src/cli/commands/requirements-import.ts
|
|
6285
|
+
var requirements_import_exports = {};
|
|
6286
|
+
__export(requirements_import_exports, {
|
|
6287
|
+
MAX_MARKDOWN_BYTES: () => MAX_MARKDOWN_BYTES,
|
|
6288
|
+
RequirementsImportError: () => RequirementsImportError,
|
|
6289
|
+
buildImportBody: () => buildImportBody,
|
|
6290
|
+
formatDiscover: () => formatDiscover,
|
|
6291
|
+
formatReport: () => formatReport2,
|
|
6292
|
+
handleRequirementsImport: () => handleRequirementsImport,
|
|
6293
|
+
parseMap: () => parseMap,
|
|
6294
|
+
reportRequirementsImportFailure: () => reportRequirementsImportFailure
|
|
6295
|
+
});
|
|
6296
|
+
function parseMap(flag2, raw) {
|
|
6297
|
+
const out = {};
|
|
6298
|
+
for (const pair of raw.split(",")) {
|
|
6299
|
+
if (pair.trim() === "") continue;
|
|
6300
|
+
const at = pair.indexOf("=");
|
|
6301
|
+
const from = at < 0 ? "" : pair.slice(0, at).trim();
|
|
6302
|
+
const to = at < 0 ? "" : pair.slice(at + 1).trim();
|
|
6303
|
+
if (from === "" || to === "") usage(`${flag2}: "${pair.trim()}" is not Value=Value. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6304
|
+
out[from] = to;
|
|
6305
|
+
}
|
|
6306
|
+
if (Object.keys(out).length === 0) usage(`${flag2} is empty. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6307
|
+
return out;
|
|
6308
|
+
}
|
|
6309
|
+
function buildImportBody(f) {
|
|
6310
|
+
const columnFlags = COLUMN_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6311
|
+
const sectionFlags = SECTION_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6312
|
+
const anyMapping = f.table !== void 0 || f.sections !== void 0 || columnFlags.length > 0 || sectionFlags.length > 0 || f.mapPriority !== void 0 || f.mapCategory !== void 0 || f.type !== void 0;
|
|
6313
|
+
if (!anyMapping) return null;
|
|
6314
|
+
if (f.table !== void 0 && f.sections !== void 0) usage("Give --table or --sections, not both: one import reads one table or one section.");
|
|
6315
|
+
if (f.table === void 0 && f.sections === void 0) {
|
|
6316
|
+
usage("The mapping flags need --table <n> (a table) or --sections <heading|n> (a section). Run without flags to see which the file has.");
|
|
6317
|
+
}
|
|
6318
|
+
const body = {};
|
|
6319
|
+
if (f.mapPriority !== void 0) body["priorityMap"] = parseMap("--map-priority", f.mapPriority);
|
|
6320
|
+
if (f.type !== void 0) body["defaultType"] = oneOf("--type", f.type, ["user_story", "feature", "epic"]);
|
|
6321
|
+
if (f.table !== void 0) {
|
|
6322
|
+
if (!/^\d+$/.test(f.table)) usage(`--table is the number of the table in the list (0, 1, ...), not "${f.table}".`);
|
|
6323
|
+
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6324
|
+
if (f.titleColumn !== void 0 && f.textColumn !== void 0) usage("Give --title-column or --text-column, not both.");
|
|
6325
|
+
if (f.titleColumn === void 0 && f.textColumn === void 0) usage("--table needs --title-column <header> or --text-column <header>.");
|
|
6326
|
+
body["mode"] = "table";
|
|
6327
|
+
body["table"] = Number(f.table);
|
|
6328
|
+
for (const [k, , key] of columnFlags) body[key] = f[k];
|
|
6329
|
+
if (f.mapCategory !== void 0) body["categoryMap"] = parseMap("--map-category", f.mapCategory);
|
|
6330
|
+
} else {
|
|
6331
|
+
if (columnFlags.length > 0) usage(`${columnFlags.map(([, n]) => n).join(", ")} ${columnFlags.length > 1 ? "are" : "is"} for --table, not --sections.`);
|
|
6332
|
+
if (f.mapCategory !== void 0) usage("--map-category is for --table, not --sections.");
|
|
6333
|
+
body["mode"] = "sections";
|
|
6334
|
+
const section = (f.sections ?? "").trim();
|
|
6335
|
+
if (section === "") usage("--sections needs a heading or its number in the list.");
|
|
6336
|
+
body["section"] = /^\d+$/.test(section) ? Number(section) : section;
|
|
6337
|
+
if (f.unit !== void 0) body["unit"] = oneOf("--unit", f.unit, ["bullets", "subheadings"]);
|
|
6338
|
+
if (f.keyPrefix !== void 0) body["keyPrefix"] = f.keyPrefix;
|
|
6339
|
+
if (f.priorityFrom !== void 0) body["priorityFrom"] = oneOf("--priority-from", f.priorityFrom, ["subheading"]);
|
|
6340
|
+
}
|
|
6341
|
+
return body;
|
|
6342
|
+
}
|
|
6343
|
+
function readMarkdown(cwd, file) {
|
|
6344
|
+
const target = (0, import_node_path4.resolve)(cwd, file);
|
|
6345
|
+
let size;
|
|
6346
|
+
try {
|
|
6347
|
+
const st = (0, import_node_fs7.statSync)(target);
|
|
6348
|
+
if (!st.isFile()) return usage(`${target} is not a file.`);
|
|
6349
|
+
size = st.size;
|
|
6350
|
+
} catch {
|
|
6351
|
+
return usage(`File not found: ${target}`);
|
|
6352
|
+
}
|
|
6353
|
+
if (size > MAX_MARKDOWN_BYTES) {
|
|
6354
|
+
return usage(`${target} is ${size} bytes; the platform reads at most ${MAX_MARKDOWN_BYTES} bytes (1 MiB). Nothing was sent. Split the file.`);
|
|
6355
|
+
}
|
|
6356
|
+
let text2;
|
|
6357
|
+
try {
|
|
6358
|
+
text2 = new TextDecoder("utf-8", { fatal: true }).decode((0, import_node_fs7.readFileSync)(target));
|
|
6359
|
+
} catch {
|
|
6360
|
+
return usage(`${target} is not valid UTF-8. Save it as UTF-8 and try again.`);
|
|
6361
|
+
}
|
|
6362
|
+
text2 = text2.replace(/^/, "");
|
|
6363
|
+
if (text2.trim() === "") usage(`${target} is empty.`);
|
|
6364
|
+
return text2;
|
|
6365
|
+
}
|
|
6366
|
+
function refusal(out, apiUrl, projectId) {
|
|
6367
|
+
const d = out.detail;
|
|
6368
|
+
const said = d === "" ? "" : ` (${d})`;
|
|
6369
|
+
switch (out.status) {
|
|
6370
|
+
case null:
|
|
6371
|
+
return new RequirementsImportError(`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`, 1);
|
|
6372
|
+
case 401:
|
|
6373
|
+
return new RequirementsImportError(`Your API token was rejected (revoked or invalid). ${whereToGetAToken(apiUrl)} Then run "plune login".`, 2);
|
|
6374
|
+
case 403:
|
|
6375
|
+
return new RequirementsImportError(`The platform refused: ${d || "read-only member"}. Importing needs an owner or a member of the project.`, 1);
|
|
6376
|
+
case 404:
|
|
6377
|
+
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);
|
|
6378
|
+
case 409:
|
|
6379
|
+
return new RequirementsImportError(`A requirement key was taken by another change while the import ran. Nothing was written; run it again${said}.`, 1);
|
|
6380
|
+
case 413:
|
|
6381
|
+
return new RequirementsImportError(`The platform refused the file as too large${said}. Nothing was written.`, 1);
|
|
6382
|
+
case 400:
|
|
6383
|
+
return new RequirementsImportError(`The platform refused the mapping${said}.`, 1);
|
|
6384
|
+
default:
|
|
6385
|
+
return new RequirementsImportError(`The platform answered HTTP ${out.status}${said}. Nothing was written.`, 1);
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
6388
|
+
async function resolveProject(client, explicit, apiUrl) {
|
|
6389
|
+
const given = (explicit ?? process.env["PLUNE_PROJECT"])?.trim();
|
|
6390
|
+
if (given !== void 0 && given !== "") return given;
|
|
6391
|
+
const out = await client.get("/v1/projects");
|
|
6392
|
+
if (!out.ok) throw refusal(out, apiUrl, "");
|
|
6393
|
+
const projects = out.body.projects ?? [];
|
|
6394
|
+
if (projects.length === 1 && projects[0] !== void 0) return projects[0].id;
|
|
6395
|
+
const list2 = projects.map((p) => `${p.id}${p.name ? ` (${p.name})` : ""}`).join(", ");
|
|
6396
|
+
return usage(
|
|
6397
|
+
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).`
|
|
6398
|
+
);
|
|
6399
|
+
}
|
|
6400
|
+
async function handleRequirementsImport(deps) {
|
|
6401
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
6402
|
+
const mapping = buildImportBody(deps);
|
|
6403
|
+
const markdown = readMarkdown(deps.cwd ?? process.cwd(), deps.file);
|
|
6404
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
6405
|
+
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
6406
|
+
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
6407
|
+
const projectId = await resolveProject(client, deps.project, apiUrl);
|
|
6408
|
+
const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
|
|
6409
|
+
if (mapping === null) {
|
|
6410
|
+
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
6411
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId);
|
|
6412
|
+
return { step: "discover", result: out2.body };
|
|
6413
|
+
}
|
|
6414
|
+
const out = await client.post(base, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
6415
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId);
|
|
6416
|
+
return { step: "import", report: out.body };
|
|
6417
|
+
}
|
|
6418
|
+
function formatDiscover(file, r) {
|
|
6419
|
+
const out = [`${file}: ${plural(r.tables.length, "table")}, ${plural(r.sections.length, "section")}`];
|
|
6420
|
+
if (r.frontmatter.keys.length > 0) out.push(`Frontmatter keys: ${r.frontmatter.keys.join(", ")}`);
|
|
6421
|
+
out.push("", "Tables");
|
|
6422
|
+
if (r.tables.length === 0) out.push(" (none)");
|
|
6423
|
+
for (const t of r.tables) {
|
|
6424
|
+
const mark = t.unreadable !== void 0 ? ` cannot be read: ${t.unreadable}` : t.looksLikeRequirements ? " <- looks like requirements" : "";
|
|
6425
|
+
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}`);
|
|
6426
|
+
}
|
|
6427
|
+
out.push("", "Sections");
|
|
6428
|
+
if (r.sections.length === 0) out.push(" (none)");
|
|
6429
|
+
for (const s of r.sections) {
|
|
6430
|
+
out.push(` #${s.index} line ${s.line} ${"#".repeat(s.level)} ${flat(s.heading)} ${plural(s.bulletCount, "bullet")}, ${plural(s.subheadingCount, "sub-heading")}`);
|
|
6431
|
+
}
|
|
6432
|
+
if (r.truncated) out.push("", "The file has more headings or tables than are listed here.");
|
|
6433
|
+
out.push(
|
|
6434
|
+
"",
|
|
6435
|
+
"Pick one and map it (a dry run, nothing is written):",
|
|
6436
|
+
` plune requirements import ${file} --table <n> --key-column <header> --title-column <header>`,
|
|
6437
|
+
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings`
|
|
6438
|
+
);
|
|
6439
|
+
return out.join("\n") + "\n";
|
|
6440
|
+
}
|
|
6441
|
+
function formatReport2(report) {
|
|
6442
|
+
const s = report.summary;
|
|
6443
|
+
const out = [`create ${s.create} \xB7 update ${s.update} \xB7 unchanged ${s.unchanged} \xB7 skip ${s.skip}`];
|
|
6444
|
+
for (const action of ["create", "update", "skip", "unchanged"]) {
|
|
6445
|
+
const rows = report.items.filter((i) => i.action === action);
|
|
6446
|
+
if (rows.length === 0) continue;
|
|
6447
|
+
out.push("", `${action} (${rows.length})`);
|
|
6448
|
+
for (const i of rows.slice(0, SHOWN)) {
|
|
6449
|
+
const changes = i.changes !== void 0 && i.changes.length > 0 ? `; changes: ${i.changes.join(", ")}` : "";
|
|
6450
|
+
const notes = i.notes !== void 0 && i.notes.length > 0 ? `; note: ${i.notes.join("; ")}` : "";
|
|
6451
|
+
out.push(` line ${i.line} ${i.key ?? "-"} ${cut2(flat(i.title), 80)} - ${flat(i.reason)}${changes}${notes}`);
|
|
6452
|
+
}
|
|
6453
|
+
if (rows.length > SHOWN) out.push(` ... and ${rows.length - SHOWN} more (--json lists every row)`);
|
|
6454
|
+
}
|
|
6455
|
+
for (const [label, values] of [["Priority values", report.values.priority], ["Category values", report.values.category]]) {
|
|
6456
|
+
if (values.length === 0) continue;
|
|
6457
|
+
out.push("", label);
|
|
6458
|
+
for (const v of values.slice(0, SHOWN)) out.push(` ${v.value} x${v.count} -> ${v.mappedTo ?? "(no mapping: kept as written)"}`);
|
|
6459
|
+
}
|
|
6460
|
+
const n = report.notImported;
|
|
6461
|
+
if (n.tables.length + n.sections.length + n.frontmatterKeys.length > 0) {
|
|
6462
|
+
out.push("", "Not imported");
|
|
6463
|
+
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")})`);
|
|
6464
|
+
for (const x of n.sections) out.push(` section #${x.index} "${flat(x.heading)}" (${plural(x.bulletCount, "bullet")})`);
|
|
6465
|
+
if (n.frontmatterKeys.length > 0) out.push(` frontmatter keys: ${n.frontmatterKeys.join(", ")}`);
|
|
6466
|
+
}
|
|
6467
|
+
out.push("", report.applied ? `applied \u2014 ${plural(s.create, "requirement")} created, ${s.update} updated.` : "dry run \u2014 nothing was written; add --yes to apply");
|
|
6468
|
+
return out.join("\n") + "\n";
|
|
6469
|
+
}
|
|
6470
|
+
function reportRequirementsImportFailure(err, write) {
|
|
6471
|
+
if (!(err instanceof RequirementsImportError)) return null;
|
|
6472
|
+
write(err.message + "\n");
|
|
6473
|
+
return err.exitCode;
|
|
6474
|
+
}
|
|
6475
|
+
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6476
|
+
var init_requirements_import = __esm({
|
|
6477
|
+
"src/cli/commands/requirements-import.ts"() {
|
|
6478
|
+
"use strict";
|
|
6479
|
+
init_cjs_shims();
|
|
6480
|
+
import_node_fs7 = require("fs");
|
|
6481
|
+
import_node_path4 = require("path");
|
|
6482
|
+
init_client();
|
|
6483
|
+
init_api_url();
|
|
6484
|
+
init_credentials();
|
|
6485
|
+
MAX_MARKDOWN_BYTES = 1048576;
|
|
6486
|
+
RequirementsImportError = class extends Error {
|
|
6487
|
+
constructor(message, exitCode) {
|
|
6488
|
+
super(message);
|
|
6489
|
+
this.exitCode = exitCode;
|
|
6490
|
+
this.name = "RequirementsImportError";
|
|
6491
|
+
}
|
|
6492
|
+
exitCode;
|
|
6493
|
+
};
|
|
6494
|
+
usage = (message) => {
|
|
6495
|
+
throw new RequirementsImportError(message, 2);
|
|
6496
|
+
};
|
|
6497
|
+
COLUMN_FLAGS = [
|
|
6498
|
+
["keyColumn", "--key-column", "keyColumn"],
|
|
6499
|
+
["titleColumn", "--title-column", "titleColumn"],
|
|
6500
|
+
["textColumn", "--text-column", "textColumn"],
|
|
6501
|
+
["descriptionColumn", "--description-column", "descriptionColumn"],
|
|
6502
|
+
["priorityColumn", "--priority-column", "priorityColumn"],
|
|
6503
|
+
["categoryColumn", "--category-column", "categoryColumn"],
|
|
6504
|
+
["typeColumn", "--type-column", "typeColumn"],
|
|
6505
|
+
["statusColumn", "--status-column", "statusColumn"],
|
|
6506
|
+
["criteriaColumn", "--criteria-column", "acceptanceCriteriaColumn"]
|
|
6507
|
+
];
|
|
6508
|
+
SECTION_FLAGS = [
|
|
6509
|
+
["unit", "--unit"],
|
|
6510
|
+
["keyPrefix", "--key-prefix"],
|
|
6511
|
+
["priorityFrom", "--priority-from"]
|
|
6512
|
+
];
|
|
6513
|
+
oneOf = (flag2, value2, allowed) => allowed.includes(value2) ? value2 : usage(`${flag2} must be ${allowed.join(" or ")}, not "${value2}".`);
|
|
6514
|
+
SUPPORT = "This platform does not support requirements import yet. Nothing was sent to a project; try a newer Plune platform.";
|
|
6515
|
+
SHOWN = 50;
|
|
6516
|
+
flat = (s) => s.replace(/\s+/g, " ").trim();
|
|
6517
|
+
cut2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
6518
|
+
plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
6519
|
+
}
|
|
6520
|
+
});
|
|
6521
|
+
|
|
6188
6522
|
// src/cli.ts
|
|
6189
6523
|
var cli_exports = {};
|
|
6190
6524
|
__export(cli_exports, {
|
|
@@ -6194,11 +6528,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
6194
6528
|
init_cjs_shims();
|
|
6195
6529
|
init_api_url();
|
|
6196
6530
|
var import_commander = require("commander");
|
|
6197
|
-
var
|
|
6198
|
-
var
|
|
6531
|
+
var import_node_fs8 = require("fs");
|
|
6532
|
+
var import_node_path5 = require("path");
|
|
6199
6533
|
var import_node_url = require("url");
|
|
6200
|
-
var __dirname = (0,
|
|
6201
|
-
var pkg = JSON.parse((0,
|
|
6534
|
+
var __dirname = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
6535
|
+
var pkg = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
6202
6536
|
function toFormat(s) {
|
|
6203
6537
|
return s === "json" || s === "markdown" ? s : "console";
|
|
6204
6538
|
}
|
|
@@ -6207,7 +6541,7 @@ function shouldColor(outputPath) {
|
|
|
6207
6541
|
}
|
|
6208
6542
|
function writeReport(text2, outputPath) {
|
|
6209
6543
|
if (outputPath !== void 0) {
|
|
6210
|
-
(0,
|
|
6544
|
+
(0, import_node_fs8.writeFileSync)(outputPath, text2);
|
|
6211
6545
|
} else {
|
|
6212
6546
|
process.stdout.write(text2 + "\n");
|
|
6213
6547
|
}
|
|
@@ -6252,7 +6586,7 @@ function createProgram() {
|
|
|
6252
6586
|
program.hook("preAction", async (_program, action) => {
|
|
6253
6587
|
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
6254
6588
|
const { config } = action.optsWithGlobals();
|
|
6255
|
-
if (config !== void 0) loadEnv2((0,
|
|
6589
|
+
if (config !== void 0) loadEnv2((0, import_node_path5.dirname)((0, import_node_path5.resolve)(config)));
|
|
6256
6590
|
loadEnv2(process.cwd());
|
|
6257
6591
|
});
|
|
6258
6592
|
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(
|
|
@@ -6434,7 +6768,7 @@ function createProgram() {
|
|
|
6434
6768
|
false
|
|
6435
6769
|
).option(
|
|
6436
6770
|
"--upload-concurrency <n>",
|
|
6437
|
-
"File uploads in flight at once, 1 to 32 (default
|
|
6771
|
+
"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)",
|
|
6438
6772
|
(v) => {
|
|
6439
6773
|
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 32) throw new import_commander.InvalidArgumentError("a whole number from 1 to 32.");
|
|
6440
6774
|
return Number(v);
|
|
@@ -6485,7 +6819,7 @@ function createProgram() {
|
|
|
6485
6819
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
6486
6820
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
6487
6821
|
]);
|
|
6488
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
6822
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(globals.config)) : process.cwd();
|
|
6489
6823
|
try {
|
|
6490
6824
|
const result = handleReport2({ cwd });
|
|
6491
6825
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -6582,7 +6916,7 @@ function createProgram() {
|
|
|
6582
6916
|
const globals = command.optsWithGlobals();
|
|
6583
6917
|
const verbose = globals.verbose === true;
|
|
6584
6918
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
6585
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
6919
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(globals.config)) : process.cwd();
|
|
6586
6920
|
try {
|
|
6587
6921
|
const { id, url, gate } = await handleSync2({
|
|
6588
6922
|
cwd,
|
|
@@ -6653,13 +6987,13 @@ function createProgram() {
|
|
|
6653
6987
|
"Send a Markdown document of test cases back to Plune (POST /v1/test-cases/markdown)"
|
|
6654
6988
|
).option("--dry-run", "Show what would change and write nothing", false).action(async (file, options, command) => {
|
|
6655
6989
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
6656
|
-
const { handlePush: handlePush2, formatReport:
|
|
6990
|
+
const { handlePush: handlePush2, formatReport: formatReport3, reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
6657
6991
|
try {
|
|
6658
6992
|
const report = await handlePush2({
|
|
6659
6993
|
...file !== void 0 ? { file } : {},
|
|
6660
6994
|
dryRun: options.dryRun
|
|
6661
6995
|
});
|
|
6662
|
-
process.stdout.write(
|
|
6996
|
+
process.stdout.write(formatReport3(report));
|
|
6663
6997
|
if (report.refused.length > 0) process.exitCode = 4;
|
|
6664
6998
|
} catch (err) {
|
|
6665
6999
|
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
@@ -6671,6 +7005,29 @@ function createProgram() {
|
|
|
6671
7005
|
failUnexpected(err, verbose);
|
|
6672
7006
|
}
|
|
6673
7007
|
});
|
|
7008
|
+
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7009
|
+
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7010
|
+
"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"
|
|
7011
|
+
).option("--table <n>", "Read the table with this number (from the list)").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) => {
|
|
7012
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
7013
|
+
const { handleRequirementsImport: handleRequirementsImport2, formatDiscover: formatDiscover2, formatReport: formatReport3, reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7014
|
+
try {
|
|
7015
|
+
const res = await handleRequirementsImport2({ file, ...options });
|
|
7016
|
+
if (options.json === true) {
|
|
7017
|
+
process.stdout.write(JSON.stringify(res.step === "discover" ? res.result : res.report, null, 2) + "\n");
|
|
7018
|
+
} else {
|
|
7019
|
+
process.stdout.write(res.step === "discover" ? formatDiscover2(file, res.result) : formatReport3(res.report));
|
|
7020
|
+
}
|
|
7021
|
+
} catch (err) {
|
|
7022
|
+
const code = reportRequirementsImportFailure2(err, (s) => process.stderr.write(s));
|
|
7023
|
+
if (code !== null) {
|
|
7024
|
+
maybeStack(err, verbose);
|
|
7025
|
+
process.exitCode = code;
|
|
7026
|
+
return;
|
|
7027
|
+
}
|
|
7028
|
+
failUnexpected(err, verbose);
|
|
7029
|
+
}
|
|
7030
|
+
});
|
|
6674
7031
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
6675
7032
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
6676
7033
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
@@ -6706,7 +7063,7 @@ function isMainModule() {
|
|
|
6706
7063
|
const entry = process.argv[1];
|
|
6707
7064
|
if (entry === void 0) return false;
|
|
6708
7065
|
try {
|
|
6709
|
-
return (0,
|
|
7066
|
+
return (0, import_node_fs8.realpathSync)(entry) === (0, import_node_fs8.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
6710
7067
|
} catch {
|
|
6711
7068
|
return false;
|
|
6712
7069
|
}
|