@plune-ai/cli 0.6.0 → 0.7.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 +851 -158
- 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 +407 -0
- package/dist/reporter-core.d.cts +315 -0
- package/dist/reporter-core.d.ts +315 -0
- package/dist/reporter-core.js +363 -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,549 @@ 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 readEnv(env = process.env) {
|
|
2271
|
+
const externalKey = value(env, "PLUNE_RUN");
|
|
2272
|
+
const token = value(env, "PLUNE_TOKEN");
|
|
2273
|
+
const apiUrl = value(env, "PLUNE_API_URL");
|
|
2274
|
+
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
2275
|
+
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
2276
|
+
return {
|
|
2277
|
+
config: {
|
|
2278
|
+
...externalKey !== void 0 ? { externalKey } : {},
|
|
2279
|
+
...token !== void 0 ? { token } : {},
|
|
2280
|
+
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
2281
|
+
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
2282
|
+
...batchSize !== void 0 ? { batchSize } : {}
|
|
2283
|
+
},
|
|
2284
|
+
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2285
|
+
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
2286
|
+
// way this process must not be the one to call it finished.
|
|
2287
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
|
|
2288
|
+
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
var UNSUPPORTED_VARS;
|
|
2292
|
+
var init_env2 = __esm({
|
|
2293
|
+
"src/reporter-core/env.ts"() {
|
|
2294
|
+
"use strict";
|
|
2295
|
+
init_cjs_shims();
|
|
2296
|
+
UNSUPPORTED_VARS = [
|
|
2297
|
+
"PLUNE_RUN_TITLE",
|
|
2298
|
+
"PLUNE_ENV",
|
|
2299
|
+
"PLUNE_LABELS",
|
|
2300
|
+
"PLUNE_GROUP"
|
|
2301
|
+
];
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
// src/reporter-core/fallback.ts
|
|
2306
|
+
function appendBatch(file, batch) {
|
|
2307
|
+
fs6.mkdirSync(path6.dirname(file), { recursive: true });
|
|
2308
|
+
const line = {
|
|
2309
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2310
|
+
runId: batch.runId,
|
|
2311
|
+
externalKey: batch.externalKey,
|
|
2312
|
+
results: batch.results
|
|
2313
|
+
};
|
|
2314
|
+
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
2315
|
+
`, "utf8");
|
|
2316
|
+
}
|
|
2317
|
+
var fs6, path6, DEFAULT_FALLBACK_PATH;
|
|
2318
|
+
var init_fallback = __esm({
|
|
2319
|
+
"src/reporter-core/fallback.ts"() {
|
|
2320
|
+
"use strict";
|
|
2321
|
+
init_cjs_shims();
|
|
2322
|
+
fs6 = __toESM(require("fs"), 1);
|
|
2323
|
+
path6 = __toESM(require("path"), 1);
|
|
2324
|
+
DEFAULT_FALLBACK_PATH = path6.join(".plune", "pending-results.jsonl");
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
// src/reporter-core/session.ts
|
|
2329
|
+
function chunk(items, size) {
|
|
2330
|
+
const out = [];
|
|
2331
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2332
|
+
return out;
|
|
2333
|
+
}
|
|
2334
|
+
async function startRun(passed, expected) {
|
|
2335
|
+
const env = readEnv();
|
|
2336
|
+
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2337
|
+
const cfg = { ...env.config, ...stated };
|
|
2338
|
+
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
2339
|
+
`));
|
|
2340
|
+
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2341
|
+
const externalKey = cfg.externalKey ?? null;
|
|
2342
|
+
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2343
|
+
const token = cfg.token ?? loadToken() ?? "";
|
|
2344
|
+
const client = createClient({
|
|
2345
|
+
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
2346
|
+
token,
|
|
2347
|
+
...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
|
|
2348
|
+
});
|
|
2349
|
+
const stats = {
|
|
2350
|
+
accepted: 0,
|
|
2351
|
+
duplicate: 0,
|
|
2352
|
+
conflict: 0,
|
|
2353
|
+
rejected: 0,
|
|
2354
|
+
unresolved: 0,
|
|
2355
|
+
deferred: 0
|
|
2356
|
+
};
|
|
2357
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2358
|
+
let buffer = [];
|
|
2359
|
+
let runId = null;
|
|
2360
|
+
let joined = false;
|
|
2361
|
+
let offline = token === "";
|
|
2362
|
+
let done = false;
|
|
2363
|
+
if (offline) {
|
|
2364
|
+
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
2365
|
+
}
|
|
2366
|
+
if (env.ignored.length > 0) {
|
|
2367
|
+
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2368
|
+
}
|
|
2369
|
+
function defer(results) {
|
|
2370
|
+
appendBatch(fallbackPath, { runId, externalKey, results });
|
|
2371
|
+
stats.deferred += results.length;
|
|
2372
|
+
}
|
|
2373
|
+
async function resolveKeys(values) {
|
|
2374
|
+
const unknown = values.filter((k) => !resolved.has(k.value));
|
|
2375
|
+
if (unknown.length === 0 || offline) return;
|
|
2376
|
+
for (const part of chunk(unknown, RESOLVE_CHUNK)) {
|
|
2377
|
+
const out = await client.post(
|
|
2378
|
+
"/v1/test-cases/resolve",
|
|
2379
|
+
{ keys: part }
|
|
2380
|
+
);
|
|
2381
|
+
if (!out.ok) {
|
|
2382
|
+
log(`plune: could not look up test cases (${out.detail || out.kind}).`);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
if (!offline) {
|
|
2389
|
+
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
2390
|
+
const declared = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
2391
|
+
const configuration = declared.length === 0 ? void 0 : {
|
|
2392
|
+
expected: declared.flatMap((keys) => {
|
|
2393
|
+
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
2394
|
+
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
2395
|
+
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
2396
|
+
})
|
|
2397
|
+
};
|
|
2398
|
+
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
2399
|
+
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
2400
|
+
}
|
|
2401
|
+
const start = await client.post("/v1/runs", {
|
|
2402
|
+
schemaVersion: 2,
|
|
2403
|
+
kind: cfg.kind ?? "automated",
|
|
2404
|
+
...externalKey !== null ? { externalKey } : {},
|
|
2405
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2406
|
+
...configuration !== void 0 ? { configuration } : {}
|
|
2407
|
+
});
|
|
2408
|
+
if (start.ok) {
|
|
2409
|
+
runId = start.body.run.id;
|
|
2410
|
+
joined = start.status === 200;
|
|
2411
|
+
} else {
|
|
2412
|
+
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function matchFor(result) {
|
|
2416
|
+
if (result.testCaseId !== void 0) return { kind: "found", testCaseId: result.testCaseId };
|
|
2417
|
+
let unanswered = false;
|
|
2418
|
+
for (const key of result.keys) {
|
|
2419
|
+
const hit = resolved.get(key.value);
|
|
2420
|
+
if (typeof hit === "string") return { kind: "found", testCaseId: hit };
|
|
2421
|
+
if (hit === void 0) unanswered = true;
|
|
2422
|
+
}
|
|
2423
|
+
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
2424
|
+
}
|
|
2425
|
+
async function flush() {
|
|
2426
|
+
if (buffer.length === 0) return;
|
|
2427
|
+
const pending = buffer;
|
|
2428
|
+
buffer = [];
|
|
2429
|
+
if (offline || runId === null) {
|
|
2430
|
+
defer(pending);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
2434
|
+
const submissions = [];
|
|
2435
|
+
const unanswered = [];
|
|
2436
|
+
for (const result of pending) {
|
|
2437
|
+
const match = matchFor(result);
|
|
2438
|
+
if (match.kind === "found") {
|
|
2439
|
+
const { keys: _keys, ...rest } = result;
|
|
2440
|
+
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
2441
|
+
} else if (match.kind === "unmatched") {
|
|
2442
|
+
stats.unresolved += 1;
|
|
2443
|
+
} else {
|
|
2444
|
+
unanswered.push(result);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (unanswered.length > 0) defer(unanswered);
|
|
2448
|
+
if (submissions.length === 0) return;
|
|
2449
|
+
const out = await client.post(
|
|
2450
|
+
`/v1/runs/${runId}/results`,
|
|
2451
|
+
{ results: submissions }
|
|
2452
|
+
);
|
|
2453
|
+
if (out.ok) {
|
|
2454
|
+
stats.accepted += out.body.counts.accepted ?? 0;
|
|
2455
|
+
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
2456
|
+
stats.conflict += out.body.counts.conflict ?? 0;
|
|
2457
|
+
stats.rejected += out.body.counts.rejected ?? 0;
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
if (out.kind === "auth") {
|
|
2461
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
2462
|
+
offline = true;
|
|
2463
|
+
} else if (out.kind === "conflict") {
|
|
2464
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
2465
|
+
} else {
|
|
2466
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
2467
|
+
}
|
|
2468
|
+
defer(submissions);
|
|
2469
|
+
}
|
|
2470
|
+
function summarise() {
|
|
2471
|
+
const parts = [`${stats.accepted} accepted`];
|
|
2472
|
+
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
|
2473
|
+
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
2474
|
+
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
2475
|
+
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2476
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
2477
|
+
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
2478
|
+
}
|
|
2479
|
+
return {
|
|
2480
|
+
get runId() {
|
|
2481
|
+
return runId;
|
|
2482
|
+
},
|
|
2483
|
+
get joined() {
|
|
2484
|
+
return joined;
|
|
2485
|
+
},
|
|
2486
|
+
get stats() {
|
|
2487
|
+
return stats;
|
|
2488
|
+
},
|
|
2489
|
+
async add(result) {
|
|
2490
|
+
buffer.push(result);
|
|
2491
|
+
if (buffer.length >= batchSize) await flush();
|
|
2492
|
+
},
|
|
2493
|
+
flush,
|
|
2494
|
+
async finish(reason) {
|
|
2495
|
+
if (done) return;
|
|
2496
|
+
done = true;
|
|
2497
|
+
await flush();
|
|
2498
|
+
if (runId !== null && !offline) {
|
|
2499
|
+
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
2500
|
+
event: "finish",
|
|
2501
|
+
...reason !== void 0 ? { reason } : {}
|
|
2502
|
+
});
|
|
2503
|
+
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
2504
|
+
}
|
|
2505
|
+
summarise();
|
|
2506
|
+
},
|
|
2507
|
+
async leaveOpen() {
|
|
2508
|
+
if (done) return;
|
|
2509
|
+
done = true;
|
|
2510
|
+
await flush();
|
|
2511
|
+
if (runId !== null) {
|
|
2512
|
+
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
2513
|
+
}
|
|
2514
|
+
summarise();
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
var RESOLVE_CHUNK, BATCH_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2519
|
+
var init_session = __esm({
|
|
2520
|
+
"src/reporter-core/session.ts"() {
|
|
2521
|
+
"use strict";
|
|
2522
|
+
init_cjs_shims();
|
|
2523
|
+
init_api_url();
|
|
2524
|
+
init_credentials();
|
|
2525
|
+
init_client();
|
|
2526
|
+
init_env2();
|
|
2527
|
+
init_fallback();
|
|
2528
|
+
RESOLVE_CHUNK = 500;
|
|
2529
|
+
BATCH_MAX = 500;
|
|
2530
|
+
BATCH_DEFAULT = 100;
|
|
2531
|
+
EXPECTED_MAX = 1e4;
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
// src/cli/commands/run-lifecycle.ts
|
|
2536
|
+
var run_lifecycle_exports = {};
|
|
2537
|
+
__export(run_lifecycle_exports, {
|
|
2538
|
+
NoTokenError: () => NoTokenError,
|
|
2539
|
+
RunCommandError: () => RunCommandError,
|
|
2540
|
+
handleRunExec: () => handleRunExec,
|
|
2541
|
+
handleRunFinish: () => handleRunFinish,
|
|
2542
|
+
handleRunReport: () => handleRunReport,
|
|
2543
|
+
handleRunStart: () => handleRunStart
|
|
2544
|
+
});
|
|
2545
|
+
function connect(deps) {
|
|
2546
|
+
const env = readEnv();
|
|
2547
|
+
const apiUrl = resolveApiUrl(deps.apiUrl ?? env.config.apiUrl);
|
|
2548
|
+
const token = deps.token ?? env.config.token ?? loadToken() ?? "";
|
|
2549
|
+
if (token === "") throw new NoTokenError();
|
|
2550
|
+
return {
|
|
2551
|
+
client: createClient({
|
|
2552
|
+
apiUrl,
|
|
2553
|
+
token,
|
|
2554
|
+
...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {}
|
|
2555
|
+
}),
|
|
2556
|
+
apiUrl,
|
|
2557
|
+
write: deps.write ?? ((line) => void process.stdout.write(`${line}
|
|
2558
|
+
`))
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
function runUrl(apiUrl, id) {
|
|
2562
|
+
const web = dashboardUrl(apiUrl);
|
|
2563
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id}` : `${web}/runs/${id}`;
|
|
2564
|
+
}
|
|
2565
|
+
async function handleRunStart(options = {}) {
|
|
2566
|
+
const { client, apiUrl, write } = connect(options);
|
|
2567
|
+
const env = readEnv();
|
|
2568
|
+
const externalKey = options.key ?? env.config.externalKey ?? `plune-${(options.now ?? Date.now)().toString(36)}`;
|
|
2569
|
+
const out = await client.post("/v1/runs", {
|
|
2570
|
+
schemaVersion: 2,
|
|
2571
|
+
kind: options.kind ?? "automated",
|
|
2572
|
+
externalKey
|
|
2573
|
+
});
|
|
2574
|
+
if (!out.ok) throw new RunCommandError(`Could not start the run \u2014 ${out.detail || out.kind}.`);
|
|
2575
|
+
const result = {
|
|
2576
|
+
id: out.body.run.id,
|
|
2577
|
+
joined: out.status === 200,
|
|
2578
|
+
externalKey,
|
|
2579
|
+
url: runUrl(apiUrl, out.body.run.id)
|
|
2580
|
+
};
|
|
2581
|
+
if (options.json === true) {
|
|
2582
|
+
write(JSON.stringify(result));
|
|
2583
|
+
} else {
|
|
2584
|
+
write(`${result.joined ? "Joined" : "Started"} run ${result.id}`);
|
|
2585
|
+
write(result.url);
|
|
2586
|
+
}
|
|
2587
|
+
return result;
|
|
2588
|
+
}
|
|
2589
|
+
async function handleRunFinish(id, options = {}) {
|
|
2590
|
+
const { client, write } = connect(options);
|
|
2591
|
+
const event = options.terminate === true ? "terminate" : "finish";
|
|
2592
|
+
const out = await client.post(`/v1/runs/${id}/events`, {
|
|
2593
|
+
event,
|
|
2594
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
2595
|
+
});
|
|
2596
|
+
if (!out.ok) {
|
|
2597
|
+
throw new RunCommandError(
|
|
2598
|
+
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}.`
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
write(`Run ${id} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
2602
|
+
}
|
|
2603
|
+
async function handleRunExec(options) {
|
|
2604
|
+
const [command, ...args] = options.argv;
|
|
2605
|
+
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
2606
|
+
const run = await handleRunStart(options);
|
|
2607
|
+
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
2608
|
+
const code = await new Promise((resolve6) => {
|
|
2609
|
+
const child = spawnImpl(command, args, {
|
|
2610
|
+
stdio: "inherit",
|
|
2611
|
+
shell: process.platform === "win32",
|
|
2612
|
+
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
2613
|
+
});
|
|
2614
|
+
child.on("close", (exitCode) => resolve6(exitCode ?? 1));
|
|
2615
|
+
child.on("error", () => resolve6(1));
|
|
2616
|
+
});
|
|
2617
|
+
await handleRunFinish(run.id, options);
|
|
2618
|
+
return code;
|
|
2619
|
+
}
|
|
2620
|
+
async function handleRunReport(options = {}) {
|
|
2621
|
+
const file = options.file ?? DEFAULT_FALLBACK_PATH;
|
|
2622
|
+
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
2623
|
+
`));
|
|
2624
|
+
let raw;
|
|
2625
|
+
try {
|
|
2626
|
+
raw = fs7.readFileSync(file, "utf8");
|
|
2627
|
+
} catch {
|
|
2628
|
+
throw new RunCommandError(`Nothing to send \u2014 no file at ${file}.`);
|
|
2629
|
+
}
|
|
2630
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
|
|
2631
|
+
if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
|
|
2632
|
+
const { client } = connect(options);
|
|
2633
|
+
const result = { batches: lines.length, sent: 0, failed: 0 };
|
|
2634
|
+
for (const line of lines) {
|
|
2635
|
+
const pending = line.results.filter(isPending);
|
|
2636
|
+
const ready = line.results.filter((r) => !isPending(r));
|
|
2637
|
+
if (line.runId !== null && ready.length > 0) {
|
|
2638
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
|
|
2639
|
+
if (out.ok) result.sent += ready.length;
|
|
2640
|
+
else {
|
|
2641
|
+
result.failed += ready.length;
|
|
2642
|
+
write(`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
if (pending.length > 0) {
|
|
2646
|
+
const session = await startRun({
|
|
2647
|
+
...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
|
|
2648
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
2649
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2650
|
+
...line.externalKey !== null ? { externalKey: line.externalKey } : {},
|
|
2651
|
+
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
2652
|
+
log: write
|
|
2653
|
+
});
|
|
2654
|
+
for (const r of pending) await session.add(r);
|
|
2655
|
+
await session.leaveOpen();
|
|
2656
|
+
result.sent += session.stats.accepted + session.stats.duplicate;
|
|
2657
|
+
result.failed += session.stats.deferred + session.stats.unresolved;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
var import_node_child_process, fs7, NoTokenError, RunCommandError, isPending;
|
|
2664
|
+
var init_run_lifecycle = __esm({
|
|
2665
|
+
"src/cli/commands/run-lifecycle.ts"() {
|
|
2666
|
+
"use strict";
|
|
2667
|
+
init_cjs_shims();
|
|
2668
|
+
import_node_child_process = require("child_process");
|
|
2669
|
+
fs7 = __toESM(require("fs"), 1);
|
|
2670
|
+
init_api_url();
|
|
2671
|
+
init_credentials();
|
|
2672
|
+
init_client();
|
|
2673
|
+
init_env2();
|
|
2674
|
+
init_fallback();
|
|
2675
|
+
init_session();
|
|
2676
|
+
NoTokenError = class extends Error {
|
|
2677
|
+
constructor() {
|
|
2678
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
2679
|
+
this.name = "NoTokenError";
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
RunCommandError = class extends Error {
|
|
2683
|
+
constructor(message) {
|
|
2684
|
+
super(message);
|
|
2685
|
+
this.name = "RunCommandError";
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
isPending = (r) => "keys" in r;
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2120
2692
|
// src/cli/commands/report.ts
|
|
2121
2693
|
var report_exports = {};
|
|
2122
2694
|
__export(report_exports, {
|
|
@@ -2130,10 +2702,10 @@ function isRunResult(v) {
|
|
|
2130
2702
|
}
|
|
2131
2703
|
function handleReport(opts = {}) {
|
|
2132
2704
|
const cwd = opts.cwd ?? process.cwd();
|
|
2133
|
-
const file =
|
|
2705
|
+
const file = path7.join(cwd, ".plune", "last-run.json");
|
|
2134
2706
|
let raw;
|
|
2135
2707
|
try {
|
|
2136
|
-
raw =
|
|
2708
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
2137
2709
|
} catch {
|
|
2138
2710
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
2139
2711
|
}
|
|
@@ -2148,13 +2720,13 @@ function handleReport(opts = {}) {
|
|
|
2148
2720
|
}
|
|
2149
2721
|
return parsed;
|
|
2150
2722
|
}
|
|
2151
|
-
var
|
|
2723
|
+
var fs8, path7, ReportNotFoundError;
|
|
2152
2724
|
var init_report = __esm({
|
|
2153
2725
|
"src/cli/commands/report.ts"() {
|
|
2154
2726
|
"use strict";
|
|
2155
2727
|
init_cjs_shims();
|
|
2156
|
-
|
|
2157
|
-
|
|
2728
|
+
fs8 = __toESM(require("fs"), 1);
|
|
2729
|
+
path7 = __toESM(require("path"), 1);
|
|
2158
2730
|
ReportNotFoundError = class extends Error {
|
|
2159
2731
|
code = "NO_SAVED_RUN";
|
|
2160
2732
|
constructor(message) {
|
|
@@ -2177,7 +2749,7 @@ function evalStatus(e) {
|
|
|
2177
2749
|
if (statuses.includes("errored")) return "errored";
|
|
2178
2750
|
return "passed";
|
|
2179
2751
|
}
|
|
2180
|
-
function
|
|
2752
|
+
function classify4(baseline, current) {
|
|
2181
2753
|
if (current === "errored") return "errored";
|
|
2182
2754
|
if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
|
|
2183
2755
|
if (current === "absent") return "removed";
|
|
@@ -2191,19 +2763,19 @@ function diffRuns(baseline, current) {
|
|
|
2191
2763
|
const evals = ids.map((id) => {
|
|
2192
2764
|
const baselineP = baseMap.get(id) ?? "absent";
|
|
2193
2765
|
const currentP = curMap.get(id) ?? "absent";
|
|
2194
|
-
return { id, status:
|
|
2766
|
+
return { id, status: classify4(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
2195
2767
|
});
|
|
2196
|
-
const
|
|
2768
|
+
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
2197
2769
|
const summary = {
|
|
2198
|
-
regressions:
|
|
2199
|
-
improvements:
|
|
2200
|
-
preExistingFails:
|
|
2201
|
-
newFails:
|
|
2202
|
-
newPasses:
|
|
2203
|
-
stablePasses:
|
|
2204
|
-
errored:
|
|
2205
|
-
removed:
|
|
2206
|
-
hasRegression:
|
|
2770
|
+
regressions: count2("regression"),
|
|
2771
|
+
improvements: count2("improvement"),
|
|
2772
|
+
preExistingFails: count2("pre-existing-fail"),
|
|
2773
|
+
newFails: count2("new-fail"),
|
|
2774
|
+
newPasses: count2("new-pass"),
|
|
2775
|
+
stablePasses: count2("stable-pass"),
|
|
2776
|
+
errored: count2("errored"),
|
|
2777
|
+
removed: count2("removed"),
|
|
2778
|
+
hasRegression: count2("regression") > 0
|
|
2207
2779
|
};
|
|
2208
2780
|
return { evals, summary };
|
|
2209
2781
|
}
|
|
@@ -2228,7 +2800,7 @@ function isRunResult2(v) {
|
|
|
2228
2800
|
function loadRunResult(file, label) {
|
|
2229
2801
|
let raw;
|
|
2230
2802
|
try {
|
|
2231
|
-
raw =
|
|
2803
|
+
raw = fs9.readFileSync(file, "utf8");
|
|
2232
2804
|
} catch {
|
|
2233
2805
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
2234
2806
|
}
|
|
@@ -2248,12 +2820,12 @@ function handleDiff(opts) {
|
|
|
2248
2820
|
const current = loadRunResult(opts.currentPath, "current");
|
|
2249
2821
|
return diffRuns(baseline, current);
|
|
2250
2822
|
}
|
|
2251
|
-
var
|
|
2823
|
+
var fs9, DiffInputError;
|
|
2252
2824
|
var init_diff2 = __esm({
|
|
2253
2825
|
"src/cli/commands/diff.ts"() {
|
|
2254
2826
|
"use strict";
|
|
2255
2827
|
init_cjs_shims();
|
|
2256
|
-
|
|
2828
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2257
2829
|
init_diff();
|
|
2258
2830
|
DiffInputError = class extends Error {
|
|
2259
2831
|
code = "DIFF_INPUT";
|
|
@@ -2378,8 +2950,8 @@ async function runInitWizard(cwd) {
|
|
|
2378
2950
|
process.exit(1);
|
|
2379
2951
|
return;
|
|
2380
2952
|
}
|
|
2381
|
-
const outputPath =
|
|
2382
|
-
if (
|
|
2953
|
+
const outputPath = path8.join(cwd, "plune.yaml");
|
|
2954
|
+
if (fs10.existsSync(outputPath)) {
|
|
2383
2955
|
const overwrite = await clack.confirm({
|
|
2384
2956
|
message: "plune.yaml already exists. Overwrite?"
|
|
2385
2957
|
});
|
|
@@ -2418,18 +2990,18 @@ async function runInitWizard(cwd) {
|
|
|
2418
2990
|
`;
|
|
2419
2991
|
const spin = clack.spinner();
|
|
2420
2992
|
spin.start("Writing plune.yaml\u2026");
|
|
2421
|
-
|
|
2993
|
+
fs10.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
2422
2994
|
spin.stop("plune.yaml created!");
|
|
2423
|
-
clack.outro("
|
|
2995
|
+
clack.outro("plune.yaml is ready.");
|
|
2424
2996
|
}
|
|
2425
|
-
var clack,
|
|
2997
|
+
var clack, fs10, path8, import_yaml2;
|
|
2426
2998
|
var init_wizard = __esm({
|
|
2427
2999
|
"src/config/init/wizard.ts"() {
|
|
2428
3000
|
"use strict";
|
|
2429
3001
|
init_cjs_shims();
|
|
2430
3002
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
2431
|
-
|
|
2432
|
-
|
|
3003
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3004
|
+
path8 = __toESM(require("path"), 1);
|
|
2433
3005
|
import_yaml2 = require("yaml");
|
|
2434
3006
|
init_errors();
|
|
2435
3007
|
}
|
|
@@ -2437,21 +3009,21 @@ var init_wizard = __esm({
|
|
|
2437
3009
|
|
|
2438
3010
|
// src/cli/templates/index.ts
|
|
2439
3011
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
2440
|
-
const target =
|
|
2441
|
-
if (!force &&
|
|
3012
|
+
const target = path9.join(cwd, rel);
|
|
3013
|
+
if (!force && fs11.existsSync(target)) {
|
|
2442
3014
|
return "skipped";
|
|
2443
3015
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
3016
|
+
fs11.mkdirSync(path9.dirname(target), { recursive: true });
|
|
3017
|
+
fs11.writeFileSync(target, content, "utf8");
|
|
2446
3018
|
return "written";
|
|
2447
3019
|
}
|
|
2448
|
-
var
|
|
3020
|
+
var fs11, path9, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
2449
3021
|
var init_templates = __esm({
|
|
2450
3022
|
"src/cli/templates/index.ts"() {
|
|
2451
3023
|
"use strict";
|
|
2452
3024
|
init_cjs_shims();
|
|
2453
|
-
|
|
2454
|
-
|
|
3025
|
+
fs11 = __toESM(require("fs"), 1);
|
|
3026
|
+
path9 = __toESM(require("path"), 1);
|
|
2455
3027
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
2456
3028
|
# .env is gitignored; .env.example is safe to commit.
|
|
2457
3029
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -2487,6 +3059,7 @@ evals:
|
|
|
2487
3059
|
// src/cli/commands/init.ts
|
|
2488
3060
|
var init_exports = {};
|
|
2489
3061
|
__export(init_exports, {
|
|
3062
|
+
NEXT_STEPS: () => NEXT_STEPS,
|
|
2490
3063
|
initCommand: () => initCommand
|
|
2491
3064
|
});
|
|
2492
3065
|
function reportWrite(cwd, rel, content, force) {
|
|
@@ -2506,8 +3079,9 @@ async function initCommand({ cwd, force, yes = false }) {
|
|
|
2506
3079
|
for (const [rel, content] of TEMPLATES) {
|
|
2507
3080
|
reportWrite(cwd, rel, content, force);
|
|
2508
3081
|
}
|
|
3082
|
+
process.stdout.write(NEXT_STEPS);
|
|
2509
3083
|
}
|
|
2510
|
-
var TEMPLATES;
|
|
3084
|
+
var TEMPLATES, NEXT_STEPS;
|
|
2511
3085
|
var init_init = __esm({
|
|
2512
3086
|
"src/cli/commands/init.ts"() {
|
|
2513
3087
|
"use strict";
|
|
@@ -2518,43 +3092,14 @@ var init_init = __esm({
|
|
|
2518
3092
|
["datasets/example.jsonl", EXAMPLE_JSONL_TPL],
|
|
2519
3093
|
[".env.example", ENV_EXAMPLE_TPL]
|
|
2520
3094
|
];
|
|
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");
|
|
3095
|
+
NEXT_STEPS = [
|
|
3096
|
+
"",
|
|
3097
|
+
"Next:",
|
|
3098
|
+
" 1. plune run \u2014 runs locally. No account, no token needed.",
|
|
3099
|
+
" 2. plune login \u2014 only if you want history and trends: paste a token from the dashboard.",
|
|
3100
|
+
" 3. plune sync \u2014 sends the last run to the platform.",
|
|
3101
|
+
""
|
|
3102
|
+
].join("\n");
|
|
2558
3103
|
}
|
|
2559
3104
|
});
|
|
2560
3105
|
|
|
@@ -2562,25 +3107,81 @@ var init_credentials = __esm({
|
|
|
2562
3107
|
var login_exports = {};
|
|
2563
3108
|
__export(login_exports, {
|
|
2564
3109
|
EmptyTokenError: () => EmptyTokenError,
|
|
2565
|
-
|
|
3110
|
+
LoginHttpError: () => LoginHttpError,
|
|
3111
|
+
LoginNetworkError: () => LoginNetworkError,
|
|
3112
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
3113
|
+
handleLogin: () => handleLogin,
|
|
3114
|
+
reportLoginFailure: () => reportLoginFailure
|
|
2566
3115
|
});
|
|
2567
|
-
function handleLogin(
|
|
2568
|
-
const token =
|
|
2569
|
-
|
|
2570
|
-
|
|
3116
|
+
async function handleLogin(deps) {
|
|
3117
|
+
const token = deps.token.trim();
|
|
3118
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
3119
|
+
if (token.length === 0) throw new EmptyTokenError(apiUrl);
|
|
3120
|
+
if (deps.skipVerify === true) return { path: saveToken(token), verified: false };
|
|
3121
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
3122
|
+
let res;
|
|
3123
|
+
try {
|
|
3124
|
+
res = await doFetch(`${apiUrl}/v1/runs?limit=1`, {
|
|
3125
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3126
|
+
});
|
|
3127
|
+
} catch {
|
|
3128
|
+
throw new LoginNetworkError(apiUrl);
|
|
3129
|
+
}
|
|
3130
|
+
await res.text().catch(() => "");
|
|
3131
|
+
if (res.status === 401) throw new TokenRejectedError(apiUrl);
|
|
3132
|
+
if (!res.ok) throw new LoginHttpError(res.status);
|
|
3133
|
+
return { path: saveToken(token), verified: true };
|
|
3134
|
+
}
|
|
3135
|
+
function reportLoginFailure(err, write) {
|
|
3136
|
+
if (err instanceof EmptyTokenError || err instanceof TokenRejectedError) {
|
|
3137
|
+
write(`${err.message}
|
|
3138
|
+
`);
|
|
3139
|
+
return 2;
|
|
3140
|
+
}
|
|
3141
|
+
if (err instanceof LoginNetworkError || err instanceof LoginHttpError) {
|
|
3142
|
+
write(`${err.message}
|
|
3143
|
+
`);
|
|
3144
|
+
return 1;
|
|
3145
|
+
}
|
|
3146
|
+
return null;
|
|
2571
3147
|
}
|
|
2572
|
-
var EmptyTokenError;
|
|
3148
|
+
var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
|
|
2573
3149
|
var init_login = __esm({
|
|
2574
3150
|
"src/cli/commands/login.ts"() {
|
|
2575
3151
|
"use strict";
|
|
2576
3152
|
init_cjs_shims();
|
|
3153
|
+
init_api_url();
|
|
2577
3154
|
init_credentials();
|
|
2578
3155
|
EmptyTokenError = class extends Error {
|
|
2579
|
-
constructor() {
|
|
2580
|
-
super(
|
|
3156
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
3157
|
+
super(
|
|
3158
|
+
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
3159
|
+
);
|
|
2581
3160
|
this.name = "EmptyTokenError";
|
|
2582
3161
|
}
|
|
2583
3162
|
};
|
|
3163
|
+
TokenRejectedError = class extends Error {
|
|
3164
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
3165
|
+
super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
|
|
3166
|
+
this.name = "TokenRejectedError";
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
LoginNetworkError = class extends Error {
|
|
3170
|
+
constructor(apiUrl) {
|
|
3171
|
+
super(
|
|
3172
|
+
`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.`
|
|
3173
|
+
);
|
|
3174
|
+
this.name = "LoginNetworkError";
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
LoginHttpError = class extends Error {
|
|
3178
|
+
constructor(status) {
|
|
3179
|
+
super(
|
|
3180
|
+
`The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
|
|
3181
|
+
);
|
|
3182
|
+
this.name = "LoginHttpError";
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
2584
3185
|
}
|
|
2585
3186
|
});
|
|
2586
3187
|
|
|
@@ -2601,8 +3202,8 @@ var init_logout = __esm({
|
|
|
2601
3202
|
});
|
|
2602
3203
|
|
|
2603
3204
|
// src/types/run-result-schema.ts
|
|
2604
|
-
function parseRunResult(
|
|
2605
|
-
return runResultSchema.parse(
|
|
3205
|
+
function parseRunResult(value2) {
|
|
3206
|
+
return runResultSchema.parse(value2);
|
|
2606
3207
|
}
|
|
2607
3208
|
var import_zod2, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
2608
3209
|
var init_run_result_schema = __esm({
|
|
@@ -2632,7 +3233,7 @@ var init_run_result_schema = __esm({
|
|
|
2632
3233
|
message: import_zod2.z.string()
|
|
2633
3234
|
});
|
|
2634
3235
|
rowResultSchema = import_zod2.z.object({
|
|
2635
|
-
vars: import_zod2.z.record(import_zod2.z.unknown()),
|
|
3236
|
+
vars: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()),
|
|
2636
3237
|
output: import_zod2.z.string().nullable(),
|
|
2637
3238
|
cached: import_zod2.z.boolean(),
|
|
2638
3239
|
usage: usageSchema.optional(),
|
|
@@ -2673,19 +3274,15 @@ __export(sync_exports, {
|
|
|
2673
3274
|
SyncFileError: () => SyncFileError,
|
|
2674
3275
|
SyncHttpError: () => SyncHttpError,
|
|
2675
3276
|
SyncNetworkError: () => SyncNetworkError,
|
|
2676
|
-
TokenRejectedError: () =>
|
|
3277
|
+
TokenRejectedError: () => TokenRejectedError2,
|
|
2677
3278
|
handleSync: () => handleSync,
|
|
2678
3279
|
reportSyncFailure: () => reportSyncFailure
|
|
2679
3280
|
});
|
|
2680
|
-
function resolveApiUrl(explicit) {
|
|
2681
|
-
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
2682
|
-
return raw.replace(/\/+$/, "");
|
|
2683
|
-
}
|
|
2684
3281
|
function readRun(cwd, file) {
|
|
2685
|
-
const target = file !== void 0 ?
|
|
3282
|
+
const target = file !== void 0 ? path10.resolve(cwd, file) : path10.join(cwd, ".plune", "last-run.json");
|
|
2686
3283
|
let raw;
|
|
2687
3284
|
try {
|
|
2688
|
-
raw =
|
|
3285
|
+
raw = fs12.readFileSync(target, "utf8");
|
|
2689
3286
|
} catch {
|
|
2690
3287
|
throw new SyncFileError(
|
|
2691
3288
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -2731,7 +3328,7 @@ async function handleSync(deps = {}) {
|
|
|
2731
3328
|
}
|
|
2732
3329
|
if (!res.ok) {
|
|
2733
3330
|
const detail = await safeErrorDetail(res);
|
|
2734
|
-
if (res.status === 401) throw new
|
|
3331
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
2735
3332
|
throw new SyncHttpError(res.status, detail);
|
|
2736
3333
|
}
|
|
2737
3334
|
const body = await res.json().catch(() => ({}));
|
|
@@ -2739,7 +3336,7 @@ async function handleSync(deps = {}) {
|
|
|
2739
3336
|
return { id, url: `${endpoint}/${id}` };
|
|
2740
3337
|
}
|
|
2741
3338
|
function reportSyncFailure(err, write) {
|
|
2742
|
-
if (err instanceof NotLoggedInError || err instanceof
|
|
3339
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
2743
3340
|
write(err.message + "\n");
|
|
2744
3341
|
return 2;
|
|
2745
3342
|
}
|
|
@@ -2749,23 +3346,23 @@ function reportSyncFailure(err, write) {
|
|
|
2749
3346
|
}
|
|
2750
3347
|
return null;
|
|
2751
3348
|
}
|
|
2752
|
-
var
|
|
3349
|
+
var fs12, path10, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
2753
3350
|
var init_sync = __esm({
|
|
2754
3351
|
"src/cli/commands/sync.ts"() {
|
|
2755
3352
|
"use strict";
|
|
2756
3353
|
init_cjs_shims();
|
|
2757
|
-
|
|
2758
|
-
|
|
3354
|
+
fs12 = __toESM(require("fs"), 1);
|
|
3355
|
+
path10 = __toESM(require("path"), 1);
|
|
2759
3356
|
init_run_result_schema();
|
|
3357
|
+
init_api_url();
|
|
2760
3358
|
init_credentials();
|
|
2761
|
-
DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
2762
3359
|
NotLoggedInError = class extends Error {
|
|
2763
3360
|
constructor() {
|
|
2764
3361
|
super('Not logged in. Run "plune login" first to save your API token.');
|
|
2765
3362
|
this.name = "NotLoggedInError";
|
|
2766
3363
|
}
|
|
2767
3364
|
};
|
|
2768
|
-
|
|
3365
|
+
TokenRejectedError2 = class extends Error {
|
|
2769
3366
|
constructor() {
|
|
2770
3367
|
super(
|
|
2771
3368
|
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
@@ -2799,15 +3396,34 @@ var init_sync = __esm({
|
|
|
2799
3396
|
});
|
|
2800
3397
|
|
|
2801
3398
|
// src/cli/commands/cairn-artifact.ts
|
|
3399
|
+
async function findLatestCairnRun(runsDir = "runs") {
|
|
3400
|
+
const entries = await fs13.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
3401
|
+
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
3402
|
+
let newest;
|
|
3403
|
+
for (const entry of entries) {
|
|
3404
|
+
if (!entry.isDirectory()) continue;
|
|
3405
|
+
const dir = path11.join(runsDir, entry.name);
|
|
3406
|
+
try {
|
|
3407
|
+
const report = await fs13.lstat(path11.join(dir, "report.json"));
|
|
3408
|
+
if (!report.isFile()) continue;
|
|
3409
|
+
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
3410
|
+
newest = { dir, at: report.mtimeMs };
|
|
3411
|
+
}
|
|
3412
|
+
} catch {
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (newest === void 0) throw new NoCairnRunsError(runsDir);
|
|
3416
|
+
return newest.dir;
|
|
3417
|
+
}
|
|
2802
3418
|
async function readJson(file) {
|
|
2803
3419
|
try {
|
|
2804
|
-
return JSON.parse(await
|
|
3420
|
+
return JSON.parse(await fs13.readFile(file, "utf8"));
|
|
2805
3421
|
} catch {
|
|
2806
3422
|
return void 0;
|
|
2807
3423
|
}
|
|
2808
3424
|
}
|
|
2809
3425
|
async function defaultReadEvidence(dir) {
|
|
2810
|
-
const raw = await readJson(
|
|
3426
|
+
const raw = await readJson(path11.join(dir, "api-evidence.json"));
|
|
2811
3427
|
if (!Array.isArray(raw)) return void 0;
|
|
2812
3428
|
return raw.map((r) => {
|
|
2813
3429
|
const rec = asRecord(r);
|
|
@@ -2849,7 +3465,7 @@ function casesFromApi(report, verdicts) {
|
|
|
2849
3465
|
});
|
|
2850
3466
|
}
|
|
2851
3467
|
async function readCairnRun(dir, deps = {}) {
|
|
2852
|
-
const raw = await readJson(
|
|
3468
|
+
const raw = await readJson(path11.join(dir, "report.json"));
|
|
2853
3469
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
2854
3470
|
const report = asRecord(raw);
|
|
2855
3471
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -2860,7 +3476,9 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
2860
3476
|
}
|
|
2861
3477
|
const mode = report["mode"];
|
|
2862
3478
|
if (mode !== "design" && mode !== "explore" && mode !== "api") {
|
|
2863
|
-
throw new CairnRunVersionError(
|
|
3479
|
+
throw new CairnRunVersionError(
|
|
3480
|
+
`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`
|
|
3481
|
+
);
|
|
2864
3482
|
}
|
|
2865
3483
|
let cases;
|
|
2866
3484
|
if (mode === "api") {
|
|
@@ -2877,25 +3495,33 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
2877
3495
|
return {
|
|
2878
3496
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
2879
3497
|
run: {
|
|
2880
|
-
runId: str(report["runId"],
|
|
3498
|
+
runId: str(report["runId"], path11.basename(dir)),
|
|
2881
3499
|
mode,
|
|
2882
3500
|
target: str(report["url"], "unknown")
|
|
2883
3501
|
},
|
|
2884
3502
|
cases
|
|
2885
3503
|
};
|
|
2886
3504
|
}
|
|
2887
|
-
var
|
|
3505
|
+
var fs13, path11, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
2888
3506
|
var init_cairn_artifact = __esm({
|
|
2889
3507
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
2890
3508
|
"use strict";
|
|
2891
3509
|
init_cjs_shims();
|
|
2892
|
-
|
|
2893
|
-
|
|
3510
|
+
fs13 = __toESM(require("fs/promises"), 1);
|
|
3511
|
+
path11 = __toESM(require("path"), 1);
|
|
2894
3512
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
2895
3513
|
INGEST_SCHEMA_VERSION = 1;
|
|
3514
|
+
NoCairnRunsError = class extends Error {
|
|
3515
|
+
constructor(runsDir) {
|
|
3516
|
+
super(`No Cairn run found under ${runsDir} \u2014 name the directory instead: plune ingest <dir>.`);
|
|
3517
|
+
this.name = "NoCairnRunsError";
|
|
3518
|
+
}
|
|
3519
|
+
};
|
|
2896
3520
|
CairnRunNotFoundError = class extends Error {
|
|
2897
3521
|
constructor(dir) {
|
|
2898
|
-
super(
|
|
3522
|
+
super(
|
|
3523
|
+
`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`
|
|
3524
|
+
);
|
|
2899
3525
|
this.name = "CairnRunNotFoundError";
|
|
2900
3526
|
}
|
|
2901
3527
|
};
|
|
@@ -2937,10 +3563,6 @@ __export(ingest_exports, {
|
|
|
2937
3563
|
handleIngest: () => handleIngest,
|
|
2938
3564
|
reportIngestFailure: () => reportIngestFailure
|
|
2939
3565
|
});
|
|
2940
|
-
function resolveApiUrl2(explicit) {
|
|
2941
|
-
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
|
|
2942
|
-
return raw.replace(/\/+$/, "");
|
|
2943
|
-
}
|
|
2944
3566
|
async function safeErrorDetail2(res) {
|
|
2945
3567
|
try {
|
|
2946
3568
|
const body = await res.json();
|
|
@@ -2950,10 +3572,11 @@ async function safeErrorDetail2(res) {
|
|
|
2950
3572
|
}
|
|
2951
3573
|
}
|
|
2952
3574
|
async function handleIngest(deps) {
|
|
2953
|
-
const apiUrl =
|
|
3575
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
2954
3576
|
const token = (deps.loadToken ?? loadToken)();
|
|
2955
3577
|
if (token === null) throw new IngestNotLoggedInError();
|
|
2956
|
-
const
|
|
3578
|
+
const dir = deps.dir ?? await findLatestCairnRun(deps.runsDir);
|
|
3579
|
+
const payload = await readCairnRun(dir);
|
|
2957
3580
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2958
3581
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2959
3582
|
let res;
|
|
@@ -2980,7 +3603,7 @@ async function handleIngest(deps) {
|
|
|
2980
3603
|
};
|
|
2981
3604
|
}
|
|
2982
3605
|
function reportIngestFailure(err, write) {
|
|
2983
|
-
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
3606
|
+
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
2984
3607
|
write(`${err.message}
|
|
2985
3608
|
`);
|
|
2986
3609
|
return 2;
|
|
@@ -3002,14 +3625,14 @@ function formatIngestResult(r) {
|
|
|
3002
3625
|
return `${lines.join("\n")}
|
|
3003
3626
|
`;
|
|
3004
3627
|
}
|
|
3005
|
-
var
|
|
3628
|
+
var IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
3006
3629
|
var init_ingest = __esm({
|
|
3007
3630
|
"src/cli/commands/ingest.ts"() {
|
|
3008
3631
|
"use strict";
|
|
3009
3632
|
init_cjs_shims();
|
|
3633
|
+
init_api_url();
|
|
3010
3634
|
init_credentials();
|
|
3011
3635
|
init_cairn_artifact();
|
|
3012
|
-
DEFAULT_API_URL2 = "https://beta-api.plune.ai";
|
|
3013
3636
|
IngestNotLoggedInError = class extends Error {
|
|
3014
3637
|
constructor() {
|
|
3015
3638
|
super('Not logged in. Run "plune login" first to save your API token.');
|
|
@@ -3046,6 +3669,7 @@ __export(cli_exports, {
|
|
|
3046
3669
|
});
|
|
3047
3670
|
module.exports = __toCommonJS(cli_exports);
|
|
3048
3671
|
init_cjs_shims();
|
|
3672
|
+
init_api_url();
|
|
3049
3673
|
var import_commander = require("commander");
|
|
3050
3674
|
var import_node_fs2 = require("fs");
|
|
3051
3675
|
var import_node_path2 = require("path");
|
|
@@ -3076,10 +3700,17 @@ function failUnexpected(err, verbose) {
|
|
|
3076
3700
|
} else {
|
|
3077
3701
|
process.stderr.write(String(err) + "\n");
|
|
3078
3702
|
}
|
|
3703
|
+
process.stderr.write(
|
|
3704
|
+
`This one is on us. Run again with --verbose and send the output: ${FEEDBACK_URL}
|
|
3705
|
+
`
|
|
3706
|
+
);
|
|
3079
3707
|
process.exit(1);
|
|
3080
3708
|
}
|
|
3081
3709
|
async function readTokenFromStdin() {
|
|
3082
3710
|
if (process.stdin.isTTY === true) {
|
|
3711
|
+
const { resolveApiUrl: resolveApiUrl2, whereToGetAToken: whereToGetAToken2 } = await Promise.resolve().then(() => (init_api_url(), api_url_exports));
|
|
3712
|
+
process.stderr.write(`${whereToGetAToken2(resolveApiUrl2())}
|
|
3713
|
+
`);
|
|
3083
3714
|
process.stderr.write("Paste your API token, then press Enter:\n");
|
|
3084
3715
|
}
|
|
3085
3716
|
const { createInterface } = await import("readline");
|
|
@@ -3095,7 +3726,7 @@ function createProgram() {
|
|
|
3095
3726
|
const program = new import_commander.Command();
|
|
3096
3727
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
3097
3728
|
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(
|
|
3729
|
+
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
3730
|
"--only <selector>",
|
|
3100
3731
|
"Run a subset by eval id or tag (repeatable)",
|
|
3101
3732
|
(val, prev) => [...prev, val],
|
|
@@ -3171,6 +3802,58 @@ function createProgram() {
|
|
|
3171
3802
|
}
|
|
3172
3803
|
}
|
|
3173
3804
|
);
|
|
3805
|
+
const failRunCommand = async (err, verbose) => {
|
|
3806
|
+
const { NoTokenError: NoTokenError2, RunCommandError: RunCommandError2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3807
|
+
if (err instanceof NoTokenError2) {
|
|
3808
|
+
process.stderr.write(err.message + "\n");
|
|
3809
|
+
process.exit(2);
|
|
3810
|
+
}
|
|
3811
|
+
if (err instanceof RunCommandError2) {
|
|
3812
|
+
process.stderr.write(err.message + "\n");
|
|
3813
|
+
process.exit(1);
|
|
3814
|
+
}
|
|
3815
|
+
failUnexpected(err, verbose);
|
|
3816
|
+
process.exit(1);
|
|
3817
|
+
};
|
|
3818
|
+
const verboseOf = (command) => command.optsWithGlobals().verbose === true;
|
|
3819
|
+
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) => {
|
|
3820
|
+
try {
|
|
3821
|
+
const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3822
|
+
await handleRunStart2({ ...options.key !== void 0 ? { key: options.key } : {}, json: options.json });
|
|
3823
|
+
} catch (err) {
|
|
3824
|
+
await failRunCommand(err, verboseOf(command));
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
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) => {
|
|
3828
|
+
try {
|
|
3829
|
+
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3830
|
+
await handleRunFinish2(id, {
|
|
3831
|
+
terminate: options.terminate,
|
|
3832
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
3833
|
+
});
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
await failRunCommand(err, verboseOf(command));
|
|
3836
|
+
}
|
|
3837
|
+
});
|
|
3838
|
+
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) => {
|
|
3839
|
+
try {
|
|
3840
|
+
const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3841
|
+
process.exitCode = await handleRunExec2({
|
|
3842
|
+
argv,
|
|
3843
|
+
...options.key !== void 0 ? { key: options.key } : {}
|
|
3844
|
+
});
|
|
3845
|
+
} catch (err) {
|
|
3846
|
+
await failRunCommand(err, verboseOf(command));
|
|
3847
|
+
}
|
|
3848
|
+
});
|
|
3849
|
+
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) => {
|
|
3850
|
+
try {
|
|
3851
|
+
const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
3852
|
+
await handleRunReport2({ ...options.file !== void 0 ? { file: options.file } : {} });
|
|
3853
|
+
} catch (err) {
|
|
3854
|
+
await failRunCommand(err, verboseOf(command));
|
|
3855
|
+
}
|
|
3856
|
+
});
|
|
3174
3857
|
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
3858
|
const globals = command.optsWithGlobals();
|
|
3176
3859
|
const [{ loadEnv: loadEnv2 }, { handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
@@ -3234,18 +3917,26 @@ function createProgram() {
|
|
|
3234
3917
|
failUnexpected(err, verbose);
|
|
3235
3918
|
}
|
|
3236
3919
|
});
|
|
3237
|
-
program.command("login").description("Save your Plune API token
|
|
3920
|
+
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(
|
|
3921
|
+
"--skip-verify",
|
|
3922
|
+
"Save without checking the token against the API (offline setup)",
|
|
3923
|
+
false
|
|
3924
|
+
).action(async (options, command) => {
|
|
3238
3925
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
3239
|
-
const { handleLogin: handleLogin2,
|
|
3926
|
+
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
3240
3927
|
try {
|
|
3241
3928
|
const token = options.token ?? await readTokenFromStdin();
|
|
3242
|
-
const { path:
|
|
3243
|
-
process.stdout.write(
|
|
3244
|
-
`
|
|
3929
|
+
const { path: path12, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
3930
|
+
process.stdout.write(
|
|
3931
|
+
verified ? `Logged in. Token checked against the API and saved to ${path12}
|
|
3932
|
+
` : `Token saved to ${path12} WITHOUT being checked (--skip-verify).
|
|
3933
|
+
`
|
|
3934
|
+
);
|
|
3245
3935
|
} catch (err) {
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3936
|
+
const code = reportLoginFailure2(err, (s) => process.stderr.write(s));
|
|
3937
|
+
if (code !== null) {
|
|
3938
|
+
maybeStack(err, verbose);
|
|
3939
|
+
process.exitCode = code;
|
|
3249
3940
|
return;
|
|
3250
3941
|
}
|
|
3251
3942
|
failUnexpected(err, verbose);
|
|
@@ -3255,9 +3946,9 @@ function createProgram() {
|
|
|
3255
3946
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
3256
3947
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
3257
3948
|
try {
|
|
3258
|
-
const { removed, path:
|
|
3949
|
+
const { removed, path: path12 } = handleLogout2();
|
|
3259
3950
|
process.stdout.write(
|
|
3260
|
-
removed ? `Logged out. Removed ${
|
|
3951
|
+
removed ? `Logged out. Removed ${path12}
|
|
3261
3952
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
3262
3953
|
);
|
|
3263
3954
|
} catch (err) {
|
|
@@ -3286,12 +3977,14 @@ function createProgram() {
|
|
|
3286
3977
|
failUnexpected(err, verbose);
|
|
3287
3978
|
}
|
|
3288
3979
|
});
|
|
3289
|
-
program.command("ingest").argument("
|
|
3980
|
+
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
3981
|
const globals = command.optsWithGlobals();
|
|
3291
3982
|
const verbose = globals.verbose === true;
|
|
3292
3983
|
const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
3293
3984
|
try {
|
|
3294
|
-
process.stdout.write(
|
|
3985
|
+
process.stdout.write(
|
|
3986
|
+
formatIngestResult2(await handleIngest2({ ...dir !== void 0 ? { dir } : {} }))
|
|
3987
|
+
);
|
|
3295
3988
|
} catch (err) {
|
|
3296
3989
|
const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
|
|
3297
3990
|
if (code !== null) {
|