@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +5 -0
  3. package/dist/bin/cli.js +16 -438
  4. package/dist/bin/dispatch.d.ts +16 -0
  5. package/dist/bin/dispatch.js +150 -0
  6. package/dist/bin/help.d.ts +1 -0
  7. package/dist/bin/help.js +284 -0
  8. package/dist/commands/audit/runner.js +43 -26
  9. package/dist/commands/dashboard/parser.d.ts +2 -0
  10. package/dist/commands/dashboard/parser.js +59 -0
  11. package/dist/commands/dashboard/runner.d.ts +2 -0
  12. package/dist/commands/dashboard/runner.js +47 -0
  13. package/dist/commands/doctor/parser.js +12 -0
  14. package/dist/commands/doctor/runner.js +5 -2
  15. package/dist/commands/ga/parser.d.ts +2 -0
  16. package/dist/commands/ga/parser.js +50 -0
  17. package/dist/commands/ga/runner.d.ts +2 -0
  18. package/dist/commands/ga/runner.js +129 -0
  19. package/dist/commands/resolve/runner.js +7 -3
  20. package/dist/commands/review/parser.js +6 -0
  21. package/dist/commands/review/runner.js +4 -3
  22. package/dist/core/analysis/options.d.ts +6 -0
  23. package/dist/core/analysis/options.js +69 -0
  24. package/dist/core/analysis/review-items.d.ts +4 -0
  25. package/dist/core/analysis/review-items.js +128 -0
  26. package/dist/core/analysis/run-silenced.d.ts +1 -0
  27. package/dist/core/analysis/run-silenced.js +14 -0
  28. package/dist/core/analysis-bundle.d.ts +4 -0
  29. package/dist/core/analysis-bundle.js +33 -0
  30. package/dist/core/artifacts.d.ts +3 -0
  31. package/dist/core/artifacts.js +48 -0
  32. package/dist/core/check.js +6 -1
  33. package/dist/core/doctor/findings.d.ts +2 -0
  34. package/dist/core/doctor/findings.js +166 -0
  35. package/dist/core/doctor/render.d.ts +3 -0
  36. package/dist/core/doctor/render.js +44 -0
  37. package/dist/core/doctor/result.d.ts +2 -0
  38. package/dist/core/doctor/result.js +55 -0
  39. package/dist/core/doctor/score.d.ts +5 -0
  40. package/dist/core/doctor/score.js +28 -0
  41. package/dist/core/options.d.ts +7 -1
  42. package/dist/core/options.js +14 -0
  43. package/dist/core/review-model.d.ts +3 -3
  44. package/dist/core/review-model.js +55 -245
  45. package/dist/core/review-verdict.d.ts +2 -0
  46. package/dist/core/review-verdict.js +14 -0
  47. package/dist/core/summary.js +19 -0
  48. package/dist/output/format.js +22 -0
  49. package/dist/output/github.js +12 -0
  50. package/dist/output/sarif.js +16 -0
  51. package/dist/types/index.d.ts +120 -0
  52. package/dist/ui/dashboard/DashboardTUI.d.ts +6 -0
  53. package/dist/ui/dashboard/DashboardTUI.js +34 -0
  54. package/dist/ui/dashboard/components/DetailPanel.d.ts +4 -0
  55. package/dist/ui/dashboard/components/DetailPanel.js +30 -0
  56. package/dist/ui/dashboard/components/Footer.d.ts +4 -0
  57. package/dist/ui/dashboard/components/Footer.js +9 -0
  58. package/dist/ui/dashboard/components/Header.d.ts +4 -0
  59. package/dist/ui/dashboard/components/Header.js +12 -0
  60. package/dist/ui/dashboard/components/Sidebar.d.ts +4 -0
  61. package/dist/ui/dashboard/components/Sidebar.js +23 -0
  62. package/dist/ui/dashboard/store.d.ts +34 -0
  63. package/dist/ui/dashboard/store.js +148 -0
  64. package/dist/ui/tui.d.ts +2 -2
  65. package/dist/ui/tui.js +310 -79
  66. 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
- process.stdout.write(renderDoctorResult(doctor, options.verdictOnly) + "\n");
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,2 @@
1
+ import type { GaOptions } from "../../types/index.js";
2
+ export declare function parseGaArgs(args: string[]): GaOptions;
@@ -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,2 @@
1
+ import type { GaOptions, GaResult } from "../../types/index.js";
2
+ export declare function runGa(options: GaOptions): Promise<GaResult>;
@@ -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
- process.stdout.write(renderConflictsTable(result, options) + "\n");
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
- process.stderr.write(`[resolve] JSON report written to ${options.jsonFile}\n`);
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 selectedUpdates = review.updates;
9
+ let selectedItems = review.items;
10
10
  if (options.interactive && review.updates.length > 0) {
11
- selectedUpdates = await runTui(review.updates);
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,4 @@
1
+ import type { AnalysisBundle, CheckOptions, DoctorOptions, ReviewOptions } from "../types/index.js";
2
+ export declare function buildAnalysisBundle(options: ReviewOptions | DoctorOptions | CheckOptions, config?: {
3
+ includeChangelog?: boolean;
4
+ }): Promise<AnalysisBundle>;
@@ -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
+ }
@@ -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]) => ({ key, items: sortUpdates(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) {