hermex 2.0.0-beta.9 ā 2.0.0
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/cli.mjs +169 -68
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +11 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import Table from "cli-table3";
|
|
5
5
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
@@ -27,6 +27,31 @@ import ora from "ora";
|
|
|
27
27
|
function formatCount(num) {
|
|
28
28
|
return num.toLocaleString();
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Format how far an upgrade candidate is past its age threshold
|
|
32
|
+
* @returns Formatted string (e.g., "40 days overdue", "1 day overdue")
|
|
33
|
+
*/
|
|
34
|
+
function formatDaysOverdue(releasedDaysAgo, thresholdDays) {
|
|
35
|
+
const overdue = releasedDaysAgo - thresholdDays;
|
|
36
|
+
return `${overdue} day${overdue === 1 ? "" : "s"} overdue`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format how long until an upgrade candidate breaches its age threshold
|
|
40
|
+
* @returns Formatted string (e.g., "12 days remaining", "1 day remaining")
|
|
41
|
+
*/
|
|
42
|
+
function formatDaysRemaining(daysRemaining) {
|
|
43
|
+
return `${daysRemaining} day${daysRemaining === 1 ? "" : "s"} remaining`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Join a list of items, showing only the first `limit` and summarizing the rest
|
|
47
|
+
* @returns Formatted string (e.g., "a, b and 3 other files", "a, b and 1 other file")
|
|
48
|
+
*/
|
|
49
|
+
function formatTruncatedList(items, noun, limit = 2) {
|
|
50
|
+
const shown = items.slice(0, limit).join(", ");
|
|
51
|
+
const rest = items.length - limit;
|
|
52
|
+
if (rest <= 0) return shown;
|
|
53
|
+
return `${shown} and ${rest} other ${noun}${rest === 1 ? "" : "s"}`;
|
|
54
|
+
}
|
|
30
55
|
//#endregion
|
|
31
56
|
//#region src/utils/print-summary.ts
|
|
32
57
|
function printHeader$4() {
|
|
@@ -171,26 +196,88 @@ function printPatternsChart(patterns) {
|
|
|
171
196
|
})), { maxWidth: 50 });
|
|
172
197
|
}
|
|
173
198
|
//#endregion
|
|
199
|
+
//#region src/utils/severity-format.ts
|
|
200
|
+
const ICONS = {
|
|
201
|
+
error: "š“",
|
|
202
|
+
warn: "š”",
|
|
203
|
+
info: "šµ",
|
|
204
|
+
success: "š¢"
|
|
205
|
+
};
|
|
206
|
+
const COLORS = {
|
|
207
|
+
error: chalk.red,
|
|
208
|
+
warn: chalk.yellow,
|
|
209
|
+
info: chalk.blue,
|
|
210
|
+
success: chalk.green
|
|
211
|
+
};
|
|
212
|
+
/** Colored-circle glyph for a severity ā carries meaning without relying on ANSI color. */
|
|
213
|
+
function severityIcon(severity) {
|
|
214
|
+
return ICONS[severity];
|
|
215
|
+
}
|
|
216
|
+
/** chalk color function for a severity, for text that needs coloring. */
|
|
217
|
+
function severityColor(severity) {
|
|
218
|
+
return COLORS[severity];
|
|
219
|
+
}
|
|
220
|
+
function formatViolationLine(opts) {
|
|
221
|
+
return ` ${opts.icon} ${opts.label.padEnd(14)} ${opts.description}`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolves an explicit color-on/off override from CLI flags or the NO_COLOR
|
|
225
|
+
* convention (https://no-color.org). Returns `undefined` when there's no
|
|
226
|
+
* explicit signal, meaning chalk's own auto-detection should be left alone
|
|
227
|
+
* (including its GitHub-Actions-aware detection of non-TTY streams).
|
|
228
|
+
*/
|
|
229
|
+
function resolveColorLevel(opts) {
|
|
230
|
+
if (opts.colorFlag === false) return 0;
|
|
231
|
+
if (opts.colorFlag === true) return 1;
|
|
232
|
+
if (opts.noColorEnv !== void 0) return 0;
|
|
233
|
+
}
|
|
234
|
+
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
235
|
+
function stripAnsiWrites(stream) {
|
|
236
|
+
const originalWrite = stream.write.bind(stream);
|
|
237
|
+
stream.write = ((chunk, ...rest) => {
|
|
238
|
+
return originalWrite(typeof chunk === "string" ? chunk.replace(ANSI_ESCAPE_PATTERN, "") : chunk, ...rest);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Beyond setting chalk's own level, this strips ANSI codes at the stream
|
|
243
|
+
* boundary when color is explicitly disabled. hermex also prints through
|
|
244
|
+
* cli-table3 and ora (whose spinner symbols come from log-symbols/yoctocolors)
|
|
245
|
+
* ā neither shares hermex's chalk instance or honors chalk.level, so mutating
|
|
246
|
+
* chalk alone can't make NO_COLOR/--no-color hold for their output too.
|
|
247
|
+
*/
|
|
248
|
+
function applyColorLevel(level) {
|
|
249
|
+
if (level === void 0) return;
|
|
250
|
+
chalk.level = level;
|
|
251
|
+
if (level === 0) {
|
|
252
|
+
stripAnsiWrites(process.stdout);
|
|
253
|
+
stripAnsiWrites(process.stderr);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
174
257
|
//#region src/utils/print-packages.ts
|
|
175
258
|
function printHeader() {
|
|
176
259
|
console.log(chalk.blueBright.bold("\nš¦ Packages\n"));
|
|
177
260
|
}
|
|
178
261
|
function formatPackageName(pkg, banned) {
|
|
179
262
|
let prefix = "";
|
|
180
|
-
if (pkg.releaseAge?.deprecated) prefix +=
|
|
181
|
-
if (banned) prefix += banned.severity === "error" ?
|
|
182
|
-
else if (pkg.internal) prefix +=
|
|
263
|
+
if (pkg.releaseAge?.deprecated) prefix += severityColor("error")("[DEPRECATED] ");
|
|
264
|
+
if (banned) prefix += banned.severity === "error" ? severityColor("error")("[BANNED] ") : severityColor("warn")("[RESTRICTED] ");
|
|
265
|
+
else if (pkg.internal) prefix += severityColor("warn")("[int] ");
|
|
183
266
|
return prefix + pkg.packageName;
|
|
184
267
|
}
|
|
185
268
|
function formatUpgradeCell(releaseAge) {
|
|
186
269
|
if (!releaseAge) return "";
|
|
187
|
-
const { worstLevel, upgrades, severity } = releaseAge;
|
|
188
|
-
if (!worstLevel)
|
|
270
|
+
const { worstLevel, upgrades, severity, pendingUpgrade } = releaseAge;
|
|
271
|
+
if (!worstLevel) {
|
|
272
|
+
if (pendingUpgrade) return `${severityIcon("info")} ${pendingUpgrade.semverBump} ${pendingUpgrade.version} (${formatDaysRemaining(pendingUpgrade.daysRemaining)})`;
|
|
273
|
+
return severityIcon("success");
|
|
274
|
+
}
|
|
189
275
|
const top = upgrades[0];
|
|
190
|
-
if (!top) return
|
|
276
|
+
if (!top) return severityIcon("success");
|
|
191
277
|
const suffix = severity === "warn" ? chalk.gray(" [not enforced]") : "";
|
|
192
|
-
|
|
193
|
-
return
|
|
278
|
+
const overdue = formatDaysOverdue(top.releasedDaysAgo, top.thresholdDays);
|
|
279
|
+
if (worstLevel === "mandatory_upgrade") return `${severityIcon(severity === "warn" ? "warn" : "error")} ${top.semverBump} ${top.version} (${overdue})${suffix}`;
|
|
280
|
+
return `${severityIcon("warn")} ${top.semverBump} ${top.version} (${overdue})${suffix}`;
|
|
194
281
|
}
|
|
195
282
|
function getBannedViolation(pkg, violations) {
|
|
196
283
|
return violations.find((v) => v.packageName === pkg.packageName);
|
|
@@ -265,13 +352,6 @@ function renderBar(percentage) {
|
|
|
265
352
|
const empty = BAR_WIDTH - filled;
|
|
266
353
|
return chalk.cyan("ā".repeat(filled)) + chalk.gray("ā".repeat(empty));
|
|
267
354
|
}
|
|
268
|
-
function formatComponents(components, max = 3) {
|
|
269
|
-
if (components.length === 0) return "";
|
|
270
|
-
const shown = components.slice(0, max);
|
|
271
|
-
const rest = components.length - max;
|
|
272
|
-
const list = shown.join(", ");
|
|
273
|
-
return rest > 0 ? `${list} (+${rest} more)` : list;
|
|
274
|
-
}
|
|
275
355
|
function printVersusResult(result) {
|
|
276
356
|
console.log(chalk.bold(` ${result.name}`));
|
|
277
357
|
console.log(chalk.gray(` ${"ā".repeat(50)}`));
|
|
@@ -281,7 +361,7 @@ function printVersusResult(result) {
|
|
|
281
361
|
const bar = renderBar(entry.percentage);
|
|
282
362
|
const pct = chalk.bold(`${entry.percentage.toFixed(1)}%`);
|
|
283
363
|
const usage = chalk.gray(`(${entry.count} usages)`);
|
|
284
|
-
const components = entry.components.length > 0 ? chalk.gray(` ${
|
|
364
|
+
const components = entry.components.length > 0 ? chalk.gray(` ${formatTruncatedList(entry.components, "component")}`) : "";
|
|
285
365
|
console.log(` ${name} ${bar} ${pct} ${usage}${components}`);
|
|
286
366
|
}
|
|
287
367
|
if (result.totalCount === 0) console.log(chalk.gray(" No usage detected for any package in this group."));
|
|
@@ -298,30 +378,23 @@ function formatRuleType(type) {
|
|
|
298
378
|
switch (type) {
|
|
299
379
|
case "detect_files": return "detect_files";
|
|
300
380
|
case "require_files": return "require_files";
|
|
301
|
-
case "forbid_packages": return "forbid_packages";
|
|
302
381
|
case "require_packages": return "require_packages";
|
|
303
382
|
case "require_scripts": return "require_scripts";
|
|
304
|
-
case "require_package_fields": return "
|
|
305
|
-
case "forbid_package_fields": return "
|
|
383
|
+
case "require_package_fields": return "package_fields";
|
|
384
|
+
case "forbid_package_fields": return "package_fields";
|
|
306
385
|
case "engine_version": return "engine_version";
|
|
307
386
|
case "codeowners": return "codeowners";
|
|
308
387
|
}
|
|
309
388
|
}
|
|
310
|
-
function ruleIcon(violation) {
|
|
311
|
-
if (violation.severity === "error") return chalk.red("ā");
|
|
312
|
-
if (violation.severity === "info") return chalk.blue("ā¹");
|
|
313
|
-
return chalk.yellow("ā ");
|
|
314
|
-
}
|
|
315
389
|
function describeViolation(v) {
|
|
316
390
|
const patterns = v.patterns.join(", ");
|
|
317
391
|
const suffix = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
318
|
-
if (v.type === "detect_files") return `${patterns} detected (${v.matchedFiles.map((f) => {
|
|
392
|
+
if (v.type === "detect_files") return `${patterns} detected (${formatTruncatedList(v.matchedFiles.map((f) => {
|
|
319
393
|
const parts = f.replace(/\\/g, "/").split("/");
|
|
320
394
|
return parts[parts.length - 1];
|
|
321
|
-
})
|
|
395
|
+
}), "file")})${suffix}`;
|
|
322
396
|
if (v.type === "require_files") return `${patterns} not found${suffix}`;
|
|
323
397
|
if (v.type === "require_packages") return `${patterns} not installed${suffix}`;
|
|
324
|
-
if (v.type === "forbid_packages") return `${patterns} is forbidden${suffix}`;
|
|
325
398
|
if (v.type === "require_scripts") return `script ${patterns} missing in package.json${suffix}`;
|
|
326
399
|
if (v.type === "require_package_fields") {
|
|
327
400
|
if (v.fieldPath && v.actualValue !== void 0) return `field ${v.fieldPath} is ${chalk.yellow(v.actualValue)}, does not match required value${suffix}`;
|
|
@@ -334,9 +407,7 @@ function describeViolation(v) {
|
|
|
334
407
|
}
|
|
335
408
|
if (v.type === "codeowners") {
|
|
336
409
|
if (v.matchedFiles.length === 0) return `CODEOWNERS not found (looked in ${patterns})${suffix}`;
|
|
337
|
-
|
|
338
|
-
const more = v.matchedFiles.length > 3 ? ` and ${v.matchedFiles.length - 3} more` : "";
|
|
339
|
-
return `${v.matchedFiles.length} scanned file(s) have no owner: ${shown}${more}${suffix}`;
|
|
410
|
+
return `${v.matchedFiles.length} scanned file(s) have no owner: ${formatTruncatedList(v.matchedFiles, "file")}${suffix}`;
|
|
340
411
|
}
|
|
341
412
|
return `${patterns} not present${suffix}`;
|
|
342
413
|
}
|
|
@@ -345,25 +416,23 @@ function printRules(aggregated) {
|
|
|
345
416
|
const hasRuleViolations = ruleViolations.length > 0;
|
|
346
417
|
const hasBannedViolations = bannedPackageViolations.length > 0;
|
|
347
418
|
if (!hasRuleViolations && !hasBannedViolations) {
|
|
348
|
-
console.log(chalk.greenBright.bold("
|
|
419
|
+
console.log(chalk.greenBright.bold(`\n${severityIcon("success")} Compliance\n`));
|
|
349
420
|
console.log(chalk.gray(" All compliance checks passed"));
|
|
350
421
|
return;
|
|
351
422
|
}
|
|
352
423
|
console.log(chalk.blueBright.bold("\nš Compliance\n"));
|
|
353
|
-
if (hasRuleViolations) for (const v of ruleViolations) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
console.log(` ${icon} ${tag} ${v.packageName}${msg}`);
|
|
366
|
-
}
|
|
424
|
+
if (hasRuleViolations) for (const v of ruleViolations) console.log(formatViolationLine({
|
|
425
|
+
icon: severityIcon(v.severity),
|
|
426
|
+
label: formatRuleType(v.type),
|
|
427
|
+
description: describeViolation(v)
|
|
428
|
+
}));
|
|
429
|
+
if (hasBannedViolations) for (const v of bannedPackageViolations) {
|
|
430
|
+
const msg = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
431
|
+
console.log(formatViolationLine({
|
|
432
|
+
icon: severityIcon(v.severity),
|
|
433
|
+
label: "forbid_packages",
|
|
434
|
+
description: `${v.packageName} is forbidden${msg}`
|
|
435
|
+
}));
|
|
367
436
|
}
|
|
368
437
|
const errorCount = [...ruleViolations.filter((v) => v.severity === "error"), ...bannedPackageViolations.filter((v) => v.severity === "error")].length;
|
|
369
438
|
const warnCount = [...ruleViolations.filter((v) => v.severity === "warn"), ...bannedPackageViolations.filter((v) => v.severity === "warn")].length;
|
|
@@ -2003,6 +2072,9 @@ function classifyBump(installed, candidate) {
|
|
|
2003
2072
|
if (diff === "major" || diff === "premajor") return "major";
|
|
2004
2073
|
return null;
|
|
2005
2074
|
}
|
|
2075
|
+
function pickNewest(versions) {
|
|
2076
|
+
return versions.reduce((a, b) => a.daysAgo < b.daysAgo ? a : b);
|
|
2077
|
+
}
|
|
2006
2078
|
function upgradeLevel(daysAgo, bump, thresholds) {
|
|
2007
2079
|
const threshold = thresholds[bump];
|
|
2008
2080
|
if (threshold === false || threshold === void 0) return null;
|
|
@@ -2037,12 +2109,13 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, di
|
|
|
2037
2109
|
for (const [bump, versions] of byBump.entries()) {
|
|
2038
2110
|
const level = upgradeLevel(Math.max(...versions.map((v) => v.daysAgo)), bump, thresholds);
|
|
2039
2111
|
if (!level) continue;
|
|
2040
|
-
const newest = versions
|
|
2112
|
+
const newest = pickNewest(versions);
|
|
2041
2113
|
upgrades.push({
|
|
2042
2114
|
version: newest.version,
|
|
2043
2115
|
releasedDaysAgo: newest.daysAgo,
|
|
2044
2116
|
semverBump: bump,
|
|
2045
|
-
level
|
|
2117
|
+
level,
|
|
2118
|
+
thresholdDays: thresholds[bump]
|
|
2046
2119
|
});
|
|
2047
2120
|
}
|
|
2048
2121
|
const finalUpgrades = upgrades.sort((a, b) => b.releasedDaysAgo - a.releasedDaysAgo);
|
|
@@ -2050,10 +2123,29 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, di
|
|
|
2050
2123
|
const latestEntry = latestVersion ? timeMap[latestVersion] : void 0;
|
|
2051
2124
|
const latestReleasedDaysAgo = latestEntry ? daysSince(latestEntry) : void 0;
|
|
2052
2125
|
for (const upgrade of finalUpgrades) if (latestVersion && upgrade.version === latestVersion) upgrade.isLatest = true;
|
|
2126
|
+
const worstLevel = finalUpgrades.some((u) => u.level === "mandatory_upgrade") ? "mandatory_upgrade" : finalUpgrades.length > 0 ? "needs_upgrade" : null;
|
|
2127
|
+
let pendingUpgrade;
|
|
2128
|
+
if (worstLevel === null) for (const [bump, versions] of byBump.entries()) {
|
|
2129
|
+
const threshold = thresholds[bump];
|
|
2130
|
+
if (threshold === false || threshold === void 0) continue;
|
|
2131
|
+
const daysRemaining = threshold - Math.max(...versions.map((v) => v.daysAgo));
|
|
2132
|
+
if (daysRemaining <= 0) continue;
|
|
2133
|
+
if (!pendingUpgrade || daysRemaining < pendingUpgrade.daysRemaining) {
|
|
2134
|
+
const newest = pickNewest(versions);
|
|
2135
|
+
pendingUpgrade = {
|
|
2136
|
+
version: newest.version,
|
|
2137
|
+
semverBump: bump,
|
|
2138
|
+
releasedDaysAgo: newest.daysAgo,
|
|
2139
|
+
thresholdDays: threshold,
|
|
2140
|
+
daysRemaining
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2053
2144
|
return {
|
|
2054
2145
|
installedVersion,
|
|
2055
2146
|
upgrades: finalUpgrades,
|
|
2056
|
-
worstLevel
|
|
2147
|
+
worstLevel,
|
|
2148
|
+
pendingUpgrade,
|
|
2057
2149
|
deprecated,
|
|
2058
2150
|
latestVersion,
|
|
2059
2151
|
latestReleasedDaysAgo,
|
|
@@ -2162,8 +2254,12 @@ async function runPipeline(config, spinner, isJson) {
|
|
|
2162
2254
|
* Shared command preamble: routes human-readable chrome (version line,
|
|
2163
2255
|
* spinner) to stderr when the command emits JSON on stdout.
|
|
2164
2256
|
*/
|
|
2165
|
-
function createCommandContext(config) {
|
|
2166
|
-
|
|
2257
|
+
function createCommandContext(config, options = {}) {
|
|
2258
|
+
applyColorLevel(resolveColorLevel({
|
|
2259
|
+
colorFlag: options.color === false ? false : void 0,
|
|
2260
|
+
noColorEnv: process.env["NO_COLOR"]
|
|
2261
|
+
}));
|
|
2262
|
+
const isJson = (options.format ?? config.output.format) === "json";
|
|
2167
2263
|
const stream = isJson ? process.stderr : process.stdout;
|
|
2168
2264
|
stream.write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
2169
2265
|
return {
|
|
@@ -2177,12 +2273,15 @@ function createCommandContext(config) {
|
|
|
2177
2273
|
//#endregion
|
|
2178
2274
|
//#region src/commands/scan.ts
|
|
2179
2275
|
function registerScanCommand(program) {
|
|
2180
|
-
program.command("scan").description("Scan and analyze local files").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
2181
|
-
await executeScan(await loadConfig(process.cwd(), options.config)
|
|
2276
|
+
program.command("scan").description("Scan and analyze local files").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").addOption(new Option("--format <format>", "Output format, overrides output.format in the config file").choices(["human", "json"])).option("--no-color", "Disable colored output (see also NO_COLOR env var)").action(async (options) => {
|
|
2277
|
+
await executeScan(await loadConfig(process.cwd(), options.config), {
|
|
2278
|
+
format: options.format,
|
|
2279
|
+
color: options.color
|
|
2280
|
+
});
|
|
2182
2281
|
});
|
|
2183
2282
|
}
|
|
2184
|
-
async function executeScan(config) {
|
|
2185
|
-
const { isJson, spinner } = createCommandContext(config);
|
|
2283
|
+
async function executeScan(config, contextOptions = {}) {
|
|
2284
|
+
const { isJson, spinner } = createCommandContext(config, contextOptions);
|
|
2186
2285
|
try {
|
|
2187
2286
|
const aggregated = await runPipeline(config, spinner, isJson);
|
|
2188
2287
|
if (!aggregated) return;
|
|
@@ -2205,21 +2304,19 @@ function printScanResults(aggregated, config) {
|
|
|
2205
2304
|
}
|
|
2206
2305
|
//#endregion
|
|
2207
2306
|
//#region src/utils/print-compliance.ts
|
|
2307
|
+
/**
|
|
2308
|
+
* Prints only the bottom-line verdict ā the Compliance and Packages sections
|
|
2309
|
+
* printed above this already itemize every violation (rule and release-age
|
|
2310
|
+
* alike), so repeating them here would just restate the same š“ rows.
|
|
2311
|
+
*/
|
|
2208
2312
|
function printComplianceVerdict(result) {
|
|
2209
2313
|
const mandatoryCount = result.errorRuleViolations.length + result.errorBannedPackageViolations.length + result.mandatoryReleaseAgeViolations.length;
|
|
2210
2314
|
if (result.compliant) {
|
|
2211
|
-
console.log(chalk.greenBright.bold("
|
|
2315
|
+
console.log(chalk.greenBright.bold(`\n${severityIcon("success")} COMPLIANT\n`));
|
|
2212
2316
|
return;
|
|
2213
2317
|
}
|
|
2214
|
-
console.log(chalk.redBright.bold(`\n
|
|
2318
|
+
console.log(chalk.redBright.bold(`\n${severityIcon("error")} NOT COMPLIANT`));
|
|
2215
2319
|
console.log(chalk.red(` ${mandatoryCount} mandatory violation${mandatoryCount > 1 ? "s" : ""} found`));
|
|
2216
|
-
if (result.mandatoryReleaseAgeViolations.length > 0) {
|
|
2217
|
-
console.log();
|
|
2218
|
-
for (const pkg of result.mandatoryReleaseAgeViolations) {
|
|
2219
|
-
const top = pkg.releaseAge?.upgrades[0];
|
|
2220
|
-
console.log(chalk.red(` ā releaseAge ${pkg.packageName} is ${top?.semverBump} version behind (${top?.releasedDaysAgo}d) ā mandatory upgrade [ERROR]`));
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
2320
|
console.log();
|
|
2224
2321
|
}
|
|
2225
2322
|
//#endregion
|
|
@@ -2245,12 +2342,15 @@ function computeCompliance(aggregated) {
|
|
|
2245
2342
|
//#endregion
|
|
2246
2343
|
//#region src/commands/comply.ts
|
|
2247
2344
|
function registerComplyCommand(program) {
|
|
2248
|
-
program.command("comply").description("Check compliance with hermex.config.ts rules and release-age policy (exits non-zero if not compliant)").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
2249
|
-
await executeComply(await loadConfig(process.cwd(), options.config)
|
|
2345
|
+
program.command("comply").description("Check compliance with hermex.config.ts rules and release-age policy (exits non-zero if not compliant)").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").addOption(new Option("--format <format>", "Output format, overrides output.format in the config file").choices(["human", "json"])).option("--no-color", "Disable colored output (see also NO_COLOR env var)").action(async (options) => {
|
|
2346
|
+
await executeComply(await loadConfig(process.cwd(), options.config), {
|
|
2347
|
+
format: options.format,
|
|
2348
|
+
color: options.color
|
|
2349
|
+
});
|
|
2250
2350
|
});
|
|
2251
2351
|
}
|
|
2252
|
-
async function executeComply(config) {
|
|
2253
|
-
const { isJson, spinner } = createCommandContext(config);
|
|
2352
|
+
async function executeComply(config, contextOptions = {}) {
|
|
2353
|
+
const { isJson, spinner } = createCommandContext(config, contextOptions);
|
|
2254
2354
|
try {
|
|
2255
2355
|
const aggregated = await runPipeline(config, spinner, isJson);
|
|
2256
2356
|
if (!aggregated) {
|
|
@@ -2262,6 +2362,7 @@ async function executeComply(config) {
|
|
|
2262
2362
|
else {
|
|
2263
2363
|
printRules(aggregated);
|
|
2264
2364
|
if (config.releaseAge.enabled) printPackages(aggregated, "table");
|
|
2365
|
+
if (config.output.versus) printVersus(aggregated);
|
|
2265
2366
|
printComplianceVerdict(compliance);
|
|
2266
2367
|
}
|
|
2267
2368
|
process.exitCode = compliance.compliant ? 0 : 1;
|