@plune-ai/cli 0.3.0 → 0.5.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 +19 -0
- package/dist/cli.cjs +351 -9
- package/dist/index.cjs +220 -3
- package/dist/index.d.cts +588 -1
- package/dist/index.d.ts +588 -1
- package/dist/index.js +206 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,11 +100,30 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
100
100
|
| `plune report` | Re-render the most recent run. Flags: `--format`, `-o`. |
|
|
101
101
|
| `plune diff <baseline> <current>` | Compare two `plune run --format json` outputs and report pass→fail regressions. Flags: `--fail-on-regression`, `--format`, `-o`. |
|
|
102
102
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
103
|
+
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` can reach it. Flags: `--token <token>` (omit to paste it or pipe it via stdin). |
|
|
104
|
+
| `plune logout` | Remove the saved token. |
|
|
105
|
+
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON. |
|
|
103
106
|
|
|
104
107
|
Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
105
108
|
|
|
106
109
|
**Exit codes:** `0` everything passed · `1` an assertion failed · `2` configuration or execution error.
|
|
107
110
|
|
|
111
|
+
## Optional: keep a history
|
|
112
|
+
|
|
113
|
+
Everything above works with no account, no network, and no token — that does not change. If you
|
|
114
|
+
also want run history, trends, and a shared dashboard, the three cloud commands push your local
|
|
115
|
+
runs to the [Plune platform](https://plune.ai/platform):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
plune login # paste the API token from your platform settings page
|
|
119
|
+
plune run # exactly as before — the run is saved locally
|
|
120
|
+
plune sync # upload .plune/last-run.json, print the read-back URL
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The token is stored at `~/.config/plune/credentials.json` (mode `0600`, honours `XDG_CONFIG_HOME`)
|
|
124
|
+
and is never printed or logged. `PLUNE_API_URL` points `sync` at a different server — useful for a
|
|
125
|
+
self-hosted backend.
|
|
126
|
+
|
|
108
127
|
## Programmatic API
|
|
109
128
|
|
|
110
129
|
The same engine that powers `plune run` is exported for use from your own code. Unlike the
|
package/dist/cli.cjs
CHANGED
|
@@ -483,7 +483,7 @@ var init_retry = __esm({
|
|
|
483
483
|
"use strict";
|
|
484
484
|
init_cjs_shims();
|
|
485
485
|
init_errors2();
|
|
486
|
-
defaultSleep = (ms) => new Promise((
|
|
486
|
+
defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
487
487
|
}
|
|
488
488
|
});
|
|
489
489
|
|
|
@@ -2519,6 +2519,283 @@ var init_init = __esm({
|
|
|
2519
2519
|
}
|
|
2520
2520
|
});
|
|
2521
2521
|
|
|
2522
|
+
// src/cli/credentials.ts
|
|
2523
|
+
function credentialsFile() {
|
|
2524
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
2525
|
+
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
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");
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
// src/cli/commands/login.ts
|
|
2560
|
+
var login_exports = {};
|
|
2561
|
+
__export(login_exports, {
|
|
2562
|
+
EmptyTokenError: () => EmptyTokenError,
|
|
2563
|
+
handleLogin: () => handleLogin
|
|
2564
|
+
});
|
|
2565
|
+
function handleLogin(opts) {
|
|
2566
|
+
const token = opts.token.trim();
|
|
2567
|
+
if (token.length === 0) throw new EmptyTokenError();
|
|
2568
|
+
return { path: saveToken(token) };
|
|
2569
|
+
}
|
|
2570
|
+
var EmptyTokenError;
|
|
2571
|
+
var init_login = __esm({
|
|
2572
|
+
"src/cli/commands/login.ts"() {
|
|
2573
|
+
"use strict";
|
|
2574
|
+
init_cjs_shims();
|
|
2575
|
+
init_credentials();
|
|
2576
|
+
EmptyTokenError = class extends Error {
|
|
2577
|
+
constructor() {
|
|
2578
|
+
super("No API token provided. Pass --token <token> or pipe it via stdin.");
|
|
2579
|
+
this.name = "EmptyTokenError";
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
// src/cli/commands/logout.ts
|
|
2586
|
+
var logout_exports = {};
|
|
2587
|
+
__export(logout_exports, {
|
|
2588
|
+
handleLogout: () => handleLogout
|
|
2589
|
+
});
|
|
2590
|
+
function handleLogout() {
|
|
2591
|
+
return clearToken();
|
|
2592
|
+
}
|
|
2593
|
+
var init_logout = __esm({
|
|
2594
|
+
"src/cli/commands/logout.ts"() {
|
|
2595
|
+
"use strict";
|
|
2596
|
+
init_cjs_shims();
|
|
2597
|
+
init_credentials();
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
// src/types/run-result-schema.ts
|
|
2602
|
+
function parseRunResult(value) {
|
|
2603
|
+
return runResultSchema.parse(value);
|
|
2604
|
+
}
|
|
2605
|
+
var import_zod2, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
2606
|
+
var init_run_result_schema = __esm({
|
|
2607
|
+
"src/types/run-result-schema.ts"() {
|
|
2608
|
+
"use strict";
|
|
2609
|
+
init_cjs_shims();
|
|
2610
|
+
import_zod2 = require("zod");
|
|
2611
|
+
RUN_RESULT_SCHEMA_VERSION = 1;
|
|
2612
|
+
binaryVerdictSchema = import_zod2.z.object({
|
|
2613
|
+
question: import_zod2.z.string().min(1),
|
|
2614
|
+
passed: import_zod2.z.boolean()
|
|
2615
|
+
});
|
|
2616
|
+
assertionResultRecordSchema = import_zod2.z.object({
|
|
2617
|
+
type: import_zod2.z.string(),
|
|
2618
|
+
passed: import_zod2.z.boolean(),
|
|
2619
|
+
score: import_zod2.z.number().optional(),
|
|
2620
|
+
reason: import_zod2.z.string().optional(),
|
|
2621
|
+
binaryVerdicts: import_zod2.z.array(binaryVerdictSchema).optional()
|
|
2622
|
+
});
|
|
2623
|
+
usageSchema = import_zod2.z.object({
|
|
2624
|
+
input_tokens: import_zod2.z.number(),
|
|
2625
|
+
output_tokens: import_zod2.z.number(),
|
|
2626
|
+
cost_usd: import_zod2.z.number()
|
|
2627
|
+
});
|
|
2628
|
+
runErrorSchema = import_zod2.z.object({
|
|
2629
|
+
code: import_zod2.z.string(),
|
|
2630
|
+
message: import_zod2.z.string()
|
|
2631
|
+
});
|
|
2632
|
+
rowResultSchema = import_zod2.z.object({
|
|
2633
|
+
vars: import_zod2.z.record(import_zod2.z.unknown()),
|
|
2634
|
+
output: import_zod2.z.string().nullable(),
|
|
2635
|
+
cached: import_zod2.z.boolean(),
|
|
2636
|
+
usage: usageSchema.optional(),
|
|
2637
|
+
latency_ms: import_zod2.z.number().optional(),
|
|
2638
|
+
error: runErrorSchema.optional(),
|
|
2639
|
+
assertions: import_zod2.z.array(assertionResultRecordSchema)
|
|
2640
|
+
});
|
|
2641
|
+
summarySchema = import_zod2.z.object({
|
|
2642
|
+
total: import_zod2.z.number(),
|
|
2643
|
+
passed: import_zod2.z.number(),
|
|
2644
|
+
failed: import_zod2.z.number(),
|
|
2645
|
+
errored: import_zod2.z.number(),
|
|
2646
|
+
cost_usd: import_zod2.z.number(),
|
|
2647
|
+
duration_ms: import_zod2.z.number()
|
|
2648
|
+
});
|
|
2649
|
+
evalResultSchema = import_zod2.z.object({
|
|
2650
|
+
id: import_zod2.z.string(),
|
|
2651
|
+
tags: import_zod2.z.array(import_zod2.z.string()),
|
|
2652
|
+
rows: import_zod2.z.array(rowResultSchema),
|
|
2653
|
+
passed: import_zod2.z.boolean()
|
|
2654
|
+
});
|
|
2655
|
+
runResultSchema = import_zod2.z.object({
|
|
2656
|
+
schemaVersion: import_zod2.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
2657
|
+
plune_version: import_zod2.z.string(),
|
|
2658
|
+
started_at: import_zod2.z.string(),
|
|
2659
|
+
finished_at: import_zod2.z.string(),
|
|
2660
|
+
config_hash: import_zod2.z.string(),
|
|
2661
|
+
summary: summarySchema,
|
|
2662
|
+
evals: import_zod2.z.array(evalResultSchema)
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
|
|
2667
|
+
// src/cli/commands/sync.ts
|
|
2668
|
+
var sync_exports = {};
|
|
2669
|
+
__export(sync_exports, {
|
|
2670
|
+
NotLoggedInError: () => NotLoggedInError,
|
|
2671
|
+
SyncFileError: () => SyncFileError,
|
|
2672
|
+
SyncHttpError: () => SyncHttpError,
|
|
2673
|
+
SyncNetworkError: () => SyncNetworkError,
|
|
2674
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
2675
|
+
handleSync: () => handleSync,
|
|
2676
|
+
reportSyncFailure: () => reportSyncFailure
|
|
2677
|
+
});
|
|
2678
|
+
function resolveApiUrl(explicit) {
|
|
2679
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
2680
|
+
return raw.replace(/\/+$/, "");
|
|
2681
|
+
}
|
|
2682
|
+
function readRun(cwd, file) {
|
|
2683
|
+
const target = file !== void 0 ? path9.resolve(cwd, file) : path9.join(cwd, ".plune", "last-run.json");
|
|
2684
|
+
let raw;
|
|
2685
|
+
try {
|
|
2686
|
+
raw = fs10.readFileSync(target, "utf8");
|
|
2687
|
+
} catch {
|
|
2688
|
+
throw new SyncFileError(
|
|
2689
|
+
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
let json;
|
|
2693
|
+
try {
|
|
2694
|
+
json = JSON.parse(raw);
|
|
2695
|
+
} catch {
|
|
2696
|
+
throw new SyncFileError(`Run file is not valid JSON: ${target}`);
|
|
2697
|
+
}
|
|
2698
|
+
try {
|
|
2699
|
+
return parseRunResult(json);
|
|
2700
|
+
} catch {
|
|
2701
|
+
throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async function safeErrorDetail(res) {
|
|
2705
|
+
try {
|
|
2706
|
+
const body = await res.json();
|
|
2707
|
+
return typeof body.error === "string" ? body.error : "";
|
|
2708
|
+
} catch {
|
|
2709
|
+
return "";
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
async function handleSync(deps = {}) {
|
|
2713
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
2714
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
2715
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
2716
|
+
if (token === null) throw new NotLoggedInError();
|
|
2717
|
+
const run = readRun(cwd, deps.file);
|
|
2718
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2719
|
+
const endpoint = `${apiUrl}/v1/runs`;
|
|
2720
|
+
let res;
|
|
2721
|
+
try {
|
|
2722
|
+
res = await doFetch(endpoint, {
|
|
2723
|
+
method: "POST",
|
|
2724
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
2725
|
+
body: JSON.stringify(run)
|
|
2726
|
+
});
|
|
2727
|
+
} catch {
|
|
2728
|
+
throw new SyncNetworkError(apiUrl);
|
|
2729
|
+
}
|
|
2730
|
+
if (!res.ok) {
|
|
2731
|
+
const detail = await safeErrorDetail(res);
|
|
2732
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
2733
|
+
throw new SyncHttpError(res.status, detail);
|
|
2734
|
+
}
|
|
2735
|
+
const body = await res.json().catch(() => ({}));
|
|
2736
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
2737
|
+
return { id, url: `${endpoint}/${id}` };
|
|
2738
|
+
}
|
|
2739
|
+
function reportSyncFailure(err, write) {
|
|
2740
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
2741
|
+
write(err.message + "\n");
|
|
2742
|
+
return 2;
|
|
2743
|
+
}
|
|
2744
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
2745
|
+
write(err.message + "\n");
|
|
2746
|
+
return 1;
|
|
2747
|
+
}
|
|
2748
|
+
return null;
|
|
2749
|
+
}
|
|
2750
|
+
var fs10, path9, DEFAULT_API_URL, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
2751
|
+
var init_sync = __esm({
|
|
2752
|
+
"src/cli/commands/sync.ts"() {
|
|
2753
|
+
"use strict";
|
|
2754
|
+
init_cjs_shims();
|
|
2755
|
+
fs10 = __toESM(require("fs"), 1);
|
|
2756
|
+
path9 = __toESM(require("path"), 1);
|
|
2757
|
+
init_run_result_schema();
|
|
2758
|
+
init_credentials();
|
|
2759
|
+
DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
2760
|
+
NotLoggedInError = class extends Error {
|
|
2761
|
+
constructor() {
|
|
2762
|
+
super('Not logged in. Run "plune login" first to save your API token.');
|
|
2763
|
+
this.name = "NotLoggedInError";
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
TokenRejectedError = class extends Error {
|
|
2767
|
+
constructor() {
|
|
2768
|
+
super(
|
|
2769
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
2770
|
+
);
|
|
2771
|
+
this.name = "TokenRejectedError";
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
SyncFileError = class extends Error {
|
|
2775
|
+
constructor(message) {
|
|
2776
|
+
super(message);
|
|
2777
|
+
this.name = "SyncFileError";
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
SyncNetworkError = class extends Error {
|
|
2781
|
+
constructor(apiUrl) {
|
|
2782
|
+
super(
|
|
2783
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
2784
|
+
);
|
|
2785
|
+
this.name = "SyncNetworkError";
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
SyncHttpError = class extends Error {
|
|
2789
|
+
constructor(status, detail) {
|
|
2790
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
2791
|
+
this.status = status;
|
|
2792
|
+
this.name = "SyncHttpError";
|
|
2793
|
+
}
|
|
2794
|
+
status;
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2522
2799
|
// src/cli.ts
|
|
2523
2800
|
var cli_exports = {};
|
|
2524
2801
|
__export(cli_exports, {
|
|
@@ -2527,11 +2804,11 @@ __export(cli_exports, {
|
|
|
2527
2804
|
module.exports = __toCommonJS(cli_exports);
|
|
2528
2805
|
init_cjs_shims();
|
|
2529
2806
|
var import_commander = require("commander");
|
|
2530
|
-
var
|
|
2531
|
-
var
|
|
2807
|
+
var import_node_fs2 = require("fs");
|
|
2808
|
+
var import_node_path2 = require("path");
|
|
2532
2809
|
var import_node_url = require("url");
|
|
2533
|
-
var __dirname = (0,
|
|
2534
|
-
var pkg = JSON.parse((0,
|
|
2810
|
+
var __dirname = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
2811
|
+
var pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
2535
2812
|
function toFormat(s) {
|
|
2536
2813
|
return s === "json" || s === "markdown" ? s : "console";
|
|
2537
2814
|
}
|
|
@@ -2540,7 +2817,7 @@ function shouldColor(outputPath) {
|
|
|
2540
2817
|
}
|
|
2541
2818
|
function writeReport(text2, outputPath) {
|
|
2542
2819
|
if (outputPath !== void 0) {
|
|
2543
|
-
(0,
|
|
2820
|
+
(0, import_node_fs2.writeFileSync)(outputPath, text2);
|
|
2544
2821
|
} else {
|
|
2545
2822
|
process.stdout.write(text2 + "\n");
|
|
2546
2823
|
}
|
|
@@ -2558,6 +2835,19 @@ function failUnexpected(err, verbose) {
|
|
|
2558
2835
|
}
|
|
2559
2836
|
process.exit(1);
|
|
2560
2837
|
}
|
|
2838
|
+
async function readTokenFromStdin() {
|
|
2839
|
+
if (process.stdin.isTTY === true) {
|
|
2840
|
+
process.stderr.write("Paste your API token, then press Enter:\n");
|
|
2841
|
+
}
|
|
2842
|
+
const { createInterface } = await import("readline");
|
|
2843
|
+
const rl = createInterface({ input: process.stdin });
|
|
2844
|
+
try {
|
|
2845
|
+
for await (const line of rl) return line;
|
|
2846
|
+
return "";
|
|
2847
|
+
} finally {
|
|
2848
|
+
rl.close();
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2561
2851
|
function createProgram() {
|
|
2562
2852
|
const program = new import_commander.Command();
|
|
2563
2853
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
@@ -2586,7 +2876,7 @@ function createProgram() {
|
|
|
2586
2876
|
Promise.resolve().then(() => (init_errors(), errors_exports)),
|
|
2587
2877
|
Promise.resolve().then(() => (init_errors2(), errors_exports2))
|
|
2588
2878
|
]);
|
|
2589
|
-
loadEnv2(configPath !== void 0 ? (0,
|
|
2879
|
+
loadEnv2(configPath !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(configPath)) : process.cwd());
|
|
2590
2880
|
try {
|
|
2591
2881
|
const result = await handleRun2({
|
|
2592
2882
|
dryRun: options.dryRun,
|
|
@@ -2645,7 +2935,7 @@ function createProgram() {
|
|
|
2645
2935
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
2646
2936
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
2647
2937
|
]);
|
|
2648
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
2938
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(globals.config)) : process.cwd();
|
|
2649
2939
|
loadEnv2(cwd);
|
|
2650
2940
|
try {
|
|
2651
2941
|
const result = handleReport2({ cwd });
|
|
@@ -2701,6 +2991,58 @@ function createProgram() {
|
|
|
2701
2991
|
failUnexpected(err, verbose);
|
|
2702
2992
|
}
|
|
2703
2993
|
});
|
|
2994
|
+
program.command("login").description("Save your Plune API token so the CLI can reach the platform").option("--token <token>", "API token (omit to paste it / pipe it via stdin)").action(async (options, command) => {
|
|
2995
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
2996
|
+
const { handleLogin: handleLogin2, EmptyTokenError: EmptyTokenError2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
2997
|
+
try {
|
|
2998
|
+
const token = options.token ?? await readTokenFromStdin();
|
|
2999
|
+
const { path: path10 } = handleLogin2({ token });
|
|
3000
|
+
process.stdout.write(`Logged in. Token saved to ${path10}
|
|
3001
|
+
`);
|
|
3002
|
+
} catch (err) {
|
|
3003
|
+
if (err instanceof EmptyTokenError2) {
|
|
3004
|
+
process.stderr.write(err.message + "\n");
|
|
3005
|
+
process.exit(2);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
failUnexpected(err, verbose);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
program.command("logout").description("Remove the saved Plune API token").action(async (_options, command) => {
|
|
3012
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
3013
|
+
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
3014
|
+
try {
|
|
3015
|
+
const { removed, path: path10 } = handleLogout2();
|
|
3016
|
+
process.stdout.write(
|
|
3017
|
+
removed ? `Logged out. Removed ${path10}
|
|
3018
|
+
` : "Not logged in \u2014 nothing to remove.\n"
|
|
3019
|
+
);
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
failUnexpected(err, verbose);
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").action(async (options, command) => {
|
|
3025
|
+
const globals = command.optsWithGlobals();
|
|
3026
|
+
const verbose = globals.verbose === true;
|
|
3027
|
+
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
3028
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(globals.config)) : process.cwd();
|
|
3029
|
+
try {
|
|
3030
|
+
const { id, url } = await handleSync2({
|
|
3031
|
+
cwd,
|
|
3032
|
+
...options.file !== void 0 ? { file: options.file } : {}
|
|
3033
|
+
});
|
|
3034
|
+
process.stdout.write(`Synced run ${id} \u2192 ${url}
|
|
3035
|
+
`);
|
|
3036
|
+
} catch (err) {
|
|
3037
|
+
const code = reportSyncFailure2(err, (s) => process.stderr.write(s));
|
|
3038
|
+
if (code !== null) {
|
|
3039
|
+
maybeStack(err, verbose);
|
|
3040
|
+
process.exitCode = code;
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
failUnexpected(err, verbose);
|
|
3044
|
+
}
|
|
3045
|
+
});
|
|
2704
3046
|
program.on("command:*", (operands) => {
|
|
2705
3047
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
2706
3048
|
`);
|
|
@@ -2712,7 +3054,7 @@ function isMainModule() {
|
|
|
2712
3054
|
const entry = process.argv[1];
|
|
2713
3055
|
if (entry === void 0) return false;
|
|
2714
3056
|
try {
|
|
2715
|
-
return (0,
|
|
3057
|
+
return (0, import_node_fs2.realpathSync)(entry) === (0, import_node_fs2.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
2716
3058
|
} catch {
|
|
2717
3059
|
return false;
|
|
2718
3060
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
|
|
33
|
+
NotLoggedInError: () => NotLoggedInError,
|
|
34
|
+
RUN_RESULT_SCHEMA_VERSION: () => RUN_RESULT_SCHEMA_VERSION,
|
|
35
|
+
SyncFileError: () => SyncFileError,
|
|
36
|
+
SyncHttpError: () => SyncHttpError,
|
|
37
|
+
SyncNetworkError: () => SyncNetworkError,
|
|
38
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
39
|
+
assertionConfigSchema: () => assertionConfigSchema,
|
|
40
|
+
assertionResultRecordSchema: () => assertionResultRecordSchema,
|
|
41
|
+
binaryVerdictSchema: () => binaryVerdictSchema,
|
|
42
|
+
parseRunResult: () => parseRunResult,
|
|
43
|
+
reportSyncFailure: () => reportSyncFailure,
|
|
44
|
+
run: () => handleRun,
|
|
45
|
+
runResultSchema: () => runResultSchema,
|
|
46
|
+
sync: () => handleSync
|
|
34
47
|
});
|
|
35
48
|
module.exports = __toCommonJS(src_exports);
|
|
36
49
|
|
|
@@ -351,7 +364,7 @@ function normalizeProviderError(err, secret, envVar) {
|
|
|
351
364
|
}
|
|
352
365
|
|
|
353
366
|
// src/providers/retry.ts
|
|
354
|
-
var defaultSleep = (ms) => new Promise((
|
|
367
|
+
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
355
368
|
function retryAfterMs(err) {
|
|
356
369
|
if (typeof err !== "object" || err === null || !("headers" in err)) return void 0;
|
|
357
370
|
const raw = readHeader(err.headers, "retry-after");
|
|
@@ -1496,7 +1509,211 @@ async function handleRun(options, depsFactory) {
|
|
|
1496
1509
|
deps.cache.close();
|
|
1497
1510
|
}
|
|
1498
1511
|
}
|
|
1512
|
+
|
|
1513
|
+
// src/cli/commands/sync.ts
|
|
1514
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
1515
|
+
var path5 = __toESM(require("path"), 1);
|
|
1516
|
+
|
|
1517
|
+
// src/types/run-result-schema.ts
|
|
1518
|
+
var import_zod2 = require("zod");
|
|
1519
|
+
var RUN_RESULT_SCHEMA_VERSION = 1;
|
|
1520
|
+
var binaryVerdictSchema = import_zod2.z.object({
|
|
1521
|
+
question: import_zod2.z.string().min(1),
|
|
1522
|
+
passed: import_zod2.z.boolean()
|
|
1523
|
+
});
|
|
1524
|
+
var assertionResultRecordSchema = import_zod2.z.object({
|
|
1525
|
+
type: import_zod2.z.string(),
|
|
1526
|
+
passed: import_zod2.z.boolean(),
|
|
1527
|
+
score: import_zod2.z.number().optional(),
|
|
1528
|
+
reason: import_zod2.z.string().optional(),
|
|
1529
|
+
binaryVerdicts: import_zod2.z.array(binaryVerdictSchema).optional()
|
|
1530
|
+
});
|
|
1531
|
+
var usageSchema = import_zod2.z.object({
|
|
1532
|
+
input_tokens: import_zod2.z.number(),
|
|
1533
|
+
output_tokens: import_zod2.z.number(),
|
|
1534
|
+
cost_usd: import_zod2.z.number()
|
|
1535
|
+
});
|
|
1536
|
+
var runErrorSchema = import_zod2.z.object({
|
|
1537
|
+
code: import_zod2.z.string(),
|
|
1538
|
+
message: import_zod2.z.string()
|
|
1539
|
+
});
|
|
1540
|
+
var rowResultSchema = import_zod2.z.object({
|
|
1541
|
+
vars: import_zod2.z.record(import_zod2.z.unknown()),
|
|
1542
|
+
output: import_zod2.z.string().nullable(),
|
|
1543
|
+
cached: import_zod2.z.boolean(),
|
|
1544
|
+
usage: usageSchema.optional(),
|
|
1545
|
+
latency_ms: import_zod2.z.number().optional(),
|
|
1546
|
+
error: runErrorSchema.optional(),
|
|
1547
|
+
assertions: import_zod2.z.array(assertionResultRecordSchema)
|
|
1548
|
+
});
|
|
1549
|
+
var summarySchema = import_zod2.z.object({
|
|
1550
|
+
total: import_zod2.z.number(),
|
|
1551
|
+
passed: import_zod2.z.number(),
|
|
1552
|
+
failed: import_zod2.z.number(),
|
|
1553
|
+
errored: import_zod2.z.number(),
|
|
1554
|
+
cost_usd: import_zod2.z.number(),
|
|
1555
|
+
duration_ms: import_zod2.z.number()
|
|
1556
|
+
});
|
|
1557
|
+
var evalResultSchema = import_zod2.z.object({
|
|
1558
|
+
id: import_zod2.z.string(),
|
|
1559
|
+
tags: import_zod2.z.array(import_zod2.z.string()),
|
|
1560
|
+
rows: import_zod2.z.array(rowResultSchema),
|
|
1561
|
+
passed: import_zod2.z.boolean()
|
|
1562
|
+
});
|
|
1563
|
+
var runResultSchema = import_zod2.z.object({
|
|
1564
|
+
schemaVersion: import_zod2.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
1565
|
+
plune_version: import_zod2.z.string(),
|
|
1566
|
+
started_at: import_zod2.z.string(),
|
|
1567
|
+
finished_at: import_zod2.z.string(),
|
|
1568
|
+
config_hash: import_zod2.z.string(),
|
|
1569
|
+
summary: summarySchema,
|
|
1570
|
+
evals: import_zod2.z.array(evalResultSchema)
|
|
1571
|
+
});
|
|
1572
|
+
function parseRunResult(value) {
|
|
1573
|
+
return runResultSchema.parse(value);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/cli/credentials.ts
|
|
1577
|
+
var import_node_fs = require("fs");
|
|
1578
|
+
var import_node_os = require("os");
|
|
1579
|
+
var import_node_path = require("path");
|
|
1580
|
+
function credentialsFile() {
|
|
1581
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
1582
|
+
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
1583
|
+
}
|
|
1584
|
+
function loadToken() {
|
|
1585
|
+
try {
|
|
1586
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
1587
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
1588
|
+
} catch {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/cli/commands/sync.ts
|
|
1594
|
+
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
1595
|
+
var NotLoggedInError = class extends Error {
|
|
1596
|
+
constructor() {
|
|
1597
|
+
super('Not logged in. Run "plune login" first to save your API token.');
|
|
1598
|
+
this.name = "NotLoggedInError";
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
var TokenRejectedError = class extends Error {
|
|
1602
|
+
constructor() {
|
|
1603
|
+
super(
|
|
1604
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1605
|
+
);
|
|
1606
|
+
this.name = "TokenRejectedError";
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
var SyncFileError = class extends Error {
|
|
1610
|
+
constructor(message) {
|
|
1611
|
+
super(message);
|
|
1612
|
+
this.name = "SyncFileError";
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
var SyncNetworkError = class extends Error {
|
|
1616
|
+
constructor(apiUrl) {
|
|
1617
|
+
super(
|
|
1618
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1619
|
+
);
|
|
1620
|
+
this.name = "SyncNetworkError";
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
var SyncHttpError = class extends Error {
|
|
1624
|
+
constructor(status, detail) {
|
|
1625
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1626
|
+
this.status = status;
|
|
1627
|
+
this.name = "SyncHttpError";
|
|
1628
|
+
}
|
|
1629
|
+
status;
|
|
1630
|
+
};
|
|
1631
|
+
function resolveApiUrl(explicit) {
|
|
1632
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
1633
|
+
return raw.replace(/\/+$/, "");
|
|
1634
|
+
}
|
|
1635
|
+
function readRun(cwd, file) {
|
|
1636
|
+
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1637
|
+
let raw;
|
|
1638
|
+
try {
|
|
1639
|
+
raw = fs6.readFileSync(target, "utf8");
|
|
1640
|
+
} catch {
|
|
1641
|
+
throw new SyncFileError(
|
|
1642
|
+
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
let json;
|
|
1646
|
+
try {
|
|
1647
|
+
json = JSON.parse(raw);
|
|
1648
|
+
} catch {
|
|
1649
|
+
throw new SyncFileError(`Run file is not valid JSON: ${target}`);
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
return parseRunResult(json);
|
|
1653
|
+
} catch {
|
|
1654
|
+
throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async function safeErrorDetail(res) {
|
|
1658
|
+
try {
|
|
1659
|
+
const body = await res.json();
|
|
1660
|
+
return typeof body.error === "string" ? body.error : "";
|
|
1661
|
+
} catch {
|
|
1662
|
+
return "";
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async function handleSync(deps = {}) {
|
|
1666
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1667
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
1668
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
1669
|
+
if (token === null) throw new NotLoggedInError();
|
|
1670
|
+
const run = readRun(cwd, deps.file);
|
|
1671
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
1672
|
+
const endpoint = `${apiUrl}/v1/runs`;
|
|
1673
|
+
let res;
|
|
1674
|
+
try {
|
|
1675
|
+
res = await doFetch(endpoint, {
|
|
1676
|
+
method: "POST",
|
|
1677
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
1678
|
+
body: JSON.stringify(run)
|
|
1679
|
+
});
|
|
1680
|
+
} catch {
|
|
1681
|
+
throw new SyncNetworkError(apiUrl);
|
|
1682
|
+
}
|
|
1683
|
+
if (!res.ok) {
|
|
1684
|
+
const detail = await safeErrorDetail(res);
|
|
1685
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
1686
|
+
throw new SyncHttpError(res.status, detail);
|
|
1687
|
+
}
|
|
1688
|
+
const body = await res.json().catch(() => ({}));
|
|
1689
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1690
|
+
return { id, url: `${endpoint}/${id}` };
|
|
1691
|
+
}
|
|
1692
|
+
function reportSyncFailure(err, write) {
|
|
1693
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
1694
|
+
write(err.message + "\n");
|
|
1695
|
+
return 2;
|
|
1696
|
+
}
|
|
1697
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
1698
|
+
write(err.message + "\n");
|
|
1699
|
+
return 1;
|
|
1700
|
+
}
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1499
1703
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1500
1704
|
0 && (module.exports = {
|
|
1501
|
-
|
|
1705
|
+
NotLoggedInError,
|
|
1706
|
+
RUN_RESULT_SCHEMA_VERSION,
|
|
1707
|
+
SyncFileError,
|
|
1708
|
+
SyncHttpError,
|
|
1709
|
+
SyncNetworkError,
|
|
1710
|
+
TokenRejectedError,
|
|
1711
|
+
assertionConfigSchema,
|
|
1712
|
+
assertionResultRecordSchema,
|
|
1713
|
+
binaryVerdictSchema,
|
|
1714
|
+
parseRunResult,
|
|
1715
|
+
reportSyncFailure,
|
|
1716
|
+
run,
|
|
1717
|
+
runResultSchema,
|
|
1718
|
+
sync
|
|
1502
1719
|
});
|