@plune-ai/cli 0.15.0 → 0.16.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 +20 -3
- package/dist/cli.cjs +223 -83
- package/dist/index.cjs +34 -3
- package/dist/index.js +34 -3
- package/dist/reporter-core.cjs +41 -1
- package/dist/reporter-core.d.cts +38 -1
- package/dist/reporter-core.d.ts +38 -1
- package/dist/reporter-core.js +41 -2
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -75,8 +75,8 @@ var env_exports = {};
|
|
|
75
75
|
__export(env_exports, {
|
|
76
76
|
loadEnv: () => loadEnv
|
|
77
77
|
});
|
|
78
|
-
function loadEnv(
|
|
79
|
-
(0, import_dotenv.config)({ path: path.join(
|
|
78
|
+
function loadEnv(dir) {
|
|
79
|
+
(0, import_dotenv.config)({ path: path.join(dir, ".env"), override: false });
|
|
80
80
|
}
|
|
81
81
|
var import_dotenv, path;
|
|
82
82
|
var init_env = __esm({
|
|
@@ -523,11 +523,25 @@ var init_prices = __esm({
|
|
|
523
523
|
"use strict";
|
|
524
524
|
init_cjs_shims();
|
|
525
525
|
PRICE_TABLE = {
|
|
526
|
-
// Anthropic
|
|
526
|
+
// Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
|
|
527
|
+
// input and output tokens per million, read on 2026-09-29, kept here per thousand.
|
|
528
|
+
"claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
529
|
+
"claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
|
|
530
|
+
"claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
531
|
+
"claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
532
|
+
// The dated id of the same model; a config may name either.
|
|
533
|
+
"claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
534
|
+
// Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
|
|
535
|
+
// retired these models — a new project should not start on them.
|
|
527
536
|
"claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
|
|
528
537
|
"claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
|
|
529
538
|
"claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
|
|
530
|
-
// OpenAI
|
|
539
|
+
// OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
|
|
540
|
+
// short-context rates (a long-context request costs more), read on 2026-09-29.
|
|
541
|
+
"gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
542
|
+
"gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
543
|
+
"gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
|
|
544
|
+
// OpenAI, older ids: unchanged (the same rates the page above still lists for both).
|
|
531
545
|
"gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
|
|
532
546
|
"gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
|
|
533
547
|
// OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
|
|
@@ -776,6 +790,25 @@ var init_providers = __esm({
|
|
|
776
790
|
}
|
|
777
791
|
});
|
|
778
792
|
|
|
793
|
+
// src/providers/estimate.ts
|
|
794
|
+
function makeEstimateProvider(config, pricing) {
|
|
795
|
+
return {
|
|
796
|
+
complete: () => Promise.reject(
|
|
797
|
+
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
798
|
+
),
|
|
799
|
+
estimateCost: (usage, reportedCostUsd) => ({
|
|
800
|
+
cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
|
|
801
|
+
})
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
var init_estimate = __esm({
|
|
805
|
+
"src/providers/estimate.ts"() {
|
|
806
|
+
"use strict";
|
|
807
|
+
init_cjs_shims();
|
|
808
|
+
init_cost();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
779
812
|
// src/providers/mock.ts
|
|
780
813
|
function makeMockProvider() {
|
|
781
814
|
return {
|
|
@@ -1878,11 +1911,16 @@ __export(run_exports, {
|
|
|
1878
1911
|
function isMockMode(env) {
|
|
1879
1912
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
1880
1913
|
}
|
|
1914
|
+
function resolveProviderFor(config, dryRun) {
|
|
1915
|
+
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
1916
|
+
if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
1917
|
+
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
1918
|
+
}
|
|
1881
1919
|
function buildRealDeps(config, baseDir, dryRun) {
|
|
1882
1920
|
const dir = path5.join(baseDir, ".plune");
|
|
1883
1921
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1884
1922
|
return {
|
|
1885
|
-
resolveProvider:
|
|
1923
|
+
resolveProvider: resolveProviderFor(config, dryRun),
|
|
1886
1924
|
embedder: getDefaultEmbedder(),
|
|
1887
1925
|
cache: dryRun ? NOOP_CACHE : openCache(path5.join(dir, "cache.db")),
|
|
1888
1926
|
now: Date.now,
|
|
@@ -1929,6 +1967,7 @@ var init_run2 = __esm({
|
|
|
1929
1967
|
path5 = __toESM(require("path"), 1);
|
|
1930
1968
|
init_loader();
|
|
1931
1969
|
init_providers();
|
|
1970
|
+
init_estimate();
|
|
1932
1971
|
init_mock();
|
|
1933
1972
|
init_embeddings();
|
|
1934
1973
|
init_cache2();
|
|
@@ -2241,7 +2280,7 @@ function createClient(opts) {
|
|
|
2241
2280
|
const wait = opts.wait ?? sleep;
|
|
2242
2281
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2243
2282
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2244
|
-
async function send(method,
|
|
2283
|
+
async function send(method, path15, payload) {
|
|
2245
2284
|
let last = {
|
|
2246
2285
|
kind: "unavailable",
|
|
2247
2286
|
status: null,
|
|
@@ -2250,7 +2289,7 @@ function createClient(opts) {
|
|
|
2250
2289
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2251
2290
|
let res;
|
|
2252
2291
|
try {
|
|
2253
|
-
res = await doFetch(`${base}${
|
|
2292
|
+
res = await doFetch(`${base}${path15}`, {
|
|
2254
2293
|
method,
|
|
2255
2294
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2256
2295
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2282,9 +2321,9 @@ function createClient(opts) {
|
|
|
2282
2321
|
return { ok: false, ...last };
|
|
2283
2322
|
}
|
|
2284
2323
|
return {
|
|
2285
|
-
post: (
|
|
2286
|
-
upload: (
|
|
2287
|
-
del: (
|
|
2324
|
+
post: (path15, body) => send("POST", path15, { body: JSON.stringify(body), type: "application/json" }),
|
|
2325
|
+
upload: (path15, bytes, contentType) => send("POST", path15, { body: bytes, type: contentType }),
|
|
2326
|
+
del: (path15) => send("DELETE", path15)
|
|
2288
2327
|
};
|
|
2289
2328
|
}
|
|
2290
2329
|
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
@@ -2360,13 +2399,35 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
2360
2399
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
2361
2400
|
return `${path6.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
2362
2401
|
}
|
|
2363
|
-
|
|
2402
|
+
function ciCommit(reported, env = process.env) {
|
|
2403
|
+
const said = typeof reported === "object" && reported !== null ? reported : {};
|
|
2404
|
+
const text2 = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
2405
|
+
const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
|
|
2406
|
+
const job = {
|
|
2407
|
+
sha: provider === void 0 ? void 0 : value(env, provider.sha),
|
|
2408
|
+
branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
|
|
2409
|
+
};
|
|
2410
|
+
const sha = text2(said["commitHash"]);
|
|
2411
|
+
const sameCommit = sha === void 0 || sha === job.sha;
|
|
2412
|
+
const commit = sha ?? job.sha;
|
|
2413
|
+
const branch = text2(said["branch"]) ?? (sameCommit ? job.branch : void 0);
|
|
2414
|
+
return {
|
|
2415
|
+
...commit !== void 0 ? { sha: commit } : {},
|
|
2416
|
+
...branch !== void 0 ? { branch } : {}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
var path6, UNSUPPORTED_VARS, PROVIDERS;
|
|
2364
2420
|
var init_env2 = __esm({
|
|
2365
2421
|
"src/reporter-core/env.ts"() {
|
|
2366
2422
|
"use strict";
|
|
2367
2423
|
init_cjs_shims();
|
|
2368
2424
|
path6 = __toESM(require("path"), 1);
|
|
2369
2425
|
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2426
|
+
PROVIDERS = [
|
|
2427
|
+
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
2428
|
+
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
2429
|
+
{ is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
|
|
2430
|
+
];
|
|
2370
2431
|
}
|
|
2371
2432
|
});
|
|
2372
2433
|
|
|
@@ -2377,6 +2438,8 @@ function appendBatch(file, batch) {
|
|
|
2377
2438
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2378
2439
|
runId: batch.runId,
|
|
2379
2440
|
externalKey: batch.externalKey,
|
|
2441
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
2442
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
2380
2443
|
results: batch.results
|
|
2381
2444
|
};
|
|
2382
2445
|
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -2414,6 +2477,12 @@ function boundKey(key) {
|
|
|
2414
2477
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
2415
2478
|
return { ...key, value: `${head}#${digest}` };
|
|
2416
2479
|
}
|
|
2480
|
+
function whyUnoffered(out) {
|
|
2481
|
+
if (out.ok || out.status !== 429) return "other";
|
|
2482
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
2483
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
2484
|
+
return "other";
|
|
2485
|
+
}
|
|
2417
2486
|
function boundTitle(title) {
|
|
2418
2487
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
2419
2488
|
}
|
|
@@ -2455,6 +2524,7 @@ async function startRun(passed, declared) {
|
|
|
2455
2524
|
`));
|
|
2456
2525
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2457
2526
|
const externalKey = cfg.externalKey ?? null;
|
|
2527
|
+
const sessionMarker = (0, import_node_crypto2.randomUUID)();
|
|
2458
2528
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2459
2529
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2460
2530
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -2506,7 +2576,13 @@ async function startRun(passed, declared) {
|
|
|
2506
2576
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2507
2577
|
}
|
|
2508
2578
|
function defer(results) {
|
|
2509
|
-
appendBatch(fallbackPath, {
|
|
2579
|
+
appendBatch(fallbackPath, {
|
|
2580
|
+
runId,
|
|
2581
|
+
externalKey,
|
|
2582
|
+
session: sessionMarker,
|
|
2583
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2584
|
+
results
|
|
2585
|
+
});
|
|
2510
2586
|
stats.deferred += results.length;
|
|
2511
2587
|
}
|
|
2512
2588
|
async function resolveKeys(values) {
|
|
@@ -2603,6 +2679,7 @@ async function startRun(passed, declared) {
|
|
|
2603
2679
|
});
|
|
2604
2680
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
2605
2681
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
2682
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
2606
2683
|
log(
|
|
2607
2684
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
2608
2685
|
);
|
|
@@ -2959,23 +3036,42 @@ async function handleRunReport(options = {}) {
|
|
|
2959
3036
|
if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
|
|
2960
3037
|
const { client } = connect(options);
|
|
2961
3038
|
const result = { batches: lines.length, sent: 0, failed: 0 };
|
|
3039
|
+
const groups = [];
|
|
3040
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
2962
3041
|
for (const line of lines) {
|
|
2963
|
-
const
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3042
|
+
const group = line.session === void 0 ? void 0 : bySession.get(line.session);
|
|
3043
|
+
if (group !== void 0) {
|
|
3044
|
+
group.push(line);
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
const alone = [line];
|
|
3048
|
+
groups.push(alone);
|
|
3049
|
+
if (line.session !== void 0) bySession.set(line.session, alone);
|
|
3050
|
+
}
|
|
3051
|
+
for (const group of groups) {
|
|
3052
|
+
for (const line of group) {
|
|
3053
|
+
const ready = line.results.filter((r) => !isPending(r));
|
|
3054
|
+
if (line.runId !== null && ready.length > 0) {
|
|
3055
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
|
|
3056
|
+
if (out.ok) result.sent += ready.length;
|
|
3057
|
+
else {
|
|
3058
|
+
result.failed += ready.length;
|
|
3059
|
+
write(
|
|
3060
|
+
`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
2971
3063
|
}
|
|
2972
3064
|
}
|
|
3065
|
+
const pending = group.flatMap((line) => line.results.filter(isPending));
|
|
2973
3066
|
if (pending.length > 0) {
|
|
3067
|
+
const first = group[0];
|
|
2974
3068
|
const session = await startRun({
|
|
2975
3069
|
...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
|
|
2976
3070
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
2977
3071
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2978
|
-
...
|
|
3072
|
+
...first.externalKey !== null ? { externalKey: first.externalKey } : {},
|
|
3073
|
+
// Where the run came from, as the reporter recorded it (plune#927) — never this machine's own.
|
|
3074
|
+
...first.meta !== void 0 ? { meta: first.meta } : {},
|
|
2979
3075
|
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
2980
3076
|
log: write
|
|
2981
3077
|
});
|
|
@@ -2986,15 +3082,34 @@ async function handleRunReport(options = {}) {
|
|
|
2986
3082
|
}
|
|
2987
3083
|
}
|
|
2988
3084
|
write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
|
|
3085
|
+
if (result.failed === 0) {
|
|
3086
|
+
const sentAs = sentName(file, (options.now ?? Date.now)());
|
|
3087
|
+
try {
|
|
3088
|
+
fs7.renameSync(file, sentAs);
|
|
3089
|
+
write(
|
|
3090
|
+
`Every result was sent \u2014 ${file} is now ${sentAs}, so a later replay does not send them twice.`
|
|
3091
|
+
);
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
3094
|
+
write(
|
|
3095
|
+
`Every result was sent, but ${file} could not be renamed (${why}) \u2014 take it away by hand, or a later replay sends them again.`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
2989
3099
|
return result;
|
|
2990
3100
|
}
|
|
2991
|
-
|
|
3101
|
+
function sentName(file, at) {
|
|
3102
|
+
const { dir, name, ext } = path8.parse(file);
|
|
3103
|
+
return path8.join(dir, `${name}.${new Date(at).toISOString().replace(/[:.]/g, "-")}.sent${ext}`);
|
|
3104
|
+
}
|
|
3105
|
+
var import_node_child_process, fs7, path8, NoTokenError, RunCommandError, isPending;
|
|
2992
3106
|
var init_run_lifecycle = __esm({
|
|
2993
3107
|
"src/cli/commands/run-lifecycle.ts"() {
|
|
2994
3108
|
"use strict";
|
|
2995
3109
|
init_cjs_shims();
|
|
2996
3110
|
import_node_child_process = require("child_process");
|
|
2997
3111
|
fs7 = __toESM(require("fs"), 1);
|
|
3112
|
+
path8 = __toESM(require("path"), 1);
|
|
2998
3113
|
init_api_url();
|
|
2999
3114
|
init_credentials();
|
|
3000
3115
|
init_client();
|
|
@@ -3202,7 +3317,12 @@ function errorTextOf(detail) {
|
|
|
3202
3317
|
const head = [detail.attrs["message"], detail.attrs["type"]].filter((s) => s !== void 0 && s !== "");
|
|
3203
3318
|
return [...head, detail.text.trim()].filter((s) => s !== "").join("\n\n");
|
|
3204
3319
|
}
|
|
3205
|
-
function
|
|
3320
|
+
function playwrightOf(classname, name) {
|
|
3321
|
+
if (!SPEC_FILE.test(classname)) return void 0;
|
|
3322
|
+
return { file: classname.replaceAll("\\", "/"), titles: name.split(" \u203A ") };
|
|
3323
|
+
}
|
|
3324
|
+
function keyOf(classname, name, playwright) {
|
|
3325
|
+
if (playwright !== void 0) return { kind: "path-title", value: [playwright.file, ...playwright.titles].join("#") };
|
|
3206
3326
|
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
3207
3327
|
}
|
|
3208
3328
|
function titleOf(classname, name) {
|
|
@@ -3242,12 +3362,15 @@ function readJUnit(source, file) {
|
|
|
3242
3362
|
const name = test.attrs["name"] ?? "";
|
|
3243
3363
|
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
3244
3364
|
const { status, detail } = outcomeOf(test);
|
|
3245
|
-
const
|
|
3365
|
+
const playwright = playwrightOf(classname, name);
|
|
3366
|
+
const key = keyOf(classname, name, playwright);
|
|
3367
|
+
const stated = TOKEN.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
3246
3368
|
const specRef = specRefOf(test, suite, classname);
|
|
3247
3369
|
const errorContext = errorTextOf(detail);
|
|
3248
3370
|
const execution = executionOf(test, suite);
|
|
3249
3371
|
return {
|
|
3250
3372
|
resultKey: resultKey(key.value, 0),
|
|
3373
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3251
3374
|
keys: [key],
|
|
3252
3375
|
source: SOURCE,
|
|
3253
3376
|
rawStatus: status,
|
|
@@ -3261,7 +3384,7 @@ function readJUnit(source, file) {
|
|
|
3261
3384
|
function looksLikeJUnit(source) {
|
|
3262
3385
|
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
3263
3386
|
}
|
|
3264
|
-
var SOURCE;
|
|
3387
|
+
var SOURCE, TOKEN, SPEC_FILE;
|
|
3265
3388
|
var init_junit = __esm({
|
|
3266
3389
|
"src/importers/junit.ts"() {
|
|
3267
3390
|
"use strict";
|
|
@@ -3269,6 +3392,8 @@ var init_junit = __esm({
|
|
|
3269
3392
|
init_result_key();
|
|
3270
3393
|
init_xml();
|
|
3271
3394
|
SOURCE = "junit";
|
|
3395
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3396
|
+
SPEC_FILE = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
3272
3397
|
}
|
|
3273
3398
|
});
|
|
3274
3399
|
|
|
@@ -3312,12 +3437,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
3312
3437
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
3313
3438
|
return text2;
|
|
3314
3439
|
}
|
|
3315
|
-
function pathIn(
|
|
3316
|
-
const parts =
|
|
3440
|
+
function pathIn(path15) {
|
|
3441
|
+
const parts = path15.split(/[\\/]+/);
|
|
3317
3442
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3318
3443
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3319
3444
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
3320
|
-
const escaped = /^\/(?!\/)/.test(
|
|
3445
|
+
const escaped = /^\/(?!\/)/.test(path15) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
3321
3446
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
3322
3447
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3323
3448
|
}
|
|
@@ -3362,12 +3487,12 @@ function placeOf(error, testFile) {
|
|
|
3362
3487
|
}
|
|
3363
3488
|
function fileOf(raw) {
|
|
3364
3489
|
if (!raw.startsWith("file://")) return raw;
|
|
3365
|
-
let
|
|
3490
|
+
let path15 = raw.slice("file://".length);
|
|
3366
3491
|
try {
|
|
3367
|
-
|
|
3492
|
+
path15 = decodeURIComponent(path15);
|
|
3368
3493
|
} catch {
|
|
3369
3494
|
}
|
|
3370
|
-
return /^\/[A-Za-z]:\//.test(
|
|
3495
|
+
return /^\/[A-Za-z]:\//.test(path15) ? path15.slice(1) : path15;
|
|
3371
3496
|
}
|
|
3372
3497
|
function fromRoot(place, root) {
|
|
3373
3498
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -3403,7 +3528,7 @@ var init_failure_detail = __esm({
|
|
|
3403
3528
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3404
3529
|
FRAME_MAX = 8192;
|
|
3405
3530
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3406
|
-
normal = (
|
|
3531
|
+
normal = (path15) => import_node_path3.posix.normalize(path15.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3407
3532
|
}
|
|
3408
3533
|
});
|
|
3409
3534
|
|
|
@@ -3411,7 +3536,7 @@ var init_failure_detail = __esm({
|
|
|
3411
3536
|
function statedIdOf(test, title) {
|
|
3412
3537
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3413
3538
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3414
|
-
return
|
|
3539
|
+
return TOKEN2.exec(title)?.[1];
|
|
3415
3540
|
}
|
|
3416
3541
|
function attemptOf(result, file, ctx) {
|
|
3417
3542
|
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
@@ -3422,10 +3547,10 @@ function attemptOf(result, file, ctx) {
|
|
|
3422
3547
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3423
3548
|
})),
|
|
3424
3549
|
steps: steps(result.steps),
|
|
3425
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
3550
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path15 }) => ({
|
|
3426
3551
|
name: name ?? "",
|
|
3427
3552
|
...contentType !== void 0 ? { contentType } : {},
|
|
3428
|
-
...
|
|
3553
|
+
...path15 !== void 0 ? { path: path15 } : {}
|
|
3429
3554
|
})),
|
|
3430
3555
|
...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
|
|
3431
3556
|
// Spec files are posix and relative to the root dir; the core reads either slash.
|
|
@@ -3507,12 +3632,17 @@ function readPlaywrightJson(source, file) {
|
|
|
3507
3632
|
const out = [];
|
|
3508
3633
|
for (const suite of suites) walk(suite, [], 0, ctx, out);
|
|
3509
3634
|
const ciUrl = webLink(ctx.buildHref);
|
|
3510
|
-
|
|
3635
|
+
const ci = config?.metadata?.ci;
|
|
3636
|
+
return {
|
|
3637
|
+
results: out,
|
|
3638
|
+
...ciUrl !== void 0 ? { ciUrl } : {},
|
|
3639
|
+
...ci !== void 0 ? { ci } : {}
|
|
3640
|
+
};
|
|
3511
3641
|
}
|
|
3512
3642
|
function looksLikePlaywrightJson(source) {
|
|
3513
3643
|
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3514
3644
|
}
|
|
3515
|
-
var import_node_fs4, SOURCE2,
|
|
3645
|
+
var import_node_fs4, SOURCE2, TOKEN2, NOT_FAILED, JsonReportError;
|
|
3516
3646
|
var init_playwright_json = __esm({
|
|
3517
3647
|
"src/importers/playwright-json.ts"() {
|
|
3518
3648
|
"use strict";
|
|
@@ -3521,7 +3651,7 @@ var init_playwright_json = __esm({
|
|
|
3521
3651
|
init_failure_detail();
|
|
3522
3652
|
init_result_key();
|
|
3523
3653
|
SOURCE2 = "playwright";
|
|
3524
|
-
|
|
3654
|
+
TOKEN2 = /@P([A-Za-z0-9_-]+)/;
|
|
3525
3655
|
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3526
3656
|
JsonReportError = class extends Error {
|
|
3527
3657
|
constructor(file, detail) {
|
|
@@ -3590,7 +3720,7 @@ async function handleRunImport(options) {
|
|
|
3590
3720
|
} catch {
|
|
3591
3721
|
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
3592
3722
|
}
|
|
3593
|
-
const { format, results, ciUrl } = readReport(source, options.file, options.format);
|
|
3723
|
+
const { format, results, ciUrl, ci } = readReport(source, options.file, options.format);
|
|
3594
3724
|
if (results.length === 0) {
|
|
3595
3725
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3596
3726
|
}
|
|
@@ -3607,7 +3737,10 @@ async function handleRunImport(options) {
|
|
|
3607
3737
|
...options.create === true ? { offerDiscovered: true } : {},
|
|
3608
3738
|
kind: "automated",
|
|
3609
3739
|
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
3610
|
-
|
|
3740
|
+
// The commit and branch likewise, where the report names a commit; a report that names none —
|
|
3741
|
+
// JUnit's never does — takes this job's own, which is where the tests ran when import follows
|
|
3742
|
+
// them in one job (plune-ai/plune#927).
|
|
3743
|
+
meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {}, ...ciCommit(ci) },
|
|
3611
3744
|
log: write
|
|
3612
3745
|
},
|
|
3613
3746
|
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
@@ -3631,6 +3764,7 @@ async function handleRunImport(options) {
|
|
|
3631
3764
|
offered: stats.offered,
|
|
3632
3765
|
created: stats.created,
|
|
3633
3766
|
unoffered: stats.unoffered,
|
|
3767
|
+
...stats.unofferedWhy !== void 0 ? { unofferedWhy: stats.unofferedWhy } : {},
|
|
3634
3768
|
deferred: stats.deferred,
|
|
3635
3769
|
screenshots: { ...stats.screenshots },
|
|
3636
3770
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
@@ -3681,7 +3815,7 @@ function describe(result, apiUrl) {
|
|
|
3681
3815
|
}
|
|
3682
3816
|
if (result.unoffered > 0) {
|
|
3683
3817
|
lines.push(
|
|
3684
|
-
`${result.unoffered} more could not be offered \u2014
|
|
3818
|
+
`${result.unoffered} more could not be offered \u2014 ${NEXT[result.unofferedWhy ?? "other"]}`
|
|
3685
3819
|
);
|
|
3686
3820
|
}
|
|
3687
3821
|
if (result.unlocatable > 0) {
|
|
@@ -3692,7 +3826,7 @@ function describe(result, apiUrl) {
|
|
|
3692
3826
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3693
3827
|
return lines;
|
|
3694
3828
|
}
|
|
3695
|
-
var fs8;
|
|
3829
|
+
var fs8, NEXT;
|
|
3696
3830
|
var init_run_import = __esm({
|
|
3697
3831
|
"src/cli/commands/run-import.ts"() {
|
|
3698
3832
|
"use strict";
|
|
@@ -3704,6 +3838,11 @@ var init_run_import = __esm({
|
|
|
3704
3838
|
init_api_url();
|
|
3705
3839
|
init_credentials();
|
|
3706
3840
|
init_env2();
|
|
3841
|
+
NEXT = {
|
|
3842
|
+
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
3843
|
+
cases: "the project is at its test case limit. Delete cases you no longer need, or ask an operator to raise the limit, then import this report again to offer the rest.",
|
|
3844
|
+
other: 'the reason is in the "could not offer" line above.'
|
|
3845
|
+
};
|
|
3707
3846
|
}
|
|
3708
3847
|
});
|
|
3709
3848
|
|
|
@@ -3720,7 +3859,7 @@ function isRunResult(v) {
|
|
|
3720
3859
|
}
|
|
3721
3860
|
function handleReport(opts = {}) {
|
|
3722
3861
|
const cwd = opts.cwd ?? process.cwd();
|
|
3723
|
-
const file =
|
|
3862
|
+
const file = path9.join(cwd, ".plune", "last-run.json");
|
|
3724
3863
|
let raw;
|
|
3725
3864
|
try {
|
|
3726
3865
|
raw = fs9.readFileSync(file, "utf8");
|
|
@@ -3738,13 +3877,13 @@ function handleReport(opts = {}) {
|
|
|
3738
3877
|
}
|
|
3739
3878
|
return parsed;
|
|
3740
3879
|
}
|
|
3741
|
-
var fs9,
|
|
3880
|
+
var fs9, path9, ReportNotFoundError;
|
|
3742
3881
|
var init_report = __esm({
|
|
3743
3882
|
"src/cli/commands/report.ts"() {
|
|
3744
3883
|
"use strict";
|
|
3745
3884
|
init_cjs_shims();
|
|
3746
3885
|
fs9 = __toESM(require("fs"), 1);
|
|
3747
|
-
|
|
3886
|
+
path9 = __toESM(require("path"), 1);
|
|
3748
3887
|
ReportNotFoundError = class extends Error {
|
|
3749
3888
|
code = "NO_SAVED_RUN";
|
|
3750
3889
|
constructor(message) {
|
|
@@ -3948,7 +4087,7 @@ async function runInitWizard(cwd) {
|
|
|
3948
4087
|
process.exit(1);
|
|
3949
4088
|
return;
|
|
3950
4089
|
}
|
|
3951
|
-
const defaultModel = provider === "anthropic" ? "claude-
|
|
4090
|
+
const defaultModel = provider === "anthropic" ? "claude-sonnet-5-5" : "gpt-4o-mini";
|
|
3952
4091
|
const model = await clack.text({
|
|
3953
4092
|
message: "Model name:",
|
|
3954
4093
|
defaultValue: defaultModel,
|
|
@@ -3968,7 +4107,7 @@ async function runInitWizard(cwd) {
|
|
|
3968
4107
|
process.exit(1);
|
|
3969
4108
|
return;
|
|
3970
4109
|
}
|
|
3971
|
-
const outputPath =
|
|
4110
|
+
const outputPath = path10.join(cwd, "plune.yaml");
|
|
3972
4111
|
if (fs11.existsSync(outputPath)) {
|
|
3973
4112
|
const overwrite = await clack.confirm({
|
|
3974
4113
|
message: "plune.yaml already exists. Overwrite?"
|
|
@@ -4012,14 +4151,14 @@ async function runInitWizard(cwd) {
|
|
|
4012
4151
|
spin.stop("plune.yaml created!");
|
|
4013
4152
|
clack.outro("plune.yaml is ready.");
|
|
4014
4153
|
}
|
|
4015
|
-
var clack, fs11,
|
|
4154
|
+
var clack, fs11, path10, import_yaml2;
|
|
4016
4155
|
var init_wizard = __esm({
|
|
4017
4156
|
"src/config/init/wizard.ts"() {
|
|
4018
4157
|
"use strict";
|
|
4019
4158
|
init_cjs_shims();
|
|
4020
4159
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
4021
4160
|
fs11 = __toESM(require("fs"), 1);
|
|
4022
|
-
|
|
4161
|
+
path10 = __toESM(require("path"), 1);
|
|
4023
4162
|
import_yaml2 = require("yaml");
|
|
4024
4163
|
init_errors();
|
|
4025
4164
|
}
|
|
@@ -4027,21 +4166,21 @@ var init_wizard = __esm({
|
|
|
4027
4166
|
|
|
4028
4167
|
// src/cli/templates/index.ts
|
|
4029
4168
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
4030
|
-
const target =
|
|
4169
|
+
const target = path11.join(cwd, rel);
|
|
4031
4170
|
if (!force && fs12.existsSync(target)) {
|
|
4032
4171
|
return "skipped";
|
|
4033
4172
|
}
|
|
4034
|
-
fs12.mkdirSync(
|
|
4173
|
+
fs12.mkdirSync(path11.dirname(target), { recursive: true });
|
|
4035
4174
|
fs12.writeFileSync(target, content, "utf8");
|
|
4036
4175
|
return "written";
|
|
4037
4176
|
}
|
|
4038
|
-
var fs12,
|
|
4177
|
+
var fs12, path11, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
4039
4178
|
var init_templates = __esm({
|
|
4040
4179
|
"src/cli/templates/index.ts"() {
|
|
4041
4180
|
"use strict";
|
|
4042
4181
|
init_cjs_shims();
|
|
4043
4182
|
fs12 = __toESM(require("fs"), 1);
|
|
4044
|
-
|
|
4183
|
+
path11 = __toESM(require("path"), 1);
|
|
4045
4184
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
4046
4185
|
# .env is gitignored; .env.example is safe to commit.
|
|
4047
4186
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -4062,7 +4201,7 @@ OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY_HERE
|
|
|
4062
4201
|
version: 1
|
|
4063
4202
|
provider:
|
|
4064
4203
|
type: anthropic
|
|
4065
|
-
model: claude-
|
|
4204
|
+
model: claude-sonnet-5-5
|
|
4066
4205
|
evals:
|
|
4067
4206
|
- id: example
|
|
4068
4207
|
prompt: "Answer concisely. {{question}}"
|
|
@@ -4297,7 +4436,7 @@ __export(sync_exports, {
|
|
|
4297
4436
|
reportSyncFailure: () => reportSyncFailure
|
|
4298
4437
|
});
|
|
4299
4438
|
function readRun(cwd, file) {
|
|
4300
|
-
const target = file !== void 0 ?
|
|
4439
|
+
const target = file !== void 0 ? path12.resolve(cwd, file) : path12.join(cwd, ".plune", "last-run.json");
|
|
4301
4440
|
let raw;
|
|
4302
4441
|
try {
|
|
4303
4442
|
raw = fs13.readFileSync(target, "utf8");
|
|
@@ -4364,13 +4503,13 @@ function reportSyncFailure(err, write) {
|
|
|
4364
4503
|
}
|
|
4365
4504
|
return null;
|
|
4366
4505
|
}
|
|
4367
|
-
var fs13,
|
|
4506
|
+
var fs13, path12, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4368
4507
|
var init_sync = __esm({
|
|
4369
4508
|
"src/cli/commands/sync.ts"() {
|
|
4370
4509
|
"use strict";
|
|
4371
4510
|
init_cjs_shims();
|
|
4372
4511
|
fs13 = __toESM(require("fs"), 1);
|
|
4373
|
-
|
|
4512
|
+
path12 = __toESM(require("path"), 1);
|
|
4374
4513
|
init_run_result_schema();
|
|
4375
4514
|
init_api_url();
|
|
4376
4515
|
init_credentials();
|
|
@@ -4420,9 +4559,9 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
4420
4559
|
let newest;
|
|
4421
4560
|
for (const entry of entries) {
|
|
4422
4561
|
if (!entry.isDirectory()) continue;
|
|
4423
|
-
const dir =
|
|
4562
|
+
const dir = path13.join(runsDir, entry.name);
|
|
4424
4563
|
try {
|
|
4425
|
-
const report = await fs14.lstat(
|
|
4564
|
+
const report = await fs14.lstat(path13.join(dir, "report.json"));
|
|
4426
4565
|
if (!report.isFile()) continue;
|
|
4427
4566
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
4428
4567
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -4441,7 +4580,7 @@ async function readJson(file) {
|
|
|
4441
4580
|
}
|
|
4442
4581
|
}
|
|
4443
4582
|
async function defaultReadEvidence(dir) {
|
|
4444
|
-
const raw = await readJson(
|
|
4583
|
+
const raw = await readJson(path13.join(dir, "api-evidence.json"));
|
|
4445
4584
|
if (!Array.isArray(raw)) return void 0;
|
|
4446
4585
|
return raw.map((r) => {
|
|
4447
4586
|
const rec = asRecord(r);
|
|
@@ -4483,7 +4622,7 @@ function casesFromApi(report, verdicts) {
|
|
|
4483
4622
|
});
|
|
4484
4623
|
}
|
|
4485
4624
|
async function readCairnRun(dir, deps = {}) {
|
|
4486
|
-
const raw = await readJson(
|
|
4625
|
+
const raw = await readJson(path13.join(dir, "report.json"));
|
|
4487
4626
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
4488
4627
|
const report = asRecord(raw);
|
|
4489
4628
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -4513,20 +4652,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
4513
4652
|
return {
|
|
4514
4653
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
4515
4654
|
run: {
|
|
4516
|
-
runId: str(report["runId"],
|
|
4655
|
+
runId: str(report["runId"], path13.basename(dir)),
|
|
4517
4656
|
mode,
|
|
4518
4657
|
target: str(report["url"], "unknown")
|
|
4519
4658
|
},
|
|
4520
4659
|
cases
|
|
4521
4660
|
};
|
|
4522
4661
|
}
|
|
4523
|
-
var fs14,
|
|
4662
|
+
var fs14, path13, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
4524
4663
|
var init_cairn_artifact = __esm({
|
|
4525
4664
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
4526
4665
|
"use strict";
|
|
4527
4666
|
init_cjs_shims();
|
|
4528
4667
|
fs14 = __toESM(require("fs/promises"), 1);
|
|
4529
|
-
|
|
4668
|
+
path13 = __toESM(require("path"), 1);
|
|
4530
4669
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
4531
4670
|
INGEST_SCHEMA_VERSION = 1;
|
|
4532
4671
|
NoCairnRunsError = class extends Error {
|
|
@@ -4693,7 +4832,7 @@ __export(cases_exports, {
|
|
|
4693
4832
|
function gitDirty(file) {
|
|
4694
4833
|
try {
|
|
4695
4834
|
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4696
|
-
cwd:
|
|
4835
|
+
cwd: path14.dirname(file),
|
|
4697
4836
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4698
4837
|
encoding: "utf8"
|
|
4699
4838
|
});
|
|
@@ -4719,9 +4858,9 @@ async function handlePull(deps = {}) {
|
|
|
4719
4858
|
const cwd = deps.cwd ?? process.cwd();
|
|
4720
4859
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4721
4860
|
const token = tokenOf(deps);
|
|
4722
|
-
const file =
|
|
4861
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4723
4862
|
if (fs15.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
4724
|
-
throw new DirtyFileError(
|
|
4863
|
+
throw new DirtyFileError(path14.relative(cwd, file) || file);
|
|
4725
4864
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4726
4865
|
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4727
4866
|
let res;
|
|
@@ -4736,7 +4875,7 @@ async function handlePull(deps = {}) {
|
|
|
4736
4875
|
throw new SyncHttpError(res.status, detail);
|
|
4737
4876
|
}
|
|
4738
4877
|
const text2 = await res.text();
|
|
4739
|
-
fs15.mkdirSync(
|
|
4878
|
+
fs15.mkdirSync(path14.dirname(file), { recursive: true });
|
|
4740
4879
|
fs15.writeFileSync(file, text2, "utf8");
|
|
4741
4880
|
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4742
4881
|
}
|
|
@@ -4744,7 +4883,7 @@ async function handlePush(deps = {}) {
|
|
|
4744
4883
|
const cwd = deps.cwd ?? process.cwd();
|
|
4745
4884
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4746
4885
|
const token = tokenOf(deps);
|
|
4747
|
-
const file =
|
|
4886
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4748
4887
|
let text2;
|
|
4749
4888
|
try {
|
|
4750
4889
|
text2 = fs15.readFileSync(file, "utf8");
|
|
@@ -4798,18 +4937,18 @@ function reportCasesFailure(err, write) {
|
|
|
4798
4937
|
}
|
|
4799
4938
|
return null;
|
|
4800
4939
|
}
|
|
4801
|
-
var import_node_child_process2, fs15,
|
|
4940
|
+
var import_node_child_process2, fs15, path14, DEFAULT_CASES_FILE, DirtyFileError;
|
|
4802
4941
|
var init_cases = __esm({
|
|
4803
4942
|
"src/cli/commands/cases.ts"() {
|
|
4804
4943
|
"use strict";
|
|
4805
4944
|
init_cjs_shims();
|
|
4806
4945
|
import_node_child_process2 = require("child_process");
|
|
4807
4946
|
fs15 = __toESM(require("fs"), 1);
|
|
4808
|
-
|
|
4947
|
+
path14 = __toESM(require("path"), 1);
|
|
4809
4948
|
init_api_url();
|
|
4810
4949
|
init_credentials();
|
|
4811
4950
|
init_sync();
|
|
4812
|
-
DEFAULT_CASES_FILE =
|
|
4951
|
+
DEFAULT_CASES_FILE = path14.join("plune", "cases.md");
|
|
4813
4952
|
DirtyFileError = class extends Error {
|
|
4814
4953
|
constructor(file) {
|
|
4815
4954
|
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
@@ -4945,6 +5084,12 @@ function createProgram() {
|
|
|
4945
5084
|
const program = new import_commander.Command();
|
|
4946
5085
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
4947
5086
|
program.option("-c, --config <path>", "Path to plune.yaml config file (applies to all commands)").option("-v, --verbose", "Verbose output, including stack traces on unexpected errors", false).option("--no-color", "Disable colored output regardless of TTY");
|
|
5087
|
+
program.hook("preAction", async (_program, action) => {
|
|
5088
|
+
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
5089
|
+
const { config } = action.optsWithGlobals();
|
|
5090
|
+
if (config !== void 0) loadEnv2((0, import_node_path4.dirname)((0, import_node_path4.resolve)(config)));
|
|
5091
|
+
loadEnv2(process.cwd());
|
|
5092
|
+
});
|
|
4948
5093
|
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(
|
|
4949
5094
|
"--only <selector>",
|
|
4950
5095
|
"Run a subset by eval id or tag (repeatable)",
|
|
@@ -4955,21 +5100,18 @@ function createProgram() {
|
|
|
4955
5100
|
const globals = command.optsWithGlobals();
|
|
4956
5101
|
const configPath = globals.config;
|
|
4957
5102
|
const [
|
|
4958
|
-
{ loadEnv: loadEnv2 },
|
|
4959
5103
|
{ handleRun: handleRun2 },
|
|
4960
5104
|
{ exitCodeFor: exitCodeFor2, RunConfigError: RunConfigError2 },
|
|
4961
5105
|
{ renderReport: renderReport2 },
|
|
4962
5106
|
{ ConfigNotFoundError: ConfigNotFoundError2, ConfigValidationError: ConfigValidationError2, YamlParseError: YamlParseError2 },
|
|
4963
5107
|
{ AuthError: AuthError2 }
|
|
4964
5108
|
] = await Promise.all([
|
|
4965
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
4966
5109
|
Promise.resolve().then(() => (init_run2(), run_exports)),
|
|
4967
5110
|
Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports)),
|
|
4968
5111
|
Promise.resolve().then(() => (init_reporters(), reporters_exports)),
|
|
4969
5112
|
Promise.resolve().then(() => (init_errors(), errors_exports)),
|
|
4970
5113
|
Promise.resolve().then(() => (init_errors2(), errors_exports2))
|
|
4971
5114
|
]);
|
|
4972
|
-
loadEnv2(configPath !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(configPath)) : process.cwd());
|
|
4973
5115
|
try {
|
|
4974
5116
|
const result = await handleRun2({
|
|
4975
5117
|
dryRun: options.dryRun,
|
|
@@ -5129,13 +5271,11 @@ function createProgram() {
|
|
|
5129
5271
|
);
|
|
5130
5272
|
program.command("report").description("Render the most recent run in a chosen format").option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options, command) => {
|
|
5131
5273
|
const globals = command.optsWithGlobals();
|
|
5132
|
-
const [{
|
|
5133
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
5274
|
+
const [{ handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
5134
5275
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
5135
5276
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
5136
5277
|
]);
|
|
5137
5278
|
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
5138
|
-
loadEnv2(cwd);
|
|
5139
5279
|
try {
|
|
5140
5280
|
const result = handleReport2({ cwd });
|
|
5141
5281
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -5199,10 +5339,10 @@ function createProgram() {
|
|
|
5199
5339
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
5200
5340
|
try {
|
|
5201
5341
|
const token = options.token ?? await readTokenFromStdin();
|
|
5202
|
-
const { path:
|
|
5342
|
+
const { path: path15, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
5203
5343
|
process.stdout.write(
|
|
5204
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
5205
|
-
` : `Token saved to ${
|
|
5344
|
+
verified ? `Logged in. Token checked against the API and saved to ${path15}
|
|
5345
|
+
` : `Token saved to ${path15} WITHOUT being checked (--skip-verify).
|
|
5206
5346
|
`
|
|
5207
5347
|
);
|
|
5208
5348
|
} catch (err) {
|
|
@@ -5219,9 +5359,9 @@ function createProgram() {
|
|
|
5219
5359
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
5220
5360
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
5221
5361
|
try {
|
|
5222
|
-
const { removed, path:
|
|
5362
|
+
const { removed, path: path15 } = handleLogout2();
|
|
5223
5363
|
process.stdout.write(
|
|
5224
|
-
removed ? `Logged out. Removed ${
|
|
5364
|
+
removed ? `Logged out. Removed ${path15}
|
|
5225
5365
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
5226
5366
|
);
|
|
5227
5367
|
} catch (err) {
|