@rainy-updates/cli 0.5.6 → 0.6.0

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 (102) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +24 -482
  4. package/dist/bin/dispatch.d.ts +16 -0
  5. package/dist/bin/dispatch.js +147 -0
  6. package/dist/bin/help.d.ts +1 -0
  7. package/dist/bin/help.js +314 -0
  8. package/dist/cache/cache.js +13 -11
  9. package/dist/commands/audit/parser.js +2 -2
  10. package/dist/commands/audit/runner.js +27 -46
  11. package/dist/commands/audit/targets.js +13 -13
  12. package/dist/commands/bisect/oracle.js +28 -11
  13. package/dist/commands/bisect/parser.js +3 -3
  14. package/dist/commands/bisect/runner.js +15 -8
  15. package/dist/commands/changelog/fetcher.js +11 -5
  16. package/dist/commands/dashboard/parser.js +103 -1
  17. package/dist/commands/dashboard/runner.d.ts +2 -2
  18. package/dist/commands/dashboard/runner.js +67 -37
  19. package/dist/commands/doctor/parser.js +15 -4
  20. package/dist/commands/doctor/runner.js +6 -3
  21. package/dist/commands/ga/parser.js +4 -4
  22. package/dist/commands/ga/runner.js +13 -7
  23. package/dist/commands/health/parser.js +2 -2
  24. package/dist/commands/licenses/runner.js +4 -4
  25. package/dist/commands/resolve/runner.js +9 -4
  26. package/dist/commands/review/parser.js +57 -4
  27. package/dist/commands/review/runner.js +31 -5
  28. package/dist/commands/snapshot/runner.js +17 -17
  29. package/dist/commands/snapshot/store.d.ts +0 -12
  30. package/dist/commands/snapshot/store.js +26 -38
  31. package/dist/commands/unused/runner.js +6 -7
  32. package/dist/commands/unused/scanner.js +17 -20
  33. package/dist/config/loader.d.ts +2 -2
  34. package/dist/config/loader.js +2 -5
  35. package/dist/config/policy.js +20 -11
  36. package/dist/core/analysis/options.d.ts +6 -0
  37. package/dist/core/analysis/options.js +69 -0
  38. package/dist/core/analysis/review-items.d.ts +4 -0
  39. package/dist/core/analysis/review-items.js +128 -0
  40. package/dist/core/analysis/run-silenced.d.ts +1 -0
  41. package/dist/core/analysis/run-silenced.js +13 -0
  42. package/dist/core/analysis-bundle.js +3 -211
  43. package/dist/core/artifacts.js +6 -5
  44. package/dist/core/baseline.js +3 -5
  45. package/dist/core/check.js +2 -2
  46. package/dist/core/ci.js +52 -1
  47. package/dist/core/decision-plan.d.ts +14 -0
  48. package/dist/core/decision-plan.js +107 -0
  49. package/dist/core/doctor/findings.d.ts +2 -0
  50. package/dist/core/doctor/findings.js +166 -0
  51. package/dist/core/doctor/render.d.ts +3 -0
  52. package/dist/core/doctor/render.js +44 -0
  53. package/dist/core/doctor/result.d.ts +2 -0
  54. package/dist/core/doctor/result.js +58 -0
  55. package/dist/core/doctor/score.d.ts +5 -0
  56. package/dist/core/doctor/score.js +28 -0
  57. package/dist/core/fix-pr-batch.js +38 -28
  58. package/dist/core/fix-pr.js +27 -24
  59. package/dist/core/init-ci.js +25 -21
  60. package/dist/core/options.js +95 -4
  61. package/dist/core/review-model.d.ts +3 -3
  62. package/dist/core/review-model.js +6 -67
  63. package/dist/core/review-verdict.d.ts +2 -0
  64. package/dist/core/review-verdict.js +14 -0
  65. package/dist/core/summary.js +12 -0
  66. package/dist/core/upgrade.js +64 -2
  67. package/dist/core/verification.d.ts +2 -0
  68. package/dist/core/verification.js +106 -0
  69. package/dist/core/warm-cache.js +2 -2
  70. package/dist/output/format.js +22 -0
  71. package/dist/output/github.js +10 -0
  72. package/dist/output/sarif.js +16 -12
  73. package/dist/parsers/package-json.js +2 -4
  74. package/dist/pm/detect.d.ts +3 -1
  75. package/dist/pm/detect.js +24 -12
  76. package/dist/pm/install.d.ts +2 -1
  77. package/dist/pm/install.js +15 -16
  78. package/dist/registry/npm.js +34 -76
  79. package/dist/rup +0 -0
  80. package/dist/types/index.d.ts +104 -5
  81. package/dist/ui/tui.d.ts +4 -1
  82. package/dist/ui/tui.js +5 -4
  83. package/dist/utils/io.js +5 -6
  84. package/dist/utils/lockfile.js +24 -19
  85. package/dist/utils/runtime-paths.d.ts +4 -0
  86. package/dist/utils/runtime-paths.js +35 -0
  87. package/dist/utils/runtime.d.ts +7 -0
  88. package/dist/utils/runtime.js +32 -0
  89. package/dist/workspace/discover.js +55 -51
  90. package/package.json +16 -16
  91. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  92. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  93. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  94. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  95. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  96. package/dist/ui/dashboard/components/Footer.js +0 -9
  97. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  98. package/dist/ui/dashboard/components/Header.js +0 -12
  99. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  100. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  101. package/dist/ui/dashboard/store.d.ts +0 -34
  102. package/dist/ui/dashboard/store.js +0 -148
@@ -1,19 +1,24 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
2
  export async function loadPolicy(cwd, policyFile) {
4
- const candidates = policyFile ? [policyFile] : [
5
- path.join(cwd, ".rainyupdates-policy.json"),
6
- path.join(cwd, "rainy-updates.policy.json"),
7
- ];
3
+ const candidates = policyFile
4
+ ? [policyFile]
5
+ : [
6
+ path.join(cwd, ".rainyupdates-policy.json"),
7
+ path.join(cwd, "rainy-updates.policy.json"),
8
+ ];
8
9
  for (const candidate of candidates) {
9
- const filePath = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
10
+ const filePath = path.isAbsolute(candidate)
11
+ ? candidate
12
+ : path.resolve(cwd, candidate);
10
13
  try {
11
- const content = await fs.readFile(filePath, "utf8");
12
- const parsed = JSON.parse(content);
14
+ const parsed = (await Bun.file(filePath).json());
13
15
  return {
14
16
  ignorePatterns: parsed.ignore ?? [],
15
17
  cooldownDays: asNonNegativeInt(parsed.cooldownDays),
16
- packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [pkg, normalizeRule(rule)])),
18
+ packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [
19
+ pkg,
20
+ normalizeRule(rule),
21
+ ])),
17
22
  matchRules: Object.values(parsed.packageRules ?? {})
18
23
  .map((rule) => normalizeRule(rule))
19
24
  .filter((rule) => typeof rule.match === "string" && rule.match.length > 0),
@@ -44,7 +49,9 @@ function normalizeRule(rule) {
44
49
  maxUpdatesPerRun: asNonNegativeInt(rule.maxUpdatesPerRun),
45
50
  cooldownDays: asNonNegativeInt(rule.cooldownDays),
46
51
  allowPrerelease: rule.allowPrerelease === true,
47
- group: typeof rule.group === "string" && rule.group.trim().length > 0 ? rule.group.trim() : undefined,
52
+ group: typeof rule.group === "string" && rule.group.trim().length > 0
53
+ ? rule.group.trim()
54
+ : undefined,
48
55
  priority: asNonNegativeInt(rule.priority),
49
56
  target: rule.target,
50
57
  autofix: rule.autofix !== false,
@@ -60,7 +67,9 @@ function matchesPattern(value, pattern) {
60
67
  return false;
61
68
  if (pattern === "*")
62
69
  return true;
63
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
70
+ const escaped = pattern
71
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
72
+ .replace(/\*/g, ".*");
64
73
  const regex = new RegExp(`^${escaped}$`);
65
74
  return regex.test(value);
66
75
  }
@@ -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,13 @@
1
+ export async function runSilenced(fn) {
2
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
3
+ const stderrWrite = process.stderr.write.bind(process.stderr);
4
+ process.stdout.write = (() => true);
5
+ process.stderr.write = (() => true);
6
+ try {
7
+ return await fn();
8
+ }
9
+ finally {
10
+ process.stdout.write = stdoutWrite;
11
+ process.stderr.write = stderrWrite;
12
+ }
13
+ }
@@ -1,7 +1,7 @@
1
- import { fetchChangelog } from "../commands/changelog/fetcher.js";
2
- import { applyImpactScores } from "./impact.js";
3
- import { applyRiskAssessments } from "../risk/index.js";
4
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
5
  export async function buildAnalysisBundle(options, config = {}) {
6
6
  const baseCheckOptions = {
7
7
  ...options,
@@ -31,211 +31,3 @@ export async function buildAnalysisBundle(options, config = {}) {
31
31
  .map((source) => source.source),
32
32
  };
33
33
  }
34
- async function buildReviewItems(updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config) {
35
- const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
36
- const impactedUpdates = applyImpactScores(updates, {
37
- advisoryPackages,
38
- workspaceDependentCount: (name) => updates.filter((item) => item.name === name).length,
39
- });
40
- const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
41
- const advisoriesByName = new Map();
42
- const conflictsByName = new Map();
43
- const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
44
- const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
45
- const unusedByName = new Map();
46
- for (const advisory of auditResult.advisories) {
47
- const list = advisoriesByName.get(advisory.packageName) ?? [];
48
- list.push(advisory);
49
- advisoriesByName.set(advisory.packageName, list);
50
- }
51
- for (const conflict of resolveResult.conflicts) {
52
- const list = conflictsByName.get(conflict.requester) ?? [];
53
- list.push(conflict);
54
- conflictsByName.set(conflict.requester, list);
55
- const peerList = conflictsByName.get(conflict.peer) ?? [];
56
- peerList.push(conflict);
57
- conflictsByName.set(conflict.peer, peerList);
58
- }
59
- for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
60
- const list = unusedByName.get(issue.name) ?? [];
61
- list.push(issue);
62
- unusedByName.set(issue.name, list);
63
- }
64
- const enrichedUpdates = await maybeAttachReleaseNotes(impactedUpdates, Boolean(config.includeChangelog));
65
- return applyRiskAssessments(enrichedUpdates.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName)), {
66
- knownPackageNames: new Set(updates.map((item) => item.name)),
67
- }).map((item) => ({
68
- ...item,
69
- update: {
70
- ...item.update,
71
- policyAction: derivePolicyAction(item),
72
- decisionState: deriveDecisionState(item),
73
- selectedByDefault: deriveDecisionState(item) !== "blocked",
74
- blockedReason: deriveDecisionState(item) === "blocked"
75
- ? item.update.recommendedAction
76
- : undefined,
77
- monitorReason: item.update.healthStatus === "stale" ? "Package health should be monitored." : undefined,
78
- },
79
- }));
80
- }
81
- function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
82
- const advisories = advisoriesByName.get(update.name) ?? [];
83
- const peerConflicts = conflictsByName.get(update.name) ?? [];
84
- const health = healthByName.get(update.name);
85
- const license = licenseByName.get(update.name);
86
- const unusedIssues = unusedByName.get(update.name) ?? [];
87
- return {
88
- update: {
89
- ...update,
90
- advisoryCount: advisories.length,
91
- peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
92
- ? "error"
93
- : peerConflicts.length > 0
94
- ? "warning"
95
- : "none",
96
- licenseStatus: licenseViolationNames.has(update.name)
97
- ? "denied"
98
- : license
99
- ? "allowed"
100
- : "review",
101
- healthStatus: health?.flags[0] ?? "healthy",
102
- },
103
- advisories,
104
- health,
105
- peerConflicts,
106
- license,
107
- unusedIssues,
108
- selected: true,
109
- };
110
- }
111
- function derivePolicyAction(item) {
112
- if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
113
- return "block";
114
- }
115
- if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
116
- return "review";
117
- }
118
- if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
119
- return "monitor";
120
- }
121
- return "allow";
122
- }
123
- function deriveDecisionState(item) {
124
- if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
125
- return "blocked";
126
- }
127
- if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
128
- return "actionable";
129
- }
130
- if (item.update.riskLevel === "high" || item.update.diffType === "major") {
131
- return "review";
132
- }
133
- return "safe";
134
- }
135
- async function maybeAttachReleaseNotes(updates, includeChangelog) {
136
- if (!includeChangelog || updates.length === 0) {
137
- return updates;
138
- }
139
- const enriched = await Promise.all(updates.map(async (update) => ({
140
- ...update,
141
- releaseNotesSummary: summarizeChangelog(await fetchChangelog(update.name, update.repository)),
142
- })));
143
- return enriched;
144
- }
145
- function summarizeChangelog(content) {
146
- if (!content)
147
- return undefined;
148
- const lines = content
149
- .split(/\r?\n/)
150
- .map((line) => line.trim())
151
- .filter(Boolean);
152
- const title = lines.find((line) => line.startsWith("#"))?.replace(/^#+\s*/, "") ?? "Release notes";
153
- const excerpt = lines.find((line) => !line.startsWith("#")) ?? "No summary available.";
154
- return {
155
- source: content.includes("# Release") ? "github-release" : "changelog-file",
156
- title,
157
- excerpt: excerpt.slice(0, 240),
158
- };
159
- }
160
- async function runSilenced(fn) {
161
- const stdoutWrite = process.stdout.write.bind(process.stdout);
162
- const stderrWrite = process.stderr.write.bind(process.stderr);
163
- process.stdout.write = (() => true);
164
- process.stderr.write = (() => true);
165
- try {
166
- return await fn();
167
- }
168
- finally {
169
- process.stdout.write = stdoutWrite;
170
- process.stderr.write = stderrWrite;
171
- }
172
- }
173
- function toAuditOptions(options) {
174
- return {
175
- cwd: options.cwd,
176
- workspace: options.workspace,
177
- severity: undefined,
178
- fix: false,
179
- dryRun: true,
180
- commit: false,
181
- packageManager: "auto",
182
- reportFormat: "json",
183
- sourceMode: "auto",
184
- jsonFile: undefined,
185
- concurrency: options.concurrency,
186
- registryTimeoutMs: options.registryTimeoutMs,
187
- silent: true,
188
- };
189
- }
190
- function toResolveOptions(options) {
191
- return {
192
- cwd: options.cwd,
193
- workspace: options.workspace,
194
- afterUpdate: true,
195
- safe: false,
196
- jsonFile: undefined,
197
- concurrency: options.concurrency,
198
- registryTimeoutMs: options.registryTimeoutMs,
199
- cacheTtlSeconds: options.cacheTtlSeconds,
200
- silent: true,
201
- };
202
- }
203
- function toHealthOptions(options) {
204
- return {
205
- cwd: options.cwd,
206
- workspace: options.workspace,
207
- staleDays: 365,
208
- includeDeprecated: true,
209
- includeAlternatives: false,
210
- reportFormat: "json",
211
- jsonFile: undefined,
212
- concurrency: options.concurrency,
213
- registryTimeoutMs: options.registryTimeoutMs,
214
- };
215
- }
216
- function toLicenseOptions(options) {
217
- return {
218
- cwd: options.cwd,
219
- workspace: options.workspace,
220
- allow: undefined,
221
- deny: undefined,
222
- sbomFile: undefined,
223
- jsonFile: undefined,
224
- diffMode: false,
225
- concurrency: options.concurrency,
226
- registryTimeoutMs: options.registryTimeoutMs,
227
- cacheTtlSeconds: options.cacheTtlSeconds,
228
- };
229
- }
230
- function toUnusedOptions(options) {
231
- return {
232
- cwd: options.cwd,
233
- workspace: options.workspace,
234
- srcDirs: ["src", "."],
235
- includeDevDependencies: true,
236
- fix: false,
237
- dryRun: true,
238
- jsonFile: undefined,
239
- concurrency: options.concurrency,
240
- };
241
- }
@@ -1,10 +1,9 @@
1
- import crypto from "node:crypto";
2
1
  import path from "node:path";
3
2
  import { stableStringify } from "../utils/stable-json.js";
4
3
  import { writeFileAtomic } from "../utils/io.js";
5
4
  export function createRunId(command, options, result) {
6
- const hash = crypto.createHash("sha256");
7
- hash.update(stableStringify({
5
+ const hasher = new Bun.CryptoHasher("sha256");
6
+ hasher.update(stableStringify({
8
7
  command,
9
8
  cwd: path.resolve(options.cwd),
10
9
  target: options.target,
@@ -17,14 +16,15 @@ export function createRunId(command, options, result) {
17
16
  toRange: update.toRange,
18
17
  })),
19
18
  }, 0));
20
- return hash.digest("hex").slice(0, 16);
19
+ return hasher.digest("hex").slice(0, 16);
21
20
  }
22
21
  export async function writeArtifactManifest(command, options, result) {
23
22
  const shouldWrite = options.ci ||
24
23
  Boolean(options.jsonFile) ||
25
24
  Boolean(options.githubOutputFile) ||
26
25
  Boolean(options.sarifFile) ||
27
- Boolean(options.prReportFile);
26
+ Boolean(options.prReportFile) ||
27
+ Boolean(options.verificationReportFile);
28
28
  if (!shouldWrite)
29
29
  return null;
30
30
  const runId = result.summary.runId ?? createRunId(command, options, result);
@@ -41,6 +41,7 @@ export async function writeArtifactManifest(command, options, result) {
41
41
  githubOutputFile: options.githubOutputFile,
42
42
  sarifFile: options.sarifFile,
43
43
  prReportFile: options.prReportFile,
44
+ verificationReportFile: options.verificationReportFile,
44
45
  },
45
46
  };
46
47
  await writeFileAtomic(artifactManifestPath, stableStringify(manifest, 2) + "\n");
@@ -1,6 +1,6 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
2
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
3
+ import { writeFileAtomic } from "../utils/io.js";
4
4
  import { discoverPackageDirs } from "../workspace/discover.js";
5
5
  export async function saveBaseline(options) {
6
6
  const entries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
@@ -9,16 +9,14 @@ export async function saveBaseline(options) {
9
9
  createdAt: new Date().toISOString(),
10
10
  entries,
11
11
  };
12
- await fs.mkdir(path.dirname(options.filePath), { recursive: true });
13
- await fs.writeFile(options.filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
12
+ await writeFileAtomic(options.filePath, JSON.stringify(payload, null, 2) + "\n");
14
13
  return {
15
14
  filePath: options.filePath,
16
15
  entries: entries.length,
17
16
  };
18
17
  }
19
18
  export async function diffBaseline(options) {
20
- const content = await fs.readFile(options.filePath, "utf8");
21
- const baseline = JSON.parse(content);
19
+ const baseline = (await Bun.file(options.filePath).json());
22
20
  const currentEntries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
23
21
  const baselineMap = new Map(baseline.entries.map((entry) => [toKey(entry), entry]));
24
22
  const currentMap = new Map(currentEntries.map((entry) => [toKey(entry), entry]));
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import process from "node:process";
3
2
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
4
3
  import { matchesPattern } from "../utils/pattern.js";
5
4
  import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersionFromAvailable } from "../utils/semver.js";
@@ -11,6 +10,7 @@ import { loadPolicy, resolvePolicyRule } from "../config/policy.js";
11
10
  import { createSummary, finalizeSummary } from "./summary.js";
12
11
  import { applyImpactScores } from "./impact.js";
13
12
  import { formatClassifiedMessage } from "./errors.js";
13
+ import { writeStdout } from "../utils/runtime.js";
14
14
  export async function check(options) {
15
15
  const startedAt = Date.now();
16
16
  let discoveryMs = 0;
@@ -48,7 +48,7 @@ export async function check(options) {
48
48
  if (!options.stream)
49
49
  return;
50
50
  streamedEvents += 1;
51
- process.stdout.write(`${message}\n`);
51
+ writeStdout(`${message}\n`);
52
52
  };
53
53
  for (const packageDir of packageDirs) {
54
54
  let manifest;
package/dist/core/ci.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { check } from "./check.js";
2
2
  import { warmCache } from "./warm-cache.js";
3
+ import { buildReviewResult, createDoctorResult } from "./review-model.js";
4
+ import { createDecisionPlan, defaultDecisionPlanPath, writeDecisionPlan, } from "./decision-plan.js";
5
+ import { upgrade } from "./upgrade.js";
3
6
  export async function runCi(options) {
4
7
  const profile = options.ciProfile;
5
8
  if (profile !== "minimal") {
@@ -16,5 +19,53 @@ export async function runCi(options) {
16
19
  offline: profile === "minimal" ? options.offline : true,
17
20
  concurrency: profile === "enterprise" ? Math.max(options.concurrency, 32) : options.concurrency,
18
21
  };
19
- return await check(checkOptions);
22
+ if (options.ciGate === "check") {
23
+ return await check(checkOptions);
24
+ }
25
+ if (options.ciGate === "doctor") {
26
+ const review = await buildReviewResult(checkOptions);
27
+ createDoctorResult(review);
28
+ return reviewToCheckResult(review);
29
+ }
30
+ if (options.ciGate === "review") {
31
+ const review = await buildReviewResult(checkOptions);
32
+ const selectedItems = review.items.filter((item) => item.update.selectedByDefault !== false &&
33
+ item.update.decisionState !== "blocked");
34
+ const decisionPlanFile = options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd);
35
+ const plan = createDecisionPlan({
36
+ review,
37
+ selectedItems,
38
+ sourceCommand: "ci",
39
+ mode: "review",
40
+ focus: "all",
41
+ });
42
+ await writeDecisionPlan(decisionPlanFile, plan);
43
+ review.decisionPlan = plan;
44
+ review.summary.decisionPlan = decisionPlanFile;
45
+ review.summary.interactiveSurface = "dashboard";
46
+ review.summary.queueFocus = "all";
47
+ review.summary.suggestedCommand = `rup upgrade --from-plan ${decisionPlanFile}`;
48
+ return reviewToCheckResult(review);
49
+ }
50
+ const decisionPlanFile = options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd);
51
+ return upgrade({
52
+ ...checkOptions,
53
+ install: false,
54
+ packageManager: "auto",
55
+ sync: checkOptions.workspace,
56
+ fromPlanFile: decisionPlanFile,
57
+ });
58
+ }
59
+ function reviewToCheckResult(review) {
60
+ return {
61
+ projectPath: review.projectPath,
62
+ packagePaths: review.analysis.check.packagePaths,
63
+ packageManager: review.analysis.check.packageManager,
64
+ target: review.target,
65
+ timestamp: review.analysis.check.timestamp,
66
+ summary: review.summary,
67
+ updates: review.updates,
68
+ errors: review.errors,
69
+ warnings: review.warnings,
70
+ };
20
71
  }
@@ -0,0 +1,14 @@
1
+ import type { DashboardMode, DecisionPlan, PackageUpdate, QueueFocus, ReviewItem, ReviewResult, UpgradeOptions } from "../types/index.js";
2
+ export declare function defaultDecisionPlanPath(cwd: string): string;
3
+ export declare function filterReviewItemsByFocus(items: ReviewItem[], focus: QueueFocus): ReviewItem[];
4
+ export declare function createDecisionPlan(input: {
5
+ review: ReviewResult;
6
+ selectedItems: ReviewItem[];
7
+ sourceCommand: string;
8
+ mode: DashboardMode;
9
+ focus: QueueFocus;
10
+ }): DecisionPlan;
11
+ export declare function writeDecisionPlan(filePath: string, plan: DecisionPlan): Promise<void>;
12
+ export declare function readDecisionPlan(filePath: string): Promise<DecisionPlan>;
13
+ export declare function selectedUpdatesFromPlan(plan: DecisionPlan): PackageUpdate[];
14
+ export declare function resolveDecisionPlanPath(options: Pick<UpgradeOptions, "cwd" | "decisionPlanFile">, explicit?: string): string;