@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 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((resolve5) => setTimeout(resolve5, ms));
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 import_node_fs = require("fs");
2531
- var import_node_path = require("path");
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, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
2534
- var pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, "..", "package.json"), "utf-8"));
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, import_node_fs.writeFileSync)(outputPath, text2);
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, import_node_path.dirname)((0, import_node_path.resolve)(configPath)) : process.cwd());
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, import_node_path.dirname)((0, import_node_path.resolve)(globals.config)) : process.cwd();
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, import_node_fs.realpathSync)(entry) === (0, import_node_fs.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
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
- run: () => handleRun
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((resolve4) => setTimeout(resolve4, ms));
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
- run
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
  });