sverka 0.2.14 → 0.2.16

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;;;;;UChCa;EACf,SAAS;;;;;;;wBAgZW,KAAK,gBAAgB,OAAO,WAAW;;;;KCtajD,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;;;;;UChCa;EACf,SAAS;;;;;;;wBAuZW,KAAK,gBAAgB,OAAO,WAAW;;;;KC7ajD,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,11 +3,11 @@ 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, realpathSync, writeFileSync } from "node:fs";
6
+ import { constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
7
  import { 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
- import { collectConstructWarnings, synthesize, validateGraph } from "@sverka/workflow";
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
13
  import { createDockerDriver, createEngine, createHostDriver } from "@sverka/runtime";
@@ -720,6 +720,48 @@ async function graphCommand(global, output, start) {
720
720
  return ExitCode.Success;
721
721
  }
722
722
  //#endregion
723
+ //#region src/internal/implicit.ts
724
+ /** Load the graph `sverka run` should execute: the real config when one
725
+ * exists on disk (or --config names one), else the implicit detected
726
+ * pipeline. */
727
+ async function loadRunGraph(global) {
728
+ if (global.config === null && await findConfig(global.root) === null) return implicitGraph(global.root);
729
+ const { graph, warnings } = await loadProjectGraph(global);
730
+ return {
731
+ graph,
732
+ warnings
733
+ };
734
+ }
735
+ async function implicitGraph(root) {
736
+ const checks = await detectProjectChecks(root);
737
+ if (checks.length === 0) throw new CliError("no config found and no checks detected (run `sverka init` to create one)", "MISSING_ARG", ExitCode.UsageError);
738
+ const project = new Project("default");
739
+ const pipeline = new Pipeline(project, "default");
740
+ const steps = checks.map((check) => new ShellStep(pipeline, check.checkId, shellStepProps(check)));
741
+ const entry = new Entry(pipeline, "run", {
742
+ trigger: manual(),
743
+ roots: steps.map((step) => step.node.id)
744
+ });
745
+ return {
746
+ graph: synthesize(project),
747
+ warnings: collectConstructWarnings(project),
748
+ detected: entry.roots
749
+ };
750
+ }
751
+ /** Detected check → ShellStep props. A SARIF-on-stdout check keeps the
752
+ * `fromStdout` artifact declaration `init --detect` generates, so the
753
+ * implicit run feeds findings collection the same way. */
754
+ function shellStepProps(check) {
755
+ return {
756
+ command: check.command,
757
+ runtime: { mode: "host" },
758
+ ...check.sarifOutput !== void 0 ? { outputs: { [check.sarifOutput]: {
759
+ type: "artifact",
760
+ fromStdout: true
761
+ } } } : {}
762
+ };
763
+ }
764
+ //#endregion
723
765
  //#region src/internal/runtime-check.ts
724
766
  /**
725
767
  * Check if a binary is available on PATH by spawning `<binary> --version`.
@@ -922,7 +964,8 @@ async function runCommand(args, global, output, start) {
922
964
  const fmt = resolveFormats(args, global);
923
965
  output.debug(`run: root=${global.root} executor=${executor} entry=${args.entryId ?? "(first)"} format=${global.format}`);
924
966
  assertExecutorAvailable(executor);
925
- const { graph, warnings } = await loadProjectGraph(global);
967
+ const { graph, warnings, detected } = await loadRunGraph(global);
968
+ if (detected !== void 0) output.errorLine(`no sverka.config — running ${detected.length} detected checks (${detected.join(", ")})`);
926
969
  for (const warning of warnings) output.errorLine(`warning: ${warning}`);
927
970
  const entryId = resolveEntryId(graph, args.entryId);
928
971
  const plan = bindRunPlan({
@@ -947,13 +990,11 @@ async function runCommand(args, global, output, start) {
947
990
  durationMs,
948
991
  evalResult: evaluation.summary,
949
992
  artifactDir,
950
- sinceMs: start
993
+ sinceMs: start,
994
+ ...detected !== void 0 ? { detected } : {},
995
+ ...args.report !== void 0 ? { report: args.report } : {}
951
996
  }, events, output);
952
- if (fmt.isHtml && renderer) {
953
- const reportPath = args.output ?? join(global.root, ".sverka", "report.html");
954
- output.writeLine(`Wrote HTML report to ${reportPath}`);
955
- }
956
- if (renderer && "waitUntilExit" in renderer) await renderer.waitUntilExit();
997
+ await finalizeRenderer(fmt, renderer, args, global, output);
957
998
  if (evaluation.collectionFailed) return evaluation.exitCode;
958
999
  writeRunOutput({
959
1000
  planId: plan.id,
@@ -963,12 +1004,36 @@ async function runCommand(args, global, output, start) {
963
1004
  global,
964
1005
  output,
965
1006
  evalResult: evaluation.summary,
966
- report
1007
+ report,
1008
+ ...detected !== void 0 ? { detected } : {}
967
1009
  });
968
- if (report !== void 0 && global.format === "text") output.writeLine(` report: ${report.html ?? report.json}`);
1010
+ printReportTail(report, args.report, global, output);
969
1011
  if (evaluation.exitCode !== 0) return evaluation.exitCode;
970
1012
  return exitCodeForStatus(runStatus);
971
1013
  }
1014
+ /** Post-run: announce the HTML report path (sarif/web print their own),
1015
+ * then keep interactive renderers mounted until the user quits. */
1016
+ async function finalizeRenderer(fmt, renderer, args, global, output) {
1017
+ if (fmt.isHtml && renderer) {
1018
+ const reportPath = args.output ?? join(global.root, ".sverka", "report.html");
1019
+ output.writeLine(`Wrote HTML report to ${reportPath}`);
1020
+ }
1021
+ if (renderer && "waitUntilExit" in renderer) await renderer.waitUntilExit();
1022
+ }
1023
+ /** Human-mode tail (Spec 53): findings summary, then the report location —
1024
+ * the report is discoverable, not hidden. The `sverka view` hint applies
1025
+ * only when report.html sits at the default path — view resolves
1026
+ * .sverka/runs/<latest>/report.html, not a --report relocation. */
1027
+ function printReportTail(report, reportFlag, global, output) {
1028
+ if (report === void 0 || global.format !== "text") return;
1029
+ output.writeLine(` findings: ${report.findings}`);
1030
+ if (report.html === null) {
1031
+ output.writeLine(` report: ${report.json}`);
1032
+ return;
1033
+ }
1034
+ const hint = reportFlag !== void 0 ? "(open in a browser)" : "(sverka view to open)";
1035
+ output.writeLine(` report: ${report.html} ${hint}`);
1036
+ }
972
1037
  /** Derive output modes — `--format html`, or `--output` without
973
1038
  * sarif/web, implies HTML; evaluation runs for any structured format. */
974
1039
  function resolveFormats(args, global) {
@@ -1018,11 +1083,14 @@ async function writeReportSafe(ctx, events, output) {
1018
1083
  durationMs: ctx.durationMs,
1019
1084
  evalResult: ctx.evalResult,
1020
1085
  artifactDir: ctx.artifactDir,
1021
- sinceMs: ctx.sinceMs
1086
+ sinceMs: ctx.sinceMs,
1087
+ ...ctx.detected !== void 0 ? { detected: ctx.detected } : {},
1088
+ ...ctx.report !== void 0 ? { reportPath: ctx.report } : {}
1022
1089
  });
1023
1090
  return {
1024
1091
  html: artifacts.htmlPath,
1025
- json: join(artifacts.dir, "report.json")
1092
+ json: join(artifacts.dir, "report.json"),
1093
+ findings: artifacts.findingsCount
1026
1094
  };
1027
1095
  } catch (err) {
1028
1096
  if (err instanceof CliError) throw err;
@@ -1049,9 +1117,10 @@ async function runWatch(args, global, output) {
1049
1117
  const configPath = global.config === null ? null : resolve(global.root, global.config);
1050
1118
  const extraPaths = configPath !== null && !configPath.startsWith(`${resolve(global.root)}/`) ? [configPath] : [];
1051
1119
  const ignored = [];
1052
- if (args.output !== void 0) {
1053
- const out = resolve(global.root, args.output);
1054
- if (out.startsWith(`${resolve(global.root)}/`)) ignored.push(out);
1120
+ for (const file of [args.output, args.report]) {
1121
+ if (file === void 0) continue;
1122
+ const abs = resolve(global.root, file);
1123
+ if (abs.startsWith(`${resolve(global.root)}/`)) ignored.push(abs);
1055
1124
  }
1056
1125
  return await watchLoop({
1057
1126
  root: global.root,
@@ -1259,6 +1328,7 @@ function writeRunOutput(opts) {
1259
1328
  planId,
1260
1329
  status: runStatus,
1261
1330
  steps,
1331
+ ...opts.detected !== void 0 ? { detected: opts.detected } : {},
1262
1332
  ...evalResult ? {
1263
1333
  findings: evalResult.findings.length,
1264
1334
  verdict: evalResult.verdict,
@@ -1266,7 +1336,8 @@ function writeRunOutput(opts) {
1266
1336
  } : {},
1267
1337
  ...report !== void 0 ? { report: {
1268
1338
  json: report.json,
1269
- ...report.html !== null ? { html: report.html } : {}
1339
+ ...report.html !== null ? { html: report.html } : {},
1340
+ findings: report.findings
1270
1341
  } } : {}
1271
1342
  },
1272
1343
  durationMs
@@ -1341,26 +1412,27 @@ async function collectRunFindings(opts, warnings) {
1341
1412
  }
1342
1413
  /** Best-effort report.html — a render failure never fails the run; it is
1343
1414
  * recorded in the report's `warnings` instead of being dropped silently. */
1344
- async function writeReportHtml(dir, findings, warnings) {
1415
+ async function writeReportHtml(htmlPath, findings, warnings) {
1345
1416
  try {
1346
1417
  const { generateSarifHtml } = await import("@sverka/sarif-viewer-web");
1347
- const candidate = join(dir, "report.html");
1348
- writeFileSync(candidate, generateSarifHtml(findings), "utf-8");
1349
- return candidate;
1418
+ mkdirSync(dirname(htmlPath), { recursive: true });
1419
+ writeFileSync(htmlPath, generateSarifHtml(findings), "utf-8");
1420
+ return htmlPath;
1350
1421
  } catch (e) {
1351
1422
  warnings.push(`report.html generation failed: ${e instanceof Error ? e.message : String(e)}`);
1352
1423
  return null;
1353
1424
  }
1354
1425
  }
1355
- function runReportPayload(opts, warnings) {
1426
+ function runReportPayload(opts, findingsCount, warnings) {
1356
1427
  return {
1357
1428
  schema: "sverka.run/v1",
1358
1429
  data: {
1359
1430
  planId: opts.planId,
1360
1431
  status: opts.runStatus,
1361
1432
  steps: boundStepOutput(summarizeSteps(opts.events)),
1433
+ ...opts.detected !== void 0 ? { detected: opts.detected } : {},
1434
+ findings: findingsCount,
1362
1435
  ...opts.evalResult ? {
1363
- findings: opts.evalResult.findings.length,
1364
1436
  verdict: opts.evalResult.verdict,
1365
1437
  summary: opts.evalResult.summary
1366
1438
  } : {},
@@ -1375,11 +1447,13 @@ function runReportPayload(opts, warnings) {
1375
1447
  async function writeRunArtifacts(opts) {
1376
1448
  const dir = reportDir(opts.root, opts.runId);
1377
1449
  const warnings = [];
1378
- const htmlPath = await writeReportHtml(dir, await collectRunFindings(opts, warnings), warnings);
1379
- writeFileSync(join(dir, "report.json"), JSON.stringify(runReportPayload(opts, warnings), null, 2), "utf-8");
1450
+ const findings = await collectRunFindings(opts, warnings);
1451
+ const htmlPath = await writeReportHtml(opts.reportPath !== void 0 ? resolveUnderRoot(opts.root, opts.reportPath) : join(dir, "report.html"), findings, warnings);
1452
+ writeFileSync(join(dir, "report.json"), JSON.stringify(runReportPayload(opts, findings.length, warnings), null, 2), "utf-8");
1380
1453
  return {
1381
1454
  dir,
1382
- htmlPath
1455
+ htmlPath,
1456
+ findingsCount: findings.length
1383
1457
  };
1384
1458
  }
1385
1459
  function exitCodeForStatus(runStatus) {
@@ -2007,6 +2081,77 @@ function runCheck(name, binary, required) {
2007
2081
  }
2008
2082
  }
2009
2083
  //#endregion
2084
+ //#region src/internal/open-report.ts
2085
+ /** Newest `.sverka/runs/<runId>/report.html` under root, by file mtime.
2086
+ * Returns undefined when no run report exists. */
2087
+ function findLatestReportHtml(root) {
2088
+ let names;
2089
+ try {
2090
+ names = readdirSync(join(root, ".sverka", "runs"));
2091
+ } catch {
2092
+ return;
2093
+ }
2094
+ let best;
2095
+ for (const name of names) {
2096
+ const candidate = join(root, ".sverka", "runs", name, "report.html");
2097
+ let stat;
2098
+ try {
2099
+ stat = statSync(candidate);
2100
+ } catch (e) {
2101
+ const code = e.code;
2102
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
2103
+ throw e;
2104
+ }
2105
+ if (stat.isFile() && (best === void 0 || stat.mtimeMs > best.mtimeMs)) best = {
2106
+ path: candidate,
2107
+ mtimeMs: stat.mtimeMs
2108
+ };
2109
+ }
2110
+ return best?.path;
2111
+ }
2112
+ /** WSL exposes Windows browsers through wslu, not xdg-open. */
2113
+ function isWsl() {
2114
+ if (process$1.platform !== "linux") return false;
2115
+ try {
2116
+ return readFileSync("/proc/sys/kernel/osrelease", "utf8").toLowerCase().includes("microsoft");
2117
+ } catch {
2118
+ return false;
2119
+ }
2120
+ }
2121
+ function openerCandidates(path) {
2122
+ if (process$1.platform === "darwin") return [["open", [path]]];
2123
+ if (process$1.platform === "win32") return [[
2124
+ "cmd",
2125
+ [
2126
+ "/c",
2127
+ "start",
2128
+ "\"\"",
2129
+ "\"%SVERKA_VIEW_TARGET%\""
2130
+ ],
2131
+ {
2132
+ windowsVerbatimArguments: true,
2133
+ env: {
2134
+ ...process$1.env,
2135
+ SVERKA_VIEW_TARGET: path
2136
+ }
2137
+ }
2138
+ ]];
2139
+ return isWsl() ? [["wslview", [path]], ["xdg-open", [path]]] : [["xdg-open", [path]]];
2140
+ }
2141
+ /** Open a file with the platform's default handler. Returns false when no
2142
+ * opener binary exists or every attempt failed — callers print the path. */
2143
+ function openReportInBrowser(path) {
2144
+ for (const [cmd, args, options] of openerCandidates(path)) {
2145
+ const res = spawnSync(cmd, args, {
2146
+ stdio: "ignore",
2147
+ timeout: 5e3,
2148
+ ...options
2149
+ });
2150
+ if (res.error === void 0 && res.status === 0) return true;
2151
+ }
2152
+ return false;
2153
+ }
2154
+ //#endregion
2010
2155
  //#region src/commands/view.ts
2011
2156
  /**
2012
2157
  * View SARIF findings in the terminal TUI or generate an HTML report.
@@ -2024,10 +2169,7 @@ async function viewCommand(args, global, output, start) {
2024
2169
  output.errorLine("sverka view: stdin is empty.");
2025
2170
  return ExitCode.RuntimeError;
2026
2171
  }
2027
- } else {
2028
- output.errorLine("sverka view: no input. Provide a SARIF file argument or pipe SARIF via stdin.");
2029
- return ExitCode.UsageError;
2030
- }
2172
+ } else return openLatestReport(global, output);
2031
2173
  let sarif;
2032
2174
  try {
2033
2175
  sarif = JSON.parse(sarifJson);
@@ -2038,6 +2180,18 @@ async function viewCommand(args, global, output, start) {
2038
2180
  if (args.format === "web") return renderWeb(sarif, args, output);
2039
2181
  return renderTui(sarif, output);
2040
2182
  }
2183
+ /** Interactive with no input — open the latest run report (Spec 53:
2184
+ * `sverka run` tails with `report: <path> (sverka view to open)`). */
2185
+ function openLatestReport(global, output) {
2186
+ const report = findLatestReportHtml(global.root);
2187
+ if (report === void 0) {
2188
+ output.errorLine("sverka view: no input. Provide a SARIF file argument, pipe SARIF via stdin, or run `sverka run` first — no run report found under .sverka/runs/.");
2189
+ return ExitCode.UsageError;
2190
+ }
2191
+ output.writeLine(`report: ${report}`);
2192
+ if (!openReportInBrowser(report)) output.errorLine("sverka view: could not open a browser — open the report path above manually.");
2193
+ return ExitCode.Success;
2194
+ }
2041
2195
  /** Launch the TUI viewer using @sverka/sarif-viewer-tui. */
2042
2196
  async function renderTui(sarif, output) {
2043
2197
  try {
@@ -2148,6 +2302,9 @@ function addRunCommand(y) {
2148
2302
  type: "string",
2149
2303
  alias: "o",
2150
2304
  describe: "Output file path for HTML report (implies --format html)"
2305
+ }).option("report", {
2306
+ type: "string",
2307
+ describe: "Relocate the per-run HTML report (report.json stays under .sverka/runs/)"
2151
2308
  }).option("tui", {
2152
2309
  type: "boolean",
2153
2310
  describe: "Interactive terminal UI (off by default; opt-in only)"
@@ -2206,7 +2363,7 @@ function addCompileCommand(y) {
2206
2363
  function addViewCommand(y) {
2207
2364
  return y.positional("file", {
2208
2365
  type: "string",
2209
- describe: "Path to a .sarif file (reads from stdin if omitted)"
2366
+ describe: "Path to a .sarif file (piped stdin when omitted; bare `view` opens the latest run report)"
2210
2367
  }).option("format", {
2211
2368
  type: "string",
2212
2369
  alias: "f",
@@ -2261,7 +2418,7 @@ function buildParser() {
2261
2418
  type: "boolean",
2262
2419
  alias: "v",
2263
2420
  default: false
2264
- }).command("init", "Create a sverka.config.ts", addInitCommand).command("validate", "Validate a sverka config").command("plan", "Bind Entry + inputs → Run Plan", addPlanCommand).command("graph", "Display the Definition Graph").command("run", "Execute the workflow locally", addRunCommand).command("discover", "Discover and display project context").command("check", "Resolve proposed checks → StepDefinitions").command("policy", "Evaluate findings against policy", addPolicyCommand).command("synth", "Alias for compile — compile to a target CI YAML", addSynthCommand).command("compile", "Compile to a target CI YAML", addCompileCommand).command("mcp-server", "Expose Sverka as an MCP server (stdio)").command("doctor", "Diagnose environment and dependencies").command("view [file]", "View SARIF findings in TUI or generate HTML report", addViewCommand).command("ui", "Start local web dashboard server", addUiCommand).demandCommand(1, "No command given").strict().fail((msg, err) => {
2421
+ }).command("init", "Create a sverka.config.ts", addInitCommand).command("validate", "Validate a sverka config").command("plan", "Bind Entry + inputs → Run Plan", addPlanCommand).command("graph", "Display the Definition Graph").command("run", "Execute the workflow locally", addRunCommand).command("discover", "Discover and display project context").command("check", "Resolve proposed checks → StepDefinitions").command("policy", "Evaluate findings against policy", addPolicyCommand).command("synth", "Alias for compile — compile to a target CI YAML", addSynthCommand).command("compile", "Compile to a target CI YAML", addCompileCommand).command("mcp-server", "Expose Sverka as an MCP server (stdio)").command("doctor", "Diagnose environment and dependencies").command("view [file]", "Open the latest run report, or view SARIF findings (TUI/HTML)", addViewCommand).command("ui", "Start local web dashboard server", addUiCommand).demandCommand(1, "No command given").strict().fail((msg, err) => {
2265
2422
  if (err) throw err;
2266
2423
  throw new CliError(msg, "UNKNOWN_COMMAND", ExitCode.UsageError);
2267
2424
  });
@@ -2315,6 +2472,7 @@ function dispatchRun(parsed, global, output, start) {
2315
2472
  }
2316
2473
  if (typeof parsed.entry === "string") args.entryId = parsed.entry;
2317
2474
  if (typeof parsed.output === "string") args.output = parsed.output;
2475
+ if (typeof parsed.report === "string") args.report = parsed.report;
2318
2476
  if (parsed.tui === true) args.tui = true;
2319
2477
  if (parsed.tui === false) args.tui = false;
2320
2478
  if (parsed.watch === true) args.watch = true;