@rainy-updates/cli 0.5.2-rc.2 → 0.5.3
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 +107 -0
- package/README.md +105 -22
- package/dist/bin/cli.js +124 -9
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +9 -2
- package/dist/commands/audit/runner.js +8 -1
- package/dist/commands/audit/sources/index.js +8 -1
- package/dist/commands/doctor/parser.d.ts +2 -0
- package/dist/commands/doctor/parser.js +92 -0
- package/dist/commands/doctor/runner.d.ts +2 -0
- package/dist/commands/doctor/runner.js +13 -0
- package/dist/commands/resolve/runner.js +3 -0
- package/dist/commands/review/parser.d.ts +2 -0
- package/dist/commands/review/parser.js +174 -0
- package/dist/commands/review/runner.d.ts +2 -0
- package/dist/commands/review/runner.js +30 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/core/check.js +64 -5
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +6 -0
- package/dist/core/options.d.ts +8 -1
- package/dist/core/options.js +43 -0
- package/dist/core/review-model.d.ts +5 -0
- package/dist/core/review-model.js +372 -0
- package/dist/core/summary.js +11 -2
- package/dist/core/upgrade.d.ts +1 -0
- package/dist/core/upgrade.js +27 -21
- package/dist/core/warm-cache.js +28 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/output/format.d.ts +4 -1
- package/dist/output/format.js +41 -3
- package/dist/output/github.js +6 -1
- package/dist/output/sarif.js +14 -0
- package/dist/registry/npm.d.ts +22 -0
- package/dist/registry/npm.js +33 -4
- package/dist/risk/index.d.ts +3 -0
- package/dist/risk/index.js +24 -0
- package/dist/risk/scorer.d.ts +3 -0
- package/dist/risk/scorer.js +114 -0
- package/dist/risk/signals/fresh-package.d.ts +3 -0
- package/dist/risk/signals/fresh-package.js +22 -0
- package/dist/risk/signals/install-scripts.d.ts +3 -0
- package/dist/risk/signals/install-scripts.js +10 -0
- package/dist/risk/signals/maintainer-churn.d.ts +3 -0
- package/dist/risk/signals/maintainer-churn.js +11 -0
- package/dist/risk/signals/metadata.d.ts +3 -0
- package/dist/risk/signals/metadata.js +18 -0
- package/dist/risk/signals/mutable-source.d.ts +3 -0
- package/dist/risk/signals/mutable-source.js +24 -0
- package/dist/risk/signals/typosquat.d.ts +3 -0
- package/dist/risk/signals/typosquat.js +70 -0
- package/dist/risk/types.d.ts +15 -0
- package/dist/risk/types.js +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/ui/tui.d.ts +0 -4
- package/dist/ui/tui.js +103 -21
- package/package.json +10 -2
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { check } from "./check.js";
|
|
4
|
+
import { createSummary, finalizeSummary } from "./summary.js";
|
|
5
|
+
import { applyImpactScores } from "./impact.js";
|
|
6
|
+
import { applyRiskAssessments } from "../risk/index.js";
|
|
7
|
+
export async function buildReviewResult(options) {
|
|
8
|
+
const baseCheckOptions = {
|
|
9
|
+
...options,
|
|
10
|
+
interactive: false,
|
|
11
|
+
showImpact: true,
|
|
12
|
+
showHomepage: true,
|
|
13
|
+
};
|
|
14
|
+
const checkResult = await check(baseCheckOptions);
|
|
15
|
+
const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await Promise.all([
|
|
16
|
+
runSilenced(() => import("../commands/audit/runner.js").then((mod) => mod.runAudit(toAuditOptions(options)))),
|
|
17
|
+
runSilenced(() => import("../commands/resolve/runner.js").then((mod) => mod.runResolve(toResolveOptions(options)))),
|
|
18
|
+
runSilenced(() => import("../commands/health/runner.js").then((mod) => mod.runHealth(toHealthOptions(options)))),
|
|
19
|
+
runSilenced(() => import("../commands/licenses/runner.js").then((mod) => mod.runLicenses(toLicenseOptions(options)))),
|
|
20
|
+
runSilenced(() => import("../commands/unused/runner.js").then((mod) => mod.runUnused(toUnusedOptions(options)))),
|
|
21
|
+
]);
|
|
22
|
+
const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
|
|
23
|
+
const impactedUpdates = applyImpactScores(checkResult.updates, {
|
|
24
|
+
advisoryPackages,
|
|
25
|
+
workspaceDependentCount: (name) => checkResult.updates.filter((item) => item.name === name).length,
|
|
26
|
+
});
|
|
27
|
+
const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
|
|
28
|
+
const advisoriesByName = new Map();
|
|
29
|
+
const conflictsByName = new Map();
|
|
30
|
+
const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
|
|
31
|
+
const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
|
|
32
|
+
const unusedByName = new Map();
|
|
33
|
+
for (const advisory of auditResult.advisories) {
|
|
34
|
+
const list = advisoriesByName.get(advisory.packageName) ?? [];
|
|
35
|
+
list.push(advisory);
|
|
36
|
+
advisoriesByName.set(advisory.packageName, list);
|
|
37
|
+
}
|
|
38
|
+
for (const conflict of resolveResult.conflicts) {
|
|
39
|
+
const list = conflictsByName.get(conflict.requester) ?? [];
|
|
40
|
+
list.push(conflict);
|
|
41
|
+
conflictsByName.set(conflict.requester, list);
|
|
42
|
+
const peerList = conflictsByName.get(conflict.peer) ?? [];
|
|
43
|
+
peerList.push(conflict);
|
|
44
|
+
conflictsByName.set(conflict.peer, peerList);
|
|
45
|
+
}
|
|
46
|
+
for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
|
|
47
|
+
const list = unusedByName.get(issue.name) ?? [];
|
|
48
|
+
list.push(issue);
|
|
49
|
+
unusedByName.set(issue.name, list);
|
|
50
|
+
}
|
|
51
|
+
const baseItems = impactedUpdates
|
|
52
|
+
.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName))
|
|
53
|
+
.filter((item) => matchesReviewFilters(item, options));
|
|
54
|
+
const items = applyRiskAssessments(baseItems, {
|
|
55
|
+
knownPackageNames: new Set(checkResult.updates.map((item) => item.name)),
|
|
56
|
+
}).filter((item) => matchesReviewFilters(item, options));
|
|
57
|
+
const summary = createReviewSummary(checkResult.summary, items, [
|
|
58
|
+
...checkResult.errors,
|
|
59
|
+
...auditResult.errors,
|
|
60
|
+
...resolveResult.errors,
|
|
61
|
+
...healthResult.errors,
|
|
62
|
+
...licenseResult.errors,
|
|
63
|
+
...unusedResult.errors,
|
|
64
|
+
], [
|
|
65
|
+
...checkResult.warnings,
|
|
66
|
+
...auditResult.warnings,
|
|
67
|
+
...resolveResult.warnings,
|
|
68
|
+
...healthResult.warnings,
|
|
69
|
+
...licenseResult.warnings,
|
|
70
|
+
...unusedResult.warnings,
|
|
71
|
+
], options.interactive === true);
|
|
72
|
+
return {
|
|
73
|
+
projectPath: checkResult.projectPath,
|
|
74
|
+
target: checkResult.target,
|
|
75
|
+
summary,
|
|
76
|
+
items,
|
|
77
|
+
updates: items.map((item) => item.update),
|
|
78
|
+
errors: [...checkResult.errors, ...auditResult.errors, ...resolveResult.errors, ...healthResult.errors, ...licenseResult.errors, ...unusedResult.errors],
|
|
79
|
+
warnings: [...checkResult.warnings, ...auditResult.warnings, ...resolveResult.warnings, ...healthResult.warnings, ...licenseResult.warnings, ...unusedResult.warnings],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function createDoctorResult(review) {
|
|
83
|
+
const verdict = review.summary.verdict ?? deriveVerdict(review.items, review.errors);
|
|
84
|
+
const primaryFindings = buildPrimaryFindings(review);
|
|
85
|
+
return {
|
|
86
|
+
verdict,
|
|
87
|
+
summary: review.summary,
|
|
88
|
+
review,
|
|
89
|
+
primaryFindings,
|
|
90
|
+
recommendedCommand: recommendCommand(review, verdict),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function renderReviewResult(review) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push(`Project: ${review.projectPath}`);
|
|
96
|
+
lines.push(`Target: ${review.target}`);
|
|
97
|
+
lines.push(`Verdict: ${review.summary.verdict ?? "safe"}`);
|
|
98
|
+
lines.push("");
|
|
99
|
+
if (review.items.length === 0) {
|
|
100
|
+
lines.push("No reviewable updates found.");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
lines.push("Updates:");
|
|
104
|
+
for (const item of review.items) {
|
|
105
|
+
const notes = [
|
|
106
|
+
item.update.diffType,
|
|
107
|
+
item.update.riskLevel ? `risk=${item.update.riskLevel}` : undefined,
|
|
108
|
+
typeof item.update.riskScore === "number"
|
|
109
|
+
? `score=${item.update.riskScore}`
|
|
110
|
+
: undefined,
|
|
111
|
+
item.update.advisoryCount ? `security=${item.update.advisoryCount}` : undefined,
|
|
112
|
+
item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none"
|
|
113
|
+
? `peer=${item.update.peerConflictSeverity}`
|
|
114
|
+
: undefined,
|
|
115
|
+
item.update.licenseStatus && item.update.licenseStatus !== "allowed"
|
|
116
|
+
? `license=${item.update.licenseStatus}`
|
|
117
|
+
: undefined,
|
|
118
|
+
].filter(Boolean);
|
|
119
|
+
lines.push(`- ${path.basename(item.update.packagePath)} :: ${item.update.name} ${item.update.fromRange} -> ${item.update.toRange} (${notes.join(", ")})`);
|
|
120
|
+
if (item.update.riskReasons && item.update.riskReasons.length > 0) {
|
|
121
|
+
lines.push(` reasons: ${item.update.riskReasons.join("; ")}`);
|
|
122
|
+
}
|
|
123
|
+
if (item.update.recommendedAction) {
|
|
124
|
+
lines.push(` action: ${item.update.recommendedAction}`);
|
|
125
|
+
}
|
|
126
|
+
if (item.update.homepage) {
|
|
127
|
+
lines.push(` homepage: ${item.update.homepage}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (review.errors.length > 0) {
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("Errors:");
|
|
134
|
+
for (const error of review.errors) {
|
|
135
|
+
lines.push(`- ${error}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (review.warnings.length > 0) {
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push("Warnings:");
|
|
141
|
+
for (const warning of review.warnings) {
|
|
142
|
+
lines.push(`- ${warning}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(`Summary: ${review.summary.updatesFound} updates, riskPackages=${review.summary.riskPackages ?? 0}, securityPackages=${review.summary.securityPackages ?? 0}, peerConflictPackages=${review.summary.peerConflictPackages ?? 0}`);
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
export function renderDoctorResult(result, verdictOnly = false) {
|
|
150
|
+
const lines = [
|
|
151
|
+
`State: ${result.verdict}`,
|
|
152
|
+
`PrimaryRisk: ${result.primaryFindings[0] ?? "No blocking findings."}`,
|
|
153
|
+
`NextAction: ${result.recommendedCommand}`,
|
|
154
|
+
];
|
|
155
|
+
if (!verdictOnly) {
|
|
156
|
+
lines.push(`Counts: updates=${result.summary.updatesFound}, security=${result.summary.securityPackages ?? 0}, risk=${result.summary.riskPackages ?? 0}, peer=${result.summary.peerConflictPackages ?? 0}, license=${result.summary.licenseViolationPackages ?? 0}`);
|
|
157
|
+
}
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
|
|
161
|
+
const advisories = advisoriesByName.get(update.name) ?? [];
|
|
162
|
+
const peerConflicts = conflictsByName.get(update.name) ?? [];
|
|
163
|
+
const health = healthByName.get(update.name);
|
|
164
|
+
const license = licenseByName.get(update.name);
|
|
165
|
+
const unusedIssues = unusedByName.get(update.name) ?? [];
|
|
166
|
+
return {
|
|
167
|
+
update: {
|
|
168
|
+
...update,
|
|
169
|
+
advisoryCount: advisories.length,
|
|
170
|
+
peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
|
|
171
|
+
? "error"
|
|
172
|
+
: peerConflicts.length > 0
|
|
173
|
+
? "warning"
|
|
174
|
+
: "none",
|
|
175
|
+
licenseStatus: licenseViolationNames.has(update.name)
|
|
176
|
+
? "denied"
|
|
177
|
+
: license
|
|
178
|
+
? "allowed"
|
|
179
|
+
: "review",
|
|
180
|
+
healthStatus: health?.flags[0] ?? "healthy",
|
|
181
|
+
},
|
|
182
|
+
advisories,
|
|
183
|
+
health,
|
|
184
|
+
peerConflicts,
|
|
185
|
+
license,
|
|
186
|
+
unusedIssues,
|
|
187
|
+
selected: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function matchesReviewFilters(item, options) {
|
|
191
|
+
if ("securityOnly" in options && options.securityOnly && item.advisories.length === 0) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if ("risk" in options && options.risk && !riskMatches(item.update.riskLevel, options.risk)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if ("diff" in options && options.diff && item.update.diffType !== options.diff) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
function riskMatches(current, threshold) {
|
|
203
|
+
const order = {
|
|
204
|
+
critical: 4,
|
|
205
|
+
high: 3,
|
|
206
|
+
medium: 2,
|
|
207
|
+
low: 1,
|
|
208
|
+
};
|
|
209
|
+
return order[current ?? "low"] >= order[threshold];
|
|
210
|
+
}
|
|
211
|
+
function createReviewSummary(base, items, errors, warnings, interactiveSession) {
|
|
212
|
+
const summary = finalizeSummary(createSummary({
|
|
213
|
+
scannedPackages: base.scannedPackages,
|
|
214
|
+
totalDependencies: base.totalDependencies,
|
|
215
|
+
checkedDependencies: base.checkedDependencies,
|
|
216
|
+
updatesFound: items.length,
|
|
217
|
+
upgraded: base.upgraded,
|
|
218
|
+
skipped: base.skipped,
|
|
219
|
+
warmedPackages: base.warmedPackages,
|
|
220
|
+
errors,
|
|
221
|
+
warnings,
|
|
222
|
+
durations: {
|
|
223
|
+
totalMs: base.durationMs.total,
|
|
224
|
+
discoveryMs: base.durationMs.discovery,
|
|
225
|
+
registryMs: base.durationMs.registry,
|
|
226
|
+
cacheMs: base.durationMs.cache,
|
|
227
|
+
},
|
|
228
|
+
groupedUpdates: base.groupedUpdates,
|
|
229
|
+
cooldownSkipped: base.cooldownSkipped,
|
|
230
|
+
ciProfile: base.ciProfile,
|
|
231
|
+
prLimitHit: base.prLimitHit,
|
|
232
|
+
policyOverridesApplied: base.policyOverridesApplied,
|
|
233
|
+
}));
|
|
234
|
+
summary.durationMs.render = base.durationMs.render;
|
|
235
|
+
summary.streamedEvents = base.streamedEvents;
|
|
236
|
+
summary.fixPrApplied = base.fixPrApplied;
|
|
237
|
+
summary.fixBranchName = base.fixBranchName;
|
|
238
|
+
summary.fixCommitSha = base.fixCommitSha;
|
|
239
|
+
summary.fixPrBranchesCreated = base.fixPrBranchesCreated;
|
|
240
|
+
summary.interactiveSession = interactiveSession;
|
|
241
|
+
summary.securityPackages = items.filter((item) => item.advisories.length > 0).length;
|
|
242
|
+
summary.riskPackages = items.filter((item) => item.update.riskLevel === "critical" || item.update.riskLevel === "high").length;
|
|
243
|
+
summary.peerConflictPackages = items.filter((item) => item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none").length;
|
|
244
|
+
summary.licenseViolationPackages = items.filter((item) => item.update.licenseStatus === "denied").length;
|
|
245
|
+
summary.verdict = deriveVerdict(items, errors);
|
|
246
|
+
return summary;
|
|
247
|
+
}
|
|
248
|
+
function deriveVerdict(items, errors) {
|
|
249
|
+
if (items.some((item) => item.update.peerConflictSeverity === "error" ||
|
|
250
|
+
item.update.licenseStatus === "denied")) {
|
|
251
|
+
return "blocked";
|
|
252
|
+
}
|
|
253
|
+
if (items.some((item) => item.advisories.length > 0 || item.update.riskLevel === "critical")) {
|
|
254
|
+
return "actionable";
|
|
255
|
+
}
|
|
256
|
+
if (errors.length > 0 ||
|
|
257
|
+
items.some((item) => item.update.riskLevel === "high" || item.update.diffType === "major")) {
|
|
258
|
+
return "review";
|
|
259
|
+
}
|
|
260
|
+
return "safe";
|
|
261
|
+
}
|
|
262
|
+
function buildPrimaryFindings(review) {
|
|
263
|
+
const findings = [];
|
|
264
|
+
if ((review.summary.peerConflictPackages ?? 0) > 0) {
|
|
265
|
+
findings.push(`${review.summary.peerConflictPackages} package(s) have peer conflicts.`);
|
|
266
|
+
}
|
|
267
|
+
if ((review.summary.licenseViolationPackages ?? 0) > 0) {
|
|
268
|
+
findings.push(`${review.summary.licenseViolationPackages} package(s) violate license policy.`);
|
|
269
|
+
}
|
|
270
|
+
if ((review.summary.securityPackages ?? 0) > 0) {
|
|
271
|
+
findings.push(`${review.summary.securityPackages} package(s) have security advisories.`);
|
|
272
|
+
}
|
|
273
|
+
if ((review.summary.riskPackages ?? 0) > 0) {
|
|
274
|
+
findings.push(`${review.summary.riskPackages} package(s) are high risk.`);
|
|
275
|
+
}
|
|
276
|
+
if (review.errors.length > 0) {
|
|
277
|
+
findings.push(`${review.errors.length} execution error(s) need review before treating the result as clean.`);
|
|
278
|
+
}
|
|
279
|
+
if (findings.length === 0) {
|
|
280
|
+
findings.push("No blocking findings; remaining updates are low-risk.");
|
|
281
|
+
}
|
|
282
|
+
return findings;
|
|
283
|
+
}
|
|
284
|
+
function recommendCommand(review, verdict) {
|
|
285
|
+
if (verdict === "blocked")
|
|
286
|
+
return "rup review --interactive";
|
|
287
|
+
if ((review.summary.securityPackages ?? 0) > 0)
|
|
288
|
+
return "rup review --security-only";
|
|
289
|
+
if (review.errors.length > 0 || review.items.length > 0)
|
|
290
|
+
return "rup review --interactive";
|
|
291
|
+
return "rup check";
|
|
292
|
+
}
|
|
293
|
+
async function runSilenced(fn) {
|
|
294
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
295
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
296
|
+
process.stdout.write = (() => true);
|
|
297
|
+
process.stderr.write = (() => true);
|
|
298
|
+
try {
|
|
299
|
+
return await fn();
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
process.stdout.write = stdoutWrite;
|
|
303
|
+
process.stderr.write = stderrWrite;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function toAuditOptions(options) {
|
|
307
|
+
return {
|
|
308
|
+
cwd: options.cwd,
|
|
309
|
+
workspace: options.workspace,
|
|
310
|
+
severity: undefined,
|
|
311
|
+
fix: false,
|
|
312
|
+
dryRun: true,
|
|
313
|
+
commit: false,
|
|
314
|
+
packageManager: "auto",
|
|
315
|
+
reportFormat: "json",
|
|
316
|
+
sourceMode: "auto",
|
|
317
|
+
jsonFile: undefined,
|
|
318
|
+
concurrency: options.concurrency,
|
|
319
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function toResolveOptions(options) {
|
|
323
|
+
return {
|
|
324
|
+
cwd: options.cwd,
|
|
325
|
+
workspace: options.workspace,
|
|
326
|
+
afterUpdate: true,
|
|
327
|
+
safe: false,
|
|
328
|
+
jsonFile: undefined,
|
|
329
|
+
concurrency: options.concurrency,
|
|
330
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
331
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function toHealthOptions(options) {
|
|
335
|
+
return {
|
|
336
|
+
cwd: options.cwd,
|
|
337
|
+
workspace: options.workspace,
|
|
338
|
+
staleDays: 365,
|
|
339
|
+
includeDeprecated: true,
|
|
340
|
+
includeAlternatives: false,
|
|
341
|
+
reportFormat: "json",
|
|
342
|
+
jsonFile: undefined,
|
|
343
|
+
concurrency: options.concurrency,
|
|
344
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function toLicenseOptions(options) {
|
|
348
|
+
return {
|
|
349
|
+
cwd: options.cwd,
|
|
350
|
+
workspace: options.workspace,
|
|
351
|
+
allow: undefined,
|
|
352
|
+
deny: undefined,
|
|
353
|
+
sbomFile: undefined,
|
|
354
|
+
jsonFile: undefined,
|
|
355
|
+
diffMode: false,
|
|
356
|
+
concurrency: options.concurrency,
|
|
357
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
358
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function toUnusedOptions(options) {
|
|
362
|
+
return {
|
|
363
|
+
cwd: options.cwd,
|
|
364
|
+
workspace: options.workspace,
|
|
365
|
+
srcDirs: ["src", "."],
|
|
366
|
+
includeDevDependencies: true,
|
|
367
|
+
fix: false,
|
|
368
|
+
dryRun: true,
|
|
369
|
+
jsonFile: undefined,
|
|
370
|
+
concurrency: options.concurrency,
|
|
371
|
+
};
|
|
372
|
+
}
|
package/dist/core/summary.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasErrorCode } from "./errors.js";
|
|
1
2
|
export function createSummary(input) {
|
|
2
3
|
const offlineCacheMiss = input.errors.filter((error) => isOfflineCacheMissError(error)).length;
|
|
3
4
|
const registryFailure = input.errors.filter((error) => isRegistryFailureError(error)).length;
|
|
@@ -42,6 +43,13 @@ export function createSummary(input) {
|
|
|
42
43
|
prLimitHit: input.prLimitHit === true,
|
|
43
44
|
streamedEvents: 0,
|
|
44
45
|
policyOverridesApplied: Math.max(0, Math.round(input.policyOverridesApplied ?? 0)),
|
|
46
|
+
verdict: undefined,
|
|
47
|
+
interactiveSession: false,
|
|
48
|
+
riskPackages: 0,
|
|
49
|
+
securityPackages: 0,
|
|
50
|
+
peerConflictPackages: 0,
|
|
51
|
+
licenseViolationPackages: 0,
|
|
52
|
+
privateRegistryPackages: 0,
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
55
|
export function finalizeSummary(summary) {
|
|
@@ -83,11 +91,12 @@ function isOfflineCacheMissError(value) {
|
|
|
83
91
|
return value.includes("Offline cache miss");
|
|
84
92
|
}
|
|
85
93
|
function isRegistryFailureError(value) {
|
|
86
|
-
return (value
|
|
94
|
+
return (hasErrorCode(value, "REGISTRY_ERROR") ||
|
|
95
|
+
value.includes("Unable to resolve") ||
|
|
87
96
|
value.includes("Unable to warm") ||
|
|
88
97
|
value.includes("Registry request failed") ||
|
|
89
98
|
value.includes("Registry temporary error"));
|
|
90
99
|
}
|
|
91
100
|
function isRegistryAuthError(value) {
|
|
92
|
-
return value.includes("Registry authentication failed") || value.includes("401");
|
|
101
|
+
return hasErrorCode(value, "AUTH_ERROR") || value.includes("Registry authentication failed") || value.includes("401");
|
|
93
102
|
}
|
package/dist/core/upgrade.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { UpgradeOptions, UpgradeResult } from "../types/index.js";
|
|
2
2
|
export declare function upgrade(options: UpgradeOptions): Promise<UpgradeResult>;
|
|
3
|
+
export declare function applySelectedUpdates(options: UpgradeOptions, updates: UpgradeResult["updates"]): Promise<void>;
|
package/dist/core/upgrade.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { check } from "./check.js";
|
|
2
2
|
import { readManifest, writeManifest } from "../parsers/package-json.js";
|
|
3
3
|
import { installDependencies } from "../pm/install.js";
|
|
4
|
+
import { detectPackageManager } from "../pm/detect.js";
|
|
4
5
|
import { applyRangeStyle, parseVersion, compareVersions } from "../utils/semver.js";
|
|
5
6
|
import { buildWorkspaceGraph } from "../workspace/graph.js";
|
|
6
7
|
import { captureLockfileSnapshot, changedLockfiles, validateLockfileMode } from "../utils/lockfile.js";
|
|
@@ -14,8 +15,30 @@ export async function upgrade(options) {
|
|
|
14
15
|
changed: false,
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
await applySelectedUpdates(options, checkResult.updates);
|
|
19
|
+
const lockfileChanges = await changedLockfiles(options.cwd, lockfilesBefore);
|
|
20
|
+
if (lockfileChanges.length > 0 && (options.lockfileMode === "preserve" || options.lockfileMode === "error")) {
|
|
21
|
+
throw new Error(`Lockfile changes detected in ${options.lockfileMode} mode: ${lockfileChanges.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
if (lockfileChanges.length > 0 && options.lockfileMode === "update") {
|
|
24
|
+
checkResult.warnings.push(`Lockfiles changed: ${lockfileChanges.map((item) => item.split("/").pop()).join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
...checkResult,
|
|
28
|
+
changed: true,
|
|
29
|
+
summary: {
|
|
30
|
+
...checkResult.summary,
|
|
31
|
+
upgraded: checkResult.updates.length,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function applySelectedUpdates(options, updates) {
|
|
36
|
+
validateLockfileMode(options.lockfileMode, options.install);
|
|
37
|
+
if (updates.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
17
40
|
const manifestsByPath = new Map();
|
|
18
|
-
for (const update of
|
|
41
|
+
for (const update of updates) {
|
|
19
42
|
const manifestPath = update.packagePath;
|
|
20
43
|
let manifest = manifestsByPath.get(manifestPath);
|
|
21
44
|
if (!manifest) {
|
|
@@ -26,32 +49,15 @@ export async function upgrade(options) {
|
|
|
26
49
|
}
|
|
27
50
|
if (options.sync) {
|
|
28
51
|
const graph = buildWorkspaceGraph(manifestsByPath, options.includeKinds);
|
|
29
|
-
|
|
30
|
-
checkResult.warnings.push(`Workspace graph contains cycle(s): ${graph.cycles.map((cycle) => cycle.join(" -> ")).join(" | ")}`);
|
|
31
|
-
}
|
|
32
|
-
applyWorkspaceSync(manifestsByPath, graph.orderedPaths, graph.localPackageNames, options.includeKinds, checkResult.updates);
|
|
52
|
+
applyWorkspaceSync(manifestsByPath, graph.orderedPaths, graph.localPackageNames, options.includeKinds, updates);
|
|
33
53
|
}
|
|
34
54
|
for (const [manifestPath, manifest] of manifestsByPath) {
|
|
35
55
|
await writeManifest(manifestPath, manifest);
|
|
36
56
|
}
|
|
37
57
|
if (options.install) {
|
|
38
|
-
await
|
|
39
|
-
|
|
40
|
-
const lockfileChanges = await changedLockfiles(options.cwd, lockfilesBefore);
|
|
41
|
-
if (lockfileChanges.length > 0 && (options.lockfileMode === "preserve" || options.lockfileMode === "error")) {
|
|
42
|
-
throw new Error(`Lockfile changes detected in ${options.lockfileMode} mode: ${lockfileChanges.join(", ")}`);
|
|
43
|
-
}
|
|
44
|
-
if (lockfileChanges.length > 0 && options.lockfileMode === "update") {
|
|
45
|
-
checkResult.warnings.push(`Lockfiles changed: ${lockfileChanges.map((item) => item.split("/").pop()).join(", ")}`);
|
|
58
|
+
const detected = await detectPackageManager(options.cwd);
|
|
59
|
+
await installDependencies(options.cwd, options.packageManager, detected);
|
|
46
60
|
}
|
|
47
|
-
return {
|
|
48
|
-
...checkResult,
|
|
49
|
-
changed: true,
|
|
50
|
-
summary: {
|
|
51
|
-
...checkResult.summary,
|
|
52
|
-
upgraded: checkResult.updates.length,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
61
|
}
|
|
56
62
|
function applyWorkspaceSync(manifestsByPath, orderedPaths, localPackageNames, includeKinds, updates) {
|
|
57
63
|
const desiredByPackage = new Map();
|
package/dist/core/warm-cache.js
CHANGED
|
@@ -6,6 +6,7 @@ import { NpmRegistryClient } from "../registry/npm.js";
|
|
|
6
6
|
import { detectPackageManager } from "../pm/detect.js";
|
|
7
7
|
import { discoverPackageDirs } from "../workspace/discover.js";
|
|
8
8
|
import { createSummary, finalizeSummary } from "./summary.js";
|
|
9
|
+
import { formatClassifiedMessage } from "./errors.js";
|
|
9
10
|
export async function warmCache(options) {
|
|
10
11
|
const startedAt = Date.now();
|
|
11
12
|
let discoveryMs = 0;
|
|
@@ -23,7 +24,13 @@ export async function warmCache(options) {
|
|
|
23
24
|
const errors = [];
|
|
24
25
|
const warnings = [];
|
|
25
26
|
if (cache.degraded) {
|
|
26
|
-
warnings.push(
|
|
27
|
+
warnings.push(formatClassifiedMessage({
|
|
28
|
+
code: "CACHE_BACKEND_FALLBACK",
|
|
29
|
+
whatFailed: cache.fallbackReason ?? "Preferred SQLite cache backend is unavailable.",
|
|
30
|
+
intact: "Warm-cache continues using the file cache backend.",
|
|
31
|
+
validity: "partial",
|
|
32
|
+
next: "Restore SQLite support or unset the forced file backend override if you need the preferred backend.",
|
|
33
|
+
}));
|
|
27
34
|
}
|
|
28
35
|
let totalDependencies = 0;
|
|
29
36
|
const packageNames = new Set();
|
|
@@ -72,7 +79,13 @@ export async function warmCache(options) {
|
|
|
72
79
|
warmed += 1;
|
|
73
80
|
}
|
|
74
81
|
else {
|
|
75
|
-
errors.push(
|
|
82
|
+
errors.push(formatClassifiedMessage({
|
|
83
|
+
code: "REGISTRY_ERROR",
|
|
84
|
+
whatFailed: `Offline cache miss for ${pkg}.`,
|
|
85
|
+
intact: "Previously cached packages remain available.",
|
|
86
|
+
validity: "invalid",
|
|
87
|
+
next: "Retry warm-cache without --offline.",
|
|
88
|
+
}));
|
|
76
89
|
emitStream(`[error] Offline cache miss for ${pkg}`);
|
|
77
90
|
}
|
|
78
91
|
}
|
|
@@ -96,8 +109,19 @@ export async function warmCache(options) {
|
|
|
96
109
|
}
|
|
97
110
|
cacheMs += Date.now() - cacheWriteStartedAt;
|
|
98
111
|
for (const [pkg, error] of fetched.errors) {
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
const classified = formatClassifiedMessage({
|
|
113
|
+
code: error.includes("401") || error.includes("403")
|
|
114
|
+
? "AUTH_ERROR"
|
|
115
|
+
: "REGISTRY_ERROR",
|
|
116
|
+
whatFailed: `Unable to warm ${pkg}: ${error}.`,
|
|
117
|
+
intact: "Any successfully warmed packages remain cached.",
|
|
118
|
+
validity: "partial",
|
|
119
|
+
next: error.includes("401") || error.includes("403")
|
|
120
|
+
? "Check registry credentials in .npmrc and retry warm-cache."
|
|
121
|
+
: "Retry warm-cache or continue with stale cache if available.",
|
|
122
|
+
});
|
|
123
|
+
errors.push(classified);
|
|
124
|
+
emitStream(`[error] ${classified}`);
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
127
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,6 @@ export { saveBaseline, diffBaseline } from "./core/baseline.js";
|
|
|
7
7
|
export { createSarifReport } from "./output/sarif.js";
|
|
8
8
|
export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
|
|
9
9
|
export { renderPrReport } from "./output/pr-report.js";
|
|
10
|
-
export
|
|
10
|
+
export { buildReviewResult, createDoctorResult } from "./core/review-model.js";
|
|
11
|
+
export { applyRiskAssessments } from "./risk/index.js";
|
|
12
|
+
export type { CheckOptions, CheckResult, CiProfile, DependencyKind, FailOnLevel, GroupBy, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, ReviewOptions, ReviewResult, DoctorOptions, DoctorResult, Verdict, RiskLevel, RiskCategory, RiskAssessment, RiskFactor, MaintainerChurnStatus, } from "./types/index.js";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,5 @@ export { saveBaseline, diffBaseline } from "./core/baseline.js";
|
|
|
7
7
|
export { createSarifReport } from "./output/sarif.js";
|
|
8
8
|
export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
|
|
9
9
|
export { renderPrReport } from "./output/pr-report.js";
|
|
10
|
+
export { buildReviewResult, createDoctorResult } from "./core/review-model.js";
|
|
11
|
+
export { applyRiskAssessments } from "./risk/index.js";
|
package/dist/output/format.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { CheckResult, OutputFormat } from "../types/index.js";
|
|
2
|
-
export declare function renderResult(result: CheckResult, format: OutputFormat
|
|
2
|
+
export declare function renderResult(result: CheckResult, format: OutputFormat, display?: {
|
|
3
|
+
showImpact?: boolean;
|
|
4
|
+
showHomepage?: boolean;
|
|
5
|
+
}): string;
|
package/dist/output/format.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderGitHubAnnotations } from "./github.js";
|
|
2
2
|
import { stableStringify } from "../utils/stable-json.js";
|
|
3
|
-
export function renderResult(result, format) {
|
|
3
|
+
export function renderResult(result, format, display = {}) {
|
|
4
4
|
if (format === "json") {
|
|
5
5
|
return stableStringify(result, 2);
|
|
6
6
|
}
|
|
@@ -8,10 +8,27 @@ export function renderResult(result, format) {
|
|
|
8
8
|
if (result.updates.length === 0 && result.summary.warmedPackages > 0) {
|
|
9
9
|
return `Cache warmed for ${result.summary.warmedPackages} package(s).`;
|
|
10
10
|
}
|
|
11
|
+
if (result.updates.length === 0 && result.errors.length > 0) {
|
|
12
|
+
const [firstError] = result.errors;
|
|
13
|
+
const suffix = result.errors.length > 1 ? ` (+${result.errors.length - 1} more errors)` : "";
|
|
14
|
+
return `${firstError}${suffix}`;
|
|
15
|
+
}
|
|
11
16
|
if (result.updates.length === 0)
|
|
12
17
|
return "No updates found.";
|
|
13
18
|
return result.updates
|
|
14
|
-
.map((item) =>
|
|
19
|
+
.map((item) => {
|
|
20
|
+
const parts = [`${item.packagePath} :: ${item.name}: ${item.fromRange} -> ${item.toRange}`];
|
|
21
|
+
if (display.showImpact && item.impactScore) {
|
|
22
|
+
parts.push(`impact=${item.impactScore.rank}:${item.impactScore.score}`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof item.riskScore === "number") {
|
|
25
|
+
parts.push(`risk=${item.riskLevel}:${item.riskScore}`);
|
|
26
|
+
}
|
|
27
|
+
if (display.showHomepage && item.homepage) {
|
|
28
|
+
parts.push(item.homepage);
|
|
29
|
+
}
|
|
30
|
+
return parts.join(" | ");
|
|
31
|
+
})
|
|
15
32
|
.join("\n");
|
|
16
33
|
}
|
|
17
34
|
if (format === "github") {
|
|
@@ -40,6 +57,11 @@ export function renderResult(result, format) {
|
|
|
40
57
|
`policy_overrides_applied=${result.summary.policyOverridesApplied}`,
|
|
41
58
|
`registry_auth_failures=${result.summary.errorCounts.registryAuthFailure}`,
|
|
42
59
|
`fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
|
|
60
|
+
`verdict=${result.summary.verdict ?? ""}`,
|
|
61
|
+
`risk_packages=${result.summary.riskPackages ?? 0}`,
|
|
62
|
+
`security_packages=${result.summary.securityPackages ?? 0}`,
|
|
63
|
+
`peer_conflict_packages=${result.summary.peerConflictPackages ?? 0}`,
|
|
64
|
+
`license_violation_packages=${result.summary.licenseViolationPackages ?? 0}`,
|
|
43
65
|
].join("\n");
|
|
44
66
|
}
|
|
45
67
|
const lines = [];
|
|
@@ -59,7 +81,20 @@ export function renderResult(result, format) {
|
|
|
59
81
|
else {
|
|
60
82
|
lines.push("Updates:");
|
|
61
83
|
for (const update of result.updates) {
|
|
62
|
-
lines.push(`- ${update.packagePath} :: ${update.name} [${update.kind}] ${update.fromRange} -> ${update.toRange} (${
|
|
84
|
+
lines.push(`- ${update.packagePath} :: ${update.name} [${update.kind}] ${update.fromRange} -> ${update.toRange} (${[
|
|
85
|
+
update.diffType,
|
|
86
|
+
display.showImpact && update.impactScore
|
|
87
|
+
? `impact=${update.impactScore.rank}:${update.impactScore.score}`
|
|
88
|
+
: undefined,
|
|
89
|
+
display.showHomepage && update.homepage ? update.homepage : undefined,
|
|
90
|
+
update.riskLevel ? `risk=${update.riskLevel}` : undefined,
|
|
91
|
+
typeof update.riskScore === "number" ? `score=${update.riskScore}` : undefined,
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(", ")})`);
|
|
95
|
+
if (update.recommendedAction) {
|
|
96
|
+
lines.push(` action: ${update.recommendedAction}`);
|
|
97
|
+
}
|
|
63
98
|
}
|
|
64
99
|
}
|
|
65
100
|
if (result.errors.length > 0) {
|
|
@@ -80,6 +115,9 @@ export function renderResult(result, format) {
|
|
|
80
115
|
lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
|
|
81
116
|
lines.push(`Groups=${result.summary.groupedUpdates}, cooldownSkipped=${result.summary.cooldownSkipped}, ciProfile=${result.summary.ciProfile}, prLimitHit=${result.summary.prLimitHit ? "yes" : "no"}`);
|
|
82
117
|
lines.push(`StreamedEvents=${result.summary.streamedEvents}, policyOverrides=${result.summary.policyOverridesApplied}, registryAuthFailures=${result.summary.errorCounts.registryAuthFailure}`);
|
|
118
|
+
if (result.summary.verdict) {
|
|
119
|
+
lines.push(`Verdict=${result.summary.verdict}, riskPackages=${result.summary.riskPackages ?? 0}, securityPackages=${result.summary.securityPackages ?? 0}, peerConflictPackages=${result.summary.peerConflictPackages ?? 0}, licenseViolationPackages=${result.summary.licenseViolationPackages ?? 0}`);
|
|
120
|
+
}
|
|
83
121
|
lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
|
|
84
122
|
if (result.summary.fixPrApplied) {
|
|
85
123
|
lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
|
package/dist/output/github.js
CHANGED
|
@@ -21,6 +21,11 @@ export async function writeGitHubOutput(filePath, result) {
|
|
|
21
21
|
`streamed_events=${result.summary.streamedEvents}`,
|
|
22
22
|
`policy_overrides_applied=${result.summary.policyOverridesApplied}`,
|
|
23
23
|
`registry_auth_failures=${result.summary.errorCounts.registryAuthFailure}`,
|
|
24
|
+
`verdict=${result.summary.verdict ?? ""}`,
|
|
25
|
+
`risk_packages=${result.summary.riskPackages ?? 0}`,
|
|
26
|
+
`security_packages=${result.summary.securityPackages ?? 0}`,
|
|
27
|
+
`peer_conflict_packages=${result.summary.peerConflictPackages ?? 0}`,
|
|
28
|
+
`license_violation_packages=${result.summary.licenseViolationPackages ?? 0}`,
|
|
24
29
|
`fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
|
|
25
30
|
`fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
|
|
26
31
|
`fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
|
|
@@ -37,7 +42,7 @@ export function renderGitHubAnnotations(result) {
|
|
|
37
42
|
return left.packagePath.localeCompare(right.packagePath);
|
|
38
43
|
});
|
|
39
44
|
for (const update of sortedUpdates) {
|
|
40
|
-
lines.push(`::notice title=Dependency Update::${update.name} ${update.fromRange} -> ${update.toRange} (${update.packagePath})`);
|
|
45
|
+
lines.push(`::notice title=Dependency Update::${update.name} ${update.fromRange} -> ${update.toRange} (${update.packagePath})${typeof update.riskScore === "number" ? ` [risk=${update.riskLevel}:${update.riskScore}]` : ""}`);
|
|
41
46
|
}
|
|
42
47
|
for (const warning of [...result.warnings].sort((a, b) => a.localeCompare(b))) {
|
|
43
48
|
lines.push(`::warning title=Rainy Updates::${warning}`);
|