@rainy-updates/cli 0.5.4 → 0.5.7
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/CHANGELOG.md +136 -0
- package/README.md +5 -0
- package/dist/bin/cli.js +16 -438
- package/dist/bin/dispatch.d.ts +16 -0
- package/dist/bin/dispatch.js +150 -0
- package/dist/bin/help.d.ts +1 -0
- package/dist/bin/help.js +284 -0
- package/dist/commands/audit/runner.js +43 -26
- package/dist/commands/dashboard/parser.d.ts +2 -0
- package/dist/commands/dashboard/parser.js +59 -0
- package/dist/commands/dashboard/runner.d.ts +2 -0
- package/dist/commands/dashboard/runner.js +47 -0
- package/dist/commands/doctor/parser.js +12 -0
- package/dist/commands/doctor/runner.js +5 -2
- package/dist/commands/ga/parser.d.ts +2 -0
- package/dist/commands/ga/parser.js +50 -0
- package/dist/commands/ga/runner.d.ts +2 -0
- package/dist/commands/ga/runner.js +129 -0
- package/dist/commands/resolve/runner.js +7 -3
- package/dist/commands/review/parser.js +6 -0
- package/dist/commands/review/runner.js +4 -3
- package/dist/core/analysis/options.d.ts +6 -0
- package/dist/core/analysis/options.js +69 -0
- package/dist/core/analysis/review-items.d.ts +4 -0
- package/dist/core/analysis/review-items.js +128 -0
- package/dist/core/analysis/run-silenced.d.ts +1 -0
- package/dist/core/analysis/run-silenced.js +14 -0
- package/dist/core/analysis-bundle.d.ts +4 -0
- package/dist/core/analysis-bundle.js +33 -0
- package/dist/core/artifacts.d.ts +3 -0
- package/dist/core/artifacts.js +48 -0
- package/dist/core/check.js +6 -1
- package/dist/core/doctor/findings.d.ts +2 -0
- package/dist/core/doctor/findings.js +166 -0
- package/dist/core/doctor/render.d.ts +3 -0
- package/dist/core/doctor/render.js +44 -0
- package/dist/core/doctor/result.d.ts +2 -0
- package/dist/core/doctor/result.js +55 -0
- package/dist/core/doctor/score.d.ts +5 -0
- package/dist/core/doctor/score.js +28 -0
- package/dist/core/options.d.ts +7 -1
- package/dist/core/options.js +14 -0
- package/dist/core/review-model.d.ts +3 -3
- package/dist/core/review-model.js +55 -245
- package/dist/core/review-verdict.d.ts +2 -0
- package/dist/core/review-verdict.js +14 -0
- package/dist/core/summary.js +19 -0
- package/dist/output/format.js +22 -0
- package/dist/output/github.js +12 -0
- package/dist/output/sarif.js +16 -0
- package/dist/types/index.d.ts +120 -0
- package/dist/ui/dashboard/DashboardTUI.d.ts +6 -0
- package/dist/ui/dashboard/DashboardTUI.js +34 -0
- package/dist/ui/dashboard/components/DetailPanel.d.ts +4 -0
- package/dist/ui/dashboard/components/DetailPanel.js +30 -0
- package/dist/ui/dashboard/components/Footer.d.ts +4 -0
- package/dist/ui/dashboard/components/Footer.js +9 -0
- package/dist/ui/dashboard/components/Header.d.ts +4 -0
- package/dist/ui/dashboard/components/Header.js +12 -0
- package/dist/ui/dashboard/components/Sidebar.d.ts +4 -0
- package/dist/ui/dashboard/components/Sidebar.js +23 -0
- package/dist/ui/dashboard/store.d.ts +34 -0
- package/dist/ui/dashboard/store.js +148 -0
- package/dist/ui/tui.d.ts +2 -2
- package/dist/ui/tui.js +310 -79
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ export function parseDoctorArgs(args) {
|
|
|
42
42
|
showImpact: true,
|
|
43
43
|
showHomepage: true,
|
|
44
44
|
verdictOnly: false,
|
|
45
|
+
includeChangelog: false,
|
|
46
|
+
agentReport: false,
|
|
45
47
|
};
|
|
46
48
|
for (let i = 0; i < args.length; i += 1) {
|
|
47
49
|
const current = args[i];
|
|
@@ -61,6 +63,14 @@ export function parseDoctorArgs(args) {
|
|
|
61
63
|
options.verdictOnly = true;
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
66
|
+
if (current === "--include-changelog") {
|
|
67
|
+
options.includeChangelog = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (current === "--agent-report") {
|
|
71
|
+
options.agentReport = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
64
74
|
if (current === "--json-file" && next) {
|
|
65
75
|
options.jsonFile = path.resolve(options.cwd, next);
|
|
66
76
|
i += 1;
|
|
@@ -86,6 +96,8 @@ Usage:
|
|
|
86
96
|
|
|
87
97
|
Options:
|
|
88
98
|
--verdict-only Print the 3-line quick verdict without counts
|
|
99
|
+
--include-changelog Include release note summaries in the aggregated review data
|
|
100
|
+
--agent-report Print a prompt-ready remediation report for coding agents
|
|
89
101
|
--workspace Scan all workspace packages
|
|
90
102
|
--json-file <path> Write JSON doctor report to file
|
|
91
103
|
--cwd <path>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { buildReviewResult, createDoctorResult, renderDoctorResult } from "../../core/review-model.js";
|
|
2
|
+
import { buildReviewResult, createDoctorResult, renderDoctorAgentReport, renderDoctorResult, } from "../../core/review-model.js";
|
|
3
3
|
import { stableStringify } from "../../utils/stable-json.js";
|
|
4
4
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
5
5
|
export async function runDoctor(options) {
|
|
6
6
|
const review = await buildReviewResult(options);
|
|
7
7
|
const doctor = createDoctorResult(review);
|
|
8
|
-
|
|
8
|
+
const output = options.agentReport
|
|
9
|
+
? renderDoctorAgentReport(doctor)
|
|
10
|
+
: renderDoctorResult(doctor, options.verdictOnly);
|
|
11
|
+
process.stdout.write(output + "\n");
|
|
9
12
|
if (options.jsonFile) {
|
|
10
13
|
await writeFileAtomic(options.jsonFile, stableStringify(doctor, 2) + "\n");
|
|
11
14
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
export function parseGaArgs(args) {
|
|
4
|
+
const options = {
|
|
5
|
+
cwd: process.cwd(),
|
|
6
|
+
workspace: false,
|
|
7
|
+
jsonFile: undefined,
|
|
8
|
+
};
|
|
9
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
10
|
+
const current = args[i];
|
|
11
|
+
const next = args[i + 1];
|
|
12
|
+
if (current === "--cwd" && next) {
|
|
13
|
+
options.cwd = path.resolve(next);
|
|
14
|
+
i += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (current === "--cwd")
|
|
18
|
+
throw new Error("Missing value for --cwd");
|
|
19
|
+
if (current === "--workspace") {
|
|
20
|
+
options.workspace = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (current === "--json-file" && next) {
|
|
24
|
+
options.jsonFile = path.resolve(options.cwd, next);
|
|
25
|
+
i += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (current === "--json-file")
|
|
29
|
+
throw new Error("Missing value for --json-file");
|
|
30
|
+
if (current === "--help" || current === "-h") {
|
|
31
|
+
process.stdout.write(GA_HELP);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (current.startsWith("-"))
|
|
35
|
+
throw new Error(`Unknown ga option: ${current}`);
|
|
36
|
+
throw new Error(`Unexpected ga argument: ${current}`);
|
|
37
|
+
}
|
|
38
|
+
return options;
|
|
39
|
+
}
|
|
40
|
+
const GA_HELP = `
|
|
41
|
+
rup ga — Audit release and CI readiness for Rainy Updates
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
rup ga [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--workspace Evaluate workspace package coverage
|
|
48
|
+
--json-file <path> Write JSON GA report to file
|
|
49
|
+
--cwd <path>
|
|
50
|
+
`.trimStart();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { VersionCache } from "../../cache/cache.js";
|
|
5
|
+
import { detectPackageManager } from "../../pm/detect.js";
|
|
6
|
+
import { discoverPackageDirs } from "../../workspace/discover.js";
|
|
7
|
+
import { stableStringify } from "../../utils/stable-json.js";
|
|
8
|
+
import { writeFileAtomic } from "../../utils/io.js";
|
|
9
|
+
export async function runGa(options) {
|
|
10
|
+
const packageManager = await detectPackageManager(options.cwd);
|
|
11
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
12
|
+
const cache = await VersionCache.create();
|
|
13
|
+
const checks = [];
|
|
14
|
+
checks.push({
|
|
15
|
+
name: "package-manager",
|
|
16
|
+
status: packageManager === "unknown" ? "warn" : "pass",
|
|
17
|
+
detail: packageManager === "unknown"
|
|
18
|
+
? "No supported lockfile was detected. npm-compatible execution is still possible."
|
|
19
|
+
: `Detected package manager: ${packageManager}.`,
|
|
20
|
+
});
|
|
21
|
+
checks.push({
|
|
22
|
+
name: "workspace-discovery",
|
|
23
|
+
status: packageDirs.length > 0 ? "pass" : "fail",
|
|
24
|
+
detail: `Discovered ${packageDirs.length} package manifest path(s).`,
|
|
25
|
+
});
|
|
26
|
+
const lockfileCheck = await detectLockfile(options.cwd);
|
|
27
|
+
checks.push(lockfileCheck);
|
|
28
|
+
checks.push({
|
|
29
|
+
name: "cache-backend",
|
|
30
|
+
status: cache.backend === "sqlite" ? "pass" : cache.degraded ? "warn" : "pass",
|
|
31
|
+
detail: cache.backend === "sqlite"
|
|
32
|
+
? "SQLite cache backend is available."
|
|
33
|
+
: `File cache backend active${cache.fallbackReason ? ` (${cache.fallbackReason})` : "."}`,
|
|
34
|
+
});
|
|
35
|
+
const distBuildExists = await fileExists(path.resolve(options.cwd, "dist/bin/cli.js"));
|
|
36
|
+
checks.push({
|
|
37
|
+
name: "dist-build",
|
|
38
|
+
status: distBuildExists ? "pass" : "warn",
|
|
39
|
+
detail: distBuildExists
|
|
40
|
+
? "Built CLI entrypoint exists in dist/bin/cli.js."
|
|
41
|
+
: "Built CLI entrypoint is missing; run the build before publishing a release artifact.",
|
|
42
|
+
});
|
|
43
|
+
checks.push({
|
|
44
|
+
name: "benchmark-gates",
|
|
45
|
+
status: (await fileExists(path.resolve(options.cwd, "scripts/perf-smoke.mjs"))) &&
|
|
46
|
+
(await fileExists(path.resolve(options.cwd, "scripts/benchmark.mjs")))
|
|
47
|
+
? "pass"
|
|
48
|
+
: "warn",
|
|
49
|
+
detail: "Benchmark scripts and perf smoke gates were checked for release readiness.",
|
|
50
|
+
});
|
|
51
|
+
checks.push({
|
|
52
|
+
name: "docs-contract",
|
|
53
|
+
status: (await fileExists(path.resolve(options.cwd, "README.md"))) &&
|
|
54
|
+
(await fileExists(path.resolve(options.cwd, "CHANGELOG.md")))
|
|
55
|
+
? "pass"
|
|
56
|
+
: "warn",
|
|
57
|
+
detail: "README and CHANGELOG presence verified.",
|
|
58
|
+
});
|
|
59
|
+
const errors = checks.filter((check) => check.status === "fail").map((check) => check.detail);
|
|
60
|
+
const warnings = checks.filter((check) => check.status === "warn").map((check) => check.detail);
|
|
61
|
+
const result = {
|
|
62
|
+
ready: errors.length === 0,
|
|
63
|
+
projectPath: options.cwd,
|
|
64
|
+
packageManager,
|
|
65
|
+
workspacePackages: packageDirs.length,
|
|
66
|
+
cacheBackend: cache.backend,
|
|
67
|
+
checks,
|
|
68
|
+
warnings,
|
|
69
|
+
errors,
|
|
70
|
+
};
|
|
71
|
+
process.stdout.write(renderGaResult(result) + "\n");
|
|
72
|
+
if (options.jsonFile) {
|
|
73
|
+
await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function renderGaResult(result) {
|
|
78
|
+
const lines = [
|
|
79
|
+
`Project: ${result.projectPath}`,
|
|
80
|
+
`GA Ready: ${result.ready ? "yes" : "no"}`,
|
|
81
|
+
`Package Manager: ${result.packageManager}`,
|
|
82
|
+
`Workspace Packages: ${result.workspacePackages}`,
|
|
83
|
+
`Cache Backend: ${result.cacheBackend}`,
|
|
84
|
+
"",
|
|
85
|
+
"Checks:",
|
|
86
|
+
...result.checks.map((check) => `- [${check.status}] ${check.name}: ${check.detail}`),
|
|
87
|
+
];
|
|
88
|
+
if (result.warnings.length > 0) {
|
|
89
|
+
lines.push("", "Warnings:");
|
|
90
|
+
lines.push(...result.warnings.map((warning) => `- ${warning}`));
|
|
91
|
+
}
|
|
92
|
+
if (result.errors.length > 0) {
|
|
93
|
+
lines.push("", "Errors:");
|
|
94
|
+
lines.push(...result.errors.map((error) => `- ${error}`));
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
async function detectLockfile(cwd) {
|
|
99
|
+
const lockfiles = [
|
|
100
|
+
"pnpm-lock.yaml",
|
|
101
|
+
"package-lock.json",
|
|
102
|
+
"npm-shrinkwrap.json",
|
|
103
|
+
"bun.lock",
|
|
104
|
+
"yarn.lock",
|
|
105
|
+
];
|
|
106
|
+
for (const candidate of lockfiles) {
|
|
107
|
+
if (await fileExists(path.resolve(cwd, candidate))) {
|
|
108
|
+
return {
|
|
109
|
+
name: "lockfile",
|
|
110
|
+
status: "pass",
|
|
111
|
+
detail: `Detected lockfile: ${candidate}.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: "lockfile",
|
|
117
|
+
status: "warn",
|
|
118
|
+
detail: "No supported lockfile was detected.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function fileExists(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(filePath);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -25,7 +25,7 @@ export async function runResolve(options) {
|
|
|
25
25
|
let versionOverrides;
|
|
26
26
|
if (options.afterUpdate) {
|
|
27
27
|
versionOverrides = await fetchProposedVersions(options);
|
|
28
|
-
if (versionOverrides.size === 0) {
|
|
28
|
+
if (versionOverrides.size === 0 && !options.silent) {
|
|
29
29
|
process.stderr.write("[resolve] No pending updates found — checking current state.\n");
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -41,10 +41,14 @@ export async function runResolve(options) {
|
|
|
41
41
|
result.conflicts = conflicts;
|
|
42
42
|
result.errorConflicts = conflicts.filter((c) => c.severity === "error").length;
|
|
43
43
|
result.warningConflicts = conflicts.filter((c) => c.severity === "warning").length;
|
|
44
|
-
|
|
44
|
+
if (!options.silent) {
|
|
45
|
+
process.stdout.write(renderConflictsTable(result, options) + "\n");
|
|
46
|
+
}
|
|
45
47
|
if (options.jsonFile) {
|
|
46
48
|
await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
|
|
47
|
-
|
|
49
|
+
if (!options.silent) {
|
|
50
|
+
process.stderr.write(`[resolve] JSON report written to ${options.jsonFile}\n`);
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
return result;
|
|
50
54
|
}
|
|
@@ -46,6 +46,7 @@ export function parseReviewArgs(args) {
|
|
|
46
46
|
risk: undefined,
|
|
47
47
|
diff: undefined,
|
|
48
48
|
applySelected: false,
|
|
49
|
+
showChangelog: false,
|
|
49
50
|
};
|
|
50
51
|
for (let i = 0; i < args.length; i += 1) {
|
|
51
52
|
const current = args[i];
|
|
@@ -93,6 +94,10 @@ export function parseReviewArgs(args) {
|
|
|
93
94
|
options.applySelected = true;
|
|
94
95
|
continue;
|
|
95
96
|
}
|
|
97
|
+
if (current === "--show-changelog") {
|
|
98
|
+
options.showChangelog = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
96
101
|
if (current === "--json-file" && next) {
|
|
97
102
|
options.jsonFile = path.resolve(options.cwd, next);
|
|
98
103
|
i += 1;
|
|
@@ -164,6 +169,7 @@ Options:
|
|
|
164
169
|
--risk <level> Minimum risk: critical, high, medium, low
|
|
165
170
|
--diff <level> Filter by patch, minor, major, latest
|
|
166
171
|
--apply-selected Apply all filtered updates after review
|
|
172
|
+
--show-changelog Fetch release notes summaries for review output
|
|
167
173
|
--workspace Scan all workspace packages
|
|
168
174
|
--policy-file <path> Load policy overrides
|
|
169
175
|
--json-file <path> Write JSON review report to file
|
|
@@ -6,10 +6,11 @@ import { stableStringify } from "../../utils/stable-json.js";
|
|
|
6
6
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
7
7
|
export async function runReview(options) {
|
|
8
8
|
const review = await buildReviewResult(options);
|
|
9
|
-
let
|
|
9
|
+
let selectedItems = review.items;
|
|
10
10
|
if (options.interactive && review.updates.length > 0) {
|
|
11
|
-
|
|
11
|
+
selectedItems = await runTui(review.items);
|
|
12
12
|
}
|
|
13
|
+
const selectedUpdates = selectedItems.map((item) => item.update);
|
|
13
14
|
if (options.applySelected && selectedUpdates.length > 0) {
|
|
14
15
|
await applySelectedUpdates({
|
|
15
16
|
...options,
|
|
@@ -20,8 +21,8 @@ export async function runReview(options) {
|
|
|
20
21
|
}
|
|
21
22
|
process.stdout.write(renderReviewResult({
|
|
22
23
|
...review,
|
|
24
|
+
items: selectedItems,
|
|
23
25
|
updates: selectedUpdates,
|
|
24
|
-
items: review.items.filter((item) => selectedUpdates.some((selected) => selected.name === item.update.name && selected.packagePath === item.update.packagePath)),
|
|
25
26
|
}) + "\n");
|
|
26
27
|
if (options.jsonFile) {
|
|
27
28
|
await writeFileAtomic(options.jsonFile, stableStringify(review, 2) + "\n");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuditOptions, CheckOptions, HealthOptions, LicenseOptions, ResolveOptions, UnusedOptions } from "../../types/index.js";
|
|
2
|
+
export declare function toAuditOptions(options: CheckOptions): AuditOptions;
|
|
3
|
+
export declare function toResolveOptions(options: CheckOptions): ResolveOptions;
|
|
4
|
+
export declare function toHealthOptions(options: CheckOptions): HealthOptions;
|
|
5
|
+
export declare function toLicenseOptions(options: CheckOptions): LicenseOptions;
|
|
6
|
+
export declare function toUnusedOptions(options: CheckOptions): UnusedOptions;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function toAuditOptions(options) {
|
|
2
|
+
return {
|
|
3
|
+
cwd: options.cwd,
|
|
4
|
+
workspace: options.workspace,
|
|
5
|
+
severity: undefined,
|
|
6
|
+
fix: false,
|
|
7
|
+
dryRun: true,
|
|
8
|
+
commit: false,
|
|
9
|
+
packageManager: "auto",
|
|
10
|
+
reportFormat: "json",
|
|
11
|
+
sourceMode: "auto",
|
|
12
|
+
jsonFile: undefined,
|
|
13
|
+
concurrency: options.concurrency,
|
|
14
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
15
|
+
silent: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function toResolveOptions(options) {
|
|
19
|
+
return {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
workspace: options.workspace,
|
|
22
|
+
afterUpdate: true,
|
|
23
|
+
safe: false,
|
|
24
|
+
jsonFile: undefined,
|
|
25
|
+
concurrency: options.concurrency,
|
|
26
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
27
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
28
|
+
silent: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function toHealthOptions(options) {
|
|
32
|
+
return {
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
workspace: options.workspace,
|
|
35
|
+
staleDays: 365,
|
|
36
|
+
includeDeprecated: true,
|
|
37
|
+
includeAlternatives: false,
|
|
38
|
+
reportFormat: "json",
|
|
39
|
+
jsonFile: undefined,
|
|
40
|
+
concurrency: options.concurrency,
|
|
41
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function toLicenseOptions(options) {
|
|
45
|
+
return {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
workspace: options.workspace,
|
|
48
|
+
allow: undefined,
|
|
49
|
+
deny: undefined,
|
|
50
|
+
sbomFile: undefined,
|
|
51
|
+
jsonFile: undefined,
|
|
52
|
+
diffMode: false,
|
|
53
|
+
concurrency: options.concurrency,
|
|
54
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
55
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function toUnusedOptions(options) {
|
|
59
|
+
return {
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
workspace: options.workspace,
|
|
62
|
+
srcDirs: ["src", "."],
|
|
63
|
+
includeDevDependencies: true,
|
|
64
|
+
fix: false,
|
|
65
|
+
dryRun: true,
|
|
66
|
+
jsonFile: undefined,
|
|
67
|
+
concurrency: options.concurrency,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisBundle, AuditResult, PackageUpdate, ResolveResult, ReviewItem, UnusedResult } from "../../types/index.js";
|
|
2
|
+
export declare function buildReviewItems(updates: PackageUpdate[], auditResult: AuditResult, resolveResult: ResolveResult, healthResult: AnalysisBundle["health"], licenseResult: AnalysisBundle["licenses"], unusedResult: UnusedResult, config: {
|
|
3
|
+
includeChangelog?: boolean;
|
|
4
|
+
}): Promise<ReviewItem[]>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { fetchChangelog } from "../../commands/changelog/fetcher.js";
|
|
2
|
+
import { applyRiskAssessments } from "../../risk/index.js";
|
|
3
|
+
import { applyImpactScores } from "../impact.js";
|
|
4
|
+
export async function buildReviewItems(updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config) {
|
|
5
|
+
const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
|
|
6
|
+
const impactedUpdates = applyImpactScores(updates, {
|
|
7
|
+
advisoryPackages,
|
|
8
|
+
workspaceDependentCount: (name) => updates.filter((item) => item.name === name).length,
|
|
9
|
+
});
|
|
10
|
+
const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
|
|
11
|
+
const advisoriesByName = new Map();
|
|
12
|
+
const conflictsByName = new Map();
|
|
13
|
+
const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
|
|
14
|
+
const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
|
|
15
|
+
const unusedByName = new Map();
|
|
16
|
+
for (const advisory of auditResult.advisories) {
|
|
17
|
+
const list = advisoriesByName.get(advisory.packageName) ?? [];
|
|
18
|
+
list.push(advisory);
|
|
19
|
+
advisoriesByName.set(advisory.packageName, list);
|
|
20
|
+
}
|
|
21
|
+
for (const conflict of resolveResult.conflicts) {
|
|
22
|
+
const list = conflictsByName.get(conflict.requester) ?? [];
|
|
23
|
+
list.push(conflict);
|
|
24
|
+
conflictsByName.set(conflict.requester, list);
|
|
25
|
+
const peerList = conflictsByName.get(conflict.peer) ?? [];
|
|
26
|
+
peerList.push(conflict);
|
|
27
|
+
conflictsByName.set(conflict.peer, peerList);
|
|
28
|
+
}
|
|
29
|
+
for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
|
|
30
|
+
const list = unusedByName.get(issue.name) ?? [];
|
|
31
|
+
list.push(issue);
|
|
32
|
+
unusedByName.set(issue.name, list);
|
|
33
|
+
}
|
|
34
|
+
const enrichedUpdates = await maybeAttachReleaseNotes(impactedUpdates, Boolean(config.includeChangelog));
|
|
35
|
+
return applyRiskAssessments(enrichedUpdates.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName)), {
|
|
36
|
+
knownPackageNames: new Set(updates.map((item) => item.name)),
|
|
37
|
+
}).map((item) => ({
|
|
38
|
+
...item,
|
|
39
|
+
update: {
|
|
40
|
+
...item.update,
|
|
41
|
+
policyAction: derivePolicyAction(item),
|
|
42
|
+
decisionState: deriveDecisionState(item),
|
|
43
|
+
selectedByDefault: deriveDecisionState(item) !== "blocked",
|
|
44
|
+
blockedReason: deriveDecisionState(item) === "blocked"
|
|
45
|
+
? item.update.recommendedAction
|
|
46
|
+
: undefined,
|
|
47
|
+
monitorReason: item.update.healthStatus === "stale" ? "Package health should be monitored." : undefined,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
|
|
52
|
+
const advisories = advisoriesByName.get(update.name) ?? [];
|
|
53
|
+
const peerConflicts = conflictsByName.get(update.name) ?? [];
|
|
54
|
+
const health = healthByName.get(update.name);
|
|
55
|
+
const license = licenseByName.get(update.name);
|
|
56
|
+
const unusedIssues = unusedByName.get(update.name) ?? [];
|
|
57
|
+
return {
|
|
58
|
+
update: {
|
|
59
|
+
...update,
|
|
60
|
+
advisoryCount: advisories.length,
|
|
61
|
+
peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
|
|
62
|
+
? "error"
|
|
63
|
+
: peerConflicts.length > 0
|
|
64
|
+
? "warning"
|
|
65
|
+
: "none",
|
|
66
|
+
licenseStatus: licenseViolationNames.has(update.name)
|
|
67
|
+
? "denied"
|
|
68
|
+
: license
|
|
69
|
+
? "allowed"
|
|
70
|
+
: "review",
|
|
71
|
+
healthStatus: health?.flags[0] ?? "healthy",
|
|
72
|
+
},
|
|
73
|
+
advisories,
|
|
74
|
+
health,
|
|
75
|
+
peerConflicts,
|
|
76
|
+
license,
|
|
77
|
+
unusedIssues,
|
|
78
|
+
selected: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function derivePolicyAction(item) {
|
|
82
|
+
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
83
|
+
return "block";
|
|
84
|
+
}
|
|
85
|
+
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
86
|
+
return "review";
|
|
87
|
+
}
|
|
88
|
+
if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
|
|
89
|
+
return "monitor";
|
|
90
|
+
}
|
|
91
|
+
return "allow";
|
|
92
|
+
}
|
|
93
|
+
function deriveDecisionState(item) {
|
|
94
|
+
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
95
|
+
return "blocked";
|
|
96
|
+
}
|
|
97
|
+
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
98
|
+
return "actionable";
|
|
99
|
+
}
|
|
100
|
+
if (item.update.riskLevel === "high" || item.update.diffType === "major") {
|
|
101
|
+
return "review";
|
|
102
|
+
}
|
|
103
|
+
return "safe";
|
|
104
|
+
}
|
|
105
|
+
async function maybeAttachReleaseNotes(updates, includeChangelog) {
|
|
106
|
+
if (!includeChangelog || updates.length === 0) {
|
|
107
|
+
return updates;
|
|
108
|
+
}
|
|
109
|
+
return Promise.all(updates.map(async (update) => ({
|
|
110
|
+
...update,
|
|
111
|
+
releaseNotesSummary: summarizeChangelog(await fetchChangelog(update.name, update.repository)),
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
114
|
+
function summarizeChangelog(content) {
|
|
115
|
+
if (!content)
|
|
116
|
+
return undefined;
|
|
117
|
+
const lines = content
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.map((line) => line.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
const title = lines.find((line) => line.startsWith("#"))?.replace(/^#+\s*/, "") ?? "Release notes";
|
|
122
|
+
const excerpt = lines.find((line) => !line.startsWith("#")) ?? "No summary available.";
|
|
123
|
+
return {
|
|
124
|
+
source: content.includes("# Release") ? "github-release" : "changelog-file",
|
|
125
|
+
title,
|
|
126
|
+
excerpt: excerpt.slice(0, 240),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSilenced<T>(fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
export async function runSilenced(fn) {
|
|
3
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
4
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
5
|
+
process.stdout.write = (() => true);
|
|
6
|
+
process.stderr.write = (() => true);
|
|
7
|
+
try {
|
|
8
|
+
return await fn();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
process.stdout.write = stdoutWrite;
|
|
12
|
+
process.stderr.write = stderrWrite;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { check } from "./check.js";
|
|
2
|
+
import { toAuditOptions, toHealthOptions, toLicenseOptions, toResolveOptions, toUnusedOptions, } from "./analysis/options.js";
|
|
3
|
+
import { buildReviewItems } from "./analysis/review-items.js";
|
|
4
|
+
import { runSilenced } from "./analysis/run-silenced.js";
|
|
5
|
+
export async function buildAnalysisBundle(options, config = {}) {
|
|
6
|
+
const baseCheckOptions = {
|
|
7
|
+
...options,
|
|
8
|
+
interactive: false,
|
|
9
|
+
showImpact: true,
|
|
10
|
+
showHomepage: true,
|
|
11
|
+
};
|
|
12
|
+
const checkResult = await check(baseCheckOptions);
|
|
13
|
+
const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await runSilenced(() => Promise.all([
|
|
14
|
+
import("../commands/audit/runner.js").then((mod) => mod.runAudit(toAuditOptions(options))),
|
|
15
|
+
import("../commands/resolve/runner.js").then((mod) => mod.runResolve(toResolveOptions(options))),
|
|
16
|
+
import("../commands/health/runner.js").then((mod) => mod.runHealth(toHealthOptions(options))),
|
|
17
|
+
import("../commands/licenses/runner.js").then((mod) => mod.runLicenses(toLicenseOptions(options))),
|
|
18
|
+
import("../commands/unused/runner.js").then((mod) => mod.runUnused(toUnusedOptions(options))),
|
|
19
|
+
]));
|
|
20
|
+
const items = await buildReviewItems(checkResult.updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config);
|
|
21
|
+
return {
|
|
22
|
+
check: checkResult,
|
|
23
|
+
audit: auditResult,
|
|
24
|
+
resolve: resolveResult,
|
|
25
|
+
health: healthResult,
|
|
26
|
+
licenses: licenseResult,
|
|
27
|
+
unused: unusedResult,
|
|
28
|
+
items,
|
|
29
|
+
degradedSources: auditResult.sourceHealth
|
|
30
|
+
.filter((source) => source.status !== "ok")
|
|
31
|
+
.map((source) => source.source),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ArtifactManifest, CheckResult, RunOptions } from "../types/index.js";
|
|
2
|
+
export declare function createRunId(command: string, options: RunOptions, result: CheckResult): string;
|
|
3
|
+
export declare function writeArtifactManifest(command: string, options: RunOptions, result: CheckResult): Promise<ArtifactManifest | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stableStringify } from "../utils/stable-json.js";
|
|
4
|
+
import { writeFileAtomic } from "../utils/io.js";
|
|
5
|
+
export function createRunId(command, options, result) {
|
|
6
|
+
const hash = crypto.createHash("sha256");
|
|
7
|
+
hash.update(stableStringify({
|
|
8
|
+
command,
|
|
9
|
+
cwd: path.resolve(options.cwd),
|
|
10
|
+
target: options.target,
|
|
11
|
+
workspace: options.workspace,
|
|
12
|
+
ciProfile: options.ciProfile,
|
|
13
|
+
updates: result.updates.map((update) => ({
|
|
14
|
+
packagePath: update.packagePath,
|
|
15
|
+
name: update.name,
|
|
16
|
+
fromRange: update.fromRange,
|
|
17
|
+
toRange: update.toRange,
|
|
18
|
+
})),
|
|
19
|
+
}, 0));
|
|
20
|
+
return hash.digest("hex").slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
export async function writeArtifactManifest(command, options, result) {
|
|
23
|
+
const shouldWrite = options.ci ||
|
|
24
|
+
Boolean(options.jsonFile) ||
|
|
25
|
+
Boolean(options.githubOutputFile) ||
|
|
26
|
+
Boolean(options.sarifFile) ||
|
|
27
|
+
Boolean(options.prReportFile);
|
|
28
|
+
if (!shouldWrite)
|
|
29
|
+
return null;
|
|
30
|
+
const runId = result.summary.runId ?? createRunId(command, options, result);
|
|
31
|
+
const artifactManifestPath = path.resolve(options.cwd, ".artifacts", `rainy-manifest-${runId}.json`);
|
|
32
|
+
const manifest = {
|
|
33
|
+
runId,
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
command,
|
|
36
|
+
projectPath: result.projectPath,
|
|
37
|
+
ciProfile: options.ciProfile,
|
|
38
|
+
artifactManifestPath,
|
|
39
|
+
outputs: {
|
|
40
|
+
jsonFile: options.jsonFile,
|
|
41
|
+
githubOutputFile: options.githubOutputFile,
|
|
42
|
+
sarifFile: options.sarifFile,
|
|
43
|
+
prReportFile: options.prReportFile,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
await writeFileAtomic(artifactManifestPath, stableStringify(manifest, 2) + "\n");
|
|
47
|
+
return manifest;
|
|
48
|
+
}
|
package/dist/core/check.js
CHANGED
|
@@ -208,6 +208,7 @@ export async function check(options) {
|
|
|
208
208
|
continue;
|
|
209
209
|
updates.push({
|
|
210
210
|
packagePath: path.resolve(task.packageDir),
|
|
211
|
+
workspaceGroup: path.basename(task.packageDir),
|
|
211
212
|
name: task.dependency.name,
|
|
212
213
|
kind: task.dependency.kind,
|
|
213
214
|
fromRange: task.dependency.range,
|
|
@@ -267,6 +268,7 @@ export async function check(options) {
|
|
|
267
268
|
policyOverridesApplied,
|
|
268
269
|
}));
|
|
269
270
|
summary.streamedEvents = streamedEvents;
|
|
271
|
+
summary.cacheBackend = cache.backend;
|
|
270
272
|
summary.riskPackages = limitedUpdates.filter((item) => item.impactScore?.rank === "critical" || item.impactScore?.rank === "high").length;
|
|
271
273
|
return {
|
|
272
274
|
projectPath: options.cwd,
|
|
@@ -305,7 +307,10 @@ function groupUpdates(updates, groupBy) {
|
|
|
305
307
|
byGroup.set(key, current);
|
|
306
308
|
}
|
|
307
309
|
return Array.from(byGroup.entries())
|
|
308
|
-
.map(([key, items]) => ({
|
|
310
|
+
.map(([key, items]) => ({
|
|
311
|
+
key,
|
|
312
|
+
items: sortUpdates(items).map((item) => ({ ...item, groupKey: key })),
|
|
313
|
+
}))
|
|
309
314
|
.sort((left, right) => left.key.localeCompare(right.key));
|
|
310
315
|
}
|
|
311
316
|
function groupKey(update, groupBy) {
|