sverka 0.2.23 → 0.2.25
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.d.mts.map +1 -1
- package/dist/index.mjs +631 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -10
package/dist/index.mjs
CHANGED
|
@@ -3,18 +3,21 @@ import * as nodeModule from "node:module";
|
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import process$1 from "node:process";
|
|
5
5
|
import yargs from "yargs";
|
|
6
|
-
import { constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { mkdir, open, readFile, realpath, writeFile } from "node:fs/promises";
|
|
10
10
|
import { Entry, Pipeline, Project, ShellStep, collectConstructWarnings, manual, synthesize, validateGraph } from "@sverka/workflow";
|
|
11
11
|
import { bindRunPlan, createPlanner } from "@sverka/sdk";
|
|
12
12
|
import { DEFAULT_POLICY, createBuiltinResolver, evaluatePolicy, loadBaseline, normalizeSarif, serializeSarif, synthesizeCheckSteps } from "@sverka/verification";
|
|
13
|
-
import { createDockerDriver, createEngine, createHostDriver } from "@sverka/runtime";
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { AGENT_ENV, collectAgentWrites, createDockerDriver, createEngine, createFileCacheStore, createHostDriver, noAgentDriverError, resolveAgentDrivers, resolveAgentEngine } from "@sverka/runtime";
|
|
14
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
15
|
import { watch } from "chokidar";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { createRemoteCacheStore, createRemoteCircuit, createRemoteSnapshotStore, createTieredCacheStore } from "@sverka/storage";
|
|
16
18
|
import { compileGithub, compileGitlab } from "@sverka/compiler";
|
|
17
19
|
import { z } from "zod";
|
|
20
|
+
import { startHubServer } from "@sverka/hub";
|
|
18
21
|
//#region src/types.ts
|
|
19
22
|
/** Exit codes used by the CLI. */
|
|
20
23
|
const ExitCode = {
|
|
@@ -190,17 +193,31 @@ async function registerConfigHooks() {
|
|
|
190
193
|
/**
|
|
191
194
|
* Find a sverka.config.ts or sverka.config.js file in the given root.
|
|
192
195
|
* Returns the absolute path or null if not found.
|
|
196
|
+
* Spec 37/54: when no config exists, fall back to `.sverka/*.md` files
|
|
197
|
+
* (markdown authoring) — sorted, first file wins.
|
|
193
198
|
*/
|
|
194
199
|
async function findConfig(root) {
|
|
195
200
|
for (const candidate of [
|
|
196
201
|
"sverka.config.ts",
|
|
202
|
+
"sverka.config.mts",
|
|
197
203
|
"sverka.config.js",
|
|
198
204
|
"sverka.config.mjs"
|
|
199
205
|
]) {
|
|
200
206
|
const path = join(root, candidate);
|
|
201
207
|
if (existsSync(path)) return path;
|
|
202
208
|
}
|
|
203
|
-
return
|
|
209
|
+
return findMarkdownConfig(root);
|
|
210
|
+
}
|
|
211
|
+
/** First `.sverka/*.md` file under root (sorted), or null. */
|
|
212
|
+
function findMarkdownConfig(root) {
|
|
213
|
+
const dir = join(root, ".sverka");
|
|
214
|
+
let names;
|
|
215
|
+
try {
|
|
216
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".md")).sort();
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return names.length > 0 ? join(dir, names[0]) : null;
|
|
204
221
|
}
|
|
205
222
|
/**
|
|
206
223
|
* Resolve the effective config path from global flags.
|
|
@@ -236,6 +253,12 @@ function assertConstructLike(value) {
|
|
|
236
253
|
async function loadConfig(configPath) {
|
|
237
254
|
const absPath = isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);
|
|
238
255
|
if (!existsSync(absPath)) throw new CliError(`config not found: ${absPath}`, "MISSING_ARG", ExitCode.UsageError);
|
|
256
|
+
if (absPath.endsWith(".md")) try {
|
|
257
|
+
const { loadMarkdownFile } = await import("@sverka/sdk");
|
|
258
|
+
return await loadMarkdownFile(absPath);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new CliError(`failed to load config: ${e instanceof Error ? e.message : String(e)}`, "SDK_ERROR", ExitCode.RuntimeError, e);
|
|
261
|
+
}
|
|
239
262
|
let mod;
|
|
240
263
|
try {
|
|
241
264
|
await registerConfigHooks();
|
|
@@ -947,6 +970,119 @@ function watchLoop(opts) {
|
|
|
947
970
|
};
|
|
948
971
|
}
|
|
949
972
|
//#endregion
|
|
973
|
+
//#region src/internal/hub.ts
|
|
974
|
+
function configHome() {
|
|
975
|
+
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
976
|
+
return typeof xdg === "string" && xdg !== "" ? xdg : join(homedir(), ".config");
|
|
977
|
+
}
|
|
978
|
+
function credentialsPath() {
|
|
979
|
+
return join(configHome(), "sverka", "credentials");
|
|
980
|
+
}
|
|
981
|
+
function readCredentials() {
|
|
982
|
+
try {
|
|
983
|
+
const text = readFileSync(credentialsPath(), "utf8");
|
|
984
|
+
const parsed = JSON.parse(text);
|
|
985
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
986
|
+
} catch {}
|
|
987
|
+
return {};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Store a hub token at `~/.config/sverka/credentials` with mode 0600.
|
|
991
|
+
* The file is a JSON map keyed by hub URL so multiple hubs coexist.
|
|
992
|
+
*/
|
|
993
|
+
function storeCredentials(url, token) {
|
|
994
|
+
const path = credentialsPath();
|
|
995
|
+
const hubs = {
|
|
996
|
+
...readCredentials().hubs ?? {},
|
|
997
|
+
[url]: token
|
|
998
|
+
};
|
|
999
|
+
mkdirSync(dirname(path), {
|
|
1000
|
+
recursive: true,
|
|
1001
|
+
mode: 448
|
|
1002
|
+
});
|
|
1003
|
+
writeFileSync(path, `${JSON.stringify({ hubs }, null, 2)}\n`, { mode: 384 });
|
|
1004
|
+
chmodSync(path, 384);
|
|
1005
|
+
return path;
|
|
1006
|
+
}
|
|
1007
|
+
function storedTokenFor(url) {
|
|
1008
|
+
return readCredentials().hubs?.[url];
|
|
1009
|
+
}
|
|
1010
|
+
/** Read `.sverka/hub.json` when present. Malformed JSON is ignored. */
|
|
1011
|
+
function readHubFileConfig(root) {
|
|
1012
|
+
const path = join(root, ".sverka", "hub.json");
|
|
1013
|
+
if (!existsSync(path)) return {};
|
|
1014
|
+
try {
|
|
1015
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1016
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
1017
|
+
const outer = parsed;
|
|
1018
|
+
const inner = typeof outer.remote === "object" && outer.remote !== null ? outer.remote : outer;
|
|
1019
|
+
return {
|
|
1020
|
+
url: typeof inner.url === "string" ? inner.url : void 0,
|
|
1021
|
+
project: typeof inner.project === "string" ? inner.project : void 0,
|
|
1022
|
+
enabled: inner.enabled === true,
|
|
1023
|
+
cache: typeof inner.cache === "boolean" ? inner.cache : void 0,
|
|
1024
|
+
upload: typeof inner.upload === "boolean" ? inner.upload : void 0
|
|
1025
|
+
};
|
|
1026
|
+
} catch {
|
|
1027
|
+
return {};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/** Derive "owner/repo" from `git remote.origin.url`; falls back to the
|
|
1031
|
+
* directory basename when there is no usable remote. */
|
|
1032
|
+
function projectSlug(root) {
|
|
1033
|
+
try {
|
|
1034
|
+
const url = execFileSync("git", [
|
|
1035
|
+
"config",
|
|
1036
|
+
"--get",
|
|
1037
|
+
"remote.origin.url"
|
|
1038
|
+
], {
|
|
1039
|
+
cwd: root,
|
|
1040
|
+
encoding: "utf8",
|
|
1041
|
+
stdio: [
|
|
1042
|
+
"ignore",
|
|
1043
|
+
"pipe",
|
|
1044
|
+
"ignore"
|
|
1045
|
+
]
|
|
1046
|
+
}).trim();
|
|
1047
|
+
const slug = (/^[^@]+@[^:]+:(.+)$/.exec(url)?.[1] ?? new URL(url).pathname).replace(/\.git$/, "").replace(/^\/+/, "");
|
|
1048
|
+
if (slug !== "") return slug;
|
|
1049
|
+
} catch {}
|
|
1050
|
+
return basename(resolve(root));
|
|
1051
|
+
}
|
|
1052
|
+
function envString(name) {
|
|
1053
|
+
const value = process.env[name];
|
|
1054
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1055
|
+
}
|
|
1056
|
+
/** Canonical credential key — `sverka login` stores tokens under the URL
|
|
1057
|
+
* with trailing slashes stripped; lookups must normalize identically. */
|
|
1058
|
+
function normalizeHubUrl(url) {
|
|
1059
|
+
let end = url.length;
|
|
1060
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
|
|
1061
|
+
return url.slice(0, end);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Resolve the effective hub connection for a root. Returns null when no
|
|
1065
|
+
* hub is configured at all (no url, no token anywhere) — the caller then
|
|
1066
|
+
* treats `--remote` as a usage error and a bare `run` stays local.
|
|
1067
|
+
*/
|
|
1068
|
+
function resolveHub(root) {
|
|
1069
|
+
const file = readHubFileConfig(root);
|
|
1070
|
+
const rawUrl = envString("SVERKA_HUB_URL") ?? file.url;
|
|
1071
|
+
const url = rawUrl !== void 0 ? normalizeHubUrl(rawUrl) : void 0;
|
|
1072
|
+
const token = envString("SVERKA_HUB_TOKEN") ?? (url !== void 0 ? storedTokenFor(url) : void 0);
|
|
1073
|
+
if (url === void 0 || token === void 0) return null;
|
|
1074
|
+
return {
|
|
1075
|
+
config: {
|
|
1076
|
+
url,
|
|
1077
|
+
token,
|
|
1078
|
+
project: file.project ?? projectSlug(root)
|
|
1079
|
+
},
|
|
1080
|
+
cache: file.cache ?? true,
|
|
1081
|
+
upload: file.upload ?? true,
|
|
1082
|
+
enabled: file.enabled === true
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
//#endregion
|
|
950
1086
|
//#region src/commands/run.ts
|
|
951
1087
|
/** Pure-argument guards — an invalid flag fails fast instead of surfacing
|
|
952
1088
|
* as a recurring run failure inside the watch supervisor. */
|
|
@@ -968,6 +1104,7 @@ async function runCommand(args, global, output, start) {
|
|
|
968
1104
|
if (detected !== void 0) output.errorLine(`no sverka.config — running ${detected.length} detected checks (${detected.join(", ")})`);
|
|
969
1105
|
for (const warning of warnings) output.errorLine(`warning: ${warning}`);
|
|
970
1106
|
const entryId = resolveEntryId(graph, args.entryId);
|
|
1107
|
+
const remote = resolveRemote(args, global, output);
|
|
971
1108
|
const plan = bindRunPlan({
|
|
972
1109
|
graph,
|
|
973
1110
|
entryId
|
|
@@ -975,10 +1112,13 @@ async function runCommand(args, global, output, start) {
|
|
|
975
1112
|
const artifactDir = join(global.root, ".sverka", "artifacts");
|
|
976
1113
|
const { events, runStatus, renderer } = await consumeEvents(createEngine({
|
|
977
1114
|
drivers: buildDrivers(executor),
|
|
1115
|
+
agentDrivers: resolveAgentDrivers(process$1.env),
|
|
978
1116
|
maxConcurrent: args.jobs ?? 4
|
|
979
1117
|
}), plan, {
|
|
980
1118
|
workspace: global.root,
|
|
981
|
-
artifactDir
|
|
1119
|
+
artifactDir,
|
|
1120
|
+
cache: remote?.stores.cache,
|
|
1121
|
+
snapshotStore: remote?.stores.snapshotStore
|
|
982
1122
|
}, global, output, graph, args);
|
|
983
1123
|
const durationMs = Date.now() - start;
|
|
984
1124
|
const evaluation = await evaluateRun(fmt.evaluate, artifactDir, global, output, events, renderer, args, start);
|
|
@@ -994,6 +1134,7 @@ async function runCommand(args, global, output, start) {
|
|
|
994
1134
|
...detected !== void 0 ? { detected } : {},
|
|
995
1135
|
...args.report !== void 0 ? { report: args.report } : {}
|
|
996
1136
|
}, events, output);
|
|
1137
|
+
if (remote !== void 0 && remote.upload && report !== void 0) await uploadReportSafe(remote, report, entryId, findRunId(events), output);
|
|
997
1138
|
await finalizeRenderer(fmt, renderer, args, global, output);
|
|
998
1139
|
if (evaluation.collectionFailed) return evaluation.exitCode;
|
|
999
1140
|
writeRunOutput({
|
|
@@ -1011,6 +1152,52 @@ async function runCommand(args, global, output, start) {
|
|
|
1011
1152
|
if (evaluation.exitCode !== 0) return evaluation.exitCode;
|
|
1012
1153
|
return exitCodeForStatus(runStatus);
|
|
1013
1154
|
}
|
|
1155
|
+
/** Build the remote wiring for a run. `--remote` with no hub configured
|
|
1156
|
+
* is a usage error naming `sverka login`; a hub.json-enabled run whose
|
|
1157
|
+
* credentials are missing warns and runs local-only (degrade, don't
|
|
1158
|
+
* fail). */
|
|
1159
|
+
function resolveRemote(args, global, output) {
|
|
1160
|
+
const hub = resolveHub(global.root);
|
|
1161
|
+
const requested = args.remote === true;
|
|
1162
|
+
if (hub === null) {
|
|
1163
|
+
if (requested) throw new CliError("--remote needs a hub: set SVERKA_HUB_URL + SVERKA_HUB_TOKEN, run `sverka login --hub <url> --token <t>`, or add .sverka/hub.json", "MISSING_ARG", ExitCode.UsageError);
|
|
1164
|
+
if (readHubFileConfig(global.root).enabled === true) output.errorLine("warning: .sverka/hub.json enables remote runs but no hub credentials resolved — running local-only (see `sverka login`)");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (!requested && !hub.enabled) return void 0;
|
|
1168
|
+
const circuit = createRemoteCircuit();
|
|
1169
|
+
const remoteConfig = {
|
|
1170
|
+
...hub.config,
|
|
1171
|
+
circuit
|
|
1172
|
+
};
|
|
1173
|
+
const cache = hub.cache ? createTieredCacheStore({
|
|
1174
|
+
local: createFileCacheStore({ cacheDir: join(global.root, ".sverka", "cache") }),
|
|
1175
|
+
remote: createRemoteCacheStore(remoteConfig),
|
|
1176
|
+
onWarn: (msg) => output.errorLine(`warning: ${msg}`)
|
|
1177
|
+
}) : void 0;
|
|
1178
|
+
return {
|
|
1179
|
+
config: remoteConfig,
|
|
1180
|
+
upload: hub.upload,
|
|
1181
|
+
stores: {
|
|
1182
|
+
...cache !== void 0 ? { cache } : {},
|
|
1183
|
+
snapshotStore: createRemoteSnapshotStore(remoteConfig)
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/** Upload the run report — warns `remote.upload-failed` and swallows the
|
|
1188
|
+
* error per Spec 55 ("remote failures degrade to local + warn"). */
|
|
1189
|
+
async function uploadReportSafe(remote, report, entryId, runId, output) {
|
|
1190
|
+
try {
|
|
1191
|
+
const { uploadRunReport } = await import("@sverka/storage");
|
|
1192
|
+
const result = await uploadRunReport(remote.config, report.payload, report.findingsList, {
|
|
1193
|
+
entry: entryId,
|
|
1194
|
+
...runId !== void 0 ? { runId } : {}
|
|
1195
|
+
});
|
|
1196
|
+
output.debug(`remote: uploaded run report as ${result.runId}`);
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
output.errorLine(`warning: remote.upload-failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1014
1201
|
/** Post-run: announce the HTML report path (sarif/web print their own),
|
|
1015
1202
|
* then keep interactive renderers mounted until the user quits. */
|
|
1016
1203
|
async function finalizeRenderer(fmt, renderer, args, global, output) {
|
|
@@ -1090,7 +1277,9 @@ async function writeReportSafe(ctx, events, output) {
|
|
|
1090
1277
|
return {
|
|
1091
1278
|
html: artifacts.htmlPath,
|
|
1092
1279
|
json: join(artifacts.dir, "report.json"),
|
|
1093
|
-
findings: artifacts.findingsCount
|
|
1280
|
+
findings: artifacts.findingsCount,
|
|
1281
|
+
payload: artifacts.payload,
|
|
1282
|
+
findingsList: artifacts.findings
|
|
1094
1283
|
};
|
|
1095
1284
|
} catch (err) {
|
|
1096
1285
|
if (err instanceof CliError) throw err;
|
|
@@ -1198,7 +1387,9 @@ async function consumeEvents(engine, plan, ctx, global, output, graph, args) {
|
|
|
1198
1387
|
for await (const event of engine.run({
|
|
1199
1388
|
plan,
|
|
1200
1389
|
workspace: ctx.workspace,
|
|
1201
|
-
artifactDir: ctx.artifactDir
|
|
1390
|
+
artifactDir: ctx.artifactDir,
|
|
1391
|
+
...ctx.cache !== void 0 ? { cache: ctx.cache } : {},
|
|
1392
|
+
...ctx.snapshotStore !== void 0 ? { snapshotStore: ctx.snapshotStore } : {}
|
|
1202
1393
|
})) {
|
|
1203
1394
|
events.push(event);
|
|
1204
1395
|
renderer?.onEvent(event);
|
|
@@ -1464,11 +1655,14 @@ async function writeRunArtifacts(opts) {
|
|
|
1464
1655
|
const warnings = [];
|
|
1465
1656
|
const findings = await collectRunFindings(opts, warnings);
|
|
1466
1657
|
const htmlPath = await writeReportHtml(opts.reportPath !== void 0 ? resolveUnderRoot(opts.root, opts.reportPath) : join(dir, "report.html"), findings, warnings);
|
|
1467
|
-
|
|
1658
|
+
const payload = runReportPayload(opts, findings.length, warnings);
|
|
1659
|
+
writeFileSync(join(dir, "report.json"), JSON.stringify(payload, null, 2), "utf-8");
|
|
1468
1660
|
return {
|
|
1469
1661
|
dir,
|
|
1470
1662
|
htmlPath,
|
|
1471
|
-
findingsCount: findings.length
|
|
1663
|
+
findingsCount: findings.length,
|
|
1664
|
+
payload,
|
|
1665
|
+
findings
|
|
1472
1666
|
};
|
|
1473
1667
|
}
|
|
1474
1668
|
function exitCodeForStatus(runStatus) {
|
|
@@ -2265,6 +2459,336 @@ async function uiCommand(args, global, output, _start) {
|
|
|
2265
2459
|
}
|
|
2266
2460
|
}
|
|
2267
2461
|
//#endregion
|
|
2462
|
+
//#region src/commands/agent.ts
|
|
2463
|
+
const RESULT_FILE = "agent-result.json";
|
|
2464
|
+
const WRITES_FILE$1 = "sverka-writes.json";
|
|
2465
|
+
async function agentCommand(args, global, output, _start) {
|
|
2466
|
+
const env = process$1.env;
|
|
2467
|
+
const engine = resolveAgentEngine(env);
|
|
2468
|
+
const prompt = env[AGENT_ENV.prompt];
|
|
2469
|
+
if (prompt === void 0 || prompt === "") throw new CliError(`sverka agent: ${AGENT_ENV.prompt} is not set — the compiled agent job provides it`, "MISSING_ARG", ExitCode.UsageError);
|
|
2470
|
+
const outDir = args.outputDir ?? global.root;
|
|
2471
|
+
const mentions = parseMentionList(env[AGENT_ENV.mention]);
|
|
2472
|
+
const commentBody = env["COMMENT_BODY"];
|
|
2473
|
+
if (mentions.length > 0 && commentBody !== void 0 && !mentions.some((m) => commentBody.includes(m))) {
|
|
2474
|
+
output.writeLine(`sverka agent: comment does not contain a configured mention (${mentions.join(", ")}) — skipping`);
|
|
2475
|
+
await writeArtifacts(outDir, {
|
|
2476
|
+
text: "",
|
|
2477
|
+
finishReason: "stop"
|
|
2478
|
+
});
|
|
2479
|
+
return ExitCode.Success;
|
|
2480
|
+
}
|
|
2481
|
+
const driver = resolveAgentDrivers(env).find((d) => d.canExecute(engine));
|
|
2482
|
+
if (driver === void 0) throw noAgentDriverError(engine, `sverka agent, engine '${engine}'`);
|
|
2483
|
+
const model = env[AGENT_ENV.model];
|
|
2484
|
+
const maxTokens = env[AGENT_ENV.maxTokens];
|
|
2485
|
+
const request = {
|
|
2486
|
+
engine,
|
|
2487
|
+
prompt,
|
|
2488
|
+
...model !== void 0 ? { model } : {},
|
|
2489
|
+
...maxTokens !== void 0 ? { maxTokens: parseMaxTokens(maxTokens) } : {}
|
|
2490
|
+
};
|
|
2491
|
+
output.debug(`sverka agent: engine=${engine} driver=${driver.name}`);
|
|
2492
|
+
const result = await driver.executeAgent(request);
|
|
2493
|
+
if (result.finishReason === "error") {
|
|
2494
|
+
output.errorLine("sverka agent: agent finished with reason 'error'");
|
|
2495
|
+
return ExitCode.RuntimeError;
|
|
2496
|
+
}
|
|
2497
|
+
await writeArtifacts(outDir, result, collectAgentWrites(result));
|
|
2498
|
+
if (result.text !== "") output.writeLine(result.text);
|
|
2499
|
+
return ExitCode.Success;
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* SVERKA_MENTION carries one mention string, or a JSON array when several
|
|
2503
|
+
* comment entries with different mentions reach the same job.
|
|
2504
|
+
*/
|
|
2505
|
+
function parseMentionList(raw) {
|
|
2506
|
+
if (raw === void 0) return [];
|
|
2507
|
+
try {
|
|
2508
|
+
const parsed = JSON.parse(raw);
|
|
2509
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((m) => typeof m === "string")) return parsed;
|
|
2510
|
+
} catch {}
|
|
2511
|
+
return [raw];
|
|
2512
|
+
}
|
|
2513
|
+
function parseMaxTokens(raw) {
|
|
2514
|
+
const n = Number(raw);
|
|
2515
|
+
if (!Number.isInteger(n) || n <= 0) throw new CliError(`sverka agent: ${AGENT_ENV.maxTokens} must be a positive integer, got '${raw}'`, "INVALID_FLAG", ExitCode.UsageError);
|
|
2516
|
+
return n;
|
|
2517
|
+
}
|
|
2518
|
+
async function writeArtifacts(dir, result, writes = []) {
|
|
2519
|
+
await writeFile(join(dir, RESULT_FILE), JSON.stringify(result, null, 2), "utf-8");
|
|
2520
|
+
await writeFile(join(dir, WRITES_FILE$1), JSON.stringify({ writes }, null, 2), "utf-8");
|
|
2521
|
+
}
|
|
2522
|
+
//#endregion
|
|
2523
|
+
//#region src/commands/apply.ts
|
|
2524
|
+
const WRITES_FILE = "sverka-writes.json";
|
|
2525
|
+
const DECLARATIONS_ENV = "SVERKA_WRITE_DECLARATIONS";
|
|
2526
|
+
/** Write kinds `sverka apply` can execute. Everything else fails loudly. */
|
|
2527
|
+
const SUPPORTED_WRITE_KINDS = /* @__PURE__ */ new Set(["comment"]);
|
|
2528
|
+
async function applyCommand(args, global, output, _start) {
|
|
2529
|
+
const provider = args.provider ?? detectProvider();
|
|
2530
|
+
const writes = await loadWrites(args.file ?? join(global.root, WRITES_FILE));
|
|
2531
|
+
const declarations = loadDeclarations();
|
|
2532
|
+
const applied = [];
|
|
2533
|
+
for (const write of writes) {
|
|
2534
|
+
if (!declarations.some((d) => d.kind === write.kind)) throw new CliError(`sverka apply: write kind '${write.kind}' is not declared in permissions.write (declarations: ${declarations.map((d) => d.kind).join(", ") || "none"})`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2535
|
+
if (!SUPPORTED_WRITE_KINDS.has(write.kind)) throw new CliError(`sverka apply: write kind '${write.kind}' is declared but not supported by sverka apply (supported: ${[...SUPPORTED_WRITE_KINDS].join(", ")})`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2536
|
+
applied.push(await applyWrite(provider, write, declarations));
|
|
2537
|
+
}
|
|
2538
|
+
const summary = applied.length > 0 ? ` — ${applied.join("; ")}` : "";
|
|
2539
|
+
output.writeLine(`sverka apply: applied ${applied.length} write(s) via ${provider}${summary}`);
|
|
2540
|
+
return ExitCode.Success;
|
|
2541
|
+
}
|
|
2542
|
+
function detectProvider() {
|
|
2543
|
+
if (process$1.env["GITLAB_CI"] !== void 0 || process$1.env["CI_API_V4_URL"]) return "gitlab";
|
|
2544
|
+
if (process$1.env["GITHUB_ACTIONS"] !== void 0) return "github";
|
|
2545
|
+
throw new CliError("sverka apply: --provider is required when CI provider cannot be detected (gitlab|github)", "MISSING_ARG", ExitCode.UsageError);
|
|
2546
|
+
}
|
|
2547
|
+
async function loadWrites(file) {
|
|
2548
|
+
let raw;
|
|
2549
|
+
try {
|
|
2550
|
+
raw = await readFile(file, "utf-8");
|
|
2551
|
+
} catch {
|
|
2552
|
+
throw new CliError(`sverka apply: writes artifact not found: ${file} — the agent job must produce ${WRITES_FILE}`, "MISSING_ARG", ExitCode.RuntimeError);
|
|
2553
|
+
}
|
|
2554
|
+
let parsed;
|
|
2555
|
+
try {
|
|
2556
|
+
parsed = JSON.parse(raw);
|
|
2557
|
+
} catch (e) {
|
|
2558
|
+
throw new CliError(`sverka apply: ${WRITES_FILE} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2559
|
+
}
|
|
2560
|
+
const items = extractWritesItems(parsed);
|
|
2561
|
+
if (items === void 0) throw new CliError(`sverka apply: ${WRITES_FILE} must be an object with a 'writes' array`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2562
|
+
return items.map((item, i) => {
|
|
2563
|
+
if (typeof item !== "object" || item === null || typeof item.kind !== "string") throw new CliError(`sverka apply: writes[${i}] must be an object with a string 'kind'`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2564
|
+
return item;
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
/** The artifact is either a bare array or an object with a 'writes' array. */
|
|
2568
|
+
function extractWritesItems(parsed) {
|
|
2569
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2570
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
2571
|
+
const w = parsed.writes;
|
|
2572
|
+
return Array.isArray(w) ? w : void 0;
|
|
2573
|
+
}
|
|
2574
|
+
function loadDeclarations() {
|
|
2575
|
+
const raw = process$1.env[DECLARATIONS_ENV];
|
|
2576
|
+
if (raw === void 0 || raw === "") throw new CliError(`sverka apply: ${DECLARATIONS_ENV} is not set — the __apply job declares it from permissions.write`, "MISSING_ARG", ExitCode.RuntimeError);
|
|
2577
|
+
let parsed;
|
|
2578
|
+
try {
|
|
2579
|
+
parsed = JSON.parse(raw);
|
|
2580
|
+
} catch (e) {
|
|
2581
|
+
throw new CliError(`sverka apply: ${DECLARATIONS_ENV} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2582
|
+
}
|
|
2583
|
+
if (!Array.isArray(parsed)) throw new CliError(`sverka apply: ${DECLARATIONS_ENV} must be a JSON array of write declarations`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2584
|
+
return parsed;
|
|
2585
|
+
}
|
|
2586
|
+
async function applyWrite(provider, write, declarations) {
|
|
2587
|
+
if (write.kind === "comment") return provider === "gitlab" ? applyGitlabComment(write, declarations) : applyGithubComment(write);
|
|
2588
|
+
throw new CliError(`sverka apply: write kind '${write.kind}' is not supported`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2589
|
+
}
|
|
2590
|
+
function requireBody(write) {
|
|
2591
|
+
if (typeof write.body !== "string" || write.body === "") throw new CliError("sverka apply: comment write requires a non-empty 'body' string", "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2592
|
+
return write.body;
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* The object kind a comment write targets. `write.on` wins when present
|
|
2596
|
+
* (and must be a known kind — anything else fails loudly rather than
|
|
2597
|
+
* silently falling back); otherwise the triggering context decides: a
|
|
2598
|
+
* forwarded MR_IID means the comment fired on a merge request.
|
|
2599
|
+
*/
|
|
2600
|
+
function resolveCommentTarget(write) {
|
|
2601
|
+
if (write.on === "issue" || write.on === "merge_request") return write.on;
|
|
2602
|
+
if (write.on !== void 0) throw new CliError(`sverka apply: comment write 'on' must be issue|merge_request, got '${JSON.stringify(write.on)}'`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2603
|
+
return process$1.env["MR_IID"] !== void 0 ? "merge_request" : "issue";
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Resolve the target iid for a comment write. When the triggering context
|
|
2607
|
+
* supplies an iid (the webhook forwarded MR_IID/ISSUE_IID), it wins — a
|
|
2608
|
+
* write.iid that disagrees is model output trying to redirect the comment
|
|
2609
|
+
* to another target, which fails loudly.
|
|
2610
|
+
*/
|
|
2611
|
+
function requireIid(write, envKeys) {
|
|
2612
|
+
const contextRaw = envKeys.map((k) => process$1.env[k]).find((v) => v !== void 0 && v !== "");
|
|
2613
|
+
if (contextRaw !== void 0) {
|
|
2614
|
+
const ctx = Number(contextRaw);
|
|
2615
|
+
if (!Number.isInteger(ctx) || ctx <= 0) throw new CliError(`sverka apply: context iid '${contextRaw}' is not a positive integer`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2616
|
+
if (write.iid !== void 0 && Number(write.iid) !== ctx) throw new CliError(`sverka apply: write iid '${JSON.stringify(write.iid)}' does not match the triggering context iid ${ctx}`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2617
|
+
return ctx;
|
|
2618
|
+
}
|
|
2619
|
+
if (write.iid === void 0) throw new CliError(`sverka apply: comment write needs a target iid (${envKeys.join(" or ")} env var, or an 'iid' field)`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2620
|
+
const n = Number(write.iid);
|
|
2621
|
+
if (!Number.isInteger(n) || n <= 0) throw new CliError(`sverka apply: comment write iid must be a positive integer, got '${JSON.stringify(write.iid)}'`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2622
|
+
return n;
|
|
2623
|
+
}
|
|
2624
|
+
async function applyGitlabComment(write, declarations) {
|
|
2625
|
+
const { api, project, token } = gitlabApplyEnv();
|
|
2626
|
+
const body = requireBody(write);
|
|
2627
|
+
const on = resolveCommentTarget(write);
|
|
2628
|
+
if (!declarations.some((d) => d.kind === "comment" && d.target === on)) throw new CliError(`sverka apply: comment write targets '${on}' but no declaration declares it (declared comment targets: ${declarations.filter((d) => d.kind === "comment").map((d) => d.target).join(", ") || "none"})`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2629
|
+
const iid = requireIid(write, on === "merge_request" ? ["MR_IID"] : ["ISSUE_IID", "SVERKA_ISSUE_IID"]);
|
|
2630
|
+
await postJson(`${api}${on === "merge_request" ? `/projects/${encodeURIComponent(project)}/merge_requests/${iid}/notes` : `/projects/${encodeURIComponent(project)}/issues/${iid}/notes`}`, token, { body }, "PRIVATE-TOKEN");
|
|
2631
|
+
return `comment on ${on} !${iid}`;
|
|
2632
|
+
}
|
|
2633
|
+
/** Provider-supplied env for a GitLab apply call — all CI-controlled. */
|
|
2634
|
+
function gitlabApplyEnv() {
|
|
2635
|
+
const env = process$1.env;
|
|
2636
|
+
const api = env["CI_API_V4_URL"] ?? "https://gitlab.com/api/v4";
|
|
2637
|
+
const project = env["CI_PROJECT_ID"];
|
|
2638
|
+
if (project === void 0 || project === "") throw new CliError("sverka apply: CI_PROJECT_ID is not set — run inside a GitLab job or set it explicitly", "MISSING_ARG", ExitCode.RuntimeError);
|
|
2639
|
+
const token = env["SVERKA_APPLY_TOKEN"] || env["GITLAB_TOKEN"];
|
|
2640
|
+
if (token === void 0 || token === "") throw new CliError("sverka apply: SVERKA_APPLY_TOKEN is not set — scope it to the 'sverka-apply' environment (see engdocs/user/gitlab/agentic.md)", "MISSING_ARG", ExitCode.RuntimeError);
|
|
2641
|
+
return {
|
|
2642
|
+
api,
|
|
2643
|
+
project,
|
|
2644
|
+
token
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
async function applyGithubComment(write) {
|
|
2648
|
+
const env = process$1.env;
|
|
2649
|
+
const api = env["GITHUB_API_URL"] ?? "https://api.github.com";
|
|
2650
|
+
const repo = env["GITHUB_REPOSITORY"];
|
|
2651
|
+
if (repo === void 0 || repo === "") throw new CliError("sverka apply: GITHUB_REPOSITORY is not set — run inside a GitHub Actions job or set it explicitly", "MISSING_ARG", ExitCode.RuntimeError);
|
|
2652
|
+
const token = env["SVERKA_APPLY_TOKEN"] || env["GH_TOKEN"] || env["GITHUB_TOKEN"];
|
|
2653
|
+
if (token === void 0 || token === "") throw new CliError("sverka apply: GH_TOKEN/GITHUB_TOKEN is not set", "MISSING_ARG", ExitCode.RuntimeError);
|
|
2654
|
+
const body = requireBody(write);
|
|
2655
|
+
const issue = requireIid(write, ["SVERKA_ISSUE_IID", "ISSUE_IID"]);
|
|
2656
|
+
await postJson(`${api}/repos/${repo}/issues/${issue}/comments`, token, { body }, "Bearer");
|
|
2657
|
+
return `comment on issue #${issue}`;
|
|
2658
|
+
}
|
|
2659
|
+
async function postJson(url, token, body, auth) {
|
|
2660
|
+
const headers = { "content-type": "application/json" };
|
|
2661
|
+
if (auth === "PRIVATE-TOKEN") headers["PRIVATE-TOKEN"] = token;
|
|
2662
|
+
else headers["authorization"] = `Bearer ${token}`;
|
|
2663
|
+
const res = await fetch(url, {
|
|
2664
|
+
method: "POST",
|
|
2665
|
+
headers,
|
|
2666
|
+
body: JSON.stringify(body)
|
|
2667
|
+
});
|
|
2668
|
+
if (!res.ok) {
|
|
2669
|
+
const text = await res.text().catch(() => "");
|
|
2670
|
+
throw new CliError(`sverka apply: provider request failed (${res.status} ${res.url}): ${text.slice(0, 300)}`, "PACKAGE_ERROR", ExitCode.RuntimeError);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
//#endregion
|
|
2674
|
+
//#region src/commands/login.ts
|
|
2675
|
+
async function loginCommand(args, _global, output, _start) {
|
|
2676
|
+
const url = args.hub ?? process.env["SVERKA_HUB_URL"];
|
|
2677
|
+
const token = args.token ?? process.env["SVERKA_HUB_TOKEN"];
|
|
2678
|
+
if (url === void 0) throw new CliError("missing --hub <url> (or set SVERKA_HUB_URL)", "MISSING_ARG", ExitCode.UsageError);
|
|
2679
|
+
if (!token) throw new CliError("missing --token <t> (or set SVERKA_HUB_TOKEN)", "MISSING_ARG", ExitCode.UsageError);
|
|
2680
|
+
let parsed;
|
|
2681
|
+
try {
|
|
2682
|
+
parsed = new URL(url);
|
|
2683
|
+
} catch {
|
|
2684
|
+
throw new CliError(`--hub must be a URL, got "${url}"`, "INVALID_FLAG", ExitCode.UsageError);
|
|
2685
|
+
}
|
|
2686
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new CliError(`--hub must be http(s), got "${parsed.protocol}"`, "INVALID_FLAG", ExitCode.UsageError);
|
|
2687
|
+
const normalized = normalizeHubUrl(url);
|
|
2688
|
+
const path = storeCredentials(normalized, token);
|
|
2689
|
+
output.writeLine(`stored credentials for ${normalized} in ${path}`);
|
|
2690
|
+
return ExitCode.Success;
|
|
2691
|
+
}
|
|
2692
|
+
//#endregion
|
|
2693
|
+
//#region src/commands/hub.ts
|
|
2694
|
+
async function hubCommand(args, global, output, _start) {
|
|
2695
|
+
if (args.action !== "serve") throw new CliError(`unknown hub action "${args.action ?? ""}" (supported: serve)`, "UNKNOWN_COMMAND", ExitCode.UsageError);
|
|
2696
|
+
if (args.port !== void 0 && (!Number.isInteger(args.port) || args.port < 0 || args.port > 65535)) throw new CliError("--port must be an integer in [0, 65535]", "INVALID_FLAG", ExitCode.UsageError);
|
|
2697
|
+
const dataDir = args.data ?? `${global.root}/.sverka-hub`;
|
|
2698
|
+
const server = await startHubServer({
|
|
2699
|
+
dataDir,
|
|
2700
|
+
...args.port !== void 0 ? { port: args.port } : {},
|
|
2701
|
+
...args.host !== void 0 ? { host: args.host } : {}
|
|
2702
|
+
});
|
|
2703
|
+
output.writeLine(`sverka hub listening on ${server.url}`);
|
|
2704
|
+
output.writeLine(` data: ${server.dataDir}`);
|
|
2705
|
+
output.writeLine(` dashboard: ${server.url}/`);
|
|
2706
|
+
output.writeLine(" (Ctrl+C to stop)");
|
|
2707
|
+
await new Promise((done) => {
|
|
2708
|
+
const stop = () => {
|
|
2709
|
+
process.removeListener("SIGINT", stop);
|
|
2710
|
+
process.removeListener("SIGTERM", stop);
|
|
2711
|
+
done();
|
|
2712
|
+
};
|
|
2713
|
+
process.on("SIGINT", stop);
|
|
2714
|
+
process.on("SIGTERM", stop);
|
|
2715
|
+
});
|
|
2716
|
+
await server.close();
|
|
2717
|
+
return ExitCode.Success;
|
|
2718
|
+
}
|
|
2719
|
+
//#endregion
|
|
2720
|
+
//#region src/commands/runs.ts
|
|
2721
|
+
function listLocalRuns(root) {
|
|
2722
|
+
const dir = join(root, ".sverka", "runs");
|
|
2723
|
+
if (!existsSync(dir)) return [];
|
|
2724
|
+
const rows = [];
|
|
2725
|
+
for (const entry of readdirSync(dir)) {
|
|
2726
|
+
const reportPath = join(dir, entry, "report.json");
|
|
2727
|
+
if (!existsSync(reportPath)) continue;
|
|
2728
|
+
let status = "unknown";
|
|
2729
|
+
let findings = null;
|
|
2730
|
+
try {
|
|
2731
|
+
const data = JSON.parse(readFileSync(reportPath, "utf8")).data ?? {};
|
|
2732
|
+
if (typeof data.status === "string") status = data.status;
|
|
2733
|
+
if (typeof data.findings === "number") findings = data.findings;
|
|
2734
|
+
} catch {}
|
|
2735
|
+
rows.push({
|
|
2736
|
+
runId: entry,
|
|
2737
|
+
status,
|
|
2738
|
+
findings,
|
|
2739
|
+
mtimeMs: statSync(reportPath).mtimeMs
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
return rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2743
|
+
}
|
|
2744
|
+
async function runsCommand(args, global, output, _start) {
|
|
2745
|
+
if (args.remote === true) return remoteRuns(args, global, output);
|
|
2746
|
+
return localRuns(args, global, output);
|
|
2747
|
+
}
|
|
2748
|
+
async function remoteRuns(args, global, output) {
|
|
2749
|
+
const hub = resolveHub(global.root);
|
|
2750
|
+
if (hub === null) throw new CliError("no hub configured — set SVERKA_HUB_URL + SVERKA_HUB_TOKEN, run `sverka login`, or add .sverka/hub.json", "MISSING_ARG", ExitCode.UsageError);
|
|
2751
|
+
const { listRuns } = await import("@sverka/storage");
|
|
2752
|
+
const runs = await listRuns(hub.config, { ...args.limit !== void 0 ? { limit: args.limit } : {} });
|
|
2753
|
+
if (global.format === "json") {
|
|
2754
|
+
output.writeLine(JSON.stringify({
|
|
2755
|
+
command: "runs",
|
|
2756
|
+
data: runs
|
|
2757
|
+
}));
|
|
2758
|
+
return ExitCode.Success;
|
|
2759
|
+
}
|
|
2760
|
+
if (runs.length === 0) {
|
|
2761
|
+
output.writeLine(`no remote runs for ${hub.config.project} — run \`sverka run --remote\` to upload one`);
|
|
2762
|
+
return ExitCode.Success;
|
|
2763
|
+
}
|
|
2764
|
+
output.writeLine(`remote runs for ${hub.config.project} (${hub.config.url})`);
|
|
2765
|
+
for (const run of runs) {
|
|
2766
|
+
const when = run.startedAt === null ? "—" : new Date(run.startedAt).toISOString().slice(0, 19) + "Z";
|
|
2767
|
+
const duration = run.durationMs === null ? "—" : `${(run.durationMs / 1e3).toFixed(1)}s`;
|
|
2768
|
+
output.writeLine(` ${run.runId} ${run.status.padEnd(9)} ${run.entry.padEnd(20)} ${when} ${duration} findings:${run.findingCounts.total}`);
|
|
2769
|
+
}
|
|
2770
|
+
return ExitCode.Success;
|
|
2771
|
+
}
|
|
2772
|
+
function localRuns(args, global, output) {
|
|
2773
|
+
const shown = listLocalRuns(global.root).slice(0, args.limit ?? 20);
|
|
2774
|
+
if (global.format === "json") {
|
|
2775
|
+
output.writeLine(JSON.stringify({
|
|
2776
|
+
command: "runs",
|
|
2777
|
+
data: shown
|
|
2778
|
+
}));
|
|
2779
|
+
return ExitCode.Success;
|
|
2780
|
+
}
|
|
2781
|
+
if (shown.length === 0) {
|
|
2782
|
+
output.writeLine("no runs recorded — run `sverka run` first");
|
|
2783
|
+
return ExitCode.Success;
|
|
2784
|
+
}
|
|
2785
|
+
for (const run of shown) {
|
|
2786
|
+
const when = new Date(run.mtimeMs).toISOString().slice(0, 19) + "Z";
|
|
2787
|
+
output.writeLine(` ${run.runId} ${run.status.padEnd(9)} ${when} findings:${run.findings ?? "—"}`);
|
|
2788
|
+
}
|
|
2789
|
+
return ExitCode.Success;
|
|
2790
|
+
}
|
|
2791
|
+
//#endregion
|
|
2268
2792
|
//#region src/main.ts
|
|
2269
2793
|
/** Build GlobalFlags from parsed yargs options. */
|
|
2270
2794
|
function buildGlobalFlags(parsed) {
|
|
@@ -2334,6 +2858,10 @@ function addRunCommand(y) {
|
|
|
2334
2858
|
type: "boolean",
|
|
2335
2859
|
alias: "w",
|
|
2336
2860
|
describe: "Re-run on file changes (debounced 300ms)"
|
|
2861
|
+
}).option("remote", {
|
|
2862
|
+
type: "boolean",
|
|
2863
|
+
default: false,
|
|
2864
|
+
describe: "Remote hub run: remote cache + snapshot store + report upload"
|
|
2337
2865
|
});
|
|
2338
2866
|
}
|
|
2339
2867
|
/** Configure the plan subcommand options. */
|
|
@@ -2347,13 +2875,9 @@ function addPolicyCommand(y) {
|
|
|
2347
2875
|
demandOption: true
|
|
2348
2876
|
}).option("baseline", { type: "string" });
|
|
2349
2877
|
}
|
|
2350
|
-
/** Configure the synth subcommand options. */
|
|
2878
|
+
/** Configure the synth subcommand options. Alias for compile — same flags. */
|
|
2351
2879
|
function addSynthCommand(y) {
|
|
2352
|
-
return y
|
|
2353
|
-
type: "string",
|
|
2354
|
-
demandOption: true,
|
|
2355
|
-
choices: ["github", "gitlab"]
|
|
2356
|
-
});
|
|
2880
|
+
return addCompileCommand(y);
|
|
2357
2881
|
}
|
|
2358
2882
|
/** Configure the compile subcommand options. */
|
|
2359
2883
|
function addCompileCommand(y) {
|
|
@@ -2407,6 +2931,45 @@ function addUiCommand(y) {
|
|
|
2407
2931
|
describe: "Host to bind"
|
|
2408
2932
|
});
|
|
2409
2933
|
}
|
|
2934
|
+
/** Configure the login subcommand options. */
|
|
2935
|
+
function addLoginCommand(y) {
|
|
2936
|
+
return y.option("hub", {
|
|
2937
|
+
type: "string",
|
|
2938
|
+
describe: "Hub base URL (or SVERKA_HUB_URL)"
|
|
2939
|
+
}).option("token", {
|
|
2940
|
+
type: "string",
|
|
2941
|
+
describe: "Hub bearer token (or SVERKA_HUB_TOKEN)"
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
/** Configure the hub subcommand options. */
|
|
2945
|
+
function addHubCommand(y) {
|
|
2946
|
+
return y.positional("action", {
|
|
2947
|
+
type: "string",
|
|
2948
|
+
choices: ["serve"],
|
|
2949
|
+
describe: "hub action (serve)"
|
|
2950
|
+
}).option("port", {
|
|
2951
|
+
type: "number",
|
|
2952
|
+
default: 7357,
|
|
2953
|
+
describe: "Port to listen on"
|
|
2954
|
+
}).option("host", {
|
|
2955
|
+
type: "string",
|
|
2956
|
+
describe: "Host to bind"
|
|
2957
|
+
}).option("data", {
|
|
2958
|
+
type: "string",
|
|
2959
|
+
describe: "Data directory (default: .sverka-hub under --root)"
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
/** Configure the runs subcommand options. */
|
|
2963
|
+
function addRunsCommand(y) {
|
|
2964
|
+
return y.option("remote", {
|
|
2965
|
+
type: "boolean",
|
|
2966
|
+
default: false,
|
|
2967
|
+
describe: "List runs stored on the configured hub"
|
|
2968
|
+
}).option("limit", {
|
|
2969
|
+
type: "number",
|
|
2970
|
+
describe: "Max runs to list"
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2410
2973
|
function buildParser() {
|
|
2411
2974
|
return yargs([]).scriptName("sverka").version(cliVersion()).option("format", {
|
|
2412
2975
|
type: "string",
|
|
@@ -2433,7 +2996,17 @@ function buildParser() {
|
|
|
2433
2996
|
type: "boolean",
|
|
2434
2997
|
alias: "v",
|
|
2435
2998
|
default: false
|
|
2436
|
-
}).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).
|
|
2999
|
+
}).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).command("agent", "Run a single agent step from SVERKA_AGENT_* env (CI agent jobs)", (y) => y.option("output-dir", {
|
|
3000
|
+
type: "string",
|
|
3001
|
+
describe: "Directory for agent-result.json + sverka-writes.json (default: root)"
|
|
3002
|
+
})).command("apply", "Validate + apply agent writes artifact (CI __apply jobs)", (y) => y.option("provider", {
|
|
3003
|
+
type: "string",
|
|
3004
|
+
choices: ["gitlab", "github"],
|
|
3005
|
+
describe: "CI provider (auto-detected from CI env when omitted)"
|
|
3006
|
+
}).option("file", {
|
|
3007
|
+
type: "string",
|
|
3008
|
+
describe: "Path to sverka-writes.json (default: <root>/sverka-writes.json)"
|
|
3009
|
+
})).command("login", "Store hub credentials (~/.config/sverka/credentials)", addLoginCommand).command("hub <action>", "Self-hosted run hub (serve: cache + history + findings)", addHubCommand).command("runs", "List run history (local, or --remote)", addRunsCommand).demandCommand(1, "No command given").strict().fail((msg, err) => {
|
|
2437
3010
|
if (err) throw err;
|
|
2438
3011
|
throw new CliError(msg, "UNKNOWN_COMMAND", ExitCode.UsageError);
|
|
2439
3012
|
});
|
|
@@ -2455,6 +3028,11 @@ async function dispatch(command, parsed, global, output, start) {
|
|
|
2455
3028
|
case "doctor": return doctorCommand(global, output, start);
|
|
2456
3029
|
case "view": return dispatchView(parsed, global, output, start);
|
|
2457
3030
|
case "ui": return dispatchUi(parsed, global, output, start);
|
|
3031
|
+
case "agent": return dispatchAgent(parsed, global, output, start);
|
|
3032
|
+
case "apply": return dispatchApply(parsed, global, output, start);
|
|
3033
|
+
case "login": return dispatchLogin(parsed, global, output, start);
|
|
3034
|
+
case "hub": return dispatchHub(parsed, global, output, start);
|
|
3035
|
+
case "runs": return dispatchRuns(parsed, global, output, start);
|
|
2458
3036
|
default: throw new CliError(`unknown command: ${command}`, "UNKNOWN_COMMAND", ExitCode.UsageError);
|
|
2459
3037
|
}
|
|
2460
3038
|
}
|
|
@@ -2491,6 +3069,7 @@ function dispatchRun(parsed, global, output, start) {
|
|
|
2491
3069
|
if (parsed.tui === true) args.tui = true;
|
|
2492
3070
|
if (parsed.tui === false) args.tui = false;
|
|
2493
3071
|
if (parsed.watch === true) args.watch = true;
|
|
3072
|
+
if (parsed.remote === true) args.remote = true;
|
|
2494
3073
|
return runCommand(args, global, output, start);
|
|
2495
3074
|
}
|
|
2496
3075
|
function dispatchPolicy(parsed, global, output, start) {
|
|
@@ -2499,7 +3078,11 @@ function dispatchPolicy(parsed, global, output, start) {
|
|
|
2499
3078
|
return policyCommand(args, global, output, start);
|
|
2500
3079
|
}
|
|
2501
3080
|
function dispatchSynth(parsed, global, output, start) {
|
|
2502
|
-
|
|
3081
|
+
const args = { target: parsed.target === "gitlab" ? "gitlab" : "github" };
|
|
3082
|
+
if (typeof parsed.output === "string") args.output = parsed.output;
|
|
3083
|
+
if (typeof parsed.outputDir === "string") args.outputDir = parsed.outputDir;
|
|
3084
|
+
if (parsed.pin === true) args.pin = true;
|
|
3085
|
+
return synthCommand(args, global, output, start);
|
|
2503
3086
|
}
|
|
2504
3087
|
function dispatchCompile(parsed, global, output, start) {
|
|
2505
3088
|
const args = { target: parsed.target === "gitlab" ? "gitlab" : "github" };
|
|
@@ -2520,6 +3103,36 @@ function dispatchUi(parsed, global, output, start) {
|
|
|
2520
3103
|
if (typeof parsed.host === "string") args.host = parsed.host;
|
|
2521
3104
|
return uiCommand(args, global, output, start);
|
|
2522
3105
|
}
|
|
3106
|
+
function dispatchAgent(parsed, global, output, start) {
|
|
3107
|
+
const args = {};
|
|
3108
|
+
if (typeof parsed.outputDir === "string") args.outputDir = parsed.outputDir;
|
|
3109
|
+
return agentCommand(args, global, output, start);
|
|
3110
|
+
}
|
|
3111
|
+
function dispatchApply(parsed, global, output, start) {
|
|
3112
|
+
const args = {};
|
|
3113
|
+
if (parsed.provider === "gitlab" || parsed.provider === "github") args.provider = parsed.provider;
|
|
3114
|
+
if (typeof parsed.file === "string") args.file = parsed.file;
|
|
3115
|
+
return applyCommand(args, global, output, start);
|
|
3116
|
+
}
|
|
3117
|
+
function dispatchLogin(parsed, global, output, start) {
|
|
3118
|
+
const args = {};
|
|
3119
|
+
if (typeof parsed.hub === "string") args.hub = parsed.hub;
|
|
3120
|
+
if (typeof parsed.token === "string") args.token = parsed.token;
|
|
3121
|
+
return loginCommand(args, global, output, start);
|
|
3122
|
+
}
|
|
3123
|
+
function dispatchHub(parsed, global, output, start) {
|
|
3124
|
+
const args = {};
|
|
3125
|
+
if (typeof parsed.action === "string") args.action = parsed.action;
|
|
3126
|
+
if (typeof parsed.port === "number") args.port = parsed.port;
|
|
3127
|
+
if (typeof parsed.data === "string") args.data = parsed.data;
|
|
3128
|
+
if (typeof parsed.host === "string") args.host = parsed.host;
|
|
3129
|
+
return hubCommand(args, global, output, start);
|
|
3130
|
+
}
|
|
3131
|
+
function dispatchRuns(parsed, global, output, start) {
|
|
3132
|
+
const args = { remote: parsed.remote === true };
|
|
3133
|
+
if (typeof parsed.limit === "number") args.limit = parsed.limit;
|
|
3134
|
+
return runsCommand(args, global, output, start);
|
|
3135
|
+
}
|
|
2523
3136
|
/**
|
|
2524
3137
|
* The main CLI entry point.
|
|
2525
3138
|
* @param argv Command-line arguments (excluding node and script path).
|