sverka 0.0.0 → 0.2.3

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/dist/index.mjs ADDED
@@ -0,0 +1,2038 @@
1
+ import { t as missingBuildHint } from "./errors-DHioC6LB.mjs";
2
+ import * as nodeModule from "node:module";
3
+ import { register } from "node:module";
4
+ import process$1 from "node:process";
5
+ import yargs from "yargs";
6
+ import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { mkdir, open, readFile, realpath, writeFile } from "node:fs/promises";
10
+ import { collectConstructWarnings, synthesize, validateGraph } from "@sverka/workflow";
11
+ import { bindRunPlan, createPlanner } from "@sverka/sdk";
12
+ import { DEFAULT_POLICY, createBuiltinResolver, evaluatePolicy, loadBaseline, normalizeSarif, serializeSarif, synthesizeCheckSteps } from "@sverka/verification";
13
+ import { createDockerDriver, createEngine, createHostDriver } from "@sverka/runtime";
14
+ import { spawnSync } from "node:child_process";
15
+ import { compileGithub, compileGitlab } from "@sverka/compiler";
16
+ import { z } from "zod";
17
+ //#region src/types.ts
18
+ /** Exit codes used by the CLI. */
19
+ const ExitCode = {
20
+ Success: 0,
21
+ PolicyFail: 1,
22
+ UsageError: 2,
23
+ RuntimeError: 3
24
+ };
25
+ /** Error thrown when CLI argument parsing or command execution fails. */
26
+ var CliError = class extends Error {
27
+ code;
28
+ exitCode;
29
+ cause;
30
+ constructor(message, code, exitCode, cause) {
31
+ super(message);
32
+ this.name = "CliError";
33
+ this.code = code;
34
+ this.exitCode = exitCode;
35
+ this.cause = cause;
36
+ }
37
+ };
38
+ //#endregion
39
+ //#region src/output.ts
40
+ /**
41
+ * OutputWriter that writes to caller-provided sinks (stdout/stderr).
42
+ * In production, the sinks write to process.stdout/process.stderr.
43
+ * In tests, the sinks capture into arrays.
44
+ */
45
+ var ConsoleOutputWriter = class {
46
+ #out;
47
+ #err;
48
+ #quiet;
49
+ #verbose;
50
+ constructor(out, err, quiet = false, verbose = false) {
51
+ this.#out = out;
52
+ this.#err = err;
53
+ this.#quiet = quiet;
54
+ this.#verbose = verbose;
55
+ }
56
+ write(text) {
57
+ if (this.#quiet) return;
58
+ this.#out(text);
59
+ }
60
+ writeLine(text) {
61
+ if (this.#quiet) return;
62
+ this.#out(text + "\n");
63
+ }
64
+ error(text) {
65
+ this.#err(text);
66
+ }
67
+ errorLine(text) {
68
+ this.#err(text + "\n");
69
+ }
70
+ /** Write a debug line to stderr, only when --verbose is set. */
71
+ debug(text) {
72
+ if (this.#verbose) this.#err(text + "\n");
73
+ }
74
+ };
75
+ /**
76
+ * Wrapper that applies --quiet / --verbose / --format semantics to any
77
+ * underlying OutputWriter. Used to make injected test writers respect the
78
+ * same suppression rules as the production writer.
79
+ *
80
+ * - `--quiet` (text format) suppresses stdout writes; JSON stdout is kept.
81
+ * - `--verbose` routes `debug()` to the underlying `errorLine()`.
82
+ * - stderr is never suppressed.
83
+ */
84
+ var FlagAwareWriter = class {
85
+ #base;
86
+ #quiet;
87
+ #verbose;
88
+ constructor(base, quiet, verbose) {
89
+ this.#base = base;
90
+ this.#quiet = quiet;
91
+ this.#verbose = verbose;
92
+ }
93
+ write(text) {
94
+ if (this.#quiet) return;
95
+ this.#base.write(text);
96
+ }
97
+ writeLine(text) {
98
+ if (this.#quiet) return;
99
+ this.#base.writeLine(text);
100
+ }
101
+ error(text) {
102
+ this.#base.error(text);
103
+ }
104
+ errorLine(text) {
105
+ this.#base.errorLine(text);
106
+ }
107
+ debug(text) {
108
+ if (this.#verbose) this.#base.errorLine(text);
109
+ }
110
+ };
111
+ /**
112
+ * Create an OutputWriter from global flags. In JSON format, stdout is NOT
113
+ * suppressed by --quiet (the JSON result is the data and must be emitted).
114
+ */
115
+ function createOutputWriter(global, out, err) {
116
+ return new ConsoleOutputWriter(out, err, global.quiet && global.format === "text", global.verbose);
117
+ }
118
+ /**
119
+ * Wrap any OutputWriter with --quiet / --verbose semantics from global flags.
120
+ * Use for injected test writers so they respect the same suppression rules.
121
+ */
122
+ function wrapOutputWriter(global, base) {
123
+ return new FlagAwareWriter(base, global.quiet && global.format === "text", global.verbose);
124
+ }
125
+ //#endregion
126
+ //#region src/internal/version.ts
127
+ /**
128
+ * Resolve the sverka CLI version from its own package.json.
129
+ * Walks up from this module so it works from src/ (dev) and bundled
130
+ * dist/ alike, and never picks up the consumer's cwd package.json
131
+ * (which is what yargs' default --version resolution does).
132
+ */
133
+ function cliVersion() {
134
+ let dir = dirname(fileURLToPath(import.meta.url));
135
+ while (true) {
136
+ try {
137
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
138
+ if (pkg.name === "sverka" && pkg.version) return pkg.version;
139
+ } catch {}
140
+ const parent = dirname(dir);
141
+ if (parent === dir) return "0.0.0";
142
+ dir = parent;
143
+ }
144
+ }
145
+ //#endregion
146
+ //#region src/internal/paths.ts
147
+ /**
148
+ * Resolve a path against a root directory. Absolute paths are returned
149
+ * unchanged; relative paths are joined under `root` so that `--root` governs
150
+ * path resolution regardless of the process working directory.
151
+ */
152
+ function resolveUnderRoot(root, path) {
153
+ return isAbsolute(path) ? path : join(root, path);
154
+ }
155
+ //#endregion
156
+ //#region src/internal/config.ts
157
+ let hooksRegistered = false;
158
+ function findHooksFile() {
159
+ for (const rel of ["./config-hooks.mjs", "./internal/config-hooks.mjs"]) try {
160
+ const candidate = fileURLToPath(new URL(rel, import.meta.url));
161
+ if (existsSync(candidate)) return candidate;
162
+ } catch {}
163
+ return null;
164
+ }
165
+ /**
166
+ * Register ESM resolve hooks so a config file that cannot resolve @sverka/*
167
+ * packages from its own location falls back to the copies bundled with the
168
+ * CLI. This makes a globally-installed `sverka` self-contained.
169
+ */
170
+ async function registerConfigHooks() {
171
+ if (hooksRegistered) return;
172
+ const hooksFile = findHooksFile();
173
+ if (hooksFile === null) return;
174
+ const href = pathToFileURL(hooksFile).href;
175
+ try {
176
+ const registerHooks = nodeModule.registerHooks;
177
+ if (typeof registerHooks === "function") {
178
+ const hooks = await import(href);
179
+ if (typeof hooks.resolve === "function") {
180
+ registerHooks.call(nodeModule, { resolve: hooks.resolve });
181
+ hooksRegistered = true;
182
+ return;
183
+ }
184
+ }
185
+ register(href);
186
+ hooksRegistered = true;
187
+ } catch {}
188
+ }
189
+ /**
190
+ * Find a sverka.config.ts or sverka.config.js file in the given root.
191
+ * Returns the absolute path or null if not found.
192
+ */
193
+ async function findConfig(root) {
194
+ for (const candidate of [
195
+ "sverka.config.ts",
196
+ "sverka.config.js",
197
+ "sverka.config.mjs"
198
+ ]) {
199
+ const path = join(root, candidate);
200
+ if (existsSync(path)) return path;
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * Resolve the effective config path from global flags.
206
+ * Throws a usage error when no config can be found.
207
+ */
208
+ async function resolveConfigPath$1(global) {
209
+ let configPath = global.config ? resolveUnderRoot(global.root, global.config) : null;
210
+ if (!configPath) configPath = await findConfig(global.root);
211
+ if (!configPath) throw new CliError("no config found (use --config to specify a path)", "MISSING_ARG", ExitCode.UsageError);
212
+ return configPath;
213
+ }
214
+ /**
215
+ * Structural check that avoids `instanceof` across package boundaries.
216
+ * A construct-like object (Project or bare Pipeline — single-pipeline
217
+ * configs may skip the Project) has a `node` with an `id` string and a
218
+ * `children` array.
219
+ */
220
+ function notConstructError() {
221
+ return new CliError("config must export a Project or Pipeline instance (default or named 'project')", "SDK_ERROR", ExitCode.RuntimeError);
222
+ }
223
+ function assertConstructLike(value) {
224
+ if (value === null || typeof value !== "object") throw notConstructError();
225
+ const node = value.node;
226
+ if (typeof node !== "object" || node === null || typeof node.id !== "string" || !Array.isArray(node.children) || typeof node.findAll !== "function") throw notConstructError();
227
+ }
228
+ /**
229
+ * Load a sverka config file and return the Project construct.
230
+ * The config file must export a `default` Project-like instance or a named
231
+ * `project` Project-like instance.
232
+ *
233
+ * @throws CliError if the config cannot be loaded or doesn't export a Project.
234
+ */
235
+ async function loadConfig(configPath) {
236
+ const absPath = isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);
237
+ if (!existsSync(absPath)) throw new CliError(`config not found: ${absPath}`, "MISSING_ARG", ExitCode.UsageError);
238
+ let mod;
239
+ try {
240
+ await registerConfigHooks();
241
+ mod = await import(pathToFileURL(absPath).href);
242
+ } catch (e) {
243
+ throw new CliError(`failed to load config: ${e instanceof Error ? e.message : String(e)}`, "SDK_ERROR", ExitCode.RuntimeError, e);
244
+ }
245
+ const construct = mod.project ?? mod.default;
246
+ assertConstructLike(construct);
247
+ return construct;
248
+ }
249
+ /**
250
+ * Resolve, load, and synthesize the config for commands that need a graph.
251
+ */
252
+ async function loadProjectGraph(global) {
253
+ const configPath = await resolveConfigPath$1(global);
254
+ const project = await loadConfig(configPath);
255
+ const warnings = collectConstructWarnings(project);
256
+ return {
257
+ configPath,
258
+ project,
259
+ graph: synthesize(project),
260
+ warnings
261
+ };
262
+ }
263
+ /**
264
+ * Detect the package manager used by the project at `root`.
265
+ * Falls back to `npm` when no lockfile or packageManager field is found.
266
+ */
267
+ function detectPackageManager$1(root) {
268
+ const fromLockfile = detectFromLockfile(root);
269
+ if (fromLockfile) return fromLockfile;
270
+ const fromPkg = detectFromPackageJson(root);
271
+ if (fromPkg) return fromPkg;
272
+ return "npm";
273
+ }
274
+ function detectFromLockfile(root) {
275
+ if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun";
276
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
277
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
278
+ if (existsSync(join(root, "package-lock.json"))) return "npm";
279
+ }
280
+ function detectFromPackageJson(root) {
281
+ const pkgPath = join(root, "package.json");
282
+ if (!existsSync(pkgPath)) return void 0;
283
+ try {
284
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
285
+ if (typeof pkg.packageManager === "string") return pmFromPackageManagerField(pkg.packageManager);
286
+ } catch {}
287
+ }
288
+ function pmFromPackageManagerField(packageManager) {
289
+ if (packageManager.startsWith("npm")) return "npm";
290
+ if (packageManager.startsWith("pnpm")) return "pnpm";
291
+ if (packageManager.startsWith("yarn")) return "yarn";
292
+ if (packageManager.startsWith("bun")) return "bun";
293
+ }
294
+ /**
295
+ * Ensure `@sverka/workflow` is declared as a devDependency of the target
296
+ * project. Creates a minimal package.json when one does not exist.
297
+ */
298
+ async function ensureConstructsDependency(root) {
299
+ const pkgPath = join(root, "package.json");
300
+ const base = await loadPackageBase(pkgPath);
301
+ const pkg = {
302
+ name: base.name ?? "sverka-project",
303
+ version: base.version ?? "0.0.0",
304
+ ...base
305
+ };
306
+ const declared = ensureConstructsDeclared(pkg, root);
307
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
308
+ return declared;
309
+ }
310
+ async function loadPackageBase(pkgPath) {
311
+ try {
312
+ return JSON.parse(await readFile(pkgPath, "utf8"));
313
+ } catch (e) {
314
+ if (e.code === "ENOENT") return {
315
+ name: "sverka-project",
316
+ version: "0.0.0"
317
+ };
318
+ throw new CliError(`failed to read package.json: ${e instanceof Error ? e.message : String(e)}`, "PACKAGE_ERROR", ExitCode.RuntimeError, e);
319
+ }
320
+ }
321
+ /** Dependency names that were renamed to @sverka/workflow. */
322
+ const LEGACY_DEP_NAMES = ["@sverka/constructs", "@sverka/cdk"];
323
+ /** Drop renamed legacy package entries from dependencies/devDependencies. */
324
+ function removeLegacyDeps(pkg) {
325
+ for (const key of ["dependencies", "devDependencies"]) {
326
+ const deps = pkg[key];
327
+ if (deps === void 0) continue;
328
+ for (const name of LEGACY_DEP_NAMES) delete deps[name];
329
+ if (Object.keys(deps).length === 0) delete pkg[key];
330
+ }
331
+ }
332
+ function ensureConstructsDeclared(pkg, root) {
333
+ removeLegacyDeps(pkg);
334
+ const deps = pkg.dependencies ?? {};
335
+ const devDeps = pkg.devDependencies ?? {};
336
+ const existing = deps["@sverka/workflow"] ?? devDeps["@sverka/workflow"];
337
+ if (typeof existing === "string") return existing;
338
+ if (existing !== void 0) {
339
+ delete deps["@sverka/workflow"];
340
+ delete devDeps["@sverka/workflow"];
341
+ }
342
+ const spec = constructsDepSpec(root);
343
+ if (spec === null) return null;
344
+ pkg.devDependencies = {
345
+ ...devDeps,
346
+ "@sverka/workflow": spec
347
+ };
348
+ return spec;
349
+ }
350
+ /**
351
+ * Dependency spec for @sverka/workflow in the target project.
352
+ *
353
+ * - `workspace:*` inside the sverka monorepo itself.
354
+ * - `^<version>` when the package resolves from a node_modules tree (a real
355
+ * registry install).
356
+ * - `link:<dir>` (or `file:<dir>` for npm, which rejects the `link:`
357
+ * protocol) when it resolves to a bare checkout — a semver spec would
358
+ * fail to install when the version was never published.
359
+ * - null when the package cannot be resolved at all — nothing is declared,
360
+ * the caller warns instead of writing an uninstallable spec.
361
+ */
362
+ function constructsDepSpec(root) {
363
+ if (isLocalWorkspace(root)) return "workspace:*";
364
+ const resolved = resolveConstructsPackage();
365
+ if (resolved === null) return null;
366
+ if (resolved.dir.includes(`${sep}node_modules${sep}`)) return `^${resolved.version}`;
367
+ return `${detectPackageManager$1(root) === "npm" ? "file:" : "link:"}${resolved.dir}`;
368
+ }
369
+ /** Resolve the @sverka/workflow package directory and version from the CLI's context. */
370
+ function resolveConstructsPackage() {
371
+ let entry;
372
+ try {
373
+ entry = fileURLToPath(import.meta.resolve("@sverka/workflow"));
374
+ } catch {
375
+ return null;
376
+ }
377
+ let dir = dirname(entry);
378
+ while (true) {
379
+ const pkgPath = join(dir, "package.json");
380
+ if (existsSync(pkgPath)) try {
381
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
382
+ if (pkg.name === "@sverka/workflow") return {
383
+ dir,
384
+ version: pkg.version ?? "0.0.0"
385
+ };
386
+ } catch {}
387
+ const parent = dirname(dir);
388
+ if (parent === dir) return null;
389
+ dir = parent;
390
+ }
391
+ }
392
+ function isLocalWorkspace(root) {
393
+ const rootPkg = join(root, "package.json");
394
+ const constructsPkg = join(root, "packages", "workflow", "package.json");
395
+ try {
396
+ const rootData = JSON.parse(readFileSync(rootPkg, "utf8"));
397
+ if (JSON.parse(readFileSync(constructsPkg, "utf8")).name !== "@sverka/workflow") return false;
398
+ return (Array.isArray(rootData.workspaces) ? rootData.workspaces : rootData.workspaces?.packages ?? []).some((pattern) => pattern.startsWith("packages"));
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+ //#endregion
404
+ //#region src/internal/detect.ts
405
+ /** package.json script names that map to pipeline steps. */
406
+ const DETECT_SCRIPT_CHECKS = [
407
+ "lint",
408
+ "typecheck",
409
+ "test",
410
+ "build",
411
+ "format",
412
+ "check"
413
+ ];
414
+ /**
415
+ * Read package.json scripts under root. Returns an empty map when absent or
416
+ * malformed. Ground truth for JS/TS projects.
417
+ */
418
+ function readPackageScripts(root) {
419
+ try {
420
+ const scripts = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).scripts ?? {};
421
+ const out = {};
422
+ for (const [name, value] of Object.entries(scripts)) if (typeof value === "string" && value.length > 0) out[name] = value;
423
+ return out;
424
+ } catch {
425
+ return {};
426
+ }
427
+ }
428
+ /**
429
+ * True when a command is a package-manager script invocation
430
+ * (`<pm> run <script>`) whose script does not exist — a phantom step that
431
+ * would fail with "Missing script" on first run.
432
+ */
433
+ function isPhantomScriptStep(command, scripts) {
434
+ const m = /^(?:bun|npm|pnpm|yarn|deno)\s+run\s+([A-Za-z0-9:_-]+)/.exec(command.trim());
435
+ if (m === null) return false;
436
+ return !(m[1] in scripts);
437
+ }
438
+ /**
439
+ * Detect checks for the project at `root`: planner proposals (filtered to
440
+ * real scripts) plus any known check scripts the planner did not cover.
441
+ */
442
+ async function detectProjectChecks(root) {
443
+ const pm = detectPackageManager$1(root);
444
+ const scripts = readPackageScripts(root);
445
+ const checks = [];
446
+ try {
447
+ const planner = createPlanner();
448
+ const ctx = await planner.discover({
449
+ root,
450
+ scopeToRoot: true
451
+ });
452
+ const proposal = await planner.plan(ctx);
453
+ if (proposal.checks.length > 0) {
454
+ const resolver = createBuiltinResolver();
455
+ const resolved = synthesizeCheckSteps(proposal.checks, ctx, resolver);
456
+ for (const r of resolved) {
457
+ const shellOp = r.step.operations.find((o) => o.kind === "shell");
458
+ const command = shellOp?.kind === "shell" ? shellOp.command : "";
459
+ if (isPhantomScriptStep(command, scripts)) continue;
460
+ const sarifOut = r.outputs.find((o) => o.format === "sarif");
461
+ checks.push({
462
+ checkId: r.checkId,
463
+ command,
464
+ ...sarifOut !== void 0 ? { sarifOutput: sarifOut.path } : {},
465
+ source: "planner"
466
+ });
467
+ }
468
+ }
469
+ } catch {}
470
+ for (const name of DETECT_SCRIPT_CHECKS) {
471
+ if (!(name in scripts)) continue;
472
+ if (checks.some((c) => c.checkId === name)) continue;
473
+ checks.push({
474
+ checkId: name,
475
+ command: `${pm} run ${name}`,
476
+ source: "script"
477
+ });
478
+ }
479
+ return checks;
480
+ }
481
+ //#endregion
482
+ //#region src/commands/init.ts
483
+ function buildMinimalTemplate(pm) {
484
+ return [
485
+ "import { Project, Pipeline, ShellStep, Entry } from \"@sverka/workflow\";",
486
+ "",
487
+ "const proj = new Project(\"verify\");",
488
+ "const ci = new Pipeline(proj, \"ci\");",
489
+ `new ShellStep(ci, "lint", { command: "${pm} run lint" });`,
490
+ `new ShellStep(ci, "typecheck", { command: "${pm} run typecheck" });`,
491
+ `new ShellStep(ci, "test", { command: "${pm} run test" });`,
492
+ "new Entry(ci, \"on-push\", { trigger: { kind: \"push\" }, roots: [\"lint\", \"typecheck\", \"test\"] });",
493
+ "",
494
+ "export default proj;",
495
+ ""
496
+ ].join("\n");
497
+ }
498
+ function buildFullTemplate(pm) {
499
+ return [
500
+ "import { Project, Pipeline, ShellStep, Entry } from \"@sverka/workflow\";",
501
+ "",
502
+ "const proj = new Project(\"verify\");",
503
+ "const ci = new Pipeline(proj, \"ci\");",
504
+ `new ShellStep(ci, "lint", { command: "${pm} run lint" });`,
505
+ `new ShellStep(ci, "typecheck", { command: "${pm} run typecheck" });`,
506
+ `new ShellStep(ci, "test", { command: "${pm} run test" });`,
507
+ `new ShellStep(ci, "build", { command: "${pm} run build" });`,
508
+ "new Entry(ci, \"on-push\", { trigger: { kind: \"push\" }, roots: [\"lint\", \"typecheck\", \"test\", \"build\"] });",
509
+ "",
510
+ "export default proj;",
511
+ ""
512
+ ].join("\n");
513
+ }
514
+ /** Build a config from detected checks (planner + package.json scripts). */
515
+ async function buildDetectedTemplate(root) {
516
+ const checks = await detectProjectChecks(root);
517
+ if (checks.length === 0) return null;
518
+ const stepLines = checks.map((c) => {
519
+ const id = JSON.stringify(c.checkId);
520
+ const cmd = JSON.stringify(c.command);
521
+ if (c.sarifOutput !== void 0) return `new ShellStep(ci, ${id}, { command: ${cmd}, outputs: ${"{ " + JSON.stringify(c.sarifOutput) + ": { type: \"artifact\", fromStdout: true } }"} });`;
522
+ return `new ShellStep(ci, ${id}, { command: ${cmd} });`;
523
+ });
524
+ const rootList = checks.map((c) => JSON.stringify(c.checkId)).join(", ");
525
+ return [
526
+ "import { Project, Pipeline, ShellStep, Entry, push } from \"@sverka/workflow\";",
527
+ "",
528
+ "const proj = new Project(\"verify\");",
529
+ "const ci = new Pipeline(proj, \"ci\");",
530
+ ...stepLines,
531
+ `new Entry(ci, "on-push", { trigger: push(), roots: [${rootList}] });`,
532
+ "",
533
+ "export default proj;",
534
+ ""
535
+ ].join("\n");
536
+ }
537
+ /** Resolve and validate the template name, returning its file content. */
538
+ function resolveTemplateContent(template, pm) {
539
+ if (template !== "minimal" && template !== "full") throw new CliError(`invalid template: ${template} (expected minimal|full)`, "INVALID_FLAG", ExitCode.UsageError);
540
+ return template === "full" ? buildFullTemplate(pm) : buildMinimalTemplate(pm);
541
+ }
542
+ /** Write the config file, creating parent directories and using exclusive create when not forcing. */
543
+ async function writeConfig(configPath, content, force) {
544
+ if (existsSync(configPath) && !force) throw new CliError(`config already exists: ${configPath} (use --force to overwrite)`, "CONFIG_EXISTS", ExitCode.UsageError);
545
+ const flags = force ? "utf8" : {
546
+ encoding: "utf8",
547
+ flag: "wx"
548
+ };
549
+ try {
550
+ await mkdir(dirname(configPath), { recursive: true });
551
+ await writeFile(configPath, content, flags);
552
+ } catch (e) {
553
+ if (e instanceof Error && "code" in e && e.code === "EEXIST") throw new CliError(`config already exists: ${configPath} (use --force to overwrite)`, "CONFIG_EXISTS", ExitCode.UsageError, e);
554
+ throw e;
555
+ }
556
+ }
557
+ /** Emit the init result in the requested output format. */
558
+ function emitInitResult(output, format, configPath, template, start) {
559
+ if (format === "json") output.writeLine(JSON.stringify({
560
+ command: "init",
561
+ data: {
562
+ path: configPath,
563
+ template
564
+ },
565
+ durationMs: Date.now() - start
566
+ }));
567
+ else output.writeLine(`Created ${configPath} (template: ${template})`);
568
+ }
569
+ /** Resolve the config path relative to root, honoring an explicit --config. */
570
+ function resolveConfigPath(root, config) {
571
+ const selected = config ?? "sverka.config.ts";
572
+ return isAbsolute(selected) ? selected : join(root, selected);
573
+ }
574
+ /**
575
+ * Create a sverka.config.ts in the root directory.
576
+ */
577
+ async function initCommand(args, global, output, start) {
578
+ const configPath = resolveConfigPath(global.root, global.config);
579
+ if (existsSync(configPath) && !args.force) throw new CliError(`config already exists: ${configPath} (use --force to overwrite)`, "CONFIG_EXISTS", ExitCode.UsageError);
580
+ let content;
581
+ let template;
582
+ if (args.detect) {
583
+ await mkdir(global.root, { recursive: true });
584
+ output.debug(`init: root=${global.root} detect=true force=${Boolean(args.force)}`);
585
+ const detected = await buildDetectedTemplate(global.root);
586
+ if (detected !== null) {
587
+ content = detected;
588
+ template = "detect";
589
+ } else {
590
+ output.debug("init: detection found no checks, falling back to minimal template");
591
+ content = buildMinimalTemplate(detectPackageManager$1(global.root));
592
+ template = "minimal";
593
+ }
594
+ } else {
595
+ template = args.template ?? "minimal";
596
+ const pm = detectPackageManager$1(global.root);
597
+ output.debug(`init: root=${global.root} template=${template} pm=${pm} force=${Boolean(args.force)}`);
598
+ content = resolveTemplateContent(template, pm);
599
+ await mkdir(global.root, { recursive: true });
600
+ }
601
+ if (await ensureConstructsDependency(global.root) === null) output.errorLine("note: could not resolve @sverka/workflow — the generated config imports it; install it (e.g. bun add -d @sverka/workflow) before running sverka");
602
+ await writeConfig(configPath, content, Boolean(args.force));
603
+ emitInitResult(output, global.format, configPath, template, start);
604
+ return ExitCode.Success;
605
+ }
606
+ //#endregion
607
+ //#region src/commands/validate.ts
608
+ /**
609
+ * Validate a sverka config: synthesize the Definition Graph and run validators.
610
+ * Exit 0 if valid, 2 if missing config, 3 if load/synthesis fails.
611
+ */
612
+ async function validateCommand(global, output, start) {
613
+ output.debug(`validate: root=${global.root} config=${global.config ?? "(auto)"}`);
614
+ const { configPath, graph, warnings } = await loadProjectGraph(global);
615
+ validateGraph(graph);
616
+ const durationMs = Date.now() - start;
617
+ if (global.format === "json") output.writeLine(JSON.stringify({
618
+ command: "validate",
619
+ data: {
620
+ path: configPath,
621
+ valid: true,
622
+ pipelines: graph.project.pipelines.length,
623
+ warnings
624
+ },
625
+ durationMs
626
+ }));
627
+ else {
628
+ for (const warning of warnings) output.errorLine(`warning: ${warning}`);
629
+ output.writeLine(`Config valid: ${configPath}`);
630
+ output.writeLine(` pipelines: ${graph.project.pipelines.length}`);
631
+ for (const pipeline of graph.project.pipelines) output.writeLine(` ${pipeline.id}: ${pipeline.steps.length} steps, ${pipeline.entries.length} entries`);
632
+ }
633
+ return ExitCode.Success;
634
+ }
635
+ //#endregion
636
+ //#region src/internal/graph.ts
637
+ /**
638
+ * Return the id of the first entry found across all pipelines.
639
+ * Returns `undefined` when the graph contains no entries.
640
+ */
641
+ function resolveDefaultEntryId(graph) {
642
+ for (const pipeline of graph.project.pipelines) {
643
+ const entry = pipeline.entries[0];
644
+ if (entry) return entry.id;
645
+ }
646
+ }
647
+ /**
648
+ * Return whether an entry with the given id exists in the graph.
649
+ */
650
+ function entryExists(graph, entryId) {
651
+ for (const pipeline of graph.project.pipelines) for (const entry of pipeline.entries) if (entry.id === entryId) return true;
652
+ return false;
653
+ }
654
+ //#endregion
655
+ //#region src/commands/plan.ts
656
+ /**
657
+ * Bind an Entry and Inputs into a Run Plan and display it.
658
+ */
659
+ async function planCommand(args, global, output, start) {
660
+ output.debug(`plan: root=${global.root} entry=${args.entryId ?? "(first)"}`);
661
+ const { graph } = await loadProjectGraph(global);
662
+ const entryId = args.entryId ?? resolveDefaultEntryId(graph);
663
+ if (!entryId) throw new CliError("no entries in graph", "MISSING_ARG", ExitCode.UsageError);
664
+ if (!entryExists(graph, entryId)) throw new CliError(`entry "${entryId}" not found in graph`, "MISSING_ARG", ExitCode.UsageError);
665
+ const plan = bindRunPlan({
666
+ graph,
667
+ entryId
668
+ });
669
+ const durationMs = Date.now() - start;
670
+ if (global.format === "json") output.writeLine(JSON.stringify({
671
+ command: "plan",
672
+ data: {
673
+ id: plan.id,
674
+ graphId: plan.graphId,
675
+ entry: plan.entry,
676
+ steps: plan.steps.map((s) => s.id),
677
+ inputs: plan.inputs
678
+ },
679
+ durationMs
680
+ }));
681
+ else {
682
+ output.writeLine(`Run Plan: ${plan.id}`);
683
+ output.writeLine(` entry: ${plan.entry.id}`);
684
+ output.writeLine(` steps: ${plan.steps.length}`);
685
+ for (const step of plan.steps) {
686
+ const deps = step.dependencies.length > 0 ? ` (depends: ${step.dependencies.map((d) => d.producer).join(", ")})` : "";
687
+ output.writeLine(` - ${step.id}${deps}`);
688
+ }
689
+ }
690
+ return ExitCode.Success;
691
+ }
692
+ //#endregion
693
+ //#region src/commands/graph.ts
694
+ /**
695
+ * Display the Definition Graph: pipelines, entries, steps, and dependencies.
696
+ */
697
+ async function graphCommand(global, output, start) {
698
+ output.debug(`graph: root=${global.root}`);
699
+ const { graph } = await loadProjectGraph(global);
700
+ const durationMs = Date.now() - start;
701
+ if (global.format === "json") output.writeLine(JSON.stringify({
702
+ command: "graph",
703
+ data: graph,
704
+ durationMs
705
+ }));
706
+ else {
707
+ output.writeLine(`Definition Graph: ${graph.project.id}`);
708
+ for (const pipeline of graph.project.pipelines) {
709
+ output.writeLine(` Pipeline: ${pipeline.id}`);
710
+ output.writeLine(` entries:`);
711
+ for (const entry of pipeline.entries) output.writeLine(` ${entry.id} (trigger: ${entry.trigger.kind}, roots: ${entry.roots.join(", ")})`);
712
+ output.writeLine(` steps:`);
713
+ for (const step of pipeline.steps) {
714
+ const deps = step.dependencies.length > 0 ? ` → [${step.dependencies.map((d) => `${d.kind}:${d.producer}`).join(", ")}]` : "";
715
+ output.writeLine(` ${step.id}${deps}`);
716
+ }
717
+ }
718
+ }
719
+ return ExitCode.Success;
720
+ }
721
+ //#endregion
722
+ //#region src/internal/runtime-check.ts
723
+ /**
724
+ * Check if a binary is available on PATH by spawning `<binary> --version`.
725
+ * Returns true if the binary exists and exits 0, false otherwise.
726
+ */
727
+ function isBinaryAvailable(binary) {
728
+ const result = spawnSync(binary, ["--version"], {
729
+ stdio: [
730
+ "ignore",
731
+ "pipe",
732
+ "ignore"
733
+ ],
734
+ encoding: "utf8",
735
+ timeout: 5e3
736
+ });
737
+ return result.status === 0 && result.error === void 0;
738
+ }
739
+ //#endregion
740
+ //#region src/internal/report-context.ts
741
+ function git(root, ...args) {
742
+ const res = spawnSync("git", args, {
743
+ cwd: root,
744
+ encoding: "utf-8",
745
+ timeout: 2e3
746
+ });
747
+ return (res.status === 0 ? res.stdout?.trim() : void 0) || void 0;
748
+ }
749
+ /** Normalize a git remote URL (ssh or https) to a clickable web URL.
750
+ * Credentials embedded in https remotes (user:token@host) are stripped. */
751
+ function repoWebUrl(remote) {
752
+ if (!remote) return void 0;
753
+ const ssh = /^git@([^:]+):(.+?)(\.git)?$/.exec(remote);
754
+ if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
755
+ try {
756
+ const url = new URL(remote);
757
+ if (url.protocol !== "http:" && url.protocol !== "https:") return;
758
+ url.username = "";
759
+ url.password = "";
760
+ url.pathname = url.pathname.replace(/\.git$/, "");
761
+ return url.toString().replace(/\/$/, "");
762
+ } catch {
763
+ return;
764
+ }
765
+ }
766
+ function collectLinks(repo, commit) {
767
+ const links = [];
768
+ if (repo) links.push({
769
+ label: "Repo",
770
+ url: repo
771
+ });
772
+ if (repo && commit) links.push({
773
+ label: "Commit",
774
+ url: `${repo}/commit/${commit}`
775
+ });
776
+ if (repo && process$1.env.GITHUB_RUN_ID) links.push({
777
+ label: "CI run",
778
+ url: `${repo}/actions/runs/${process$1.env.GITHUB_RUN_ID}`
779
+ });
780
+ return links;
781
+ }
782
+ function collectReportContext(root, command) {
783
+ const env = process$1.env;
784
+ const repo = (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0) ?? repoWebUrl(git(root, "remote", "get-url", "origin"));
785
+ const commit = env.GITHUB_SHA ?? git(root, "rev-parse", "HEAD");
786
+ const branch = env.GITHUB_REF_NAME ?? git(root, "rev-parse", "--abbrev-ref", "HEAD");
787
+ const meta = [...branch ? [{
788
+ label: "Branch",
789
+ value: branch
790
+ }] : [], {
791
+ label: "Directory",
792
+ value: root
793
+ }];
794
+ return {
795
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
796
+ command,
797
+ meta,
798
+ links: collectLinks(repo, commit)
799
+ };
800
+ }
801
+ //#endregion
802
+ //#region src/commands/run.ts
803
+ /**
804
+ * Execute a Run Plan through the native engine and print events.
805
+ */
806
+ async function runCommand(args, global, output, start) {
807
+ const executor = args.executor ?? "host";
808
+ const isSarif = global.format === "sarif";
809
+ const isWeb = global.format === "web";
810
+ const isHtml = global.format === "html" || args.output !== void 0 && !isSarif && !isWeb;
811
+ const evaluate = args.evaluate || isHtml || isSarif || isWeb;
812
+ output.debug(`run: root=${global.root} executor=${executor} entry=${args.entryId ?? "(first)"} format=${global.format}`);
813
+ assertExecutorAvailable(executor);
814
+ const { graph, warnings } = await loadProjectGraph(global);
815
+ for (const warning of warnings) output.errorLine(`warning: ${warning}`);
816
+ const entryId = resolveEntryId(graph, args.entryId);
817
+ const plan = bindRunPlan({
818
+ graph,
819
+ entryId
820
+ });
821
+ const workspace = global.root;
822
+ const artifactDir = join(global.root, ".sverka", "artifacts");
823
+ if (args.jobs !== void 0 && (!Number.isInteger(args.jobs) || args.jobs < 1 || args.jobs > 64)) throw new CliError("--jobs must be an integer in [1, 64]", "INVALID_FLAG", ExitCode.UsageError);
824
+ const { events, runStatus, renderer } = await consumeEvents(createEngine({
825
+ drivers: buildDrivers(executor),
826
+ maxConcurrent: args.jobs ?? 4
827
+ }), plan, {
828
+ workspace,
829
+ artifactDir
830
+ }, global, output, graph, args);
831
+ const durationMs = Date.now() - start;
832
+ let policyExitCode = 0;
833
+ let evalResult = null;
834
+ let collectionFailed = false;
835
+ if (evaluate) {
836
+ const result = await runEvaluation(artifactDir, global, output, events, renderer, args);
837
+ policyExitCode = result.exitCode;
838
+ evalResult = result.summary;
839
+ collectionFailed = result.summary === null && result.exitCode === ExitCode.RuntimeError;
840
+ }
841
+ renderer?.flush();
842
+ if (isHtml && renderer) {
843
+ const reportPath = args.output ?? join(global.root, ".sverka", "report.html");
844
+ output.writeLine(`Wrote HTML report to ${reportPath}`);
845
+ }
846
+ if (renderer && "waitUntilExit" in renderer) await renderer.waitUntilExit();
847
+ if (collectionFailed) return policyExitCode;
848
+ writeRunOutput(plan.id, runStatus, events, durationMs, global, output, evalResult);
849
+ if (evaluate && policyExitCode !== 0) return policyExitCode;
850
+ return exitCodeForStatus(runStatus);
851
+ }
852
+ function assertExecutorAvailable(executor) {
853
+ if (executor === "docker" && !isBinaryAvailable("docker")) throw new CliError("docker executor not available (docker not found on PATH)", "RUNTIME_NOT_AVAILABLE", ExitCode.RuntimeError);
854
+ }
855
+ function resolveEntryId(graph, entryId) {
856
+ const resolved = entryId ?? resolveDefaultEntryId(graph);
857
+ if (!resolved) throw new CliError("no entries in graph", "MISSING_ARG", ExitCode.UsageError);
858
+ if (!entryExists(graph, resolved)) throw new CliError(`entry "${resolved}" not found in graph`, "MISSING_ARG", ExitCode.UsageError);
859
+ return resolved;
860
+ }
861
+ function buildDrivers(executor) {
862
+ const drivers = [createHostDriver({
863
+ enabled: true,
864
+ allowlist: {
865
+ entries: [],
866
+ isAllowed: () => true
867
+ },
868
+ envAllowlist: [
869
+ "PATH",
870
+ "HOME",
871
+ "USER",
872
+ "SHELL",
873
+ "TMPDIR",
874
+ "XDG_CONFIG_HOME",
875
+ "XDG_CACHE_HOME",
876
+ "XDG_DATA_HOME",
877
+ "XDG_STATE_HOME",
878
+ "XDG_RUNTIME_DIR"
879
+ ]
880
+ })];
881
+ if (executor === "docker") drivers.push(createDockerDriver({}));
882
+ return drivers;
883
+ }
884
+ async function consumeEvents(engine, plan, ctx, global, output, graph, args) {
885
+ const events = [];
886
+ let runStatus = "failure";
887
+ const { createTextRenderer, createHtmlRenderer, createInkRenderer } = await import("@sverka/reporter");
888
+ let renderer = null;
889
+ const isSarif = global.format === "sarif";
890
+ const isWeb = global.format === "web";
891
+ const isHtml = global.format === "html" || args.output !== void 0 && !isSarif && !isWeb;
892
+ if (args.tui === true && !global.quiet && !isHtml && !isSarif && !isWeb && global.format === "text") try {
893
+ renderer = createInkRenderer({ graph });
894
+ } catch {
895
+ renderer = createTextRenderer({ writer: output });
896
+ }
897
+ else if (global.format === "text" && !isHtml) renderer = createTextRenderer({ writer: output });
898
+ else if (isHtml) renderer = createHtmlRenderer({
899
+ outputPath: args.output ?? join(global.root, ".sverka", "report.html"),
900
+ graph,
901
+ context: collectReportContext(global.root, `sverka ${process$1.argv.slice(2).join(" ")}`)
902
+ });
903
+ for await (const event of engine.run({
904
+ plan,
905
+ workspace: ctx.workspace,
906
+ artifactDir: ctx.artifactDir
907
+ })) {
908
+ events.push(event);
909
+ renderer?.onEvent(event);
910
+ if (event.type === "run-completed") runStatus = event.status;
911
+ }
912
+ return {
913
+ events,
914
+ runStatus,
915
+ renderer
916
+ };
917
+ }
918
+ async function runEvaluation(artifactDir, global, output, _events, renderer, args) {
919
+ const { collectFindings, evaluateGate, ReporterError } = await import("@sverka/reporter");
920
+ let rows;
921
+ try {
922
+ rows = await collectFindings({ artifactDir });
923
+ } catch (e) {
924
+ if (e instanceof ReporterError) {
925
+ if (global.format === "json") output.writeLine(JSON.stringify({
926
+ command: "run",
927
+ error: "COLLECTION_FAILED",
928
+ message: e.message
929
+ }));
930
+ else output.writeLine(`Collection failed: ${e.message}`);
931
+ return {
932
+ exitCode: ExitCode.RuntimeError,
933
+ summary: null
934
+ };
935
+ }
936
+ throw e;
937
+ }
938
+ const findings = rows.map((r) => r.finding);
939
+ const { result, exitCode } = evaluateGate({ findings });
940
+ if (renderer) {
941
+ renderer.onFindings(findings);
942
+ renderer.onVerdict(result);
943
+ renderer.flush();
944
+ }
945
+ if (global.format === "sarif") {
946
+ const sarifPath = args.output ?? join(global.root, ".sverka", "findings.sarif");
947
+ const sarifLog = serializeSarif(findings);
948
+ mkdirSync(dirname(sarifPath), { recursive: true });
949
+ writeFileSync(sarifPath, JSON.stringify(sarifLog, null, 2), "utf-8");
950
+ output.writeLine(`Wrote SARIF to ${sarifPath}`);
951
+ }
952
+ if (global.format === "web") {
953
+ const webPath = args.output ?? join(global.root, ".sverka", "report.html");
954
+ try {
955
+ const { generateSarifHtml } = await import("@sverka/sarif-viewer-web");
956
+ const html = generateSarifHtml(findings);
957
+ mkdirSync(dirname(webPath), { recursive: true });
958
+ writeFileSync(webPath, html, "utf-8");
959
+ output.writeLine(`Wrote HTML report to ${webPath}`);
960
+ } catch (e) {
961
+ output.errorLine(`sverka run: failed to generate web report: ${e instanceof Error ? e.message : String(e)}`);
962
+ return {
963
+ exitCode: 1,
964
+ summary: {
965
+ findings,
966
+ verdict: result.verdict,
967
+ summary: result.summary
968
+ }
969
+ };
970
+ }
971
+ }
972
+ return {
973
+ exitCode,
974
+ summary: {
975
+ findings,
976
+ verdict: result.verdict,
977
+ summary: result.summary
978
+ }
979
+ };
980
+ }
981
+ const SIMPLE_STEP_STATUSES = {
982
+ "step-skipped": "skipped",
983
+ "step-cancelled": "cancelled",
984
+ "step-suspended": "suspended"
985
+ };
986
+ /** Extract per-step results from the event stream for JSON output. */
987
+ function summarizeSteps(events) {
988
+ const steps = [];
989
+ for (const event of events) {
990
+ const step = summarizeStep(event);
991
+ if (step !== void 0) steps.push(step);
992
+ }
993
+ return steps;
994
+ }
995
+ function summarizeStep(event) {
996
+ if (event.type === "step-succeeded" || event.type === "step-failed") return {
997
+ stepId: event.stepId,
998
+ status: event.type === "step-succeeded" ? "succeeded" : "failed",
999
+ durationMs: event.durationMs,
1000
+ ...event.type === "step-failed" ? { error: event.error } : {},
1001
+ ...capturedOutputFields(event)
1002
+ };
1003
+ if (event.type === "step-cache-hit") return {
1004
+ stepId: event.stepId,
1005
+ status: "cache-hit",
1006
+ cacheKey: event.key
1007
+ };
1008
+ const status = SIMPLE_STEP_STATUSES[event.type];
1009
+ if (status === void 0 || !("stepId" in event)) return void 0;
1010
+ return {
1011
+ stepId: event.stepId,
1012
+ status
1013
+ };
1014
+ }
1015
+ function capturedOutputFields(event) {
1016
+ return {
1017
+ ...event.stdout !== void 0 ? { stdout: event.stdout } : {},
1018
+ ...event.stderr !== void 0 ? { stderr: event.stderr } : {},
1019
+ ...event.exitCode !== void 0 ? { exitCode: event.exitCode } : {}
1020
+ };
1021
+ }
1022
+ function writeRunOutput(planId, runStatus, events, durationMs, global, output, evalResult) {
1023
+ if (global.format === "json") {
1024
+ const steps = summarizeSteps(events);
1025
+ output.writeLine(JSON.stringify({
1026
+ command: "run",
1027
+ data: {
1028
+ planId,
1029
+ status: runStatus,
1030
+ steps,
1031
+ ...evalResult ? {
1032
+ findings: evalResult.findings.length,
1033
+ verdict: evalResult.verdict,
1034
+ summary: evalResult.summary
1035
+ } : {}
1036
+ },
1037
+ durationMs
1038
+ }));
1039
+ }
1040
+ }
1041
+ function exitCodeForStatus(runStatus) {
1042
+ if (runStatus === "success") return ExitCode.Success;
1043
+ if (runStatus === "failure") return ExitCode.PolicyFail;
1044
+ return ExitCode.RuntimeError;
1045
+ }
1046
+ //#endregion
1047
+ //#region src/commands/discover.ts
1048
+ /**
1049
+ * Discover project context and display it.
1050
+ */
1051
+ async function discoverCommand(global, output, start) {
1052
+ output.debug(`discover: root=${global.root}`);
1053
+ const ctx = await createPlanner().discover({ root: global.root });
1054
+ const durationMs = Date.now() - start;
1055
+ if (global.format === "json") output.writeLine(JSON.stringify({
1056
+ command: "discover",
1057
+ data: {
1058
+ root: ctx.root,
1059
+ commit: ctx.commit,
1060
+ dirty: ctx.dirty,
1061
+ languages: ctx.languages,
1062
+ packageManagers: ctx.packageManagers,
1063
+ hasContainerBuild: ctx.hasContainerBuild,
1064
+ hasCiDefinition: ctx.hasCiDefinition,
1065
+ monorepo: ctx.monorepo,
1066
+ changedFiles: ctx.changedFiles.length
1067
+ },
1068
+ durationMs
1069
+ }));
1070
+ else {
1071
+ output.writeLine(`Project: ${ctx.root}`);
1072
+ output.writeLine(` commit: ${ctx.commit || "(none)"}`);
1073
+ output.writeLine(` dirty: ${ctx.dirty}`);
1074
+ output.writeLine(` languages: ${ctx.languages.map((l) => l.name).join(", ") || "(none)"}`);
1075
+ output.writeLine(` package managers: ${ctx.packageManagers.map((p) => p.name).join(", ") || "(none)"}`);
1076
+ output.writeLine(` container build: ${ctx.hasContainerBuild}`);
1077
+ output.writeLine(` CI definition: ${ctx.hasCiDefinition}`);
1078
+ if (ctx.monorepo) output.writeLine(` monorepo: ${ctx.monorepo.tool} (${ctx.monorepo.workspaces.length} workspaces)`);
1079
+ output.writeLine(` changed files: ${ctx.changedFiles.length}`);
1080
+ }
1081
+ return ExitCode.Success;
1082
+ }
1083
+ //#endregion
1084
+ //#region src/commands/check.ts
1085
+ /**
1086
+ * Discover project checks (planner + package.json scripts) and display the
1087
+ * steps they would become — the same detection `init --detect` uses.
1088
+ */
1089
+ async function checkCommand(global, output, start) {
1090
+ output.debug(`check: root=${global.root}`);
1091
+ const checks = await detectProjectChecks(global.root);
1092
+ const durationMs = Date.now() - start;
1093
+ if (global.format === "json") output.writeLine(JSON.stringify({
1094
+ command: "check",
1095
+ data: {
1096
+ proposed: checks.map((c) => c.checkId),
1097
+ resolved: checks.map((c) => ({
1098
+ id: c.checkId,
1099
+ command: c.command
1100
+ }))
1101
+ },
1102
+ durationMs
1103
+ }));
1104
+ else {
1105
+ output.writeLine(`Proposed checks: ${checks.length}`);
1106
+ for (const c of checks) output.writeLine(` - ${c.checkId} (${c.source}): ${c.command}`);
1107
+ }
1108
+ return ExitCode.Success;
1109
+ }
1110
+ //#endregion
1111
+ //#region src/commands/policy.ts
1112
+ /**
1113
+ * Evaluate findings against the default policy.
1114
+ * In v0, this is a placeholder — findings come from a baseline file
1115
+ * or are empty (no execution output to extract from yet).
1116
+ */
1117
+ async function policyCommand(args, global, output, start) {
1118
+ output.debug(`policy: root=${global.root} findings=${args.findings} baseline=${args.baseline ?? "(none)"}`);
1119
+ const findings = normalizeFindings(await loadFindingsFile(resolveUnderRoot(global.root, args.findings)), {
1120
+ root: global.root,
1121
+ checkIdPrefix: "",
1122
+ defaultConfidence: .5
1123
+ });
1124
+ const baselineFingerprints = await loadBaselineFingerprints(global.root, args.baseline);
1125
+ const result = evaluatePolicy(findings, DEFAULT_POLICY, [...baselineFingerprints]);
1126
+ writePolicyOutput(result, global, output, Date.now() - start);
1127
+ return result.verdict === "pass" ? ExitCode.Success : ExitCode.PolicyFail;
1128
+ }
1129
+ async function loadFindingsFile(findingsPath) {
1130
+ if (!existsSync(findingsPath)) throw new CliError(`findings file not found: ${findingsPath}`, "MISSING_ARG", ExitCode.UsageError);
1131
+ try {
1132
+ return JSON.parse(await readFile(findingsPath, "utf8"));
1133
+ } catch (e) {
1134
+ throw new CliError(`failed to parse findings file: ${e instanceof Error ? e.message : String(e)}`, "SDK_ERROR", ExitCode.RuntimeError, e);
1135
+ }
1136
+ }
1137
+ function normalizeFindings(raw, ctx) {
1138
+ try {
1139
+ return normalizeSarif(raw, ctx);
1140
+ } catch (e) {
1141
+ throw new CliError(`failed to normalize findings: ${e instanceof Error ? e.message : String(e)}`, "SDK_ERROR", ExitCode.RuntimeError, e);
1142
+ }
1143
+ }
1144
+ async function loadBaselineFingerprints(root, baseline) {
1145
+ if (!baseline) return [];
1146
+ const baselinePath = resolveUnderRoot(root, baseline);
1147
+ try {
1148
+ return (await loadBaseline(baselinePath)).fingerprints;
1149
+ } catch (e) {
1150
+ throw new CliError(`failed to load baseline: ${e instanceof Error ? e.message : String(e)}`, "SDK_ERROR", ExitCode.RuntimeError, e);
1151
+ }
1152
+ }
1153
+ function writePolicyOutput(result, global, output, durationMs) {
1154
+ if (global.format === "json") output.writeLine(JSON.stringify({
1155
+ command: "policy",
1156
+ data: {
1157
+ verdict: result.verdict,
1158
+ summary: result.summary,
1159
+ triggered: result.triggered.length
1160
+ },
1161
+ durationMs
1162
+ }));
1163
+ else {
1164
+ output.writeLine(`Policy: ${result.verdict}`);
1165
+ output.writeLine(` ${result.summary}`);
1166
+ }
1167
+ }
1168
+ //#endregion
1169
+ //#region src/internal/ci-setup.ts
1170
+ /**
1171
+ * Detect the toolchain setup a generated GitHub workflow needs on a clean
1172
+ * runner: the package-manager action + dependency install, and recursive
1173
+ * submodule checkout when the repo has .gitmodules.
1174
+ *
1175
+ * The declared `packageManager` version is honored where the toolchain
1176
+ * supports it: `bun-version` for bun, Corepack for pnpm/yarn (both read the
1177
+ * `packageManager` field), and an explicit global install for npm.
1178
+ *
1179
+ * Always emits `checkoutWith` with `persist-credentials: false` —
1180
+ * generated YAML never persists the GITHUB_TOKEN, even when nothing else
1181
+ * was detected.
1182
+ */
1183
+ function detectCiSetup(root) {
1184
+ const setup = detectPackageManagerSetup(root);
1185
+ const checkoutWith = existsSync(join(root, ".gitmodules")) ? {
1186
+ "persist-credentials": false,
1187
+ submodules: "recursive"
1188
+ } : { "persist-credentials": false };
1189
+ return {
1190
+ ...setup.length > 0 ? { setup } : {},
1191
+ checkoutWith
1192
+ };
1193
+ }
1194
+ function detectPackageManagerSetup(root) {
1195
+ const { name, version } = detectPackageManager(root);
1196
+ if (name === "bun") return bunSteps(version);
1197
+ if (name === void 0) return [];
1198
+ const node = setupNodeStep(root);
1199
+ switch (name) {
1200
+ case "pnpm": return pnpmSteps(version, node);
1201
+ case "yarn": return yarnSteps(version, node);
1202
+ case "npm": return npmSteps(root, version, node);
1203
+ default: return [];
1204
+ }
1205
+ }
1206
+ /** setup-node step — pins Node from engines.node (a semver range), else lts. */
1207
+ function setupNodeStep(root) {
1208
+ return {
1209
+ name: "Setup Node",
1210
+ uses: "actions/setup-node@v7",
1211
+ with: { "node-version": nodeVersion(root) }
1212
+ };
1213
+ }
1214
+ function nodeVersion(root) {
1215
+ try {
1216
+ const raw = readFileSync(join(root, "package.json"), "utf8");
1217
+ const engines = JSON.parse(raw).engines?.node;
1218
+ if (typeof engines === "string" && engines.trim() !== "") return engines;
1219
+ } catch {}
1220
+ return "lts/*";
1221
+ }
1222
+ function bunSteps(version) {
1223
+ return [{
1224
+ name: "Setup Bun",
1225
+ uses: "oven-sh/setup-bun@v2",
1226
+ ...version ? { with: { "bun-version": version } } : {}
1227
+ }, {
1228
+ name: "Install dependencies",
1229
+ run: "bun install --frozen-lockfile --ignore-scripts"
1230
+ }];
1231
+ }
1232
+ function pnpmSteps(version, node) {
1233
+ return [
1234
+ {
1235
+ name: "Setup pnpm",
1236
+ uses: "pnpm/action-setup@v4",
1237
+ ...version ? {} : { with: { version: "latest" } }
1238
+ },
1239
+ node,
1240
+ {
1241
+ name: "Install dependencies",
1242
+ run: "pnpm install --frozen-lockfile --ignore-scripts"
1243
+ }
1244
+ ];
1245
+ }
1246
+ function yarnSteps(version, node) {
1247
+ return [
1248
+ node,
1249
+ ...version ? [{
1250
+ name: "Enable Corepack",
1251
+ run: "corepack enable"
1252
+ }] : [],
1253
+ {
1254
+ name: "Install dependencies",
1255
+ run: "yarn install"
1256
+ }
1257
+ ];
1258
+ }
1259
+ function npmSteps(root, version, node) {
1260
+ return [
1261
+ node,
1262
+ ...version ? [{
1263
+ name: "Pin npm",
1264
+ run: `npm install -g "npm@${version}"`
1265
+ }] : [],
1266
+ {
1267
+ name: "Install dependencies",
1268
+ run: existsSync(join(root, "package-lock.json")) ? "npm ci --ignore-scripts" : "npm install --ignore-scripts"
1269
+ }
1270
+ ];
1271
+ }
1272
+ function detectPackageManager(root) {
1273
+ const declared = packageManagerField(root);
1274
+ if (declared) return declared;
1275
+ if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) return { name: "bun" };
1276
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return { name: "pnpm" };
1277
+ if (existsSync(join(root, "yarn.lock"))) return { name: "yarn" };
1278
+ if (existsSync(join(root, "package-lock.json"))) return { name: "npm" };
1279
+ if (existsSync(join(root, "package.json"))) return { name: "npm" };
1280
+ return {};
1281
+ }
1282
+ function packageManagerField(root) {
1283
+ try {
1284
+ const raw = readFileSync(join(root, "package.json"), "utf8");
1285
+ const pm = JSON.parse(raw).packageManager;
1286
+ if (typeof pm !== "string") return void 0;
1287
+ const [name, version] = pm.split("@");
1288
+ if (name === "bun" || name === "pnpm" || name === "yarn" || name === "npm") return version !== void 0 ? {
1289
+ name,
1290
+ version
1291
+ } : { name };
1292
+ } catch {}
1293
+ }
1294
+ //#endregion
1295
+ //#region src/commands/compile.ts
1296
+ /** Compile the Definition Graph to a target CI YAML. */
1297
+ async function compileCommand(args, global, output, start) {
1298
+ const target = args.target;
1299
+ if (target !== "github" && target !== "gitlab") throw new CliError(`invalid compile target: ${target} (expected github or gitlab)`, "INVALID_FLAG", ExitCode.UsageError);
1300
+ if (args.output && args.outputDir) throw new CliError("--output and --output-dir are mutually exclusive", "INVALID_FLAG", ExitCode.UsageError);
1301
+ output.debug(`compile: root=${global.root} target=${target} output=${args.output ?? "stdout"}`);
1302
+ const { graph } = await loadProjectGraph(global);
1303
+ const result = target === "github" ? compileGithub(graph, {
1304
+ ...detectCiSetup(global.root),
1305
+ ...args.pin ? { pinning: { mode: "strict" } } : {}
1306
+ }) : compileGitlab(graph);
1307
+ let hasError = false;
1308
+ for (const d of result.diagnostics) {
1309
+ output.errorLine(`[${d.severity}] ${d.capability}: ${d.message}`);
1310
+ if (d.severity === "error") hasError = true;
1311
+ }
1312
+ const yaml = result.artifacts.map((a) => a.content).join("\n---\n");
1313
+ if (result.artifacts.length > 1 && !args.outputDir) {
1314
+ if (args.output) throw new CliError(`--output cannot represent ${result.artifacts.length} artifacts; use --output-dir`, "INVALID_FLAG", ExitCode.UsageError);
1315
+ output.errorLine(`[warning] ${result.artifacts.length} artifacts glued into one stream; use --output-dir to write separate files`);
1316
+ }
1317
+ if (args.outputDir) {
1318
+ const outDir = resolve(global.root, args.outputDir);
1319
+ await mkdir(outDir, { recursive: true });
1320
+ const realOutDir = await realpath(outDir);
1321
+ const contained = (base, p) => {
1322
+ const rel = relative(base, p);
1323
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
1324
+ };
1325
+ const written = [];
1326
+ for (const a of result.artifacts) {
1327
+ const outPath = resolve(outDir, a.path);
1328
+ const escapes = () => new CliError(`artifact path escapes output dir: ${a.path}`, "INVALID_FLAG", ExitCode.UsageError);
1329
+ if (!contained(outDir, outPath)) throw escapes();
1330
+ for (let ancestor = dirname(outPath);;) try {
1331
+ const realAncestor = await realpath(ancestor);
1332
+ if (!contained(realOutDir, join(realAncestor, relative(ancestor, outPath)))) throw escapes();
1333
+ break;
1334
+ } catch (e) {
1335
+ if (e instanceof CliError) throw e;
1336
+ if (e.code !== "ENOENT") throw e;
1337
+ const up = dirname(ancestor);
1338
+ if (up === ancestor) break;
1339
+ ancestor = up;
1340
+ }
1341
+ await mkdir(dirname(outPath), { recursive: true });
1342
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW;
1343
+ let fh;
1344
+ try {
1345
+ fh = await open(outPath, flags);
1346
+ } catch (e) {
1347
+ if (e.code === "ELOOP") throw escapes();
1348
+ throw e;
1349
+ }
1350
+ try {
1351
+ await fh.writeFile(a.content);
1352
+ } finally {
1353
+ await fh.close();
1354
+ }
1355
+ written.push(outPath);
1356
+ }
1357
+ if (global.format === "json") output.writeLine(JSON.stringify({
1358
+ command: "compile",
1359
+ data: {
1360
+ target,
1361
+ paths: written
1362
+ },
1363
+ durationMs: Date.now() - start
1364
+ }));
1365
+ else for (const p of written) output.writeLine(`Compiled ${target} workflow to ${p}`);
1366
+ } else if (args.output) {
1367
+ const outPath = resolve(global.root, args.output);
1368
+ await mkdir(dirname(outPath), { recursive: true });
1369
+ await writeFile(outPath, yaml, "utf8");
1370
+ if (global.format === "json") output.writeLine(JSON.stringify({
1371
+ command: "compile",
1372
+ data: {
1373
+ target,
1374
+ path: outPath
1375
+ },
1376
+ durationMs: Date.now() - start
1377
+ }));
1378
+ else output.writeLine(`Compiled ${target} workflow to ${outPath}`);
1379
+ } else if (global.format === "json") output.writeLine(JSON.stringify({
1380
+ command: "compile",
1381
+ data: {
1382
+ target,
1383
+ yaml
1384
+ },
1385
+ durationMs: Date.now() - start
1386
+ }));
1387
+ else output.write(yaml);
1388
+ return hasError ? ExitCode.RuntimeError : ExitCode.Success;
1389
+ }
1390
+ //#endregion
1391
+ //#region src/commands/synth.ts
1392
+ /**
1393
+ * Alias for `compile`. Delegates to compileCommand.
1394
+ */
1395
+ async function synthCommand(args, global, output, start) {
1396
+ return compileCommand(args, global, output, start);
1397
+ }
1398
+ //#endregion
1399
+ //#region src/internal/buffering-writer.ts
1400
+ /**
1401
+ * OutputWriter for MCP tool execution: captures structured stdout output
1402
+ * (command handlers emit JSON when format="json") and routes all error/debug
1403
+ * output to stderr. stdout is reserved for MCP JSON-RPC transport.
1404
+ */
1405
+ var BufferingOutputWriter = class {
1406
+ #buffer = "";
1407
+ write(text) {
1408
+ this.#buffer += text;
1409
+ }
1410
+ writeLine(text) {
1411
+ this.#buffer += text + "\n";
1412
+ }
1413
+ error(text) {
1414
+ process$1.stderr.write(text);
1415
+ }
1416
+ errorLine(text) {
1417
+ process$1.stderr.write(text + "\n");
1418
+ }
1419
+ debug(text) {
1420
+ process$1.stderr.write(text + "\n");
1421
+ }
1422
+ /** The captured stdout content (JSON string from the command handler). */
1423
+ get captured() {
1424
+ return this.#buffer;
1425
+ }
1426
+ };
1427
+ //#endregion
1428
+ //#region src/internal/mcp-tools.ts
1429
+ /** Build GlobalFlags for JSON-format command execution. */
1430
+ function jsonFlags(root) {
1431
+ return {
1432
+ format: "json",
1433
+ config: null,
1434
+ root,
1435
+ quiet: false,
1436
+ verbose: false
1437
+ };
1438
+ }
1439
+ /** Safely parse JSON, returning null on failure. */
1440
+ function safeJsonParse(text) {
1441
+ try {
1442
+ return JSON.parse(text);
1443
+ } catch {
1444
+ return null;
1445
+ }
1446
+ }
1447
+ /**
1448
+ * Run a CLI command handler with a buffering writer and return an MCP
1449
+ * CallToolResult. On success (exit 0), the captured JSON `data` field is
1450
+ * returned as text content. On failure, isError is set with the error message.
1451
+ */
1452
+ async function runCommandAsTool(fn, root) {
1453
+ const writer = new BufferingOutputWriter();
1454
+ const global = jsonFlags(root);
1455
+ const start = Date.now();
1456
+ try {
1457
+ const exitCode = await fn(writer, global, start);
1458
+ const text = writer.captured.trim();
1459
+ if (exitCode === ExitCode.Success) {
1460
+ const parsed = text ? safeJsonParse(text) : null;
1461
+ const data = parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : parsed ?? {};
1462
+ return { content: [{
1463
+ type: "text",
1464
+ text: JSON.stringify(data)
1465
+ }] };
1466
+ }
1467
+ const parsed = text ? safeJsonParse(text) : null;
1468
+ return {
1469
+ content: [{
1470
+ type: "text",
1471
+ text: parsed ? JSON.stringify(parsed) : text || `exit code ${exitCode}`
1472
+ }],
1473
+ isError: true
1474
+ };
1475
+ } catch (e) {
1476
+ return {
1477
+ content: [{
1478
+ type: "text",
1479
+ text: e instanceof Error ? e.message : String(e)
1480
+ }],
1481
+ isError: true
1482
+ };
1483
+ }
1484
+ }
1485
+ z.string().optional(), z.string().optional(), z.string().optional(), z.string().optional(), z.string().optional(), z.string().optional(), z.enum(["host", "docker"]).optional(), z.string().optional(), z.enum(["github", "gitlab"]);
1486
+ /**
1487
+ * Register all 5 Sverka MCP tools on the given McpServer.
1488
+ * Each tool maps to the corresponding CLI command handler.
1489
+ */
1490
+ function registerSverkaTools(server, defaultRoot) {
1491
+ server.tool("sverka.validate", "Validate a sverka config: synthesize the Definition Graph and run validators.", { root: z.string().optional() }, async (args) => {
1492
+ return runCommandAsTool((w, g, s) => validateCommand(g, w, s), args.root ?? defaultRoot);
1493
+ });
1494
+ server.tool("sverka.plan", "Bind an Entry and inputs into a Run Plan and return the step list.", {
1495
+ root: z.string().optional(),
1496
+ entryId: z.string().optional()
1497
+ }, async (args) => {
1498
+ return runCommandAsTool((w, g, s) => planCommand({ ...args.entryId !== void 0 && { entryId: args.entryId } }, g, w, s), args.root ?? defaultRoot);
1499
+ });
1500
+ server.tool("sverka.graph", "Display the Definition Graph: pipelines, entries, steps, and dependencies.", { root: z.string().optional() }, async (args) => {
1501
+ return runCommandAsTool((w, g, s) => graphCommand(g, w, s), args.root ?? defaultRoot);
1502
+ });
1503
+ server.tool("sverka.run", "Execute the workflow locally through the native engine and return the run status.", {
1504
+ root: z.string().optional(),
1505
+ entryId: z.string().optional(),
1506
+ executor: z.enum(["host", "docker"]).optional()
1507
+ }, async (args) => {
1508
+ return runCommandAsTool((w, g, s) => runCommand({
1509
+ ...args.entryId !== void 0 && { entryId: args.entryId },
1510
+ ...args.executor !== void 0 && { executor: args.executor }
1511
+ }, g, w, s), args.root ?? defaultRoot);
1512
+ });
1513
+ server.tool("sverka.synth", "Compile the workflow to a target CI YAML (github or gitlab).", {
1514
+ root: z.string().optional(),
1515
+ target: z.enum(["github", "gitlab"])
1516
+ }, async (args) => {
1517
+ return synthTool(args.root ?? defaultRoot, args.target);
1518
+ });
1519
+ }
1520
+ /** Run the synth tool: load graph, compile to target, return artifacts. */
1521
+ async function synthTool(root, target) {
1522
+ try {
1523
+ const { graph } = await loadProjectGraph({
1524
+ root,
1525
+ config: null
1526
+ });
1527
+ const result = target === "github" ? compileGithub(graph) : compileGitlab(graph);
1528
+ const data = {
1529
+ target,
1530
+ artifacts: result.artifacts.map((a) => ({
1531
+ path: a.path,
1532
+ content: a.content
1533
+ })),
1534
+ diagnostics: result.diagnostics
1535
+ };
1536
+ return { content: [{
1537
+ type: "text",
1538
+ text: JSON.stringify(data)
1539
+ }] };
1540
+ } catch (e) {
1541
+ return {
1542
+ content: [{
1543
+ type: "text",
1544
+ text: e instanceof Error ? e.message : String(e)
1545
+ }],
1546
+ isError: true
1547
+ };
1548
+ }
1549
+ }
1550
+ //#endregion
1551
+ //#region src/commands/mcp-server.ts
1552
+ /**
1553
+ * Start an MCP server over stdio that exposes Sverka operations as tools.
1554
+ * External AI agents can invoke sverka.validate, sverka.plan, sverka.graph,
1555
+ * sverka.run, and sverka.synth via the Model Context Protocol.
1556
+ *
1557
+ * The server runs until stdin closes or SIGTERM is received. stdout is
1558
+ * reserved for MCP JSON-RPC transport — all logs go to stderr.
1559
+ *
1560
+ * @param shutdownSignal Optional promise that resolves to signal shutdown
1561
+ * (for testing). In production, the server waits for stdin close / SIGTERM.
1562
+ */
1563
+ async function mcpServerCommand(_args, global, output, _start, shutdownSignal) {
1564
+ output.debug(`mcp-server: root=${global.root}`);
1565
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
1566
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
1567
+ const server = new McpServer({
1568
+ name: "sverka",
1569
+ version: cliVersion()
1570
+ });
1571
+ registerSverkaTools(server, global.root);
1572
+ const transport = new StdioServerTransport();
1573
+ try {
1574
+ await server.connect(transport);
1575
+ } catch (e) {
1576
+ const msg = e instanceof Error ? e.message : String(e);
1577
+ output.errorLine(`mcp-server: failed to start: ${msg}`);
1578
+ return ExitCode.RuntimeError;
1579
+ }
1580
+ await (shutdownSignal ?? waitForStdinClose());
1581
+ await server.close();
1582
+ return ExitCode.Success;
1583
+ }
1584
+ /** Wait for stdin close or SIGTERM. */
1585
+ function waitForStdinClose() {
1586
+ return new Promise((resolve) => {
1587
+ const cleanup = () => {
1588
+ process$1.stdin.off("end", cleanup);
1589
+ process$1.stdin.off("close", cleanup);
1590
+ process$1.off("SIGTERM", cleanup);
1591
+ resolve();
1592
+ };
1593
+ process$1.stdin.on("end", cleanup);
1594
+ process$1.stdin.on("close", cleanup);
1595
+ process$1.on("SIGTERM", cleanup);
1596
+ });
1597
+ }
1598
+ //#endregion
1599
+ //#region src/commands/doctor.ts
1600
+ /**
1601
+ * Check the environment: Node.js, Bun, git. Reports status of each.
1602
+ * Exit 0 when all pass, 3 when a required tool is missing.
1603
+ */
1604
+ async function doctorCommand(global, output, start) {
1605
+ output.debug(`doctor: root=${global.root}`);
1606
+ const checks = [
1607
+ runCheck("node", "node", true),
1608
+ runCheck("bun", "bun", false),
1609
+ runCheck("git", "git", true)
1610
+ ];
1611
+ const allOk = checks.every((c) => !c.required || c.status === "ok");
1612
+ const exitCode = allOk ? ExitCode.Success : ExitCode.RuntimeError;
1613
+ const durationMs = Date.now() - start;
1614
+ if (global.format === "json") output.writeLine(JSON.stringify({
1615
+ command: "doctor",
1616
+ data: {
1617
+ checks,
1618
+ allOk
1619
+ },
1620
+ durationMs
1621
+ }));
1622
+ else {
1623
+ output.writeLine("Environment diagnostics:");
1624
+ for (const check of checks) {
1625
+ const version = check.version ? `v${check.version}` : "NOT FOUND";
1626
+ output.writeLine(` ${check.name}: ${version}`);
1627
+ }
1628
+ output.writeLine(allOk ? "All checks passed." : "Some checks failed.");
1629
+ }
1630
+ return exitCode;
1631
+ }
1632
+ function runCheck(name, binary, required) {
1633
+ try {
1634
+ const result = spawnSync(binary, ["--version"], {
1635
+ stdio: [
1636
+ "ignore",
1637
+ "pipe",
1638
+ "ignore"
1639
+ ],
1640
+ encoding: "utf8",
1641
+ timeout: 5e3
1642
+ });
1643
+ if (result.status !== 0 || result.error) return {
1644
+ name,
1645
+ status: "missing",
1646
+ version: null,
1647
+ required
1648
+ };
1649
+ const out = result.stdout.trim();
1650
+ return {
1651
+ name,
1652
+ status: "ok",
1653
+ version: out.match(/(\d+\.\d+\.\d+[^\s]*)/)?.[1] ?? out,
1654
+ required
1655
+ };
1656
+ } catch {
1657
+ return {
1658
+ name,
1659
+ status: "missing",
1660
+ version: null,
1661
+ required
1662
+ };
1663
+ }
1664
+ }
1665
+ //#endregion
1666
+ //#region src/commands/view.ts
1667
+ /**
1668
+ * View SARIF findings in the terminal TUI or generate an HTML report.
1669
+ */
1670
+ async function viewCommand(args, global, output, start) {
1671
+ let sarifJson;
1672
+ if (args.file) try {
1673
+ sarifJson = readFileSync(args.file, "utf8");
1674
+ } catch (e) {
1675
+ throw new CliError(`failed to read SARIF file: ${e instanceof Error ? e.message : String(e)}`, "MISSING_ARG", ExitCode.RuntimeError);
1676
+ }
1677
+ else if (!process$1.stdin.isTTY) {
1678
+ sarifJson = readFileSync(0, "utf8");
1679
+ if (sarifJson.trim() === "") {
1680
+ output.errorLine("sverka view: stdin is empty.");
1681
+ return ExitCode.RuntimeError;
1682
+ }
1683
+ } else {
1684
+ output.errorLine("sverka view: no input. Provide a SARIF file argument or pipe SARIF via stdin.");
1685
+ return ExitCode.UsageError;
1686
+ }
1687
+ let sarif;
1688
+ try {
1689
+ sarif = JSON.parse(sarifJson);
1690
+ } catch (e) {
1691
+ output.errorLine(`sverka view: invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
1692
+ return ExitCode.RuntimeError;
1693
+ }
1694
+ if (args.format === "web") return renderWeb(sarif, args, output);
1695
+ return renderTui(sarif, output);
1696
+ }
1697
+ /** Launch the TUI viewer using @sverka/sarif-viewer-tui. */
1698
+ async function renderTui(sarif, output) {
1699
+ try {
1700
+ const { renderSarifTui } = await import("@sverka/sarif-viewer-tui");
1701
+ await renderSarifTui({ sarif });
1702
+ return ExitCode.Success;
1703
+ } catch (e) {
1704
+ output.errorLine(`sverka view: ${e instanceof Error ? e.message : String(e)}`);
1705
+ return ExitCode.RuntimeError;
1706
+ }
1707
+ }
1708
+ /** Generate an HTML report using @sverka/sarif-viewer-web. */
1709
+ async function renderWeb(sarif, args, output) {
1710
+ try {
1711
+ const { renderSarifWeb } = await import("@sverka/sarif-viewer-web");
1712
+ const outputPath = args.output ?? "sarif-report.html";
1713
+ renderSarifWeb({
1714
+ sarif,
1715
+ outputPath
1716
+ });
1717
+ output.writeLine(`Wrote ${outputPath}`);
1718
+ return ExitCode.Success;
1719
+ } catch (e) {
1720
+ output.errorLine(`sverka view: ${e instanceof Error ? e.message : String(e)}`);
1721
+ return ExitCode.RuntimeError;
1722
+ }
1723
+ }
1724
+ //#endregion
1725
+ //#region src/commands/ui.ts
1726
+ /**
1727
+ * Start a local web dashboard server that lists SARIF files
1728
+ * and renders individual findings reports.
1729
+ */
1730
+ async function uiCommand(args, global, output, _start) {
1731
+ const artifactsDir = join(global.root, ".sverka", "artifacts");
1732
+ const port = args.port ?? 3e3;
1733
+ const host = args.host ?? "localhost";
1734
+ try {
1735
+ const { startUiServer } = await import("@sverka/ui");
1736
+ const server = await startUiServer({
1737
+ artifactsDir,
1738
+ port,
1739
+ host
1740
+ });
1741
+ output.writeLine(`Sverka UI running at ${server.url}`);
1742
+ output.writeLine(`Artifacts: ${artifactsDir}`);
1743
+ output.writeLine("Press Ctrl+C to stop.");
1744
+ process$1.once("SIGINT", () => {
1745
+ server.close();
1746
+ process$1.exit(ExitCode.Success);
1747
+ });
1748
+ return new Promise(() => {});
1749
+ } catch (e) {
1750
+ output.errorLine(`sverka ui: ${e instanceof Error ? e.message : String(e)}`);
1751
+ return ExitCode.RuntimeError;
1752
+ }
1753
+ }
1754
+ //#endregion
1755
+ //#region src/main.ts
1756
+ /** Build GlobalFlags from parsed yargs options. */
1757
+ function buildGlobalFlags(parsed) {
1758
+ return {
1759
+ format: resolveFormat(parsed.format),
1760
+ config: typeof parsed.config === "string" ? parsed.config : null,
1761
+ root: typeof parsed.root === "string" ? parsed.root : process$1.cwd(),
1762
+ quiet: Boolean(parsed.quiet),
1763
+ verbose: Boolean(parsed.verbose)
1764
+ };
1765
+ }
1766
+ /** Resolve the format string from parsed yargs. */
1767
+ function resolveFormat(format) {
1768
+ if (format === "json") return "json";
1769
+ if (format === "html") return "html";
1770
+ if (format === "sarif") return "sarif";
1771
+ if (format === "web") return "web";
1772
+ return "text";
1773
+ }
1774
+ /** Resolve the real output writer, wrapping injected writers with flag semantics. */
1775
+ function resolveOutputWriter(global, deps) {
1776
+ if (deps?.output) return wrapOutputWriter(global, deps.output);
1777
+ return createOutputWriter(global, (s) => process$1.stdout.write(s), (s) => process$1.stderr.write(s));
1778
+ }
1779
+ /** Configure the init subcommand options. */
1780
+ function addInitCommand(y) {
1781
+ return y.option("template", {
1782
+ type: "string",
1783
+ default: "minimal",
1784
+ choices: ["minimal", "full"]
1785
+ }).option("force", {
1786
+ type: "boolean",
1787
+ default: false
1788
+ }).option("detect", {
1789
+ type: "boolean",
1790
+ default: false,
1791
+ describe: "Generate config from detected project checks (overrides --template)"
1792
+ });
1793
+ }
1794
+ /** Configure the run subcommand options. */
1795
+ function addRunCommand(y) {
1796
+ return y.option("entry", { type: "string" }).option("executor", {
1797
+ type: "string",
1798
+ default: "host",
1799
+ choices: ["host", "docker"]
1800
+ }).option("evaluate", {
1801
+ type: "boolean",
1802
+ default: false
1803
+ }).option("output", {
1804
+ type: "string",
1805
+ alias: "o",
1806
+ describe: "Output file path for HTML report (implies --format html)"
1807
+ }).option("tui", {
1808
+ type: "boolean",
1809
+ describe: "Interactive terminal UI (off by default; opt-in only)"
1810
+ }).option("jobs", {
1811
+ type: "number",
1812
+ alias: "j",
1813
+ describe: "Max steps running concurrently (default: 4)"
1814
+ });
1815
+ }
1816
+ /** Configure the plan subcommand options. */
1817
+ function addPlanCommand(y) {
1818
+ return y.option("entry", { type: "string" });
1819
+ }
1820
+ /** Configure the policy subcommand options. */
1821
+ function addPolicyCommand(y) {
1822
+ return y.option("findings", {
1823
+ type: "string",
1824
+ demandOption: true
1825
+ }).option("baseline", { type: "string" });
1826
+ }
1827
+ /** Configure the synth subcommand options. */
1828
+ function addSynthCommand(y) {
1829
+ return y.option("target", {
1830
+ type: "string",
1831
+ demandOption: true,
1832
+ choices: ["github", "gitlab"]
1833
+ });
1834
+ }
1835
+ /** Configure the compile subcommand options. */
1836
+ function addCompileCommand(y) {
1837
+ return y.option("target", {
1838
+ type: "string",
1839
+ demandOption: true,
1840
+ choices: ["github", "gitlab"]
1841
+ }).option("output", {
1842
+ type: "string",
1843
+ alias: "o",
1844
+ describe: "Write YAML to a file instead of stdout"
1845
+ }).option("output-dir", {
1846
+ type: "string",
1847
+ describe: "Write each artifact to <dir>/<artifact path> (multi-pipeline)"
1848
+ }).option("pin", {
1849
+ type: "boolean",
1850
+ default: false,
1851
+ describe: "Pin GitHub action refs to commit SHAs (github target)"
1852
+ });
1853
+ }
1854
+ /** Configure the view subcommand options. */
1855
+ function addViewCommand(y) {
1856
+ return y.positional("file", {
1857
+ type: "string",
1858
+ describe: "Path to a .sarif file (reads from stdin if omitted)"
1859
+ }).option("format", {
1860
+ type: "string",
1861
+ alias: "f",
1862
+ choices: [
1863
+ "tui",
1864
+ "web",
1865
+ "html"
1866
+ ],
1867
+ default: "tui",
1868
+ describe: "View format: tui (terminal), web/html (HTML report)"
1869
+ }).option("output", {
1870
+ type: "string",
1871
+ alias: "o",
1872
+ describe: "Output HTML file path (for --format web, default: sarif-report.html)"
1873
+ });
1874
+ }
1875
+ /** Configure the ui subcommand options. */
1876
+ function addUiCommand(y) {
1877
+ return y.option("port", {
1878
+ type: "number",
1879
+ default: 3e3,
1880
+ describe: "Port to listen on"
1881
+ }).option("host", {
1882
+ type: "string",
1883
+ default: "localhost",
1884
+ describe: "Host to bind"
1885
+ });
1886
+ }
1887
+ function buildParser() {
1888
+ return yargs([]).scriptName("sverka").version(cliVersion()).option("format", {
1889
+ type: "string",
1890
+ alias: "f",
1891
+ choices: [
1892
+ "text",
1893
+ "json",
1894
+ "html",
1895
+ "sarif",
1896
+ "web"
1897
+ ]
1898
+ }).option("config", {
1899
+ type: "string",
1900
+ alias: "c"
1901
+ }).option("root", {
1902
+ type: "string",
1903
+ alias: "r",
1904
+ default: process$1.cwd()
1905
+ }).option("quiet", {
1906
+ type: "boolean",
1907
+ alias: "q",
1908
+ default: false
1909
+ }).option("verbose", {
1910
+ type: "boolean",
1911
+ alias: "v",
1912
+ default: false
1913
+ }).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) => {
1914
+ if (err) throw err;
1915
+ throw new CliError(msg, "UNKNOWN_COMMAND", ExitCode.UsageError);
1916
+ });
1917
+ }
1918
+ /** Dispatch a single command to its handler. */
1919
+ async function dispatch(command, parsed, global, output, start) {
1920
+ switch (command) {
1921
+ case "init": return dispatchInit(parsed, global, output, start);
1922
+ case "validate": return validateCommand(global, output, start);
1923
+ case "plan": return dispatchPlan(parsed, global, output, start);
1924
+ case "graph": return graphCommand(global, output, start);
1925
+ case "run": return dispatchRun(parsed, global, output, start);
1926
+ case "discover": return discoverCommand(global, output, start);
1927
+ case "check": return checkCommand(global, output, start);
1928
+ case "policy": return dispatchPolicy(parsed, global, output, start);
1929
+ case "synth": return dispatchSynth(parsed, global, output, start);
1930
+ case "compile": return dispatchCompile(parsed, global, output, start);
1931
+ case "mcp-server": return mcpServerCommand({}, global, output, start);
1932
+ case "doctor": return doctorCommand(global, output, start);
1933
+ case "view": return dispatchView(parsed, global, output, start);
1934
+ case "ui": return dispatchUi(parsed, global, output, start);
1935
+ default: throw new CliError(`unknown command: ${command}`, "UNKNOWN_COMMAND", ExitCode.UsageError);
1936
+ }
1937
+ }
1938
+ function dispatchInit(parsed, global, output, start) {
1939
+ return initCommand({
1940
+ template: typeof parsed.template === "string" ? parsed.template : void 0,
1941
+ force: Boolean(parsed.force),
1942
+ detect: Boolean(parsed.detect)
1943
+ }, global, output, start);
1944
+ }
1945
+ function dispatchPlan(parsed, global, output, start) {
1946
+ const args = {};
1947
+ if (typeof parsed.entry === "string") args.entryId = parsed.entry;
1948
+ return planCommand(args, global, output, start);
1949
+ }
1950
+ function dispatchRun(parsed, global, output, start) {
1951
+ const args = {
1952
+ executor: parsed.executor === "docker" ? "docker" : "host",
1953
+ evaluate: Boolean(parsed.evaluate)
1954
+ };
1955
+ if (parsed.jobs !== void 0) {
1956
+ const jobs = Number(parsed.jobs);
1957
+ if (!Number.isInteger(jobs) || jobs < 1 || jobs > 64) throw new CliError("--jobs must be an integer in [1, 64]", "INVALID_FLAG", ExitCode.UsageError);
1958
+ args.jobs = jobs;
1959
+ }
1960
+ if (typeof parsed.entry === "string") args.entryId = parsed.entry;
1961
+ if (typeof parsed.output === "string") args.output = parsed.output;
1962
+ if (parsed.tui === true) args.tui = true;
1963
+ if (parsed.tui === false) args.tui = false;
1964
+ return runCommand(args, global, output, start);
1965
+ }
1966
+ function dispatchPolicy(parsed, global, output, start) {
1967
+ const args = { findings: String(parsed.findings) };
1968
+ if (typeof parsed.baseline === "string") args.baseline = parsed.baseline;
1969
+ return policyCommand(args, global, output, start);
1970
+ }
1971
+ function dispatchSynth(parsed, global, output, start) {
1972
+ return synthCommand({ target: parsed.target === "gitlab" ? "gitlab" : "github" }, global, output, start);
1973
+ }
1974
+ function dispatchCompile(parsed, global, output, start) {
1975
+ const args = { target: parsed.target === "gitlab" ? "gitlab" : "github" };
1976
+ if (typeof parsed.output === "string") args.output = parsed.output;
1977
+ if (typeof parsed.outputDir === "string") args.outputDir = parsed.outputDir;
1978
+ if (parsed.pin === true) args.pin = true;
1979
+ return compileCommand(args, global, output, start);
1980
+ }
1981
+ function dispatchView(parsed, global, output, start) {
1982
+ const args = { format: parsed.format === "web" || parsed.format === "html" ? "web" : "tui" };
1983
+ if (typeof parsed.file === "string") args.file = parsed.file;
1984
+ if (typeof parsed.output === "string") args.output = parsed.output;
1985
+ return viewCommand(args, global, output, start);
1986
+ }
1987
+ function dispatchUi(parsed, global, output, start) {
1988
+ const args = {};
1989
+ if (typeof parsed.port === "number") args.port = parsed.port;
1990
+ if (typeof parsed.host === "string") args.host = parsed.host;
1991
+ return uiCommand(args, global, output, start);
1992
+ }
1993
+ /**
1994
+ * The main CLI entry point.
1995
+ * @param argv Command-line arguments (excluding node and script path).
1996
+ * @returns Exit code (0 = success, 1 = policy fail, 2 = usage error, 3 = runtime error).
1997
+ */
1998
+ async function main(argv, deps) {
1999
+ const start = Date.now();
2000
+ const output = deps?.output ?? createOutputWriter({
2001
+ format: "text",
2002
+ config: null,
2003
+ root: process$1.cwd(),
2004
+ quiet: false,
2005
+ verbose: false
2006
+ }, (s) => process$1.stdout.write(s), (s) => process$1.stderr.write(s));
2007
+ let parsed;
2008
+ try {
2009
+ parsed = await buildParser().parseAsync(argv);
2010
+ } catch (e) {
2011
+ return handleError(e, output, start);
2012
+ }
2013
+ const global = buildGlobalFlags(parsed);
2014
+ const realOutput = resolveOutputWriter(global, deps);
2015
+ const command = String(parsed._[0] ?? "");
2016
+ realOutput.debug(`sverka: command=${command} root=${global.root} format=${global.format}`);
2017
+ try {
2018
+ return await dispatch(command, parsed, global, realOutput, start);
2019
+ } catch (e) {
2020
+ return handleError(e, realOutput, start);
2021
+ }
2022
+ }
2023
+ function handleError(e, output, _start) {
2024
+ const hint = missingBuildHint(e);
2025
+ if (e instanceof CliError) {
2026
+ output.errorLine(`error: ${e.message}`);
2027
+ if (hint !== null) output.errorLine(`hint: ${hint}`);
2028
+ return e.exitCode;
2029
+ }
2030
+ const msg = e instanceof Error ? e.message : String(e);
2031
+ output.errorLine(`error: ${msg}`);
2032
+ if (hint !== null) output.errorLine(`hint: ${hint}`);
2033
+ return ExitCode.RuntimeError;
2034
+ }
2035
+ //#endregion
2036
+ export { CliError, ConsoleOutputWriter, ExitCode, createOutputWriter, main, wrapOutputWriter };
2037
+
2038
+ //# sourceMappingURL=index.mjs.map