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