@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +105 -22
  3. package/dist/bin/cli.js +124 -9
  4. package/dist/cache/cache.d.ts +1 -0
  5. package/dist/cache/cache.js +9 -2
  6. package/dist/commands/audit/runner.js +8 -1
  7. package/dist/commands/audit/sources/index.js +8 -1
  8. package/dist/commands/doctor/parser.d.ts +2 -0
  9. package/dist/commands/doctor/parser.js +92 -0
  10. package/dist/commands/doctor/runner.d.ts +2 -0
  11. package/dist/commands/doctor/runner.js +13 -0
  12. package/dist/commands/resolve/runner.js +3 -0
  13. package/dist/commands/review/parser.d.ts +2 -0
  14. package/dist/commands/review/parser.js +174 -0
  15. package/dist/commands/review/runner.d.ts +2 -0
  16. package/dist/commands/review/runner.js +30 -0
  17. package/dist/config/loader.d.ts +3 -0
  18. package/dist/core/check.js +64 -5
  19. package/dist/core/errors.d.ts +11 -0
  20. package/dist/core/errors.js +6 -0
  21. package/dist/core/options.d.ts +8 -1
  22. package/dist/core/options.js +43 -0
  23. package/dist/core/review-model.d.ts +5 -0
  24. package/dist/core/review-model.js +372 -0
  25. package/dist/core/summary.js +11 -2
  26. package/dist/core/upgrade.d.ts +1 -0
  27. package/dist/core/upgrade.js +27 -21
  28. package/dist/core/warm-cache.js +28 -4
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/output/format.d.ts +4 -1
  32. package/dist/output/format.js +41 -3
  33. package/dist/output/github.js +6 -1
  34. package/dist/output/sarif.js +14 -0
  35. package/dist/registry/npm.d.ts +22 -0
  36. package/dist/registry/npm.js +33 -4
  37. package/dist/risk/index.d.ts +3 -0
  38. package/dist/risk/index.js +24 -0
  39. package/dist/risk/scorer.d.ts +3 -0
  40. package/dist/risk/scorer.js +114 -0
  41. package/dist/risk/signals/fresh-package.d.ts +3 -0
  42. package/dist/risk/signals/fresh-package.js +22 -0
  43. package/dist/risk/signals/install-scripts.d.ts +3 -0
  44. package/dist/risk/signals/install-scripts.js +10 -0
  45. package/dist/risk/signals/maintainer-churn.d.ts +3 -0
  46. package/dist/risk/signals/maintainer-churn.js +11 -0
  47. package/dist/risk/signals/metadata.d.ts +3 -0
  48. package/dist/risk/signals/metadata.js +18 -0
  49. package/dist/risk/signals/mutable-source.d.ts +3 -0
  50. package/dist/risk/signals/mutable-source.js +24 -0
  51. package/dist/risk/signals/typosquat.d.ts +3 -0
  52. package/dist/risk/signals/typosquat.js +70 -0
  53. package/dist/risk/types.d.ts +15 -0
  54. package/dist/risk/types.js +1 -0
  55. package/dist/types/index.d.ts +85 -0
  56. package/dist/ui/tui.d.ts +0 -4
  57. package/dist/ui/tui.js +103 -21
  58. package/package.json +10 -2
package/dist/ui/tui.js CHANGED
@@ -1,44 +1,126 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
- import { render, Text, Box, useInput } from "ink";
4
- // Basic version diff string parser to split major.minor.patch
5
- export function VersionDiff({ from, to }) {
6
- if (from === to)
7
- return _jsx(Text, { color: "gray", children: to });
8
- // Very simplistic semver coloring: highlight the changed part
9
- // E.g., from 1.2.3 to 1.3.0 -> 1 is dim, 3.0 is bright green
10
- return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [from, " \u2192 "] }), _jsx(Text, { color: "green", children: to })] }));
3
+ import { Box, render, Text, useInput } from "ink";
4
+ const FILTER_ORDER = [
5
+ "all",
6
+ "security",
7
+ "risky",
8
+ "major",
9
+ ];
10
+ function VersionDiff({ from, to }) {
11
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: from }), _jsxs(Text, { color: "gray", children: [" ", " -> ", " "] }), _jsx(Text, { color: "green", children: to })] }));
12
+ }
13
+ function riskColor(level) {
14
+ switch (level) {
15
+ case "critical":
16
+ return "red";
17
+ case "high":
18
+ return "yellow";
19
+ case "medium":
20
+ return "cyan";
21
+ default:
22
+ return "green";
23
+ }
24
+ }
25
+ function diffColor(level) {
26
+ switch (level) {
27
+ case "major":
28
+ return "red";
29
+ case "minor":
30
+ return "yellow";
31
+ case "patch":
32
+ return "green";
33
+ default:
34
+ return "cyan";
35
+ }
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
+ }
11
60
  }
12
61
  function TuiApp({ updates, onComplete }) {
13
62
  const [cursorIndex, setCursorIndex] = useState(0);
14
- const [selectedIndices, setSelectedIndices] = useState(new Set(updates.map((_, i) => i)));
63
+ const [filterIndex, setFilterIndex] = useState(0);
64
+ const [selectedIndices, setSelectedIndices] = useState(new Set(updates.map((_, index) => index)));
65
+ const activeFilter = FILTER_ORDER[filterIndex] ?? "all";
66
+ const filteredIndices = updates
67
+ .map((update, index) => ({ update, index }))
68
+ .filter(({ update }) => {
69
+ if (activeFilter === "security")
70
+ return (update.advisoryCount ?? 0) > 0;
71
+ if (activeFilter === "risky") {
72
+ return update.riskLevel === "critical" || update.riskLevel === "high";
73
+ }
74
+ if (activeFilter === "major")
75
+ return update.diffType === "major";
76
+ return true;
77
+ })
78
+ .map(({ index }) => index);
79
+ const boundedCursor = Math.min(cursorIndex, Math.max(0, filteredIndices.length - 1));
80
+ const focusedIndex = filteredIndices[boundedCursor] ?? 0;
81
+ const focusedUpdate = updates[focusedIndex];
15
82
  useInput((input, key) => {
83
+ if (key.leftArrow) {
84
+ setFilterIndex((prev) => Math.max(0, prev - 1));
85
+ setCursorIndex(0);
86
+ }
87
+ if (key.rightArrow) {
88
+ setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
89
+ setCursorIndex(0);
90
+ }
16
91
  if (key.upArrow) {
17
92
  setCursorIndex((prev) => Math.max(0, prev - 1));
18
93
  }
19
94
  if (key.downArrow) {
20
- setCursorIndex((prev) => Math.min(updates.length - 1, prev + 1));
95
+ setCursorIndex((prev) => Math.min(filteredIndices.length - 1, Math.max(0, prev + 1)));
96
+ }
97
+ if (input === "a") {
98
+ setSelectedIndices(new Set(filteredIndices));
99
+ }
100
+ if (input === "n") {
101
+ setSelectedIndices(new Set());
21
102
  }
22
103
  if (input === " ") {
23
104
  setSelectedIndices((prev) => {
24
105
  const next = new Set(prev);
25
- if (next.has(cursorIndex))
26
- next.delete(cursorIndex);
106
+ if (next.has(focusedIndex))
107
+ next.delete(focusedIndex);
27
108
  else
28
- next.add(cursorIndex);
109
+ next.add(focusedIndex);
29
110
  return next;
30
111
  });
31
112
  }
32
113
  if (key.return) {
33
- const selected = updates.filter((_, i) => selectedIndices.has(i));
34
- onComplete(selected);
114
+ onComplete(updates.filter((_, index) => selectedIndices.has(index)));
35
115
  }
36
116
  });
37
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Choose updates to install (Space to toggle, Enter to confirm, Up/Down to navigate)" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: updates.map((update, index) => {
38
- const isSelected = selectedIndices.has(index);
39
- const isFocused = cursorIndex === index;
40
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? "❯ " : " ", isSelected ? "◉ " : "◯ "] }), _jsx(Box, { width: 30, children: _jsx(Text, { bold: isFocused, children: update.name }) }), _jsx(Box, { width: 15, children: _jsx(Text, { color: "gray", children: update.diffType }) }), _jsx(VersionDiff, { from: update.fromRange, to: update.toVersionResolved })] }, update.name));
41
- }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: [selectedIndices.size, " of ", updates.length, " selected"] }) })] }));
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) => {
118
+ const update = updates[index];
119
+ const isFocused = visibleIndex === boundedCursor;
120
+ const isSelected = selectedIndices.has(index);
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."] }) })] }));
42
124
  }
43
125
  export async function runTui(updates) {
44
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.2-rc.2",
3
+ "version": "0.5.3",
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,7 +56,15 @@
56
56
  "test": "bun test",
57
57
  "lint": "bunx biome check src tests",
58
58
  "check": "bun run typecheck && bun test",
59
- "perf:smoke": "node scripts/perf-smoke.mjs",
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",
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",
65
+ "perf:check": "node scripts/perf-smoke.mjs",
66
+ "perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve node scripts/perf-smoke.mjs",
67
+ "perf:ci": "RAINY_UPDATES_PERF_SCENARIO=ci node scripts/perf-smoke.mjs",
60
68
  "test:prod": "node dist/bin/cli.js --help && node dist/bin/cli.js --version",
61
69
  "prepublishOnly": "bun run check && bun run build && bun run test:prod"
62
70
  },