@rainy-updates/cli 0.5.2-rc.1 → 0.5.2

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +34 -1
  3. package/dist/bin/cli.js +128 -3
  4. package/dist/cache/cache.d.ts +1 -0
  5. package/dist/cache/cache.js +9 -2
  6. package/dist/commands/audit/fetcher.d.ts +2 -6
  7. package/dist/commands/audit/fetcher.js +2 -79
  8. package/dist/commands/audit/mapper.d.ts +8 -1
  9. package/dist/commands/audit/mapper.js +105 -9
  10. package/dist/commands/audit/parser.js +36 -2
  11. package/dist/commands/audit/runner.js +186 -15
  12. package/dist/commands/audit/sources/github.d.ts +2 -0
  13. package/dist/commands/audit/sources/github.js +125 -0
  14. package/dist/commands/audit/sources/index.d.ts +6 -0
  15. package/dist/commands/audit/sources/index.js +99 -0
  16. package/dist/commands/audit/sources/osv.d.ts +2 -0
  17. package/dist/commands/audit/sources/osv.js +131 -0
  18. package/dist/commands/audit/sources/types.d.ts +21 -0
  19. package/dist/commands/audit/sources/types.js +1 -0
  20. package/dist/commands/audit/targets.d.ts +20 -0
  21. package/dist/commands/audit/targets.js +314 -0
  22. package/dist/commands/changelog/fetcher.d.ts +9 -0
  23. package/dist/commands/changelog/fetcher.js +130 -0
  24. package/dist/commands/doctor/parser.d.ts +2 -0
  25. package/dist/commands/doctor/parser.js +92 -0
  26. package/dist/commands/doctor/runner.d.ts +2 -0
  27. package/dist/commands/doctor/runner.js +13 -0
  28. package/dist/commands/resolve/runner.js +3 -0
  29. package/dist/commands/review/parser.d.ts +2 -0
  30. package/dist/commands/review/parser.js +174 -0
  31. package/dist/commands/review/runner.d.ts +2 -0
  32. package/dist/commands/review/runner.js +30 -0
  33. package/dist/config/loader.d.ts +3 -0
  34. package/dist/core/check.js +39 -5
  35. package/dist/core/errors.d.ts +11 -0
  36. package/dist/core/errors.js +6 -0
  37. package/dist/core/options.d.ts +8 -1
  38. package/dist/core/options.js +43 -0
  39. package/dist/core/review-model.d.ts +5 -0
  40. package/dist/core/review-model.js +382 -0
  41. package/dist/core/summary.js +11 -2
  42. package/dist/core/upgrade.d.ts +1 -0
  43. package/dist/core/upgrade.js +27 -21
  44. package/dist/core/warm-cache.js +28 -4
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +1 -0
  47. package/dist/output/format.d.ts +4 -1
  48. package/dist/output/format.js +29 -3
  49. package/dist/output/github.js +5 -0
  50. package/dist/output/sarif.js +11 -0
  51. package/dist/registry/npm.d.ts +20 -0
  52. package/dist/registry/npm.js +27 -4
  53. package/dist/types/index.d.ts +91 -1
  54. package/dist/ui/tui.d.ts +2 -0
  55. package/dist/ui/tui.js +107 -0
  56. package/package.json +12 -2
@@ -0,0 +1,382 @@
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
+ export async function buildReviewResult(options) {
7
+ const baseCheckOptions = {
8
+ ...options,
9
+ interactive: false,
10
+ showImpact: true,
11
+ showHomepage: true,
12
+ };
13
+ const checkResult = await check(baseCheckOptions);
14
+ const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await Promise.all([
15
+ runSilenced(() => import("../commands/audit/runner.js").then((mod) => mod.runAudit(toAuditOptions(options)))),
16
+ runSilenced(() => import("../commands/resolve/runner.js").then((mod) => mod.runResolve(toResolveOptions(options)))),
17
+ runSilenced(() => import("../commands/health/runner.js").then((mod) => mod.runHealth(toHealthOptions(options)))),
18
+ runSilenced(() => import("../commands/licenses/runner.js").then((mod) => mod.runLicenses(toLicenseOptions(options)))),
19
+ runSilenced(() => import("../commands/unused/runner.js").then((mod) => mod.runUnused(toUnusedOptions(options)))),
20
+ ]);
21
+ const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
22
+ const impactedUpdates = applyImpactScores(checkResult.updates, {
23
+ advisoryPackages,
24
+ workspaceDependentCount: (name) => checkResult.updates.filter((item) => item.name === name).length,
25
+ });
26
+ const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
27
+ const advisoriesByName = new Map();
28
+ const conflictsByName = new Map();
29
+ const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
30
+ const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
31
+ const unusedByName = new Map();
32
+ for (const advisory of auditResult.advisories) {
33
+ const list = advisoriesByName.get(advisory.packageName) ?? [];
34
+ list.push(advisory);
35
+ advisoriesByName.set(advisory.packageName, list);
36
+ }
37
+ for (const conflict of resolveResult.conflicts) {
38
+ const list = conflictsByName.get(conflict.requester) ?? [];
39
+ list.push(conflict);
40
+ conflictsByName.set(conflict.requester, list);
41
+ const peerList = conflictsByName.get(conflict.peer) ?? [];
42
+ peerList.push(conflict);
43
+ conflictsByName.set(conflict.peer, peerList);
44
+ }
45
+ for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
46
+ const list = unusedByName.get(issue.name) ?? [];
47
+ list.push(issue);
48
+ unusedByName.set(issue.name, list);
49
+ }
50
+ const items = impactedUpdates
51
+ .map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName))
52
+ .filter((item) => matchesReviewFilters(item, options));
53
+ const summary = createReviewSummary(checkResult.summary, items, [
54
+ ...checkResult.errors,
55
+ ...auditResult.errors,
56
+ ...resolveResult.errors,
57
+ ...healthResult.errors,
58
+ ...licenseResult.errors,
59
+ ...unusedResult.errors,
60
+ ], [
61
+ ...checkResult.warnings,
62
+ ...auditResult.warnings,
63
+ ...resolveResult.warnings,
64
+ ...healthResult.warnings,
65
+ ...licenseResult.warnings,
66
+ ...unusedResult.warnings,
67
+ ], options.interactive === true);
68
+ return {
69
+ projectPath: checkResult.projectPath,
70
+ target: checkResult.target,
71
+ summary,
72
+ items,
73
+ updates: items.map((item) => item.update),
74
+ errors: [...checkResult.errors, ...auditResult.errors, ...resolveResult.errors, ...healthResult.errors, ...licenseResult.errors, ...unusedResult.errors],
75
+ warnings: [...checkResult.warnings, ...auditResult.warnings, ...resolveResult.warnings, ...healthResult.warnings, ...licenseResult.warnings, ...unusedResult.warnings],
76
+ };
77
+ }
78
+ export function createDoctorResult(review) {
79
+ const verdict = review.summary.verdict ?? deriveVerdict(review.items);
80
+ const primaryFindings = buildPrimaryFindings(review);
81
+ return {
82
+ verdict,
83
+ summary: review.summary,
84
+ review,
85
+ primaryFindings,
86
+ recommendedCommand: recommendCommand(review, verdict),
87
+ };
88
+ }
89
+ export function renderReviewResult(review) {
90
+ const lines = [];
91
+ lines.push(`Project: ${review.projectPath}`);
92
+ lines.push(`Target: ${review.target}`);
93
+ lines.push(`Verdict: ${review.summary.verdict ?? "safe"}`);
94
+ lines.push("");
95
+ if (review.items.length === 0) {
96
+ lines.push("No reviewable updates found.");
97
+ }
98
+ else {
99
+ lines.push("Updates:");
100
+ for (const item of review.items) {
101
+ const notes = [
102
+ item.update.diffType,
103
+ item.update.riskLevel ? `risk=${item.update.riskLevel}` : undefined,
104
+ item.update.advisoryCount ? `security=${item.update.advisoryCount}` : undefined,
105
+ item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none"
106
+ ? `peer=${item.update.peerConflictSeverity}`
107
+ : undefined,
108
+ item.update.licenseStatus && item.update.licenseStatus !== "allowed"
109
+ ? `license=${item.update.licenseStatus}`
110
+ : undefined,
111
+ ].filter(Boolean);
112
+ lines.push(`- ${path.basename(item.update.packagePath)} :: ${item.update.name} ${item.update.fromRange} -> ${item.update.toRange} (${notes.join(", ")})`);
113
+ if (item.update.riskReasons && item.update.riskReasons.length > 0) {
114
+ lines.push(` reasons: ${item.update.riskReasons.join("; ")}`);
115
+ }
116
+ if (item.update.homepage) {
117
+ lines.push(` homepage: ${item.update.homepage}`);
118
+ }
119
+ }
120
+ }
121
+ if (review.errors.length > 0) {
122
+ lines.push("");
123
+ lines.push("Errors:");
124
+ for (const error of review.errors) {
125
+ lines.push(`- ${error}`);
126
+ }
127
+ }
128
+ if (review.warnings.length > 0) {
129
+ lines.push("");
130
+ lines.push("Warnings:");
131
+ for (const warning of review.warnings) {
132
+ lines.push(`- ${warning}`);
133
+ }
134
+ }
135
+ lines.push("");
136
+ lines.push(`Summary: ${review.summary.updatesFound} updates, riskPackages=${review.summary.riskPackages ?? 0}, securityPackages=${review.summary.securityPackages ?? 0}, peerConflictPackages=${review.summary.peerConflictPackages ?? 0}`);
137
+ return lines.join("\n");
138
+ }
139
+ export function renderDoctorResult(result, verdictOnly = false) {
140
+ const lines = [
141
+ `State: ${result.verdict}`,
142
+ `PrimaryRisk: ${result.primaryFindings[0] ?? "No blocking findings."}`,
143
+ `NextAction: ${result.recommendedCommand}`,
144
+ ];
145
+ if (!verdictOnly) {
146
+ 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}`);
147
+ }
148
+ return lines.join("\n");
149
+ }
150
+ function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
151
+ const advisories = advisoriesByName.get(update.name) ?? [];
152
+ const peerConflicts = conflictsByName.get(update.name) ?? [];
153
+ const health = healthByName.get(update.name);
154
+ const license = licenseByName.get(update.name);
155
+ const unusedIssues = unusedByName.get(update.name) ?? [];
156
+ const riskReasons = [
157
+ advisories.length > 0 ? `${advisories.length} advisory finding(s)` : undefined,
158
+ peerConflicts.some((item) => item.severity === "error") ? "peer conflict requires review" : undefined,
159
+ health?.flags.includes("deprecated") ? "package is deprecated" : undefined,
160
+ health?.flags.includes("stale") ? "package is stale" : undefined,
161
+ licenseViolationNames.has(update.name) ? "license policy violation" : undefined,
162
+ unusedIssues.length > 0 ? `${unusedIssues.length} unused/missing dependency signal(s)` : undefined,
163
+ update.diffType === "major" ? "major version jump" : undefined,
164
+ ].filter((value) => Boolean(value));
165
+ const riskLevel = deriveRiskLevel(update, advisories.length, peerConflicts.length, licenseViolationNames.has(update.name), health?.flags ?? []);
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
+ riskLevel,
182
+ riskReasons,
183
+ },
184
+ advisories,
185
+ health,
186
+ peerConflicts,
187
+ license,
188
+ unusedIssues,
189
+ selected: true,
190
+ };
191
+ }
192
+ function matchesReviewFilters(item, options) {
193
+ if ("securityOnly" in options && options.securityOnly && item.advisories.length === 0) {
194
+ return false;
195
+ }
196
+ if ("risk" in options && options.risk && !riskMatches(item.update.riskLevel, options.risk)) {
197
+ return false;
198
+ }
199
+ if ("diff" in options && options.diff && item.update.diffType !== options.diff) {
200
+ return false;
201
+ }
202
+ return true;
203
+ }
204
+ function riskMatches(current, threshold) {
205
+ const order = {
206
+ critical: 4,
207
+ high: 3,
208
+ medium: 2,
209
+ low: 1,
210
+ };
211
+ return order[current ?? "low"] >= order[threshold];
212
+ }
213
+ function createReviewSummary(base, items, errors, warnings, interactiveSession) {
214
+ const summary = finalizeSummary(createSummary({
215
+ scannedPackages: base.scannedPackages,
216
+ totalDependencies: base.totalDependencies,
217
+ checkedDependencies: base.checkedDependencies,
218
+ updatesFound: items.length,
219
+ upgraded: base.upgraded,
220
+ skipped: base.skipped,
221
+ warmedPackages: base.warmedPackages,
222
+ errors,
223
+ warnings,
224
+ durations: {
225
+ totalMs: base.durationMs.total,
226
+ discoveryMs: base.durationMs.discovery,
227
+ registryMs: base.durationMs.registry,
228
+ cacheMs: base.durationMs.cache,
229
+ },
230
+ groupedUpdates: base.groupedUpdates,
231
+ cooldownSkipped: base.cooldownSkipped,
232
+ ciProfile: base.ciProfile,
233
+ prLimitHit: base.prLimitHit,
234
+ policyOverridesApplied: base.policyOverridesApplied,
235
+ }));
236
+ summary.durationMs.render = base.durationMs.render;
237
+ summary.streamedEvents = base.streamedEvents;
238
+ summary.fixPrApplied = base.fixPrApplied;
239
+ summary.fixBranchName = base.fixBranchName;
240
+ summary.fixCommitSha = base.fixCommitSha;
241
+ summary.fixPrBranchesCreated = base.fixPrBranchesCreated;
242
+ summary.interactiveSession = interactiveSession;
243
+ summary.securityPackages = items.filter((item) => item.advisories.length > 0).length;
244
+ summary.riskPackages = items.filter((item) => item.update.riskLevel === "critical" || item.update.riskLevel === "high").length;
245
+ summary.peerConflictPackages = items.filter((item) => item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none").length;
246
+ summary.licenseViolationPackages = items.filter((item) => item.update.licenseStatus === "denied").length;
247
+ summary.verdict = deriveVerdict(items);
248
+ return summary;
249
+ }
250
+ function deriveVerdict(items) {
251
+ if (items.some((item) => item.update.peerConflictSeverity === "error" ||
252
+ item.update.licenseStatus === "denied")) {
253
+ return "blocked";
254
+ }
255
+ if (items.some((item) => item.advisories.length > 0 || item.update.riskLevel === "critical")) {
256
+ return "actionable";
257
+ }
258
+ if (items.some((item) => item.update.riskLevel === "high" || item.update.diffType === "major")) {
259
+ return "review";
260
+ }
261
+ return "safe";
262
+ }
263
+ function deriveRiskLevel(update, advisories, conflicts, hasLicenseViolation, healthFlags) {
264
+ if (hasLicenseViolation || conflicts > 0 || advisories > 0) {
265
+ return update.diffType === "major" || advisories > 0 ? "critical" : "high";
266
+ }
267
+ if (healthFlags.includes("deprecated") || update.diffType === "major") {
268
+ return "high";
269
+ }
270
+ if (healthFlags.length > 0 || update.diffType === "minor") {
271
+ return "medium";
272
+ }
273
+ return update.impactScore?.rank ?? "low";
274
+ }
275
+ function buildPrimaryFindings(review) {
276
+ const findings = [];
277
+ if ((review.summary.peerConflictPackages ?? 0) > 0) {
278
+ findings.push(`${review.summary.peerConflictPackages} package(s) have peer conflicts.`);
279
+ }
280
+ if ((review.summary.licenseViolationPackages ?? 0) > 0) {
281
+ findings.push(`${review.summary.licenseViolationPackages} package(s) violate license policy.`);
282
+ }
283
+ if ((review.summary.securityPackages ?? 0) > 0) {
284
+ findings.push(`${review.summary.securityPackages} package(s) have security advisories.`);
285
+ }
286
+ if ((review.summary.riskPackages ?? 0) > 0) {
287
+ findings.push(`${review.summary.riskPackages} package(s) are high risk.`);
288
+ }
289
+ if (findings.length === 0) {
290
+ findings.push("No blocking findings; remaining updates are low-risk.");
291
+ }
292
+ return findings;
293
+ }
294
+ function recommendCommand(review, verdict) {
295
+ if (verdict === "blocked")
296
+ return "rup resolve --after-update";
297
+ if ((review.summary.securityPackages ?? 0) > 0)
298
+ return "rup audit --fix";
299
+ if (review.items.length > 0)
300
+ return "rup review --interactive";
301
+ return "rup check";
302
+ }
303
+ async function runSilenced(fn) {
304
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
305
+ const stderrWrite = process.stderr.write.bind(process.stderr);
306
+ process.stdout.write = (() => true);
307
+ process.stderr.write = (() => true);
308
+ try {
309
+ return await fn();
310
+ }
311
+ finally {
312
+ process.stdout.write = stdoutWrite;
313
+ process.stderr.write = stderrWrite;
314
+ }
315
+ }
316
+ function toAuditOptions(options) {
317
+ return {
318
+ cwd: options.cwd,
319
+ workspace: options.workspace,
320
+ severity: undefined,
321
+ fix: false,
322
+ dryRun: true,
323
+ commit: false,
324
+ packageManager: "auto",
325
+ reportFormat: "json",
326
+ sourceMode: "auto",
327
+ jsonFile: undefined,
328
+ concurrency: options.concurrency,
329
+ registryTimeoutMs: options.registryTimeoutMs,
330
+ };
331
+ }
332
+ function toResolveOptions(options) {
333
+ return {
334
+ cwd: options.cwd,
335
+ workspace: options.workspace,
336
+ afterUpdate: true,
337
+ safe: false,
338
+ jsonFile: undefined,
339
+ concurrency: options.concurrency,
340
+ registryTimeoutMs: options.registryTimeoutMs,
341
+ cacheTtlSeconds: options.cacheTtlSeconds,
342
+ };
343
+ }
344
+ function toHealthOptions(options) {
345
+ return {
346
+ cwd: options.cwd,
347
+ workspace: options.workspace,
348
+ staleDays: 365,
349
+ includeDeprecated: true,
350
+ includeAlternatives: false,
351
+ reportFormat: "json",
352
+ jsonFile: undefined,
353
+ concurrency: options.concurrency,
354
+ registryTimeoutMs: options.registryTimeoutMs,
355
+ };
356
+ }
357
+ function toLicenseOptions(options) {
358
+ return {
359
+ cwd: options.cwd,
360
+ workspace: options.workspace,
361
+ allow: undefined,
362
+ deny: undefined,
363
+ sbomFile: undefined,
364
+ jsonFile: undefined,
365
+ diffMode: false,
366
+ concurrency: options.concurrency,
367
+ registryTimeoutMs: options.registryTimeoutMs,
368
+ cacheTtlSeconds: options.cacheTtlSeconds,
369
+ };
370
+ }
371
+ function toUnusedOptions(options) {
372
+ return {
373
+ cwd: options.cwd,
374
+ workspace: options.workspace,
375
+ srcDirs: ["src", "."],
376
+ includeDevDependencies: true,
377
+ fix: false,
378
+ dryRun: true,
379
+ jsonFile: undefined,
380
+ concurrency: options.concurrency,
381
+ };
382
+ }
@@ -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.includes("Unable to resolve") ||
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
  }
@@ -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>;
@@ -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 checkResult.updates) {
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
- if (graph.cycles.length > 0) {
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 installDependencies(options.cwd, options.packageManager, checkResult.packageManager);
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();
@@ -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("SQLite cache backend unavailable in Bun runtime. Falling back to file cache backend.");
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(`Offline cache miss for ${pkg}. Cannot warm cache in --offline mode.`);
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
- errors.push(`Unable to warm ${pkg}: ${error}`);
100
- emitStream(`[error] Unable to warm ${pkg}: ${error}`);
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,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 type { CheckOptions, CheckResult, CiProfile, DependencyKind, FailOnLevel, GroupBy, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
10
+ export { buildReviewResult, createDoctorResult } from "./core/review-model.js";
11
+ export type { CheckOptions, CheckResult, CiProfile, DependencyKind, FailOnLevel, GroupBy, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, ReviewOptions, ReviewResult, DoctorOptions, DoctorResult, Verdict, RiskLevel, } from "./types/index.js";
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ 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";
@@ -1,2 +1,5 @@
1
1
  import type { CheckResult, OutputFormat } from "../types/index.js";
2
- export declare function renderResult(result: CheckResult, format: OutputFormat): string;
2
+ export declare function renderResult(result: CheckResult, format: OutputFormat, display?: {
3
+ showImpact?: boolean;
4
+ showHomepage?: boolean;
5
+ }): string;
@@ -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
  }
@@ -11,7 +11,16 @@ export function renderResult(result, format) {
11
11
  if (result.updates.length === 0)
12
12
  return "No updates found.";
13
13
  return result.updates
14
- .map((item) => `${item.packagePath} :: ${item.name}: ${item.fromRange} -> ${item.toRange}`)
14
+ .map((item) => {
15
+ const parts = [`${item.packagePath} :: ${item.name}: ${item.fromRange} -> ${item.toRange}`];
16
+ if (display.showImpact && item.impactScore) {
17
+ parts.push(`impact=${item.impactScore.rank}:${item.impactScore.score}`);
18
+ }
19
+ if (display.showHomepage && item.homepage) {
20
+ parts.push(item.homepage);
21
+ }
22
+ return parts.join(" | ");
23
+ })
15
24
  .join("\n");
16
25
  }
17
26
  if (format === "github") {
@@ -40,6 +49,11 @@ export function renderResult(result, format) {
40
49
  `policy_overrides_applied=${result.summary.policyOverridesApplied}`,
41
50
  `registry_auth_failures=${result.summary.errorCounts.registryAuthFailure}`,
42
51
  `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
52
+ `verdict=${result.summary.verdict ?? ""}`,
53
+ `risk_packages=${result.summary.riskPackages ?? 0}`,
54
+ `security_packages=${result.summary.securityPackages ?? 0}`,
55
+ `peer_conflict_packages=${result.summary.peerConflictPackages ?? 0}`,
56
+ `license_violation_packages=${result.summary.licenseViolationPackages ?? 0}`,
43
57
  ].join("\n");
44
58
  }
45
59
  const lines = [];
@@ -59,7 +73,16 @@ export function renderResult(result, format) {
59
73
  else {
60
74
  lines.push("Updates:");
61
75
  for (const update of result.updates) {
62
- lines.push(`- ${update.packagePath} :: ${update.name} [${update.kind}] ${update.fromRange} -> ${update.toRange} (${update.diffType})`);
76
+ lines.push(`- ${update.packagePath} :: ${update.name} [${update.kind}] ${update.fromRange} -> ${update.toRange} (${[
77
+ update.diffType,
78
+ display.showImpact && update.impactScore
79
+ ? `impact=${update.impactScore.rank}:${update.impactScore.score}`
80
+ : undefined,
81
+ display.showHomepage && update.homepage ? update.homepage : undefined,
82
+ update.riskLevel ? `risk=${update.riskLevel}` : undefined,
83
+ ]
84
+ .filter(Boolean)
85
+ .join(", ")})`);
63
86
  }
64
87
  }
65
88
  if (result.errors.length > 0) {
@@ -80,6 +103,9 @@ export function renderResult(result, format) {
80
103
  lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
81
104
  lines.push(`Groups=${result.summary.groupedUpdates}, cooldownSkipped=${result.summary.cooldownSkipped}, ciProfile=${result.summary.ciProfile}, prLimitHit=${result.summary.prLimitHit ? "yes" : "no"}`);
82
105
  lines.push(`StreamedEvents=${result.summary.streamedEvents}, policyOverrides=${result.summary.policyOverridesApplied}, registryAuthFailures=${result.summary.errorCounts.registryAuthFailure}`);
106
+ if (result.summary.verdict) {
107
+ 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}`);
108
+ }
83
109
  lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
84
110
  if (result.summary.fixPrApplied) {
85
111
  lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
@@ -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 ?? ""}`,