@rainy-updates/cli 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -0
  3. package/dist/bin/cli.js +37 -1
  4. package/dist/commands/audit/runner.js +43 -26
  5. package/dist/commands/dashboard/parser.d.ts +2 -0
  6. package/dist/commands/dashboard/parser.js +59 -0
  7. package/dist/commands/dashboard/runner.d.ts +2 -0
  8. package/dist/commands/dashboard/runner.js +47 -0
  9. package/dist/commands/doctor/parser.js +6 -0
  10. package/dist/commands/ga/parser.d.ts +2 -0
  11. package/dist/commands/ga/parser.js +50 -0
  12. package/dist/commands/ga/runner.d.ts +2 -0
  13. package/dist/commands/ga/runner.js +129 -0
  14. package/dist/commands/resolve/runner.js +7 -3
  15. package/dist/commands/review/parser.js +6 -0
  16. package/dist/commands/review/runner.js +4 -3
  17. package/dist/core/analysis-bundle.d.ts +4 -0
  18. package/dist/core/analysis-bundle.js +241 -0
  19. package/dist/core/artifacts.d.ts +3 -0
  20. package/dist/core/artifacts.js +48 -0
  21. package/dist/core/check.js +6 -1
  22. package/dist/core/options.d.ts +7 -1
  23. package/dist/core/options.js +14 -0
  24. package/dist/core/review-model.js +51 -177
  25. package/dist/core/summary.js +13 -0
  26. package/dist/output/format.js +15 -0
  27. package/dist/output/github.js +8 -0
  28. package/dist/output/sarif.js +12 -0
  29. package/dist/types/index.d.ts +92 -0
  30. package/dist/ui/dashboard/DashboardTUI.d.ts +6 -0
  31. package/dist/ui/dashboard/DashboardTUI.js +34 -0
  32. package/dist/ui/dashboard/components/DetailPanel.d.ts +4 -0
  33. package/dist/ui/dashboard/components/DetailPanel.js +30 -0
  34. package/dist/ui/dashboard/components/Footer.d.ts +4 -0
  35. package/dist/ui/dashboard/components/Footer.js +9 -0
  36. package/dist/ui/dashboard/components/Header.d.ts +4 -0
  37. package/dist/ui/dashboard/components/Header.js +12 -0
  38. package/dist/ui/dashboard/components/Sidebar.d.ts +4 -0
  39. package/dist/ui/dashboard/components/Sidebar.js +23 -0
  40. package/dist/ui/dashboard/store.d.ts +34 -0
  41. package/dist/ui/dashboard/store.js +148 -0
  42. package/dist/ui/tui.d.ts +2 -2
  43. package/dist/ui/tui.js +310 -79
  44. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { VersionCache } from "../../cache/cache.js";
5
+ import { detectPackageManager } from "../../pm/detect.js";
6
+ import { discoverPackageDirs } from "../../workspace/discover.js";
7
+ import { stableStringify } from "../../utils/stable-json.js";
8
+ import { writeFileAtomic } from "../../utils/io.js";
9
+ export async function runGa(options) {
10
+ const packageManager = await detectPackageManager(options.cwd);
11
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
12
+ const cache = await VersionCache.create();
13
+ const checks = [];
14
+ checks.push({
15
+ name: "package-manager",
16
+ status: packageManager === "unknown" ? "warn" : "pass",
17
+ detail: packageManager === "unknown"
18
+ ? "No supported lockfile was detected. npm-compatible execution is still possible."
19
+ : `Detected package manager: ${packageManager}.`,
20
+ });
21
+ checks.push({
22
+ name: "workspace-discovery",
23
+ status: packageDirs.length > 0 ? "pass" : "fail",
24
+ detail: `Discovered ${packageDirs.length} package manifest path(s).`,
25
+ });
26
+ const lockfileCheck = await detectLockfile(options.cwd);
27
+ checks.push(lockfileCheck);
28
+ checks.push({
29
+ name: "cache-backend",
30
+ status: cache.backend === "sqlite" ? "pass" : cache.degraded ? "warn" : "pass",
31
+ detail: cache.backend === "sqlite"
32
+ ? "SQLite cache backend is available."
33
+ : `File cache backend active${cache.fallbackReason ? ` (${cache.fallbackReason})` : "."}`,
34
+ });
35
+ const distBuildExists = await fileExists(path.resolve(options.cwd, "dist/bin/cli.js"));
36
+ checks.push({
37
+ name: "dist-build",
38
+ status: distBuildExists ? "pass" : "warn",
39
+ detail: distBuildExists
40
+ ? "Built CLI entrypoint exists in dist/bin/cli.js."
41
+ : "Built CLI entrypoint is missing; run the build before publishing a release artifact.",
42
+ });
43
+ checks.push({
44
+ name: "benchmark-gates",
45
+ status: (await fileExists(path.resolve(options.cwd, "scripts/perf-smoke.mjs"))) &&
46
+ (await fileExists(path.resolve(options.cwd, "scripts/benchmark.mjs")))
47
+ ? "pass"
48
+ : "warn",
49
+ detail: "Benchmark scripts and perf smoke gates were checked for release readiness.",
50
+ });
51
+ checks.push({
52
+ name: "docs-contract",
53
+ status: (await fileExists(path.resolve(options.cwd, "README.md"))) &&
54
+ (await fileExists(path.resolve(options.cwd, "CHANGELOG.md")))
55
+ ? "pass"
56
+ : "warn",
57
+ detail: "README and CHANGELOG presence verified.",
58
+ });
59
+ const errors = checks.filter((check) => check.status === "fail").map((check) => check.detail);
60
+ const warnings = checks.filter((check) => check.status === "warn").map((check) => check.detail);
61
+ const result = {
62
+ ready: errors.length === 0,
63
+ projectPath: options.cwd,
64
+ packageManager,
65
+ workspacePackages: packageDirs.length,
66
+ cacheBackend: cache.backend,
67
+ checks,
68
+ warnings,
69
+ errors,
70
+ };
71
+ process.stdout.write(renderGaResult(result) + "\n");
72
+ if (options.jsonFile) {
73
+ await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
74
+ }
75
+ return result;
76
+ }
77
+ function renderGaResult(result) {
78
+ const lines = [
79
+ `Project: ${result.projectPath}`,
80
+ `GA Ready: ${result.ready ? "yes" : "no"}`,
81
+ `Package Manager: ${result.packageManager}`,
82
+ `Workspace Packages: ${result.workspacePackages}`,
83
+ `Cache Backend: ${result.cacheBackend}`,
84
+ "",
85
+ "Checks:",
86
+ ...result.checks.map((check) => `- [${check.status}] ${check.name}: ${check.detail}`),
87
+ ];
88
+ if (result.warnings.length > 0) {
89
+ lines.push("", "Warnings:");
90
+ lines.push(...result.warnings.map((warning) => `- ${warning}`));
91
+ }
92
+ if (result.errors.length > 0) {
93
+ lines.push("", "Errors:");
94
+ lines.push(...result.errors.map((error) => `- ${error}`));
95
+ }
96
+ return lines.join("\n");
97
+ }
98
+ async function detectLockfile(cwd) {
99
+ const lockfiles = [
100
+ "pnpm-lock.yaml",
101
+ "package-lock.json",
102
+ "npm-shrinkwrap.json",
103
+ "bun.lock",
104
+ "yarn.lock",
105
+ ];
106
+ for (const candidate of lockfiles) {
107
+ if (await fileExists(path.resolve(cwd, candidate))) {
108
+ return {
109
+ name: "lockfile",
110
+ status: "pass",
111
+ detail: `Detected lockfile: ${candidate}.`,
112
+ };
113
+ }
114
+ }
115
+ return {
116
+ name: "lockfile",
117
+ status: "warn",
118
+ detail: "No supported lockfile was detected.",
119
+ };
120
+ }
121
+ async function fileExists(filePath) {
122
+ try {
123
+ await fs.access(filePath);
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
@@ -25,7 +25,7 @@ export async function runResolve(options) {
25
25
  let versionOverrides;
26
26
  if (options.afterUpdate) {
27
27
  versionOverrides = await fetchProposedVersions(options);
28
- if (versionOverrides.size === 0) {
28
+ if (versionOverrides.size === 0 && !options.silent) {
29
29
  process.stderr.write("[resolve] No pending updates found — checking current state.\n");
30
30
  }
31
31
  }
@@ -41,10 +41,14 @@ export async function runResolve(options) {
41
41
  result.conflicts = conflicts;
42
42
  result.errorConflicts = conflicts.filter((c) => c.severity === "error").length;
43
43
  result.warningConflicts = conflicts.filter((c) => c.severity === "warning").length;
44
- process.stdout.write(renderConflictsTable(result, options) + "\n");
44
+ if (!options.silent) {
45
+ process.stdout.write(renderConflictsTable(result, options) + "\n");
46
+ }
45
47
  if (options.jsonFile) {
46
48
  await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
47
- process.stderr.write(`[resolve] JSON report written to ${options.jsonFile}\n`);
49
+ if (!options.silent) {
50
+ process.stderr.write(`[resolve] JSON report written to ${options.jsonFile}\n`);
51
+ }
48
52
  }
49
53
  return result;
50
54
  }
@@ -46,6 +46,7 @@ export function parseReviewArgs(args) {
46
46
  risk: undefined,
47
47
  diff: undefined,
48
48
  applySelected: false,
49
+ showChangelog: false,
49
50
  };
50
51
  for (let i = 0; i < args.length; i += 1) {
51
52
  const current = args[i];
@@ -93,6 +94,10 @@ export function parseReviewArgs(args) {
93
94
  options.applySelected = true;
94
95
  continue;
95
96
  }
97
+ if (current === "--show-changelog") {
98
+ options.showChangelog = true;
99
+ continue;
100
+ }
96
101
  if (current === "--json-file" && next) {
97
102
  options.jsonFile = path.resolve(options.cwd, next);
98
103
  i += 1;
@@ -164,6 +169,7 @@ Options:
164
169
  --risk <level> Minimum risk: critical, high, medium, low
165
170
  --diff <level> Filter by patch, minor, major, latest
166
171
  --apply-selected Apply all filtered updates after review
172
+ --show-changelog Fetch release notes summaries for review output
167
173
  --workspace Scan all workspace packages
168
174
  --policy-file <path> Load policy overrides
169
175
  --json-file <path> Write JSON review report to file
@@ -6,10 +6,11 @@ import { stableStringify } from "../../utils/stable-json.js";
6
6
  import { writeFileAtomic } from "../../utils/io.js";
7
7
  export async function runReview(options) {
8
8
  const review = await buildReviewResult(options);
9
- let selectedUpdates = review.updates;
9
+ let selectedItems = review.items;
10
10
  if (options.interactive && review.updates.length > 0) {
11
- selectedUpdates = await runTui(review.updates);
11
+ selectedItems = await runTui(review.items);
12
12
  }
13
+ const selectedUpdates = selectedItems.map((item) => item.update);
13
14
  if (options.applySelected && selectedUpdates.length > 0) {
14
15
  await applySelectedUpdates({
15
16
  ...options,
@@ -20,8 +21,8 @@ export async function runReview(options) {
20
21
  }
21
22
  process.stdout.write(renderReviewResult({
22
23
  ...review,
24
+ items: selectedItems,
23
25
  updates: selectedUpdates,
24
- items: review.items.filter((item) => selectedUpdates.some((selected) => selected.name === item.update.name && selected.packagePath === item.update.packagePath)),
25
26
  }) + "\n");
26
27
  if (options.jsonFile) {
27
28
  await writeFileAtomic(options.jsonFile, stableStringify(review, 2) + "\n");
@@ -0,0 +1,4 @@
1
+ import type { AnalysisBundle, CheckOptions, DoctorOptions, ReviewOptions } from "../types/index.js";
2
+ export declare function buildAnalysisBundle(options: ReviewOptions | DoctorOptions | CheckOptions, config?: {
3
+ includeChangelog?: boolean;
4
+ }): Promise<AnalysisBundle>;
@@ -0,0 +1,241 @@
1
+ import { fetchChangelog } from "../commands/changelog/fetcher.js";
2
+ import { applyImpactScores } from "./impact.js";
3
+ import { applyRiskAssessments } from "../risk/index.js";
4
+ import { check } from "./check.js";
5
+ export async function buildAnalysisBundle(options, config = {}) {
6
+ const baseCheckOptions = {
7
+ ...options,
8
+ interactive: false,
9
+ showImpact: true,
10
+ showHomepage: true,
11
+ };
12
+ const checkResult = await check(baseCheckOptions);
13
+ const [auditResult, resolveResult, healthResult, licenseResult, unusedResult] = await runSilenced(() => Promise.all([
14
+ import("../commands/audit/runner.js").then((mod) => mod.runAudit(toAuditOptions(options))),
15
+ import("../commands/resolve/runner.js").then((mod) => mod.runResolve(toResolveOptions(options))),
16
+ import("../commands/health/runner.js").then((mod) => mod.runHealth(toHealthOptions(options))),
17
+ import("../commands/licenses/runner.js").then((mod) => mod.runLicenses(toLicenseOptions(options))),
18
+ import("../commands/unused/runner.js").then((mod) => mod.runUnused(toUnusedOptions(options))),
19
+ ]));
20
+ const items = await buildReviewItems(checkResult.updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config);
21
+ return {
22
+ check: checkResult,
23
+ audit: auditResult,
24
+ resolve: resolveResult,
25
+ health: healthResult,
26
+ licenses: licenseResult,
27
+ unused: unusedResult,
28
+ items,
29
+ degradedSources: auditResult.sourceHealth
30
+ .filter((source) => source.status !== "ok")
31
+ .map((source) => source.source),
32
+ };
33
+ }
34
+ async function buildReviewItems(updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config) {
35
+ const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
36
+ const impactedUpdates = applyImpactScores(updates, {
37
+ advisoryPackages,
38
+ workspaceDependentCount: (name) => updates.filter((item) => item.name === name).length,
39
+ });
40
+ const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
41
+ const advisoriesByName = new Map();
42
+ const conflictsByName = new Map();
43
+ const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
44
+ const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
45
+ const unusedByName = new Map();
46
+ for (const advisory of auditResult.advisories) {
47
+ const list = advisoriesByName.get(advisory.packageName) ?? [];
48
+ list.push(advisory);
49
+ advisoriesByName.set(advisory.packageName, list);
50
+ }
51
+ for (const conflict of resolveResult.conflicts) {
52
+ const list = conflictsByName.get(conflict.requester) ?? [];
53
+ list.push(conflict);
54
+ conflictsByName.set(conflict.requester, list);
55
+ const peerList = conflictsByName.get(conflict.peer) ?? [];
56
+ peerList.push(conflict);
57
+ conflictsByName.set(conflict.peer, peerList);
58
+ }
59
+ for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
60
+ const list = unusedByName.get(issue.name) ?? [];
61
+ list.push(issue);
62
+ unusedByName.set(issue.name, list);
63
+ }
64
+ const enrichedUpdates = await maybeAttachReleaseNotes(impactedUpdates, Boolean(config.includeChangelog));
65
+ return applyRiskAssessments(enrichedUpdates.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName)), {
66
+ knownPackageNames: new Set(updates.map((item) => item.name)),
67
+ }).map((item) => ({
68
+ ...item,
69
+ update: {
70
+ ...item.update,
71
+ policyAction: derivePolicyAction(item),
72
+ decisionState: deriveDecisionState(item),
73
+ selectedByDefault: deriveDecisionState(item) !== "blocked",
74
+ blockedReason: deriveDecisionState(item) === "blocked"
75
+ ? item.update.recommendedAction
76
+ : undefined,
77
+ monitorReason: item.update.healthStatus === "stale" ? "Package health should be monitored." : undefined,
78
+ },
79
+ }));
80
+ }
81
+ function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
82
+ const advisories = advisoriesByName.get(update.name) ?? [];
83
+ const peerConflicts = conflictsByName.get(update.name) ?? [];
84
+ const health = healthByName.get(update.name);
85
+ const license = licenseByName.get(update.name);
86
+ const unusedIssues = unusedByName.get(update.name) ?? [];
87
+ return {
88
+ update: {
89
+ ...update,
90
+ advisoryCount: advisories.length,
91
+ peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
92
+ ? "error"
93
+ : peerConflicts.length > 0
94
+ ? "warning"
95
+ : "none",
96
+ licenseStatus: licenseViolationNames.has(update.name)
97
+ ? "denied"
98
+ : license
99
+ ? "allowed"
100
+ : "review",
101
+ healthStatus: health?.flags[0] ?? "healthy",
102
+ },
103
+ advisories,
104
+ health,
105
+ peerConflicts,
106
+ license,
107
+ unusedIssues,
108
+ selected: true,
109
+ };
110
+ }
111
+ function derivePolicyAction(item) {
112
+ if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
113
+ return "block";
114
+ }
115
+ if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
116
+ return "review";
117
+ }
118
+ if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
119
+ return "monitor";
120
+ }
121
+ return "allow";
122
+ }
123
+ function deriveDecisionState(item) {
124
+ if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
125
+ return "blocked";
126
+ }
127
+ if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
128
+ return "actionable";
129
+ }
130
+ if (item.update.riskLevel === "high" || item.update.diffType === "major") {
131
+ return "review";
132
+ }
133
+ return "safe";
134
+ }
135
+ async function maybeAttachReleaseNotes(updates, includeChangelog) {
136
+ if (!includeChangelog || updates.length === 0) {
137
+ return updates;
138
+ }
139
+ const enriched = await Promise.all(updates.map(async (update) => ({
140
+ ...update,
141
+ releaseNotesSummary: summarizeChangelog(await fetchChangelog(update.name, update.repository)),
142
+ })));
143
+ return enriched;
144
+ }
145
+ function summarizeChangelog(content) {
146
+ if (!content)
147
+ return undefined;
148
+ const lines = content
149
+ .split(/\r?\n/)
150
+ .map((line) => line.trim())
151
+ .filter(Boolean);
152
+ const title = lines.find((line) => line.startsWith("#"))?.replace(/^#+\s*/, "") ?? "Release notes";
153
+ const excerpt = lines.find((line) => !line.startsWith("#")) ?? "No summary available.";
154
+ return {
155
+ source: content.includes("# Release") ? "github-release" : "changelog-file",
156
+ title,
157
+ excerpt: excerpt.slice(0, 240),
158
+ };
159
+ }
160
+ async function runSilenced(fn) {
161
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
162
+ const stderrWrite = process.stderr.write.bind(process.stderr);
163
+ process.stdout.write = (() => true);
164
+ process.stderr.write = (() => true);
165
+ try {
166
+ return await fn();
167
+ }
168
+ finally {
169
+ process.stdout.write = stdoutWrite;
170
+ process.stderr.write = stderrWrite;
171
+ }
172
+ }
173
+ function toAuditOptions(options) {
174
+ return {
175
+ cwd: options.cwd,
176
+ workspace: options.workspace,
177
+ severity: undefined,
178
+ fix: false,
179
+ dryRun: true,
180
+ commit: false,
181
+ packageManager: "auto",
182
+ reportFormat: "json",
183
+ sourceMode: "auto",
184
+ jsonFile: undefined,
185
+ concurrency: options.concurrency,
186
+ registryTimeoutMs: options.registryTimeoutMs,
187
+ silent: true,
188
+ };
189
+ }
190
+ function toResolveOptions(options) {
191
+ return {
192
+ cwd: options.cwd,
193
+ workspace: options.workspace,
194
+ afterUpdate: true,
195
+ safe: false,
196
+ jsonFile: undefined,
197
+ concurrency: options.concurrency,
198
+ registryTimeoutMs: options.registryTimeoutMs,
199
+ cacheTtlSeconds: options.cacheTtlSeconds,
200
+ silent: true,
201
+ };
202
+ }
203
+ function toHealthOptions(options) {
204
+ return {
205
+ cwd: options.cwd,
206
+ workspace: options.workspace,
207
+ staleDays: 365,
208
+ includeDeprecated: true,
209
+ includeAlternatives: false,
210
+ reportFormat: "json",
211
+ jsonFile: undefined,
212
+ concurrency: options.concurrency,
213
+ registryTimeoutMs: options.registryTimeoutMs,
214
+ };
215
+ }
216
+ function toLicenseOptions(options) {
217
+ return {
218
+ cwd: options.cwd,
219
+ workspace: options.workspace,
220
+ allow: undefined,
221
+ deny: undefined,
222
+ sbomFile: undefined,
223
+ jsonFile: undefined,
224
+ diffMode: false,
225
+ concurrency: options.concurrency,
226
+ registryTimeoutMs: options.registryTimeoutMs,
227
+ cacheTtlSeconds: options.cacheTtlSeconds,
228
+ };
229
+ }
230
+ function toUnusedOptions(options) {
231
+ return {
232
+ cwd: options.cwd,
233
+ workspace: options.workspace,
234
+ srcDirs: ["src", "."],
235
+ includeDevDependencies: true,
236
+ fix: false,
237
+ dryRun: true,
238
+ jsonFile: undefined,
239
+ concurrency: options.concurrency,
240
+ };
241
+ }
@@ -0,0 +1,3 @@
1
+ import type { ArtifactManifest, CheckResult, RunOptions } from "../types/index.js";
2
+ export declare function createRunId(command: string, options: RunOptions, result: CheckResult): string;
3
+ export declare function writeArtifactManifest(command: string, options: RunOptions, result: CheckResult): Promise<ArtifactManifest | null>;
@@ -0,0 +1,48 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { stableStringify } from "../utils/stable-json.js";
4
+ import { writeFileAtomic } from "../utils/io.js";
5
+ export function createRunId(command, options, result) {
6
+ const hash = crypto.createHash("sha256");
7
+ hash.update(stableStringify({
8
+ command,
9
+ cwd: path.resolve(options.cwd),
10
+ target: options.target,
11
+ workspace: options.workspace,
12
+ ciProfile: options.ciProfile,
13
+ updates: result.updates.map((update) => ({
14
+ packagePath: update.packagePath,
15
+ name: update.name,
16
+ fromRange: update.fromRange,
17
+ toRange: update.toRange,
18
+ })),
19
+ }, 0));
20
+ return hash.digest("hex").slice(0, 16);
21
+ }
22
+ export async function writeArtifactManifest(command, options, result) {
23
+ const shouldWrite = options.ci ||
24
+ Boolean(options.jsonFile) ||
25
+ Boolean(options.githubOutputFile) ||
26
+ Boolean(options.sarifFile) ||
27
+ Boolean(options.prReportFile);
28
+ if (!shouldWrite)
29
+ return null;
30
+ const runId = result.summary.runId ?? createRunId(command, options, result);
31
+ const artifactManifestPath = path.resolve(options.cwd, ".artifacts", `rainy-manifest-${runId}.json`);
32
+ const manifest = {
33
+ runId,
34
+ createdAt: new Date().toISOString(),
35
+ command,
36
+ projectPath: result.projectPath,
37
+ ciProfile: options.ciProfile,
38
+ artifactManifestPath,
39
+ outputs: {
40
+ jsonFile: options.jsonFile,
41
+ githubOutputFile: options.githubOutputFile,
42
+ sarifFile: options.sarifFile,
43
+ prReportFile: options.prReportFile,
44
+ },
45
+ };
46
+ await writeFileAtomic(artifactManifestPath, stableStringify(manifest, 2) + "\n");
47
+ return manifest;
48
+ }
@@ -208,6 +208,7 @@ export async function check(options) {
208
208
  continue;
209
209
  updates.push({
210
210
  packagePath: path.resolve(task.packageDir),
211
+ workspaceGroup: path.basename(task.packageDir),
211
212
  name: task.dependency.name,
212
213
  kind: task.dependency.kind,
213
214
  fromRange: task.dependency.range,
@@ -267,6 +268,7 @@ export async function check(options) {
267
268
  policyOverridesApplied,
268
269
  }));
269
270
  summary.streamedEvents = streamedEvents;
271
+ summary.cacheBackend = cache.backend;
270
272
  summary.riskPackages = limitedUpdates.filter((item) => item.impactScore?.rank === "critical" || item.impactScore?.rank === "high").length;
271
273
  return {
272
274
  projectPath: options.cwd,
@@ -305,7 +307,10 @@ function groupUpdates(updates, groupBy) {
305
307
  byGroup.set(key, current);
306
308
  }
307
309
  return Array.from(byGroup.entries())
308
- .map(([key, items]) => ({ key, items: sortUpdates(items) }))
310
+ .map(([key, items]) => ({
311
+ key,
312
+ items: sortUpdates(items).map((item) => ({ ...item, groupKey: key })),
313
+ }))
309
314
  .sort((left, right) => left.key.localeCompare(right.key));
310
315
  }
311
316
  function groupKey(update, groupBy) {
@@ -1,4 +1,4 @@
1
- import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel, DashboardOptions, GaOptions } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -52,6 +52,12 @@ export type ParsedCliArgs = {
52
52
  } | {
53
53
  command: "doctor";
54
54
  options: DoctorOptions;
55
+ } | {
56
+ command: "dashboard";
57
+ options: DashboardOptions;
58
+ } | {
59
+ command: "ga";
60
+ options: GaOptions;
55
61
  };
56
62
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
57
63
  export declare function ensureRiskLevel(value: string): RiskLevel;
@@ -23,6 +23,8 @@ const KNOWN_COMMANDS = [
23
23
  "snapshot",
24
24
  "review",
25
25
  "doctor",
26
+ "dashboard",
27
+ "ga",
26
28
  ];
27
29
  export async function parseCliArgs(argv) {
28
30
  const firstArg = argv[0];
@@ -72,6 +74,14 @@ export async function parseCliArgs(argv) {
72
74
  const { parseDoctorArgs } = await import("../commands/doctor/parser.js");
73
75
  return { command, options: parseDoctorArgs(args) };
74
76
  }
77
+ if (command === "dashboard") {
78
+ const { parseDashboardArgs } = await import("../commands/dashboard/parser.js");
79
+ return { command, options: parseDashboardArgs(args) };
80
+ }
81
+ if (command === "ga") {
82
+ const { parseGaArgs } = await import("../commands/ga/parser.js");
83
+ return { command, options: parseGaArgs(args) };
84
+ }
75
85
  const base = {
76
86
  cwd: process.cwd(),
77
87
  target: "latest",
@@ -450,6 +460,10 @@ export async function parseCliArgs(argv) {
450
460
  base.showImpact = true;
451
461
  continue;
452
462
  }
463
+ if (current === "--show-links") {
464
+ base.showHomepage = true;
465
+ continue;
466
+ }
453
467
  if (current === "--show-homepage") {
454
468
  base.showHomepage = true;
455
469
  continue;