sverka 0.2.24 → 0.2.26

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/main.ts","../src/output.ts"],"mappings":";;iBAGiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;EACf,MAAM;EACN,UAAU;EACV,MAAM;EACN,UAAU;;EAEV,MAAM;;;qBAIK;;;;;;YAOD,mBAAmB,uBAAuB;;YAG1C;;qBAWC,iBAAiB;WACnB,MAAM;WACN,UAAU;WACD;EAEhB,YAAA,iBACA,MAAM,cACN,UAAU,UACV;;;;;UC9Ba;EACf,SAAS;;;;;;;wBA6cW,KAAK,gBAAgB,OAAO,WAAW;;;;KCrejD,aAAa;;;;;;qBAOZ,+BAA+B;;EAM9B,YAAA,KAAK,WAAW,KAAK,WAAW,iBAAe;EAO3D,MAAM;EAKN,UAAU;EAKV,MAAM;EAIN,UAAU;;EAKV,MAAM;;;;;;wBAwDQ,mBACd,QAAQ,aACR,KAAK,WACL,KAAK,YACJ;;;;;wBAYa,iBACd,QAAQ,aACR,MAAM,eACL"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/main.ts","../src/output.ts"],"mappings":";;iBAGiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;EACf,MAAM;EACN,UAAU;EACV,MAAM;EACN,UAAU;;EAEV,MAAM;;;qBAIK;;;;;;YAOD,mBAAmB,uBAAuB;;YAG1C;;qBAWC,iBAAiB;WACnB,MAAM;WACN,UAAU;WACD;EAEhB,YAAA,iBACA,MAAM,cACN,UAAU,UACV;;;;;UC3Ba;EACf,SAAS;;;;;;;wBAwjBW,KAAK,gBAAgB,OAAO,WAAW;;;;KCnlBjD,aAAa;;;;;;qBAOZ,+BAA+B;;EAM9B,YAAA,KAAK,WAAW,KAAK,WAAW,iBAAe;EAO3D,MAAM;EAKN,UAAU;EAKV,MAAM;EAIN,UAAU;;EAKV,MAAM;;;;;;wBAwDQ,mBACd,QAAQ,aACR,KAAK,WACL,KAAK,YACJ;;;;;wBAYa,iBACd,QAAQ,aACR,MAAM,eACL"}
package/dist/index.mjs CHANGED
@@ -3,18 +3,21 @@ import * as nodeModule from "node:module";
3
3
  import { register } from "node:module";
4
4
  import process$1 from "node:process";
5
5
  import yargs from "yargs";
6
- import { constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { mkdir, open, readFile, realpath, writeFile } from "node:fs/promises";
10
10
  import { Entry, Pipeline, Project, ShellStep, collectConstructWarnings, manual, synthesize, validateGraph } from "@sverka/workflow";
11
11
  import { bindRunPlan, createPlanner } from "@sverka/sdk";
12
12
  import { DEFAULT_POLICY, createBuiltinResolver, evaluatePolicy, loadBaseline, normalizeSarif, serializeSarif, synthesizeCheckSteps } from "@sverka/verification";
13
- import { AGENT_ENV, collectAgentWrites, createDockerDriver, createEngine, createHostDriver, noAgentDriverError, resolveAgentDrivers, resolveAgentEngine } from "@sverka/runtime";
14
- import { spawnSync } from "node:child_process";
13
+ import { AGENT_ENV, collectAgentWrites, createDockerDriver, createEngine, createFileCacheStore, createHostDriver, noAgentDriverError, resolveAgentDrivers, resolveAgentEngine } from "@sverka/runtime";
14
+ import { execFileSync, spawnSync } from "node:child_process";
15
15
  import { watch } from "chokidar";
16
+ import { homedir } from "node:os";
17
+ import { createRemoteCacheStore, createRemoteCircuit, createRemoteSnapshotStore, createTieredCacheStore } from "@sverka/storage";
16
18
  import { compileGithub, compileGitlab } from "@sverka/compiler";
17
19
  import { z } from "zod";
20
+ import { startHubServer } from "@sverka/hub";
18
21
  //#region src/types.ts
19
22
  /** Exit codes used by the CLI. */
20
23
  const ExitCode = {
@@ -967,6 +970,119 @@ function watchLoop(opts) {
967
970
  };
968
971
  }
969
972
  //#endregion
973
+ //#region src/internal/hub.ts
974
+ function configHome() {
975
+ const xdg = process.env["XDG_CONFIG_HOME"];
976
+ return typeof xdg === "string" && xdg !== "" ? xdg : join(homedir(), ".config");
977
+ }
978
+ function credentialsPath() {
979
+ return join(configHome(), "sverka", "credentials");
980
+ }
981
+ function readCredentials() {
982
+ try {
983
+ const text = readFileSync(credentialsPath(), "utf8");
984
+ const parsed = JSON.parse(text);
985
+ if (typeof parsed === "object" && parsed !== null) return parsed;
986
+ } catch {}
987
+ return {};
988
+ }
989
+ /**
990
+ * Store a hub token at `~/.config/sverka/credentials` with mode 0600.
991
+ * The file is a JSON map keyed by hub URL so multiple hubs coexist.
992
+ */
993
+ function storeCredentials(url, token) {
994
+ const path = credentialsPath();
995
+ const hubs = {
996
+ ...readCredentials().hubs ?? {},
997
+ [url]: token
998
+ };
999
+ mkdirSync(dirname(path), {
1000
+ recursive: true,
1001
+ mode: 448
1002
+ });
1003
+ writeFileSync(path, `${JSON.stringify({ hubs }, null, 2)}\n`, { mode: 384 });
1004
+ chmodSync(path, 384);
1005
+ return path;
1006
+ }
1007
+ function storedTokenFor(url) {
1008
+ return readCredentials().hubs?.[url];
1009
+ }
1010
+ /** Read `.sverka/hub.json` when present. Malformed JSON is ignored. */
1011
+ function readHubFileConfig(root) {
1012
+ const path = join(root, ".sverka", "hub.json");
1013
+ if (!existsSync(path)) return {};
1014
+ try {
1015
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1016
+ if (typeof parsed !== "object" || parsed === null) return {};
1017
+ const outer = parsed;
1018
+ const inner = typeof outer.remote === "object" && outer.remote !== null ? outer.remote : outer;
1019
+ return {
1020
+ url: typeof inner.url === "string" ? inner.url : void 0,
1021
+ project: typeof inner.project === "string" ? inner.project : void 0,
1022
+ enabled: inner.enabled === true,
1023
+ cache: typeof inner.cache === "boolean" ? inner.cache : void 0,
1024
+ upload: typeof inner.upload === "boolean" ? inner.upload : void 0
1025
+ };
1026
+ } catch {
1027
+ return {};
1028
+ }
1029
+ }
1030
+ /** Derive "owner/repo" from `git remote.origin.url`; falls back to the
1031
+ * directory basename when there is no usable remote. */
1032
+ function projectSlug(root) {
1033
+ try {
1034
+ const url = execFileSync("git", [
1035
+ "config",
1036
+ "--get",
1037
+ "remote.origin.url"
1038
+ ], {
1039
+ cwd: root,
1040
+ encoding: "utf8",
1041
+ stdio: [
1042
+ "ignore",
1043
+ "pipe",
1044
+ "ignore"
1045
+ ]
1046
+ }).trim();
1047
+ const slug = (/^[^@]+@[^:]+:(.+)$/.exec(url)?.[1] ?? new URL(url).pathname).replace(/\.git$/, "").replace(/^\/+/, "");
1048
+ if (slug !== "") return slug;
1049
+ } catch {}
1050
+ return basename(resolve(root));
1051
+ }
1052
+ function envString(name) {
1053
+ const value = process.env[name];
1054
+ return typeof value === "string" && value !== "" ? value : void 0;
1055
+ }
1056
+ /** Canonical credential key — `sverka login` stores tokens under the URL
1057
+ * with trailing slashes stripped; lookups must normalize identically. */
1058
+ function normalizeHubUrl(url) {
1059
+ let end = url.length;
1060
+ while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
1061
+ return url.slice(0, end);
1062
+ }
1063
+ /**
1064
+ * Resolve the effective hub connection for a root. Returns null when no
1065
+ * hub is configured at all (no url, no token anywhere) — the caller then
1066
+ * treats `--remote` as a usage error and a bare `run` stays local.
1067
+ */
1068
+ function resolveHub(root) {
1069
+ const file = readHubFileConfig(root);
1070
+ const rawUrl = envString("SVERKA_HUB_URL") ?? file.url;
1071
+ const url = rawUrl !== void 0 ? normalizeHubUrl(rawUrl) : void 0;
1072
+ const token = envString("SVERKA_HUB_TOKEN") ?? (url !== void 0 ? storedTokenFor(url) : void 0);
1073
+ if (url === void 0 || token === void 0) return null;
1074
+ return {
1075
+ config: {
1076
+ url,
1077
+ token,
1078
+ project: file.project ?? projectSlug(root)
1079
+ },
1080
+ cache: file.cache ?? true,
1081
+ upload: file.upload ?? true,
1082
+ enabled: file.enabled === true
1083
+ };
1084
+ }
1085
+ //#endregion
970
1086
  //#region src/commands/run.ts
971
1087
  /** Pure-argument guards — an invalid flag fails fast instead of surfacing
972
1088
  * as a recurring run failure inside the watch supervisor. */
@@ -988,6 +1104,7 @@ async function runCommand(args, global, output, start) {
988
1104
  if (detected !== void 0) output.errorLine(`no sverka.config — running ${detected.length} detected checks (${detected.join(", ")})`);
989
1105
  for (const warning of warnings) output.errorLine(`warning: ${warning}`);
990
1106
  const entryId = resolveEntryId(graph, args.entryId);
1107
+ const remote = resolveRemote(args, global, output);
991
1108
  const plan = bindRunPlan({
992
1109
  graph,
993
1110
  entryId
@@ -999,7 +1116,9 @@ async function runCommand(args, global, output, start) {
999
1116
  maxConcurrent: args.jobs ?? 4
1000
1117
  }), plan, {
1001
1118
  workspace: global.root,
1002
- artifactDir
1119
+ artifactDir,
1120
+ cache: remote?.stores.cache,
1121
+ snapshotStore: remote?.stores.snapshotStore
1003
1122
  }, global, output, graph, args);
1004
1123
  const durationMs = Date.now() - start;
1005
1124
  const evaluation = await evaluateRun(fmt.evaluate, artifactDir, global, output, events, renderer, args, start);
@@ -1015,6 +1134,7 @@ async function runCommand(args, global, output, start) {
1015
1134
  ...detected !== void 0 ? { detected } : {},
1016
1135
  ...args.report !== void 0 ? { report: args.report } : {}
1017
1136
  }, events, output);
1137
+ if (remote !== void 0 && remote.upload && report !== void 0) await uploadReportSafe(remote, report, entryId, findRunId(events), output);
1018
1138
  await finalizeRenderer(fmt, renderer, args, global, output);
1019
1139
  if (evaluation.collectionFailed) return evaluation.exitCode;
1020
1140
  writeRunOutput({
@@ -1032,6 +1152,52 @@ async function runCommand(args, global, output, start) {
1032
1152
  if (evaluation.exitCode !== 0) return evaluation.exitCode;
1033
1153
  return exitCodeForStatus(runStatus);
1034
1154
  }
1155
+ /** Build the remote wiring for a run. `--remote` with no hub configured
1156
+ * is a usage error naming `sverka login`; a hub.json-enabled run whose
1157
+ * credentials are missing warns and runs local-only (degrade, don't
1158
+ * fail). */
1159
+ function resolveRemote(args, global, output) {
1160
+ const hub = resolveHub(global.root);
1161
+ const requested = args.remote === true;
1162
+ if (hub === null) {
1163
+ if (requested) throw new CliError("--remote needs a hub: set SVERKA_HUB_URL + SVERKA_HUB_TOKEN, run `sverka login --hub <url> --token <t>`, or add .sverka/hub.json", "MISSING_ARG", ExitCode.UsageError);
1164
+ if (readHubFileConfig(global.root).enabled === true) output.errorLine("warning: .sverka/hub.json enables remote runs but no hub credentials resolved — running local-only (see `sverka login`)");
1165
+ return;
1166
+ }
1167
+ if (!requested && !hub.enabled) return void 0;
1168
+ const circuit = createRemoteCircuit();
1169
+ const remoteConfig = {
1170
+ ...hub.config,
1171
+ circuit
1172
+ };
1173
+ const cache = hub.cache ? createTieredCacheStore({
1174
+ local: createFileCacheStore({ cacheDir: join(global.root, ".sverka", "cache") }),
1175
+ remote: createRemoteCacheStore(remoteConfig),
1176
+ onWarn: (msg) => output.errorLine(`warning: ${msg}`)
1177
+ }) : void 0;
1178
+ return {
1179
+ config: remoteConfig,
1180
+ upload: hub.upload,
1181
+ stores: {
1182
+ ...cache !== void 0 ? { cache } : {},
1183
+ snapshotStore: createRemoteSnapshotStore(remoteConfig)
1184
+ }
1185
+ };
1186
+ }
1187
+ /** Upload the run report — warns `remote.upload-failed` and swallows the
1188
+ * error per Spec 55 ("remote failures degrade to local + warn"). */
1189
+ async function uploadReportSafe(remote, report, entryId, runId, output) {
1190
+ try {
1191
+ const { uploadRunReport } = await import("@sverka/storage");
1192
+ const result = await uploadRunReport(remote.config, report.payload, report.findingsList, {
1193
+ entry: entryId,
1194
+ ...runId !== void 0 ? { runId } : {}
1195
+ });
1196
+ output.debug(`remote: uploaded run report as ${result.runId}`);
1197
+ } catch (e) {
1198
+ output.errorLine(`warning: remote.upload-failed: ${e instanceof Error ? e.message : String(e)}`);
1199
+ }
1200
+ }
1035
1201
  /** Post-run: announce the HTML report path (sarif/web print their own),
1036
1202
  * then keep interactive renderers mounted until the user quits. */
1037
1203
  async function finalizeRenderer(fmt, renderer, args, global, output) {
@@ -1111,7 +1277,9 @@ async function writeReportSafe(ctx, events, output) {
1111
1277
  return {
1112
1278
  html: artifacts.htmlPath,
1113
1279
  json: join(artifacts.dir, "report.json"),
1114
- findings: artifacts.findingsCount
1280
+ findings: artifacts.findingsCount,
1281
+ payload: artifacts.payload,
1282
+ findingsList: artifacts.findings
1115
1283
  };
1116
1284
  } catch (err) {
1117
1285
  if (err instanceof CliError) throw err;
@@ -1219,7 +1387,9 @@ async function consumeEvents(engine, plan, ctx, global, output, graph, args) {
1219
1387
  for await (const event of engine.run({
1220
1388
  plan,
1221
1389
  workspace: ctx.workspace,
1222
- artifactDir: ctx.artifactDir
1390
+ artifactDir: ctx.artifactDir,
1391
+ ...ctx.cache !== void 0 ? { cache: ctx.cache } : {},
1392
+ ...ctx.snapshotStore !== void 0 ? { snapshotStore: ctx.snapshotStore } : {}
1223
1393
  })) {
1224
1394
  events.push(event);
1225
1395
  renderer?.onEvent(event);
@@ -1485,11 +1655,14 @@ async function writeRunArtifacts(opts) {
1485
1655
  const warnings = [];
1486
1656
  const findings = await collectRunFindings(opts, warnings);
1487
1657
  const htmlPath = await writeReportHtml(opts.reportPath !== void 0 ? resolveUnderRoot(opts.root, opts.reportPath) : join(dir, "report.html"), findings, warnings);
1488
- writeFileSync(join(dir, "report.json"), JSON.stringify(runReportPayload(opts, findings.length, warnings), null, 2), "utf-8");
1658
+ const payload = runReportPayload(opts, findings.length, warnings);
1659
+ writeFileSync(join(dir, "report.json"), JSON.stringify(payload, null, 2), "utf-8");
1489
1660
  return {
1490
1661
  dir,
1491
1662
  htmlPath,
1492
- findingsCount: findings.length
1663
+ findingsCount: findings.length,
1664
+ payload,
1665
+ findings
1493
1666
  };
1494
1667
  }
1495
1668
  function exitCodeForStatus(runStatus) {
@@ -2498,6 +2671,124 @@ async function postJson(url, token, body, auth) {
2498
2671
  }
2499
2672
  }
2500
2673
  //#endregion
2674
+ //#region src/commands/login.ts
2675
+ async function loginCommand(args, _global, output, _start) {
2676
+ const url = args.hub ?? process.env["SVERKA_HUB_URL"];
2677
+ const token = args.token ?? process.env["SVERKA_HUB_TOKEN"];
2678
+ if (url === void 0) throw new CliError("missing --hub <url> (or set SVERKA_HUB_URL)", "MISSING_ARG", ExitCode.UsageError);
2679
+ if (!token) throw new CliError("missing --token <t> (or set SVERKA_HUB_TOKEN)", "MISSING_ARG", ExitCode.UsageError);
2680
+ let parsed;
2681
+ try {
2682
+ parsed = new URL(url);
2683
+ } catch {
2684
+ throw new CliError(`--hub must be a URL, got "${url}"`, "INVALID_FLAG", ExitCode.UsageError);
2685
+ }
2686
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new CliError(`--hub must be http(s), got "${parsed.protocol}"`, "INVALID_FLAG", ExitCode.UsageError);
2687
+ const normalized = normalizeHubUrl(url);
2688
+ const path = storeCredentials(normalized, token);
2689
+ output.writeLine(`stored credentials for ${normalized} in ${path}`);
2690
+ return ExitCode.Success;
2691
+ }
2692
+ //#endregion
2693
+ //#region src/commands/hub.ts
2694
+ async function hubCommand(args, global, output, _start) {
2695
+ if (args.action !== "serve") throw new CliError(`unknown hub action "${args.action ?? ""}" (supported: serve)`, "UNKNOWN_COMMAND", ExitCode.UsageError);
2696
+ if (args.port !== void 0 && (!Number.isInteger(args.port) || args.port < 0 || args.port > 65535)) throw new CliError("--port must be an integer in [0, 65535]", "INVALID_FLAG", ExitCode.UsageError);
2697
+ const dataDir = args.data ?? `${global.root}/.sverka-hub`;
2698
+ const server = await startHubServer({
2699
+ dataDir,
2700
+ ...args.port !== void 0 ? { port: args.port } : {},
2701
+ ...args.host !== void 0 ? { host: args.host } : {}
2702
+ });
2703
+ output.writeLine(`sverka hub listening on ${server.url}`);
2704
+ output.writeLine(` data: ${server.dataDir}`);
2705
+ output.writeLine(` dashboard: ${server.url}/`);
2706
+ output.writeLine(" (Ctrl+C to stop)");
2707
+ await new Promise((done) => {
2708
+ const stop = () => {
2709
+ process.removeListener("SIGINT", stop);
2710
+ process.removeListener("SIGTERM", stop);
2711
+ done();
2712
+ };
2713
+ process.on("SIGINT", stop);
2714
+ process.on("SIGTERM", stop);
2715
+ });
2716
+ await server.close();
2717
+ return ExitCode.Success;
2718
+ }
2719
+ //#endregion
2720
+ //#region src/commands/runs.ts
2721
+ function listLocalRuns(root) {
2722
+ const dir = join(root, ".sverka", "runs");
2723
+ if (!existsSync(dir)) return [];
2724
+ const rows = [];
2725
+ for (const entry of readdirSync(dir)) {
2726
+ const reportPath = join(dir, entry, "report.json");
2727
+ if (!existsSync(reportPath)) continue;
2728
+ let status = "unknown";
2729
+ let findings = null;
2730
+ try {
2731
+ const data = JSON.parse(readFileSync(reportPath, "utf8")).data ?? {};
2732
+ if (typeof data.status === "string") status = data.status;
2733
+ if (typeof data.findings === "number") findings = data.findings;
2734
+ } catch {}
2735
+ rows.push({
2736
+ runId: entry,
2737
+ status,
2738
+ findings,
2739
+ mtimeMs: statSync(reportPath).mtimeMs
2740
+ });
2741
+ }
2742
+ return rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
2743
+ }
2744
+ async function runsCommand(args, global, output, _start) {
2745
+ if (args.remote === true) return remoteRuns(args, global, output);
2746
+ return localRuns(args, global, output);
2747
+ }
2748
+ async function remoteRuns(args, global, output) {
2749
+ const hub = resolveHub(global.root);
2750
+ if (hub === null) throw new CliError("no hub configured — set SVERKA_HUB_URL + SVERKA_HUB_TOKEN, run `sverka login`, or add .sverka/hub.json", "MISSING_ARG", ExitCode.UsageError);
2751
+ const { listRuns } = await import("@sverka/storage");
2752
+ const runs = await listRuns(hub.config, { ...args.limit !== void 0 ? { limit: args.limit } : {} });
2753
+ if (global.format === "json") {
2754
+ output.writeLine(JSON.stringify({
2755
+ command: "runs",
2756
+ data: runs
2757
+ }));
2758
+ return ExitCode.Success;
2759
+ }
2760
+ if (runs.length === 0) {
2761
+ output.writeLine(`no remote runs for ${hub.config.project} — run \`sverka run --remote\` to upload one`);
2762
+ return ExitCode.Success;
2763
+ }
2764
+ output.writeLine(`remote runs for ${hub.config.project} (${hub.config.url})`);
2765
+ for (const run of runs) {
2766
+ const when = run.startedAt === null ? "—" : new Date(run.startedAt).toISOString().slice(0, 19) + "Z";
2767
+ const duration = run.durationMs === null ? "—" : `${(run.durationMs / 1e3).toFixed(1)}s`;
2768
+ output.writeLine(` ${run.runId} ${run.status.padEnd(9)} ${run.entry.padEnd(20)} ${when} ${duration} findings:${run.findingCounts.total}`);
2769
+ }
2770
+ return ExitCode.Success;
2771
+ }
2772
+ function localRuns(args, global, output) {
2773
+ const shown = listLocalRuns(global.root).slice(0, args.limit ?? 20);
2774
+ if (global.format === "json") {
2775
+ output.writeLine(JSON.stringify({
2776
+ command: "runs",
2777
+ data: shown
2778
+ }));
2779
+ return ExitCode.Success;
2780
+ }
2781
+ if (shown.length === 0) {
2782
+ output.writeLine("no runs recorded — run `sverka run` first");
2783
+ return ExitCode.Success;
2784
+ }
2785
+ for (const run of shown) {
2786
+ const when = new Date(run.mtimeMs).toISOString().slice(0, 19) + "Z";
2787
+ output.writeLine(` ${run.runId} ${run.status.padEnd(9)} ${when} findings:${run.findings ?? "—"}`);
2788
+ }
2789
+ return ExitCode.Success;
2790
+ }
2791
+ //#endregion
2501
2792
  //#region src/main.ts
2502
2793
  /** Build GlobalFlags from parsed yargs options. */
2503
2794
  function buildGlobalFlags(parsed) {
@@ -2567,6 +2858,10 @@ function addRunCommand(y) {
2567
2858
  type: "boolean",
2568
2859
  alias: "w",
2569
2860
  describe: "Re-run on file changes (debounced 300ms)"
2861
+ }).option("remote", {
2862
+ type: "boolean",
2863
+ default: false,
2864
+ describe: "Remote hub run: remote cache + snapshot store + report upload"
2570
2865
  });
2571
2866
  }
2572
2867
  /** Configure the plan subcommand options. */
@@ -2636,6 +2931,45 @@ function addUiCommand(y) {
2636
2931
  describe: "Host to bind"
2637
2932
  });
2638
2933
  }
2934
+ /** Configure the login subcommand options. */
2935
+ function addLoginCommand(y) {
2936
+ return y.option("hub", {
2937
+ type: "string",
2938
+ describe: "Hub base URL (or SVERKA_HUB_URL)"
2939
+ }).option("token", {
2940
+ type: "string",
2941
+ describe: "Hub bearer token (or SVERKA_HUB_TOKEN)"
2942
+ });
2943
+ }
2944
+ /** Configure the hub subcommand options. */
2945
+ function addHubCommand(y) {
2946
+ return y.positional("action", {
2947
+ type: "string",
2948
+ choices: ["serve"],
2949
+ describe: "hub action (serve)"
2950
+ }).option("port", {
2951
+ type: "number",
2952
+ default: 7357,
2953
+ describe: "Port to listen on"
2954
+ }).option("host", {
2955
+ type: "string",
2956
+ describe: "Host to bind"
2957
+ }).option("data", {
2958
+ type: "string",
2959
+ describe: "Data directory (default: .sverka-hub under --root)"
2960
+ });
2961
+ }
2962
+ /** Configure the runs subcommand options. */
2963
+ function addRunsCommand(y) {
2964
+ return y.option("remote", {
2965
+ type: "boolean",
2966
+ default: false,
2967
+ describe: "List runs stored on the configured hub"
2968
+ }).option("limit", {
2969
+ type: "number",
2970
+ describe: "Max runs to list"
2971
+ });
2972
+ }
2639
2973
  function buildParser() {
2640
2974
  return yargs([]).scriptName("sverka").version(cliVersion()).option("format", {
2641
2975
  type: "string",
@@ -2672,7 +3006,7 @@ function buildParser() {
2672
3006
  }).option("file", {
2673
3007
  type: "string",
2674
3008
  describe: "Path to sverka-writes.json (default: <root>/sverka-writes.json)"
2675
- })).demandCommand(1, "No command given").strict().fail((msg, err) => {
3009
+ })).command("login", "Store hub credentials (~/.config/sverka/credentials)", addLoginCommand).command("hub <action>", "Self-hosted run hub (serve: cache + history + findings)", addHubCommand).command("runs", "List run history (local, or --remote)", addRunsCommand).demandCommand(1, "No command given").strict().fail((msg, err) => {
2676
3010
  if (err) throw err;
2677
3011
  throw new CliError(msg, "UNKNOWN_COMMAND", ExitCode.UsageError);
2678
3012
  });
@@ -2696,6 +3030,9 @@ async function dispatch(command, parsed, global, output, start) {
2696
3030
  case "ui": return dispatchUi(parsed, global, output, start);
2697
3031
  case "agent": return dispatchAgent(parsed, global, output, start);
2698
3032
  case "apply": return dispatchApply(parsed, global, output, start);
3033
+ case "login": return dispatchLogin(parsed, global, output, start);
3034
+ case "hub": return dispatchHub(parsed, global, output, start);
3035
+ case "runs": return dispatchRuns(parsed, global, output, start);
2699
3036
  default: throw new CliError(`unknown command: ${command}`, "UNKNOWN_COMMAND", ExitCode.UsageError);
2700
3037
  }
2701
3038
  }
@@ -2732,6 +3069,7 @@ function dispatchRun(parsed, global, output, start) {
2732
3069
  if (parsed.tui === true) args.tui = true;
2733
3070
  if (parsed.tui === false) args.tui = false;
2734
3071
  if (parsed.watch === true) args.watch = true;
3072
+ if (parsed.remote === true) args.remote = true;
2735
3073
  return runCommand(args, global, output, start);
2736
3074
  }
2737
3075
  function dispatchPolicy(parsed, global, output, start) {
@@ -2776,6 +3114,25 @@ function dispatchApply(parsed, global, output, start) {
2776
3114
  if (typeof parsed.file === "string") args.file = parsed.file;
2777
3115
  return applyCommand(args, global, output, start);
2778
3116
  }
3117
+ function dispatchLogin(parsed, global, output, start) {
3118
+ const args = {};
3119
+ if (typeof parsed.hub === "string") args.hub = parsed.hub;
3120
+ if (typeof parsed.token === "string") args.token = parsed.token;
3121
+ return loginCommand(args, global, output, start);
3122
+ }
3123
+ function dispatchHub(parsed, global, output, start) {
3124
+ const args = {};
3125
+ if (typeof parsed.action === "string") args.action = parsed.action;
3126
+ if (typeof parsed.port === "number") args.port = parsed.port;
3127
+ if (typeof parsed.data === "string") args.data = parsed.data;
3128
+ if (typeof parsed.host === "string") args.host = parsed.host;
3129
+ return hubCommand(args, global, output, start);
3130
+ }
3131
+ function dispatchRuns(parsed, global, output, start) {
3132
+ const args = { remote: parsed.remote === true };
3133
+ if (typeof parsed.limit === "number") args.limit = parsed.limit;
3134
+ return runsCommand(args, global, output, start);
3135
+ }
2779
3136
  /**
2780
3137
  * The main CLI entry point.
2781
3138
  * @param argv Command-line arguments (excluding node and script path).