@plune-ai/cli 0.5.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 +1093 -141
- package/dist/index.cjs +264 -12
- package/dist/index.d.cts +104 -423
- package/dist/index.d.ts +104 -423
- package/dist/index.js +260 -12
- 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 +22 -5
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
|
}
|
|
@@ -808,9 +837,11 @@ var init_embedder = __esm({
|
|
|
808
837
|
return rows.map((row) => Float32Array.from(row));
|
|
809
838
|
}
|
|
810
839
|
load() {
|
|
811
|
-
this.extractor ??= import("@huggingface/transformers").
|
|
812
|
-
|
|
813
|
-
|
|
840
|
+
this.extractor ??= import("@huggingface/transformers").catch(() => {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
|
|
843
|
+
);
|
|
844
|
+
}).then(({ pipeline }) => pipeline("feature-extraction", MODEL_ID));
|
|
814
845
|
return this.extractor;
|
|
815
846
|
}
|
|
816
847
|
};
|
|
@@ -884,7 +915,7 @@ var init_cache = __esm({
|
|
|
884
915
|
return void 0;
|
|
885
916
|
}
|
|
886
917
|
}
|
|
887
|
-
set(key,
|
|
918
|
+
set(key, value2) {
|
|
888
919
|
try {
|
|
889
920
|
this.db.prepare(
|
|
890
921
|
`INSERT INTO completions (cache_key, output, input_tokens, output_tokens, created_at)
|
|
@@ -894,7 +925,7 @@ var init_cache = __esm({
|
|
|
894
925
|
input_tokens = excluded.input_tokens,
|
|
895
926
|
output_tokens = excluded.output_tokens,
|
|
896
927
|
created_at = excluded.created_at`
|
|
897
|
-
).run(key,
|
|
928
|
+
).run(key, value2.output, value2.usage.input_tokens, value2.usage.output_tokens, this.now());
|
|
898
929
|
} catch {
|
|
899
930
|
}
|
|
900
931
|
}
|
|
@@ -932,32 +963,32 @@ var init_cache2 = __esm({
|
|
|
932
963
|
});
|
|
933
964
|
|
|
934
965
|
// src/util/canonical-json.ts
|
|
935
|
-
function normalizeValue(
|
|
936
|
-
if (
|
|
937
|
-
if (typeof
|
|
938
|
-
if (typeof
|
|
939
|
-
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)) {
|
|
940
971
|
throw new Error(
|
|
941
|
-
`canonicalJson: non-finite number rejected: ${String(
|
|
972
|
+
`canonicalJson: non-finite number rejected: ${String(value2)}`
|
|
942
973
|
);
|
|
943
974
|
}
|
|
944
|
-
return
|
|
975
|
+
return value2 === 0 ? 0 : value2;
|
|
945
976
|
}
|
|
946
|
-
if (typeof
|
|
947
|
-
return
|
|
977
|
+
if (typeof value2 === "string") {
|
|
978
|
+
return value2.normalize("NFC");
|
|
948
979
|
}
|
|
949
|
-
if (Array.isArray(
|
|
950
|
-
return
|
|
980
|
+
if (Array.isArray(value2)) {
|
|
981
|
+
return value2.map(normalizeValue);
|
|
951
982
|
}
|
|
952
|
-
if (typeof
|
|
953
|
-
const proto = Object.getPrototypeOf(
|
|
983
|
+
if (typeof value2 === "object") {
|
|
984
|
+
const proto = Object.getPrototypeOf(value2);
|
|
954
985
|
if (proto !== null && proto !== Object.prototype) {
|
|
955
|
-
const name =
|
|
986
|
+
const name = value2.constructor?.name ?? "unknown";
|
|
956
987
|
throw new Error(
|
|
957
988
|
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
958
989
|
);
|
|
959
990
|
}
|
|
960
|
-
const obj =
|
|
991
|
+
const obj = value2;
|
|
961
992
|
const sorted = {};
|
|
962
993
|
for (const key of Object.keys(obj).sort()) {
|
|
963
994
|
sorted[key] = normalizeValue(obj[key]);
|
|
@@ -965,11 +996,11 @@ function normalizeValue(value) {
|
|
|
965
996
|
return sorted;
|
|
966
997
|
}
|
|
967
998
|
throw new Error(
|
|
968
|
-
`canonicalJson: unsupported value type: ${typeof
|
|
999
|
+
`canonicalJson: unsupported value type: ${typeof value2}`
|
|
969
1000
|
);
|
|
970
1001
|
}
|
|
971
|
-
function canonicalJson(
|
|
972
|
-
return JSON.stringify(normalizeValue(
|
|
1002
|
+
function canonicalJson(value2) {
|
|
1003
|
+
return JSON.stringify(normalizeValue(value2));
|
|
973
1004
|
}
|
|
974
1005
|
var init_canonical_json = __esm({
|
|
975
1006
|
"src/util/canonical-json.ts"() {
|
|
@@ -1007,8 +1038,8 @@ function interpolate(template, ctx) {
|
|
|
1007
1038
|
}
|
|
1008
1039
|
if (key.startsWith("vars.")) {
|
|
1009
1040
|
const name = key.slice("vars.".length);
|
|
1010
|
-
const
|
|
1011
|
-
return
|
|
1041
|
+
const value2 = ctx.vars[name];
|
|
1042
|
+
return value2 === void 0 ? match : String(value2);
|
|
1012
1043
|
}
|
|
1013
1044
|
return match;
|
|
1014
1045
|
});
|
|
@@ -1061,12 +1092,12 @@ var init_contains = __esm({
|
|
|
1061
1092
|
init_interpolate();
|
|
1062
1093
|
contains = {
|
|
1063
1094
|
run(ctx) {
|
|
1064
|
-
const
|
|
1095
|
+
const value2 = interpolate(ctx.params.value, ctx);
|
|
1065
1096
|
const found = norm(ctx.output, ctx.params.ignore_case).includes(
|
|
1066
|
-
norm(
|
|
1097
|
+
norm(value2, ctx.params.ignore_case)
|
|
1067
1098
|
);
|
|
1068
1099
|
return Promise.resolve(
|
|
1069
|
-
found ? { passed: true } : { passed: false, reason: `expected output to contain: "${
|
|
1100
|
+
found ? { passed: true } : { passed: false, reason: `expected output to contain: "${value2}"` }
|
|
1070
1101
|
);
|
|
1071
1102
|
}
|
|
1072
1103
|
};
|
|
@@ -2115,6 +2146,549 @@ var init_reporters = __esm({
|
|
|
2115
2146
|
}
|
|
2116
2147
|
});
|
|
2117
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
|
+
|
|
2118
2692
|
// src/cli/commands/report.ts
|
|
2119
2693
|
var report_exports = {};
|
|
2120
2694
|
__export(report_exports, {
|
|
@@ -2128,10 +2702,10 @@ function isRunResult(v) {
|
|
|
2128
2702
|
}
|
|
2129
2703
|
function handleReport(opts = {}) {
|
|
2130
2704
|
const cwd = opts.cwd ?? process.cwd();
|
|
2131
|
-
const file =
|
|
2705
|
+
const file = path7.join(cwd, ".plune", "last-run.json");
|
|
2132
2706
|
let raw;
|
|
2133
2707
|
try {
|
|
2134
|
-
raw =
|
|
2708
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
2135
2709
|
} catch {
|
|
2136
2710
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
2137
2711
|
}
|
|
@@ -2146,13 +2720,13 @@ function handleReport(opts = {}) {
|
|
|
2146
2720
|
}
|
|
2147
2721
|
return parsed;
|
|
2148
2722
|
}
|
|
2149
|
-
var
|
|
2723
|
+
var fs8, path7, ReportNotFoundError;
|
|
2150
2724
|
var init_report = __esm({
|
|
2151
2725
|
"src/cli/commands/report.ts"() {
|
|
2152
2726
|
"use strict";
|
|
2153
2727
|
init_cjs_shims();
|
|
2154
|
-
|
|
2155
|
-
|
|
2728
|
+
fs8 = __toESM(require("fs"), 1);
|
|
2729
|
+
path7 = __toESM(require("path"), 1);
|
|
2156
2730
|
ReportNotFoundError = class extends Error {
|
|
2157
2731
|
code = "NO_SAVED_RUN";
|
|
2158
2732
|
constructor(message) {
|
|
@@ -2175,7 +2749,7 @@ function evalStatus(e) {
|
|
|
2175
2749
|
if (statuses.includes("errored")) return "errored";
|
|
2176
2750
|
return "passed";
|
|
2177
2751
|
}
|
|
2178
|
-
function
|
|
2752
|
+
function classify4(baseline, current) {
|
|
2179
2753
|
if (current === "errored") return "errored";
|
|
2180
2754
|
if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
|
|
2181
2755
|
if (current === "absent") return "removed";
|
|
@@ -2189,19 +2763,19 @@ function diffRuns(baseline, current) {
|
|
|
2189
2763
|
const evals = ids.map((id) => {
|
|
2190
2764
|
const baselineP = baseMap.get(id) ?? "absent";
|
|
2191
2765
|
const currentP = curMap.get(id) ?? "absent";
|
|
2192
|
-
return { id, status:
|
|
2766
|
+
return { id, status: classify4(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
2193
2767
|
});
|
|
2194
|
-
const
|
|
2768
|
+
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
2195
2769
|
const summary = {
|
|
2196
|
-
regressions:
|
|
2197
|
-
improvements:
|
|
2198
|
-
preExistingFails:
|
|
2199
|
-
newFails:
|
|
2200
|
-
newPasses:
|
|
2201
|
-
stablePasses:
|
|
2202
|
-
errored:
|
|
2203
|
-
removed:
|
|
2204
|
-
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
|
|
2205
2779
|
};
|
|
2206
2780
|
return { evals, summary };
|
|
2207
2781
|
}
|
|
@@ -2226,7 +2800,7 @@ function isRunResult2(v) {
|
|
|
2226
2800
|
function loadRunResult(file, label) {
|
|
2227
2801
|
let raw;
|
|
2228
2802
|
try {
|
|
2229
|
-
raw =
|
|
2803
|
+
raw = fs9.readFileSync(file, "utf8");
|
|
2230
2804
|
} catch {
|
|
2231
2805
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
2232
2806
|
}
|
|
@@ -2246,12 +2820,12 @@ function handleDiff(opts) {
|
|
|
2246
2820
|
const current = loadRunResult(opts.currentPath, "current");
|
|
2247
2821
|
return diffRuns(baseline, current);
|
|
2248
2822
|
}
|
|
2249
|
-
var
|
|
2823
|
+
var fs9, DiffInputError;
|
|
2250
2824
|
var init_diff2 = __esm({
|
|
2251
2825
|
"src/cli/commands/diff.ts"() {
|
|
2252
2826
|
"use strict";
|
|
2253
2827
|
init_cjs_shims();
|
|
2254
|
-
|
|
2828
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2255
2829
|
init_diff();
|
|
2256
2830
|
DiffInputError = class extends Error {
|
|
2257
2831
|
code = "DIFF_INPUT";
|
|
@@ -2376,8 +2950,8 @@ async function runInitWizard(cwd) {
|
|
|
2376
2950
|
process.exit(1);
|
|
2377
2951
|
return;
|
|
2378
2952
|
}
|
|
2379
|
-
const outputPath =
|
|
2380
|
-
if (
|
|
2953
|
+
const outputPath = path8.join(cwd, "plune.yaml");
|
|
2954
|
+
if (fs10.existsSync(outputPath)) {
|
|
2381
2955
|
const overwrite = await clack.confirm({
|
|
2382
2956
|
message: "plune.yaml already exists. Overwrite?"
|
|
2383
2957
|
});
|
|
@@ -2416,18 +2990,18 @@ async function runInitWizard(cwd) {
|
|
|
2416
2990
|
`;
|
|
2417
2991
|
const spin = clack.spinner();
|
|
2418
2992
|
spin.start("Writing plune.yaml\u2026");
|
|
2419
|
-
|
|
2993
|
+
fs10.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
2420
2994
|
spin.stop("plune.yaml created!");
|
|
2421
|
-
clack.outro("
|
|
2995
|
+
clack.outro("plune.yaml is ready.");
|
|
2422
2996
|
}
|
|
2423
|
-
var clack,
|
|
2997
|
+
var clack, fs10, path8, import_yaml2;
|
|
2424
2998
|
var init_wizard = __esm({
|
|
2425
2999
|
"src/config/init/wizard.ts"() {
|
|
2426
3000
|
"use strict";
|
|
2427
3001
|
init_cjs_shims();
|
|
2428
3002
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
2429
|
-
|
|
2430
|
-
|
|
3003
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3004
|
+
path8 = __toESM(require("path"), 1);
|
|
2431
3005
|
import_yaml2 = require("yaml");
|
|
2432
3006
|
init_errors();
|
|
2433
3007
|
}
|
|
@@ -2435,21 +3009,21 @@ var init_wizard = __esm({
|
|
|
2435
3009
|
|
|
2436
3010
|
// src/cli/templates/index.ts
|
|
2437
3011
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
2438
|
-
const target =
|
|
2439
|
-
if (!force &&
|
|
3012
|
+
const target = path9.join(cwd, rel);
|
|
3013
|
+
if (!force && fs11.existsSync(target)) {
|
|
2440
3014
|
return "skipped";
|
|
2441
3015
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
3016
|
+
fs11.mkdirSync(path9.dirname(target), { recursive: true });
|
|
3017
|
+
fs11.writeFileSync(target, content, "utf8");
|
|
2444
3018
|
return "written";
|
|
2445
3019
|
}
|
|
2446
|
-
var
|
|
3020
|
+
var fs11, path9, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
2447
3021
|
var init_templates = __esm({
|
|
2448
3022
|
"src/cli/templates/index.ts"() {
|
|
2449
3023
|
"use strict";
|
|
2450
3024
|
init_cjs_shims();
|
|
2451
|
-
|
|
2452
|
-
|
|
3025
|
+
fs11 = __toESM(require("fs"), 1);
|
|
3026
|
+
path9 = __toESM(require("path"), 1);
|
|
2453
3027
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
2454
3028
|
# .env is gitignored; .env.example is safe to commit.
|
|
2455
3029
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -2485,6 +3059,7 @@ evals:
|
|
|
2485
3059
|
// src/cli/commands/init.ts
|
|
2486
3060
|
var init_exports = {};
|
|
2487
3061
|
__export(init_exports, {
|
|
3062
|
+
NEXT_STEPS: () => NEXT_STEPS,
|
|
2488
3063
|
initCommand: () => initCommand
|
|
2489
3064
|
});
|
|
2490
3065
|
function reportWrite(cwd, rel, content, force) {
|
|
@@ -2504,8 +3079,9 @@ async function initCommand({ cwd, force, yes = false }) {
|
|
|
2504
3079
|
for (const [rel, content] of TEMPLATES) {
|
|
2505
3080
|
reportWrite(cwd, rel, content, force);
|
|
2506
3081
|
}
|
|
3082
|
+
process.stdout.write(NEXT_STEPS);
|
|
2507
3083
|
}
|
|
2508
|
-
var TEMPLATES;
|
|
3084
|
+
var TEMPLATES, NEXT_STEPS;
|
|
2509
3085
|
var init_init = __esm({
|
|
2510
3086
|
"src/cli/commands/init.ts"() {
|
|
2511
3087
|
"use strict";
|
|
@@ -2516,43 +3092,14 @@ var init_init = __esm({
|
|
|
2516
3092
|
["datasets/example.jsonl", EXAMPLE_JSONL_TPL],
|
|
2517
3093
|
[".env.example", ENV_EXAMPLE_TPL]
|
|
2518
3094
|
];
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
function saveToken(token) {
|
|
2528
|
-
const file = credentialsFile();
|
|
2529
|
-
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true, mode: 448 });
|
|
2530
|
-
(0, import_node_fs.writeFileSync)(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
|
|
2531
|
-
(0, import_node_fs.chmodSync)(file, 384);
|
|
2532
|
-
return file;
|
|
2533
|
-
}
|
|
2534
|
-
function loadToken() {
|
|
2535
|
-
try {
|
|
2536
|
-
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2537
|
-
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
2538
|
-
} catch {
|
|
2539
|
-
return null;
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
function clearToken() {
|
|
2543
|
-
const file = credentialsFile();
|
|
2544
|
-
if (!(0, import_node_fs.existsSync)(file)) return { removed: false, path: file };
|
|
2545
|
-
(0, import_node_fs.rmSync)(file);
|
|
2546
|
-
return { removed: true, path: file };
|
|
2547
|
-
}
|
|
2548
|
-
var import_node_fs, import_node_os, import_node_path;
|
|
2549
|
-
var init_credentials = __esm({
|
|
2550
|
-
"src/cli/credentials.ts"() {
|
|
2551
|
-
"use strict";
|
|
2552
|
-
init_cjs_shims();
|
|
2553
|
-
import_node_fs = require("fs");
|
|
2554
|
-
import_node_os = require("os");
|
|
2555
|
-
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");
|
|
2556
3103
|
}
|
|
2557
3104
|
});
|
|
2558
3105
|
|
|
@@ -2560,25 +3107,81 @@ var init_credentials = __esm({
|
|
|
2560
3107
|
var login_exports = {};
|
|
2561
3108
|
__export(login_exports, {
|
|
2562
3109
|
EmptyTokenError: () => EmptyTokenError,
|
|
2563
|
-
|
|
3110
|
+
LoginHttpError: () => LoginHttpError,
|
|
3111
|
+
LoginNetworkError: () => LoginNetworkError,
|
|
3112
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
3113
|
+
handleLogin: () => handleLogin,
|
|
3114
|
+
reportLoginFailure: () => reportLoginFailure
|
|
2564
3115
|
});
|
|
2565
|
-
function handleLogin(
|
|
2566
|
-
const token =
|
|
2567
|
-
|
|
2568
|
-
|
|
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;
|
|
2569
3147
|
}
|
|
2570
|
-
var EmptyTokenError;
|
|
3148
|
+
var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
|
|
2571
3149
|
var init_login = __esm({
|
|
2572
3150
|
"src/cli/commands/login.ts"() {
|
|
2573
3151
|
"use strict";
|
|
2574
3152
|
init_cjs_shims();
|
|
3153
|
+
init_api_url();
|
|
2575
3154
|
init_credentials();
|
|
2576
3155
|
EmptyTokenError = class extends Error {
|
|
2577
|
-
constructor() {
|
|
2578
|
-
super(
|
|
3156
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
3157
|
+
super(
|
|
3158
|
+
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
3159
|
+
);
|
|
2579
3160
|
this.name = "EmptyTokenError";
|
|
2580
3161
|
}
|
|
2581
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
|
+
};
|
|
2582
3185
|
}
|
|
2583
3186
|
});
|
|
2584
3187
|
|
|
@@ -2599,8 +3202,8 @@ var init_logout = __esm({
|
|
|
2599
3202
|
});
|
|
2600
3203
|
|
|
2601
3204
|
// src/types/run-result-schema.ts
|
|
2602
|
-
function parseRunResult(
|
|
2603
|
-
return runResultSchema.parse(
|
|
3205
|
+
function parseRunResult(value2) {
|
|
3206
|
+
return runResultSchema.parse(value2);
|
|
2604
3207
|
}
|
|
2605
3208
|
var import_zod2, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
2606
3209
|
var init_run_result_schema = __esm({
|
|
@@ -2630,7 +3233,7 @@ var init_run_result_schema = __esm({
|
|
|
2630
3233
|
message: import_zod2.z.string()
|
|
2631
3234
|
});
|
|
2632
3235
|
rowResultSchema = import_zod2.z.object({
|
|
2633
|
-
vars: import_zod2.z.record(import_zod2.z.unknown()),
|
|
3236
|
+
vars: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()),
|
|
2634
3237
|
output: import_zod2.z.string().nullable(),
|
|
2635
3238
|
cached: import_zod2.z.boolean(),
|
|
2636
3239
|
usage: usageSchema.optional(),
|
|
@@ -2671,19 +3274,15 @@ __export(sync_exports, {
|
|
|
2671
3274
|
SyncFileError: () => SyncFileError,
|
|
2672
3275
|
SyncHttpError: () => SyncHttpError,
|
|
2673
3276
|
SyncNetworkError: () => SyncNetworkError,
|
|
2674
|
-
TokenRejectedError: () =>
|
|
3277
|
+
TokenRejectedError: () => TokenRejectedError2,
|
|
2675
3278
|
handleSync: () => handleSync,
|
|
2676
3279
|
reportSyncFailure: () => reportSyncFailure
|
|
2677
3280
|
});
|
|
2678
|
-
function resolveApiUrl(explicit) {
|
|
2679
|
-
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
2680
|
-
return raw.replace(/\/+$/, "");
|
|
2681
|
-
}
|
|
2682
3281
|
function readRun(cwd, file) {
|
|
2683
|
-
const target = file !== void 0 ?
|
|
3282
|
+
const target = file !== void 0 ? path10.resolve(cwd, file) : path10.join(cwd, ".plune", "last-run.json");
|
|
2684
3283
|
let raw;
|
|
2685
3284
|
try {
|
|
2686
|
-
raw =
|
|
3285
|
+
raw = fs12.readFileSync(target, "utf8");
|
|
2687
3286
|
} catch {
|
|
2688
3287
|
throw new SyncFileError(
|
|
2689
3288
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -2729,7 +3328,7 @@ async function handleSync(deps = {}) {
|
|
|
2729
3328
|
}
|
|
2730
3329
|
if (!res.ok) {
|
|
2731
3330
|
const detail = await safeErrorDetail(res);
|
|
2732
|
-
if (res.status === 401) throw new
|
|
3331
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
2733
3332
|
throw new SyncHttpError(res.status, detail);
|
|
2734
3333
|
}
|
|
2735
3334
|
const body = await res.json().catch(() => ({}));
|
|
@@ -2737,7 +3336,7 @@ async function handleSync(deps = {}) {
|
|
|
2737
3336
|
return { id, url: `${endpoint}/${id}` };
|
|
2738
3337
|
}
|
|
2739
3338
|
function reportSyncFailure(err, write) {
|
|
2740
|
-
if (err instanceof NotLoggedInError || err instanceof
|
|
3339
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
2741
3340
|
write(err.message + "\n");
|
|
2742
3341
|
return 2;
|
|
2743
3342
|
}
|
|
@@ -2747,23 +3346,23 @@ function reportSyncFailure(err, write) {
|
|
|
2747
3346
|
}
|
|
2748
3347
|
return null;
|
|
2749
3348
|
}
|
|
2750
|
-
var
|
|
3349
|
+
var fs12, path10, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
2751
3350
|
var init_sync = __esm({
|
|
2752
3351
|
"src/cli/commands/sync.ts"() {
|
|
2753
3352
|
"use strict";
|
|
2754
3353
|
init_cjs_shims();
|
|
2755
|
-
|
|
2756
|
-
|
|
3354
|
+
fs12 = __toESM(require("fs"), 1);
|
|
3355
|
+
path10 = __toESM(require("path"), 1);
|
|
2757
3356
|
init_run_result_schema();
|
|
3357
|
+
init_api_url();
|
|
2758
3358
|
init_credentials();
|
|
2759
|
-
DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
2760
3359
|
NotLoggedInError = class extends Error {
|
|
2761
3360
|
constructor() {
|
|
2762
3361
|
super('Not logged in. Run "plune login" first to save your API token.');
|
|
2763
3362
|
this.name = "NotLoggedInError";
|
|
2764
3363
|
}
|
|
2765
3364
|
};
|
|
2766
|
-
|
|
3365
|
+
TokenRejectedError2 = class extends Error {
|
|
2767
3366
|
constructor() {
|
|
2768
3367
|
super(
|
|
2769
3368
|
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
@@ -2796,6 +3395,273 @@ var init_sync = __esm({
|
|
|
2796
3395
|
}
|
|
2797
3396
|
});
|
|
2798
3397
|
|
|
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
|
+
}
|
|
3418
|
+
async function readJson(file) {
|
|
3419
|
+
try {
|
|
3420
|
+
return JSON.parse(await fs13.readFile(file, "utf8"));
|
|
3421
|
+
} catch {
|
|
3422
|
+
return void 0;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
async function defaultReadEvidence(dir) {
|
|
3426
|
+
const raw = await readJson(path11.join(dir, "api-evidence.json"));
|
|
3427
|
+
if (!Array.isArray(raw)) return void 0;
|
|
3428
|
+
return raw.map((r) => {
|
|
3429
|
+
const rec = asRecord(r);
|
|
3430
|
+
return { name: str(rec["name"]), passed: rec["passed"] === true };
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
function casesFromDesign(report) {
|
|
3434
|
+
const list = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
3435
|
+
return list.map((raw) => {
|
|
3436
|
+
const c = asRecord(raw);
|
|
3437
|
+
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
3438
|
+
return {
|
|
3439
|
+
stableId: str(c["stableId"]),
|
|
3440
|
+
title: str(c["title"], "untitled case"),
|
|
3441
|
+
execution: c["execution"] === "manual" ? "manual" : "auto",
|
|
3442
|
+
steps: steps.length > 0 ? steps : ["(no steps recorded)"],
|
|
3443
|
+
expected: str(c["expected"], "(no expected result recorded)"),
|
|
3444
|
+
...typeof c["technique"] === "string" ? { technique: c["technique"] } : {},
|
|
3445
|
+
...typeof c["priority"] === "string" ? { priority: c["priority"] } : {}
|
|
3446
|
+
};
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
function casesFromApi(report, verdicts) {
|
|
3450
|
+
const list = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
3451
|
+
return list.map((raw) => {
|
|
3452
|
+
const c = asRecord(raw);
|
|
3453
|
+
const name = str(c["name"], "unnamed operation");
|
|
3454
|
+
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
3455
|
+
const verdict = verdicts.get(name);
|
|
3456
|
+
return {
|
|
3457
|
+
stableId: str(c["stableId"]),
|
|
3458
|
+
title: name,
|
|
3459
|
+
execution: "auto",
|
|
3460
|
+
steps: [request],
|
|
3461
|
+
expected: `HTTP ${str(c["expectedStatus"], "default")}`,
|
|
3462
|
+
...verdict !== void 0 ? { verdict } : {},
|
|
3463
|
+
...typeof c["technique"] === "string" ? { technique: c["technique"] } : {}
|
|
3464
|
+
};
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
async function readCairnRun(dir, deps = {}) {
|
|
3468
|
+
const raw = await readJson(path11.join(dir, "report.json"));
|
|
3469
|
+
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
3470
|
+
const report = asRecord(raw);
|
|
3471
|
+
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
3472
|
+
throw new CairnRunVersionError(report["schemaVersion"]);
|
|
3473
|
+
}
|
|
3474
|
+
if (report["partial"] === true) {
|
|
3475
|
+
throw new CairnRunPartialError(str(report["error"], "no reason recorded"));
|
|
3476
|
+
}
|
|
3477
|
+
const mode = report["mode"];
|
|
3478
|
+
if (mode !== "design" && mode !== "explore" && mode !== "api") {
|
|
3479
|
+
throw new CairnRunVersionError(
|
|
3480
|
+
`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
let cases;
|
|
3484
|
+
if (mode === "api") {
|
|
3485
|
+
const evidence = await (deps.readEvidence ?? defaultReadEvidence)(dir);
|
|
3486
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
3487
|
+
for (const e of evidence ?? []) {
|
|
3488
|
+
if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
|
|
3489
|
+
verdicts.set(e.name, e.passed ? "passed" : "failed");
|
|
3490
|
+
}
|
|
3491
|
+
cases = casesFromApi(report, verdicts);
|
|
3492
|
+
} else {
|
|
3493
|
+
cases = casesFromDesign(report);
|
|
3494
|
+
}
|
|
3495
|
+
return {
|
|
3496
|
+
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
3497
|
+
run: {
|
|
3498
|
+
runId: str(report["runId"], path11.basename(dir)),
|
|
3499
|
+
mode,
|
|
3500
|
+
target: str(report["url"], "unknown")
|
|
3501
|
+
},
|
|
3502
|
+
cases
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
var fs13, path11, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
3506
|
+
var init_cairn_artifact = __esm({
|
|
3507
|
+
"src/cli/commands/cairn-artifact.ts"() {
|
|
3508
|
+
"use strict";
|
|
3509
|
+
init_cjs_shims();
|
|
3510
|
+
fs13 = __toESM(require("fs/promises"), 1);
|
|
3511
|
+
path11 = __toESM(require("path"), 1);
|
|
3512
|
+
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
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
|
+
};
|
|
3520
|
+
CairnRunNotFoundError = class extends Error {
|
|
3521
|
+
constructor(dir) {
|
|
3522
|
+
super(
|
|
3523
|
+
`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`
|
|
3524
|
+
);
|
|
3525
|
+
this.name = "CairnRunNotFoundError";
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
CairnRunVersionError = class extends Error {
|
|
3529
|
+
constructor(found) {
|
|
3530
|
+
super(
|
|
3531
|
+
found === void 0 ? "This run predates versioned Cairn artifacts. Re-run it with Cairn 0.7.0 or newer, which stamps the format so it can be read safely." : `Unsupported Cairn artifact version ${String(found)} (this CLI reads version ${SUPPORTED_ARTIFACT_VERSION}). Upgrade @plune-ai/cli.`
|
|
3532
|
+
);
|
|
3533
|
+
this.name = "CairnRunVersionError";
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
CairnRunPartialError = class extends Error {
|
|
3537
|
+
constructor(reason) {
|
|
3538
|
+
super(`That Cairn run did not finish, so its cases are incomplete: ${reason}`);
|
|
3539
|
+
this.name = "CairnRunPartialError";
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
CairnEvidenceAmbiguousError = class extends Error {
|
|
3543
|
+
constructor(name) {
|
|
3544
|
+
super(
|
|
3545
|
+
`Cannot attach results safely: the operation "${name}" appears more than once in this run's evidence, and the evidence file carries no case identity to disambiguate it. Nothing was uploaded.`
|
|
3546
|
+
);
|
|
3547
|
+
this.name = "CairnEvidenceAmbiguousError";
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
asRecord = (v) => typeof v === "object" && v !== null ? v : {};
|
|
3551
|
+
str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
|
|
3552
|
+
}
|
|
3553
|
+
});
|
|
3554
|
+
|
|
3555
|
+
// src/cli/commands/ingest.ts
|
|
3556
|
+
var ingest_exports = {};
|
|
3557
|
+
__export(ingest_exports, {
|
|
3558
|
+
IngestHttpError: () => IngestHttpError,
|
|
3559
|
+
IngestNetworkError: () => IngestNetworkError,
|
|
3560
|
+
IngestNotLoggedInError: () => IngestNotLoggedInError,
|
|
3561
|
+
IngestTokenRejectedError: () => IngestTokenRejectedError,
|
|
3562
|
+
formatIngestResult: () => formatIngestResult,
|
|
3563
|
+
handleIngest: () => handleIngest,
|
|
3564
|
+
reportIngestFailure: () => reportIngestFailure
|
|
3565
|
+
});
|
|
3566
|
+
async function safeErrorDetail2(res) {
|
|
3567
|
+
try {
|
|
3568
|
+
const body = await res.json();
|
|
3569
|
+
return typeof body.error === "string" ? body.error : "";
|
|
3570
|
+
} catch {
|
|
3571
|
+
return "";
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
async function handleIngest(deps) {
|
|
3575
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
3576
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
3577
|
+
if (token === null) throw new IngestNotLoggedInError();
|
|
3578
|
+
const dir = deps.dir ?? await findLatestCairnRun(deps.runsDir);
|
|
3579
|
+
const payload = await readCairnRun(dir);
|
|
3580
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
3581
|
+
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
3582
|
+
let res;
|
|
3583
|
+
try {
|
|
3584
|
+
res = await doFetch(endpoint, {
|
|
3585
|
+
method: "POST",
|
|
3586
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3587
|
+
body: JSON.stringify(payload)
|
|
3588
|
+
});
|
|
3589
|
+
} catch {
|
|
3590
|
+
throw new IngestNetworkError(apiUrl);
|
|
3591
|
+
}
|
|
3592
|
+
if (!res.ok) {
|
|
3593
|
+
const detail = await safeErrorDetail2(res);
|
|
3594
|
+
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
3595
|
+
throw new IngestHttpError(res.status, detail);
|
|
3596
|
+
}
|
|
3597
|
+
const body = await res.json().catch(() => ({}));
|
|
3598
|
+
return {
|
|
3599
|
+
runId: typeof body.runId === "string" ? body.runId : "",
|
|
3600
|
+
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
3601
|
+
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
3602
|
+
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
function reportIngestFailure(err, write) {
|
|
3606
|
+
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
3607
|
+
write(`${err.message}
|
|
3608
|
+
`);
|
|
3609
|
+
return 2;
|
|
3610
|
+
}
|
|
3611
|
+
if (err instanceof IngestNetworkError || err instanceof IngestHttpError) {
|
|
3612
|
+
write(`${err.message}
|
|
3613
|
+
`);
|
|
3614
|
+
return 1;
|
|
3615
|
+
}
|
|
3616
|
+
return null;
|
|
3617
|
+
}
|
|
3618
|
+
function formatIngestResult(r) {
|
|
3619
|
+
const lines = [
|
|
3620
|
+
`Ingested Cairn run \u2192 ${r.runId}`,
|
|
3621
|
+
` ${r.linked} result(s) attached to existing cases`,
|
|
3622
|
+
` ${r.proposed} case(s) proposed \u2014 review them in Plune to accept`,
|
|
3623
|
+
` ${r.skipped} skipped (already queued, already rejected, repeated, or never executed)`
|
|
3624
|
+
];
|
|
3625
|
+
return `${lines.join("\n")}
|
|
3626
|
+
`;
|
|
3627
|
+
}
|
|
3628
|
+
var IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
3629
|
+
var init_ingest = __esm({
|
|
3630
|
+
"src/cli/commands/ingest.ts"() {
|
|
3631
|
+
"use strict";
|
|
3632
|
+
init_cjs_shims();
|
|
3633
|
+
init_api_url();
|
|
3634
|
+
init_credentials();
|
|
3635
|
+
init_cairn_artifact();
|
|
3636
|
+
IngestNotLoggedInError = class extends Error {
|
|
3637
|
+
constructor() {
|
|
3638
|
+
super('Not logged in. Run "plune login" first to save your API token.');
|
|
3639
|
+
this.name = "IngestNotLoggedInError";
|
|
3640
|
+
}
|
|
3641
|
+
};
|
|
3642
|
+
IngestTokenRejectedError = class extends Error {
|
|
3643
|
+
constructor() {
|
|
3644
|
+
super('Your API token was rejected (401). Run "plune login" again with a fresh token.');
|
|
3645
|
+
this.name = "IngestTokenRejectedError";
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
IngestNetworkError = class extends Error {
|
|
3649
|
+
constructor(apiUrl) {
|
|
3650
|
+
super(`Could not reach the Plune API at ${apiUrl}. Check your connection, then try again.`);
|
|
3651
|
+
this.name = "IngestNetworkError";
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
IngestHttpError = class extends Error {
|
|
3655
|
+
constructor(status, detail) {
|
|
3656
|
+
super(`Plune API returned ${status}${detail !== "" ? ` \u2014 ${detail}` : ""}.`);
|
|
3657
|
+
this.status = status;
|
|
3658
|
+
this.name = "IngestHttpError";
|
|
3659
|
+
}
|
|
3660
|
+
status;
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
});
|
|
3664
|
+
|
|
2799
3665
|
// src/cli.ts
|
|
2800
3666
|
var cli_exports = {};
|
|
2801
3667
|
__export(cli_exports, {
|
|
@@ -2803,6 +3669,7 @@ __export(cli_exports, {
|
|
|
2803
3669
|
});
|
|
2804
3670
|
module.exports = __toCommonJS(cli_exports);
|
|
2805
3671
|
init_cjs_shims();
|
|
3672
|
+
init_api_url();
|
|
2806
3673
|
var import_commander = require("commander");
|
|
2807
3674
|
var import_node_fs2 = require("fs");
|
|
2808
3675
|
var import_node_path2 = require("path");
|
|
@@ -2833,10 +3700,17 @@ function failUnexpected(err, verbose) {
|
|
|
2833
3700
|
} else {
|
|
2834
3701
|
process.stderr.write(String(err) + "\n");
|
|
2835
3702
|
}
|
|
3703
|
+
process.stderr.write(
|
|
3704
|
+
`This one is on us. Run again with --verbose and send the output: ${FEEDBACK_URL}
|
|
3705
|
+
`
|
|
3706
|
+
);
|
|
2836
3707
|
process.exit(1);
|
|
2837
3708
|
}
|
|
2838
3709
|
async function readTokenFromStdin() {
|
|
2839
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
|
+
`);
|
|
2840
3714
|
process.stderr.write("Paste your API token, then press Enter:\n");
|
|
2841
3715
|
}
|
|
2842
3716
|
const { createInterface } = await import("readline");
|
|
@@ -2852,7 +3726,7 @@ function createProgram() {
|
|
|
2852
3726
|
const program = new import_commander.Command();
|
|
2853
3727
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
2854
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");
|
|
2855
|
-
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(
|
|
2856
3730
|
"--only <selector>",
|
|
2857
3731
|
"Run a subset by eval id or tag (repeatable)",
|
|
2858
3732
|
(val, prev) => [...prev, val],
|
|
@@ -2928,6 +3802,58 @@ function createProgram() {
|
|
|
2928
3802
|
}
|
|
2929
3803
|
}
|
|
2930
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
|
+
});
|
|
2931
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) => {
|
|
2932
3858
|
const globals = command.optsWithGlobals();
|
|
2933
3859
|
const [{ loadEnv: loadEnv2 }, { handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
@@ -2991,18 +3917,26 @@ function createProgram() {
|
|
|
2991
3917
|
failUnexpected(err, verbose);
|
|
2992
3918
|
}
|
|
2993
3919
|
});
|
|
2994
|
-
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) => {
|
|
2995
3925
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
2996
|
-
const { handleLogin: handleLogin2,
|
|
3926
|
+
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
2997
3927
|
try {
|
|
2998
3928
|
const token = options.token ?? await readTokenFromStdin();
|
|
2999
|
-
const { path:
|
|
3000
|
-
process.stdout.write(
|
|
3001
|
-
`
|
|
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
|
+
);
|
|
3002
3935
|
} catch (err) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3936
|
+
const code = reportLoginFailure2(err, (s) => process.stderr.write(s));
|
|
3937
|
+
if (code !== null) {
|
|
3938
|
+
maybeStack(err, verbose);
|
|
3939
|
+
process.exitCode = code;
|
|
3006
3940
|
return;
|
|
3007
3941
|
}
|
|
3008
3942
|
failUnexpected(err, verbose);
|
|
@@ -3012,9 +3946,9 @@ function createProgram() {
|
|
|
3012
3946
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
3013
3947
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
3014
3948
|
try {
|
|
3015
|
-
const { removed, path:
|
|
3949
|
+
const { removed, path: path12 } = handleLogout2();
|
|
3016
3950
|
process.stdout.write(
|
|
3017
|
-
removed ? `Logged out. Removed ${
|
|
3951
|
+
removed ? `Logged out. Removed ${path12}
|
|
3018
3952
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
3019
3953
|
);
|
|
3020
3954
|
} catch (err) {
|
|
@@ -3043,6 +3977,24 @@ function createProgram() {
|
|
|
3043
3977
|
failUnexpected(err, verbose);
|
|
3044
3978
|
}
|
|
3045
3979
|
});
|
|
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) => {
|
|
3981
|
+
const globals = command.optsWithGlobals();
|
|
3982
|
+
const verbose = globals.verbose === true;
|
|
3983
|
+
const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
3984
|
+
try {
|
|
3985
|
+
process.stdout.write(
|
|
3986
|
+
formatIngestResult2(await handleIngest2({ ...dir !== void 0 ? { dir } : {} }))
|
|
3987
|
+
);
|
|
3988
|
+
} catch (err) {
|
|
3989
|
+
const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
|
|
3990
|
+
if (code !== null) {
|
|
3991
|
+
maybeStack(err, verbose);
|
|
3992
|
+
process.exitCode = code;
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
failUnexpected(err, verbose);
|
|
3996
|
+
}
|
|
3997
|
+
});
|
|
3046
3998
|
program.on("command:*", (operands) => {
|
|
3047
3999
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
3048
4000
|
`);
|