@rainy-updates/cli 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +88 -31
- package/dist/bin/cli.js +20 -11
- package/dist/commands/doctor/parser.js +1 -1
- package/dist/core/check.js +29 -4
- package/dist/core/review-model.js +29 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/format.js +12 -0
- package/dist/output/github.js +1 -1
- package/dist/output/sarif.js +3 -0
- package/dist/registry/npm.d.ts +4 -2
- package/dist/registry/npm.js +17 -11
- package/dist/risk/index.d.ts +3 -0
- package/dist/risk/index.js +24 -0
- package/dist/risk/scorer.d.ts +3 -0
- package/dist/risk/scorer.js +114 -0
- package/dist/risk/signals/fresh-package.d.ts +3 -0
- package/dist/risk/signals/fresh-package.js +22 -0
- package/dist/risk/signals/install-scripts.d.ts +3 -0
- package/dist/risk/signals/install-scripts.js +10 -0
- package/dist/risk/signals/maintainer-churn.d.ts +3 -0
- package/dist/risk/signals/maintainer-churn.js +11 -0
- package/dist/risk/signals/metadata.d.ts +3 -0
- package/dist/risk/signals/metadata.js +18 -0
- package/dist/risk/signals/mutable-source.d.ts +3 -0
- package/dist/risk/signals/mutable-source.js +24 -0
- package/dist/risk/signals/typosquat.d.ts +3 -0
- package/dist/risk/signals/typosquat.js +70 -0
- package/dist/risk/types.d.ts +15 -0
- package/dist/risk/types.js +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/ui/tui.js +28 -3
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,79 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.4] - 2026-03-01
|
|
6
|
+
|
|
7
|
+
Production hotfix for interactive review stability.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed a production bug where `review --interactive` could leak raw output from aggregated runners such as `resolve` before the Ink TUI rendered.
|
|
12
|
+
- Fixed the review aggregation silencing model so concurrent dependency analysis no longer races on `stdout` / `stderr` restoration.
|
|
13
|
+
- Restored the intended interactive entry experience for review mode:
|
|
14
|
+
- `Rainy Review Queue`
|
|
15
|
+
- `Review Queue`
|
|
16
|
+
- `Decision Panel`
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Clarified `doctor --verdict-only` help text so it matches the real 3-line quick-verdict contract.
|
|
21
|
+
- Added dedicated TUI usage documentation in `docs/tui-guide.md` and linked it from the review workflow docs and README.
|
|
22
|
+
|
|
23
|
+
## [0.5.3] - 2026-03-01
|
|
24
|
+
|
|
25
|
+
GA stabilization and review-centered workflow refinement.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Dedicated risk engine layer** under `src/risk/`:
|
|
30
|
+
- formal risk scoring with `riskScore`, `riskLevel`, `riskReasons`, `riskCategories`, and `recommendedAction`,
|
|
31
|
+
- deterministic scoring for:
|
|
32
|
+
- known vulnerabilities,
|
|
33
|
+
- install lifecycle scripts,
|
|
34
|
+
- typosquatting heuristic,
|
|
35
|
+
- newly published packages,
|
|
36
|
+
- suspicious metadata,
|
|
37
|
+
- mutable git/http dependencies,
|
|
38
|
+
- maintainer stability heuristic,
|
|
39
|
+
- peer conflicts,
|
|
40
|
+
- license violations,
|
|
41
|
+
- stale/deprecated health signals,
|
|
42
|
+
- major version jumps.
|
|
43
|
+
- **Benchmark tooling and methodology**:
|
|
44
|
+
- `scripts/generate-benchmark-fixtures.mjs`,
|
|
45
|
+
- `scripts/benchmark.mjs`,
|
|
46
|
+
- generated fixtures under `benchmarks/fixtures/`,
|
|
47
|
+
- benchmark methodology doc: `docs/benchmarks.md`.
|
|
48
|
+
- **New workflow docs**:
|
|
49
|
+
- `docs/command-model.md`
|
|
50
|
+
- `docs/review-workflow.md`
|
|
51
|
+
- `docs/risk-engine.md`
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- `review` is now the explicit product center:
|
|
56
|
+
- `check` detects,
|
|
57
|
+
- `doctor` summarizes,
|
|
58
|
+
- `review` decides,
|
|
59
|
+
- `upgrade` applies.
|
|
60
|
+
- CLI help, README, and docs now reflect the review-centered workflow instead of a flat command surface.
|
|
61
|
+
- Review outputs now carry formal risk engine results instead of ad hoc composite signals.
|
|
62
|
+
- TUI language has been upgraded to decision-oriented semantics:
|
|
63
|
+
- review queue,
|
|
64
|
+
- decision panel,
|
|
65
|
+
- explicit state labels,
|
|
66
|
+
- recommended action per candidate.
|
|
67
|
+
- GitHub annotations, SARIF, and human-readable output now expose formal risk score/action metadata additively.
|
|
68
|
+
|
|
69
|
+
### Benchmarks
|
|
70
|
+
|
|
71
|
+
- Added package scripts for reproducible benchmark runs:
|
|
72
|
+
- `bench:fixtures`
|
|
73
|
+
- `bench:check`
|
|
74
|
+
- `bench:review`
|
|
75
|
+
- `bench:resolve`
|
|
76
|
+
- `bench:ci`
|
|
77
|
+
|
|
5
78
|
## [0.5.2] - 2026-03-01
|
|
6
79
|
|
|
7
80
|
### Added
|
package/README.md
CHANGED
|
@@ -1,19 +1,68 @@
|
|
|
1
1
|
# @rainy-updates/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rainy Updates is a deterministic dependency review and upgrade operator for Node monorepos and CI.
|
|
4
4
|
|
|
5
|
-
`@rainy-updates/cli` is built for teams that need fast dependency
|
|
5
|
+
`@rainy-updates/cli` is built for teams that need fast dependency detection, trustworthy review, controlled upgrades, and automation-ready outputs for CI/CD.
|
|
6
6
|
|
|
7
7
|
Comparison:
|
|
8
8
|
[Why Rainy vs Dependabot and Renovate](./docs/why-rainy-vs-dependabot-renovate.md)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Command model:
|
|
11
|
+
[Check → Doctor → Review → Upgrade](./docs/command-model.md)
|
|
12
|
+
|
|
13
|
+
Review workflow:
|
|
14
|
+
[Review workflow guide](./docs/review-workflow.md)
|
|
15
|
+
|
|
16
|
+
TUI usage:
|
|
17
|
+
[TUI guide](./docs/tui-guide.md)
|
|
18
|
+
|
|
19
|
+
Risk engine:
|
|
20
|
+
[Risk engine guide](./docs/risk-engine.md)
|
|
21
|
+
|
|
22
|
+
Benchmarks:
|
|
23
|
+
[Benchmark methodology](./docs/benchmarks.md)
|
|
24
|
+
|
|
25
|
+
## What it is
|
|
26
|
+
|
|
27
|
+
Rainy Updates gives teams one dependency lifecycle:
|
|
28
|
+
|
|
29
|
+
- `check` detects candidate updates.
|
|
30
|
+
- `doctor` summarizes the current situation.
|
|
31
|
+
- `review` decides what should happen.
|
|
32
|
+
- `upgrade` applies the approved change set.
|
|
33
|
+
|
|
34
|
+
Everything else supports that lifecycle: CI orchestration, advisory lookup, peer resolution, licenses, snapshots, baselines, and fix-PR automation.
|
|
35
|
+
|
|
36
|
+
## Who it is for
|
|
37
|
+
|
|
38
|
+
- Node monorepo teams that want deterministic CI artifacts.
|
|
39
|
+
- Engineers who want to review dependency risk locally before applying changes.
|
|
40
|
+
- Teams that need fewer, better upgrade decisions instead of noisy automated PR churn.
|
|
41
|
+
|
|
42
|
+
## 60-second workflow
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1) Detect what changed
|
|
46
|
+
npx @rainy-updates/cli check --workspace --show-impact
|
|
47
|
+
|
|
48
|
+
# 2) Summarize what matters
|
|
49
|
+
npx @rainy-updates/cli doctor --workspace
|
|
50
|
+
|
|
51
|
+
# 3) Decide in the review surface
|
|
52
|
+
npx @rainy-updates/cli review --interactive
|
|
53
|
+
|
|
54
|
+
# 4) Apply the approved set
|
|
55
|
+
npx @rainy-updates/cli upgrade --interactive
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why teams use it
|
|
11
59
|
|
|
12
60
|
- Detects updates quickly across single-package repos and workspaces.
|
|
61
|
+
- Centralizes security, peer, license, health, and behavioral risk review.
|
|
13
62
|
- Applies updates safely with configurable targets (`patch`, `minor`, `major`, `latest`).
|
|
14
|
-
- Enforces policy rules per package
|
|
63
|
+
- Enforces policy rules per package.
|
|
15
64
|
- Supports offline and cache-warmed execution for deterministic CI runs.
|
|
16
|
-
- Produces machine-readable artifacts
|
|
65
|
+
- Produces machine-readable artifacts: JSON, SARIF, GitHub outputs, and PR reports.
|
|
17
66
|
|
|
18
67
|
## Install
|
|
19
68
|
|
|
@@ -52,15 +101,18 @@ npx @rainy-updates/cli ci --workspace --mode strict
|
|
|
52
101
|
|
|
53
102
|
## Commands
|
|
54
103
|
|
|
55
|
-
###
|
|
104
|
+
### Primary workflow
|
|
105
|
+
|
|
106
|
+
- `check` — detect candidate dependency updates
|
|
107
|
+
- `doctor` — summarize the current dependency situation
|
|
108
|
+
- `review` — decide what to do with security, risk, peer, and policy context
|
|
109
|
+
- `upgrade` — apply the approved change set
|
|
110
|
+
|
|
111
|
+
### Supporting workflow
|
|
56
112
|
|
|
57
|
-
- `check` — analyze dependencies and report available updates
|
|
58
|
-
- `upgrade` — rewrite dependency ranges in manifests, optionally install lockfile updates
|
|
59
113
|
- `ci` — run CI-focused dependency automation (warm cache, check/upgrade, policy gates)
|
|
60
114
|
- `warm-cache` — prefetch package metadata for fast and offline checks
|
|
61
115
|
- `baseline` — save and compare dependency baseline snapshots
|
|
62
|
-
- `review` — guided review across updates, security, peer conflicts, licenses, and risk
|
|
63
|
-
- `doctor` — fast verdict command for local triage and CI summaries
|
|
64
116
|
|
|
65
117
|
### Security & health (_new in v0.5.1_)
|
|
66
118
|
|
|
@@ -77,31 +129,35 @@ npx @rainy-updates/cli ci --workspace --mode strict
|
|
|
77
129
|
npx @rainy-updates/cli check --format table
|
|
78
130
|
rup check --format table # if installed
|
|
79
131
|
|
|
80
|
-
# 2)
|
|
81
|
-
npx @rainy-updates/cli
|
|
82
|
-
rup
|
|
132
|
+
# 2) Summarize the state
|
|
133
|
+
npx @rainy-updates/cli doctor --workspace
|
|
134
|
+
rup doctor --workspace
|
|
135
|
+
|
|
136
|
+
# 3) Review and decide
|
|
137
|
+
npx @rainy-updates/cli review --security-only
|
|
138
|
+
rup review --interactive
|
|
139
|
+
|
|
140
|
+
# 4) Apply upgrades with workspace sync
|
|
141
|
+
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
142
|
+
rup upgrade --target latest --workspace --sync --install
|
|
83
143
|
|
|
84
|
-
#
|
|
144
|
+
# 5) CI orchestration with policy gates
|
|
85
145
|
npx @rainy-updates/cli ci --workspace --mode strict --format github
|
|
86
146
|
rup ci --workspace --mode strict --format github
|
|
87
147
|
|
|
88
|
-
#
|
|
148
|
+
# 6) Batch fix branches by scope (enterprise)
|
|
89
149
|
npx @rainy-updates/cli ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
|
|
90
150
|
rup ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
|
|
91
151
|
|
|
92
|
-
#
|
|
93
|
-
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
94
|
-
rup upgrade --target latest --workspace --sync --install
|
|
95
|
-
|
|
96
|
-
# 6) Warm cache → deterministic offline CI check
|
|
152
|
+
# 7) Warm cache → deterministic offline CI check
|
|
97
153
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
98
154
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
99
155
|
|
|
100
|
-
#
|
|
156
|
+
# 8) Save and compare baseline drift
|
|
101
157
|
npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
|
|
102
158
|
npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
|
|
103
159
|
|
|
104
|
-
#
|
|
160
|
+
# 9) Scan for known CVEs
|
|
105
161
|
npx @rainy-updates/cli audit
|
|
106
162
|
npx @rainy-updates/cli audit --severity high
|
|
107
163
|
npx @rainy-updates/cli audit --summary
|
|
@@ -111,26 +167,20 @@ rup audit --severity high # if installed
|
|
|
111
167
|
|
|
112
168
|
`audit` prefers npm/pnpm lockfiles today for exact installed-version inference, and now also reads simple `bun.lock` workspace entries when available. It reports source-health warnings when OSV or GitHub returns only partial coverage.
|
|
113
169
|
|
|
114
|
-
#
|
|
170
|
+
# 10) Check dependency maintenance health
|
|
115
171
|
npx @rainy-updates/cli health
|
|
116
172
|
npx @rainy-updates/cli health --stale 6m # flag packages with no release in 6 months
|
|
117
173
|
npx @rainy-updates/cli health --stale 180d # same but in days
|
|
118
174
|
rup health --stale 6m # if installed
|
|
119
175
|
|
|
120
|
-
#
|
|
176
|
+
# 11) Find which version introduced a breaking change
|
|
121
177
|
npx @rainy-updates/cli bisect axios --cmd "bun test"
|
|
122
178
|
npx @rainy-updates/cli bisect react --range "18.0.0..19.0.0" --cmd "npm test"
|
|
123
179
|
npx @rainy-updates/cli bisect lodash --cmd "npm run test:unit" --dry-run
|
|
124
180
|
rup bisect axios --cmd "bun test" # if installed
|
|
125
181
|
|
|
126
|
-
#
|
|
127
|
-
npx @rainy-updates/cli review --security-only
|
|
128
|
-
rup review --interactive
|
|
182
|
+
# 12) Focus review on high-risk changes
|
|
129
183
|
rup review --risk high --diff major
|
|
130
|
-
|
|
131
|
-
# 12) Get a fast dependency verdict for CI or local triage ── NEW in v0.5.2 GA
|
|
132
|
-
npx @rainy-updates/cli doctor
|
|
133
|
-
rup doctor --verdict-only
|
|
134
184
|
```
|
|
135
185
|
|
|
136
186
|
## What it does in production
|
|
@@ -185,6 +235,13 @@ npx @rainy-updates/cli check --policy-file .rainyupdates-policy.json
|
|
|
185
235
|
- `--format table`
|
|
186
236
|
- `--format minimal`
|
|
187
237
|
|
|
238
|
+
Review-centered outputs:
|
|
239
|
+
|
|
240
|
+
- `check` is optimized for detection.
|
|
241
|
+
- `doctor` is optimized for summary.
|
|
242
|
+
- `review` is optimized for decision-making.
|
|
243
|
+
- `upgrade` is optimized for safe application.
|
|
244
|
+
|
|
188
245
|
### Automation output
|
|
189
246
|
|
|
190
247
|
- `--format json`
|
package/dist/bin/cli.js
CHANGED
|
@@ -85,7 +85,7 @@ async function main() {
|
|
|
85
85
|
process.exitCode = result.totalFlagged > 0 ? 1 : 0;
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
// ─── v0.5.
|
|
88
|
+
// ─── v0.5.4 commands ─────────────────────────────────────────────────────
|
|
89
89
|
if (parsed.command === "unused") {
|
|
90
90
|
const { runUnused } = await import("../commands/unused/runner.js");
|
|
91
91
|
const result = await runUnused(parsed.options);
|
|
@@ -226,7 +226,11 @@ function renderHelp(command) {
|
|
|
226
226
|
if (isCommand && command === "check") {
|
|
227
227
|
return `rainy-updates check [options]
|
|
228
228
|
|
|
229
|
-
Detect
|
|
229
|
+
Detect candidate dependency updates. This is the first step in the flow:
|
|
230
|
+
check detects
|
|
231
|
+
doctor summarizes
|
|
232
|
+
review decides
|
|
233
|
+
upgrade applies
|
|
230
234
|
|
|
231
235
|
Options:
|
|
232
236
|
--workspace
|
|
@@ -291,7 +295,7 @@ Options:
|
|
|
291
295
|
if (isCommand && command === "upgrade") {
|
|
292
296
|
return `rainy-updates upgrade [options]
|
|
293
297
|
|
|
294
|
-
Apply
|
|
298
|
+
Apply an approved change set to package.json manifests.
|
|
295
299
|
|
|
296
300
|
Options:
|
|
297
301
|
--workspace
|
|
@@ -318,7 +322,11 @@ Options:
|
|
|
318
322
|
if (isCommand && command === "ci") {
|
|
319
323
|
return `rainy-updates ci [options]
|
|
320
324
|
|
|
321
|
-
Run CI-oriented
|
|
325
|
+
Run CI-oriented automation around the same lifecycle:
|
|
326
|
+
check detects
|
|
327
|
+
doctor summarizes
|
|
328
|
+
review decides
|
|
329
|
+
upgrade applies
|
|
322
330
|
|
|
323
331
|
Options:
|
|
324
332
|
--workspace
|
|
@@ -396,7 +404,8 @@ Options:
|
|
|
396
404
|
if (isCommand && command === "review") {
|
|
397
405
|
return `rainy-updates review [options]
|
|
398
406
|
|
|
399
|
-
Review
|
|
407
|
+
Review is the decision center of Rainy Updates.
|
|
408
|
+
Use it to inspect risk, security, peer, license, and policy context before applying changes.
|
|
400
409
|
|
|
401
410
|
Options:
|
|
402
411
|
--workspace
|
|
@@ -414,7 +423,7 @@ Options:
|
|
|
414
423
|
if (isCommand && command === "doctor") {
|
|
415
424
|
return `rainy-updates doctor [options]
|
|
416
425
|
|
|
417
|
-
Produce a fast
|
|
426
|
+
Produce a fast summary verdict and point the operator to review when action is needed.
|
|
418
427
|
|
|
419
428
|
Options:
|
|
420
429
|
--workspace
|
|
@@ -424,9 +433,11 @@ Options:
|
|
|
424
433
|
return `rainy-updates (rup / rainy-up) <command> [options]
|
|
425
434
|
|
|
426
435
|
Commands:
|
|
427
|
-
check Detect
|
|
428
|
-
|
|
429
|
-
|
|
436
|
+
check Detect candidate updates
|
|
437
|
+
doctor Summarize what matters
|
|
438
|
+
review Decide what to do
|
|
439
|
+
upgrade Apply the approved change set
|
|
440
|
+
ci Run CI-focused orchestration
|
|
430
441
|
warm-cache Warm local cache for fast/offline checks
|
|
431
442
|
init-ci Scaffold GitHub Actions workflow
|
|
432
443
|
baseline Save/check dependency baseline snapshots
|
|
@@ -437,8 +448,6 @@ Commands:
|
|
|
437
448
|
resolve Check peer dependency conflicts (pure-TS, no subprocess)
|
|
438
449
|
licenses Scan dependency licenses and generate SPDX SBOM
|
|
439
450
|
snapshot Save, list, restore, and diff dependency state snapshots
|
|
440
|
-
review Guided dependency review with risk/security context
|
|
441
|
-
doctor Fast dependency verdict for local or CI use
|
|
442
451
|
|
|
443
452
|
Global options:
|
|
444
453
|
--cwd <path>
|
|
@@ -85,7 +85,7 @@ Usage:
|
|
|
85
85
|
rup doctor [options]
|
|
86
86
|
|
|
87
87
|
Options:
|
|
88
|
-
--verdict-only Print
|
|
88
|
+
--verdict-only Print the 3-line quick verdict without counts
|
|
89
89
|
--workspace Scan all workspace packages
|
|
90
90
|
--json-file <path> Write JSON doctor report to file
|
|
91
91
|
--cwd <path>
|
package/dist/core/check.js
CHANGED
|
@@ -93,7 +93,8 @@ export async function check(options) {
|
|
|
93
93
|
latestVersion: cached.latestVersion,
|
|
94
94
|
availableVersions: cached.availableVersions,
|
|
95
95
|
publishedAtByVersion: {},
|
|
96
|
-
|
|
96
|
+
installScriptByVersion: {},
|
|
97
|
+
maintainerCount: null,
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
@@ -111,7 +112,8 @@ export async function check(options) {
|
|
|
111
112
|
latestVersion: stale.latestVersion,
|
|
112
113
|
availableVersions: stale.availableVersions,
|
|
113
114
|
publishedAtByVersion: {},
|
|
114
|
-
|
|
115
|
+
installScriptByVersion: {},
|
|
116
|
+
maintainerCount: null,
|
|
115
117
|
});
|
|
116
118
|
warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
|
|
117
119
|
}
|
|
@@ -143,7 +145,8 @@ export async function check(options) {
|
|
|
143
145
|
publishedAtByVersion: metadata.publishedAtByVersion,
|
|
144
146
|
homepage: metadata.homepage,
|
|
145
147
|
repository: metadata.repository,
|
|
146
|
-
|
|
148
|
+
installScriptByVersion: metadata.installScriptByVersion,
|
|
149
|
+
maintainerCount: metadata.maintainerCount,
|
|
147
150
|
});
|
|
148
151
|
if (metadata.latestVersion) {
|
|
149
152
|
await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
@@ -158,7 +161,8 @@ export async function check(options) {
|
|
|
158
161
|
latestVersion: stale.latestVersion,
|
|
159
162
|
availableVersions: stale.availableVersions,
|
|
160
163
|
publishedAtByVersion: {},
|
|
161
|
-
|
|
164
|
+
installScriptByVersion: {},
|
|
165
|
+
maintainerCount: null,
|
|
162
166
|
});
|
|
163
167
|
warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
|
|
164
168
|
}
|
|
@@ -214,6 +218,17 @@ export async function check(options) {
|
|
|
214
218
|
autofix: rule?.autofix !== false,
|
|
215
219
|
reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
|
|
216
220
|
homepage: metadata.homepage,
|
|
221
|
+
repository: metadata.repository,
|
|
222
|
+
publishedAt: metadata.publishedAtByVersion[picked]
|
|
223
|
+
? new Date(metadata.publishedAtByVersion[picked]).toISOString()
|
|
224
|
+
: undefined,
|
|
225
|
+
publishAgeDays: metadata.publishedAtByVersion[picked]
|
|
226
|
+
? Math.max(0, Math.floor((Date.now() - metadata.publishedAtByVersion[picked]) /
|
|
227
|
+
(1000 * 60 * 60 * 24)))
|
|
228
|
+
: null,
|
|
229
|
+
hasInstallScript: metadata.installScriptByVersion[picked] ?? false,
|
|
230
|
+
maintainerCount: metadata.maintainerCount,
|
|
231
|
+
maintainerChurn: deriveMaintainerChurn(metadata.maintainerCount, metadata.publishedAtByVersion[picked]),
|
|
217
232
|
});
|
|
218
233
|
emitStream(`[update] ${task.dependency.name} ${task.dependency.range} -> ${nextRange} (${classifyDiff(task.dependency.range, picked)})`);
|
|
219
234
|
}
|
|
@@ -265,6 +280,16 @@ export async function check(options) {
|
|
|
265
280
|
warnings: sortedWarnings,
|
|
266
281
|
};
|
|
267
282
|
}
|
|
283
|
+
function deriveMaintainerChurn(maintainerCount, publishedAt) {
|
|
284
|
+
if (maintainerCount === null || publishedAt === undefined) {
|
|
285
|
+
return "unknown";
|
|
286
|
+
}
|
|
287
|
+
const ageDays = Math.max(0, Math.floor((Date.now() - publishedAt) / (1000 * 60 * 60 * 24)));
|
|
288
|
+
if (maintainerCount <= 1 && ageDays <= 30) {
|
|
289
|
+
return "elevated-change";
|
|
290
|
+
}
|
|
291
|
+
return "stable";
|
|
292
|
+
}
|
|
268
293
|
function groupUpdates(updates, groupBy) {
|
|
269
294
|
if (updates.length === 0) {
|
|
270
295
|
return [];
|
|
@@ -3,6 +3,7 @@ import process from "node:process";
|
|
|
3
3
|
import { check } from "./check.js";
|
|
4
4
|
import { createSummary, finalizeSummary } from "./summary.js";
|
|
5
5
|
import { applyImpactScores } from "./impact.js";
|
|
6
|
+
import { applyRiskAssessments } from "../risk/index.js";
|
|
6
7
|
export async function buildReviewResult(options) {
|
|
7
8
|
const baseCheckOptions = {
|
|
8
9
|
...options,
|
|
@@ -11,13 +12,13 @@ export async function buildReviewResult(options) {
|
|
|
11
12
|
showHomepage: true,
|
|
12
13
|
};
|
|
13
14
|
const checkResult = await check(baseCheckOptions);
|
|
14
|
-
const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await Promise.all([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
]);
|
|
15
|
+
const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await runSilenced(() => Promise.all([
|
|
16
|
+
import("../commands/audit/runner.js").then((mod) => mod.runAudit(toAuditOptions(options))),
|
|
17
|
+
import("../commands/resolve/runner.js").then((mod) => mod.runResolve(toResolveOptions(options))),
|
|
18
|
+
import("../commands/health/runner.js").then((mod) => mod.runHealth(toHealthOptions(options))),
|
|
19
|
+
import("../commands/licenses/runner.js").then((mod) => mod.runLicenses(toLicenseOptions(options))),
|
|
20
|
+
import("../commands/unused/runner.js").then((mod) => mod.runUnused(toUnusedOptions(options))),
|
|
21
|
+
]));
|
|
21
22
|
const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
|
|
22
23
|
const impactedUpdates = applyImpactScores(checkResult.updates, {
|
|
23
24
|
advisoryPackages,
|
|
@@ -47,9 +48,12 @@ export async function buildReviewResult(options) {
|
|
|
47
48
|
list.push(issue);
|
|
48
49
|
unusedByName.set(issue.name, list);
|
|
49
50
|
}
|
|
50
|
-
const
|
|
51
|
+
const baseItems = impactedUpdates
|
|
51
52
|
.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName))
|
|
52
53
|
.filter((item) => matchesReviewFilters(item, options));
|
|
54
|
+
const items = applyRiskAssessments(baseItems, {
|
|
55
|
+
knownPackageNames: new Set(checkResult.updates.map((item) => item.name)),
|
|
56
|
+
}).filter((item) => matchesReviewFilters(item, options));
|
|
53
57
|
const summary = createReviewSummary(checkResult.summary, items, [
|
|
54
58
|
...checkResult.errors,
|
|
55
59
|
...auditResult.errors,
|
|
@@ -76,7 +80,7 @@ export async function buildReviewResult(options) {
|
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
export function createDoctorResult(review) {
|
|
79
|
-
const verdict = review.summary.verdict ?? deriveVerdict(review.items);
|
|
83
|
+
const verdict = review.summary.verdict ?? deriveVerdict(review.items, review.errors);
|
|
80
84
|
const primaryFindings = buildPrimaryFindings(review);
|
|
81
85
|
return {
|
|
82
86
|
verdict,
|
|
@@ -101,6 +105,9 @@ export function renderReviewResult(review) {
|
|
|
101
105
|
const notes = [
|
|
102
106
|
item.update.diffType,
|
|
103
107
|
item.update.riskLevel ? `risk=${item.update.riskLevel}` : undefined,
|
|
108
|
+
typeof item.update.riskScore === "number"
|
|
109
|
+
? `score=${item.update.riskScore}`
|
|
110
|
+
: undefined,
|
|
104
111
|
item.update.advisoryCount ? `security=${item.update.advisoryCount}` : undefined,
|
|
105
112
|
item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none"
|
|
106
113
|
? `peer=${item.update.peerConflictSeverity}`
|
|
@@ -113,6 +120,9 @@ export function renderReviewResult(review) {
|
|
|
113
120
|
if (item.update.riskReasons && item.update.riskReasons.length > 0) {
|
|
114
121
|
lines.push(` reasons: ${item.update.riskReasons.join("; ")}`);
|
|
115
122
|
}
|
|
123
|
+
if (item.update.recommendedAction) {
|
|
124
|
+
lines.push(` action: ${item.update.recommendedAction}`);
|
|
125
|
+
}
|
|
116
126
|
if (item.update.homepage) {
|
|
117
127
|
lines.push(` homepage: ${item.update.homepage}`);
|
|
118
128
|
}
|
|
@@ -153,16 +163,6 @@ function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, l
|
|
|
153
163
|
const health = healthByName.get(update.name);
|
|
154
164
|
const license = licenseByName.get(update.name);
|
|
155
165
|
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
166
|
return {
|
|
167
167
|
update: {
|
|
168
168
|
...update,
|
|
@@ -178,8 +178,6 @@ function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, l
|
|
|
178
178
|
? "allowed"
|
|
179
179
|
: "review",
|
|
180
180
|
healthStatus: health?.flags[0] ?? "healthy",
|
|
181
|
-
riskLevel,
|
|
182
|
-
riskReasons,
|
|
183
181
|
},
|
|
184
182
|
advisories,
|
|
185
183
|
health,
|
|
@@ -244,10 +242,10 @@ function createReviewSummary(base, items, errors, warnings, interactiveSession)
|
|
|
244
242
|
summary.riskPackages = items.filter((item) => item.update.riskLevel === "critical" || item.update.riskLevel === "high").length;
|
|
245
243
|
summary.peerConflictPackages = items.filter((item) => item.update.peerConflictSeverity && item.update.peerConflictSeverity !== "none").length;
|
|
246
244
|
summary.licenseViolationPackages = items.filter((item) => item.update.licenseStatus === "denied").length;
|
|
247
|
-
summary.verdict = deriveVerdict(items);
|
|
245
|
+
summary.verdict = deriveVerdict(items, errors);
|
|
248
246
|
return summary;
|
|
249
247
|
}
|
|
250
|
-
function deriveVerdict(items) {
|
|
248
|
+
function deriveVerdict(items, errors) {
|
|
251
249
|
if (items.some((item) => item.update.peerConflictSeverity === "error" ||
|
|
252
250
|
item.update.licenseStatus === "denied")) {
|
|
253
251
|
return "blocked";
|
|
@@ -255,23 +253,12 @@ function deriveVerdict(items) {
|
|
|
255
253
|
if (items.some((item) => item.advisories.length > 0 || item.update.riskLevel === "critical")) {
|
|
256
254
|
return "actionable";
|
|
257
255
|
}
|
|
258
|
-
if (
|
|
256
|
+
if (errors.length > 0 ||
|
|
257
|
+
items.some((item) => item.update.riskLevel === "high" || item.update.diffType === "major")) {
|
|
259
258
|
return "review";
|
|
260
259
|
}
|
|
261
260
|
return "safe";
|
|
262
261
|
}
|
|
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
262
|
function buildPrimaryFindings(review) {
|
|
276
263
|
const findings = [];
|
|
277
264
|
if ((review.summary.peerConflictPackages ?? 0) > 0) {
|
|
@@ -286,6 +273,9 @@ function buildPrimaryFindings(review) {
|
|
|
286
273
|
if ((review.summary.riskPackages ?? 0) > 0) {
|
|
287
274
|
findings.push(`${review.summary.riskPackages} package(s) are high risk.`);
|
|
288
275
|
}
|
|
276
|
+
if (review.errors.length > 0) {
|
|
277
|
+
findings.push(`${review.errors.length} execution error(s) need review before treating the result as clean.`);
|
|
278
|
+
}
|
|
289
279
|
if (findings.length === 0) {
|
|
290
280
|
findings.push("No blocking findings; remaining updates are low-risk.");
|
|
291
281
|
}
|
|
@@ -293,10 +283,10 @@ function buildPrimaryFindings(review) {
|
|
|
293
283
|
}
|
|
294
284
|
function recommendCommand(review, verdict) {
|
|
295
285
|
if (verdict === "blocked")
|
|
296
|
-
return "rup
|
|
286
|
+
return "rup review --interactive";
|
|
297
287
|
if ((review.summary.securityPackages ?? 0) > 0)
|
|
298
|
-
return "rup
|
|
299
|
-
if (review.items.length > 0)
|
|
288
|
+
return "rup review --security-only";
|
|
289
|
+
if (review.errors.length > 0 || review.items.length > 0)
|
|
300
290
|
return "rup review --interactive";
|
|
301
291
|
return "rup check";
|
|
302
292
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ 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
10
|
export { buildReviewResult, createDoctorResult } from "./core/review-model.js";
|
|
11
|
-
export
|
|
11
|
+
export { applyRiskAssessments } from "./risk/index.js";
|
|
12
|
+
export type { CheckOptions, CheckResult, CiProfile, DependencyKind, FailOnLevel, GroupBy, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, ReviewOptions, ReviewResult, DoctorOptions, DoctorResult, Verdict, RiskLevel, RiskCategory, RiskAssessment, RiskFactor, MaintainerChurnStatus, } from "./types/index.js";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ 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
10
|
export { buildReviewResult, createDoctorResult } from "./core/review-model.js";
|
|
11
|
+
export { applyRiskAssessments } from "./risk/index.js";
|
package/dist/output/format.js
CHANGED
|
@@ -8,6 +8,11 @@ export function renderResult(result, format, display = {}) {
|
|
|
8
8
|
if (result.updates.length === 0 && result.summary.warmedPackages > 0) {
|
|
9
9
|
return `Cache warmed for ${result.summary.warmedPackages} package(s).`;
|
|
10
10
|
}
|
|
11
|
+
if (result.updates.length === 0 && result.errors.length > 0) {
|
|
12
|
+
const [firstError] = result.errors;
|
|
13
|
+
const suffix = result.errors.length > 1 ? ` (+${result.errors.length - 1} more errors)` : "";
|
|
14
|
+
return `${firstError}${suffix}`;
|
|
15
|
+
}
|
|
11
16
|
if (result.updates.length === 0)
|
|
12
17
|
return "No updates found.";
|
|
13
18
|
return result.updates
|
|
@@ -16,6 +21,9 @@ export function renderResult(result, format, display = {}) {
|
|
|
16
21
|
if (display.showImpact && item.impactScore) {
|
|
17
22
|
parts.push(`impact=${item.impactScore.rank}:${item.impactScore.score}`);
|
|
18
23
|
}
|
|
24
|
+
if (typeof item.riskScore === "number") {
|
|
25
|
+
parts.push(`risk=${item.riskLevel}:${item.riskScore}`);
|
|
26
|
+
}
|
|
19
27
|
if (display.showHomepage && item.homepage) {
|
|
20
28
|
parts.push(item.homepage);
|
|
21
29
|
}
|
|
@@ -80,9 +88,13 @@ export function renderResult(result, format, display = {}) {
|
|
|
80
88
|
: undefined,
|
|
81
89
|
display.showHomepage && update.homepage ? update.homepage : undefined,
|
|
82
90
|
update.riskLevel ? `risk=${update.riskLevel}` : undefined,
|
|
91
|
+
typeof update.riskScore === "number" ? `score=${update.riskScore}` : undefined,
|
|
83
92
|
]
|
|
84
93
|
.filter(Boolean)
|
|
85
94
|
.join(", ")})`);
|
|
95
|
+
if (update.recommendedAction) {
|
|
96
|
+
lines.push(` action: ${update.recommendedAction}`);
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
if (result.errors.length > 0) {
|
package/dist/output/github.js
CHANGED
|
@@ -42,7 +42,7 @@ export function renderGitHubAnnotations(result) {
|
|
|
42
42
|
return left.packagePath.localeCompare(right.packagePath);
|
|
43
43
|
});
|
|
44
44
|
for (const update of sortedUpdates) {
|
|
45
|
-
lines.push(`::notice title=Dependency Update::${update.name} ${update.fromRange} -> ${update.toRange} (${update.packagePath})`);
|
|
45
|
+
lines.push(`::notice title=Dependency Update::${update.name} ${update.fromRange} -> ${update.toRange} (${update.packagePath})${typeof update.riskScore === "number" ? ` [risk=${update.riskLevel}:${update.riskScore}]` : ""}`);
|
|
46
46
|
}
|
|
47
47
|
for (const warning of [...result.warnings].sort((a, b) => a.localeCompare(b))) {
|
|
48
48
|
lines.push(`::warning title=Rainy Updates::${warning}`);
|
package/dist/output/sarif.js
CHANGED
|
@@ -33,6 +33,9 @@ export function createSarifReport(result) {
|
|
|
33
33
|
impactRank: update.impactScore?.rank,
|
|
34
34
|
impactScore: update.impactScore?.score,
|
|
35
35
|
riskLevel: update.riskLevel,
|
|
36
|
+
riskScore: update.riskScore,
|
|
37
|
+
riskCategories: update.riskCategories ?? [],
|
|
38
|
+
recommendedAction: update.recommendedAction,
|
|
36
39
|
advisoryCount: update.advisoryCount ?? 0,
|
|
37
40
|
peerConflictSeverity: update.peerConflictSeverity ?? "none",
|
|
38
41
|
licenseStatus: update.licenseStatus ?? "allowed",
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -24,7 +24,8 @@ export interface ResolveManyResult {
|
|
|
24
24
|
publishedAtByVersion: Record<string, number>;
|
|
25
25
|
homepage?: string;
|
|
26
26
|
repository?: string;
|
|
27
|
-
|
|
27
|
+
installScriptByVersion: Record<string, boolean>;
|
|
28
|
+
maintainerCount: number | null;
|
|
28
29
|
}>;
|
|
29
30
|
errors: Map<string, string>;
|
|
30
31
|
}
|
|
@@ -39,7 +40,8 @@ export declare class NpmRegistryClient {
|
|
|
39
40
|
publishedAtByVersion: Record<string, number>;
|
|
40
41
|
homepage?: string;
|
|
41
42
|
repository?: string;
|
|
42
|
-
|
|
43
|
+
installScriptByVersion: Record<string, boolean>;
|
|
44
|
+
maintainerCount: number | null;
|
|
43
45
|
}>;
|
|
44
46
|
resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
|
|
45
47
|
resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
|
package/dist/registry/npm.js
CHANGED
|
@@ -22,7 +22,13 @@ export class NpmRegistryClient {
|
|
|
22
22
|
try {
|
|
23
23
|
const response = await requester(packageName, timeoutMs);
|
|
24
24
|
if (response.status === 404) {
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
latestVersion: null,
|
|
27
|
+
versions: [],
|
|
28
|
+
publishedAtByVersion: {},
|
|
29
|
+
installScriptByVersion: {},
|
|
30
|
+
maintainerCount: null,
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
if (response.status === 429 || response.status >= 500) {
|
|
28
34
|
throw new RetryableRegistryError(`Registry temporary error: ${response.status}`, response.retryAfterMs ?? computeBackoffMs(attempt));
|
|
@@ -37,7 +43,10 @@ export class NpmRegistryClient {
|
|
|
37
43
|
publishedAtByVersion: extractPublishTimes(response.data?.time),
|
|
38
44
|
homepage: response.data?.homepage,
|
|
39
45
|
repository: normalizeRepository(response.data?.repository),
|
|
40
|
-
|
|
46
|
+
installScriptByVersion: detectInstallScriptsByVersion(response.data?.versions),
|
|
47
|
+
maintainerCount: Array.isArray(response.data?.maintainers)
|
|
48
|
+
? response.data?.maintainers.length
|
|
49
|
+
: null,
|
|
41
50
|
};
|
|
42
51
|
}
|
|
43
52
|
catch (error) {
|
|
@@ -104,18 +113,15 @@ function normalizeRepository(value) {
|
|
|
104
113
|
return value;
|
|
105
114
|
return value.url;
|
|
106
115
|
}
|
|
107
|
-
function
|
|
116
|
+
function detectInstallScriptsByVersion(versions) {
|
|
108
117
|
if (!versions)
|
|
109
|
-
return
|
|
110
|
-
|
|
118
|
+
return {};
|
|
119
|
+
const results = {};
|
|
120
|
+
for (const [version, metadata] of Object.entries(versions)) {
|
|
111
121
|
const scripts = metadata?.scripts;
|
|
112
|
-
|
|
113
|
-
continue;
|
|
114
|
-
if (scripts.preinstall || scripts.install || scripts.postinstall) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
122
|
+
results[version] = Boolean(scripts?.preinstall || scripts?.install || scripts?.postinstall);
|
|
117
123
|
}
|
|
118
|
-
return
|
|
124
|
+
return results;
|
|
119
125
|
}
|
|
120
126
|
function computeBackoffMs(attempt) {
|
|
121
127
|
const baseMs = Math.max(120, attempt * 180);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { assessRisk } from "./scorer.js";
|
|
2
|
+
export function applyRiskAssessments(items, context) {
|
|
3
|
+
return items.map((item) => {
|
|
4
|
+
const assessment = assessRisk({
|
|
5
|
+
update: item.update,
|
|
6
|
+
advisories: item.advisories,
|
|
7
|
+
health: item.health,
|
|
8
|
+
peerConflicts: item.peerConflicts,
|
|
9
|
+
licenseViolation: item.update.licenseStatus === "denied",
|
|
10
|
+
unusedIssues: item.unusedIssues,
|
|
11
|
+
}, context);
|
|
12
|
+
return {
|
|
13
|
+
...item,
|
|
14
|
+
update: {
|
|
15
|
+
...item.update,
|
|
16
|
+
riskLevel: assessment.level,
|
|
17
|
+
riskScore: assessment.score,
|
|
18
|
+
riskReasons: assessment.reasons,
|
|
19
|
+
riskCategories: assessment.categories,
|
|
20
|
+
recommendedAction: assessment.recommendedAction,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { detectInstallScriptsRisk } from "./signals/install-scripts.js";
|
|
2
|
+
import { detectTyposquatRisk } from "./signals/typosquat.js";
|
|
3
|
+
import { detectFreshPackageRisk } from "./signals/fresh-package.js";
|
|
4
|
+
import { detectSuspiciousMetadataRisk } from "./signals/metadata.js";
|
|
5
|
+
import { detectMutableSourceRisk } from "./signals/mutable-source.js";
|
|
6
|
+
import { detectMaintainerChurnRisk } from "./signals/maintainer-churn.js";
|
|
7
|
+
export function assessRisk(input, context) {
|
|
8
|
+
// Base factors model direct supply-chain behavior; modifiers adjust operational severity.
|
|
9
|
+
const baseFactors = [];
|
|
10
|
+
const modifierFactors = [];
|
|
11
|
+
if (input.advisories.length > 0) {
|
|
12
|
+
baseFactors.push({
|
|
13
|
+
code: "known-vulnerability",
|
|
14
|
+
weight: 35,
|
|
15
|
+
category: "known-vulnerability",
|
|
16
|
+
message: `${input.advisories.length} known vulnerability finding(s) affect this package.`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const installScripts = detectInstallScriptsRisk(input);
|
|
20
|
+
if (installScripts)
|
|
21
|
+
baseFactors.push(installScripts);
|
|
22
|
+
const typosquat = detectTyposquatRisk(input, context);
|
|
23
|
+
if (typosquat)
|
|
24
|
+
baseFactors.push(typosquat);
|
|
25
|
+
const freshPackage = detectFreshPackageRisk(input);
|
|
26
|
+
if (freshPackage)
|
|
27
|
+
baseFactors.push(freshPackage);
|
|
28
|
+
const metadata = detectSuspiciousMetadataRisk(input);
|
|
29
|
+
if (metadata)
|
|
30
|
+
baseFactors.push(metadata);
|
|
31
|
+
const mutableSource = detectMutableSourceRisk(input);
|
|
32
|
+
if (mutableSource)
|
|
33
|
+
baseFactors.push(mutableSource);
|
|
34
|
+
const maintainerChurn = detectMaintainerChurnRisk(input);
|
|
35
|
+
if (maintainerChurn)
|
|
36
|
+
baseFactors.push(maintainerChurn);
|
|
37
|
+
if (input.peerConflicts.some((conflict) => conflict.severity === "error")) {
|
|
38
|
+
modifierFactors.push({
|
|
39
|
+
code: "peer-conflict",
|
|
40
|
+
weight: 20,
|
|
41
|
+
category: "operational-health",
|
|
42
|
+
message: "Peer dependency conflicts block safe application.",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (input.licenseViolation) {
|
|
46
|
+
modifierFactors.push({
|
|
47
|
+
code: "license-violation",
|
|
48
|
+
weight: 20,
|
|
49
|
+
category: "operational-health",
|
|
50
|
+
message: "License policy would block or require review for this update.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (input.health?.flags.includes("deprecated")) {
|
|
54
|
+
modifierFactors.push({
|
|
55
|
+
code: "deprecated-package",
|
|
56
|
+
weight: 10,
|
|
57
|
+
category: "operational-health",
|
|
58
|
+
message: "Package is deprecated.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (input.health?.flags.includes("stale") ||
|
|
62
|
+
input.health?.flags.includes("unmaintained")) {
|
|
63
|
+
modifierFactors.push({
|
|
64
|
+
code: "stale-package",
|
|
65
|
+
weight: 5,
|
|
66
|
+
category: "operational-health",
|
|
67
|
+
message: "Package has stale operational health signals.",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (input.update.diffType === "major") {
|
|
71
|
+
modifierFactors.push({
|
|
72
|
+
code: "major-version",
|
|
73
|
+
weight: 10,
|
|
74
|
+
category: "operational-health",
|
|
75
|
+
message: "Update crosses a major version boundary.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const baseScore = baseFactors.reduce((sum, factor) => sum + factor.weight, 0);
|
|
79
|
+
const modifierScore = modifierFactors.reduce((sum, factor) => sum + factor.weight, 0);
|
|
80
|
+
const factors = [...baseFactors, ...modifierFactors];
|
|
81
|
+
const score = Math.min(100, baseScore + modifierScore);
|
|
82
|
+
const level = scoreToLevel(score);
|
|
83
|
+
const categories = Array.from(new Set(factors.map((factor) => factor.category)));
|
|
84
|
+
const reasons = factors.map((factor) => factor.message);
|
|
85
|
+
return {
|
|
86
|
+
score,
|
|
87
|
+
level,
|
|
88
|
+
reasons,
|
|
89
|
+
categories,
|
|
90
|
+
recommendedAction: recommendAction(level, input),
|
|
91
|
+
factors,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function scoreToLevel(score) {
|
|
95
|
+
if (score >= 70)
|
|
96
|
+
return "critical";
|
|
97
|
+
if (score >= 45)
|
|
98
|
+
return "high";
|
|
99
|
+
if (score >= 20)
|
|
100
|
+
return "medium";
|
|
101
|
+
return "low";
|
|
102
|
+
}
|
|
103
|
+
function recommendAction(level, input) {
|
|
104
|
+
if (input.peerConflicts.some((conflict) => conflict.severity === "error")) {
|
|
105
|
+
return "Run `rup resolve --after-update` before applying this update.";
|
|
106
|
+
}
|
|
107
|
+
if (input.advisories.length > 0) {
|
|
108
|
+
return "Review in `rup review` and consider `rup audit --fix` for the secure minimum patch.";
|
|
109
|
+
}
|
|
110
|
+
if (level === "critical" || level === "high") {
|
|
111
|
+
return "Keep this update in review until the risk reasons are cleared.";
|
|
112
|
+
}
|
|
113
|
+
return "Safe to keep in the review queue and apply after normal verification.";
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function detectFreshPackageRisk(input) {
|
|
2
|
+
const age = input.update.publishAgeDays;
|
|
3
|
+
if (typeof age !== "number")
|
|
4
|
+
return null;
|
|
5
|
+
if (age <= 7) {
|
|
6
|
+
return {
|
|
7
|
+
code: "fresh-package-7d",
|
|
8
|
+
weight: 20,
|
|
9
|
+
category: "behavioral-risk",
|
|
10
|
+
message: `Resolved version was published ${age} day(s) ago.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (age <= 30) {
|
|
14
|
+
return {
|
|
15
|
+
code: "fresh-package-30d",
|
|
16
|
+
weight: 10,
|
|
17
|
+
category: "behavioral-risk",
|
|
18
|
+
message: `Resolved version was published ${age} day(s) ago.`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function detectInstallScriptsRisk(input) {
|
|
2
|
+
if (!input.update.hasInstallScript)
|
|
3
|
+
return null;
|
|
4
|
+
return {
|
|
5
|
+
code: "install-scripts",
|
|
6
|
+
weight: 20,
|
|
7
|
+
category: "behavioral-risk",
|
|
8
|
+
message: "Resolved package includes install lifecycle scripts.",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function detectMaintainerChurnRisk(input) {
|
|
2
|
+
if (input.update.maintainerChurn !== "elevated-change") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return {
|
|
6
|
+
code: "maintainer-churn",
|
|
7
|
+
weight: 15,
|
|
8
|
+
category: "behavioral-risk",
|
|
9
|
+
message: "Maintainer profile looks unstable for a recent release based on available registry metadata.",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function detectSuspiciousMetadataRisk(input) {
|
|
2
|
+
const { homepage, repository } = input.update;
|
|
3
|
+
const homepageMissing = !homepage;
|
|
4
|
+
const repositoryMissing = !repository;
|
|
5
|
+
const repositoryMalformed = typeof repository === "string" &&
|
|
6
|
+
!repository.startsWith("http://") &&
|
|
7
|
+
!repository.startsWith("https://") &&
|
|
8
|
+
!repository.startsWith("git+");
|
|
9
|
+
if ((homepageMissing && repositoryMissing) || repositoryMalformed) {
|
|
10
|
+
return {
|
|
11
|
+
code: "suspicious-metadata",
|
|
12
|
+
weight: 10,
|
|
13
|
+
category: "behavioral-risk",
|
|
14
|
+
message: "Package metadata is incomplete or uses a non-canonical repository reference.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MUTABLE_PATTERNS = [
|
|
2
|
+
"git+",
|
|
3
|
+
"github:",
|
|
4
|
+
"gitlab:",
|
|
5
|
+
"http://",
|
|
6
|
+
"https://",
|
|
7
|
+
"git://",
|
|
8
|
+
];
|
|
9
|
+
export function detectMutableSourceRisk(input) {
|
|
10
|
+
const raw = input.update.fromRange;
|
|
11
|
+
if (!MUTABLE_PATTERNS.some((pattern) => raw.startsWith(pattern))) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const immutableCommitPinned = /#[a-f0-9]{7,40}$/i.test(raw);
|
|
15
|
+
if (immutableCommitPinned) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
code: "mutable-source",
|
|
20
|
+
weight: 25,
|
|
21
|
+
category: "behavioral-risk",
|
|
22
|
+
message: "Dependency uses a mutable git/http source without an immutable commit pin.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const HIGH_VALUE_PACKAGES = [
|
|
2
|
+
"react",
|
|
3
|
+
"react-dom",
|
|
4
|
+
"next",
|
|
5
|
+
"typescript",
|
|
6
|
+
"lodash",
|
|
7
|
+
"axios",
|
|
8
|
+
"zod",
|
|
9
|
+
"vite",
|
|
10
|
+
"eslint",
|
|
11
|
+
"express",
|
|
12
|
+
];
|
|
13
|
+
export function detectTyposquatRisk(input, context) {
|
|
14
|
+
const target = normalizeName(input.update.name);
|
|
15
|
+
if (target.length < 4)
|
|
16
|
+
return null;
|
|
17
|
+
const candidates = new Set([
|
|
18
|
+
...HIGH_VALUE_PACKAGES,
|
|
19
|
+
...Array.from(context.knownPackageNames),
|
|
20
|
+
]);
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
const normalizedCandidate = normalizeName(candidate);
|
|
23
|
+
if (!normalizedCandidate || normalizedCandidate === target)
|
|
24
|
+
continue;
|
|
25
|
+
if (Math.abs(normalizedCandidate.length - target.length) > 1)
|
|
26
|
+
continue;
|
|
27
|
+
if (isTransposition(normalizedCandidate, target) || levenshtein(normalizedCandidate, target) === 1) {
|
|
28
|
+
return {
|
|
29
|
+
code: "typosquat-heuristic",
|
|
30
|
+
weight: 25,
|
|
31
|
+
category: "behavioral-risk",
|
|
32
|
+
message: `Package name is highly similar to "${candidate}", which may indicate typosquatting.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function normalizeName(value) {
|
|
39
|
+
const trimmed = value.startsWith("@") ? value.split("/")[1] ?? value : value;
|
|
40
|
+
return trimmed.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
function isTransposition(left, right) {
|
|
43
|
+
if (left.length !== right.length || left === right)
|
|
44
|
+
return false;
|
|
45
|
+
const mismatches = [];
|
|
46
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
47
|
+
if (left[index] !== right[index])
|
|
48
|
+
mismatches.push(index);
|
|
49
|
+
if (mismatches.length > 2)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (mismatches.length !== 2)
|
|
53
|
+
return false;
|
|
54
|
+
const [first, second] = mismatches;
|
|
55
|
+
return left[first] === right[second] && left[second] === right[first];
|
|
56
|
+
}
|
|
57
|
+
function levenshtein(left, right) {
|
|
58
|
+
const matrix = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
|
|
59
|
+
for (let i = 0; i <= left.length; i += 1)
|
|
60
|
+
matrix[i][0] = i;
|
|
61
|
+
for (let j = 0; j <= right.length; j += 1)
|
|
62
|
+
matrix[0][j] = j;
|
|
63
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
64
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
65
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
66
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return matrix[left.length][right.length];
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HealthResult, PackageUpdate, PeerConflict, RiskAssessment, UnusedDependency, CveAdvisory } from "../types/index.js";
|
|
2
|
+
export interface RiskInput {
|
|
3
|
+
update: PackageUpdate;
|
|
4
|
+
advisories: CveAdvisory[];
|
|
5
|
+
health?: HealthResult["metrics"][number];
|
|
6
|
+
peerConflicts: PeerConflict[];
|
|
7
|
+
licenseViolation: boolean;
|
|
8
|
+
unusedIssues: UnusedDependency[];
|
|
9
|
+
}
|
|
10
|
+
export interface RiskContext {
|
|
11
|
+
knownPackageNames: ReadonlySet<string>;
|
|
12
|
+
}
|
|
13
|
+
export interface RiskSignalResult {
|
|
14
|
+
assessment: RiskAssessment;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type CiProfile = "minimal" | "strict" | "enterprise";
|
|
|
5
5
|
export type LockfileMode = "preserve" | "update" | "error";
|
|
6
6
|
export type Verdict = "safe" | "review" | "blocked" | "actionable";
|
|
7
7
|
export type RiskLevel = "critical" | "high" | "medium" | "low";
|
|
8
|
+
export type RiskCategory = "known-vulnerability" | "behavioral-risk" | "operational-health";
|
|
9
|
+
export type MaintainerChurnStatus = "unknown" | "stable" | "elevated-change";
|
|
8
10
|
export type OutputFormat = "table" | "json" | "minimal" | "github" | "metrics";
|
|
9
11
|
export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
|
|
10
12
|
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
@@ -91,8 +93,17 @@ export interface PackageUpdate {
|
|
|
91
93
|
reason?: string;
|
|
92
94
|
impactScore?: ImpactScore;
|
|
93
95
|
homepage?: string;
|
|
96
|
+
repository?: string;
|
|
97
|
+
publishedAt?: string;
|
|
98
|
+
publishAgeDays?: number | null;
|
|
99
|
+
hasInstallScript?: boolean;
|
|
100
|
+
maintainerCount?: number | null;
|
|
101
|
+
maintainerChurn?: MaintainerChurnStatus;
|
|
94
102
|
riskLevel?: RiskLevel;
|
|
103
|
+
riskScore?: number;
|
|
95
104
|
riskReasons?: string[];
|
|
105
|
+
riskCategories?: RiskCategory[];
|
|
106
|
+
recommendedAction?: string;
|
|
96
107
|
advisoryCount?: number;
|
|
97
108
|
peerConflictSeverity?: "none" | PeerConflictSeverity;
|
|
98
109
|
licenseStatus?: "allowed" | "review" | "denied";
|
|
@@ -323,8 +334,25 @@ export interface ResolveResult {
|
|
|
323
334
|
}
|
|
324
335
|
export interface RiskSignal {
|
|
325
336
|
packageName: string;
|
|
337
|
+
code: string;
|
|
338
|
+
weight: number;
|
|
339
|
+
category: RiskCategory;
|
|
340
|
+
level: RiskLevel;
|
|
341
|
+
reasons: string[];
|
|
342
|
+
}
|
|
343
|
+
export interface RiskFactor {
|
|
344
|
+
code: string;
|
|
345
|
+
weight: number;
|
|
346
|
+
category: RiskCategory;
|
|
347
|
+
message: string;
|
|
348
|
+
}
|
|
349
|
+
export interface RiskAssessment {
|
|
350
|
+
score: number;
|
|
326
351
|
level: RiskLevel;
|
|
327
352
|
reasons: string[];
|
|
353
|
+
categories: RiskCategory[];
|
|
354
|
+
recommendedAction: string;
|
|
355
|
+
factors: RiskFactor[];
|
|
328
356
|
}
|
|
329
357
|
export interface ReviewItem {
|
|
330
358
|
update: PackageUpdate;
|
package/dist/ui/tui.js
CHANGED
|
@@ -34,6 +34,30 @@ function diffColor(level) {
|
|
|
34
34
|
return "cyan";
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function decisionLabel(update) {
|
|
38
|
+
if (update.peerConflictSeverity === "error" || update.licenseStatus === "denied") {
|
|
39
|
+
return "blocked";
|
|
40
|
+
}
|
|
41
|
+
if (update.advisoryCount && update.advisoryCount > 0) {
|
|
42
|
+
return "actionable";
|
|
43
|
+
}
|
|
44
|
+
if (update.riskLevel === "critical" || update.riskLevel === "high") {
|
|
45
|
+
return "review";
|
|
46
|
+
}
|
|
47
|
+
return "safe";
|
|
48
|
+
}
|
|
49
|
+
function decisionColor(label) {
|
|
50
|
+
switch (label) {
|
|
51
|
+
case "blocked":
|
|
52
|
+
return "red";
|
|
53
|
+
case "actionable":
|
|
54
|
+
return "yellow";
|
|
55
|
+
case "review":
|
|
56
|
+
return "cyan";
|
|
57
|
+
default:
|
|
58
|
+
return "green";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
function TuiApp({ updates, onComplete }) {
|
|
38
62
|
const [cursorIndex, setCursorIndex] = useState(0);
|
|
39
63
|
const [filterIndex, setFilterIndex] = useState(0);
|
|
@@ -90,12 +114,13 @@ function TuiApp({ updates, onComplete }) {
|
|
|
90
114
|
onComplete(updates.filter((_, index) => selectedIndices.has(index)));
|
|
91
115
|
}
|
|
92
116
|
});
|
|
93
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review Queue" }), _jsx(Text, { color: "gray", children: "Detect with check, summarize with doctor, decide here in review, then apply with upgrade." }), _jsx(Text, { color: "gray", children: "Left/Right filter Up/Down move Space toggle A select visible N clear Enter confirm" }), _jsx(Box, { marginTop: 1, children: FILTER_ORDER.map((filter, index) => (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { color: index === filterIndex ? "cyan" : "gray", children: ["[", filter, "]"] }) }, filter))) }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Box, { width: 72, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Review Queue" }), filteredIndices.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this filter." })) : (filteredIndices.map((index, visibleIndex) => {
|
|
94
118
|
const update = updates[index];
|
|
95
119
|
const isFocused = visibleIndex === boundedCursor;
|
|
96
120
|
const isSelected = selectedIndices.has(index);
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
const decision = decisionLabel(update);
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? ">" : " ", " ", isSelected ? "[x]" : "[ ]", " "] }), _jsx(Box, { width: 22, children: _jsx(Text, { bold: isFocused, children: update.name }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: diffColor(update.diffType), children: update.diffType }) }), _jsx(Box, { width: 18, children: _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? update.impactScore?.rank ?? "low" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: decisionColor(decision), children: decision }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: decisionColor(decision), children: typeof update.riskScore === "number" ? update.riskScore : "--" }) }), _jsx(VersionDiff, { from: update.fromRange, to: update.toVersionResolved })] }, `${update.packagePath}:${update.name}`));
|
|
123
|
+
}))] }), _jsxs(Box, { marginLeft: 1, width: 46, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Decision Panel" }), focusedUpdate ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: focusedUpdate.name }), _jsxs(Text, { color: "gray", children: ["package: ", focusedUpdate.packagePath] }), _jsxs(Text, { children: ["state:", " ", _jsx(Text, { color: decisionColor(decisionLabel(focusedUpdate)), children: decisionLabel(focusedUpdate) })] }), _jsxs(Text, { children: ["diff: ", _jsx(Text, { color: diffColor(focusedUpdate.diffType), children: focusedUpdate.diffType })] }), _jsxs(Text, { children: ["risk: ", _jsx(Text, { color: riskColor(focusedUpdate.riskLevel), children: focusedUpdate.riskLevel ?? focusedUpdate.impactScore?.rank ?? "low" })] }), _jsxs(Text, { children: ["risk score: ", focusedUpdate.riskScore ?? 0] }), _jsxs(Text, { children: ["impact score: ", focusedUpdate.impactScore?.score ?? 0] }), _jsxs(Text, { children: ["advisories: ", focusedUpdate.advisoryCount ?? 0] }), _jsxs(Text, { children: ["peer: ", focusedUpdate.peerConflictSeverity ?? "none"] }), _jsxs(Text, { children: ["license: ", focusedUpdate.licenseStatus ?? "allowed"] }), _jsxs(Text, { children: ["health: ", focusedUpdate.healthStatus ?? "healthy"] }), _jsxs(Text, { children: ["action:", " ", _jsx(Text, { color: decisionColor(decisionLabel(focusedUpdate)), children: focusedUpdate.recommendedAction ?? "Safe to keep in the review queue." })] }), focusedUpdate.homepage ? (_jsxs(Text, { color: "blue", children: ["homepage: ", focusedUpdate.homepage] })) : (_jsx(Text, { color: "gray", children: "homepage: unavailable" })), focusedUpdate.riskReasons && focusedUpdate.riskReasons.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Reasons" }), focusedUpdate.riskReasons.slice(0, 4).map((reason) => (_jsxs(Text, { color: "gray", children: ["- ", reason] }, reason)))] })) : (_jsx(Text, { color: "gray", children: "No elevated risk reasons." }))] })) : (_jsx(Text, { color: "gray", children: "No review candidate selected." }))] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { color: "gray", children: [selectedIndices.size, " selected for apply of ", updates.length, ". Filter: ", activeFilter, ". Enter confirms the review decision set."] }) })] }));
|
|
99
124
|
}
|
|
100
125
|
export async function runTui(updates) {
|
|
101
126
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainy-updates/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and automates npm/pnpm dependencies in CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -56,6 +56,11 @@
|
|
|
56
56
|
"test": "bun test",
|
|
57
57
|
"lint": "bunx biome check src tests",
|
|
58
58
|
"check": "bun run typecheck && bun test",
|
|
59
|
+
"bench:fixtures": "node scripts/generate-benchmark-fixtures.mjs",
|
|
60
|
+
"bench:check": "bun run build && bun run bench:fixtures && node scripts/benchmark.mjs single-100 check cold 3 && node scripts/benchmark.mjs single-100 check warm 3",
|
|
61
|
+
"bench:review": "bun run build && bun run bench:fixtures && node scripts/benchmark.mjs single-100 review cold 3 && node scripts/benchmark.mjs single-100 review warm 3",
|
|
62
|
+
"bench:resolve": "bun run build && bun run bench:fixtures && node scripts/benchmark.mjs single-100 resolve cold 3 && node scripts/benchmark.mjs single-100 resolve warm 3",
|
|
63
|
+
"bench:ci": "bun run build && bun run bench:fixtures && node scripts/benchmark.mjs mono-1000 ci cold 3 && node scripts/benchmark.mjs mono-1000 ci warm 3",
|
|
59
64
|
"perf:smoke": "node scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=resolve node scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=ci node scripts/perf-smoke.mjs",
|
|
60
65
|
"perf:check": "node scripts/perf-smoke.mjs",
|
|
61
66
|
"perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve node scripts/perf-smoke.mjs",
|