@rainy-updates/cli 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/bin/cli.js +9 -467
- package/dist/bin/dispatch.d.ts +16 -0
- package/dist/bin/dispatch.js +150 -0
- package/dist/bin/help.d.ts +1 -0
- package/dist/bin/help.js +284 -0
- package/dist/commands/doctor/parser.js +6 -0
- package/dist/commands/doctor/runner.js +5 -2
- package/dist/core/analysis/options.d.ts +6 -0
- package/dist/core/analysis/options.js +69 -0
- package/dist/core/analysis/review-items.d.ts +4 -0
- package/dist/core/analysis/review-items.js +128 -0
- package/dist/core/analysis/run-silenced.d.ts +1 -0
- package/dist/core/analysis/run-silenced.js +14 -0
- package/dist/core/analysis-bundle.js +3 -211
- package/dist/core/doctor/findings.d.ts +2 -0
- package/dist/core/doctor/findings.js +166 -0
- package/dist/core/doctor/render.d.ts +3 -0
- package/dist/core/doctor/render.js +44 -0
- package/dist/core/doctor/result.d.ts +2 -0
- package/dist/core/doctor/result.js +55 -0
- package/dist/core/doctor/score.d.ts +5 -0
- package/dist/core/doctor/score.js +28 -0
- package/dist/core/review-model.d.ts +3 -3
- package/dist/core/review-model.js +4 -68
- package/dist/core/review-verdict.d.ts +2 -0
- package/dist/core/review-verdict.js +14 -0
- package/dist/core/summary.js +6 -0
- package/dist/output/format.js +7 -0
- package/dist/output/github.js +4 -0
- package/dist/output/sarif.js +4 -0
- package/dist/types/index.d.ts +28 -0
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { check } from "../core/check.js";
|
|
3
|
+
import { upgrade } from "../core/upgrade.js";
|
|
4
|
+
import { warmCache } from "../core/warm-cache.js";
|
|
5
|
+
import { runCi } from "../core/ci.js";
|
|
6
|
+
import { initCiWorkflow } from "../core/init-ci.js";
|
|
7
|
+
import { diffBaseline, saveBaseline } from "../core/baseline.js";
|
|
8
|
+
export async function handleDirectCommand(parsed) {
|
|
9
|
+
if (parsed.command === "init-ci") {
|
|
10
|
+
const workflow = await initCiWorkflow(parsed.options.cwd, parsed.options.force, {
|
|
11
|
+
mode: parsed.options.mode,
|
|
12
|
+
schedule: parsed.options.schedule,
|
|
13
|
+
});
|
|
14
|
+
process.stdout.write(workflow.created
|
|
15
|
+
? `Created CI workflow at ${workflow.path}\n`
|
|
16
|
+
: `CI workflow already exists at ${workflow.path}. Use --force to overwrite.\n`);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (parsed.command === "baseline") {
|
|
20
|
+
if (parsed.options.action === "save") {
|
|
21
|
+
const saved = await saveBaseline(parsed.options);
|
|
22
|
+
process.stdout.write(`Saved baseline at ${saved.filePath} (${saved.entries} entries)\n`);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const diff = await diffBaseline(parsed.options);
|
|
26
|
+
const changes = diff.added.length + diff.removed.length + diff.changed.length;
|
|
27
|
+
if (changes === 0) {
|
|
28
|
+
process.stdout.write(`No baseline drift detected (${diff.filePath}).\n`);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(`Baseline drift detected (${diff.filePath}).\n`);
|
|
32
|
+
if (diff.added.length > 0)
|
|
33
|
+
process.stdout.write(`Added: ${diff.added.length}\n`);
|
|
34
|
+
if (diff.removed.length > 0)
|
|
35
|
+
process.stdout.write(`Removed: ${diff.removed.length}\n`);
|
|
36
|
+
if (diff.changed.length > 0)
|
|
37
|
+
process.stdout.write(`Changed: ${diff.changed.length}\n`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (parsed.command === "bisect") {
|
|
42
|
+
const { runBisect } = await import("../commands/bisect/runner.js");
|
|
43
|
+
const result = await runBisect(parsed.options);
|
|
44
|
+
process.exitCode = result.breakingVersion ? 1 : 0;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (parsed.command === "audit") {
|
|
48
|
+
const { runAudit } = await import("../commands/audit/runner.js");
|
|
49
|
+
const result = await runAudit(parsed.options);
|
|
50
|
+
process.exitCode = result.advisories.length > 0 ? 1 : 0;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.command === "health") {
|
|
54
|
+
const { runHealth } = await import("../commands/health/runner.js");
|
|
55
|
+
const result = await runHealth(parsed.options);
|
|
56
|
+
process.exitCode = result.totalFlagged > 0 ? 1 : 0;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.command === "unused") {
|
|
60
|
+
const { runUnused } = await import("../commands/unused/runner.js");
|
|
61
|
+
const result = await runUnused(parsed.options);
|
|
62
|
+
process.exitCode = result.totalUnused > 0 || result.totalMissing > 0 ? 1 : 0;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (parsed.command === "resolve") {
|
|
66
|
+
const { runResolve } = await import("../commands/resolve/runner.js");
|
|
67
|
+
const result = await runResolve(parsed.options);
|
|
68
|
+
process.exitCode = result.errorConflicts > 0 ? 1 : 0;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (parsed.command === "licenses") {
|
|
72
|
+
const { runLicenses } = await import("../commands/licenses/runner.js");
|
|
73
|
+
const result = await runLicenses(parsed.options);
|
|
74
|
+
process.exitCode = result.totalViolations > 0 ? 1 : 0;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (parsed.command === "snapshot") {
|
|
78
|
+
const { runSnapshot } = await import("../commands/snapshot/runner.js");
|
|
79
|
+
const result = await runSnapshot(parsed.options);
|
|
80
|
+
process.exitCode = result.errors.length > 0 ? 1 : 0;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (parsed.command === "review") {
|
|
84
|
+
const { runReview } = await import("../commands/review/runner.js");
|
|
85
|
+
const result = await runReview(parsed.options);
|
|
86
|
+
process.exitCode =
|
|
87
|
+
result.summary.verdict === "blocked" ||
|
|
88
|
+
result.summary.verdict === "actionable" ||
|
|
89
|
+
result.summary.verdict === "review"
|
|
90
|
+
? 1
|
|
91
|
+
: 0;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.command === "doctor") {
|
|
95
|
+
const { runDoctor } = await import("../commands/doctor/runner.js");
|
|
96
|
+
const result = await runDoctor(parsed.options);
|
|
97
|
+
process.exitCode = result.verdict === "safe" ? 0 : 1;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (parsed.command === "dashboard") {
|
|
101
|
+
const { runDashboard } = await import("../commands/dashboard/runner.js");
|
|
102
|
+
const result = await runDashboard(parsed.options);
|
|
103
|
+
process.exitCode = result.errors.length > 0 ? 1 : 0;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (parsed.command === "ga") {
|
|
107
|
+
const { runGa } = await import("../commands/ga/runner.js");
|
|
108
|
+
const result = await runGa(parsed.options);
|
|
109
|
+
process.exitCode = result.ready ? 0 : 1;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (parsed.options.interactive &&
|
|
113
|
+
(parsed.command === "check" ||
|
|
114
|
+
parsed.command === "upgrade" ||
|
|
115
|
+
parsed.command === "ci")) {
|
|
116
|
+
const { runReview } = await import("../commands/review/runner.js");
|
|
117
|
+
const result = await runReview({
|
|
118
|
+
...parsed.options,
|
|
119
|
+
securityOnly: false,
|
|
120
|
+
risk: undefined,
|
|
121
|
+
diff: undefined,
|
|
122
|
+
applySelected: parsed.command === "upgrade",
|
|
123
|
+
});
|
|
124
|
+
process.exitCode =
|
|
125
|
+
result.summary.verdict === "safe" && result.updates.length === 0 ? 0 : 1;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
export async function runPrimaryCommand(parsed) {
|
|
131
|
+
if (parsed.command === "upgrade") {
|
|
132
|
+
return upgrade(parsed.options);
|
|
133
|
+
}
|
|
134
|
+
if (parsed.command === "warm-cache") {
|
|
135
|
+
return warmCache(parsed.options);
|
|
136
|
+
}
|
|
137
|
+
if (parsed.command === "ci") {
|
|
138
|
+
return runCi(parsed.options);
|
|
139
|
+
}
|
|
140
|
+
if (parsed.options.fixPr) {
|
|
141
|
+
const upgradeOptions = {
|
|
142
|
+
...parsed.options,
|
|
143
|
+
install: false,
|
|
144
|
+
packageManager: "auto",
|
|
145
|
+
sync: false,
|
|
146
|
+
};
|
|
147
|
+
return upgrade(upgradeOptions);
|
|
148
|
+
}
|
|
149
|
+
return check(parsed.options);
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderHelp(command?: string): string;
|
package/dist/bin/help.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
export function renderHelp(command) {
|
|
2
|
+
const isCommand = command && !command.startsWith("-");
|
|
3
|
+
if (isCommand && command === "check") {
|
|
4
|
+
return `rainy-updates check [options]
|
|
5
|
+
|
|
6
|
+
Detect candidate dependency updates. This is the first step in the flow:
|
|
7
|
+
check detects
|
|
8
|
+
doctor summarizes
|
|
9
|
+
review decides
|
|
10
|
+
upgrade applies
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--workspace
|
|
14
|
+
--target patch|minor|major|latest
|
|
15
|
+
--filter <pattern>
|
|
16
|
+
--reject <pattern>
|
|
17
|
+
--dep-kinds deps,dev,optional,peer
|
|
18
|
+
--concurrency <n>
|
|
19
|
+
--registry-timeout-ms <n>
|
|
20
|
+
--registry-retries <n>
|
|
21
|
+
--cache-ttl <seconds>
|
|
22
|
+
--stream
|
|
23
|
+
--policy-file <path>
|
|
24
|
+
--offline
|
|
25
|
+
--fix-pr
|
|
26
|
+
--fix-branch <name>
|
|
27
|
+
--fix-commit-message <text>
|
|
28
|
+
--fix-dry-run
|
|
29
|
+
--fix-pr-no-checkout
|
|
30
|
+
--fix-pr-batch-size <n>
|
|
31
|
+
--no-pr-report
|
|
32
|
+
--json-file <path>
|
|
33
|
+
--github-output <path>
|
|
34
|
+
--sarif-file <path>
|
|
35
|
+
--pr-report-file <path>
|
|
36
|
+
--fail-on none|patch|minor|major|any
|
|
37
|
+
--max-updates <n>
|
|
38
|
+
--group-by none|name|scope|kind|risk
|
|
39
|
+
--group-max <n>
|
|
40
|
+
--cooldown-days <n>
|
|
41
|
+
--pr-limit <n>
|
|
42
|
+
--only-changed
|
|
43
|
+
--interactive
|
|
44
|
+
--show-impact
|
|
45
|
+
--show-links
|
|
46
|
+
--show-homepage
|
|
47
|
+
--lockfile-mode preserve|update|error
|
|
48
|
+
--log-level error|warn|info|debug
|
|
49
|
+
--ci`;
|
|
50
|
+
}
|
|
51
|
+
if (isCommand && command === "warm-cache") {
|
|
52
|
+
return `rainy-updates warm-cache [options]
|
|
53
|
+
|
|
54
|
+
Pre-warm local metadata cache for faster CI checks.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--workspace
|
|
58
|
+
--target patch|minor|major|latest
|
|
59
|
+
--filter <pattern>
|
|
60
|
+
--reject <pattern>
|
|
61
|
+
--dep-kinds deps,dev,optional,peer
|
|
62
|
+
--concurrency <n>
|
|
63
|
+
--registry-timeout-ms <n>
|
|
64
|
+
--registry-retries <n>
|
|
65
|
+
--cache-ttl <seconds>
|
|
66
|
+
--offline
|
|
67
|
+
--stream
|
|
68
|
+
--json-file <path>
|
|
69
|
+
--github-output <path>
|
|
70
|
+
--sarif-file <path>
|
|
71
|
+
--pr-report-file <path>`;
|
|
72
|
+
}
|
|
73
|
+
if (isCommand && command === "upgrade") {
|
|
74
|
+
return `rainy-updates upgrade [options]
|
|
75
|
+
|
|
76
|
+
Apply an approved change set to package.json manifests.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--workspace
|
|
80
|
+
--sync
|
|
81
|
+
--install
|
|
82
|
+
--pm auto|npm|pnpm
|
|
83
|
+
--target patch|minor|major|latest
|
|
84
|
+
--policy-file <path>
|
|
85
|
+
--concurrency <n>
|
|
86
|
+
--registry-timeout-ms <n>
|
|
87
|
+
--registry-retries <n>
|
|
88
|
+
--fix-pr
|
|
89
|
+
--fix-branch <name>
|
|
90
|
+
--fix-commit-message <text>
|
|
91
|
+
--fix-dry-run
|
|
92
|
+
--fix-pr-no-checkout
|
|
93
|
+
--fix-pr-batch-size <n>
|
|
94
|
+
--interactive
|
|
95
|
+
--lockfile-mode preserve|update|error
|
|
96
|
+
--no-pr-report
|
|
97
|
+
--json-file <path>
|
|
98
|
+
--pr-report-file <path>`;
|
|
99
|
+
}
|
|
100
|
+
if (isCommand && command === "ci") {
|
|
101
|
+
return `rainy-updates ci [options]
|
|
102
|
+
|
|
103
|
+
Run CI-oriented automation around the same lifecycle:
|
|
104
|
+
check detects
|
|
105
|
+
doctor summarizes
|
|
106
|
+
review decides
|
|
107
|
+
upgrade applies
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
--workspace
|
|
111
|
+
--mode minimal|strict|enterprise
|
|
112
|
+
--group-by none|name|scope|kind|risk
|
|
113
|
+
--group-max <n>
|
|
114
|
+
--cooldown-days <n>
|
|
115
|
+
--pr-limit <n>
|
|
116
|
+
--only-changed
|
|
117
|
+
--offline
|
|
118
|
+
--concurrency <n>
|
|
119
|
+
--registry-timeout-ms <n>
|
|
120
|
+
--registry-retries <n>
|
|
121
|
+
--stream
|
|
122
|
+
--fix-pr
|
|
123
|
+
--fix-branch <name>
|
|
124
|
+
--fix-commit-message <text>
|
|
125
|
+
--fix-dry-run
|
|
126
|
+
--fix-pr-no-checkout
|
|
127
|
+
--fix-pr-batch-size <n>
|
|
128
|
+
--no-pr-report
|
|
129
|
+
--json-file <path>
|
|
130
|
+
--github-output <path>
|
|
131
|
+
--sarif-file <path>
|
|
132
|
+
--pr-report-file <path>
|
|
133
|
+
--fail-on none|patch|minor|major|any
|
|
134
|
+
--max-updates <n>
|
|
135
|
+
--lockfile-mode preserve|update|error
|
|
136
|
+
--log-level error|warn|info|debug
|
|
137
|
+
--ci`;
|
|
138
|
+
}
|
|
139
|
+
if (isCommand && command === "init-ci") {
|
|
140
|
+
return `rainy-updates init-ci [options]
|
|
141
|
+
|
|
142
|
+
Create a GitHub Actions workflow template at:
|
|
143
|
+
.github/workflows/rainy-updates.yml
|
|
144
|
+
|
|
145
|
+
Options:
|
|
146
|
+
--force
|
|
147
|
+
--mode minimal|strict|enterprise
|
|
148
|
+
--schedule weekly|daily|off`;
|
|
149
|
+
}
|
|
150
|
+
if (isCommand && command === "baseline") {
|
|
151
|
+
return `rainy-updates baseline [options]
|
|
152
|
+
|
|
153
|
+
Save or compare dependency baseline snapshots.
|
|
154
|
+
|
|
155
|
+
Options:
|
|
156
|
+
--save
|
|
157
|
+
--check
|
|
158
|
+
--file <path>
|
|
159
|
+
--workspace
|
|
160
|
+
--dep-kinds deps,dev,optional,peer
|
|
161
|
+
--ci`;
|
|
162
|
+
}
|
|
163
|
+
if (isCommand && command === "audit") {
|
|
164
|
+
return `rainy-updates audit [options]
|
|
165
|
+
|
|
166
|
+
Scan dependencies for CVEs using OSV.dev and GitHub Advisory Database.
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
--workspace
|
|
170
|
+
--severity critical|high|medium|low
|
|
171
|
+
--summary
|
|
172
|
+
--report table|summary|json
|
|
173
|
+
--source auto|osv|github|all
|
|
174
|
+
--fix
|
|
175
|
+
--dry-run
|
|
176
|
+
--commit
|
|
177
|
+
--pm auto|npm|pnpm|bun|yarn
|
|
178
|
+
--json-file <path>
|
|
179
|
+
--concurrency <n>
|
|
180
|
+
--registry-timeout-ms <n>`;
|
|
181
|
+
}
|
|
182
|
+
if (isCommand && command === "review") {
|
|
183
|
+
return `rainy-updates review [options]
|
|
184
|
+
|
|
185
|
+
Review is the decision center of Rainy Updates.
|
|
186
|
+
Use it to inspect risk, security, peer, license, and policy context before applying changes.
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
--workspace
|
|
190
|
+
--interactive
|
|
191
|
+
--security-only
|
|
192
|
+
--risk critical|high|medium|low
|
|
193
|
+
--diff patch|minor|major|latest
|
|
194
|
+
--apply-selected
|
|
195
|
+
--show-changelog
|
|
196
|
+
--policy-file <path>
|
|
197
|
+
--json-file <path>
|
|
198
|
+
--concurrency <n>
|
|
199
|
+
--registry-timeout-ms <n>
|
|
200
|
+
--registry-retries <n>`;
|
|
201
|
+
}
|
|
202
|
+
if (isCommand && command === "doctor") {
|
|
203
|
+
return `rainy-updates doctor [options]
|
|
204
|
+
|
|
205
|
+
Produce a fast summary verdict and point the operator to review when action is needed.
|
|
206
|
+
|
|
207
|
+
Options:
|
|
208
|
+
--workspace
|
|
209
|
+
--verdict-only
|
|
210
|
+
--include-changelog
|
|
211
|
+
--json-file <path>`;
|
|
212
|
+
}
|
|
213
|
+
if (isCommand && command === "ga") {
|
|
214
|
+
return `rainy-updates ga [options]
|
|
215
|
+
|
|
216
|
+
Audit release and CI readiness for Rainy Updates.
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
--workspace
|
|
220
|
+
--json-file <path>
|
|
221
|
+
--cwd <path>`;
|
|
222
|
+
}
|
|
223
|
+
return `rainy-updates (rup / rainy-up) <command> [options]
|
|
224
|
+
|
|
225
|
+
Commands:
|
|
226
|
+
check Detect candidate updates
|
|
227
|
+
doctor Summarize what matters
|
|
228
|
+
review Decide what to do
|
|
229
|
+
upgrade Apply the approved change set
|
|
230
|
+
dashboard Open the interactive DevOps dashboard (Ink TUI)
|
|
231
|
+
ci Run CI-focused orchestration
|
|
232
|
+
warm-cache Warm local cache for fast/offline checks
|
|
233
|
+
init-ci Scaffold GitHub Actions workflow
|
|
234
|
+
baseline Save/check dependency baseline snapshots
|
|
235
|
+
audit Scan dependencies for CVEs (OSV.dev + GitHub)
|
|
236
|
+
health Detect stale/deprecated/unmaintained packages
|
|
237
|
+
bisect Find which version of a dep introduced a failure
|
|
238
|
+
unused Detect unused or missing npm dependencies
|
|
239
|
+
resolve Check peer dependency conflicts (pure-TS, no subprocess)
|
|
240
|
+
licenses Scan dependency licenses and generate SPDX SBOM
|
|
241
|
+
snapshot Save, list, restore, and diff dependency state snapshots
|
|
242
|
+
ga Audit GA and CI readiness for this checkout
|
|
243
|
+
|
|
244
|
+
Global options:
|
|
245
|
+
--cwd <path>
|
|
246
|
+
--workspace
|
|
247
|
+
--target patch|minor|major|latest
|
|
248
|
+
--format table|json|minimal|github|metrics
|
|
249
|
+
--json-file <path>
|
|
250
|
+
--github-output <path>
|
|
251
|
+
--sarif-file <path>
|
|
252
|
+
--pr-report-file <path>
|
|
253
|
+
--policy-file <path>
|
|
254
|
+
--fail-on none|patch|minor|major|any
|
|
255
|
+
--max-updates <n>
|
|
256
|
+
--group-by none|name|scope|kind|risk
|
|
257
|
+
--group-max <n>
|
|
258
|
+
--cooldown-days <n>
|
|
259
|
+
--pr-limit <n>
|
|
260
|
+
--only-changed
|
|
261
|
+
--interactive
|
|
262
|
+
--show-impact
|
|
263
|
+
--show-links
|
|
264
|
+
--show-homepage
|
|
265
|
+
--mode minimal|strict|enterprise
|
|
266
|
+
--fix-pr
|
|
267
|
+
--fix-branch <name>
|
|
268
|
+
--fix-commit-message <text>
|
|
269
|
+
--fix-dry-run
|
|
270
|
+
--fix-pr-no-checkout
|
|
271
|
+
--fix-pr-batch-size <n>
|
|
272
|
+
--no-pr-report
|
|
273
|
+
--log-level error|warn|info|debug
|
|
274
|
+
--concurrency <n>
|
|
275
|
+
--registry-timeout-ms <n>
|
|
276
|
+
--registry-retries <n>
|
|
277
|
+
--cache-ttl <seconds>
|
|
278
|
+
--offline
|
|
279
|
+
--stream
|
|
280
|
+
--lockfile-mode preserve|update|error
|
|
281
|
+
--ci
|
|
282
|
+
--help, -h
|
|
283
|
+
--version, -v`;
|
|
284
|
+
}
|
|
@@ -43,6 +43,7 @@ export function parseDoctorArgs(args) {
|
|
|
43
43
|
showHomepage: true,
|
|
44
44
|
verdictOnly: false,
|
|
45
45
|
includeChangelog: false,
|
|
46
|
+
agentReport: false,
|
|
46
47
|
};
|
|
47
48
|
for (let i = 0; i < args.length; i += 1) {
|
|
48
49
|
const current = args[i];
|
|
@@ -66,6 +67,10 @@ export function parseDoctorArgs(args) {
|
|
|
66
67
|
options.includeChangelog = true;
|
|
67
68
|
continue;
|
|
68
69
|
}
|
|
70
|
+
if (current === "--agent-report") {
|
|
71
|
+
options.agentReport = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
69
74
|
if (current === "--json-file" && next) {
|
|
70
75
|
options.jsonFile = path.resolve(options.cwd, next);
|
|
71
76
|
i += 1;
|
|
@@ -92,6 +97,7 @@ Usage:
|
|
|
92
97
|
Options:
|
|
93
98
|
--verdict-only Print the 3-line quick verdict without counts
|
|
94
99
|
--include-changelog Include release note summaries in the aggregated review data
|
|
100
|
+
--agent-report Print a prompt-ready remediation report for coding agents
|
|
95
101
|
--workspace Scan all workspace packages
|
|
96
102
|
--json-file <path> Write JSON doctor report to file
|
|
97
103
|
--cwd <path>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { buildReviewResult, createDoctorResult, renderDoctorResult } from "../../core/review-model.js";
|
|
2
|
+
import { buildReviewResult, createDoctorResult, renderDoctorAgentReport, renderDoctorResult, } from "../../core/review-model.js";
|
|
3
3
|
import { stableStringify } from "../../utils/stable-json.js";
|
|
4
4
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
5
5
|
export async function runDoctor(options) {
|
|
6
6
|
const review = await buildReviewResult(options);
|
|
7
7
|
const doctor = createDoctorResult(review);
|
|
8
|
-
|
|
8
|
+
const output = options.agentReport
|
|
9
|
+
? renderDoctorAgentReport(doctor)
|
|
10
|
+
: renderDoctorResult(doctor, options.verdictOnly);
|
|
11
|
+
process.stdout.write(output + "\n");
|
|
9
12
|
if (options.jsonFile) {
|
|
10
13
|
await writeFileAtomic(options.jsonFile, stableStringify(doctor, 2) + "\n");
|
|
11
14
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuditOptions, CheckOptions, HealthOptions, LicenseOptions, ResolveOptions, UnusedOptions } from "../../types/index.js";
|
|
2
|
+
export declare function toAuditOptions(options: CheckOptions): AuditOptions;
|
|
3
|
+
export declare function toResolveOptions(options: CheckOptions): ResolveOptions;
|
|
4
|
+
export declare function toHealthOptions(options: CheckOptions): HealthOptions;
|
|
5
|
+
export declare function toLicenseOptions(options: CheckOptions): LicenseOptions;
|
|
6
|
+
export declare function toUnusedOptions(options: CheckOptions): UnusedOptions;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function toAuditOptions(options) {
|
|
2
|
+
return {
|
|
3
|
+
cwd: options.cwd,
|
|
4
|
+
workspace: options.workspace,
|
|
5
|
+
severity: undefined,
|
|
6
|
+
fix: false,
|
|
7
|
+
dryRun: true,
|
|
8
|
+
commit: false,
|
|
9
|
+
packageManager: "auto",
|
|
10
|
+
reportFormat: "json",
|
|
11
|
+
sourceMode: "auto",
|
|
12
|
+
jsonFile: undefined,
|
|
13
|
+
concurrency: options.concurrency,
|
|
14
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
15
|
+
silent: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function toResolveOptions(options) {
|
|
19
|
+
return {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
workspace: options.workspace,
|
|
22
|
+
afterUpdate: true,
|
|
23
|
+
safe: false,
|
|
24
|
+
jsonFile: undefined,
|
|
25
|
+
concurrency: options.concurrency,
|
|
26
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
27
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
28
|
+
silent: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function toHealthOptions(options) {
|
|
32
|
+
return {
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
workspace: options.workspace,
|
|
35
|
+
staleDays: 365,
|
|
36
|
+
includeDeprecated: true,
|
|
37
|
+
includeAlternatives: false,
|
|
38
|
+
reportFormat: "json",
|
|
39
|
+
jsonFile: undefined,
|
|
40
|
+
concurrency: options.concurrency,
|
|
41
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function toLicenseOptions(options) {
|
|
45
|
+
return {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
workspace: options.workspace,
|
|
48
|
+
allow: undefined,
|
|
49
|
+
deny: undefined,
|
|
50
|
+
sbomFile: undefined,
|
|
51
|
+
jsonFile: undefined,
|
|
52
|
+
diffMode: false,
|
|
53
|
+
concurrency: options.concurrency,
|
|
54
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
55
|
+
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function toUnusedOptions(options) {
|
|
59
|
+
return {
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
workspace: options.workspace,
|
|
62
|
+
srcDirs: ["src", "."],
|
|
63
|
+
includeDevDependencies: true,
|
|
64
|
+
fix: false,
|
|
65
|
+
dryRun: true,
|
|
66
|
+
jsonFile: undefined,
|
|
67
|
+
concurrency: options.concurrency,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisBundle, AuditResult, PackageUpdate, ResolveResult, ReviewItem, UnusedResult } from "../../types/index.js";
|
|
2
|
+
export declare function buildReviewItems(updates: PackageUpdate[], auditResult: AuditResult, resolveResult: ResolveResult, healthResult: AnalysisBundle["health"], licenseResult: AnalysisBundle["licenses"], unusedResult: UnusedResult, config: {
|
|
3
|
+
includeChangelog?: boolean;
|
|
4
|
+
}): Promise<ReviewItem[]>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { fetchChangelog } from "../../commands/changelog/fetcher.js";
|
|
2
|
+
import { applyRiskAssessments } from "../../risk/index.js";
|
|
3
|
+
import { applyImpactScores } from "../impact.js";
|
|
4
|
+
export async function buildReviewItems(updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config) {
|
|
5
|
+
const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
|
|
6
|
+
const impactedUpdates = applyImpactScores(updates, {
|
|
7
|
+
advisoryPackages,
|
|
8
|
+
workspaceDependentCount: (name) => updates.filter((item) => item.name === name).length,
|
|
9
|
+
});
|
|
10
|
+
const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
|
|
11
|
+
const advisoriesByName = new Map();
|
|
12
|
+
const conflictsByName = new Map();
|
|
13
|
+
const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
|
|
14
|
+
const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
|
|
15
|
+
const unusedByName = new Map();
|
|
16
|
+
for (const advisory of auditResult.advisories) {
|
|
17
|
+
const list = advisoriesByName.get(advisory.packageName) ?? [];
|
|
18
|
+
list.push(advisory);
|
|
19
|
+
advisoriesByName.set(advisory.packageName, list);
|
|
20
|
+
}
|
|
21
|
+
for (const conflict of resolveResult.conflicts) {
|
|
22
|
+
const list = conflictsByName.get(conflict.requester) ?? [];
|
|
23
|
+
list.push(conflict);
|
|
24
|
+
conflictsByName.set(conflict.requester, list);
|
|
25
|
+
const peerList = conflictsByName.get(conflict.peer) ?? [];
|
|
26
|
+
peerList.push(conflict);
|
|
27
|
+
conflictsByName.set(conflict.peer, peerList);
|
|
28
|
+
}
|
|
29
|
+
for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
|
|
30
|
+
const list = unusedByName.get(issue.name) ?? [];
|
|
31
|
+
list.push(issue);
|
|
32
|
+
unusedByName.set(issue.name, list);
|
|
33
|
+
}
|
|
34
|
+
const enrichedUpdates = await maybeAttachReleaseNotes(impactedUpdates, Boolean(config.includeChangelog));
|
|
35
|
+
return applyRiskAssessments(enrichedUpdates.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName)), {
|
|
36
|
+
knownPackageNames: new Set(updates.map((item) => item.name)),
|
|
37
|
+
}).map((item) => ({
|
|
38
|
+
...item,
|
|
39
|
+
update: {
|
|
40
|
+
...item.update,
|
|
41
|
+
policyAction: derivePolicyAction(item),
|
|
42
|
+
decisionState: deriveDecisionState(item),
|
|
43
|
+
selectedByDefault: deriveDecisionState(item) !== "blocked",
|
|
44
|
+
blockedReason: deriveDecisionState(item) === "blocked"
|
|
45
|
+
? item.update.recommendedAction
|
|
46
|
+
: undefined,
|
|
47
|
+
monitorReason: item.update.healthStatus === "stale" ? "Package health should be monitored." : undefined,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
|
|
52
|
+
const advisories = advisoriesByName.get(update.name) ?? [];
|
|
53
|
+
const peerConflicts = conflictsByName.get(update.name) ?? [];
|
|
54
|
+
const health = healthByName.get(update.name);
|
|
55
|
+
const license = licenseByName.get(update.name);
|
|
56
|
+
const unusedIssues = unusedByName.get(update.name) ?? [];
|
|
57
|
+
return {
|
|
58
|
+
update: {
|
|
59
|
+
...update,
|
|
60
|
+
advisoryCount: advisories.length,
|
|
61
|
+
peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
|
|
62
|
+
? "error"
|
|
63
|
+
: peerConflicts.length > 0
|
|
64
|
+
? "warning"
|
|
65
|
+
: "none",
|
|
66
|
+
licenseStatus: licenseViolationNames.has(update.name)
|
|
67
|
+
? "denied"
|
|
68
|
+
: license
|
|
69
|
+
? "allowed"
|
|
70
|
+
: "review",
|
|
71
|
+
healthStatus: health?.flags[0] ?? "healthy",
|
|
72
|
+
},
|
|
73
|
+
advisories,
|
|
74
|
+
health,
|
|
75
|
+
peerConflicts,
|
|
76
|
+
license,
|
|
77
|
+
unusedIssues,
|
|
78
|
+
selected: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function derivePolicyAction(item) {
|
|
82
|
+
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
83
|
+
return "block";
|
|
84
|
+
}
|
|
85
|
+
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
86
|
+
return "review";
|
|
87
|
+
}
|
|
88
|
+
if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
|
|
89
|
+
return "monitor";
|
|
90
|
+
}
|
|
91
|
+
return "allow";
|
|
92
|
+
}
|
|
93
|
+
function deriveDecisionState(item) {
|
|
94
|
+
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
95
|
+
return "blocked";
|
|
96
|
+
}
|
|
97
|
+
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
98
|
+
return "actionable";
|
|
99
|
+
}
|
|
100
|
+
if (item.update.riskLevel === "high" || item.update.diffType === "major") {
|
|
101
|
+
return "review";
|
|
102
|
+
}
|
|
103
|
+
return "safe";
|
|
104
|
+
}
|
|
105
|
+
async function maybeAttachReleaseNotes(updates, includeChangelog) {
|
|
106
|
+
if (!includeChangelog || updates.length === 0) {
|
|
107
|
+
return updates;
|
|
108
|
+
}
|
|
109
|
+
return Promise.all(updates.map(async (update) => ({
|
|
110
|
+
...update,
|
|
111
|
+
releaseNotesSummary: summarizeChangelog(await fetchChangelog(update.name, update.repository)),
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
114
|
+
function summarizeChangelog(content) {
|
|
115
|
+
if (!content)
|
|
116
|
+
return undefined;
|
|
117
|
+
const lines = content
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.map((line) => line.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
const title = lines.find((line) => line.startsWith("#"))?.replace(/^#+\s*/, "") ?? "Release notes";
|
|
122
|
+
const excerpt = lines.find((line) => !line.startsWith("#")) ?? "No summary available.";
|
|
123
|
+
return {
|
|
124
|
+
source: content.includes("# Release") ? "github-release" : "changelog-file",
|
|
125
|
+
title,
|
|
126
|
+
excerpt: excerpt.slice(0, 240),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSilenced<T>(fn: () => Promise<T>): Promise<T>;
|