@plune-ai/cli 0.20.0 → 0.21.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.cjs +602 -518
  3. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -3644,6 +3644,477 @@ var init_run_lifecycle = __esm({
3644
3644
  }
3645
3645
  });
3646
3646
 
3647
+ // src/types/run-result-schema.ts
3648
+ function parseRunResult(value2) {
3649
+ return runResultSchema.parse(value2);
3650
+ }
3651
+ var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
3652
+ var init_run_result_schema = __esm({
3653
+ "src/types/run-result-schema.ts"() {
3654
+ "use strict";
3655
+ init_cjs_shims();
3656
+ import_zod3 = require("zod");
3657
+ init_results();
3658
+ RUN_RESULT_SCHEMA_VERSION = 1;
3659
+ binaryVerdictSchema = import_zod3.z.object({
3660
+ question: import_zod3.z.string().min(1),
3661
+ passed: import_zod3.z.boolean()
3662
+ });
3663
+ assertionResultRecordSchema2 = import_zod3.z.object({
3664
+ type: import_zod3.z.string(),
3665
+ passed: import_zod3.z.boolean(),
3666
+ score: import_zod3.z.number().optional(),
3667
+ reason: import_zod3.z.string().optional(),
3668
+ binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
3669
+ });
3670
+ usageSchema = import_zod3.z.object({
3671
+ input_tokens: import_zod3.z.number(),
3672
+ output_tokens: import_zod3.z.number(),
3673
+ cost_usd: import_zod3.z.number()
3674
+ });
3675
+ runErrorSchema = import_zod3.z.object({
3676
+ code: import_zod3.z.string(),
3677
+ message: import_zod3.z.string()
3678
+ });
3679
+ rowResultSchema = import_zod3.z.object({
3680
+ vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
3681
+ output: import_zod3.z.string().nullable(),
3682
+ cached: import_zod3.z.boolean(),
3683
+ usage: usageSchema.optional(),
3684
+ latency_ms: import_zod3.z.number().optional(),
3685
+ error: runErrorSchema.optional(),
3686
+ assertions: import_zod3.z.array(assertionResultRecordSchema2),
3687
+ /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
3688
+ inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
3689
+ });
3690
+ summarySchema = import_zod3.z.object({
3691
+ total: import_zod3.z.number(),
3692
+ passed: import_zod3.z.number(),
3693
+ failed: import_zod3.z.number(),
3694
+ errored: import_zod3.z.number(),
3695
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
3696
+ blocked: import_zod3.z.number().optional(),
3697
+ skipped: import_zod3.z.number().optional(),
3698
+ cost_usd: import_zod3.z.number(),
3699
+ duration_ms: import_zod3.z.number()
3700
+ });
3701
+ evalResultSchema = import_zod3.z.object({
3702
+ id: import_zod3.z.string(),
3703
+ tags: import_zod3.z.array(import_zod3.z.string()),
3704
+ rows: import_zod3.z.array(rowResultSchema),
3705
+ passed: import_zod3.z.boolean()
3706
+ });
3707
+ runResultSchema = import_zod3.z.object({
3708
+ schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
3709
+ plune_version: import_zod3.z.string(),
3710
+ started_at: import_zod3.z.string(),
3711
+ finished_at: import_zod3.z.string(),
3712
+ config_hash: import_zod3.z.string(),
3713
+ summary: summarySchema,
3714
+ evals: import_zod3.z.array(evalResultSchema)
3715
+ });
3716
+ }
3717
+ });
3718
+
3719
+ // src/cli/commands/sync.ts
3720
+ var sync_exports = {};
3721
+ __export(sync_exports, {
3722
+ NotLoggedInError: () => NotLoggedInError,
3723
+ SyncFileError: () => SyncFileError,
3724
+ SyncHttpError: () => SyncHttpError,
3725
+ SyncNetworkError: () => SyncNetworkError,
3726
+ TokenRejectedError: () => TokenRejectedError,
3727
+ handleSync: () => handleSync,
3728
+ reportSyncFailure: () => reportSyncFailure
3729
+ });
3730
+ function readRun(cwd, file) {
3731
+ const target = file !== void 0 ? path9.resolve(cwd, file) : path9.join(cwd, ".plune", "last-run.json");
3732
+ let raw;
3733
+ try {
3734
+ raw = fs8.readFileSync(target, "utf8");
3735
+ } catch {
3736
+ throw new SyncFileError(
3737
+ file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
3738
+ );
3739
+ }
3740
+ let json;
3741
+ try {
3742
+ json = JSON.parse(raw);
3743
+ } catch {
3744
+ throw new SyncFileError(`Run file is not valid JSON: ${target}`);
3745
+ }
3746
+ try {
3747
+ return parseRunResult(json);
3748
+ } catch {
3749
+ throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
3750
+ }
3751
+ }
3752
+ async function safeErrorDetail(res) {
3753
+ try {
3754
+ const body = await res.json();
3755
+ return typeof body.error === "string" ? body.error : "";
3756
+ } catch {
3757
+ return "";
3758
+ }
3759
+ }
3760
+ async function handleSync(deps = {}) {
3761
+ const cwd = deps.cwd ?? process.cwd();
3762
+ const apiUrl = resolveApiUrl(deps.apiUrl);
3763
+ const token = (deps.loadToken ?? loadToken)();
3764
+ if (token === null) throw new NotLoggedInError();
3765
+ const run = readRun(cwd, deps.file);
3766
+ const doFetch = deps.fetchImpl ?? fetch;
3767
+ const endpoint = `${apiUrl}/v1/runs`;
3768
+ let res;
3769
+ try {
3770
+ res = await doFetch(endpoint, {
3771
+ method: "POST",
3772
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
3773
+ body: JSON.stringify(run)
3774
+ });
3775
+ } catch {
3776
+ throw new SyncNetworkError(apiUrl);
3777
+ }
3778
+ if (!res.ok) {
3779
+ const detail = await safeErrorDetail(res);
3780
+ if (res.status === 401) throw new TokenRejectedError();
3781
+ throw new SyncHttpError(res.status, detail);
3782
+ }
3783
+ const body = await res.json().catch(() => ({}));
3784
+ const id = typeof body.id === "string" ? body.id : "";
3785
+ const result = { id, url: `${endpoint}/${id}` };
3786
+ if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
3787
+ return result;
3788
+ }
3789
+ async function readVerdict(doFetch, url, token) {
3790
+ try {
3791
+ const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
3792
+ const body = await res.json().catch(() => void 0);
3793
+ return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
3794
+ } catch {
3795
+ return void 0;
3796
+ }
3797
+ }
3798
+ function reportSyncFailure(err, write) {
3799
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
3800
+ write(err.message + "\n");
3801
+ return 2;
3802
+ }
3803
+ if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
3804
+ write(err.message + "\n");
3805
+ return 1;
3806
+ }
3807
+ return null;
3808
+ }
3809
+ var fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
3810
+ var init_sync = __esm({
3811
+ "src/cli/commands/sync.ts"() {
3812
+ "use strict";
3813
+ init_cjs_shims();
3814
+ fs8 = __toESM(require("fs"), 1);
3815
+ path9 = __toESM(require("path"), 1);
3816
+ init_run_result_schema();
3817
+ init_api_url();
3818
+ init_credentials();
3819
+ init_gate();
3820
+ NotLoggedInError = class extends Error {
3821
+ constructor() {
3822
+ super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
3823
+ this.name = "NotLoggedInError";
3824
+ }
3825
+ };
3826
+ TokenRejectedError = class extends Error {
3827
+ constructor() {
3828
+ super(
3829
+ 'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
3830
+ );
3831
+ this.name = "TokenRejectedError";
3832
+ }
3833
+ };
3834
+ SyncFileError = class extends Error {
3835
+ constructor(message) {
3836
+ super(message);
3837
+ this.name = "SyncFileError";
3838
+ }
3839
+ };
3840
+ SyncNetworkError = class extends Error {
3841
+ constructor(apiUrl) {
3842
+ super(
3843
+ `Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
3844
+ );
3845
+ this.name = "SyncNetworkError";
3846
+ }
3847
+ };
3848
+ SyncHttpError = class extends Error {
3849
+ constructor(status, detail) {
3850
+ super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
3851
+ this.status = status;
3852
+ this.name = "SyncHttpError";
3853
+ }
3854
+ status;
3855
+ };
3856
+ }
3857
+ });
3858
+
3859
+ // src/cli/commands/plan.ts
3860
+ var plan_exports = {};
3861
+ __export(plan_exports, {
3862
+ RunRefError: () => RunRefError,
3863
+ formatGrep: () => formatGrep,
3864
+ grepFragment: () => grepFragment,
3865
+ handlePlanGrep: () => handlePlanGrep,
3866
+ handleRunGrep: () => handleRunGrep,
3867
+ runGrepSummary: () => runGrepSummary
3868
+ });
3869
+ function grepFragment(c) {
3870
+ const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
3871
+ const segments = pathTitle?.split("#") ?? [c.title];
3872
+ return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
3873
+ }
3874
+ function formatGrep(cases) {
3875
+ if (cases.length === 0) return "(?!)";
3876
+ return cases.map(grepFragment).join("|");
3877
+ }
3878
+ async function safeErrorDetail2(res) {
3879
+ try {
3880
+ const body = await res.json();
3881
+ return typeof body.error === "string" ? body.error : "";
3882
+ } catch {
3883
+ return "";
3884
+ }
3885
+ }
3886
+ function platformGet(deps) {
3887
+ const apiUrl = resolveApiUrl(deps.apiUrl);
3888
+ const token = (deps.loadToken ?? loadToken)();
3889
+ if (token === null) throw new NotLoggedInError();
3890
+ const doFetch = deps.fetchImpl ?? fetch;
3891
+ return async (path17) => {
3892
+ let res;
3893
+ try {
3894
+ res = await doFetch(`${apiUrl}${path17}`, { headers: { authorization: `Bearer ${token}` } });
3895
+ } catch {
3896
+ throw new SyncNetworkError(apiUrl);
3897
+ }
3898
+ if (!res.ok) {
3899
+ const detail = await safeErrorDetail2(res);
3900
+ if (res.status === 401) throw new TokenRejectedError();
3901
+ throw new SyncHttpError(res.status, detail);
3902
+ }
3903
+ return await res.json();
3904
+ };
3905
+ }
3906
+ async function handlePlanGrep(deps) {
3907
+ const get = platformGet(deps);
3908
+ const { cases } = await get(
3909
+ `/v1/plans/${encodeURIComponent(deps.planId)}/cases`
3910
+ );
3911
+ return { pattern: formatGrep(cases), cases: cases.length, keyed: keyedOf(cases) };
3912
+ }
3913
+ async function handleRunGrep(deps) {
3914
+ const byKey = RUN_KEY.test(deps.run);
3915
+ if (!byKey && !RUN_ID.test(deps.run)) {
3916
+ throw new RunRefError(
3917
+ `${JSON.stringify(deps.run)} is not a run \u2014 pass its id (a UUID, as in the run page's address) or its key (rk_ and ten hex, the PLUNE_RUN Plune shows).`,
3918
+ 2
3919
+ );
3920
+ }
3921
+ const get = platformGet(deps);
3922
+ let runId = deps.run;
3923
+ if (byKey) {
3924
+ const { runs } = await get(`/v1/runs?key=${deps.run}&limit=1`);
3925
+ const run = runs[0];
3926
+ if (run === void 0) {
3927
+ throw new RunRefError(
3928
+ `No run carries the key ${deps.run} \u2014 a key becomes a run's when the run is saved or launched in Plune.`,
3929
+ 1
3930
+ );
3931
+ }
3932
+ runId = run.id;
3933
+ }
3934
+ const { cases } = await get(
3935
+ `/v1/runs/${encodeURIComponent(runId)}/cases${deps.failed === true ? "?status=failed,broken" : ""}`
3936
+ );
3937
+ const automated = cases.filter((c) => c.type === "automated");
3938
+ return {
3939
+ pattern: formatGrep(automated),
3940
+ cases: automated.length,
3941
+ keyed: keyedOf(automated),
3942
+ skipped: cases.length - automated.length
3943
+ };
3944
+ }
3945
+ function runGrepSummary(out, failed) {
3946
+ if (out.cases === 0) {
3947
+ const none = failed ? "no automated case failed or broke in the run" : "the run holds no automated case";
3948
+ return `plune: ${none} (${out.skipped} manual left out) \u2014 the pattern matches nothing.
3949
+ `;
3950
+ }
3951
+ const which = failed ? "that failed or broke in the run" : "of the run";
3952
+ return `plune: ${out.cases} automated case(s) ${which}, ${out.keyed} with a test key, the rest by title; ${out.skipped} manual left out.
3953
+ `;
3954
+ }
3955
+ var escape, BOUNDARY, keyedOf, RUN_ID, RUN_KEY, RunRefError;
3956
+ var init_plan = __esm({
3957
+ "src/cli/commands/plan.ts"() {
3958
+ "use strict";
3959
+ init_cjs_shims();
3960
+ init_api_url();
3961
+ init_credentials();
3962
+ init_sync();
3963
+ escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3964
+ BOUNDARY = "[ >#]+";
3965
+ keyedOf = (cases) => cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length;
3966
+ RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3967
+ RUN_KEY = /^rk_[0-9a-f]{10}$/;
3968
+ RunRefError = class extends Error {
3969
+ constructor(message, exitCode) {
3970
+ super(message);
3971
+ this.exitCode = exitCode;
3972
+ this.name = "RunRefError";
3973
+ }
3974
+ exitCode;
3975
+ };
3976
+ }
3977
+ });
3978
+
3979
+ // src/cli/commands/cases.ts
3980
+ var cases_exports = {};
3981
+ __export(cases_exports, {
3982
+ DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
3983
+ DirtyFileError: () => DirtyFileError,
3984
+ formatReport: () => formatReport,
3985
+ handlePull: () => handlePull,
3986
+ handlePush: () => handlePush,
3987
+ reportCasesFailure: () => reportCasesFailure
3988
+ });
3989
+ function gitDirty(file) {
3990
+ try {
3991
+ const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
3992
+ cwd: path10.dirname(file),
3993
+ stdio: ["ignore", "pipe", "ignore"],
3994
+ encoding: "utf8"
3995
+ });
3996
+ return out.trim() !== "";
3997
+ } catch {
3998
+ return false;
3999
+ }
4000
+ }
4001
+ async function safeErrorDetail3(res) {
4002
+ try {
4003
+ const body = await res.json();
4004
+ return typeof body.error === "string" ? body.error : "";
4005
+ } catch {
4006
+ return "";
4007
+ }
4008
+ }
4009
+ function tokenOf(deps) {
4010
+ const token = (deps.loadToken ?? loadToken)();
4011
+ if (token === null) throw new NotLoggedInError();
4012
+ return token;
4013
+ }
4014
+ async function handlePull(deps = {}) {
4015
+ const cwd = deps.cwd ?? process.cwd();
4016
+ const apiUrl = resolveApiUrl(deps.apiUrl);
4017
+ const token = tokenOf(deps);
4018
+ const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
4019
+ if (fs9.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
4020
+ throw new DirtyFileError(path10.relative(cwd, file) || file);
4021
+ const doFetch = deps.fetchImpl ?? fetch;
4022
+ const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
4023
+ let res;
4024
+ try {
4025
+ res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
4026
+ } catch {
4027
+ throw new SyncNetworkError(apiUrl);
4028
+ }
4029
+ if (!res.ok) {
4030
+ const detail = await safeErrorDetail3(res);
4031
+ if (res.status === 401) throw new TokenRejectedError();
4032
+ throw new SyncHttpError(res.status, detail);
4033
+ }
4034
+ const text2 = await res.text();
4035
+ fs9.mkdirSync(path10.dirname(file), { recursive: true });
4036
+ fs9.writeFileSync(file, text2, "utf8");
4037
+ return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
4038
+ }
4039
+ async function handlePush(deps = {}) {
4040
+ const cwd = deps.cwd ?? process.cwd();
4041
+ const apiUrl = resolveApiUrl(deps.apiUrl);
4042
+ const token = tokenOf(deps);
4043
+ const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
4044
+ let text2;
4045
+ try {
4046
+ text2 = fs9.readFileSync(file, "utf8");
4047
+ } catch {
4048
+ throw new SyncFileError(
4049
+ `No document to push: ${file}. Run "plune pull" first (or pass a file).`
4050
+ );
4051
+ }
4052
+ if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
4053
+ const doFetch = deps.fetchImpl ?? fetch;
4054
+ let res;
4055
+ try {
4056
+ res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
4057
+ method: "POST",
4058
+ headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
4059
+ body: text2
4060
+ });
4061
+ } catch {
4062
+ throw new SyncNetworkError(apiUrl);
4063
+ }
4064
+ if (!res.ok) {
4065
+ const detail = await safeErrorDetail3(res);
4066
+ if (res.status === 401) throw new TokenRejectedError();
4067
+ throw new SyncHttpError(res.status, detail);
4068
+ }
4069
+ return await res.json();
4070
+ }
4071
+ function formatReport(report) {
4072
+ const verb = (past, future) => report.dryRun ? future : past;
4073
+ const lines = [
4074
+ `${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
4075
+ ];
4076
+ for (const r of report.refused)
4077
+ lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
4078
+ for (const w of report.warnings)
4079
+ lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
4080
+ return lines.join("\n") + "\n";
4081
+ }
4082
+ function reportCasesFailure(err, write) {
4083
+ if (err instanceof DirtyFileError) {
4084
+ write(err.message + "\n");
4085
+ return 3;
4086
+ }
4087
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
4088
+ write(err.message + "\n");
4089
+ return 2;
4090
+ }
4091
+ if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
4092
+ write(err.message + "\n");
4093
+ return 1;
4094
+ }
4095
+ return null;
4096
+ }
4097
+ var import_node_child_process2, fs9, path10, DEFAULT_CASES_FILE, DirtyFileError;
4098
+ var init_cases = __esm({
4099
+ "src/cli/commands/cases.ts"() {
4100
+ "use strict";
4101
+ init_cjs_shims();
4102
+ import_node_child_process2 = require("child_process");
4103
+ fs9 = __toESM(require("fs"), 1);
4104
+ path10 = __toESM(require("path"), 1);
4105
+ init_api_url();
4106
+ init_credentials();
4107
+ init_sync();
4108
+ DEFAULT_CASES_FILE = path10.join("plune", "cases.md");
4109
+ DirtyFileError = class extends Error {
4110
+ constructor(file) {
4111
+ super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
4112
+ this.name = "DirtyFileError";
4113
+ }
4114
+ };
4115
+ }
4116
+ });
4117
+
3647
4118
  // src/reporter-core/result-key.ts
3648
4119
  function resultKey(testId, retry) {
3649
4120
  const plain = `${testId}#${retry}`;
@@ -3662,33 +4133,33 @@ var init_result_key = __esm({
3662
4133
 
3663
4134
  // src/importers/attachments.ts
3664
4135
  function reportFolder(report) {
3665
- const folder = path9.dirname(path9.resolve(report));
4136
+ const folder = path11.dirname(path11.resolve(report));
3666
4137
  try {
3667
- return fs8.realpathSync(folder);
4138
+ return fs10.realpathSync(folder);
3668
4139
  } catch {
3669
4140
  return folder;
3670
4141
  }
3671
4142
  }
3672
4143
  function reportFolders(report) {
3673
- return { lexical: path9.dirname(path9.resolve(report)), real: reportFolder(report) };
4144
+ return { lexical: path11.dirname(path11.resolve(report)), real: reportFolder(report) };
3674
4145
  }
3675
4146
  function within(folder, target) {
3676
- const rel = path9.relative(folder, target);
3677
- return rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
4147
+ const rel = path11.relative(folder, target);
4148
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${path11.sep}`) && !path11.isAbsolute(rel);
3678
4149
  }
3679
4150
  function resolveAttachment(folder, ref, lexical = folder) {
3680
4151
  if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
3681
- const face = path9.resolve(lexical, ref);
4152
+ const face = path11.resolve(lexical, ref);
3682
4153
  if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
3683
4154
  let real;
3684
4155
  try {
3685
- real = fs8.realpathSync(path9.resolve(folder, ref));
4156
+ real = fs10.realpathSync(path11.resolve(folder, ref));
3686
4157
  } catch {
3687
4158
  return { why: "absent" };
3688
4159
  }
3689
4160
  if (!within(folder, real)) return { why: "outside" };
3690
4161
  try {
3691
- return fs8.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
4162
+ return fs10.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
3692
4163
  } catch {
3693
4164
  return { why: "absent" };
3694
4165
  }
@@ -3720,18 +4191,18 @@ function filesOfTest(test, folder, title, missing, options = {}) {
3720
4191
  }
3721
4192
  const real = found.file;
3722
4193
  if (!files.some((f) => f.path === real)) {
3723
- files.push({ name: printable(ref.split(/[\\/]/).pop() || path9.basename(real)), contentType: typeOfFile(real), path: real });
4194
+ files.push({ name: printable(ref.split(/[\\/]/).pop() || path11.basename(real)), contentType: typeOfFile(real), path: real });
3724
4195
  }
3725
4196
  }
3726
4197
  return files;
3727
4198
  }
3728
- var fs8, path9, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
4199
+ var fs10, path11, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
3729
4200
  var init_attachments = __esm({
3730
4201
  "src/importers/attachments.ts"() {
3731
4202
  "use strict";
3732
4203
  init_cjs_shims();
3733
- fs8 = __toESM(require("fs"), 1);
3734
- path9 = __toESM(require("path"), 1);
4204
+ fs10 = __toESM(require("fs"), 1);
4205
+ path11 = __toESM(require("path"), 1);
3735
4206
  init_failure_detail();
3736
4207
  MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
3737
4208
  TYPES = {
@@ -3755,7 +4226,7 @@ var init_attachments = __esm({
3755
4226
  ".mp4": "video/mp4",
3756
4227
  ".har": "application/json"
3757
4228
  };
3758
- typeOfFile = (file) => TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream";
4229
+ typeOfFile = (file) => TYPES[path11.extname(file).toLowerCase()] ?? "application/octet-stream";
3759
4230
  MARKERS_PER_TEST = 100;
3760
4231
  }
3761
4232
  });
@@ -3763,7 +4234,7 @@ var init_attachments = __esm({
3763
4234
  // src/importers/allure.ts
3764
4235
  function isFolder(path17) {
3765
4236
  try {
3766
- return fs9.statSync(path17).isDirectory();
4237
+ return fs11.statSync(path17).isDirectory();
3767
4238
  } catch {
3768
4239
  return false;
3769
4240
  }
@@ -3771,7 +4242,7 @@ function isFolder(path17) {
3771
4242
  function load(file, maxBytes) {
3772
4243
  let size;
3773
4244
  try {
3774
- size = fs9.statSync(file).size;
4245
+ size = fs11.statSync(file).size;
3775
4246
  } catch (err) {
3776
4247
  throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
3777
4248
  }
@@ -3780,7 +4251,7 @@ function load(file, maxBytes) {
3780
4251
  }
3781
4252
  let parsed;
3782
4253
  try {
3783
- parsed = JSON.parse(fs9.readFileSync(file, "utf8"));
4254
+ parsed = JSON.parse(fs11.readFileSync(file, "utf8"));
3784
4255
  } catch (err) {
3785
4256
  throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
3786
4257
  }
@@ -3837,9 +4308,9 @@ function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
3837
4308
  `${printable(dir)} \u2014 no *-result.json files in it, so there is no Allure report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), or at a JUnit or Playwright JSON file.`
3838
4309
  );
3839
4310
  }
3840
- const { lexical, real: folder } = reportFolders(path10.join(dir, "_"));
3841
- const tests = resultNames.map((n) => load(path10.join(dir, n), maxBytes));
3842
- const containers = namesIn(dir, CONTAINER).map((n) => load(path10.join(dir, n), maxBytes));
4311
+ const { lexical, real: folder } = reportFolders(path12.join(dir, "_"));
4312
+ const tests = resultNames.map((n) => load(path12.join(dir, n), maxBytes));
4313
+ const containers = namesIn(dir, CONTAINER).map((n) => load(path12.join(dir, n), maxBytes));
3843
4314
  const direct = /* @__PURE__ */ new Map();
3844
4315
  for (const c of containers) {
3845
4316
  for (const child of Array.isArray(c.children) ? c.children : []) {
@@ -3907,7 +4378,7 @@ function pendingOf(r, retry, ctx) {
3907
4378
  }
3908
4379
  if (files.some((f) => f.path === found.file)) return;
3909
4380
  const type = textOf(att.type).trim();
3910
- const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path10.basename(found.file));
4381
+ const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path12.basename(found.file));
3911
4382
  files.push({ name: name2, contentType: type !== "" ? type : typeOfFile(found.file), path: found.file });
3912
4383
  };
3913
4384
  const holders = [];
@@ -3966,13 +4437,13 @@ function pendingOf(r, retry, ctx) {
3966
4437
  ...files.length > 0 ? { files } : {}
3967
4438
  };
3968
4439
  }
3969
- var fs9, path10, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
4440
+ var fs11, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
3970
4441
  var init_allure = __esm({
3971
4442
  "src/importers/allure.ts"() {
3972
4443
  "use strict";
3973
4444
  init_cjs_shims();
3974
- fs9 = __toESM(require("fs"), 1);
3975
- path10 = __toESM(require("path"), 1);
4445
+ fs11 = __toESM(require("fs"), 1);
4446
+ path12 = __toESM(require("path"), 1);
3976
4447
  init_failure_detail();
3977
4448
  init_result_key();
3978
4449
  init_attachments();
@@ -3989,7 +4460,7 @@ var init_allure = __esm({
3989
4460
  MAX_JSON_BYTES = 64 * 1024 * 1024;
3990
4461
  MAX_DEPTH = 256;
3991
4462
  MAX_TIME = 864e13;
3992
- namesIn = (dir, pattern) => fs9.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
4463
+ namesIn = (dir, pattern) => fs11.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
3993
4464
  timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
3994
4465
  textOf = (v) => typeof v === "string" ? v : "";
3995
4466
  labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
@@ -4463,7 +4934,7 @@ async function handleRunImport(options) {
4463
4934
  } else {
4464
4935
  let source;
4465
4936
  try {
4466
- source = fs10.readFileSync(options.file, "utf8");
4937
+ source = fs12.readFileSync(options.file, "utf8");
4467
4938
  } catch {
4468
4939
  throw new RunCommandError(`Cannot read ${options.file}.`);
4469
4940
  }
@@ -4594,12 +5065,12 @@ function describe(result, apiUrl) {
4594
5065
  if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
4595
5066
  return lines;
4596
5067
  }
4597
- var fs10, NEXT;
5068
+ var fs12, NEXT;
4598
5069
  var init_run_import = __esm({
4599
5070
  "src/cli/commands/run-import.ts"() {
4600
5071
  "use strict";
4601
5072
  init_cjs_shims();
4602
- fs10 = __toESM(require("fs"), 1);
5073
+ fs12 = __toESM(require("fs"), 1);
4603
5074
  init_session();
4604
5075
  init_failure_detail();
4605
5076
  init_importers();
@@ -4629,10 +5100,10 @@ function isRunResult(v) {
4629
5100
  }
4630
5101
  function handleReport(opts = {}) {
4631
5102
  const cwd = opts.cwd ?? process.cwd();
4632
- const file = path11.join(cwd, ".plune", "last-run.json");
5103
+ const file = path13.join(cwd, ".plune", "last-run.json");
4633
5104
  let raw;
4634
5105
  try {
4635
- raw = fs11.readFileSync(file, "utf8");
5106
+ raw = fs13.readFileSync(file, "utf8");
4636
5107
  } catch {
4637
5108
  throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
4638
5109
  }
@@ -4647,13 +5118,13 @@ function handleReport(opts = {}) {
4647
5118
  }
4648
5119
  return parsed;
4649
5120
  }
4650
- var fs11, path11, ReportNotFoundError;
5121
+ var fs13, path13, ReportNotFoundError;
4651
5122
  var init_report = __esm({
4652
5123
  "src/cli/commands/report.ts"() {
4653
5124
  "use strict";
4654
5125
  init_cjs_shims();
4655
- fs11 = __toESM(require("fs"), 1);
4656
- path11 = __toESM(require("path"), 1);
5126
+ fs13 = __toESM(require("fs"), 1);
5127
+ path13 = __toESM(require("path"), 1);
4657
5128
  ReportNotFoundError = class extends Error {
4658
5129
  code = "NO_SAVED_RUN";
4659
5130
  constructor(message) {
@@ -4723,7 +5194,7 @@ function isRunResult2(v) {
4723
5194
  function loadRunResult(file, label) {
4724
5195
  let raw;
4725
5196
  try {
4726
- raw = fs12.readFileSync(file, "utf8");
5197
+ raw = fs14.readFileSync(file, "utf8");
4727
5198
  } catch {
4728
5199
  throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
4729
5200
  }
@@ -4743,12 +5214,12 @@ function handleDiff(opts) {
4743
5214
  const current = loadRunResult(opts.currentPath, "current");
4744
5215
  return diffRuns(baseline, current);
4745
5216
  }
4746
- var fs12, DiffInputError;
5217
+ var fs14, DiffInputError;
4747
5218
  var init_diff2 = __esm({
4748
5219
  "src/cli/commands/diff.ts"() {
4749
5220
  "use strict";
4750
5221
  init_cjs_shims();
4751
- fs12 = __toESM(require("fs"), 1);
5222
+ fs14 = __toESM(require("fs"), 1);
4752
5223
  init_diff();
4753
5224
  DiffInputError = class extends Error {
4754
5225
  code = "DIFF_INPUT";
@@ -4873,8 +5344,8 @@ async function runInitWizard(cwd) {
4873
5344
  process.exit(1);
4874
5345
  return;
4875
5346
  }
4876
- const outputPath = path12.join(cwd, "plune.yaml");
4877
- if (fs13.existsSync(outputPath)) {
5347
+ const outputPath = path14.join(cwd, "plune.yaml");
5348
+ if (fs15.existsSync(outputPath)) {
4878
5349
  const overwrite = await clack.confirm({
4879
5350
  message: "plune.yaml already exists. Overwrite?"
4880
5351
  });
@@ -4913,18 +5384,18 @@ async function runInitWizard(cwd) {
4913
5384
  `;
4914
5385
  const spin = clack.spinner();
4915
5386
  spin.start("Writing plune.yaml\u2026");
4916
- fs13.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
5387
+ fs15.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
4917
5388
  spin.stop("plune.yaml created!");
4918
5389
  clack.outro("plune.yaml is ready.");
4919
5390
  }
4920
- var clack, fs13, path12, import_yaml2;
5391
+ var clack, fs15, path14, import_yaml2;
4921
5392
  var init_wizard = __esm({
4922
5393
  "src/config/init/wizard.ts"() {
4923
5394
  "use strict";
4924
5395
  init_cjs_shims();
4925
5396
  clack = __toESM(require("@clack/prompts"), 1);
4926
- fs13 = __toESM(require("fs"), 1);
4927
- path12 = __toESM(require("path"), 1);
5397
+ fs15 = __toESM(require("fs"), 1);
5398
+ path14 = __toESM(require("path"), 1);
4928
5399
  import_yaml2 = require("yaml");
4929
5400
  init_errors();
4930
5401
  }
@@ -4932,21 +5403,21 @@ var init_wizard = __esm({
4932
5403
 
4933
5404
  // src/cli/templates/index.ts
4934
5405
  function writeTemplateFile(cwd, rel, content, force) {
4935
- const target = path13.join(cwd, rel);
4936
- if (!force && fs14.existsSync(target)) {
5406
+ const target = path15.join(cwd, rel);
5407
+ if (!force && fs16.existsSync(target)) {
4937
5408
  return "skipped";
4938
5409
  }
4939
- fs14.mkdirSync(path13.dirname(target), { recursive: true });
4940
- fs14.writeFileSync(target, content, "utf8");
5410
+ fs16.mkdirSync(path15.dirname(target), { recursive: true });
5411
+ fs16.writeFileSync(target, content, "utf8");
4941
5412
  return "written";
4942
5413
  }
4943
- var fs14, path13, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
5414
+ var fs16, path15, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
4944
5415
  var init_templates = __esm({
4945
5416
  "src/cli/templates/index.ts"() {
4946
5417
  "use strict";
4947
5418
  init_cjs_shims();
4948
- fs14 = __toESM(require("fs"), 1);
4949
- path13 = __toESM(require("path"), 1);
5419
+ fs16 = __toESM(require("fs"), 1);
5420
+ path15 = __toESM(require("path"), 1);
4950
5421
  ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
4951
5422
  # .env is gitignored; .env.example is safe to commit.
4952
5423
  # Plune reads whichever key matches your configured provider.
@@ -5032,7 +5503,7 @@ __export(login_exports, {
5032
5503
  EmptyTokenError: () => EmptyTokenError,
5033
5504
  LoginHttpError: () => LoginHttpError,
5034
5505
  LoginNetworkError: () => LoginNetworkError,
5035
- TokenRejectedError: () => TokenRejectedError,
5506
+ TokenRejectedError: () => TokenRejectedError2,
5036
5507
  handleLogin: () => handleLogin,
5037
5508
  reportLoginFailure: () => reportLoginFailure
5038
5509
  });
@@ -5051,12 +5522,12 @@ async function handleLogin(deps) {
5051
5522
  throw new LoginNetworkError(apiUrl);
5052
5523
  }
5053
5524
  await res.text().catch(() => "");
5054
- if (res.status === 401) throw new TokenRejectedError(apiUrl);
5525
+ if (res.status === 401) throw new TokenRejectedError2(apiUrl);
5055
5526
  if (!res.ok) throw new LoginHttpError(res.status);
5056
5527
  return { path: saveToken(token), verified: true };
5057
5528
  }
5058
5529
  function reportLoginFailure(err, write) {
5059
- if (err instanceof EmptyTokenError || err instanceof TokenRejectedError) {
5530
+ if (err instanceof EmptyTokenError || err instanceof TokenRejectedError2) {
5060
5531
  write(`${err.message}
5061
5532
  `);
5062
5533
  return 2;
@@ -5066,286 +5537,74 @@ function reportLoginFailure(err, write) {
5066
5537
  `);
5067
5538
  return 1;
5068
5539
  }
5069
- return null;
5070
- }
5071
- var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
5072
- var init_login = __esm({
5073
- "src/cli/commands/login.ts"() {
5074
- "use strict";
5075
- init_cjs_shims();
5076
- init_api_url();
5077
- init_credentials();
5078
- EmptyTokenError = class extends Error {
5079
- constructor(apiUrl = resolveApiUrl()) {
5080
- super(
5081
- `No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
5082
- );
5083
- this.name = "EmptyTokenError";
5084
- }
5085
- };
5086
- TokenRejectedError = class extends Error {
5087
- constructor(apiUrl = resolveApiUrl()) {
5088
- super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
5089
- this.name = "TokenRejectedError";
5090
- }
5091
- };
5092
- LoginNetworkError = class extends Error {
5093
- constructor(apiUrl) {
5094
- super(
5095
- `Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
5096
- );
5097
- this.name = "LoginNetworkError";
5098
- }
5099
- };
5100
- LoginHttpError = class extends Error {
5101
- constructor(status) {
5102
- super(
5103
- `The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
5104
- );
5105
- this.name = "LoginHttpError";
5106
- }
5107
- };
5108
- }
5109
- });
5110
-
5111
- // src/cli/commands/logout.ts
5112
- var logout_exports = {};
5113
- __export(logout_exports, {
5114
- handleLogout: () => handleLogout
5115
- });
5116
- function handleLogout() {
5117
- return clearToken();
5118
- }
5119
- var init_logout = __esm({
5120
- "src/cli/commands/logout.ts"() {
5121
- "use strict";
5122
- init_cjs_shims();
5123
- init_credentials();
5124
- }
5125
- });
5126
-
5127
- // src/types/run-result-schema.ts
5128
- function parseRunResult(value2) {
5129
- return runResultSchema.parse(value2);
5130
- }
5131
- var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
5132
- var init_run_result_schema = __esm({
5133
- "src/types/run-result-schema.ts"() {
5134
- "use strict";
5135
- init_cjs_shims();
5136
- import_zod3 = require("zod");
5137
- init_results();
5138
- RUN_RESULT_SCHEMA_VERSION = 1;
5139
- binaryVerdictSchema = import_zod3.z.object({
5140
- question: import_zod3.z.string().min(1),
5141
- passed: import_zod3.z.boolean()
5142
- });
5143
- assertionResultRecordSchema2 = import_zod3.z.object({
5144
- type: import_zod3.z.string(),
5145
- passed: import_zod3.z.boolean(),
5146
- score: import_zod3.z.number().optional(),
5147
- reason: import_zod3.z.string().optional(),
5148
- binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
5149
- });
5150
- usageSchema = import_zod3.z.object({
5151
- input_tokens: import_zod3.z.number(),
5152
- output_tokens: import_zod3.z.number(),
5153
- cost_usd: import_zod3.z.number()
5154
- });
5155
- runErrorSchema = import_zod3.z.object({
5156
- code: import_zod3.z.string(),
5157
- message: import_zod3.z.string()
5158
- });
5159
- rowResultSchema = import_zod3.z.object({
5160
- vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
5161
- output: import_zod3.z.string().nullable(),
5162
- cached: import_zod3.z.boolean(),
5163
- usage: usageSchema.optional(),
5164
- latency_ms: import_zod3.z.number().optional(),
5165
- error: runErrorSchema.optional(),
5166
- assertions: import_zod3.z.array(assertionResultRecordSchema2),
5167
- /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
5168
- inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
5169
- });
5170
- summarySchema = import_zod3.z.object({
5171
- total: import_zod3.z.number(),
5172
- passed: import_zod3.z.number(),
5173
- failed: import_zod3.z.number(),
5174
- errored: import_zod3.z.number(),
5175
- /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
5176
- blocked: import_zod3.z.number().optional(),
5177
- skipped: import_zod3.z.number().optional(),
5178
- cost_usd: import_zod3.z.number(),
5179
- duration_ms: import_zod3.z.number()
5180
- });
5181
- evalResultSchema = import_zod3.z.object({
5182
- id: import_zod3.z.string(),
5183
- tags: import_zod3.z.array(import_zod3.z.string()),
5184
- rows: import_zod3.z.array(rowResultSchema),
5185
- passed: import_zod3.z.boolean()
5186
- });
5187
- runResultSchema = import_zod3.z.object({
5188
- schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
5189
- plune_version: import_zod3.z.string(),
5190
- started_at: import_zod3.z.string(),
5191
- finished_at: import_zod3.z.string(),
5192
- config_hash: import_zod3.z.string(),
5193
- summary: summarySchema,
5194
- evals: import_zod3.z.array(evalResultSchema)
5195
- });
5196
- }
5197
- });
5198
-
5199
- // src/cli/commands/sync.ts
5200
- var sync_exports = {};
5201
- __export(sync_exports, {
5202
- NotLoggedInError: () => NotLoggedInError,
5203
- SyncFileError: () => SyncFileError,
5204
- SyncHttpError: () => SyncHttpError,
5205
- SyncNetworkError: () => SyncNetworkError,
5206
- TokenRejectedError: () => TokenRejectedError2,
5207
- handleSync: () => handleSync,
5208
- reportSyncFailure: () => reportSyncFailure
5209
- });
5210
- function readRun(cwd, file) {
5211
- const target = file !== void 0 ? path14.resolve(cwd, file) : path14.join(cwd, ".plune", "last-run.json");
5212
- let raw;
5213
- try {
5214
- raw = fs15.readFileSync(target, "utf8");
5215
- } catch {
5216
- throw new SyncFileError(
5217
- file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
5218
- );
5219
- }
5220
- let json;
5221
- try {
5222
- json = JSON.parse(raw);
5223
- } catch {
5224
- throw new SyncFileError(`Run file is not valid JSON: ${target}`);
5225
- }
5226
- try {
5227
- return parseRunResult(json);
5228
- } catch {
5229
- throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
5230
- }
5231
- }
5232
- async function safeErrorDetail(res) {
5233
- try {
5234
- const body = await res.json();
5235
- return typeof body.error === "string" ? body.error : "";
5236
- } catch {
5237
- return "";
5238
- }
5239
- }
5240
- async function handleSync(deps = {}) {
5241
- const cwd = deps.cwd ?? process.cwd();
5242
- const apiUrl = resolveApiUrl(deps.apiUrl);
5243
- const token = (deps.loadToken ?? loadToken)();
5244
- if (token === null) throw new NotLoggedInError();
5245
- const run = readRun(cwd, deps.file);
5246
- const doFetch = deps.fetchImpl ?? fetch;
5247
- const endpoint = `${apiUrl}/v1/runs`;
5248
- let res;
5249
- try {
5250
- res = await doFetch(endpoint, {
5251
- method: "POST",
5252
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
5253
- body: JSON.stringify(run)
5254
- });
5255
- } catch {
5256
- throw new SyncNetworkError(apiUrl);
5257
- }
5258
- if (!res.ok) {
5259
- const detail = await safeErrorDetail(res);
5260
- if (res.status === 401) throw new TokenRejectedError2();
5261
- throw new SyncHttpError(res.status, detail);
5262
- }
5263
- const body = await res.json().catch(() => ({}));
5264
- const id = typeof body.id === "string" ? body.id : "";
5265
- const result = { id, url: `${endpoint}/${id}` };
5266
- if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
5267
- return result;
5268
- }
5269
- async function readVerdict(doFetch, url, token) {
5270
- try {
5271
- const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
5272
- const body = await res.json().catch(() => void 0);
5273
- return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
5274
- } catch {
5275
- return void 0;
5276
- }
5277
- }
5278
- function reportSyncFailure(err, write) {
5279
- if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
5280
- write(err.message + "\n");
5281
- return 2;
5282
- }
5283
- if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
5284
- write(err.message + "\n");
5285
- return 1;
5286
- }
5287
- return null;
5288
- }
5289
- var fs15, path14, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
5290
- var init_sync = __esm({
5291
- "src/cli/commands/sync.ts"() {
5292
- "use strict";
5293
- init_cjs_shims();
5294
- fs15 = __toESM(require("fs"), 1);
5295
- path14 = __toESM(require("path"), 1);
5296
- init_run_result_schema();
5297
- init_api_url();
5298
- init_credentials();
5299
- init_gate();
5300
- NotLoggedInError = class extends Error {
5301
- constructor() {
5302
- super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
5303
- this.name = "NotLoggedInError";
5304
- }
5305
- };
5306
- TokenRejectedError2 = class extends Error {
5307
- constructor() {
5540
+ return null;
5541
+ }
5542
+ var EmptyTokenError, TokenRejectedError2, LoginNetworkError, LoginHttpError;
5543
+ var init_login = __esm({
5544
+ "src/cli/commands/login.ts"() {
5545
+ "use strict";
5546
+ init_cjs_shims();
5547
+ init_api_url();
5548
+ init_credentials();
5549
+ EmptyTokenError = class extends Error {
5550
+ constructor(apiUrl = resolveApiUrl()) {
5308
5551
  super(
5309
- 'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
5552
+ `No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
5310
5553
  );
5311
- this.name = "TokenRejectedError";
5554
+ this.name = "EmptyTokenError";
5312
5555
  }
5313
5556
  };
5314
- SyncFileError = class extends Error {
5315
- constructor(message) {
5316
- super(message);
5317
- this.name = "SyncFileError";
5557
+ TokenRejectedError2 = class extends Error {
5558
+ constructor(apiUrl = resolveApiUrl()) {
5559
+ super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
5560
+ this.name = "TokenRejectedError";
5318
5561
  }
5319
5562
  };
5320
- SyncNetworkError = class extends Error {
5563
+ LoginNetworkError = class extends Error {
5321
5564
  constructor(apiUrl) {
5322
5565
  super(
5323
- `Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
5566
+ `Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
5324
5567
  );
5325
- this.name = "SyncNetworkError";
5568
+ this.name = "LoginNetworkError";
5326
5569
  }
5327
5570
  };
5328
- SyncHttpError = class extends Error {
5329
- constructor(status, detail) {
5330
- super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
5331
- this.status = status;
5332
- this.name = "SyncHttpError";
5571
+ LoginHttpError = class extends Error {
5572
+ constructor(status) {
5573
+ super(
5574
+ `The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
5575
+ );
5576
+ this.name = "LoginHttpError";
5333
5577
  }
5334
- status;
5335
5578
  };
5336
5579
  }
5337
5580
  });
5338
5581
 
5582
+ // src/cli/commands/logout.ts
5583
+ var logout_exports = {};
5584
+ __export(logout_exports, {
5585
+ handleLogout: () => handleLogout
5586
+ });
5587
+ function handleLogout() {
5588
+ return clearToken();
5589
+ }
5590
+ var init_logout = __esm({
5591
+ "src/cli/commands/logout.ts"() {
5592
+ "use strict";
5593
+ init_cjs_shims();
5594
+ init_credentials();
5595
+ }
5596
+ });
5597
+
5339
5598
  // src/cli/commands/cairn-artifact.ts
5340
5599
  async function findLatestCairnRun(runsDir = "runs") {
5341
- const entries = await fs16.readdir(runsDir, { withFileTypes: true }).catch(() => null);
5600
+ const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
5342
5601
  if (entries === null) throw new NoCairnRunsError(runsDir);
5343
5602
  let newest;
5344
5603
  for (const entry of entries) {
5345
5604
  if (!entry.isDirectory()) continue;
5346
- const dir = path15.join(runsDir, entry.name);
5605
+ const dir = path16.join(runsDir, entry.name);
5347
5606
  try {
5348
- const report = await fs16.lstat(path15.join(dir, "report.json"));
5607
+ const report = await fs17.lstat(path16.join(dir, "report.json"));
5349
5608
  if (!report.isFile()) continue;
5350
5609
  if (newest === void 0 || report.mtimeMs > newest.at) {
5351
5610
  newest = { dir, at: report.mtimeMs };
@@ -5358,13 +5617,13 @@ async function findLatestCairnRun(runsDir = "runs") {
5358
5617
  }
5359
5618
  async function readJson(file) {
5360
5619
  try {
5361
- return JSON.parse(await fs16.readFile(file, "utf8"));
5620
+ return JSON.parse(await fs17.readFile(file, "utf8"));
5362
5621
  } catch {
5363
5622
  return void 0;
5364
5623
  }
5365
5624
  }
5366
5625
  async function defaultReadEvidence(dir) {
5367
- const raw = await readJson(path15.join(dir, "api-evidence.json"));
5626
+ const raw = await readJson(path16.join(dir, "api-evidence.json"));
5368
5627
  if (!Array.isArray(raw)) return void 0;
5369
5628
  return raw.map((r) => {
5370
5629
  const rec = asRecord(r);
@@ -5407,7 +5666,7 @@ function casesFromApi(report, verdicts) {
5407
5666
  });
5408
5667
  }
5409
5668
  async function readCairnRun(dir, deps = {}) {
5410
- const raw = await readJson(path15.join(dir, "report.json"));
5669
+ const raw = await readJson(path16.join(dir, "report.json"));
5411
5670
  if (raw === void 0) throw new CairnRunNotFoundError(dir);
5412
5671
  const report = asRecord(raw);
5413
5672
  if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
@@ -5437,20 +5696,20 @@ async function readCairnRun(dir, deps = {}) {
5437
5696
  return {
5438
5697
  schemaVersion: INGEST_SCHEMA_VERSION,
5439
5698
  run: {
5440
- runId: str(report["runId"], path15.basename(dir)),
5699
+ runId: str(report["runId"], path16.basename(dir)),
5441
5700
  mode,
5442
5701
  target: str(report["url"], "unknown")
5443
5702
  },
5444
5703
  cases
5445
5704
  };
5446
5705
  }
5447
- var fs16, path15, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
5706
+ var fs17, path16, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
5448
5707
  var init_cairn_artifact = __esm({
5449
5708
  "src/cli/commands/cairn-artifact.ts"() {
5450
5709
  "use strict";
5451
5710
  init_cjs_shims();
5452
- fs16 = __toESM(require("fs/promises"), 1);
5453
- path15 = __toESM(require("path"), 1);
5711
+ fs17 = __toESM(require("fs/promises"), 1);
5712
+ path16 = __toESM(require("path"), 1);
5454
5713
  SUPPORTED_ARTIFACT_VERSION = 1;
5455
5714
  INGEST_SCHEMA_VERSION = 1;
5456
5715
  NoCairnRunsError = class extends Error {
@@ -5505,7 +5764,7 @@ __export(ingest_exports, {
5505
5764
  handleIngest: () => handleIngest,
5506
5765
  reportIngestFailure: () => reportIngestFailure
5507
5766
  });
5508
- async function safeErrorDetail2(res) {
5767
+ async function safeErrorDetail4(res) {
5509
5768
  try {
5510
5769
  const body = await res.json();
5511
5770
  return typeof body.error === "string" ? body.error : "";
@@ -5532,7 +5791,7 @@ async function handleIngest(deps) {
5532
5791
  throw new IngestNetworkError(apiUrl);
5533
5792
  }
5534
5793
  if (!res.ok) {
5535
- const detail = await safeErrorDetail2(res);
5794
+ const detail = await safeErrorDetail4(res);
5536
5795
  if (res.status === 401) throw new IngestTokenRejectedError();
5537
5796
  throw new IngestHttpError(res.status, detail);
5538
5797
  }
@@ -5604,207 +5863,6 @@ var init_ingest = __esm({
5604
5863
  }
5605
5864
  });
5606
5865
 
5607
- // src/cli/commands/cases.ts
5608
- var cases_exports = {};
5609
- __export(cases_exports, {
5610
- DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
5611
- DirtyFileError: () => DirtyFileError,
5612
- formatReport: () => formatReport,
5613
- handlePull: () => handlePull,
5614
- handlePush: () => handlePush,
5615
- reportCasesFailure: () => reportCasesFailure
5616
- });
5617
- function gitDirty(file) {
5618
- try {
5619
- const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
5620
- cwd: path16.dirname(file),
5621
- stdio: ["ignore", "pipe", "ignore"],
5622
- encoding: "utf8"
5623
- });
5624
- return out.trim() !== "";
5625
- } catch {
5626
- return false;
5627
- }
5628
- }
5629
- async function safeErrorDetail3(res) {
5630
- try {
5631
- const body = await res.json();
5632
- return typeof body.error === "string" ? body.error : "";
5633
- } catch {
5634
- return "";
5635
- }
5636
- }
5637
- function tokenOf(deps) {
5638
- const token = (deps.loadToken ?? loadToken)();
5639
- if (token === null) throw new NotLoggedInError();
5640
- return token;
5641
- }
5642
- async function handlePull(deps = {}) {
5643
- const cwd = deps.cwd ?? process.cwd();
5644
- const apiUrl = resolveApiUrl(deps.apiUrl);
5645
- const token = tokenOf(deps);
5646
- const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
5647
- if (fs17.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
5648
- throw new DirtyFileError(path16.relative(cwd, file) || file);
5649
- const doFetch = deps.fetchImpl ?? fetch;
5650
- const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
5651
- let res;
5652
- try {
5653
- res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
5654
- } catch {
5655
- throw new SyncNetworkError(apiUrl);
5656
- }
5657
- if (!res.ok) {
5658
- const detail = await safeErrorDetail3(res);
5659
- if (res.status === 401) throw new TokenRejectedError2();
5660
- throw new SyncHttpError(res.status, detail);
5661
- }
5662
- const text2 = await res.text();
5663
- fs17.mkdirSync(path16.dirname(file), { recursive: true });
5664
- fs17.writeFileSync(file, text2, "utf8");
5665
- return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
5666
- }
5667
- async function handlePush(deps = {}) {
5668
- const cwd = deps.cwd ?? process.cwd();
5669
- const apiUrl = resolveApiUrl(deps.apiUrl);
5670
- const token = tokenOf(deps);
5671
- const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
5672
- let text2;
5673
- try {
5674
- text2 = fs17.readFileSync(file, "utf8");
5675
- } catch {
5676
- throw new SyncFileError(
5677
- `No document to push: ${file}. Run "plune pull" first (or pass a file).`
5678
- );
5679
- }
5680
- if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
5681
- const doFetch = deps.fetchImpl ?? fetch;
5682
- let res;
5683
- try {
5684
- res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
5685
- method: "POST",
5686
- headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
5687
- body: text2
5688
- });
5689
- } catch {
5690
- throw new SyncNetworkError(apiUrl);
5691
- }
5692
- if (!res.ok) {
5693
- const detail = await safeErrorDetail3(res);
5694
- if (res.status === 401) throw new TokenRejectedError2();
5695
- throw new SyncHttpError(res.status, detail);
5696
- }
5697
- return await res.json();
5698
- }
5699
- function formatReport(report) {
5700
- const verb = (past, future) => report.dryRun ? future : past;
5701
- const lines = [
5702
- `${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
5703
- ];
5704
- for (const r of report.refused)
5705
- lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
5706
- for (const w of report.warnings)
5707
- lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
5708
- return lines.join("\n") + "\n";
5709
- }
5710
- function reportCasesFailure(err, write) {
5711
- if (err instanceof DirtyFileError) {
5712
- write(err.message + "\n");
5713
- return 3;
5714
- }
5715
- if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
5716
- write(err.message + "\n");
5717
- return 2;
5718
- }
5719
- if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
5720
- write(err.message + "\n");
5721
- return 1;
5722
- }
5723
- return null;
5724
- }
5725
- var import_node_child_process2, fs17, path16, DEFAULT_CASES_FILE, DirtyFileError;
5726
- var init_cases = __esm({
5727
- "src/cli/commands/cases.ts"() {
5728
- "use strict";
5729
- init_cjs_shims();
5730
- import_node_child_process2 = require("child_process");
5731
- fs17 = __toESM(require("fs"), 1);
5732
- path16 = __toESM(require("path"), 1);
5733
- init_api_url();
5734
- init_credentials();
5735
- init_sync();
5736
- DEFAULT_CASES_FILE = path16.join("plune", "cases.md");
5737
- DirtyFileError = class extends Error {
5738
- constructor(file) {
5739
- super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
5740
- this.name = "DirtyFileError";
5741
- }
5742
- };
5743
- }
5744
- });
5745
-
5746
- // src/cli/commands/plan.ts
5747
- var plan_exports = {};
5748
- __export(plan_exports, {
5749
- formatGrep: () => formatGrep,
5750
- grepFragment: () => grepFragment,
5751
- handlePlanGrep: () => handlePlanGrep
5752
- });
5753
- function grepFragment(c) {
5754
- const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
5755
- const segments = pathTitle?.split("#") ?? [c.title];
5756
- return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
5757
- }
5758
- function formatGrep(cases) {
5759
- if (cases.length === 0) return "(?!)";
5760
- return cases.map(grepFragment).join("|");
5761
- }
5762
- async function safeErrorDetail4(res) {
5763
- try {
5764
- const body = await res.json();
5765
- return typeof body.error === "string" ? body.error : "";
5766
- } catch {
5767
- return "";
5768
- }
5769
- }
5770
- async function handlePlanGrep(deps) {
5771
- const apiUrl = resolveApiUrl(deps.apiUrl);
5772
- const token = (deps.loadToken ?? loadToken)();
5773
- if (token === null) throw new NotLoggedInError();
5774
- const doFetch = deps.fetchImpl ?? fetch;
5775
- let res;
5776
- try {
5777
- res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
5778
- headers: { authorization: `Bearer ${token}` }
5779
- });
5780
- } catch {
5781
- throw new SyncNetworkError(apiUrl);
5782
- }
5783
- if (!res.ok) {
5784
- const detail = await safeErrorDetail4(res);
5785
- if (res.status === 401) throw new TokenRejectedError2();
5786
- throw new SyncHttpError(res.status, detail);
5787
- }
5788
- const { cases } = await res.json();
5789
- return {
5790
- pattern: formatGrep(cases),
5791
- cases: cases.length,
5792
- keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
5793
- };
5794
- }
5795
- var escape, BOUNDARY;
5796
- var init_plan = __esm({
5797
- "src/cli/commands/plan.ts"() {
5798
- "use strict";
5799
- init_cjs_shims();
5800
- init_api_url();
5801
- init_credentials();
5802
- init_sync();
5803
- escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5804
- BOUNDARY = "[ >#]+";
5805
- }
5806
- });
5807
-
5808
5866
  // src/cli.ts
5809
5867
  var cli_exports = {};
5810
5868
  __export(cli_exports, {
@@ -6014,6 +6072,32 @@ function createProgram() {
6014
6072
  await failRunCommand(err, verboseOf(command));
6015
6073
  }
6016
6074
  });
6075
+ runCommand.command("grep").argument("<run>", "The run: its id, or its key (rk_\u2026) \u2014 the one PLUNE_RUN joins by").description(
6076
+ "Print the run's automated cases as one --grep pattern for the runner (GET /v1/runs/:id/cases)"
6077
+ ).option("--failed", "Only the cases whose latest result failed or broke \u2014 a muted failure does not count", false).action(async (run, options, command) => {
6078
+ const verbose = verboseOf(command);
6079
+ const { handleRunGrep: handleRunGrep2, runGrepSummary: runGrepSummary2, RunRefError: RunRefError2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
6080
+ const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
6081
+ try {
6082
+ const out = await handleRunGrep2({ run, failed: options.failed });
6083
+ process.stdout.write(out.pattern + "\n");
6084
+ process.stderr.write(runGrepSummary2(out, options.failed));
6085
+ } catch (err) {
6086
+ if (err instanceof RunRefError2) {
6087
+ process.stderr.write(`plune: ${err.message}
6088
+ `);
6089
+ process.exitCode = err.exitCode;
6090
+ return;
6091
+ }
6092
+ const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
6093
+ if (code !== null) {
6094
+ maybeStack(err, verbose);
6095
+ process.exitCode = code;
6096
+ return;
6097
+ }
6098
+ failUnexpected(err, verbose);
6099
+ }
6100
+ });
6017
6101
  runCommand.command("report").description("Send what the reporter could not \u2014 replays .plune/pending-results.jsonl").option("--file <path>", "The fallback file to replay").action(async (options, command) => {
6018
6102
  try {
6019
6103
  const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));