@rainy-updates/cli 0.5.2-rc.2 → 0.5.3
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 +107 -0
- package/README.md +105 -22
- package/dist/bin/cli.js +124 -9
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +9 -2
- package/dist/commands/audit/runner.js +8 -1
- package/dist/commands/audit/sources/index.js +8 -1
- package/dist/commands/doctor/parser.d.ts +2 -0
- package/dist/commands/doctor/parser.js +92 -0
- package/dist/commands/doctor/runner.d.ts +2 -0
- package/dist/commands/doctor/runner.js +13 -0
- package/dist/commands/resolve/runner.js +3 -0
- package/dist/commands/review/parser.d.ts +2 -0
- package/dist/commands/review/parser.js +174 -0
- package/dist/commands/review/runner.d.ts +2 -0
- package/dist/commands/review/runner.js +30 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/core/check.js +64 -5
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +6 -0
- package/dist/core/options.d.ts +8 -1
- package/dist/core/options.js +43 -0
- package/dist/core/review-model.d.ts +5 -0
- package/dist/core/review-model.js +372 -0
- package/dist/core/summary.js +11 -2
- package/dist/core/upgrade.d.ts +1 -0
- package/dist/core/upgrade.js +27 -21
- package/dist/core/warm-cache.js +28 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/output/format.d.ts +4 -1
- package/dist/output/format.js +41 -3
- package/dist/output/github.js +6 -1
- package/dist/output/sarif.js +14 -0
- package/dist/registry/npm.d.ts +22 -0
- package/dist/registry/npm.js +33 -4
- 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 +85 -0
- package/dist/ui/tui.d.ts +0 -4
- package/dist/ui/tui.js +103 -21
- package/package.json +10 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
export function parseDoctorArgs(args) {
|
|
4
|
+
const options = {
|
|
5
|
+
cwd: process.cwd(),
|
|
6
|
+
target: "latest",
|
|
7
|
+
filter: undefined,
|
|
8
|
+
reject: undefined,
|
|
9
|
+
cacheTtlSeconds: 3600,
|
|
10
|
+
includeKinds: ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"],
|
|
11
|
+
ci: false,
|
|
12
|
+
format: "table",
|
|
13
|
+
workspace: false,
|
|
14
|
+
jsonFile: undefined,
|
|
15
|
+
githubOutputFile: undefined,
|
|
16
|
+
sarifFile: undefined,
|
|
17
|
+
concurrency: 16,
|
|
18
|
+
registryTimeoutMs: 8000,
|
|
19
|
+
registryRetries: 3,
|
|
20
|
+
offline: false,
|
|
21
|
+
stream: false,
|
|
22
|
+
policyFile: undefined,
|
|
23
|
+
prReportFile: undefined,
|
|
24
|
+
failOn: "none",
|
|
25
|
+
maxUpdates: undefined,
|
|
26
|
+
fixPr: false,
|
|
27
|
+
fixBranch: "chore/rainy-updates",
|
|
28
|
+
fixCommitMessage: undefined,
|
|
29
|
+
fixDryRun: false,
|
|
30
|
+
fixPrNoCheckout: false,
|
|
31
|
+
fixPrBatchSize: undefined,
|
|
32
|
+
noPrReport: false,
|
|
33
|
+
logLevel: "info",
|
|
34
|
+
groupBy: "risk",
|
|
35
|
+
groupMax: undefined,
|
|
36
|
+
cooldownDays: undefined,
|
|
37
|
+
prLimit: undefined,
|
|
38
|
+
onlyChanged: false,
|
|
39
|
+
ciProfile: "minimal",
|
|
40
|
+
lockfileMode: "preserve",
|
|
41
|
+
interactive: false,
|
|
42
|
+
showImpact: true,
|
|
43
|
+
showHomepage: true,
|
|
44
|
+
verdictOnly: false,
|
|
45
|
+
};
|
|
46
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
47
|
+
const current = args[i];
|
|
48
|
+
const next = args[i + 1];
|
|
49
|
+
if (current === "--cwd" && next) {
|
|
50
|
+
options.cwd = path.resolve(next);
|
|
51
|
+
i += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (current === "--cwd")
|
|
55
|
+
throw new Error("Missing value for --cwd");
|
|
56
|
+
if (current === "--workspace") {
|
|
57
|
+
options.workspace = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (current === "--verdict-only") {
|
|
61
|
+
options.verdictOnly = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (current === "--json-file" && next) {
|
|
65
|
+
options.jsonFile = path.resolve(options.cwd, next);
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (current === "--json-file")
|
|
70
|
+
throw new Error("Missing value for --json-file");
|
|
71
|
+
if (current === "--help" || current === "-h") {
|
|
72
|
+
process.stdout.write(DOCTOR_HELP);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (current.startsWith("-"))
|
|
76
|
+
throw new Error(`Unknown doctor option: ${current}`);
|
|
77
|
+
throw new Error(`Unexpected doctor argument: ${current}`);
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
const DOCTOR_HELP = `
|
|
82
|
+
rup doctor — Fast dependency verdict across updates, security, policy, and peer conflicts
|
|
83
|
+
|
|
84
|
+
Usage:
|
|
85
|
+
rup doctor [options]
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--verdict-only Print the 3-line quick verdict without counts
|
|
89
|
+
--workspace Scan all workspace packages
|
|
90
|
+
--json-file <path> Write JSON doctor report to file
|
|
91
|
+
--cwd <path>
|
|
92
|
+
`.trimStart();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { buildReviewResult, createDoctorResult, renderDoctorResult } from "../../core/review-model.js";
|
|
3
|
+
import { stableStringify } from "../../utils/stable-json.js";
|
|
4
|
+
import { writeFileAtomic } from "../../utils/io.js";
|
|
5
|
+
export async function runDoctor(options) {
|
|
6
|
+
const review = await buildReviewResult(options);
|
|
7
|
+
const doctor = createDoctorResult(review);
|
|
8
|
+
process.stdout.write(renderDoctorResult(doctor, options.verdictOnly) + "\n");
|
|
9
|
+
if (options.jsonFile) {
|
|
10
|
+
await writeFileAtomic(options.jsonFile, stableStringify(doctor, 2) + "\n");
|
|
11
|
+
}
|
|
12
|
+
return doctor;
|
|
13
|
+
}
|
|
@@ -93,6 +93,9 @@ async function fetchProposedVersions(options) {
|
|
|
93
93
|
onlyChanged: false,
|
|
94
94
|
ciProfile: "minimal",
|
|
95
95
|
lockfileMode: "preserve",
|
|
96
|
+
interactive: false,
|
|
97
|
+
showImpact: false,
|
|
98
|
+
showHomepage: false,
|
|
96
99
|
});
|
|
97
100
|
for (const update of checkResult.updates ?? []) {
|
|
98
101
|
overrides.set(update.name, update.toVersionResolved);
|
|
@@ -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,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
|
+
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/check.js
CHANGED
|
@@ -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(
|
|
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,8 @@ export async function check(options) {
|
|
|
85
93
|
latestVersion: cached.latestVersion,
|
|
86
94
|
availableVersions: cached.availableVersions,
|
|
87
95
|
publishedAtByVersion: {},
|
|
96
|
+
installScriptByVersion: {},
|
|
97
|
+
maintainerCount: null,
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
100
|
else {
|
|
@@ -102,11 +112,19 @@ export async function check(options) {
|
|
|
102
112
|
latestVersion: stale.latestVersion,
|
|
103
113
|
availableVersions: stale.availableVersions,
|
|
104
114
|
publishedAtByVersion: {},
|
|
115
|
+
installScriptByVersion: {},
|
|
116
|
+
maintainerCount: null,
|
|
105
117
|
});
|
|
106
118
|
warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
|
|
107
119
|
}
|
|
108
120
|
else {
|
|
109
|
-
errors.push(
|
|
121
|
+
errors.push(formatClassifiedMessage({
|
|
122
|
+
code: "REGISTRY_ERROR",
|
|
123
|
+
whatFailed: `Offline cache miss for ${packageName}.`,
|
|
124
|
+
intact: "Local manifests and previously cached packages remain unchanged.",
|
|
125
|
+
validity: "invalid",
|
|
126
|
+
next: `Run \`rup warm-cache --cwd ${options.cwd}\` or retry without --offline.`,
|
|
127
|
+
}));
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
130
|
cacheMs += Date.now() - cacheFallbackStartedAt;
|
|
@@ -125,6 +143,10 @@ export async function check(options) {
|
|
|
125
143
|
latestVersion: metadata.latestVersion,
|
|
126
144
|
availableVersions: metadata.versions,
|
|
127
145
|
publishedAtByVersion: metadata.publishedAtByVersion,
|
|
146
|
+
homepage: metadata.homepage,
|
|
147
|
+
repository: metadata.repository,
|
|
148
|
+
installScriptByVersion: metadata.installScriptByVersion,
|
|
149
|
+
maintainerCount: metadata.maintainerCount,
|
|
128
150
|
});
|
|
129
151
|
if (metadata.latestVersion) {
|
|
130
152
|
await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
@@ -139,12 +161,25 @@ export async function check(options) {
|
|
|
139
161
|
latestVersion: stale.latestVersion,
|
|
140
162
|
availableVersions: stale.availableVersions,
|
|
141
163
|
publishedAtByVersion: {},
|
|
164
|
+
installScriptByVersion: {},
|
|
165
|
+
maintainerCount: null,
|
|
142
166
|
});
|
|
143
167
|
warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
|
|
144
168
|
}
|
|
145
169
|
else {
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
const classified = formatClassifiedMessage({
|
|
171
|
+
code: error.includes("401") || error.includes("403")
|
|
172
|
+
? "AUTH_ERROR"
|
|
173
|
+
: "REGISTRY_ERROR",
|
|
174
|
+
whatFailed: `Unable to resolve ${packageName}: ${error}.`,
|
|
175
|
+
intact: "Other package results and local files remain intact.",
|
|
176
|
+
validity: "partial",
|
|
177
|
+
next: error.includes("401") || error.includes("403")
|
|
178
|
+
? "Check .npmrc scoped registry credentials and retry."
|
|
179
|
+
: "Retry `rup check` or warm the cache before offline runs.",
|
|
180
|
+
});
|
|
181
|
+
errors.push(classified);
|
|
182
|
+
emitStream(`[error] ${classified}`);
|
|
148
183
|
}
|
|
149
184
|
}
|
|
150
185
|
cacheMs += Date.now() - cacheStaleStartedAt;
|
|
@@ -182,10 +217,23 @@ export async function check(options) {
|
|
|
182
217
|
filtered: false,
|
|
183
218
|
autofix: rule?.autofix !== false,
|
|
184
219
|
reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
|
|
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]),
|
|
185
232
|
});
|
|
186
233
|
emitStream(`[update] ${task.dependency.name} ${task.dependency.range} -> ${nextRange} (${classifyDiff(task.dependency.range, picked)})`);
|
|
187
234
|
}
|
|
188
|
-
const
|
|
235
|
+
const scoredUpdates = applyImpactScores(updates);
|
|
236
|
+
const grouped = groupUpdates(scoredUpdates, options.groupBy);
|
|
189
237
|
const groupedUpdates = grouped.length;
|
|
190
238
|
const groupedSorted = sortUpdates(grouped.flatMap((group) => group.items));
|
|
191
239
|
const groupedCapped = typeof options.groupMax === "number" ? groupedSorted.slice(0, options.groupMax) : groupedSorted;
|
|
@@ -219,6 +267,7 @@ export async function check(options) {
|
|
|
219
267
|
policyOverridesApplied,
|
|
220
268
|
}));
|
|
221
269
|
summary.streamedEvents = streamedEvents;
|
|
270
|
+
summary.riskPackages = limitedUpdates.filter((item) => item.impactScore?.rank === "critical" || item.impactScore?.rank === "high").length;
|
|
222
271
|
return {
|
|
223
272
|
projectPath: options.cwd,
|
|
224
273
|
packagePaths: packageDirs,
|
|
@@ -231,6 +280,16 @@ export async function check(options) {
|
|
|
231
280
|
warnings: sortedWarnings,
|
|
232
281
|
};
|
|
233
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
|
+
}
|
|
234
293
|
function groupUpdates(updates, groupBy) {
|
|
235
294
|
if (updates.length === 0) {
|
|
236
295
|
return [];
|
|
@@ -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;
|
package/dist/core/options.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/options.js
CHANGED
|
@@ -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;
|