@leostera/evalkit 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +8 -7
  3. package/dist/cli.mjs +525 -72
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/dashboard/assets/index-D9NtA46z.css +2 -0
  6. package/dist/dashboard/assets/{index-CVd_Np_w.js → index-DrBwLKJD.js} +1 -1
  7. package/dist/dashboard/index.html +2 -2
  8. package/dist/files-C-U0-PWW.mjs +26 -0
  9. package/dist/files-C-U0-PWW.mjs.map +1 -0
  10. package/dist/index-db-HdZXelOS.d.mts +142 -0
  11. package/dist/index-db-HdZXelOS.d.mts.map +1 -0
  12. package/dist/index-db-vbqEGFaU.mjs +548 -0
  13. package/dist/index-db-vbqEGFaU.mjs.map +1 -0
  14. package/dist/{index-M5eRlMfd.d.mts → index-lOw0fmak.d.mts} +96 -9
  15. package/dist/index-lOw0fmak.d.mts.map +1 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/index.mjs +2 -2
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/runner.d.mts +21 -3
  20. package/dist/runner.d.mts.map +1 -1
  21. package/dist/runner.mjs +1 -1
  22. package/dist/share-worker.d.mts +51 -0
  23. package/dist/share-worker.d.mts.map +1 -0
  24. package/dist/share-worker.mjs +73 -0
  25. package/dist/share-worker.mjs.map +1 -0
  26. package/dist/share.d.mts +116 -0
  27. package/dist/share.d.mts.map +1 -0
  28. package/dist/share.mjs +4 -0
  29. package/dist/{src-DrwwgaPn.mjs → src-ButXPY8P.mjs} +20 -5
  30. package/dist/src-ButXPY8P.mjs.map +1 -0
  31. package/dist/{src-DluldGKn.mjs → src-DoPs9Yug.mjs} +11 -7
  32. package/dist/src-DoPs9Yug.mjs.map +1 -0
  33. package/dist/src-DzZueFn3.mjs +98 -0
  34. package/dist/src-DzZueFn3.mjs.map +1 -0
  35. package/package.json +10 -1
  36. package/dist/dashboard/assets/index-CMr4aulR.css +0 -2
  37. package/dist/index-M5eRlMfd.d.mts.map +0 -1
  38. package/dist/src-DluldGKn.mjs.map +0 -1
  39. package/dist/src-DrwwgaPn.mjs.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bun
2
- import { F as defineEvalMatrix, I as canonicalParameters, m as registerEvals, n as authoringId } from "./src-DrwwgaPn.mjs";
3
- import { a as readTrialEvents, c as readTrialSummary, i as readRunSummary, l as localReportStore, n as runMatrix, o as readTrialManifest, r as readRunManifest, t as runEval } from "./src-DluldGKn.mjs";
2
+ import { I as defineEvalMatrix, L as canonicalParameters, m as registerEvals, n as authoringId } from "./src-ButXPY8P.mjs";
3
+ import { a as readTrialEvents, c as readTrialSummary, i as readRunSummary, l as localReportStore, n as runMatrix, o as readTrialManifest, r as readRunManifest, t as runEval } from "./src-DoPs9Yug.mjs";
4
+ import { c as dashboardIndex, d as encodeBundle, o as repoKey } from "./index-db-vbqEGFaU.mjs";
5
+ import { t as createDirectoryShareApp } from "./src-DzZueFn3.mjs";
4
6
  import * as Schema from "effect/Schema";
5
7
  import { Effect, LogLevel, Logger, Option } from "effect";
6
- import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
+ import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
7
9
  import * as Path from "node:path";
8
- import { basename, dirname, extname, relative, resolve, sep } from "node:path";
10
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
9
11
  import * as OS from "node:os";
10
12
  import * as Crypto from "node:crypto";
11
13
  import * as NFS from "node:fs";
@@ -65,8 +67,9 @@ import * as FiberSet from "effect/FiberSet";
65
67
  import "effect/Fiber";
66
68
  import "effect/Pool";
67
69
  import * as MutableRef from "effect/MutableRef";
68
- import { parseArgs } from "node:util";
70
+ import { parseArgs, promisify } from "node:util";
69
71
  import * as ChildProcess from "node:child_process";
72
+ import { execFile } from "node:child_process";
70
73
  import * as RcRef from "effect/RcRef";
71
74
  import * as readline from "node:readline";
72
75
  import { Hono } from "hono";
@@ -3463,7 +3466,7 @@ function handleProcessInteger(options) {
3463
3466
  };
3464
3467
  }
3465
3468
  /** @internal */
3466
- const integer = (options) => {
3469
+ const integer$3 = (options) => {
3467
3470
  const opts = {
3468
3471
  min: Number.NEGATIVE_INFINITY,
3469
3472
  max: Number.POSITIVE_INFINITY,
@@ -4029,6 +4032,12 @@ const choice = (alternatives) => {
4029
4032
  return op;
4030
4033
  };
4031
4034
  /** @internal */
4035
+ const integer$2 = /*#__PURE__*/ (() => {
4036
+ const op = /*#__PURE__*/ Object.create(proto$5);
4037
+ op._tag = "Integer";
4038
+ return op;
4039
+ })();
4040
+ /** @internal */
4032
4041
  const text$4 = /*#__PURE__*/ (() => {
4033
4042
  const op = /*#__PURE__*/ Object.create(proto$5);
4034
4043
  op._tag = "Text";
@@ -4184,7 +4193,7 @@ const wizardInternal$3 = (self, help) => {
4184
4193
  case "Integer": {
4185
4194
  const primitiveHelp = p("Enter an integer");
4186
4195
  const message = sequence(help, primitiveHelp);
4187
- return integer({ message: toAnsiText(message).trimEnd() }).pipe(map$3((value) => `${value}`));
4196
+ return integer$3({ message: toAnsiText(message).trimEnd() }).pipe(map$3((value) => `${value}`));
4188
4197
  }
4189
4198
  case "Path": {
4190
4199
  const primitiveHelp = p("Select a file system path");
@@ -4689,7 +4698,7 @@ const wizardInternal$2 = (self, config) => {
4689
4698
  case "Variadic": {
4690
4699
  const repeatHelp = p("How many times should this argument should be repeated?");
4691
4700
  const message = pipe(getHelpInternal$2(self), sequence(repeatHelp));
4692
- return integer({
4701
+ return integer$3({
4693
4702
  message: toAnsiText(message).trimEnd(),
4694
4703
  min: getMinSizeInternal$1(self),
4695
4704
  max: getMaxSizeInternal$1(self)
@@ -4874,13 +4883,15 @@ const boolean$1 = (name, options) => {
4874
4883
  const head = Arr.headNonEmpty(negationNames);
4875
4884
  const tail = Arr.tailNonEmpty(negationNames);
4876
4885
  const negationOption = makeSingle(head, tail, boolean$2(Option$1.some(!ifPresent)));
4877
- return withDefault(orElse(option, negationOption), !ifPresent);
4886
+ return withDefault$1(orElse(option, negationOption), !ifPresent);
4878
4887
  }
4879
- return withDefault(option, !ifPresent);
4888
+ return withDefault$1(option, !ifPresent);
4880
4889
  };
4881
4890
  /** @internal */
4882
4891
  const choiceWithValue = (name, choices) => makeSingle(name, Arr.empty(), choice(choices));
4883
4892
  /** @internal */
4893
+ const integer$1 = (name) => makeSingle(name, Arr.empty(), integer$2);
4894
+ /** @internal */
4884
4895
  const none = /*#__PURE__*/ (() => {
4885
4896
  const op = /*#__PURE__*/ Object.create(proto$2);
4886
4897
  op._tag = "Empty";
@@ -4895,7 +4906,7 @@ const getUsage$1 = (self) => getUsageInternal$1(self);
4895
4906
  /** @internal */
4896
4907
  const map$1 = /*#__PURE__*/ dual(2, (self, f) => makeMap(self, (a) => Either$1.right(f(a))));
4897
4908
  /** @internal */
4898
- const optional$1 = (self) => withDefault(map$1(self, Option$1.some), Option$1.none());
4909
+ const optional$1 = (self) => withDefault$1(map$1(self, Option$1.some), Option$1.none());
4899
4910
  /** @internal */
4900
4911
  const orElse = /*#__PURE__*/ dual(2, (self, that) => orElseEither(self, that).pipe(map$1(Either$1.merge)));
4901
4912
  /** @internal */
@@ -4917,7 +4928,7 @@ const withAlias = /*#__PURE__*/ dual(2, (self, alias) => modifySingle(self, (sin
4917
4928
  return makeSingle(single.name, aliases, single.primitiveType, single.description, single.pseudoName);
4918
4929
  }));
4919
4930
  /** @internal */
4920
- const withDefault = /*#__PURE__*/ dual(2, (self, fallback) => makeWithDefault(self, fallback));
4931
+ const withDefault$1 = /*#__PURE__*/ dual(2, (self, fallback) => makeWithDefault(self, fallback));
4921
4932
  /** @internal */
4922
4933
  const withDescription = /*#__PURE__*/ dual(2, (self, desc) => modifySingle(self, (single) => {
4923
4934
  const description = sequence(single.description, p(desc));
@@ -5305,7 +5316,7 @@ const wizardInternal$1 = (self, config) => {
5305
5316
  case "Variadic": {
5306
5317
  const repeatHelp = p("How many times should this argument be repeated?");
5307
5318
  const message = pipe(getHelpInternal$1(self), sequence(repeatHelp));
5308
- return integer({
5319
+ return integer$3({
5309
5320
  message: toAnsiText(message).trimEnd(),
5310
5321
  min: getMinSizeInternal(self),
5311
5322
  max: getMaxSizeInternal(self)
@@ -5957,6 +5968,11 @@ const boolean = boolean$1;
5957
5968
  * @since 1.0.0
5958
5969
  * @category constructors
5959
5970
  */
5971
+ const integer = integer$1;
5972
+ /**
5973
+ * @since 1.0.0
5974
+ * @category constructors
5975
+ */
5960
5976
  const text = text$1;
5961
5977
  /**
5962
5978
  * @since 1.0.0
@@ -5974,6 +5990,11 @@ const optional = optional$1;
5974
5990
  * @category combinators
5975
5991
  */
5976
5992
  const repeated = repeated$1;
5993
+ /**
5994
+ * @since 1.0.0
5995
+ * @category combinators
5996
+ */
5997
+ const withDefault = withDefault$1;
5977
5998
  //#endregion
5978
5999
  //#region ../../node_modules/.bun/@effect+cli@0.77.2+74e017304edf4bf0/node_modules/@effect/cli/dist/esm/internal/commandDirective.js
5979
6000
  /** @internal */
@@ -8126,6 +8147,7 @@ function createDashboardReportReader(root, registry) {
8126
8147
  return {
8127
8148
  id,
8128
8149
  ...await ifPresent(() => stat(resolve(root, id, "archived.json"))) ? { archived: true } : {},
8150
+ ...manifest.source ? { source: manifest.source } : {},
8129
8151
  evalId: manifest.evalId,
8130
8152
  ...manifest.suiteId ? { suiteId: manifest.suiteId } : {},
8131
8153
  ...manifest.matrix ? {
@@ -8244,7 +8266,7 @@ async function buildDashboard(options, step = quietStep) {
8244
8266
  await prepareOutDir(outDir);
8245
8267
  await cp(dashboardRoot, outDir, { recursive: true });
8246
8268
  await writeFile(resolve(outDir, MARKER), "Generated by `evalkit build`.\n");
8247
- await writeFile(resolve(outDir, "index.html"), staticIndex(template));
8269
+ await writeFile(resolve(outDir, "index.html"), dashboardIndex(template, { base: "./" }));
8248
8270
  }, () => "read-only UI with relative assets");
8249
8271
  await step("Eval catalog", async () => {
8250
8272
  await write("catalog.json", { evals: registry.catalog() });
@@ -8265,13 +8287,10 @@ async function buildDashboard(options, step = quietStep) {
8265
8287
  const trialIds = [];
8266
8288
  for (const [index, run] of runs.entries()) {
8267
8289
  progress(`run ${index + 1}/${runs.length} · ${run.id.slice(0, 8)}`);
8268
- const runTrials = await reports.listTrials(run.id);
8269
- await write(`runs/${run.id}/trials.json`, { trials: runTrials });
8270
- for (const trial of runTrials) {
8271
- trials += 1;
8272
- trialIds.push(trial.id);
8273
- files += await snapshotTrial(reports, write, data, run.id, trial.id);
8274
- }
8290
+ const written = await snapshotRun(reports, run.id, directorySink(resolve(data, "runs", run.id)));
8291
+ trials += written.trialIds.length;
8292
+ files += written.files;
8293
+ trialIds.push(...written.trialIds);
8275
8294
  }
8276
8295
  return {
8277
8296
  runs: runs.length,
@@ -8293,9 +8312,9 @@ async function buildDashboard(options, step = quietStep) {
8293
8312
  for (const route of routes) {
8294
8313
  const file = resolve(outDir, `${route}.html`);
8295
8314
  await mkdir(dirname(file), { recursive: true });
8296
- await writeFile(file, staticIndex(template, baseFor(route)));
8315
+ await writeFile(file, dashboardIndex(template, { base: baseFor(route) }));
8297
8316
  }
8298
- await writeFile(resolve(outDir, "404.html"), staticIndex(template, "/"));
8317
+ await writeFile(resolve(outDir, "404.html"), dashboardIndex(template, { base: "/" }));
8299
8318
  return routes.length;
8300
8319
  }, (pages) => `${count(pages, "page")} with clean /path URLs`);
8301
8320
  const { runIds: _runs, trialIds: _trials, ...summary } = snapshot;
@@ -8333,26 +8352,55 @@ function baseFor(route) {
8333
8352
  function count(value, noun) {
8334
8353
  return `${value} ${noun}${value === 1 ? "" : "s"}`;
8335
8354
  }
8336
- /** Writes one trial's JSON views and candidate files; returns the file count. */
8337
- async function snapshotTrial(reports, write, data, runId, trialId) {
8338
- const base = `runs/${runId}/trials/${trialId}`;
8339
- await write(`${base}/detail.json`, await reports.trialDetail(runId, trialId));
8340
- await write(`${base}/events.json`, { events: await reports.trajectory(runId, trialId) });
8341
- await write(`${base}/artifacts.json`, { artifacts: await reports.listArtifacts(runId, trialId) });
8342
- const workspace = await reports.listCandidateWorkspace(runId, trialId);
8343
- await write(`${base}/workspace.json`, workspace);
8344
- const root = resolve(data, base, "workspace");
8355
+ /** Writes a run's snapshot below `root` (static builds). */
8356
+ function directorySink(root) {
8357
+ const target = (path) => {
8358
+ const file = resolve(root, path);
8359
+ if (!file.startsWith(`${resolve(root)}${sep}`)) throw new Error(`Invalid snapshot path: ${path}`);
8360
+ return file;
8361
+ };
8362
+ return {
8363
+ async json(path, value) {
8364
+ const file = target(path);
8365
+ await mkdir(dirname(file), { recursive: true });
8366
+ await writeFile(file, `${JSON.stringify(value)}\n`);
8367
+ },
8368
+ async bytes(path, data) {
8369
+ const file = target(path);
8370
+ await mkdir(dirname(file), { recursive: true });
8371
+ await writeFile(file, data);
8372
+ }
8373
+ };
8374
+ }
8375
+ /**
8376
+ * One run's snapshot, shared by `evalkit build` (files on disk) and
8377
+ * `evalkit share` (an upload bundle): `trials.json`, then per trial
8378
+ * `detail.json`, `events.json`, `artifacts.json`, `workspace.json`, and the
8379
+ * candidate files under `workspace/`. Evaluator workspaces are never read.
8380
+ */
8381
+ async function snapshotRun(reports, runId, sink, options = {}) {
8382
+ const trials = await reports.listTrials(runId);
8383
+ await sink.json("trials.json", { trials });
8345
8384
  let files = 0;
8346
- for (const entry of workspace) {
8347
- if (entry.kind !== "file") continue;
8348
- const target = resolve(root, entry.path);
8349
- if (!target.startsWith(`${root}${sep}`)) throw new Error(`Invalid workspace path: ${entry.path}`);
8350
- const response = await reports.candidateFile(runId, trialId, entry.path);
8351
- await mkdir(dirname(target), { recursive: true });
8352
- await writeFile(target, new Uint8Array(await response.arrayBuffer()));
8353
- files += 1;
8385
+ for (const trial of trials) {
8386
+ const base = `trials/${trial.id}`;
8387
+ await sink.json(`${base}/detail.json`, await reports.trialDetail(runId, trial.id));
8388
+ await sink.json(`${base}/events.json`, { events: await reports.trajectory(runId, trial.id) });
8389
+ await sink.json(`${base}/artifacts.json`, { artifacts: await reports.listArtifacts(runId, trial.id) });
8390
+ const workspace = await reports.listCandidateWorkspace(runId, trial.id);
8391
+ await sink.json(`${base}/workspace.json`, workspace);
8392
+ if (options.workspaceFiles === false) continue;
8393
+ for (const entry of workspace) {
8394
+ if (entry.kind !== "file") continue;
8395
+ const response = await reports.candidateFile(runId, trial.id, entry.path);
8396
+ await sink.bytes(`${base}/workspace/${entry.path}`, new Uint8Array(await response.arrayBuffer()));
8397
+ files += 1;
8398
+ }
8354
8399
  }
8355
- return files;
8400
+ return {
8401
+ trialIds: trials.map((trial) => trial.id),
8402
+ files
8403
+ };
8356
8404
  }
8357
8405
  /** Total size and file count of a directory, for the build summary. */
8358
8406
  async function directorySize(root) {
@@ -8371,15 +8419,6 @@ async function directorySize(root) {
8371
8419
  files
8372
8420
  };
8373
8421
  }
8374
- /**
8375
- * Select the static API and make every URL relative to `<base href>`, which
8376
- * points at the snapshot root. The app derives its router basename from it.
8377
- */
8378
- function staticIndex(html, base = "./") {
8379
- const result = html.replace("<html lang=\"en\">", "<html lang=\"en\" data-evalkit-snapshot=\"static\">").replace("<head>", `<head>\n <base href="${base}" />`).replaceAll("\"/assets/", "\"./assets/");
8380
- if (!result.includes("data-evalkit-snapshot=\"static\"") || !result.includes("<base href=")) throw new Error("Bundled dashboard index.html has an unexpected shape");
8381
- return result;
8382
- }
8383
8422
  async function prepareOutDir(outDir) {
8384
8423
  let entries;
8385
8424
  try {
@@ -8678,6 +8717,126 @@ async function confirm(question) {
8678
8717
  }
8679
8718
  }
8680
8719
  //#endregion
8720
+ //#region ../cli/src/source.ts
8721
+ const exec = promisify(execFile);
8722
+ /**
8723
+ * Normalize a git remote to a stable repository key: lowercase host, no
8724
+ * credentials, port, scheme, `.git`, or trailing slash.
8725
+ *
8726
+ * git@GitLab.example.com:group/sub/repo.git → gitlab.example.com/group/sub/repo
8727
+ * https://user:token@github.com/o/r/ → github.com/o/r
8728
+ * /srv/repos/thing.git (no host) → local/thing
8729
+ */
8730
+ function normalizeRepoUrl(url) {
8731
+ const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
8732
+ const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/.exec(trimmed);
8733
+ if (scp && !/^[a-z]+:\/\//i.test(trimmed)) return `${scp[1].toLowerCase()}/${scp[2].replace(/^\/+/, "")}`;
8734
+ try {
8735
+ const parsed = new URL(trimmed);
8736
+ if (parsed.protocol !== "file:" && parsed.hostname) return `${parsed.hostname.toLowerCase()}${parsed.pathname.replace(/\/+$/, "")}`;
8737
+ } catch {}
8738
+ return `local/${basename(trimmed)}`;
8739
+ }
8740
+ async function git(cwd, ...args) {
8741
+ try {
8742
+ const { stdout } = await exec("git", [
8743
+ "-C",
8744
+ cwd,
8745
+ ...args
8746
+ ], { timeout: 1e4 });
8747
+ return stdout.trim() || void 0;
8748
+ } catch {
8749
+ return;
8750
+ }
8751
+ }
8752
+ const posix = (path) => path.split(sep).join("/");
8753
+ const projectPath = (root, projectRoot) => root ? posix(relative(root, projectRoot)) : "";
8754
+ function ciSource(env, projectRoot) {
8755
+ if (env.EVALKIT_SOURCE_REPO && env.EVALKIT_SOURCE_COMMIT) return {
8756
+ repo: normalizeRepoUrl(env.EVALKIT_SOURCE_REPO),
8757
+ project: env.EVALKIT_SOURCE_PROJECT ?? "",
8758
+ commit: env.EVALKIT_SOURCE_COMMIT,
8759
+ ...env.EVALKIT_SOURCE_REF ? { ref: env.EVALKIT_SOURCE_REF } : {},
8760
+ ...env.EVALKIT_SOURCE_COMMITTED_AT ? { committedAt: env.EVALKIT_SOURCE_COMMITTED_AT } : {}
8761
+ };
8762
+ if (env.GITLAB_CI === "true" && env.CI_PROJECT_URL && env.CI_COMMIT_SHA) return {
8763
+ repo: normalizeRepoUrl(env.CI_PROJECT_URL),
8764
+ project: projectPath(env.CI_PROJECT_DIR, projectRoot),
8765
+ commit: env.CI_COMMIT_SHA,
8766
+ ...env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME ? { ref: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME } : {},
8767
+ ...env.CI_COMMIT_TIMESTAMP ? { committedAt: env.CI_COMMIT_TIMESTAMP } : {},
8768
+ ci: {
8769
+ provider: "gitlab",
8770
+ ...env.CI_PIPELINE_ID ? { pipeline: env.CI_PIPELINE_ID } : {},
8771
+ ...env.CI_JOB_ID ? { job: env.CI_JOB_ID } : {},
8772
+ ...env.CI_JOB_URL ? { url: env.CI_JOB_URL } : {}
8773
+ }
8774
+ };
8775
+ if (env.GITHUB_ACTIONS === "true" && env.GITHUB_REPOSITORY && env.GITHUB_SHA) {
8776
+ const server = env.GITHUB_SERVER_URL ?? "https://github.com";
8777
+ return {
8778
+ repo: normalizeRepoUrl(`${server}/${env.GITHUB_REPOSITORY}`),
8779
+ project: projectPath(env.GITHUB_WORKSPACE, projectRoot),
8780
+ commit: env.GITHUB_SHA,
8781
+ ...env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME ? { ref: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME } : {},
8782
+ ci: {
8783
+ provider: "github",
8784
+ ...env.GITHUB_RUN_ID ? { pipeline: env.GITHUB_RUN_ID } : {},
8785
+ ...env.GITHUB_JOB ? { job: env.GITHUB_JOB } : {},
8786
+ ...env.GITHUB_RUN_ID ? { url: `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` } : {}
8787
+ }
8788
+ };
8789
+ }
8790
+ }
8791
+ /**
8792
+ * Determine which repository and commit this project's eval code comes from.
8793
+ * Order: EVALKIT_SOURCE_* variables, GitLab/GitHub CI variables, then git.
8794
+ * Returns undefined outside git with no CI variables ("untracked" results).
8795
+ *
8796
+ * `outputDirs` (results, sandbox, snapshot) never make the tree dirty, even
8797
+ * when a project forgot to ignore them.
8798
+ */
8799
+ async function detectSource(givenRoot, options = {}) {
8800
+ const env = options.env ?? process.env;
8801
+ const projectRoot = await realpath(givenRoot).catch(() => givenRoot);
8802
+ const top = await git(projectRoot, "rev-parse", "--show-toplevel");
8803
+ const excludes = top ? (options.outputDirs ?? []).map((dir) => join(projectRoot, relative(givenRoot, dir))).map((dir) => posix(relative(top, dir))).filter((dir) => dir && !dir.startsWith("..")).map((dir) => `:(exclude)${dir}`) : [];
8804
+ const dirty = top ? Boolean(await git(top, "status", "--porcelain", "--", ".", ...excludes)) : false;
8805
+ const fromCi = ciSource(env, projectRoot);
8806
+ if (fromCi) return {
8807
+ ...fromCi,
8808
+ ...top && !env.EVALKIT_SOURCE_PROJECT ? { project: projectPath(top, projectRoot) } : {},
8809
+ dirty
8810
+ };
8811
+ if (!top) return void 0;
8812
+ const commit = await git(top, "rev-parse", "HEAD");
8813
+ if (!commit) return void 0;
8814
+ const remote = await git(top, "config", "--get", "remote.origin.url") ?? (await git(top, "remote"))?.split("\n").map((name) => name.trim()).find(Boolean);
8815
+ const remoteUrl = remote && !remote.includes("/") && !remote.includes(":") ? await git(top, "config", "--get", `remote.${remote}.url`) : remote;
8816
+ const ref = await git(top, "symbolic-ref", "--short", "-q", "HEAD");
8817
+ const committedAt = await git(top, "show", "-s", "--format=%cI", "HEAD");
8818
+ return {
8819
+ repo: normalizeRepoUrl(remoteUrl ?? top),
8820
+ project: projectPath(top, projectRoot),
8821
+ commit,
8822
+ ...ref ? { ref } : {},
8823
+ ...committedAt ? { committedAt } : {},
8824
+ dirty
8825
+ };
8826
+ }
8827
+ /** The project's evals and suites, saved with each run (`catalog.json`). */
8828
+ function runCatalog(registry) {
8829
+ return {
8830
+ catalog: registry.catalog(),
8831
+ evals: registry.metadata(),
8832
+ suites: registry.suiteMetadata()
8833
+ };
8834
+ }
8835
+ /** `abc1234` or `abc1234+dirty`. */
8836
+ function shortSource(source) {
8837
+ return `${source.commit.slice(0, 7)}${source.dirty ? "+dirty" : ""}`;
8838
+ }
8839
+ //#endregion
8681
8840
  //#region ../cli/src/run-command.ts
8682
8841
  function parseRunArgs(args) {
8683
8842
  const { values, positionals } = parseArgs({
@@ -8824,15 +8983,24 @@ async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArg
8824
8983
  return;
8825
8984
  }
8826
8985
  if (cells > (project.config.execution?.maxCells ?? 100) && !values.all) throw new Error(`Selected ${cells} cells. Narrow the selection, inspect --dry-run, or pass --all.`);
8827
- if (!values.json) console.log(`Running ${cells} cell(s) from ${plan.matrix}`);
8986
+ const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
8987
+ const sandboxDir = resolve(project.root, project.config.sandboxDir ?? "_evalkit-sandbox");
8988
+ const source = await detectSource(project.root, { outputDirs: [
8989
+ reportDir,
8990
+ sandboxDir,
8991
+ resolve(project.root, "_evalkit-dashboard")
8992
+ ] });
8993
+ if (!values.json) console.log(`Running ${cells} cell(s) from ${plan.matrix}` + (source ? ` at ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : " (untracked: not in a git repository)"));
8828
8994
  if ((await Effect.runPromise(runnerLogs(runMatrix(matrix, {
8829
8995
  selection,
8830
8996
  concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
8831
8997
  trials,
8832
8998
  ...suiteId ? { suiteId } : {},
8833
8999
  ...values.local ? { runtime: "local" } : {},
8834
- report: localReportStore(resolve(project.root, project.config.reportDir ?? "_evalkit-results")),
8835
- workspaceRoot: resolve(project.root, project.config.sandboxDir ?? "_evalkit-sandbox"),
9000
+ ...source ? { source } : {},
9001
+ catalog: runCatalog(registry),
9002
+ report: localReportStore(reportDir),
9003
+ workspaceRoot: sandboxDir,
8836
9004
  onResult: (cell, result) => {
8837
9005
  if (values.json) console.log(JSON.stringify({
8838
9006
  cell: {
@@ -8903,6 +9071,205 @@ async function tidyReports(options) {
8903
9071
  return result;
8904
9072
  }
8905
9073
  //#endregion
9074
+ //#region ../cli/src/share-command.ts
9075
+ /**
9076
+ * Which local runs `evalkit share` uploads: finished runs with a source, at
9077
+ * HEAD (or any commit with `all`), not dirty (unless `allowDirty`), not archived.
9078
+ */
9079
+ function selectRuns(runs, head, options) {
9080
+ const result = {
9081
+ selected: [],
9082
+ untracked: 0,
9083
+ otherCommits: 0,
9084
+ dirty: 0,
9085
+ archived: 0,
9086
+ running: 0
9087
+ };
9088
+ for (const run of runs) {
9089
+ const { source } = run;
9090
+ if (!source) result.untracked += 1;
9091
+ else if (!options.all && (source.commit !== head.commit || repoKey(source) !== repoKey(head))) result.otherCommits += 1;
9092
+ else if (source.dirty && !options.allowDirty) result.dirty += 1;
9093
+ else if (run.archived) result.archived += 1;
9094
+ else if (run.status === "running") result.running += 1;
9095
+ else result.selected.push({
9096
+ ...run,
9097
+ source
9098
+ });
9099
+ }
9100
+ return result;
9101
+ }
9102
+ /** One line for the Runs step: what will be shared and what was skipped. */
9103
+ function selectionSummary(selection, options) {
9104
+ return [
9105
+ `${plural(selection.selected.length, "run")} to share`,
9106
+ selection.otherCommits && !options.all ? `${selection.otherCommits} from other commits` : "",
9107
+ selection.running ? `${selection.running} still running skipped` : "",
9108
+ selection.dirty ? `${selection.dirty} dirty skipped` : "",
9109
+ selection.untracked ? `${selection.untracked} untracked skipped` : "",
9110
+ selection.archived ? `${selection.archived} archived skipped` : ""
9111
+ ].filter(Boolean).join(" · ");
9112
+ }
9113
+ /** Why nothing was selected, and what to do about it. */
9114
+ function noRunsMessage(selection, head, options) {
9115
+ const at = shortSource(head);
9116
+ if (selection.dirty) return `No committed runs for ${at}: ${plural(selection.dirty, "run")} came from uncommitted changes. Commit and rerun \`evalkit run\`, or pass --allow-dirty.`;
9117
+ if (selection.running) return `No finished runs for ${at}: ${plural(selection.running, "run")} still running. Share again when they finish.`;
9118
+ return `No runs for ${at}. Run \`evalkit run\` first${options.all ? "" : ", or pass --all to share runs from other commits"}.`;
9119
+ }
9120
+ /** Where to look after sharing: the commit, or the repository if several commits were shared. */
9121
+ function shareTarget(serverUrl, head, runs) {
9122
+ const commits = new Set(runs.map((run) => run.source.commit));
9123
+ const commit = commits.size === 1 ? `/-/${[...commits][0]}` : "";
9124
+ return new URL(`/${repoKey(head)}${commit}`, serverUrl).href;
9125
+ }
9126
+ /** Build one run's upload bundle: run.json, catalog.json, and its snapshot. */
9127
+ async function runBundle(options) {
9128
+ const files = [];
9129
+ const encode = (value) => new TextEncoder().encode(`${JSON.stringify(value)}\n`);
9130
+ files.push({
9131
+ path: "run.json",
9132
+ data: encode(options.run)
9133
+ });
9134
+ const saved = await readFile(resolve(options.reportRoot, options.run.id, "catalog.json")).catch(() => void 0);
9135
+ files.push({
9136
+ path: "catalog.json",
9137
+ data: saved ? new Uint8Array(saved) : encode(runCatalog(options.registry))
9138
+ });
9139
+ await snapshotRun(options.reports, options.run.id, {
9140
+ async json(path, value) {
9141
+ files.push({
9142
+ path,
9143
+ data: encode(value)
9144
+ });
9145
+ },
9146
+ async bytes(path, data) {
9147
+ files.push({
9148
+ path,
9149
+ data
9150
+ });
9151
+ }
9152
+ }, { workspaceFiles: options.workspaceFiles });
9153
+ return encodeBundle(files);
9154
+ }
9155
+ /** The server refused this bundle (4xx): retrying it cannot help. */
9156
+ var UploadRejected = class extends Error {};
9157
+ /**
9158
+ * The URL does not answer as an EvalKit share server (a redirect, an HTML page,
9159
+ * an auth proxy). Every upload would fail the same way, so sharing stops.
9160
+ */
9161
+ var ShareServerError = class extends Error {};
9162
+ /** `share.headers` from the config: called once, checked, never printed. */
9163
+ async function resolveShareHeaders(headers) {
9164
+ let value;
9165
+ try {
9166
+ value = typeof headers === "function" ? await headers() : headers ?? {};
9167
+ } catch (error) {
9168
+ throw new Error(`share.headers failed: ${error instanceof Error ? error.message : String(error)}`);
9169
+ }
9170
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("share.headers must be an object of header names to values");
9171
+ const entries = Object.entries(value);
9172
+ for (const [name, item] of entries) if (typeof item !== "string" || !item) throw new Error(`share.headers "${name}" must be a non-empty string`);
9173
+ return Object.fromEntries(entries);
9174
+ }
9175
+ /** Check that the URL is an EvalKit share server and the headers get through. */
9176
+ async function checkShareServer(serverUrl, headers) {
9177
+ const response = await fetch(new URL("/api/v1/repos", serverUrl), {
9178
+ headers,
9179
+ redirect: "manual"
9180
+ });
9181
+ await expectShareServer(response, serverUrl);
9182
+ await response.body?.cancel();
9183
+ }
9184
+ /** POST one bundle, retrying network errors and 5xx responses. */
9185
+ async function uploadBundle(serverUrl, bundle, options = {}) {
9186
+ const attempts = options.attempts ?? 3;
9187
+ let lastError;
9188
+ for (let attempt = 1; attempt <= attempts; attempt++) {
9189
+ try {
9190
+ return await postBundle(serverUrl, bundle, options.headers ?? {});
9191
+ } catch (error) {
9192
+ if (error instanceof UploadRejected || error instanceof ShareServerError) throw error;
9193
+ lastError = error;
9194
+ }
9195
+ if (attempt < attempts) await Bun.sleep((options.delayMs ?? 500) * 2 ** (attempt - 1));
9196
+ }
9197
+ throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
9198
+ }
9199
+ async function postBundle(serverUrl, bundle, headers) {
9200
+ const response = await fetch(new URL("/api/v1/uploads", serverUrl), {
9201
+ method: "POST",
9202
+ headers: {
9203
+ ...headers,
9204
+ "content-type": "application/octet-stream"
9205
+ },
9206
+ body: bundle,
9207
+ redirect: "manual"
9208
+ });
9209
+ await expectShareServer(response, serverUrl);
9210
+ if (response.ok) return parseUploadResult(await response.json());
9211
+ const reason = await response.text();
9212
+ if (response.status < 500) throw new UploadRejected(`Share server rejected the upload: ${reason}`);
9213
+ throw new Error(`Share server error ${response.status}: ${reason}`);
9214
+ }
9215
+ function parseUploadResult(value) {
9216
+ const result = value;
9217
+ if ((result?.status === "created" || result?.status === "exists") && typeof result.url === "string") return {
9218
+ status: result.status,
9219
+ url: result.url
9220
+ };
9221
+ throw new ShareServerError(`Unexpected share server response: ${JSON.stringify(value)}`);
9222
+ }
9223
+ /** Redirects and non-JSON answers come from something in front of the server. */
9224
+ async function expectShareServer(response, serverUrl) {
9225
+ if (response.status >= 300 && response.status < 400) {
9226
+ const location = response.headers.get("location");
9227
+ const target = location ? new URL(location, serverUrl).host : "elsewhere";
9228
+ throw new ShareServerError(`${new URL(serverUrl).host} redirected to ${target} instead of answering. If an auth proxy is in front of the share server, set share.headers in evalkit.config.ts.`);
9229
+ }
9230
+ const type = response.headers.get("content-type") ?? "";
9231
+ const denied = response.status === 401 || response.status === 403;
9232
+ if (denied || response.ok && !type.includes("json")) throw new ShareServerError(`${new URL(serverUrl).host} did not answer as an EvalKit share server (${response.status}${type ? `, ${type.split(";")[0]}` : ""}). ` + (denied ? "Check share.headers." : "Check share.url."));
9233
+ }
9234
+ async function shareRuns(options) {
9235
+ const reports = createDashboardReportReader(options.reportRoot, options.registry);
9236
+ return options.step("Uploading", async (progress) => {
9237
+ const result = {
9238
+ created: 0,
9239
+ exists: 0,
9240
+ failed: [],
9241
+ bytes: 0
9242
+ };
9243
+ for (const [index, run] of options.runs.entries()) {
9244
+ progress(`run ${index + 1}/${options.runs.length} · ${formatBytes(result.bytes)}`);
9245
+ try {
9246
+ const bundle = await runBundle({
9247
+ reports,
9248
+ reportRoot: options.reportRoot,
9249
+ registry: options.registry,
9250
+ run,
9251
+ workspaceFiles: options.workspaceFiles
9252
+ });
9253
+ const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
9254
+ result.bytes += bundle.byteLength;
9255
+ result[uploaded.status] += 1;
9256
+ } catch (error) {
9257
+ if (error instanceof ShareServerError) throw error;
9258
+ result.failed.push({
9259
+ runId: run.id,
9260
+ error: error instanceof Error ? error.message : String(error)
9261
+ });
9262
+ }
9263
+ }
9264
+ return result;
9265
+ }, (result) => `${result.created} new · ${result.exists} already shared` + (result.failed.length ? ` · ${result.failed.length} failed` : "") + ` · ${formatBytes(result.bytes)}`);
9266
+ }
9267
+ function formatBytes(size) {
9268
+ if (size < 1024) return `${size} B`;
9269
+ if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} kB`;
9270
+ return `${(size / 1024 ** 2).toFixed(1)} MB`;
9271
+ }
9272
+ //#endregion
8906
9273
  //#region ../cli/src/new-project-template.ts
8907
9274
  /** Provider-free example: one fixture, agent, scorer, and a two-cell matrix. */
8908
9275
  const starterFiles = {
@@ -9002,12 +9369,11 @@ Replace these examples with your own agent, tasks, and checks.
9002
9369
 
9003
9370
  \`\`\`sh
9004
9371
  bun install
9005
- bun run check # type-check your eval, agent, scorer, and config
9006
- bun run evals # run the example across both styles
9007
- bun run matrix:plan # inspect the matrix without running it
9008
- bun run matrix # run the configured matrix
9009
- bun run dashboard # inspect local reports and launch runs
9010
- bun run dashboard:build # write a read-only, shareable snapshot
9372
+ bun run check # type-check your eval, agent, scorer, and config
9373
+ bun run evalkit run # run the example across both styles
9374
+ bun run evalkit run --matrix styles --dry-run # inspect the matrix without running it
9375
+ bun run evalkit dev # inspect local reports and launch runs
9376
+ bun run evalkit build # write a read-only, shareable snapshot
9011
9377
  \`\`\`
9012
9378
 
9013
9379
  EvalKit installs from npm. Reports, trial workspaces, and dashboard snapshots stay local
@@ -9017,12 +9383,13 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
9017
9383
  }
9018
9384
  //#endregion
9019
9385
  //#region ../cli/src/new-project.ts
9020
- const npmDependency = "^0.3.0";
9386
+ const npmDependency = "^0.5.0";
9021
9387
  const ignored = [
9022
9388
  "node_modules/",
9023
9389
  "_evalkit-results/",
9024
9390
  "_evalkit-sandbox/",
9025
- "_evalkit-dashboard/"
9391
+ "_evalkit-dashboard/",
9392
+ "_evalkit-share/"
9026
9393
  ];
9027
9394
  const readIfExists = async (path) => {
9028
9395
  try {
@@ -9101,11 +9468,6 @@ async function preparePackage(packagePath, name) {
9101
9468
  const devDependencies = pkg.devDependencies ?? {};
9102
9469
  if (!scripts || typeof scripts !== "object" || Array.isArray(scripts) || !dependencies || typeof dependencies !== "object" || Array.isArray(dependencies) || !devDependencies || typeof devDependencies !== "object" || Array.isArray(devDependencies)) throw new Error(`Invalid package.json scripts/dependencies: ${packagePath}`);
9103
9470
  scripts.check ??= "tsc --noEmit";
9104
- scripts.evals ??= "evalkit run greeting --local";
9105
- scripts.matrix ??= "evalkit run --matrix styles --local";
9106
- scripts["matrix:plan"] ??= "evalkit run --matrix styles --dry-run";
9107
- scripts.dashboard ??= "evalkit dev";
9108
- scripts["dashboard:build"] ??= "evalkit build";
9109
9471
  if (!dependencies["@leostera/evalkit"] && !devDependencies["@leostera/evalkit"]) dependencies["@leostera/evalkit"] = npmDependency;
9110
9472
  if (!dependencies.typescript && !devDependencies.typescript) devDependencies.typescript = "^6.0.0";
9111
9473
  if (!dependencies["@types/bun"] && !devDependencies["@types/bun"]) devDependencies["@types/bun"] = "^1.4.0";
@@ -9134,7 +9496,7 @@ async function writeStarterReadme(root, name, inPlace) {
9134
9496
  }
9135
9497
  //#endregion
9136
9498
  //#region ../cli/src/index.ts
9137
- const VERSION = "0.3.0";
9499
+ const VERSION = "0.5.0";
9138
9500
  let projectRoot = process.cwd();
9139
9501
  let reportRoot = resolve(projectRoot, "_evalkit-results");
9140
9502
  const sandboxRoot = resolve(projectRoot, "_evalkit-sandbox");
@@ -9154,6 +9516,7 @@ async function runEvals(options) {
9154
9516
  const registry = await loadRegistry();
9155
9517
  const evaluations = options.evalIds.map((id) => registry.get(id));
9156
9518
  if (evaluations.some((e) => !e)) throw new Error("Unknown eval");
9519
+ const provenance = await runProvenance(registry);
9157
9520
  await Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
9158
9521
  const name = evaluation.name ?? authoringId(evaluation);
9159
9522
  const started = performance.now();
@@ -9162,10 +9525,23 @@ async function runEvals(options) {
9162
9525
  workspaceRoot: sandboxRoot,
9163
9526
  suiteId: options.suiteId,
9164
9527
  runtime: runtimeFor(evaluation),
9165
- concurrency: options.concurrency
9528
+ concurrency: options.concurrency,
9529
+ ...provenance
9166
9530
  })), Effect.tap((result) => runLog.finished(name, {}, result, performance.now() - started)), Effect.tapError((error) => Effect.sync(() => runLog.failed(name, error))));
9167
9531
  }), { concurrency: options.concurrency })));
9168
9532
  }
9533
+ /** Detected per launch: `evalkit dev` keeps running while commits change. */
9534
+ async function runProvenance(registry) {
9535
+ const source = await detectSource(projectRoot, { outputDirs: [
9536
+ reportRoot,
9537
+ sandboxRoot,
9538
+ resolve(projectRoot, "_evalkit-dashboard")
9539
+ ] });
9540
+ return {
9541
+ ...source ? { source } : {},
9542
+ catalog: runCatalog(registry)
9543
+ };
9544
+ }
9169
9545
  function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
9170
9546
  let matrixRunning = false;
9171
9547
  app.get("/v1/matrix", (context) => context.json({ matrix: dashboardMatrix ? {
@@ -9210,15 +9586,16 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
9210
9586
  const name = evaluation.name ?? authoringId(evaluation);
9211
9587
  const started = performance.now();
9212
9588
  runLog.started(name, body.parameters ?? {}, suiteId);
9213
- Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
9589
+ runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
9214
9590
  selection,
9215
9591
  concurrency: 1,
9216
9592
  trials,
9217
9593
  ...suiteId ? { suiteId } : {},
9218
9594
  report: localReportStore(reportRoot),
9219
9595
  workspaceRoot: sandboxRoot,
9596
+ ...provenance,
9220
9597
  onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
9221
- }))).catch((error) => runLog.failed(name, error)).finally(() => {
9598
+ })))).catch((error) => runLog.failed(name, error)).finally(() => {
9222
9599
  matrixRunning = false;
9223
9600
  });
9224
9601
  } else {
@@ -9413,7 +9790,7 @@ const runCommand = make$1("run", {
9413
9790
  const newCommand = make$1("new", { directory: text$2({ name: "directory" }) }, ({ directory }) => Effect.tryPromise({
9414
9791
  try: async () => {
9415
9792
  const root = await newProject([directory]);
9416
- console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run evals.`);
9793
+ console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run evalkit run.`);
9417
9794
  },
9418
9795
  catch: (error) => error
9419
9796
  }));
@@ -9505,12 +9882,88 @@ const tidyCommand = make$1("tidy", { config }, ({ config }) => Effect.tryPromise
9505
9882
  },
9506
9883
  catch: (error) => error
9507
9884
  }));
9885
+ const shareServeCommand = make$1("serve", {
9886
+ dir: text("dir").pipe(withDefault("_evalkit-share")),
9887
+ port: integer("port").pipe(withDefault(4318))
9888
+ }, ({ dir, port }) => Effect.tryPromise({
9889
+ try: async () => {
9890
+ const root = resolve(process.cwd(), dir);
9891
+ const app = await createDirectoryShareApp({
9892
+ dir: root,
9893
+ dashboardRoot: dashboardAssets()
9894
+ });
9895
+ let server;
9896
+ try {
9897
+ server = Bun.serve({
9898
+ port,
9899
+ fetch: app.fetch
9900
+ });
9901
+ } catch (error) {
9902
+ if (error.code === "EADDRINUSE") throw new Error(`Port ${port} is already in use. Choose another: evalkit share serve --port 4319`);
9903
+ throw error;
9904
+ }
9905
+ banner("share serve", VERSION);
9906
+ console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(`http://localhost:${server.port}`)}`);
9907
+ console.log(` ${cyan("➜")} ${bold("Storage:".padEnd(9))}${relative(process.cwd(), root) || "."}`);
9908
+ console.log(`\n ${dim(`Upload with share.url = "http://localhost:${server.port}". No authentication: keep it private.`)}\n`);
9909
+ },
9910
+ catch: (error) => error
9911
+ }));
9912
+ const shareCommand = make$1("share", {
9913
+ config,
9914
+ all: boolean("all"),
9915
+ allowDirty: boolean("allow-dirty"),
9916
+ noWorkspace: boolean("no-workspace")
9917
+ }, ({ config, all, allowDirty, noWorkspace }) => Effect.tryPromise({
9918
+ try: async () => {
9919
+ const started = performance.now();
9920
+ banner("share", VERSION);
9921
+ await loadProjectStep(Option.getOrUndefined(config));
9922
+ const serverUrl = process.env.EVALKIT_SHARE_URL ?? project.config.share?.url;
9923
+ if (!serverUrl) throw new Error("No share server configured. Set share: { url } in evalkit.config.ts or EVALKIT_SHARE_URL.");
9924
+ const head = await step("Source", () => detectSource(projectRoot, { outputDirs: [
9925
+ reportRoot,
9926
+ sandboxRoot,
9927
+ resolve(projectRoot, "_evalkit-dashboard")
9928
+ ] }), (source) => source ? `${repoKey(source)} @ ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : "not a git repository");
9929
+ if (!head) throw new Error("evalkit share needs to know the commit: run it inside a git repository or set EVALKIT_SOURCE_REPO and EVALKIT_SOURCE_COMMIT. `evalkit build` works without one.");
9930
+ const registry = project.registry;
9931
+ const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot, registry).listRuns(), head, {
9932
+ all,
9933
+ allowDirty
9934
+ }), (value) => selectionSummary(value, { all }));
9935
+ if (!selection.selected.length) throw new Error(noRunsMessage(selection, head, { all }));
9936
+ const result = await shareRuns({
9937
+ serverUrl,
9938
+ headers: await step("Server", async () => {
9939
+ const headers = await resolveShareHeaders(project.config.share?.headers);
9940
+ await checkShareServer(serverUrl, headers);
9941
+ return headers;
9942
+ }, (headers) => {
9943
+ const names = Object.keys(headers);
9944
+ return `${new URL(serverUrl).host}${names.length ? dim(` with ${names.join(", ")}`) : ""}`;
9945
+ }),
9946
+ reportRoot,
9947
+ registry,
9948
+ runs: selection.selected,
9949
+ workspaceFiles: !noWorkspace,
9950
+ step
9951
+ });
9952
+ for (const failure of result.failed) console.log(` ${yellow("✗")} ${failure.runId.slice(0, 8)} ${dim(failure.error)}`);
9953
+ const target = shareTarget(serverUrl, head, selection.selected);
9954
+ console.log(`\n ${bold(result.failed.length ? "Shared with errors" : "Shared")} in ${duration(performance.now() - started)}\n`);
9955
+ console.log(` ${cyan("➜")} ${cyan(target)}\n`);
9956
+ if (result.failed.length) process.exitCode = 1;
9957
+ },
9958
+ catch: (error) => error
9959
+ })).pipe(withSubcommands([shareServeCommand]));
9508
9960
  const cli = run(make$1("evalkit").pipe(withSubcommands([
9509
9961
  newCommand,
9510
9962
  runCommand,
9511
9963
  devCommand,
9512
9964
  buildCommand,
9513
- tidyCommand
9965
+ tidyCommand,
9966
+ shareCommand
9514
9967
  ])), {
9515
9968
  name: "EvalKit",
9516
9969
  version: VERSION