@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.
- package/README.md +1 -0
- package/dist/cli.cjs +602 -518
- 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 =
|
|
4136
|
+
const folder = path11.dirname(path11.resolve(report));
|
|
3666
4137
|
try {
|
|
3667
|
-
return
|
|
4138
|
+
return fs10.realpathSync(folder);
|
|
3668
4139
|
} catch {
|
|
3669
4140
|
return folder;
|
|
3670
4141
|
}
|
|
3671
4142
|
}
|
|
3672
4143
|
function reportFolders(report) {
|
|
3673
|
-
return { lexical:
|
|
4144
|
+
return { lexical: path11.dirname(path11.resolve(report)), real: reportFolder(report) };
|
|
3674
4145
|
}
|
|
3675
4146
|
function within(folder, target) {
|
|
3676
|
-
const rel =
|
|
3677
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
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 =
|
|
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 =
|
|
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
|
|
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() ||
|
|
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
|
|
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
|
-
|
|
3734
|
-
|
|
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[
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
3841
|
-
const tests = resultNames.map((n) => load(
|
|
3842
|
-
const containers = namesIn(dir, CONTAINER).map((n) => load(
|
|
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) :
|
|
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
|
|
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
|
-
|
|
3975
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
5103
|
+
const file = path13.join(cwd, ".plune", "last-run.json");
|
|
4633
5104
|
let raw;
|
|
4634
5105
|
try {
|
|
4635
|
-
raw =
|
|
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
|
|
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
|
-
|
|
4656
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
4877
|
-
if (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
4927
|
-
|
|
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 =
|
|
4936
|
-
if (!force &&
|
|
5406
|
+
const target = path15.join(cwd, rel);
|
|
5407
|
+
if (!force && fs16.existsSync(target)) {
|
|
4937
5408
|
return "skipped";
|
|
4938
5409
|
}
|
|
4939
|
-
|
|
4940
|
-
|
|
5410
|
+
fs16.mkdirSync(path15.dirname(target), { recursive: true });
|
|
5411
|
+
fs16.writeFileSync(target, content, "utf8");
|
|
4941
5412
|
return "written";
|
|
4942
5413
|
}
|
|
4943
|
-
var
|
|
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
|
-
|
|
4949
|
-
|
|
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: () =>
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
5552
|
+
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
5310
5553
|
);
|
|
5311
|
-
this.name = "
|
|
5554
|
+
this.name = "EmptyTokenError";
|
|
5312
5555
|
}
|
|
5313
5556
|
};
|
|
5314
|
-
|
|
5315
|
-
constructor(
|
|
5316
|
-
super(
|
|
5317
|
-
this.name = "
|
|
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
|
-
|
|
5563
|
+
LoginNetworkError = class extends Error {
|
|
5321
5564
|
constructor(apiUrl) {
|
|
5322
5565
|
super(
|
|
5323
|
-
`Could not reach the Plune API at ${apiUrl}
|
|
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 = "
|
|
5568
|
+
this.name = "LoginNetworkError";
|
|
5326
5569
|
}
|
|
5327
5570
|
};
|
|
5328
|
-
|
|
5329
|
-
constructor(status
|
|
5330
|
-
super(
|
|
5331
|
-
|
|
5332
|
-
|
|
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
|
|
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 =
|
|
5605
|
+
const dir = path16.join(runsDir, entry.name);
|
|
5347
5606
|
try {
|
|
5348
|
-
const report = await
|
|
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
|
|
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(
|
|
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(
|
|
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"],
|
|
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
|
|
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
|
-
|
|
5453
|
-
|
|
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
|
|
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
|
|
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));
|