@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,174 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { ensureRiskLevel } from "../../core/options.js";
4
+ export function parseReviewArgs(args) {
5
+ const options = {
6
+ cwd: process.cwd(),
7
+ target: "latest",
8
+ filter: undefined,
9
+ reject: undefined,
10
+ cacheTtlSeconds: 3600,
11
+ includeKinds: ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"],
12
+ ci: false,
13
+ format: "table",
14
+ workspace: false,
15
+ jsonFile: undefined,
16
+ githubOutputFile: undefined,
17
+ sarifFile: undefined,
18
+ concurrency: 16,
19
+ registryTimeoutMs: 8000,
20
+ registryRetries: 3,
21
+ offline: false,
22
+ stream: false,
23
+ policyFile: undefined,
24
+ prReportFile: undefined,
25
+ failOn: "none",
26
+ maxUpdates: undefined,
27
+ fixPr: false,
28
+ fixBranch: "chore/rainy-updates",
29
+ fixCommitMessage: undefined,
30
+ fixDryRun: false,
31
+ fixPrNoCheckout: false,
32
+ fixPrBatchSize: undefined,
33
+ noPrReport: false,
34
+ logLevel: "info",
35
+ groupBy: "risk",
36
+ groupMax: undefined,
37
+ cooldownDays: undefined,
38
+ prLimit: undefined,
39
+ onlyChanged: false,
40
+ ciProfile: "minimal",
41
+ lockfileMode: "preserve",
42
+ interactive: false,
43
+ showImpact: true,
44
+ showHomepage: true,
45
+ securityOnly: false,
46
+ risk: undefined,
47
+ diff: undefined,
48
+ applySelected: false,
49
+ };
50
+ for (let i = 0; i < args.length; i += 1) {
51
+ const current = args[i];
52
+ const next = args[i + 1];
53
+ if (current === "--cwd" && next) {
54
+ options.cwd = path.resolve(next);
55
+ i += 1;
56
+ continue;
57
+ }
58
+ if (current === "--cwd")
59
+ throw new Error("Missing value for --cwd");
60
+ if (current === "--workspace") {
61
+ options.workspace = true;
62
+ continue;
63
+ }
64
+ if (current === "--interactive") {
65
+ options.interactive = true;
66
+ continue;
67
+ }
68
+ if (current === "--security-only") {
69
+ options.securityOnly = true;
70
+ continue;
71
+ }
72
+ if (current === "--risk" && next) {
73
+ options.risk = ensureRiskLevel(next);
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (current === "--risk")
78
+ throw new Error("Missing value for --risk");
79
+ if (current === "--diff" && next) {
80
+ if (next === "patch" ||
81
+ next === "minor" ||
82
+ next === "major" ||
83
+ next === "latest") {
84
+ options.diff = next;
85
+ i += 1;
86
+ continue;
87
+ }
88
+ throw new Error("--diff must be patch, minor, major or latest");
89
+ }
90
+ if (current === "--diff")
91
+ throw new Error("Missing value for --diff");
92
+ if (current === "--apply-selected") {
93
+ options.applySelected = true;
94
+ continue;
95
+ }
96
+ if (current === "--json-file" && next) {
97
+ options.jsonFile = path.resolve(options.cwd, next);
98
+ i += 1;
99
+ continue;
100
+ }
101
+ if (current === "--json-file")
102
+ throw new Error("Missing value for --json-file");
103
+ if (current === "--policy-file" && next) {
104
+ options.policyFile = path.resolve(options.cwd, next);
105
+ i += 1;
106
+ continue;
107
+ }
108
+ if (current === "--policy-file")
109
+ throw new Error("Missing value for --policy-file");
110
+ if (current === "--concurrency" && next) {
111
+ const parsed = Number(next);
112
+ if (!Number.isInteger(parsed) || parsed <= 0) {
113
+ throw new Error("--concurrency must be a positive integer");
114
+ }
115
+ options.concurrency = parsed;
116
+ i += 1;
117
+ continue;
118
+ }
119
+ if (current === "--concurrency")
120
+ throw new Error("Missing value for --concurrency");
121
+ if (current === "--registry-timeout-ms" && next) {
122
+ const parsed = Number(next);
123
+ if (!Number.isInteger(parsed) || parsed <= 0) {
124
+ throw new Error("--registry-timeout-ms must be a positive integer");
125
+ }
126
+ options.registryTimeoutMs = parsed;
127
+ i += 1;
128
+ continue;
129
+ }
130
+ if (current === "--registry-timeout-ms") {
131
+ throw new Error("Missing value for --registry-timeout-ms");
132
+ }
133
+ if (current === "--registry-retries" && next) {
134
+ const parsed = Number(next);
135
+ if (!Number.isInteger(parsed) || parsed <= 0) {
136
+ throw new Error("--registry-retries must be a positive integer");
137
+ }
138
+ options.registryRetries = parsed;
139
+ i += 1;
140
+ continue;
141
+ }
142
+ if (current === "--registry-retries") {
143
+ throw new Error("Missing value for --registry-retries");
144
+ }
145
+ if (current === "--help" || current === "-h") {
146
+ process.stdout.write(REVIEW_HELP);
147
+ process.exit(0);
148
+ }
149
+ if (current.startsWith("-"))
150
+ throw new Error(`Unknown review option: ${current}`);
151
+ throw new Error(`Unexpected review argument: ${current}`);
152
+ }
153
+ return options;
154
+ }
155
+ const REVIEW_HELP = `
156
+ rup review — Guided dependency review across updates, security, peer conflicts, and policy
157
+
158
+ Usage:
159
+ rup review [options]
160
+
161
+ Options:
162
+ --interactive Launch the interactive review TUI
163
+ --security-only Show only packages with advisories
164
+ --risk <level> Minimum risk: critical, high, medium, low
165
+ --diff <level> Filter by patch, minor, major, latest
166
+ --apply-selected Apply all filtered updates after review
167
+ --workspace Scan all workspace packages
168
+ --policy-file <path> Load policy overrides
169
+ --json-file <path> Write JSON review report to file
170
+ --registry-timeout-ms <n>
171
+ --registry-retries <n>
172
+ --concurrency <n>
173
+ --cwd <path>
174
+ `.trimStart();
@@ -0,0 +1,2 @@
1
+ import type { ReviewOptions, ReviewResult } from "../../types/index.js";
2
+ export declare function runReview(options: ReviewOptions): Promise<ReviewResult>;
@@ -0,0 +1,30 @@
1
+ import process from "node:process";
2
+ import { runTui } from "../../ui/tui.js";
3
+ import { buildReviewResult, renderReviewResult } from "../../core/review-model.js";
4
+ import { applySelectedUpdates } from "../../core/upgrade.js";
5
+ import { stableStringify } from "../../utils/stable-json.js";
6
+ import { writeFileAtomic } from "../../utils/io.js";
7
+ export async function runReview(options) {
8
+ const review = await buildReviewResult(options);
9
+ let selectedUpdates = review.updates;
10
+ if (options.interactive && review.updates.length > 0) {
11
+ selectedUpdates = await runTui(review.updates);
12
+ }
13
+ if (options.applySelected && selectedUpdates.length > 0) {
14
+ await applySelectedUpdates({
15
+ ...options,
16
+ install: false,
17
+ packageManager: "auto",
18
+ sync: false,
19
+ }, selectedUpdates);
20
+ }
21
+ process.stdout.write(renderReviewResult({
22
+ ...review,
23
+ updates: selectedUpdates,
24
+ items: review.items.filter((item) => selectedUpdates.some((selected) => selected.name === item.update.name && selected.packagePath === item.update.packagePath)),
25
+ }) + "\n");
26
+ if (options.jsonFile) {
27
+ await writeFileAtomic(options.jsonFile, stableStringify(review, 2) + "\n");
28
+ }
29
+ return review;
30
+ }
@@ -35,6 +35,9 @@ export interface FileConfig {
35
35
  onlyChanged?: boolean;
36
36
  ciProfile?: CiProfile;
37
37
  lockfileMode?: LockfileMode;
38
+ interactive?: boolean;
39
+ showImpact?: boolean;
40
+ showHomepage?: boolean;
38
41
  install?: boolean;
39
42
  packageManager?: "auto" | "npm" | "pnpm";
40
43
  sync?: boolean;
@@ -9,6 +9,8 @@ import { detectPackageManager } from "../pm/detect.js";
9
9
  import { discoverPackageDirs } from "../workspace/discover.js";
10
10
  import { loadPolicy, resolvePolicyRule } from "../config/policy.js";
11
11
  import { createSummary, finalizeSummary } from "./summary.js";
12
+ import { applyImpactScores } from "./impact.js";
13
+ import { formatClassifiedMessage } from "./errors.js";
12
14
  export async function check(options) {
13
15
  const startedAt = Date.now();
14
16
  let discoveryMs = 0;
@@ -28,7 +30,13 @@ export async function check(options) {
28
30
  const errors = [];
29
31
  const warnings = [];
30
32
  if (cache.degraded) {
31
- warnings.push("SQLite cache backend unavailable in Bun runtime. Falling back to file cache backend.");
33
+ warnings.push(formatClassifiedMessage({
34
+ code: "CACHE_BACKEND_FALLBACK",
35
+ whatFailed: cache.fallbackReason ?? "Preferred SQLite cache backend is unavailable.",
36
+ intact: "Dependency analysis continues with the file cache backend.",
37
+ validity: "partial",
38
+ next: "Run `rup warm-cache` after restoring SQLite support if you want the preferred backend again.",
39
+ }));
32
40
  }
33
41
  let totalDependencies = 0;
34
42
  const tasks = [];
@@ -85,6 +93,7 @@ export async function check(options) {
85
93
  latestVersion: cached.latestVersion,
86
94
  availableVersions: cached.availableVersions,
87
95
  publishedAtByVersion: {},
96
+ hasInstallScript: false,
88
97
  });
89
98
  }
90
99
  else {
@@ -102,11 +111,18 @@ export async function check(options) {
102
111
  latestVersion: stale.latestVersion,
103
112
  availableVersions: stale.availableVersions,
104
113
  publishedAtByVersion: {},
114
+ hasInstallScript: false,
105
115
  });
106
116
  warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
107
117
  }
108
118
  else {
109
- errors.push(`Offline cache miss for ${packageName}. Run once without --offline to warm cache.`);
119
+ errors.push(formatClassifiedMessage({
120
+ code: "REGISTRY_ERROR",
121
+ whatFailed: `Offline cache miss for ${packageName}.`,
122
+ intact: "Local manifests and previously cached packages remain unchanged.",
123
+ validity: "invalid",
124
+ next: `Run \`rup warm-cache --cwd ${options.cwd}\` or retry without --offline.`,
125
+ }));
110
126
  }
111
127
  }
112
128
  cacheMs += Date.now() - cacheFallbackStartedAt;
@@ -125,6 +141,9 @@ export async function check(options) {
125
141
  latestVersion: metadata.latestVersion,
126
142
  availableVersions: metadata.versions,
127
143
  publishedAtByVersion: metadata.publishedAtByVersion,
144
+ homepage: metadata.homepage,
145
+ repository: metadata.repository,
146
+ hasInstallScript: metadata.hasInstallScript,
128
147
  });
129
148
  if (metadata.latestVersion) {
130
149
  await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
@@ -139,12 +158,24 @@ export async function check(options) {
139
158
  latestVersion: stale.latestVersion,
140
159
  availableVersions: stale.availableVersions,
141
160
  publishedAtByVersion: {},
161
+ hasInstallScript: false,
142
162
  });
143
163
  warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
144
164
  }
145
165
  else {
146
- errors.push(`Unable to resolve ${packageName}: ${error}`);
147
- emitStream(`[error] Unable to resolve ${packageName}: ${error}`);
166
+ const classified = formatClassifiedMessage({
167
+ code: error.includes("401") || error.includes("403")
168
+ ? "AUTH_ERROR"
169
+ : "REGISTRY_ERROR",
170
+ whatFailed: `Unable to resolve ${packageName}: ${error}.`,
171
+ intact: "Other package results and local files remain intact.",
172
+ validity: "partial",
173
+ next: error.includes("401") || error.includes("403")
174
+ ? "Check .npmrc scoped registry credentials and retry."
175
+ : "Retry `rup check` or warm the cache before offline runs.",
176
+ });
177
+ errors.push(classified);
178
+ emitStream(`[error] ${classified}`);
148
179
  }
149
180
  }
150
181
  cacheMs += Date.now() - cacheStaleStartedAt;
@@ -182,10 +213,12 @@ export async function check(options) {
182
213
  filtered: false,
183
214
  autofix: rule?.autofix !== false,
184
215
  reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
216
+ homepage: metadata.homepage,
185
217
  });
186
218
  emitStream(`[update] ${task.dependency.name} ${task.dependency.range} -> ${nextRange} (${classifyDiff(task.dependency.range, picked)})`);
187
219
  }
188
- const grouped = groupUpdates(updates, options.groupBy);
220
+ const scoredUpdates = applyImpactScores(updates);
221
+ const grouped = groupUpdates(scoredUpdates, options.groupBy);
189
222
  const groupedUpdates = grouped.length;
190
223
  const groupedSorted = sortUpdates(grouped.flatMap((group) => group.items));
191
224
  const groupedCapped = typeof options.groupMax === "number" ? groupedSorted.slice(0, options.groupMax) : groupedSorted;
@@ -219,6 +252,7 @@ export async function check(options) {
219
252
  policyOverridesApplied,
220
253
  }));
221
254
  summary.streamedEvents = streamedEvents;
255
+ summary.riskPackages = limitedUpdates.filter((item) => item.impactScore?.rank === "critical" || item.impactScore?.rank === "high").length;
222
256
  return {
223
257
  projectPath: options.cwd,
224
258
  packagePaths: packageDirs,
@@ -0,0 +1,11 @@
1
+ export type ErrorCode = "REGISTRY_ERROR" | "AUTH_ERROR" | "ADVISORY_SOURCE_DEGRADED" | "ADVISORY_SOURCE_DOWN" | "CACHE_BACKEND_FALLBACK";
2
+ export type ErrorValidity = "partial" | "invalid" | "intact";
3
+ export interface ClassifiedMessageInput {
4
+ code: ErrorCode;
5
+ whatFailed: string;
6
+ intact: string;
7
+ validity: ErrorValidity;
8
+ next: string;
9
+ }
10
+ export declare function formatClassifiedMessage(input: ClassifiedMessageInput): string;
11
+ export declare function hasErrorCode(value: string, code: ErrorCode): boolean;
@@ -0,0 +1,6 @@
1
+ export function formatClassifiedMessage(input) {
2
+ return `[${input.code}] ${input.whatFailed} Intact: ${input.intact} Result: ${input.validity}. Next: ${input.next}`;
3
+ }
4
+ export function hasErrorCode(value, code) {
5
+ return value.startsWith(`[${code}]`);
6
+ }
@@ -1,4 +1,4 @@
1
- import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -46,5 +46,12 @@ export type ParsedCliArgs = {
46
46
  } | {
47
47
  command: "snapshot";
48
48
  options: SnapshotOptions;
49
+ } | {
50
+ command: "review";
51
+ options: ReviewOptions;
52
+ } | {
53
+ command: "doctor";
54
+ options: DoctorOptions;
49
55
  };
50
56
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
57
+ export declare function ensureRiskLevel(value: string): RiskLevel;
@@ -21,6 +21,8 @@ const KNOWN_COMMANDS = [
21
21
  "resolve",
22
22
  "licenses",
23
23
  "snapshot",
24
+ "review",
25
+ "doctor",
24
26
  ];
25
27
  export async function parseCliArgs(argv) {
26
28
  const firstArg = argv[0];
@@ -62,6 +64,14 @@ export async function parseCliArgs(argv) {
62
64
  const { parseSnapshotArgs } = await import("../commands/snapshot/parser.js");
63
65
  return { command, options: parseSnapshotArgs(args) };
64
66
  }
67
+ if (command === "review") {
68
+ const { parseReviewArgs } = await import("../commands/review/parser.js");
69
+ return { command, options: parseReviewArgs(args) };
70
+ }
71
+ if (command === "doctor") {
72
+ const { parseDoctorArgs } = await import("../commands/doctor/parser.js");
73
+ return { command, options: parseDoctorArgs(args) };
74
+ }
65
75
  const base = {
66
76
  cwd: process.cwd(),
67
77
  target: "latest",
@@ -99,6 +109,9 @@ export async function parseCliArgs(argv) {
99
109
  onlyChanged: false,
100
110
  ciProfile: "minimal",
101
111
  lockfileMode: "preserve",
112
+ interactive: false,
113
+ showImpact: false,
114
+ showHomepage: false,
102
115
  };
103
116
  let force = false;
104
117
  let initCiMode = "enterprise";
@@ -429,6 +442,18 @@ export async function parseCliArgs(argv) {
429
442
  base.onlyChanged = true;
430
443
  continue;
431
444
  }
445
+ if (current === "--interactive") {
446
+ base.interactive = true;
447
+ continue;
448
+ }
449
+ if (current === "--show-impact") {
450
+ base.showImpact = true;
451
+ continue;
452
+ }
453
+ if (current === "--show-homepage") {
454
+ base.showHomepage = true;
455
+ continue;
456
+ }
432
457
  if (current === "--lockfile-mode" && next) {
433
458
  base.lockfileMode = ensureLockfileMode(next);
434
459
  index += 1;
@@ -639,6 +664,15 @@ function applyConfig(base, config) {
639
664
  if (typeof config.lockfileMode === "string") {
640
665
  base.lockfileMode = ensureLockfileMode(config.lockfileMode);
641
666
  }
667
+ if (typeof config.interactive === "boolean") {
668
+ base.interactive = config.interactive;
669
+ }
670
+ if (typeof config.showImpact === "boolean") {
671
+ base.showImpact = config.showImpact;
672
+ }
673
+ if (typeof config.showHomepage === "boolean") {
674
+ base.showHomepage = config.showHomepage;
675
+ }
642
676
  }
643
677
  function parsePackageManager(args) {
644
678
  const index = args.indexOf("--pm");
@@ -743,3 +777,12 @@ function ensureLockfileMode(value) {
743
777
  }
744
778
  throw new Error("--lockfile-mode must be preserve, update or error");
745
779
  }
780
+ export function ensureRiskLevel(value) {
781
+ if (value === "critical" ||
782
+ value === "high" ||
783
+ value === "medium" ||
784
+ value === "low") {
785
+ return value;
786
+ }
787
+ throw new Error("--risk must be critical, high, medium or low");
788
+ }
@@ -0,0 +1,5 @@
1
+ import type { CheckOptions, DoctorOptions, DoctorResult, ReviewOptions, ReviewResult } from "../types/index.js";
2
+ export declare function buildReviewResult(options: ReviewOptions | DoctorOptions | CheckOptions): Promise<ReviewResult>;
3
+ export declare function createDoctorResult(review: ReviewResult): DoctorResult;
4
+ export declare function renderReviewResult(review: ReviewResult): string;
5
+ export declare function renderDoctorResult(result: DoctorResult, verdictOnly?: boolean): string;