@plune-ai/cli 0.6.0 → 0.8.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 +2 -1
- package/dist/cli.cjs +915 -162
- package/dist/index.cjs +46 -19
- package/dist/index.d.cts +39 -424
- package/dist/index.d.ts +39 -424
- package/dist/index.js +46 -19
- package/dist/reporter-core.cjs +467 -0
- package/dist/reporter-core.d.cts +339 -0
- package/dist/reporter-core.d.ts +339 -0
- package/dist/reporter-core.js +423 -0
- package/package.json +12 -3
package/dist/cli.cjs
CHANGED
|
@@ -41,6 +41,35 @@ var init_cjs_shims = __esm({
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// src/cli/api-url.ts
|
|
45
|
+
var api_url_exports = {};
|
|
46
|
+
__export(api_url_exports, {
|
|
47
|
+
FEEDBACK_URL: () => FEEDBACK_URL,
|
|
48
|
+
dashboardUrl: () => dashboardUrl,
|
|
49
|
+
resolveApiUrl: () => resolveApiUrl,
|
|
50
|
+
whereToGetAToken: () => whereToGetAToken
|
|
51
|
+
});
|
|
52
|
+
function resolveApiUrl(explicit) {
|
|
53
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
54
|
+
return raw.replace(/\/+$/, "");
|
|
55
|
+
}
|
|
56
|
+
function dashboardUrl(apiUrl = resolveApiUrl()) {
|
|
57
|
+
return apiUrl === DEFAULT_API_URL ? "https://beta.plune.ai" : void 0;
|
|
58
|
+
}
|
|
59
|
+
function whereToGetAToken(apiUrl = resolveApiUrl()) {
|
|
60
|
+
const web = dashboardUrl(apiUrl);
|
|
61
|
+
return web === void 0 ? `Get one from your Plune dashboard (the web app in front of ${apiUrl}), under Settings \u2192 API tokens.` : `Get one at ${web} \u2192 Settings \u2192 API tokens.`;
|
|
62
|
+
}
|
|
63
|
+
var FEEDBACK_URL, DEFAULT_API_URL;
|
|
64
|
+
var init_api_url = __esm({
|
|
65
|
+
"src/cli/api-url.ts"() {
|
|
66
|
+
"use strict";
|
|
67
|
+
init_cjs_shims();
|
|
68
|
+
FEEDBACK_URL = "https://github.com/plune-ai/feedback/issues/new?template=feedback.yml";
|
|
69
|
+
DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
44
73
|
// src/cli/env.ts
|
|
45
74
|
var env_exports = {};
|
|
46
75
|
__export(env_exports, {
|
|
@@ -189,9 +218,9 @@ var init_schema = __esm({
|
|
|
189
218
|
input_per_1k_usd: import_zod.z.number().nonnegative(),
|
|
190
219
|
output_per_1k_usd: import_zod.z.number().nonnegative()
|
|
191
220
|
});
|
|
192
|
-
pricingSchema = import_zod.z.record(modelPriceSchema);
|
|
221
|
+
pricingSchema = import_zod.z.record(import_zod.z.string(), modelPriceSchema);
|
|
193
222
|
datasetRowSchema = import_zod.z.object({
|
|
194
|
-
vars: import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()])),
|
|
223
|
+
vars: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()])),
|
|
195
224
|
expected: import_zod.z.string().optional()
|
|
196
225
|
});
|
|
197
226
|
datasetRefSchema = import_zod.z.union([
|
|
@@ -221,7 +250,7 @@ var init_schema = __esm({
|
|
|
221
250
|
});
|
|
222
251
|
jsonSchemaAssertionSchema = import_zod.z.object({
|
|
223
252
|
type: import_zod.z.literal("json-schema"),
|
|
224
|
-
schema: import_zod.z.record(import_zod.z.unknown()),
|
|
253
|
+
schema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()),
|
|
225
254
|
extract: import_zod.z.enum(["auto", "strict"]).optional()
|
|
226
255
|
});
|
|
227
256
|
llmJudgeAssertionSchema = import_zod.z.object({
|
|
@@ -456,7 +485,7 @@ function readHeader(headers, name) {
|
|
|
456
485
|
return typeof v === "string" || typeof v === "number" ? v : void 0;
|
|
457
486
|
}
|
|
458
487
|
async function withRetry(fn, options) {
|
|
459
|
-
const { base_delay_ms = 500, sleep = defaultSleep, random = Math.random } = options;
|
|
488
|
+
const { base_delay_ms = 500, sleep: sleep2 = defaultSleep, random = Math.random } = options;
|
|
460
489
|
const maxRetries = Math.max(0, Math.floor(options.max_retries));
|
|
461
490
|
let attempt = 0;
|
|
462
491
|
for (; ; ) {
|
|
@@ -472,7 +501,7 @@ async function withRetry(fn, options) {
|
|
|
472
501
|
const backoff = base_delay_ms * 2 ** attempt;
|
|
473
502
|
const jitter = backoff * 0.1 * random();
|
|
474
503
|
const delay = Math.max(backoff + jitter, retryAfterMs(err) ?? 0);
|
|
475
|
-
await
|
|
504
|
+
await sleep2(delay);
|
|
476
505
|
attempt += 1;
|
|
477
506
|
}
|
|
478
507
|
}
|
|
@@ -886,7 +915,7 @@ var init_cache = __esm({
|
|
|
886
915
|
return void 0;
|
|
887
916
|
}
|
|
888
917
|
}
|
|
889
|
-
set(key,
|
|
918
|
+
set(key, value2) {
|
|
890
919
|
try {
|
|
891
920
|
this.db.prepare(
|
|
892
921
|
`INSERT INTO completions (cache_key, output, input_tokens, output_tokens, created_at)
|
|
@@ -896,7 +925,7 @@ var init_cache = __esm({
|
|
|
896
925
|
input_tokens = excluded.input_tokens,
|
|
897
926
|
output_tokens = excluded.output_tokens,
|
|
898
927
|
created_at = excluded.created_at`
|
|
899
|
-
).run(key,
|
|
928
|
+
).run(key, value2.output, value2.usage.input_tokens, value2.usage.output_tokens, this.now());
|
|
900
929
|
} catch {
|
|
901
930
|
}
|
|
902
931
|
}
|
|
@@ -934,32 +963,32 @@ var init_cache2 = __esm({
|
|
|
934
963
|
});
|
|
935
964
|
|
|
936
965
|
// src/util/canonical-json.ts
|
|
937
|
-
function normalizeValue(
|
|
938
|
-
if (
|
|
939
|
-
if (typeof
|
|
940
|
-
if (typeof
|
|
941
|
-
if (!isFinite(
|
|
966
|
+
function normalizeValue(value2) {
|
|
967
|
+
if (value2 === null) return null;
|
|
968
|
+
if (typeof value2 === "boolean") return value2;
|
|
969
|
+
if (typeof value2 === "number") {
|
|
970
|
+
if (!isFinite(value2)) {
|
|
942
971
|
throw new Error(
|
|
943
|
-
`canonicalJson: non-finite number rejected: ${String(
|
|
972
|
+
`canonicalJson: non-finite number rejected: ${String(value2)}`
|
|
944
973
|
);
|
|
945
974
|
}
|
|
946
|
-
return
|
|
975
|
+
return value2 === 0 ? 0 : value2;
|
|
947
976
|
}
|
|
948
|
-
if (typeof
|
|
949
|
-
return
|
|
977
|
+
if (typeof value2 === "string") {
|
|
978
|
+
return value2.normalize("NFC");
|
|
950
979
|
}
|
|
951
|
-
if (Array.isArray(
|
|
952
|
-
return
|
|
980
|
+
if (Array.isArray(value2)) {
|
|
981
|
+
return value2.map(normalizeValue);
|
|
953
982
|
}
|
|
954
|
-
if (typeof
|
|
955
|
-
const proto = Object.getPrototypeOf(
|
|
983
|
+
if (typeof value2 === "object") {
|
|
984
|
+
const proto = Object.getPrototypeOf(value2);
|
|
956
985
|
if (proto !== null && proto !== Object.prototype) {
|
|
957
|
-
const name =
|
|
986
|
+
const name = value2.constructor?.name ?? "unknown";
|
|
958
987
|
throw new Error(
|
|
959
988
|
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
960
989
|
);
|
|
961
990
|
}
|
|
962
|
-
const obj =
|
|
991
|
+
const obj = value2;
|
|
963
992
|
const sorted = {};
|
|
964
993
|
for (const key of Object.keys(obj).sort()) {
|
|
965
994
|
sorted[key] = normalizeValue(obj[key]);
|
|
@@ -967,11 +996,11 @@ function normalizeValue(value) {
|
|
|
967
996
|
return sorted;
|
|
968
997
|
}
|
|
969
998
|
throw new Error(
|
|
970
|
-
`canonicalJson: unsupported value type: ${typeof
|
|
999
|
+
`canonicalJson: unsupported value type: ${typeof value2}`
|
|
971
1000
|
);
|
|
972
1001
|
}
|
|
973
|
-
function canonicalJson(
|
|
974
|
-
return JSON.stringify(normalizeValue(
|
|
1002
|
+
function canonicalJson(value2) {
|
|
1003
|
+
return JSON.stringify(normalizeValue(value2));
|
|
975
1004
|
}
|
|
976
1005
|
var init_canonical_json = __esm({
|
|
977
1006
|
"src/util/canonical-json.ts"() {
|
|
@@ -1009,8 +1038,8 @@ function interpolate(template, ctx) {
|
|
|
1009
1038
|
}
|
|
1010
1039
|
if (key.startsWith("vars.")) {
|
|
1011
1040
|
const name = key.slice("vars.".length);
|
|
1012
|
-
const
|
|
1013
|
-
return
|
|
1041
|
+
const value2 = ctx.vars[name];
|
|
1042
|
+
return value2 === void 0 ? match : String(value2);
|
|
1014
1043
|
}
|
|
1015
1044
|
return match;
|
|
1016
1045
|
});
|
|
@@ -1063,12 +1092,12 @@ var init_contains = __esm({
|
|
|
1063
1092
|
init_interpolate();
|
|
1064
1093
|
contains = {
|
|
1065
1094
|
run(ctx) {
|
|
1066
|
-
const
|
|
1095
|
+
const value2 = interpolate(ctx.params.value, ctx);
|
|
1067
1096
|
const found = norm(ctx.output, ctx.params.ignore_case).includes(
|
|
1068
|
-
norm(
|
|
1097
|
+
norm(value2, ctx.params.ignore_case)
|
|
1069
1098
|
);
|
|
1070
1099
|
return Promise.resolve(
|
|
1071
|
-
found ? { passed: true } : { passed: false, reason: `expected output to contain: "${
|
|
1100
|
+
found ? { passed: true } : { passed: false, reason: `expected output to contain: "${value2}"` }
|
|
1072
1101
|
);
|
|
1073
1102
|
}
|
|
1074
1103
|
};
|
|
@@ -2117,6 +2146,609 @@ var init_reporters = __esm({
|
|
|
2117
2146
|
}
|
|
2118
2147
|
});
|
|
2119
2148
|
|
|
2149
|
+
// src/cli/credentials.ts
|
|
2150
|
+
function credentialsFile() {
|
|
2151
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
2152
|
+
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
2153
|
+
}
|
|
2154
|
+
function saveToken(token) {
|
|
2155
|
+
const file = credentialsFile();
|
|
2156
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true, mode: 448 });
|
|
2157
|
+
(0, import_node_fs.writeFileSync)(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
|
|
2158
|
+
(0, import_node_fs.chmodSync)(file, 384);
|
|
2159
|
+
return file;
|
|
2160
|
+
}
|
|
2161
|
+
function loadToken() {
|
|
2162
|
+
try {
|
|
2163
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2164
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
2165
|
+
} catch {
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function clearToken() {
|
|
2170
|
+
const file = credentialsFile();
|
|
2171
|
+
if (!(0, import_node_fs.existsSync)(file)) return { removed: false, path: file };
|
|
2172
|
+
(0, import_node_fs.rmSync)(file);
|
|
2173
|
+
return { removed: true, path: file };
|
|
2174
|
+
}
|
|
2175
|
+
var import_node_fs, import_node_os, import_node_path;
|
|
2176
|
+
var init_credentials = __esm({
|
|
2177
|
+
"src/cli/credentials.ts"() {
|
|
2178
|
+
"use strict";
|
|
2179
|
+
init_cjs_shims();
|
|
2180
|
+
import_node_fs = require("fs");
|
|
2181
|
+
import_node_os = require("os");
|
|
2182
|
+
import_node_path = require("path");
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
// src/reporter-core/client.ts
|
|
2187
|
+
async function detailOf(res) {
|
|
2188
|
+
try {
|
|
2189
|
+
const body = await res.json();
|
|
2190
|
+
return typeof body.error === "string" ? body.error : "";
|
|
2191
|
+
} catch {
|
|
2192
|
+
return "";
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function retryAfterMs2(res) {
|
|
2196
|
+
const raw = res.headers.get("retry-after");
|
|
2197
|
+
if (raw === null) return null;
|
|
2198
|
+
const seconds = Number(raw.trim());
|
|
2199
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
|
|
2200
|
+
}
|
|
2201
|
+
function classify3(status) {
|
|
2202
|
+
if (status === 401 || status === 403) return "auth";
|
|
2203
|
+
if (status === 409) return "conflict";
|
|
2204
|
+
if (status === 400) return "invalid";
|
|
2205
|
+
if (status === 429 || status >= 500) return "unavailable";
|
|
2206
|
+
return "other";
|
|
2207
|
+
}
|
|
2208
|
+
function createClient(opts) {
|
|
2209
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
2210
|
+
const wait = opts.wait ?? sleep;
|
|
2211
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2212
|
+
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2213
|
+
async function post(path12, body) {
|
|
2214
|
+
let last = {
|
|
2215
|
+
kind: "unavailable",
|
|
2216
|
+
status: null,
|
|
2217
|
+
detail: "no attempt was made"
|
|
2218
|
+
};
|
|
2219
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2220
|
+
let res;
|
|
2221
|
+
try {
|
|
2222
|
+
res = await doFetch(`${base}${path12}`, {
|
|
2223
|
+
method: "POST",
|
|
2224
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
|
|
2225
|
+
body: JSON.stringify(body)
|
|
2226
|
+
});
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
|
|
2229
|
+
if (attempt < RETRY_DELAYS_MS.length) await wait(RETRY_DELAYS_MS[attempt]);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (res.ok) return { ok: true, status: res.status, body: await res.json() };
|
|
2233
|
+
const detail = scrub(await detailOf(res));
|
|
2234
|
+
const kind = classify3(res.status);
|
|
2235
|
+
last = { kind, status: res.status, detail };
|
|
2236
|
+
if (kind !== "unavailable") break;
|
|
2237
|
+
if (attempt < RETRY_DELAYS_MS.length) {
|
|
2238
|
+
await wait(retryAfterMs2(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return { ok: false, ...last };
|
|
2242
|
+
}
|
|
2243
|
+
return { post };
|
|
2244
|
+
}
|
|
2245
|
+
var RETRY_DELAYS_MS, sleep;
|
|
2246
|
+
var init_client = __esm({
|
|
2247
|
+
"src/reporter-core/client.ts"() {
|
|
2248
|
+
"use strict";
|
|
2249
|
+
init_cjs_shims();
|
|
2250
|
+
RETRY_DELAYS_MS = [300, 900];
|
|
2251
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
// src/reporter-core/env.ts
|
|
2256
|
+
function value(env, name) {
|
|
2257
|
+
const raw = env[name]?.trim();
|
|
2258
|
+
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
2259
|
+
}
|
|
2260
|
+
function flag(env, name) {
|
|
2261
|
+
const raw = value(env, name)?.toLowerCase();
|
|
2262
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
2263
|
+
}
|
|
2264
|
+
function count(env, name) {
|
|
2265
|
+
const raw = value(env, name);
|
|
2266
|
+
if (raw === void 0) return void 0;
|
|
2267
|
+
const n = Number(raw);
|
|
2268
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2269
|
+
}
|
|
2270
|
+
function list(env, name) {
|
|
2271
|
+
const raw = value(env, name);
|
|
2272
|
+
if (raw === void 0) return void 0;
|
|
2273
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2274
|
+
return items.length === 0 ? void 0 : items;
|
|
2275
|
+
}
|
|
2276
|
+
function readEnv(env = process.env) {
|
|
2277
|
+
const externalKey = value(env, "PLUNE_RUN");
|
|
2278
|
+
const token = value(env, "PLUNE_TOKEN");
|
|
2279
|
+
const apiUrl = value(env, "PLUNE_API_URL");
|
|
2280
|
+
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
2281
|
+
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
2282
|
+
const title = value(env, "PLUNE_RUN_TITLE");
|
|
2283
|
+
const environment = value(env, "PLUNE_ENV");
|
|
2284
|
+
const labels = list(env, "PLUNE_LABELS");
|
|
2285
|
+
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2286
|
+
return {
|
|
2287
|
+
config: {
|
|
2288
|
+
...externalKey !== void 0 ? { externalKey } : {},
|
|
2289
|
+
...token !== void 0 ? { token } : {},
|
|
2290
|
+
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
2291
|
+
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
2292
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
2293
|
+
...title !== void 0 ? { title } : {},
|
|
2294
|
+
...environment !== void 0 ? { environment } : {},
|
|
2295
|
+
...labels !== void 0 ? { labels } : {},
|
|
2296
|
+
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
2297
|
+
// variable is not an instruction.
|
|
2298
|
+
...offerDiscovered ? { offerDiscovered } : {}
|
|
2299
|
+
},
|
|
2300
|
+
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2301
|
+
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
2302
|
+
// way this process must not be the one to call it finished.
|
|
2303
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
|
|
2304
|
+
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
var UNSUPPORTED_VARS;
|
|
2308
|
+
var init_env2 = __esm({
|
|
2309
|
+
"src/reporter-core/env.ts"() {
|
|
2310
|
+
"use strict";
|
|
2311
|
+
init_cjs_shims();
|
|
2312
|
+
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
// src/reporter-core/fallback.ts
|
|
2317
|
+
function appendBatch(file, batch) {
|
|
2318
|
+
fs6.mkdirSync(path6.dirname(file), { recursive: true });
|
|
2319
|
+
const line = {
|
|
2320
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2321
|
+
runId: batch.runId,
|
|
2322
|
+
externalKey: batch.externalKey,
|
|
2323
|
+
results: batch.results
|
|
2324
|
+
};
|
|
2325
|
+
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
2326
|
+
`, "utf8");
|
|
2327
|
+
}
|
|
2328
|
+
var fs6, path6, DEFAULT_FALLBACK_PATH;
|
|
2329
|
+
var init_fallback = __esm({
|
|
2330
|
+
"src/reporter-core/fallback.ts"() {
|
|
2331
|
+
"use strict";
|
|
2332
|
+
init_cjs_shims();
|
|
2333
|
+
fs6 = __toESM(require("fs"), 1);
|
|
2334
|
+
path6 = __toESM(require("path"), 1);
|
|
2335
|
+
DEFAULT_FALLBACK_PATH = path6.join(".plune", "pending-results.jsonl");
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
// src/reporter-core/session.ts
|
|
2340
|
+
function chunk(items, size) {
|
|
2341
|
+
const out = [];
|
|
2342
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2343
|
+
return out;
|
|
2344
|
+
}
|
|
2345
|
+
function warnIfDropped(sent, run, log) {
|
|
2346
|
+
const names = Object.keys(sent);
|
|
2347
|
+
if (names.length === 0) return;
|
|
2348
|
+
if (names.some((name) => run[name] != null)) return;
|
|
2349
|
+
log(
|
|
2350
|
+
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
async function startRun(passed, expected) {
|
|
2354
|
+
const env = readEnv();
|
|
2355
|
+
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2356
|
+
const cfg = { ...env.config, ...stated };
|
|
2357
|
+
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
2358
|
+
`));
|
|
2359
|
+
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2360
|
+
const externalKey = cfg.externalKey ?? null;
|
|
2361
|
+
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2362
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2363
|
+
const token = cfg.token ?? loadToken() ?? "";
|
|
2364
|
+
const client = createClient({
|
|
2365
|
+
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
2366
|
+
token,
|
|
2367
|
+
...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
|
|
2368
|
+
});
|
|
2369
|
+
const stats = {
|
|
2370
|
+
accepted: 0,
|
|
2371
|
+
duplicate: 0,
|
|
2372
|
+
conflict: 0,
|
|
2373
|
+
rejected: 0,
|
|
2374
|
+
unresolved: 0,
|
|
2375
|
+
offered: 0,
|
|
2376
|
+
deferred: 0
|
|
2377
|
+
};
|
|
2378
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2379
|
+
let buffer = [];
|
|
2380
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
2381
|
+
let runId = null;
|
|
2382
|
+
let joined = false;
|
|
2383
|
+
let offline = token === "";
|
|
2384
|
+
let done = false;
|
|
2385
|
+
if (offline) {
|
|
2386
|
+
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
2387
|
+
}
|
|
2388
|
+
if (env.ignored.length > 0) {
|
|
2389
|
+
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2390
|
+
}
|
|
2391
|
+
function defer(results) {
|
|
2392
|
+
appendBatch(fallbackPath, { runId, externalKey, results });
|
|
2393
|
+
stats.deferred += results.length;
|
|
2394
|
+
}
|
|
2395
|
+
async function resolveKeys(values) {
|
|
2396
|
+
const unknown = values.filter((k) => !resolved.has(k.value));
|
|
2397
|
+
if (unknown.length === 0 || offline) return;
|
|
2398
|
+
for (const part of chunk(unknown, RESOLVE_CHUNK)) {
|
|
2399
|
+
const out = await client.post(
|
|
2400
|
+
"/v1/test-cases/resolve",
|
|
2401
|
+
{ keys: part }
|
|
2402
|
+
);
|
|
2403
|
+
if (!out.ok) {
|
|
2404
|
+
log(`plune: could not look up test cases (${out.detail || out.kind}).`);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
if (!offline) {
|
|
2411
|
+
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
2412
|
+
const declared = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
2413
|
+
const configuration = declared.length === 0 ? void 0 : {
|
|
2414
|
+
expected: declared.flatMap((keys) => {
|
|
2415
|
+
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
2416
|
+
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
2417
|
+
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
2418
|
+
})
|
|
2419
|
+
};
|
|
2420
|
+
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
2421
|
+
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
2422
|
+
}
|
|
2423
|
+
const described = {
|
|
2424
|
+
...cfg.title !== void 0 ? { title: cfg.title } : {},
|
|
2425
|
+
...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
|
|
2426
|
+
...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
|
|
2427
|
+
};
|
|
2428
|
+
const start = await client.post("/v1/runs", {
|
|
2429
|
+
schemaVersion: 2,
|
|
2430
|
+
kind: cfg.kind ?? "automated",
|
|
2431
|
+
...externalKey !== null ? { externalKey } : {},
|
|
2432
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2433
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
2434
|
+
...described
|
|
2435
|
+
});
|
|
2436
|
+
if (start.ok) {
|
|
2437
|
+
runId = start.body.run.id;
|
|
2438
|
+
joined = start.status === 200;
|
|
2439
|
+
warnIfDropped(described, start.body.run, log);
|
|
2440
|
+
} else {
|
|
2441
|
+
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function matchFor(result) {
|
|
2445
|
+
if (result.testCaseId !== void 0) return { kind: "found", testCaseId: result.testCaseId };
|
|
2446
|
+
let unanswered = false;
|
|
2447
|
+
for (const key of result.keys) {
|
|
2448
|
+
const hit = resolved.get(key.value);
|
|
2449
|
+
if (typeof hit === "string") return { kind: "found", testCaseId: hit };
|
|
2450
|
+
if (hit === void 0) unanswered = true;
|
|
2451
|
+
}
|
|
2452
|
+
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
2453
|
+
}
|
|
2454
|
+
function remember(result) {
|
|
2455
|
+
if (!offerDiscovered) return;
|
|
2456
|
+
if (result.title === void 0 || result.specRef === void 0) return;
|
|
2457
|
+
if (result.keys.length === 0) return;
|
|
2458
|
+
discoveries.set(result.keys[0].value, {
|
|
2459
|
+
keys: result.keys,
|
|
2460
|
+
title: result.title,
|
|
2461
|
+
source: result.source,
|
|
2462
|
+
specRef: result.specRef,
|
|
2463
|
+
rawStatus: result.rawStatus
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async function offer() {
|
|
2467
|
+
if (discoveries.size === 0 || offline || runId === null) return;
|
|
2468
|
+
for (const batch of chunk([...discoveries.values()], DISCOVER_MAX)) {
|
|
2469
|
+
const out = await client.post("/v1/review-items/discovered", {
|
|
2470
|
+
discovered: batch,
|
|
2471
|
+
runId
|
|
2472
|
+
});
|
|
2473
|
+
if (!out.ok) {
|
|
2474
|
+
log(`plune: could not offer ${batch.length} unknown tests for review (${out.detail || out.kind}).`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async function flush() {
|
|
2481
|
+
if (buffer.length === 0) return;
|
|
2482
|
+
const pending = buffer;
|
|
2483
|
+
buffer = [];
|
|
2484
|
+
if (offline || runId === null) {
|
|
2485
|
+
defer(pending);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
2489
|
+
const submissions = [];
|
|
2490
|
+
const unanswered = [];
|
|
2491
|
+
for (const result of pending) {
|
|
2492
|
+
const match = matchFor(result);
|
|
2493
|
+
if (match.kind === "found") {
|
|
2494
|
+
const { keys: _keys, ...rest } = result;
|
|
2495
|
+
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
2496
|
+
} else if (match.kind === "unmatched") {
|
|
2497
|
+
stats.unresolved += 1;
|
|
2498
|
+
remember(result);
|
|
2499
|
+
} else {
|
|
2500
|
+
unanswered.push(result);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (unanswered.length > 0) defer(unanswered);
|
|
2504
|
+
if (submissions.length === 0) return;
|
|
2505
|
+
const out = await client.post(
|
|
2506
|
+
`/v1/runs/${runId}/results`,
|
|
2507
|
+
{ results: submissions }
|
|
2508
|
+
);
|
|
2509
|
+
if (out.ok) {
|
|
2510
|
+
stats.accepted += out.body.counts.accepted ?? 0;
|
|
2511
|
+
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
2512
|
+
stats.conflict += out.body.counts.conflict ?? 0;
|
|
2513
|
+
stats.rejected += out.body.counts.rejected ?? 0;
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
if (out.kind === "auth") {
|
|
2517
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
2518
|
+
offline = true;
|
|
2519
|
+
} else if (out.kind === "conflict") {
|
|
2520
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
2521
|
+
} else {
|
|
2522
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
2523
|
+
}
|
|
2524
|
+
defer(submissions);
|
|
2525
|
+
}
|
|
2526
|
+
function summarise() {
|
|
2527
|
+
const parts = [`${stats.accepted} accepted`];
|
|
2528
|
+
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
|
2529
|
+
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
2530
|
+
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
2531
|
+
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2532
|
+
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2533
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
2534
|
+
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
get runId() {
|
|
2538
|
+
return runId;
|
|
2539
|
+
},
|
|
2540
|
+
get joined() {
|
|
2541
|
+
return joined;
|
|
2542
|
+
},
|
|
2543
|
+
get stats() {
|
|
2544
|
+
return stats;
|
|
2545
|
+
},
|
|
2546
|
+
async add(result) {
|
|
2547
|
+
buffer.push(result);
|
|
2548
|
+
if (buffer.length >= batchSize) await flush();
|
|
2549
|
+
},
|
|
2550
|
+
flush,
|
|
2551
|
+
async finish(reason) {
|
|
2552
|
+
if (done) return;
|
|
2553
|
+
done = true;
|
|
2554
|
+
await flush();
|
|
2555
|
+
await offer();
|
|
2556
|
+
if (runId !== null && !offline) {
|
|
2557
|
+
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
2558
|
+
event: "finish",
|
|
2559
|
+
...reason !== void 0 ? { reason } : {}
|
|
2560
|
+
});
|
|
2561
|
+
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
2562
|
+
}
|
|
2563
|
+
summarise();
|
|
2564
|
+
},
|
|
2565
|
+
async leaveOpen() {
|
|
2566
|
+
if (done) return;
|
|
2567
|
+
done = true;
|
|
2568
|
+
await flush();
|
|
2569
|
+
await offer();
|
|
2570
|
+
if (runId !== null) {
|
|
2571
|
+
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
2572
|
+
}
|
|
2573
|
+
summarise();
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
var RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2578
|
+
var init_session = __esm({
|
|
2579
|
+
"src/reporter-core/session.ts"() {
|
|
2580
|
+
"use strict";
|
|
2581
|
+
init_cjs_shims();
|
|
2582
|
+
init_api_url();
|
|
2583
|
+
init_credentials();
|
|
2584
|
+
init_client();
|
|
2585
|
+
init_env2();
|
|
2586
|
+
init_fallback();
|
|
2587
|
+
RESOLVE_CHUNK = 500;
|
|
2588
|
+
BATCH_MAX = 500;
|
|
2589
|
+
DISCOVER_MAX = 500;
|
|
2590
|
+
BATCH_DEFAULT = 100;
|
|
2591
|
+
EXPECTED_MAX = 1e4;
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
// src/cli/commands/run-lifecycle.ts
|
|
2596
|
+
var run_lifecycle_exports = {};
|
|
2597
|
+
__export(run_lifecycle_exports, {
|
|
2598
|
+
NoTokenError: () => NoTokenError,
|
|
2599
|
+
RunCommandError: () => RunCommandError,
|
|
2600
|
+
handleRunExec: () => handleRunExec,
|
|
2601
|
+
handleRunFinish: () => handleRunFinish,
|
|
2602
|
+
handleRunReport: () => handleRunReport,
|
|
2603
|
+
handleRunStart: () => handleRunStart
|
|
2604
|
+
});
|
|
2605
|
+
function connect(deps) {
|
|
2606
|
+
const env = readEnv();
|
|
2607
|
+
const apiUrl = resolveApiUrl(deps.apiUrl ?? env.config.apiUrl);
|
|
2608
|
+
const token = deps.token ?? env.config.token ?? loadToken() ?? "";
|
|
2609
|
+
if (token === "") throw new NoTokenError();
|
|
2610
|
+
return {
|
|
2611
|
+
client: createClient({
|
|
2612
|
+
apiUrl,
|
|
2613
|
+
token,
|
|
2614
|
+
...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {}
|
|
2615
|
+
}),
|
|
2616
|
+
apiUrl,
|
|
2617
|
+
write: deps.write ?? ((line) => void process.stdout.write(`${line}
|
|
2618
|
+
`))
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
function runUrl(apiUrl, id) {
|
|
2622
|
+
const web = dashboardUrl(apiUrl);
|
|
2623
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id}` : `${web}/runs/${id}`;
|
|
2624
|
+
}
|
|
2625
|
+
async function handleRunStart(options = {}) {
|
|
2626
|
+
const { client, apiUrl, write } = connect(options);
|
|
2627
|
+
const env = readEnv();
|
|
2628
|
+
const externalKey = options.key ?? env.config.externalKey ?? `plune-${(options.now ?? Date.now)().toString(36)}`;
|
|
2629
|
+
const out = await client.post("/v1/runs", {
|
|
2630
|
+
schemaVersion: 2,
|
|
2631
|
+
kind: options.kind ?? "automated",
|
|
2632
|
+
externalKey
|
|
2633
|
+
});
|
|
2634
|
+
if (!out.ok) throw new RunCommandError(`Could not start the run \u2014 ${out.detail || out.kind}.`);
|
|
2635
|
+
const result = {
|
|
2636
|
+
id: out.body.run.id,
|
|
2637
|
+
joined: out.status === 200,
|
|
2638
|
+
externalKey,
|
|
2639
|
+
url: runUrl(apiUrl, out.body.run.id)
|
|
2640
|
+
};
|
|
2641
|
+
if (options.json === true) {
|
|
2642
|
+
write(JSON.stringify(result));
|
|
2643
|
+
} else {
|
|
2644
|
+
write(`${result.joined ? "Joined" : "Started"} run ${result.id}`);
|
|
2645
|
+
write(result.url);
|
|
2646
|
+
}
|
|
2647
|
+
return result;
|
|
2648
|
+
}
|
|
2649
|
+
async function handleRunFinish(id, options = {}) {
|
|
2650
|
+
const { client, write } = connect(options);
|
|
2651
|
+
const event = options.terminate === true ? "terminate" : "finish";
|
|
2652
|
+
const out = await client.post(`/v1/runs/${id}/events`, {
|
|
2653
|
+
event,
|
|
2654
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
2655
|
+
});
|
|
2656
|
+
if (!out.ok) {
|
|
2657
|
+
throw new RunCommandError(
|
|
2658
|
+
out.status === 404 ? `No run '${id}' \u2014 check the id, or whether the token belongs to the same project.` : `Could not ${event} run ${id} \u2014 ${out.detail || out.kind}.`
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
write(`Run ${id} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
2662
|
+
}
|
|
2663
|
+
async function handleRunExec(options) {
|
|
2664
|
+
const [command, ...args] = options.argv;
|
|
2665
|
+
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
2666
|
+
const run = await handleRunStart(options);
|
|
2667
|
+
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
2668
|
+
const code = await new Promise((resolve6) => {
|
|
2669
|
+
const child = spawnImpl(command, args, {
|
|
2670
|
+
stdio: "inherit",
|
|
2671
|
+
shell: process.platform === "win32",
|
|
2672
|
+
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
2673
|
+
});
|
|
2674
|
+
child.on("close", (exitCode) => resolve6(exitCode ?? 1));
|
|
2675
|
+
child.on("error", () => resolve6(1));
|
|
2676
|
+
});
|
|
2677
|
+
await handleRunFinish(run.id, options);
|
|
2678
|
+
return code;
|
|
2679
|
+
}
|
|
2680
|
+
async function handleRunReport(options = {}) {
|
|
2681
|
+
const file = options.file ?? DEFAULT_FALLBACK_PATH;
|
|
2682
|
+
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
2683
|
+
`));
|
|
2684
|
+
let raw;
|
|
2685
|
+
try {
|
|
2686
|
+
raw = fs7.readFileSync(file, "utf8");
|
|
2687
|
+
} catch {
|
|
2688
|
+
throw new RunCommandError(`Nothing to send \u2014 no file at ${file}.`);
|
|
2689
|
+
}
|
|
2690
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
|
|
2691
|
+
if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
|
|
2692
|
+
const { client } = connect(options);
|
|
2693
|
+
const result = { batches: lines.length, sent: 0, failed: 0 };
|
|
2694
|
+
for (const line of lines) {
|
|
2695
|
+
const pending = line.results.filter(isPending);
|
|
2696
|
+
const ready = line.results.filter((r) => !isPending(r));
|
|
2697
|
+
if (line.runId !== null && ready.length > 0) {
|
|
2698
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
|
|
2699
|
+
if (out.ok) result.sent += ready.length;
|
|
2700
|
+
else {
|
|
2701
|
+
result.failed += ready.length;
|
|
2702
|
+
write(`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (pending.length > 0) {
|
|
2706
|
+
const session = await startRun({
|
|
2707
|
+
...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
|
|
2708
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
2709
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2710
|
+
...line.externalKey !== null ? { externalKey: line.externalKey } : {},
|
|
2711
|
+
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
2712
|
+
log: write
|
|
2713
|
+
});
|
|
2714
|
+
for (const r of pending) await session.add(r);
|
|
2715
|
+
await session.leaveOpen();
|
|
2716
|
+
result.sent += session.stats.accepted + session.stats.duplicate;
|
|
2717
|
+
result.failed += session.stats.deferred + session.stats.unresolved;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
|
|
2721
|
+
return result;
|
|
2722
|
+
}
|
|
2723
|
+
var import_node_child_process, fs7, NoTokenError, RunCommandError, isPending;
|
|
2724
|
+
var init_run_lifecycle = __esm({
|
|
2725
|
+
"src/cli/commands/run-lifecycle.ts"() {
|
|
2726
|
+
"use strict";
|
|
2727
|
+
init_cjs_shims();
|
|
2728
|
+
import_node_child_process = require("child_process");
|
|
2729
|
+
fs7 = __toESM(require("fs"), 1);
|
|
2730
|
+
init_api_url();
|
|
2731
|
+
init_credentials();
|
|
2732
|
+
init_client();
|
|
2733
|
+
init_env2();
|
|
2734
|
+
init_fallback();
|
|
2735
|
+
init_session();
|
|
2736
|
+
NoTokenError = class extends Error {
|
|
2737
|
+
constructor() {
|
|
2738
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
2739
|
+
this.name = "NoTokenError";
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
RunCommandError = class extends Error {
|
|
2743
|
+
constructor(message) {
|
|
2744
|
+
super(message);
|
|
2745
|
+
this.name = "RunCommandError";
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
isPending = (r) => "keys" in r;
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2120
2752
|
// src/cli/commands/report.ts
|
|
2121
2753
|
var report_exports = {};
|
|
2122
2754
|
__export(report_exports, {
|
|
@@ -2130,10 +2762,10 @@ function isRunResult(v) {
|
|
|
2130
2762
|
}
|
|
2131
2763
|
function handleReport(opts = {}) {
|
|
2132
2764
|
const cwd = opts.cwd ?? process.cwd();
|
|
2133
|
-
const file =
|
|
2765
|
+
const file = path7.join(cwd, ".plune", "last-run.json");
|
|
2134
2766
|
let raw;
|
|
2135
2767
|
try {
|
|
2136
|
-
raw =
|
|
2768
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
2137
2769
|
} catch {
|
|
2138
2770
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
2139
2771
|
}
|
|
@@ -2148,13 +2780,13 @@ function handleReport(opts = {}) {
|
|
|
2148
2780
|
}
|
|
2149
2781
|
return parsed;
|
|
2150
2782
|
}
|
|
2151
|
-
var
|
|
2783
|
+
var fs8, path7, ReportNotFoundError;
|
|
2152
2784
|
var init_report = __esm({
|
|
2153
2785
|
"src/cli/commands/report.ts"() {
|
|
2154
2786
|
"use strict";
|
|
2155
2787
|
init_cjs_shims();
|
|
2156
|
-
|
|
2157
|
-
|
|
2788
|
+
fs8 = __toESM(require("fs"), 1);
|
|
2789
|
+
path7 = __toESM(require("path"), 1);
|
|
2158
2790
|
ReportNotFoundError = class extends Error {
|
|
2159
2791
|
code = "NO_SAVED_RUN";
|
|
2160
2792
|
constructor(message) {
|
|
@@ -2177,7 +2809,7 @@ function evalStatus(e) {
|
|
|
2177
2809
|
if (statuses.includes("errored")) return "errored";
|
|
2178
2810
|
return "passed";
|
|
2179
2811
|
}
|
|
2180
|
-
function
|
|
2812
|
+
function classify4(baseline, current) {
|
|
2181
2813
|
if (current === "errored") return "errored";
|
|
2182
2814
|
if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
|
|
2183
2815
|
if (current === "absent") return "removed";
|
|
@@ -2191,19 +2823,19 @@ function diffRuns(baseline, current) {
|
|
|
2191
2823
|
const evals = ids.map((id) => {
|
|
2192
2824
|
const baselineP = baseMap.get(id) ?? "absent";
|
|
2193
2825
|
const currentP = curMap.get(id) ?? "absent";
|
|
2194
|
-
return { id, status:
|
|
2826
|
+
return { id, status: classify4(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
2195
2827
|
});
|
|
2196
|
-
const
|
|
2828
|
+
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
2197
2829
|
const summary = {
|
|
2198
|
-
regressions:
|
|
2199
|
-
improvements:
|
|
2200
|
-
preExistingFails:
|
|
2201
|
-
newFails:
|
|
2202
|
-
newPasses:
|
|
2203
|
-
stablePasses:
|
|
2204
|
-
errored:
|
|
2205
|
-
removed:
|
|
2206
|
-
hasRegression:
|
|
2830
|
+
regressions: count2("regression"),
|
|
2831
|
+
improvements: count2("improvement"),
|
|
2832
|
+
preExistingFails: count2("pre-existing-fail"),
|
|
2833
|
+
newFails: count2("new-fail"),
|
|
2834
|
+
newPasses: count2("new-pass"),
|
|
2835
|
+
stablePasses: count2("stable-pass"),
|
|
2836
|
+
errored: count2("errored"),
|
|
2837
|
+
removed: count2("removed"),
|
|
2838
|
+
hasRegression: count2("regression") > 0
|
|
2207
2839
|
};
|
|
2208
2840
|
return { evals, summary };
|
|
2209
2841
|
}
|
|
@@ -2228,7 +2860,7 @@ function isRunResult2(v) {
|
|
|
2228
2860
|
function loadRunResult(file, label) {
|
|
2229
2861
|
let raw;
|
|
2230
2862
|
try {
|
|
2231
|
-
raw =
|
|
2863
|
+
raw = fs9.readFileSync(file, "utf8");
|
|
2232
2864
|
} catch {
|
|
2233
2865
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
2234
2866
|
}
|
|
@@ -2248,12 +2880,12 @@ function handleDiff(opts) {
|
|
|
2248
2880
|
const current = loadRunResult(opts.currentPath, "current");
|
|
2249
2881
|
return diffRuns(baseline, current);
|
|
2250
2882
|
}
|
|
2251
|
-
var
|
|
2883
|
+
var fs9, DiffInputError;
|
|
2252
2884
|
var init_diff2 = __esm({
|
|
2253
2885
|
"src/cli/commands/diff.ts"() {
|
|
2254
2886
|
"use strict";
|
|
2255
2887
|
init_cjs_shims();
|
|
2256
|
-
|
|
2888
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2257
2889
|
init_diff();
|
|
2258
2890
|
DiffInputError = class extends Error {
|
|
2259
2891
|
code = "DIFF_INPUT";
|
|
@@ -2378,8 +3010,8 @@ async function runInitWizard(cwd) {
|
|
|
2378
3010
|
process.exit(1);
|
|
2379
3011
|
return;
|
|
2380
3012
|
}
|
|
2381
|
-
const outputPath =
|
|
2382
|
-
if (
|
|
3013
|
+
const outputPath = path8.join(cwd, "plune.yaml");
|
|
3014
|
+
if (fs10.existsSync(outputPath)) {
|
|
2383
3015
|
const overwrite = await clack.confirm({
|
|
2384
3016
|
message: "plune.yaml already exists. Overwrite?"
|
|
2385
3017
|
});
|
|
@@ -2418,18 +3050,18 @@ async function runInitWizard(cwd) {
|
|
|
2418
3050
|
`;
|
|
2419
3051
|
const spin = clack.spinner();
|
|
2420
3052
|
spin.start("Writing plune.yaml\u2026");
|
|
2421
|
-
|
|
3053
|
+
fs10.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
2422
3054
|
spin.stop("plune.yaml created!");
|
|
2423
|
-
clack.outro("
|
|
3055
|
+
clack.outro("plune.yaml is ready.");
|
|
2424
3056
|
}
|
|
2425
|
-
var clack,
|
|
3057
|
+
var clack, fs10, path8, import_yaml2;
|
|
2426
3058
|
var init_wizard = __esm({
|
|
2427
3059
|
"src/config/init/wizard.ts"() {
|
|
2428
3060
|
"use strict";
|
|
2429
3061
|
init_cjs_shims();
|
|
2430
3062
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
2431
|
-
|
|
2432
|
-
|
|
3063
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3064
|
+
path8 = __toESM(require("path"), 1);
|
|
2433
3065
|
import_yaml2 = require("yaml");
|
|
2434
3066
|
init_errors();
|
|
2435
3067
|
}
|
|
@@ -2437,21 +3069,21 @@ var init_wizard = __esm({
|
|
|
2437
3069
|
|
|
2438
3070
|
// src/cli/templates/index.ts
|
|
2439
3071
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
2440
|
-
const target =
|
|
2441
|
-
if (!force &&
|
|
3072
|
+
const target = path9.join(cwd, rel);
|
|
3073
|
+
if (!force && fs11.existsSync(target)) {
|
|
2442
3074
|
return "skipped";
|
|
2443
3075
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
3076
|
+
fs11.mkdirSync(path9.dirname(target), { recursive: true });
|
|
3077
|
+
fs11.writeFileSync(target, content, "utf8");
|
|
2446
3078
|
return "written";
|
|
2447
3079
|
}
|
|
2448
|
-
var
|
|
3080
|
+
var fs11, path9, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
2449
3081
|
var init_templates = __esm({
|
|
2450
3082
|
"src/cli/templates/index.ts"() {
|
|
2451
3083
|
"use strict";
|
|
2452
3084
|
init_cjs_shims();
|
|
2453
|
-
|
|
2454
|
-
|
|
3085
|
+
fs11 = __toESM(require("fs"), 1);
|
|
3086
|
+
path9 = __toESM(require("path"), 1);
|
|
2455
3087
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
2456
3088
|
# .env is gitignored; .env.example is safe to commit.
|
|
2457
3089
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -2487,6 +3119,7 @@ evals:
|
|
|
2487
3119
|
// src/cli/commands/init.ts
|
|
2488
3120
|
var init_exports = {};
|
|
2489
3121
|
__export(init_exports, {
|
|
3122
|
+
NEXT_STEPS: () => NEXT_STEPS,
|
|
2490
3123
|
initCommand: () => initCommand
|
|
2491
3124
|
});
|
|
2492
3125
|
function reportWrite(cwd, rel, content, force) {
|
|
@@ -2506,8 +3139,9 @@ async function initCommand({ cwd, force, yes = false }) {
|
|
|
2506
3139
|
for (const [rel, content] of TEMPLATES) {
|
|
2507
3140
|
reportWrite(cwd, rel, content, force);
|
|
2508
3141
|
}
|
|
3142
|
+
process.stdout.write(NEXT_STEPS);
|
|
2509
3143
|
}
|
|
2510
|
-
var TEMPLATES;
|
|
3144
|
+
var TEMPLATES, NEXT_STEPS;
|
|
2511
3145
|
var init_init = __esm({
|
|
2512
3146
|
"src/cli/commands/init.ts"() {
|
|
2513
3147
|
"use strict";
|
|
@@ -2518,43 +3152,14 @@ var init_init = __esm({
|
|
|
2518
3152
|
["datasets/example.jsonl", EXAMPLE_JSONL_TPL],
|
|
2519
3153
|
[".env.example", ENV_EXAMPLE_TPL]
|
|
2520
3154
|
];
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
function saveToken(token) {
|
|
2530
|
-
const file = credentialsFile();
|
|
2531
|
-
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true, mode: 448 });
|
|
2532
|
-
(0, import_node_fs.writeFileSync)(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
|
|
2533
|
-
(0, import_node_fs.chmodSync)(file, 384);
|
|
2534
|
-
return file;
|
|
2535
|
-
}
|
|
2536
|
-
function loadToken() {
|
|
2537
|
-
try {
|
|
2538
|
-
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2539
|
-
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
2540
|
-
} catch {
|
|
2541
|
-
return null;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
function clearToken() {
|
|
2545
|
-
const file = credentialsFile();
|
|
2546
|
-
if (!(0, import_node_fs.existsSync)(file)) return { removed: false, path: file };
|
|
2547
|
-
(0, import_node_fs.rmSync)(file);
|
|
2548
|
-
return { removed: true, path: file };
|
|
2549
|
-
}
|
|
2550
|
-
var import_node_fs, import_node_os, import_node_path;
|
|
2551
|
-
var init_credentials = __esm({
|
|
2552
|
-
"src/cli/credentials.ts"() {
|
|
2553
|
-
"use strict";
|
|
2554
|
-
init_cjs_shims();
|
|
2555
|
-
import_node_fs = require("fs");
|
|
2556
|
-
import_node_os = require("os");
|
|
2557
|
-
import_node_path = require("path");
|
|
3155
|
+
NEXT_STEPS = [
|
|
3156
|
+
"",
|
|
3157
|
+
"Next:",
|
|
3158
|
+
" 1. plune run \u2014 runs locally. No account, no token needed.",
|
|
3159
|
+
" 2. plune login \u2014 only if you want history and trends: paste a token from the dashboard.",
|
|
3160
|
+
" 3. plune sync \u2014 sends the last run to the platform.",
|
|
3161
|
+
""
|
|
3162
|
+
].join("\n");
|
|
2558
3163
|
}
|
|
2559
3164
|
});
|
|
2560
3165
|
|
|
@@ -2562,25 +3167,81 @@ var init_credentials = __esm({
|
|
|
2562
3167
|
var login_exports = {};
|
|
2563
3168
|
__export(login_exports, {
|
|
2564
3169
|
EmptyTokenError: () => EmptyTokenError,
|
|
2565
|
-
|
|
3170
|
+
LoginHttpError: () => LoginHttpError,
|
|
3171
|
+
LoginNetworkError: () => LoginNetworkError,
|
|
3172
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
3173
|
+
handleLogin: () => handleLogin,
|
|
3174
|
+
reportLoginFailure: () => reportLoginFailure
|
|
2566
3175
|
});
|
|
2567
|
-
function handleLogin(
|
|
2568
|
-
const token =
|
|
2569
|
-
|
|
2570
|
-
|
|
3176
|
+
async function handleLogin(deps) {
|
|
3177
|
+
const token = deps.token.trim();
|
|
3178
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
3179
|
+
if (token.length === 0) throw new EmptyTokenError(apiUrl);
|
|
3180
|
+
if (deps.skipVerify === true) return { path: saveToken(token), verified: false };
|
|
3181
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
3182
|
+
let res;
|
|
3183
|
+
try {
|
|
3184
|
+
res = await doFetch(`${apiUrl}/v1/runs?limit=1`, {
|
|
3185
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3186
|
+
});
|
|
3187
|
+
} catch {
|
|
3188
|
+
throw new LoginNetworkError(apiUrl);
|
|
3189
|
+
}
|
|
3190
|
+
await res.text().catch(() => "");
|
|
3191
|
+
if (res.status === 401) throw new TokenRejectedError(apiUrl);
|
|
3192
|
+
if (!res.ok) throw new LoginHttpError(res.status);
|
|
3193
|
+
return { path: saveToken(token), verified: true };
|
|
2571
3194
|
}
|
|
2572
|
-
|
|
3195
|
+
function reportLoginFailure(err, write) {
|
|
3196
|
+
if (err instanceof EmptyTokenError || err instanceof TokenRejectedError) {
|
|
3197
|
+
write(`${err.message}
|
|
3198
|
+
`);
|
|
3199
|
+
return 2;
|
|
3200
|
+
}
|
|
3201
|
+
if (err instanceof LoginNetworkError || err instanceof LoginHttpError) {
|
|
3202
|
+
write(`${err.message}
|
|
3203
|
+
`);
|
|
3204
|
+
return 1;
|
|
3205
|
+
}
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
|
|
2573
3209
|
var init_login = __esm({
|
|
2574
3210
|
"src/cli/commands/login.ts"() {
|
|
2575
3211
|
"use strict";
|
|
2576
3212
|
init_cjs_shims();
|
|
3213
|
+
init_api_url();
|
|
2577
3214
|
init_credentials();
|
|
2578
3215
|
EmptyTokenError = class extends Error {
|
|
2579
|
-
constructor() {
|
|
2580
|
-
super(
|
|
3216
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
3217
|
+
super(
|
|
3218
|
+
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
3219
|
+
);
|
|
2581
3220
|
this.name = "EmptyTokenError";
|
|
2582
3221
|
}
|
|
2583
3222
|
};
|
|
3223
|
+
TokenRejectedError = class extends Error {
|
|
3224
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
3225
|
+
super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
|
|
3226
|
+
this.name = "TokenRejectedError";
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
LoginNetworkError = class extends Error {
|
|
3230
|
+
constructor(apiUrl) {
|
|
3231
|
+
super(
|
|
3232
|
+
`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.`
|
|
3233
|
+
);
|
|
3234
|
+
this.name = "LoginNetworkError";
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
LoginHttpError = class extends Error {
|
|
3238
|
+
constructor(status) {
|
|
3239
|
+
super(
|
|
3240
|
+
`The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
|
|
3241
|
+
);
|
|
3242
|
+
this.name = "LoginHttpError";
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
2584
3245
|
}
|
|
2585
3246
|
});
|
|
2586
3247
|
|
|
@@ -2601,8 +3262,8 @@ var init_logout = __esm({
|
|
|
2601
3262
|
});
|
|
2602
3263
|
|
|
2603
3264
|
// src/types/run-result-schema.ts
|
|
2604
|
-
function parseRunResult(
|
|
2605
|
-
return runResultSchema.parse(
|
|
3265
|
+
function parseRunResult(value2) {
|
|
3266
|
+
return runResultSchema.parse(value2);
|
|
2606
3267
|
}
|
|
2607
3268
|
var import_zod2, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
2608
3269
|
var init_run_result_schema = __esm({
|
|
@@ -2632,7 +3293,7 @@ var init_run_result_schema = __esm({
|
|
|
2632
3293
|
message: import_zod2.z.string()
|
|
2633
3294
|
});
|
|
2634
3295
|
rowResultSchema = import_zod2.z.object({
|
|
2635
|
-
vars: import_zod2.z.record(import_zod2.z.unknown()),
|
|
3296
|
+
vars: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()),
|
|
2636
3297
|
output: import_zod2.z.string().nullable(),
|
|
2637
3298
|
cached: import_zod2.z.boolean(),
|
|
2638
3299
|
usage: usageSchema.optional(),
|
|
@@ -2673,19 +3334,15 @@ __export(sync_exports, {
|
|
|
2673
3334
|
SyncFileError: () => SyncFileError,
|
|
2674
3335
|
SyncHttpError: () => SyncHttpError,
|
|
2675
3336
|
SyncNetworkError: () => SyncNetworkError,
|
|
2676
|
-
TokenRejectedError: () =>
|
|
3337
|
+
TokenRejectedError: () => TokenRejectedError2,
|
|
2677
3338
|
handleSync: () => handleSync,
|
|
2678
3339
|
reportSyncFailure: () => reportSyncFailure
|
|
2679
3340
|
});
|
|
2680
|
-
function resolveApiUrl(explicit) {
|
|
2681
|
-
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
2682
|
-
return raw.replace(/\/+$/, "");
|
|
2683
|
-
}
|
|
2684
3341
|
function readRun(cwd, file) {
|
|
2685
|
-
const target = file !== void 0 ?
|
|
3342
|
+
const target = file !== void 0 ? path10.resolve(cwd, file) : path10.join(cwd, ".plune", "last-run.json");
|
|
2686
3343
|
let raw;
|
|
2687
3344
|
try {
|
|
2688
|
-
raw =
|
|
3345
|
+
raw = fs12.readFileSync(target, "utf8");
|
|
2689
3346
|
} catch {
|
|
2690
3347
|
throw new SyncFileError(
|
|
2691
3348
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -2731,7 +3388,7 @@ async function handleSync(deps = {}) {
|
|
|
2731
3388
|
}
|
|
2732
3389
|
if (!res.ok) {
|
|
2733
3390
|
const detail = await safeErrorDetail(res);
|
|
2734
|
-
if (res.status === 401) throw new
|
|
3391
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
2735
3392
|
throw new SyncHttpError(res.status, detail);
|
|
2736
3393
|
}
|
|
2737
3394
|
const body = await res.json().catch(() => ({}));
|
|
@@ -2739,7 +3396,7 @@ async function handleSync(deps = {}) {
|
|
|
2739
3396
|
return { id, url: `${endpoint}/${id}` };
|
|
2740
3397
|
}
|
|
2741
3398
|
function reportSyncFailure(err, write) {
|
|
2742
|
-
if (err instanceof NotLoggedInError || err instanceof
|
|
3399
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
2743
3400
|
write(err.message + "\n");
|
|
2744
3401
|
return 2;
|
|
2745
3402
|
}
|
|
@@ -2749,23 +3406,23 @@ function reportSyncFailure(err, write) {
|
|
|
2749
3406
|
}
|
|
2750
3407
|
return null;
|
|
2751
3408
|
}
|
|
2752
|
-
var
|
|
3409
|
+
var fs12, path10, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
2753
3410
|
var init_sync = __esm({
|
|
2754
3411
|
"src/cli/commands/sync.ts"() {
|
|
2755
3412
|
"use strict";
|
|
2756
3413
|
init_cjs_shims();
|
|
2757
|
-
|
|
2758
|
-
|
|
3414
|
+
fs12 = __toESM(require("fs"), 1);
|
|
3415
|
+
path10 = __toESM(require("path"), 1);
|
|
2759
3416
|
init_run_result_schema();
|
|
3417
|
+
init_api_url();
|
|
2760
3418
|
init_credentials();
|
|
2761
|
-
DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
2762
3419
|
NotLoggedInError = class extends Error {
|
|
2763
3420
|
constructor() {
|
|
2764
3421
|
super('Not logged in. Run "plune login" first to save your API token.');
|
|
2765
3422
|
this.name = "NotLoggedInError";
|
|
2766
3423
|
}
|
|
2767
3424
|
};
|
|
2768
|
-
|
|
3425
|
+
TokenRejectedError2 = class extends Error {
|
|
2769
3426
|
constructor() {
|
|
2770
3427
|
super(
|
|
2771
3428
|
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
@@ -2799,15 +3456,34 @@ var init_sync = __esm({
|
|
|
2799
3456
|
});
|
|
2800
3457
|
|
|
2801
3458
|
// src/cli/commands/cairn-artifact.ts
|
|
3459
|
+
async function findLatestCairnRun(runsDir = "runs") {
|
|
3460
|
+
const entries = await fs13.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
3461
|
+
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
3462
|
+
let newest;
|
|
3463
|
+
for (const entry of entries) {
|
|
3464
|
+
if (!entry.isDirectory()) continue;
|
|
3465
|
+
const dir = path11.join(runsDir, entry.name);
|
|
3466
|
+
try {
|
|
3467
|
+
const report = await fs13.lstat(path11.join(dir, "report.json"));
|
|
3468
|
+
if (!report.isFile()) continue;
|
|
3469
|
+
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
3470
|
+
newest = { dir, at: report.mtimeMs };
|
|
3471
|
+
}
|
|
3472
|
+
} catch {
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
if (newest === void 0) throw new NoCairnRunsError(runsDir);
|
|
3476
|
+
return newest.dir;
|
|
3477
|
+
}
|
|
2802
3478
|
async function readJson(file) {
|
|
2803
3479
|
try {
|
|
2804
|
-
return JSON.parse(await
|
|
3480
|
+
return JSON.parse(await fs13.readFile(file, "utf8"));
|
|
2805
3481
|
} catch {
|
|
2806
3482
|
return void 0;
|
|
2807
3483
|
}
|
|
2808
3484
|
}
|
|
2809
3485
|
async function defaultReadEvidence(dir) {
|
|
2810
|
-
const raw = await readJson(
|
|
3486
|
+
const raw = await readJson(path11.join(dir, "api-evidence.json"));
|
|
2811
3487
|
if (!Array.isArray(raw)) return void 0;
|
|
2812
3488
|
return raw.map((r) => {
|
|
2813
3489
|
const rec = asRecord(r);
|
|
@@ -2815,8 +3491,8 @@ async function defaultReadEvidence(dir) {
|
|
|
2815
3491
|
});
|
|
2816
3492
|
}
|
|
2817
3493
|
function casesFromDesign(report) {
|
|
2818
|
-
const
|
|
2819
|
-
return
|
|
3494
|
+
const list2 = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
3495
|
+
return list2.map((raw) => {
|
|
2820
3496
|
const c = asRecord(raw);
|
|
2821
3497
|
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
2822
3498
|
return {
|
|
@@ -2831,8 +3507,8 @@ function casesFromDesign(report) {
|
|
|
2831
3507
|
});
|
|
2832
3508
|
}
|
|
2833
3509
|
function casesFromApi(report, verdicts) {
|
|
2834
|
-
const
|
|
2835
|
-
return
|
|
3510
|
+
const list2 = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
3511
|
+
return list2.map((raw) => {
|
|
2836
3512
|
const c = asRecord(raw);
|
|
2837
3513
|
const name = str(c["name"], "unnamed operation");
|
|
2838
3514
|
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
@@ -2849,7 +3525,7 @@ function casesFromApi(report, verdicts) {
|
|
|
2849
3525
|
});
|
|
2850
3526
|
}
|
|
2851
3527
|
async function readCairnRun(dir, deps = {}) {
|
|
2852
|
-
const raw = await readJson(
|
|
3528
|
+
const raw = await readJson(path11.join(dir, "report.json"));
|
|
2853
3529
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
2854
3530
|
const report = asRecord(raw);
|
|
2855
3531
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -2860,7 +3536,9 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
2860
3536
|
}
|
|
2861
3537
|
const mode = report["mode"];
|
|
2862
3538
|
if (mode !== "design" && mode !== "explore" && mode !== "api") {
|
|
2863
|
-
throw new CairnRunVersionError(
|
|
3539
|
+
throw new CairnRunVersionError(
|
|
3540
|
+
`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`
|
|
3541
|
+
);
|
|
2864
3542
|
}
|
|
2865
3543
|
let cases;
|
|
2866
3544
|
if (mode === "api") {
|
|
@@ -2877,25 +3555,33 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
2877
3555
|
return {
|
|
2878
3556
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
2879
3557
|
run: {
|
|
2880
|
-
runId: str(report["runId"],
|
|
3558
|
+
runId: str(report["runId"], path11.basename(dir)),
|
|
2881
3559
|
mode,
|
|
2882
3560
|
target: str(report["url"], "unknown")
|
|
2883
3561
|
},
|
|
2884
3562
|
cases
|
|
2885
3563
|
};
|
|
2886
3564
|
}
|
|
2887
|
-
var
|
|
3565
|
+
var fs13, path11, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
2888
3566
|
var init_cairn_artifact = __esm({
|
|
2889
3567
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
2890
3568
|
"use strict";
|
|
2891
3569
|
init_cjs_shims();
|
|
2892
|
-
|
|
2893
|
-
|
|
3570
|
+
fs13 = __toESM(require("fs/promises"), 1);
|
|
3571
|
+
path11 = __toESM(require("path"), 1);
|
|
2894
3572
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
2895
3573
|
INGEST_SCHEMA_VERSION = 1;
|
|
3574
|
+
NoCairnRunsError = class extends Error {
|
|
3575
|
+
constructor(runsDir) {
|
|
3576
|
+
super(`No Cairn run found under ${runsDir} \u2014 name the directory instead: plune ingest <dir>.`);
|
|
3577
|
+
this.name = "NoCairnRunsError";
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
2896
3580
|
CairnRunNotFoundError = class extends Error {
|
|
2897
3581
|
constructor(dir) {
|
|
2898
|
-
super(
|
|
3582
|
+
super(
|
|
3583
|
+
`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`
|
|
3584
|
+
);
|
|
2899
3585
|
this.name = "CairnRunNotFoundError";
|
|
2900
3586
|
}
|
|
2901
3587
|
};
|
|
@@ -2937,10 +3623,6 @@ __export(ingest_exports, {
|
|
|
2937
3623
|
handleIngest: () => handleIngest,
|
|
2938
3624
|
reportIngestFailure: () => reportIngestFailure
|
|
2939
3625
|
});
|
|
2940
|
-
function resolveApiUrl2(explicit) {
|
|
2941
|
-
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
|
|
2942
|
-
return raw.replace(/\/+$/, "");
|
|
2943
|
-
}
|
|
2944
3626
|
async function safeErrorDetail2(res) {
|
|
2945
3627
|
try {
|
|
2946
3628
|
const body = await res.json();
|
|
@@ -2950,10 +3632,11 @@ async function safeErrorDetail2(res) {
|
|
|
2950
3632
|
}
|
|
2951
3633
|
}
|
|
2952
3634
|
async function handleIngest(deps) {
|
|
2953
|
-
const apiUrl =
|
|
3635
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
2954
3636
|
const token = (deps.loadToken ?? loadToken)();
|
|
2955
3637
|
if (token === null) throw new IngestNotLoggedInError();
|
|
2956
|
-
const
|
|
3638
|
+
const dir = deps.dir ?? await findLatestCairnRun(deps.runsDir);
|
|
3639
|
+
const payload = await readCairnRun(dir);
|
|
2957
3640
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2958
3641
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2959
3642
|
let res;
|
|
@@ -2980,7 +3663,7 @@ async function handleIngest(deps) {
|
|
|
2980
3663
|
};
|
|
2981
3664
|
}
|
|
2982
3665
|
function reportIngestFailure(err, write) {
|
|
2983
|
-
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
3666
|
+
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
2984
3667
|
write(`${err.message}
|
|
2985
3668
|
`);
|
|
2986
3669
|
return 2;
|
|
@@ -3002,14 +3685,14 @@ function formatIngestResult(r) {
|
|
|
3002
3685
|
return `${lines.join("\n")}
|
|
3003
3686
|
`;
|
|
3004
3687
|
}
|
|
3005
|
-
var
|
|
3688
|
+
var IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
3006
3689
|
var init_ingest = __esm({
|
|
3007
3690
|
"src/cli/commands/ingest.ts"() {
|
|
3008
3691
|
"use strict";
|
|
3009
3692
|
init_cjs_shims();
|
|
3693
|
+
init_api_url();
|
|
3010
3694
|
init_credentials();
|
|
3011
3695
|
init_cairn_artifact();
|
|
3012
|
-
DEFAULT_API_URL2 = "https://beta-api.plune.ai";
|
|
3013
3696
|
IngestNotLoggedInError = class extends Error {
|
|
3014
3697
|
constructor() {
|
|
3015
3698
|
super('Not logged in. Run "plune login" first to save your API token.');
|
|
@@ -3046,6 +3729,7 @@ __export(cli_exports, {
|
|
|
3046
3729
|
});
|
|
3047
3730
|
module.exports = __toCommonJS(cli_exports);
|
|
3048
3731
|
init_cjs_shims();
|
|
3732
|
+
init_api_url();
|
|
3049
3733
|
var import_commander = require("commander");
|
|
3050
3734
|
var import_node_fs2 = require("fs");
|
|
3051
3735
|
var import_node_path2 = require("path");
|
|
@@ -3076,10 +3760,17 @@ function failUnexpected(err, verbose) {
|
|
|
3076
3760
|
} else {
|
|
3077
3761
|
process.stderr.write(String(err) + "\n");
|
|
3078
3762
|
}
|
|
3763
|
+
process.stderr.write(
|
|
3764
|
+
`This one is on us. Run again with --verbose and send the output: ${FEEDBACK_URL}
|
|
3765
|
+
`
|
|
3766
|
+
);
|
|
3079
3767
|
process.exit(1);
|
|
3080
3768
|
}
|
|
3081
3769
|
async function readTokenFromStdin() {
|
|
3082
3770
|
if (process.stdin.isTTY === true) {
|
|
3771
|
+
const { resolveApiUrl: resolveApiUrl2, whereToGetAToken: whereToGetAToken2 } = await Promise.resolve().then(() => (init_api_url(), api_url_exports));
|
|
3772
|
+
process.stderr.write(`${whereToGetAToken2(resolveApiUrl2())}
|
|
3773
|
+
`);
|
|
3083
3774
|
process.stderr.write("Paste your API token, then press Enter:\n");
|
|
3084
3775
|
}
|
|
3085
3776
|
const { createInterface } = await import("readline");
|
|
@@ -3095,7 +3786,7 @@ function createProgram() {
|
|
|
3095
3786
|
const program = new import_commander.Command();
|
|
3096
3787
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
3097
3788
|
program.option("-c, --config <path>", "Path to plune.yaml config file (applies to all commands)").option("-v, --verbose", "Verbose output, including stack traces on unexpected errors", false).option("--no-color", "Disable colored output regardless of TTY");
|
|
3098
|
-
program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
|
|
3789
|
+
const runCommand = program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
|
|
3099
3790
|
"--only <selector>",
|
|
3100
3791
|
"Run a subset by eval id or tag (repeatable)",
|
|
3101
3792
|
(val, prev) => [...prev, val],
|
|
@@ -3171,6 +3862,58 @@ function createProgram() {
|
|
|
3171
3862
|
}
|
|
3172
3863
|
}
|
|
3173
3864
|
);
|
|
3865
|
+
const failRunCommand = async (err, verbose) => {
|
|
3866
|
+
const { NoTokenError: NoTokenError2, RunCommandError: RunCommandError2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3867
|
+
if (err instanceof NoTokenError2) {
|
|
3868
|
+
process.stderr.write(err.message + "\n");
|
|
3869
|
+
process.exit(2);
|
|
3870
|
+
}
|
|
3871
|
+
if (err instanceof RunCommandError2) {
|
|
3872
|
+
process.stderr.write(err.message + "\n");
|
|
3873
|
+
process.exit(1);
|
|
3874
|
+
}
|
|
3875
|
+
failUnexpected(err, verbose);
|
|
3876
|
+
process.exit(1);
|
|
3877
|
+
};
|
|
3878
|
+
const verboseOf = (command) => command.optsWithGlobals().verbose === true;
|
|
3879
|
+
runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
|
|
3880
|
+
try {
|
|
3881
|
+
const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3882
|
+
await handleRunStart2({ ...options.key !== void 0 ? { key: options.key } : {}, json: options.json });
|
|
3883
|
+
} catch (err) {
|
|
3884
|
+
await failRunCommand(err, verboseOf(command));
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").action(async (id, options, command) => {
|
|
3888
|
+
try {
|
|
3889
|
+
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3890
|
+
await handleRunFinish2(id, {
|
|
3891
|
+
terminate: options.terminate,
|
|
3892
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
3893
|
+
});
|
|
3894
|
+
} catch (err) {
|
|
3895
|
+
await failRunCommand(err, verboseOf(command));
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description("Open a run, run a command inside it, close the run \u2014 whatever the command returns").option("--key <externalKey>", "The key several processes share; generated when absent").action(async (argv, options, command) => {
|
|
3899
|
+
try {
|
|
3900
|
+
const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3901
|
+
process.exitCode = await handleRunExec2({
|
|
3902
|
+
argv,
|
|
3903
|
+
...options.key !== void 0 ? { key: options.key } : {}
|
|
3904
|
+
});
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
await failRunCommand(err, verboseOf(command));
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3909
|
+
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) => {
|
|
3910
|
+
try {
|
|
3911
|
+
const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3912
|
+
await handleRunReport2({ ...options.file !== void 0 ? { file: options.file } : {} });
|
|
3913
|
+
} catch (err) {
|
|
3914
|
+
await failRunCommand(err, verboseOf(command));
|
|
3915
|
+
}
|
|
3916
|
+
});
|
|
3174
3917
|
program.command("report").description("Render the most recent run in a chosen format").option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options, command) => {
|
|
3175
3918
|
const globals = command.optsWithGlobals();
|
|
3176
3919
|
const [{ loadEnv: loadEnv2 }, { handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
@@ -3234,18 +3977,26 @@ function createProgram() {
|
|
|
3234
3977
|
failUnexpected(err, verbose);
|
|
3235
3978
|
}
|
|
3236
3979
|
});
|
|
3237
|
-
program.command("login").description("Save your Plune API token
|
|
3980
|
+
program.command("login").description("Save your Plune API token (get one in the dashboard: Settings \u2192 API tokens)").option("--token <token>", "API token (omit to paste it / pipe it via stdin)").option(
|
|
3981
|
+
"--skip-verify",
|
|
3982
|
+
"Save without checking the token against the API (offline setup)",
|
|
3983
|
+
false
|
|
3984
|
+
).action(async (options, command) => {
|
|
3238
3985
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
3239
|
-
const { handleLogin: handleLogin2,
|
|
3986
|
+
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
3240
3987
|
try {
|
|
3241
3988
|
const token = options.token ?? await readTokenFromStdin();
|
|
3242
|
-
const { path:
|
|
3243
|
-
process.stdout.write(
|
|
3244
|
-
`
|
|
3989
|
+
const { path: path12, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
3990
|
+
process.stdout.write(
|
|
3991
|
+
verified ? `Logged in. Token checked against the API and saved to ${path12}
|
|
3992
|
+
` : `Token saved to ${path12} WITHOUT being checked (--skip-verify).
|
|
3993
|
+
`
|
|
3994
|
+
);
|
|
3245
3995
|
} catch (err) {
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3996
|
+
const code = reportLoginFailure2(err, (s) => process.stderr.write(s));
|
|
3997
|
+
if (code !== null) {
|
|
3998
|
+
maybeStack(err, verbose);
|
|
3999
|
+
process.exitCode = code;
|
|
3249
4000
|
return;
|
|
3250
4001
|
}
|
|
3251
4002
|
failUnexpected(err, verbose);
|
|
@@ -3255,9 +4006,9 @@ function createProgram() {
|
|
|
3255
4006
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
3256
4007
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
3257
4008
|
try {
|
|
3258
|
-
const { removed, path:
|
|
4009
|
+
const { removed, path: path12 } = handleLogout2();
|
|
3259
4010
|
process.stdout.write(
|
|
3260
|
-
removed ? `Logged out. Removed ${
|
|
4011
|
+
removed ? `Logged out. Removed ${path12}
|
|
3261
4012
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
3262
4013
|
);
|
|
3263
4014
|
} catch (err) {
|
|
@@ -3286,12 +4037,14 @@ function createProgram() {
|
|
|
3286
4037
|
failUnexpected(err, verbose);
|
|
3287
4038
|
}
|
|
3288
4039
|
});
|
|
3289
|
-
program.command("ingest").argument("
|
|
4040
|
+
program.command("ingest").argument("[dir]", "A Cairn run directory; omit it for the newest run under ./runs").description("Record a Cairn run in Plune \u2014 generated cases arrive as review proposals").action(async (dir, _options, command) => {
|
|
3290
4041
|
const globals = command.optsWithGlobals();
|
|
3291
4042
|
const verbose = globals.verbose === true;
|
|
3292
4043
|
const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
3293
4044
|
try {
|
|
3294
|
-
process.stdout.write(
|
|
4045
|
+
process.stdout.write(
|
|
4046
|
+
formatIngestResult2(await handleIngest2({ ...dir !== void 0 ? { dir } : {} }))
|
|
4047
|
+
);
|
|
3295
4048
|
} catch (err) {
|
|
3296
4049
|
const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
|
|
3297
4050
|
if (code !== null) {
|