hermex 2.0.0-beta.9 ā 2.0.1
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 +184 -80
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +18 -1
- package/package.json +2 -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";
|
|
@@ -13,6 +13,7 @@ import fs$1 from "fs";
|
|
|
13
13
|
import path$1 from "path";
|
|
14
14
|
import semver from "semver";
|
|
15
15
|
import { load } from "js-yaml";
|
|
16
|
+
import { parse, removeSuffix } from "@pnpm/dependency-path";
|
|
16
17
|
import lockfile from "@yarnpkg/lockfile";
|
|
17
18
|
import { randomUUID } from "node:crypto";
|
|
18
19
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -27,6 +28,31 @@ import ora from "ora";
|
|
|
27
28
|
function formatCount(num) {
|
|
28
29
|
return num.toLocaleString();
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Format how far an upgrade candidate is past its age threshold
|
|
33
|
+
* @returns Formatted string (e.g., "40 days overdue", "1 day overdue")
|
|
34
|
+
*/
|
|
35
|
+
function formatDaysOverdue(releasedDaysAgo, thresholdDays) {
|
|
36
|
+
const overdue = releasedDaysAgo - thresholdDays;
|
|
37
|
+
return `${overdue} day${overdue === 1 ? "" : "s"} overdue`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Format how long until an upgrade candidate breaches its age threshold
|
|
41
|
+
* @returns Formatted string (e.g., "12 days remaining", "1 day remaining")
|
|
42
|
+
*/
|
|
43
|
+
function formatDaysRemaining(daysRemaining) {
|
|
44
|
+
return `${daysRemaining} day${daysRemaining === 1 ? "" : "s"} remaining`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Join a list of items, showing only the first `limit` and summarizing the rest
|
|
48
|
+
* @returns Formatted string (e.g., "a, b and 3 other files", "a, b and 1 other file")
|
|
49
|
+
*/
|
|
50
|
+
function formatTruncatedList(items, noun, limit = 2) {
|
|
51
|
+
const shown = items.slice(0, limit).join(", ");
|
|
52
|
+
const rest = items.length - limit;
|
|
53
|
+
if (rest <= 0) return shown;
|
|
54
|
+
return `${shown} and ${rest} other ${noun}${rest === 1 ? "" : "s"}`;
|
|
55
|
+
}
|
|
30
56
|
//#endregion
|
|
31
57
|
//#region src/utils/print-summary.ts
|
|
32
58
|
function printHeader$4() {
|
|
@@ -171,26 +197,88 @@ function printPatternsChart(patterns) {
|
|
|
171
197
|
})), { maxWidth: 50 });
|
|
172
198
|
}
|
|
173
199
|
//#endregion
|
|
200
|
+
//#region src/utils/severity-format.ts
|
|
201
|
+
const ICONS = {
|
|
202
|
+
error: "š“",
|
|
203
|
+
warn: "š”",
|
|
204
|
+
info: "šµ",
|
|
205
|
+
success: "š¢"
|
|
206
|
+
};
|
|
207
|
+
const COLORS = {
|
|
208
|
+
error: chalk.red,
|
|
209
|
+
warn: chalk.yellow,
|
|
210
|
+
info: chalk.blue,
|
|
211
|
+
success: chalk.green
|
|
212
|
+
};
|
|
213
|
+
/** Colored-circle glyph for a severity ā carries meaning without relying on ANSI color. */
|
|
214
|
+
function severityIcon(severity) {
|
|
215
|
+
return ICONS[severity];
|
|
216
|
+
}
|
|
217
|
+
/** chalk color function for a severity, for text that needs coloring. */
|
|
218
|
+
function severityColor(severity) {
|
|
219
|
+
return COLORS[severity];
|
|
220
|
+
}
|
|
221
|
+
function formatViolationLine(opts) {
|
|
222
|
+
return ` ${opts.icon} ${opts.label.padEnd(14)} ${opts.description}`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolves an explicit color-on/off override from CLI flags or the NO_COLOR
|
|
226
|
+
* convention (https://no-color.org). Returns `undefined` when there's no
|
|
227
|
+
* explicit signal, meaning chalk's own auto-detection should be left alone
|
|
228
|
+
* (including its GitHub-Actions-aware detection of non-TTY streams).
|
|
229
|
+
*/
|
|
230
|
+
function resolveColorLevel(opts) {
|
|
231
|
+
if (opts.colorFlag === false) return 0;
|
|
232
|
+
if (opts.colorFlag === true) return 1;
|
|
233
|
+
if (opts.noColorEnv !== void 0) return 0;
|
|
234
|
+
}
|
|
235
|
+
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
236
|
+
function stripAnsiWrites(stream) {
|
|
237
|
+
const originalWrite = stream.write.bind(stream);
|
|
238
|
+
stream.write = ((chunk, ...rest) => {
|
|
239
|
+
return originalWrite(typeof chunk === "string" ? chunk.replace(ANSI_ESCAPE_PATTERN, "") : chunk, ...rest);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Beyond setting chalk's own level, this strips ANSI codes at the stream
|
|
244
|
+
* boundary when color is explicitly disabled. hermex also prints through
|
|
245
|
+
* cli-table3 and ora (whose spinner symbols come from log-symbols/yoctocolors)
|
|
246
|
+
* ā neither shares hermex's chalk instance or honors chalk.level, so mutating
|
|
247
|
+
* chalk alone can't make NO_COLOR/--no-color hold for their output too.
|
|
248
|
+
*/
|
|
249
|
+
function applyColorLevel(level) {
|
|
250
|
+
if (level === void 0) return;
|
|
251
|
+
chalk.level = level;
|
|
252
|
+
if (level === 0) {
|
|
253
|
+
stripAnsiWrites(process.stdout);
|
|
254
|
+
stripAnsiWrites(process.stderr);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
174
258
|
//#region src/utils/print-packages.ts
|
|
175
259
|
function printHeader() {
|
|
176
260
|
console.log(chalk.blueBright.bold("\nš¦ Packages\n"));
|
|
177
261
|
}
|
|
178
262
|
function formatPackageName(pkg, banned) {
|
|
179
263
|
let prefix = "";
|
|
180
|
-
if (pkg.releaseAge?.deprecated) prefix +=
|
|
181
|
-
if (banned) prefix += banned.severity === "error" ?
|
|
182
|
-
else if (pkg.internal) prefix +=
|
|
264
|
+
if (pkg.releaseAge?.deprecated) prefix += severityColor("error")("[DEPRECATED] ");
|
|
265
|
+
if (banned) prefix += banned.severity === "error" ? severityColor("error")("[BANNED] ") : severityColor("warn")("[RESTRICTED] ");
|
|
266
|
+
else if (pkg.internal) prefix += severityColor("warn")("[int] ");
|
|
183
267
|
return prefix + pkg.packageName;
|
|
184
268
|
}
|
|
185
269
|
function formatUpgradeCell(releaseAge) {
|
|
186
270
|
if (!releaseAge) return "";
|
|
187
|
-
const { worstLevel, upgrades, severity } = releaseAge;
|
|
188
|
-
if (!worstLevel)
|
|
271
|
+
const { worstLevel, upgrades, severity, pendingUpgrade } = releaseAge;
|
|
272
|
+
if (!worstLevel) {
|
|
273
|
+
if (pendingUpgrade) return `${severityIcon("info")} ${pendingUpgrade.semverBump} ${pendingUpgrade.version} (${formatDaysRemaining(pendingUpgrade.daysRemaining)})`;
|
|
274
|
+
return severityIcon("success");
|
|
275
|
+
}
|
|
189
276
|
const top = upgrades[0];
|
|
190
|
-
if (!top) return
|
|
277
|
+
if (!top) return severityIcon("success");
|
|
191
278
|
const suffix = severity === "warn" ? chalk.gray(" [not enforced]") : "";
|
|
192
|
-
|
|
193
|
-
return
|
|
279
|
+
const overdue = formatDaysOverdue(top.breachReleasedDaysAgo, top.thresholdDays);
|
|
280
|
+
if (worstLevel === "mandatory_upgrade") return `${severityIcon(severity === "warn" ? "warn" : "error")} ${top.semverBump} ${top.version} (${overdue})${suffix}`;
|
|
281
|
+
return `${severityIcon("warn")} ${top.semverBump} ${top.version} (${overdue})${suffix}`;
|
|
194
282
|
}
|
|
195
283
|
function getBannedViolation(pkg, violations) {
|
|
196
284
|
return violations.find((v) => v.packageName === pkg.packageName);
|
|
@@ -265,13 +353,6 @@ function renderBar(percentage) {
|
|
|
265
353
|
const empty = BAR_WIDTH - filled;
|
|
266
354
|
return chalk.cyan("ā".repeat(filled)) + chalk.gray("ā".repeat(empty));
|
|
267
355
|
}
|
|
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
356
|
function printVersusResult(result) {
|
|
276
357
|
console.log(chalk.bold(` ${result.name}`));
|
|
277
358
|
console.log(chalk.gray(` ${"ā".repeat(50)}`));
|
|
@@ -281,7 +362,7 @@ function printVersusResult(result) {
|
|
|
281
362
|
const bar = renderBar(entry.percentage);
|
|
282
363
|
const pct = chalk.bold(`${entry.percentage.toFixed(1)}%`);
|
|
283
364
|
const usage = chalk.gray(`(${entry.count} usages)`);
|
|
284
|
-
const components = entry.components.length > 0 ? chalk.gray(` ${
|
|
365
|
+
const components = entry.components.length > 0 ? chalk.gray(` ${formatTruncatedList(entry.components, "component")}`) : "";
|
|
285
366
|
console.log(` ${name} ${bar} ${pct} ${usage}${components}`);
|
|
286
367
|
}
|
|
287
368
|
if (result.totalCount === 0) console.log(chalk.gray(" No usage detected for any package in this group."));
|
|
@@ -298,30 +379,23 @@ function formatRuleType(type) {
|
|
|
298
379
|
switch (type) {
|
|
299
380
|
case "detect_files": return "detect_files";
|
|
300
381
|
case "require_files": return "require_files";
|
|
301
|
-
case "forbid_packages": return "forbid_packages";
|
|
302
382
|
case "require_packages": return "require_packages";
|
|
303
383
|
case "require_scripts": return "require_scripts";
|
|
304
|
-
case "require_package_fields": return "
|
|
305
|
-
case "forbid_package_fields": return "
|
|
384
|
+
case "require_package_fields": return "package_fields";
|
|
385
|
+
case "forbid_package_fields": return "package_fields";
|
|
306
386
|
case "engine_version": return "engine_version";
|
|
307
387
|
case "codeowners": return "codeowners";
|
|
308
388
|
}
|
|
309
389
|
}
|
|
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
390
|
function describeViolation(v) {
|
|
316
391
|
const patterns = v.patterns.join(", ");
|
|
317
392
|
const suffix = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
318
|
-
if (v.type === "detect_files") return `${patterns} detected (${v.matchedFiles.map((f) => {
|
|
393
|
+
if (v.type === "detect_files") return `${patterns} detected (${formatTruncatedList(v.matchedFiles.map((f) => {
|
|
319
394
|
const parts = f.replace(/\\/g, "/").split("/");
|
|
320
395
|
return parts[parts.length - 1];
|
|
321
|
-
})
|
|
396
|
+
}), "file")})${suffix}`;
|
|
322
397
|
if (v.type === "require_files") return `${patterns} not found${suffix}`;
|
|
323
398
|
if (v.type === "require_packages") return `${patterns} not installed${suffix}`;
|
|
324
|
-
if (v.type === "forbid_packages") return `${patterns} is forbidden${suffix}`;
|
|
325
399
|
if (v.type === "require_scripts") return `script ${patterns} missing in package.json${suffix}`;
|
|
326
400
|
if (v.type === "require_package_fields") {
|
|
327
401
|
if (v.fieldPath && v.actualValue !== void 0) return `field ${v.fieldPath} is ${chalk.yellow(v.actualValue)}, does not match required value${suffix}`;
|
|
@@ -334,9 +408,7 @@ function describeViolation(v) {
|
|
|
334
408
|
}
|
|
335
409
|
if (v.type === "codeowners") {
|
|
336
410
|
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}`;
|
|
411
|
+
return `${v.matchedFiles.length} scanned file(s) have no owner: ${formatTruncatedList(v.matchedFiles, "file")}${suffix}`;
|
|
340
412
|
}
|
|
341
413
|
return `${patterns} not present${suffix}`;
|
|
342
414
|
}
|
|
@@ -345,25 +417,23 @@ function printRules(aggregated) {
|
|
|
345
417
|
const hasRuleViolations = ruleViolations.length > 0;
|
|
346
418
|
const hasBannedViolations = bannedPackageViolations.length > 0;
|
|
347
419
|
if (!hasRuleViolations && !hasBannedViolations) {
|
|
348
|
-
console.log(chalk.greenBright.bold("
|
|
420
|
+
console.log(chalk.greenBright.bold(`\n${severityIcon("success")} Compliance\n`));
|
|
349
421
|
console.log(chalk.gray(" All compliance checks passed"));
|
|
350
422
|
return;
|
|
351
423
|
}
|
|
352
424
|
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
|
-
}
|
|
425
|
+
if (hasRuleViolations) for (const v of ruleViolations) console.log(formatViolationLine({
|
|
426
|
+
icon: severityIcon(v.severity),
|
|
427
|
+
label: formatRuleType(v.type),
|
|
428
|
+
description: describeViolation(v)
|
|
429
|
+
}));
|
|
430
|
+
if (hasBannedViolations) for (const v of bannedPackageViolations) {
|
|
431
|
+
const msg = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
432
|
+
console.log(formatViolationLine({
|
|
433
|
+
icon: severityIcon(v.severity),
|
|
434
|
+
label: "forbid_packages",
|
|
435
|
+
description: `${v.packageName} is forbidden${msg}`
|
|
436
|
+
}));
|
|
367
437
|
}
|
|
368
438
|
const errorCount = [...ruleViolations.filter((v) => v.severity === "error"), ...bannedPackageViolations.filter((v) => v.severity === "error")].length;
|
|
369
439
|
const warnCount = [...ruleViolations.filter((v) => v.severity === "warn"), ...bannedPackageViolations.filter((v) => v.severity === "warn")].length;
|
|
@@ -1755,13 +1825,13 @@ var NpmLockfileAdapter = class {
|
|
|
1755
1825
|
//#endregion
|
|
1756
1826
|
//#region src/lock-parser/patterns/pnpm.ts
|
|
1757
1827
|
function parsePackageKey(rawKey) {
|
|
1758
|
-
const
|
|
1759
|
-
const
|
|
1760
|
-
if (
|
|
1761
|
-
name:
|
|
1762
|
-
version:
|
|
1828
|
+
const key = rawKey.startsWith("/") ? rawKey.slice(1) : rawKey;
|
|
1829
|
+
const parsed = parse(key);
|
|
1830
|
+
if (parsed.name && parsed.version) return {
|
|
1831
|
+
name: parsed.name,
|
|
1832
|
+
version: parsed.version
|
|
1763
1833
|
};
|
|
1764
|
-
const slashMatch =
|
|
1834
|
+
const slashMatch = key.match(/^(.+?)\/(\d+\.\d+\.\d+.*)$/);
|
|
1765
1835
|
if (slashMatch) return {
|
|
1766
1836
|
name: slashMatch[1],
|
|
1767
1837
|
version: slashMatch[2]
|
|
@@ -1787,10 +1857,10 @@ var PnpmLockfileAdapter = class {
|
|
|
1787
1857
|
const rootImporter = lockData.importers["."];
|
|
1788
1858
|
if (rootImporter) {
|
|
1789
1859
|
if (rootImporter.dependencies) {
|
|
1790
|
-
for (const [name, data] of Object.entries(rootImporter.dependencies)) if (typeof data === "object" && data !== null && "version" in data) versions[name] = data.version;
|
|
1860
|
+
for (const [name, data] of Object.entries(rootImporter.dependencies)) if (typeof data === "object" && data !== null && "version" in data) versions[name] = removeSuffix(data.version);
|
|
1791
1861
|
}
|
|
1792
1862
|
if (rootImporter.devDependencies) {
|
|
1793
|
-
for (const [name, data] of Object.entries(rootImporter.devDependencies)) if (typeof data === "object" && data !== null && "version" in data) versions[name] = data.version;
|
|
1863
|
+
for (const [name, data] of Object.entries(rootImporter.devDependencies)) if (typeof data === "object" && data !== null && "version" in data) versions[name] = removeSuffix(data.version);
|
|
1794
1864
|
}
|
|
1795
1865
|
}
|
|
1796
1866
|
}
|
|
@@ -1798,12 +1868,12 @@ var PnpmLockfileAdapter = class {
|
|
|
1798
1868
|
const match = key.match(/\/(.+?)\/(\d+\.\d+\.\d+.*?)(?:_|$)/);
|
|
1799
1869
|
if (match) {
|
|
1800
1870
|
const [, pkgName, version] = match;
|
|
1801
|
-
versions[pkgName] = version;
|
|
1871
|
+
versions[pkgName] = removeSuffix(version);
|
|
1802
1872
|
}
|
|
1803
1873
|
});
|
|
1804
1874
|
if (lockData.dependencies && Object.keys(versions).length === 0) Object.entries(lockData.dependencies).forEach(([name, versionSpec]) => {
|
|
1805
|
-
if (typeof versionSpec === "string" && !versionSpec.startsWith("link:")) versions[name] = versionSpec;
|
|
1806
|
-
else if (typeof versionSpec === "object" && versionSpec.version) versions[name] = versionSpec.version;
|
|
1875
|
+
if (typeof versionSpec === "string" && !versionSpec.startsWith("link:")) versions[name] = removeSuffix(versionSpec);
|
|
1876
|
+
else if (typeof versionSpec === "object" && versionSpec.version) versions[name] = removeSuffix(versionSpec.version);
|
|
1807
1877
|
});
|
|
1808
1878
|
return versions;
|
|
1809
1879
|
} catch (error) {
|
|
@@ -2003,6 +2073,9 @@ function classifyBump(installed, candidate) {
|
|
|
2003
2073
|
if (diff === "major" || diff === "premajor") return "major";
|
|
2004
2074
|
return null;
|
|
2005
2075
|
}
|
|
2076
|
+
function pickNewest(versions) {
|
|
2077
|
+
return versions.reduce((a, b) => a.daysAgo < b.daysAgo ? a : b);
|
|
2078
|
+
}
|
|
2006
2079
|
function upgradeLevel(daysAgo, bump, thresholds) {
|
|
2007
2080
|
const threshold = thresholds[bump];
|
|
2008
2081
|
if (threshold === false || threshold === void 0) return null;
|
|
@@ -2035,14 +2108,17 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, di
|
|
|
2035
2108
|
}
|
|
2036
2109
|
const upgrades = [];
|
|
2037
2110
|
for (const [bump, versions] of byBump.entries()) {
|
|
2038
|
-
const
|
|
2111
|
+
const oldestDaysAgo = Math.max(...versions.map((v) => v.daysAgo));
|
|
2112
|
+
const level = upgradeLevel(oldestDaysAgo, bump, thresholds);
|
|
2039
2113
|
if (!level) continue;
|
|
2040
|
-
const newest = versions
|
|
2114
|
+
const newest = pickNewest(versions);
|
|
2041
2115
|
upgrades.push({
|
|
2042
2116
|
version: newest.version,
|
|
2043
2117
|
releasedDaysAgo: newest.daysAgo,
|
|
2118
|
+
breachReleasedDaysAgo: oldestDaysAgo,
|
|
2044
2119
|
semverBump: bump,
|
|
2045
|
-
level
|
|
2120
|
+
level,
|
|
2121
|
+
thresholdDays: thresholds[bump]
|
|
2046
2122
|
});
|
|
2047
2123
|
}
|
|
2048
2124
|
const finalUpgrades = upgrades.sort((a, b) => b.releasedDaysAgo - a.releasedDaysAgo);
|
|
@@ -2050,10 +2126,29 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, di
|
|
|
2050
2126
|
const latestEntry = latestVersion ? timeMap[latestVersion] : void 0;
|
|
2051
2127
|
const latestReleasedDaysAgo = latestEntry ? daysSince(latestEntry) : void 0;
|
|
2052
2128
|
for (const upgrade of finalUpgrades) if (latestVersion && upgrade.version === latestVersion) upgrade.isLatest = true;
|
|
2129
|
+
const worstLevel = finalUpgrades.some((u) => u.level === "mandatory_upgrade") ? "mandatory_upgrade" : finalUpgrades.length > 0 ? "needs_upgrade" : null;
|
|
2130
|
+
let pendingUpgrade;
|
|
2131
|
+
if (worstLevel === null) for (const [bump, versions] of byBump.entries()) {
|
|
2132
|
+
const threshold = thresholds[bump];
|
|
2133
|
+
if (threshold === false || threshold === void 0) continue;
|
|
2134
|
+
const daysRemaining = threshold - Math.max(...versions.map((v) => v.daysAgo));
|
|
2135
|
+
if (daysRemaining <= 0) continue;
|
|
2136
|
+
if (!pendingUpgrade || daysRemaining < pendingUpgrade.daysRemaining) {
|
|
2137
|
+
const newest = pickNewest(versions);
|
|
2138
|
+
pendingUpgrade = {
|
|
2139
|
+
version: newest.version,
|
|
2140
|
+
semverBump: bump,
|
|
2141
|
+
releasedDaysAgo: newest.daysAgo,
|
|
2142
|
+
thresholdDays: threshold,
|
|
2143
|
+
daysRemaining
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2053
2147
|
return {
|
|
2054
2148
|
installedVersion,
|
|
2055
2149
|
upgrades: finalUpgrades,
|
|
2056
|
-
worstLevel
|
|
2150
|
+
worstLevel,
|
|
2151
|
+
pendingUpgrade,
|
|
2057
2152
|
deprecated,
|
|
2058
2153
|
latestVersion,
|
|
2059
2154
|
latestReleasedDaysAgo,
|
|
@@ -2162,8 +2257,12 @@ async function runPipeline(config, spinner, isJson) {
|
|
|
2162
2257
|
* Shared command preamble: routes human-readable chrome (version line,
|
|
2163
2258
|
* spinner) to stderr when the command emits JSON on stdout.
|
|
2164
2259
|
*/
|
|
2165
|
-
function createCommandContext(config) {
|
|
2166
|
-
|
|
2260
|
+
function createCommandContext(config, options = {}) {
|
|
2261
|
+
applyColorLevel(resolveColorLevel({
|
|
2262
|
+
colorFlag: options.color === false ? false : void 0,
|
|
2263
|
+
noColorEnv: process.env["NO_COLOR"]
|
|
2264
|
+
}));
|
|
2265
|
+
const isJson = (options.format ?? config.output.format) === "json";
|
|
2167
2266
|
const stream = isJson ? process.stderr : process.stdout;
|
|
2168
2267
|
stream.write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
2169
2268
|
return {
|
|
@@ -2177,12 +2276,15 @@ function createCommandContext(config) {
|
|
|
2177
2276
|
//#endregion
|
|
2178
2277
|
//#region src/commands/scan.ts
|
|
2179
2278
|
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)
|
|
2279
|
+
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) => {
|
|
2280
|
+
await executeScan(await loadConfig(process.cwd(), options.config), {
|
|
2281
|
+
format: options.format,
|
|
2282
|
+
color: options.color
|
|
2283
|
+
});
|
|
2182
2284
|
});
|
|
2183
2285
|
}
|
|
2184
|
-
async function executeScan(config) {
|
|
2185
|
-
const { isJson, spinner } = createCommandContext(config);
|
|
2286
|
+
async function executeScan(config, contextOptions = {}) {
|
|
2287
|
+
const { isJson, spinner } = createCommandContext(config, contextOptions);
|
|
2186
2288
|
try {
|
|
2187
2289
|
const aggregated = await runPipeline(config, spinner, isJson);
|
|
2188
2290
|
if (!aggregated) return;
|
|
@@ -2205,21 +2307,19 @@ function printScanResults(aggregated, config) {
|
|
|
2205
2307
|
}
|
|
2206
2308
|
//#endregion
|
|
2207
2309
|
//#region src/utils/print-compliance.ts
|
|
2310
|
+
/**
|
|
2311
|
+
* Prints only the bottom-line verdict ā the Compliance and Packages sections
|
|
2312
|
+
* printed above this already itemize every violation (rule and release-age
|
|
2313
|
+
* alike), so repeating them here would just restate the same š“ rows.
|
|
2314
|
+
*/
|
|
2208
2315
|
function printComplianceVerdict(result) {
|
|
2209
2316
|
const mandatoryCount = result.errorRuleViolations.length + result.errorBannedPackageViolations.length + result.mandatoryReleaseAgeViolations.length;
|
|
2210
2317
|
if (result.compliant) {
|
|
2211
|
-
console.log(chalk.greenBright.bold("
|
|
2318
|
+
console.log(chalk.greenBright.bold(`\n${severityIcon("success")} COMPLIANT\n`));
|
|
2212
2319
|
return;
|
|
2213
2320
|
}
|
|
2214
|
-
console.log(chalk.redBright.bold(`\n
|
|
2321
|
+
console.log(chalk.redBright.bold(`\n${severityIcon("error")} NOT COMPLIANT`));
|
|
2215
2322
|
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
2323
|
console.log();
|
|
2224
2324
|
}
|
|
2225
2325
|
//#endregion
|
|
@@ -2245,12 +2345,15 @@ function computeCompliance(aggregated) {
|
|
|
2245
2345
|
//#endregion
|
|
2246
2346
|
//#region src/commands/comply.ts
|
|
2247
2347
|
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)
|
|
2348
|
+
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) => {
|
|
2349
|
+
await executeComply(await loadConfig(process.cwd(), options.config), {
|
|
2350
|
+
format: options.format,
|
|
2351
|
+
color: options.color
|
|
2352
|
+
});
|
|
2250
2353
|
});
|
|
2251
2354
|
}
|
|
2252
|
-
async function executeComply(config) {
|
|
2253
|
-
const { isJson, spinner } = createCommandContext(config);
|
|
2355
|
+
async function executeComply(config, contextOptions = {}) {
|
|
2356
|
+
const { isJson, spinner } = createCommandContext(config, contextOptions);
|
|
2254
2357
|
try {
|
|
2255
2358
|
const aggregated = await runPipeline(config, spinner, isJson);
|
|
2256
2359
|
if (!aggregated) {
|
|
@@ -2262,6 +2365,7 @@ async function executeComply(config) {
|
|
|
2262
2365
|
else {
|
|
2263
2366
|
printRules(aggregated);
|
|
2264
2367
|
if (config.releaseAge.enabled) printPackages(aggregated, "table");
|
|
2368
|
+
if (config.output.versus) printVersus(aggregated);
|
|
2265
2369
|
printComplianceVerdict(compliance);
|
|
2266
2370
|
}
|
|
2267
2371
|
process.exitCode = compliance.compliant ? 0 : 1;
|