@rainy-updates/cli 0.5.2-rc.2 → 0.5.2

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 (40) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +29 -0
  3. package/dist/bin/cli.js +108 -2
  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 +39 -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 +382 -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 +2 -1
  30. package/dist/index.js +1 -0
  31. package/dist/output/format.d.ts +4 -1
  32. package/dist/output/format.js +29 -3
  33. package/dist/output/github.js +5 -0
  34. package/dist/output/sarif.js +11 -0
  35. package/dist/registry/npm.d.ts +20 -0
  36. package/dist/registry/npm.js +27 -4
  37. package/dist/types/index.d.ts +57 -0
  38. package/dist/ui/tui.d.ts +0 -4
  39. package/dist/ui/tui.js +78 -21
  40. package/package.json +5 -2
@@ -30,6 +30,12 @@ export function createSarifReport(result) {
30
30
  kind: update.kind,
31
31
  diffType: update.diffType,
32
32
  resolvedVersion: update.toVersionResolved,
33
+ impactRank: update.impactScore?.rank,
34
+ impactScore: update.impactScore?.score,
35
+ riskLevel: update.riskLevel,
36
+ advisoryCount: update.advisoryCount ?? 0,
37
+ peerConflictSeverity: update.peerConflictSeverity ?? "none",
38
+ licenseStatus: update.licenseStatus ?? "allowed",
33
39
  },
34
40
  }));
35
41
  const errorResults = [...result.errors].sort((a, b) => a.localeCompare(b)).map((error) => ({
@@ -75,6 +81,11 @@ export function createSarifReport(result) {
75
81
  prLimitHit: result.summary.prLimitHit,
76
82
  fixPrBranchesCreated: result.summary.fixPrBranchesCreated,
77
83
  durationMs: result.summary.durationMs,
84
+ verdict: result.summary.verdict,
85
+ riskPackages: result.summary.riskPackages ?? 0,
86
+ securityPackages: result.summary.securityPackages ?? 0,
87
+ peerConflictPackages: result.summary.peerConflictPackages ?? 0,
88
+ licenseViolationPackages: result.summary.licenseViolationPackages ?? 0,
78
89
  },
79
90
  },
80
91
  ],
@@ -1,3 +1,13 @@
1
+ interface RegistryConfig {
2
+ defaultRegistry: string;
3
+ scopedRegistries: Map<string, string>;
4
+ authByRegistry: Map<string, RegistryAuth>;
5
+ }
6
+ interface RegistryAuth {
7
+ token?: string;
8
+ basicAuth?: string;
9
+ alwaysAuth: boolean;
10
+ }
1
11
  export interface ResolveManyOptions {
2
12
  concurrency: number;
3
13
  timeoutMs?: number;
@@ -12,6 +22,9 @@ export interface ResolveManyResult {
12
22
  latestVersion: string | null;
13
23
  versions: string[];
14
24
  publishedAtByVersion: Record<string, number>;
25
+ homepage?: string;
26
+ repository?: string;
27
+ hasInstallScript: boolean;
15
28
  }>;
16
29
  errors: Map<string, string>;
17
30
  }
@@ -24,6 +37,9 @@ export declare class NpmRegistryClient {
24
37
  latestVersion: string | null;
25
38
  versions: string[];
26
39
  publishedAtByVersion: Record<string, number>;
40
+ homepage?: string;
41
+ repository?: string;
42
+ hasInstallScript: boolean;
27
43
  }>;
28
44
  resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
29
45
  resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
@@ -32,3 +48,7 @@ export declare class NpmRegistryClient {
32
48
  errors: Map<string, string>;
33
49
  }>;
34
50
  }
51
+ export declare function loadRegistryConfig(cwd: string): Promise<RegistryConfig>;
52
+ export declare function resolveRegistryForPackage(packageName: string, config: RegistryConfig): string;
53
+ export declare function resolveAuthHeader(registry: string, config: RegistryConfig): string | undefined;
54
+ export {};
@@ -22,7 +22,7 @@ export class NpmRegistryClient {
22
22
  try {
23
23
  const response = await requester(packageName, timeoutMs);
24
24
  if (response.status === 404) {
25
- return { latestVersion: null, versions: [], publishedAtByVersion: {} };
25
+ return { latestVersion: null, versions: [], publishedAtByVersion: {}, hasInstallScript: false };
26
26
  }
27
27
  if (response.status === 429 || response.status >= 500) {
28
28
  throw new RetryableRegistryError(`Registry temporary error: ${response.status}`, response.retryAfterMs ?? computeBackoffMs(attempt));
@@ -35,6 +35,9 @@ export class NpmRegistryClient {
35
35
  latestVersion: response.data?.["dist-tags"]?.latest ?? null,
36
36
  versions,
37
37
  publishedAtByVersion: extractPublishTimes(response.data?.time),
38
+ homepage: response.data?.homepage,
39
+ repository: normalizeRepository(response.data?.repository),
40
+ hasInstallScript: detectInstallScript(response.data?.versions),
38
41
  };
39
42
  }
40
43
  catch (error) {
@@ -94,6 +97,26 @@ export class NpmRegistryClient {
94
97
  function sleep(ms) {
95
98
  return new Promise((resolve) => setTimeout(resolve, ms));
96
99
  }
100
+ function normalizeRepository(value) {
101
+ if (!value)
102
+ return undefined;
103
+ if (typeof value === "string")
104
+ return value;
105
+ return value.url;
106
+ }
107
+ function detectInstallScript(versions) {
108
+ if (!versions)
109
+ return false;
110
+ for (const metadata of Object.values(versions)) {
111
+ const scripts = metadata?.scripts;
112
+ if (!scripts)
113
+ continue;
114
+ if (scripts.preinstall || scripts.install || scripts.postinstall) {
115
+ return true;
116
+ }
117
+ }
118
+ return false;
119
+ }
97
120
  function computeBackoffMs(attempt) {
98
121
  const baseMs = Math.max(120, attempt * 180);
99
122
  const jitterMs = Math.floor(Math.random() * 120);
@@ -195,7 +218,7 @@ async function tryCreateUndiciRequester(registryConfig) {
195
218
  return null;
196
219
  }
197
220
  }
198
- async function loadRegistryConfig(cwd) {
221
+ export async function loadRegistryConfig(cwd) {
199
222
  const homeNpmrc = path.join(os.homedir(), ".npmrc");
200
223
  const projectNpmrc = path.join(cwd, ".npmrc");
201
224
  const merged = new Map();
@@ -274,7 +297,7 @@ function normalizeRegistryUrl(value) {
274
297
  const normalized = value.endsWith("/") ? value : `${value}/`;
275
298
  return normalized;
276
299
  }
277
- function resolveRegistryForPackage(packageName, config) {
300
+ export function resolveRegistryForPackage(packageName, config) {
278
301
  const scope = extractScope(packageName);
279
302
  if (scope) {
280
303
  const scoped = config.scopedRegistries.get(scope);
@@ -295,7 +318,7 @@ function buildRegistryUrl(registry, packageName) {
295
318
  const base = normalizeRegistryUrl(registry);
296
319
  return new URL(encodeURIComponent(packageName), base).toString();
297
320
  }
298
- function resolveAuthHeader(registry, config) {
321
+ export function resolveAuthHeader(registry, config) {
299
322
  const registryUrl = normalizeRegistryUrl(registry);
300
323
  const auth = findRegistryAuth(registryUrl, config.authByRegistry);
301
324
  if (!auth)
@@ -3,6 +3,8 @@ export type TargetLevel = "patch" | "minor" | "major" | "latest";
3
3
  export type GroupBy = "none" | "name" | "scope" | "kind" | "risk";
4
4
  export type CiProfile = "minimal" | "strict" | "enterprise";
5
5
  export type LockfileMode = "preserve" | "update" | "error";
6
+ export type Verdict = "safe" | "review" | "blocked" | "actionable";
7
+ export type RiskLevel = "critical" | "high" | "medium" | "low";
6
8
  export type OutputFormat = "table" | "json" | "minimal" | "github" | "metrics";
7
9
  export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
8
10
  export type LogLevel = "error" | "warn" | "info" | "debug";
@@ -44,6 +46,9 @@ export interface RunOptions {
44
46
  onlyChanged: boolean;
45
47
  ciProfile: CiProfile;
46
48
  lockfileMode: LockfileMode;
49
+ interactive: boolean;
50
+ showImpact: boolean;
51
+ showHomepage: boolean;
47
52
  }
48
53
  export interface CheckOptions extends RunOptions {
49
54
  }
@@ -86,6 +91,12 @@ export interface PackageUpdate {
86
91
  reason?: string;
87
92
  impactScore?: ImpactScore;
88
93
  homepage?: string;
94
+ riskLevel?: RiskLevel;
95
+ riskReasons?: string[];
96
+ advisoryCount?: number;
97
+ peerConflictSeverity?: "none" | PeerConflictSeverity;
98
+ licenseStatus?: "allowed" | "review" | "denied";
99
+ healthStatus?: "healthy" | HealthFlag;
89
100
  }
90
101
  export interface Summary {
91
102
  contractVersion: "2";
@@ -126,6 +137,13 @@ export interface Summary {
126
137
  prLimitHit: boolean;
127
138
  streamedEvents: number;
128
139
  policyOverridesApplied: number;
140
+ verdict?: Verdict;
141
+ interactiveSession?: boolean;
142
+ riskPackages?: number;
143
+ securityPackages?: number;
144
+ peerConflictPackages?: number;
145
+ licenseViolationPackages?: number;
146
+ privateRegistryPackages?: number;
129
147
  }
130
148
  export interface CheckResult {
131
149
  projectPath: string;
@@ -303,6 +321,45 @@ export interface ResolveResult {
303
321
  errors: string[];
304
322
  warnings: string[];
305
323
  }
324
+ export interface RiskSignal {
325
+ packageName: string;
326
+ level: RiskLevel;
327
+ reasons: string[];
328
+ }
329
+ export interface ReviewItem {
330
+ update: PackageUpdate;
331
+ advisories: CveAdvisory[];
332
+ health?: PackageHealthMetric;
333
+ peerConflicts: PeerConflict[];
334
+ license?: PackageLicense;
335
+ unusedIssues: UnusedDependency[];
336
+ selected: boolean;
337
+ }
338
+ export interface ReviewResult {
339
+ projectPath: string;
340
+ target: TargetLevel;
341
+ summary: Summary;
342
+ items: ReviewItem[];
343
+ updates: PackageUpdate[];
344
+ errors: string[];
345
+ warnings: string[];
346
+ }
347
+ export interface ReviewOptions extends CheckOptions {
348
+ securityOnly: boolean;
349
+ risk?: RiskLevel;
350
+ diff?: TargetLevel;
351
+ applySelected: boolean;
352
+ }
353
+ export interface DoctorOptions extends CheckOptions {
354
+ verdictOnly: boolean;
355
+ }
356
+ export interface DoctorResult {
357
+ verdict: Verdict;
358
+ summary: Summary;
359
+ review: ReviewResult;
360
+ primaryFindings: string[];
361
+ recommendedCommand: string;
362
+ }
306
363
  export type UnusedKind = "declared-not-imported" | "imported-not-declared";
307
364
  export interface UnusedDependency {
308
365
  name: string;
package/dist/ui/tui.d.ts CHANGED
@@ -1,6 +1,2 @@
1
1
  import type { PackageUpdate } from "../types/index.js";
2
- export declare function VersionDiff({ from, to }: {
3
- from: string;
4
- to: string;
5
- }): import("react/jsx-runtime").JSX.Element;
6
2
  export declare function runTui(updates: PackageUpdate[]): Promise<PackageUpdate[]>;
package/dist/ui/tui.js CHANGED
@@ -1,44 +1,101 @@
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
+ }
11
36
  }
12
37
  function TuiApp({ updates, onComplete }) {
13
38
  const [cursorIndex, setCursorIndex] = useState(0);
14
- const [selectedIndices, setSelectedIndices] = useState(new Set(updates.map((_, i) => i)));
39
+ const [filterIndex, setFilterIndex] = useState(0);
40
+ const [selectedIndices, setSelectedIndices] = useState(new Set(updates.map((_, index) => index)));
41
+ const activeFilter = FILTER_ORDER[filterIndex] ?? "all";
42
+ const filteredIndices = updates
43
+ .map((update, index) => ({ update, index }))
44
+ .filter(({ update }) => {
45
+ if (activeFilter === "security")
46
+ return (update.advisoryCount ?? 0) > 0;
47
+ if (activeFilter === "risky") {
48
+ return update.riskLevel === "critical" || update.riskLevel === "high";
49
+ }
50
+ if (activeFilter === "major")
51
+ return update.diffType === "major";
52
+ return true;
53
+ })
54
+ .map(({ index }) => index);
55
+ const boundedCursor = Math.min(cursorIndex, Math.max(0, filteredIndices.length - 1));
56
+ const focusedIndex = filteredIndices[boundedCursor] ?? 0;
57
+ const focusedUpdate = updates[focusedIndex];
15
58
  useInput((input, key) => {
59
+ if (key.leftArrow) {
60
+ setFilterIndex((prev) => Math.max(0, prev - 1));
61
+ setCursorIndex(0);
62
+ }
63
+ if (key.rightArrow) {
64
+ setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
65
+ setCursorIndex(0);
66
+ }
16
67
  if (key.upArrow) {
17
68
  setCursorIndex((prev) => Math.max(0, prev - 1));
18
69
  }
19
70
  if (key.downArrow) {
20
- setCursorIndex((prev) => Math.min(updates.length - 1, prev + 1));
71
+ setCursorIndex((prev) => Math.min(filteredIndices.length - 1, Math.max(0, prev + 1)));
72
+ }
73
+ if (input === "a") {
74
+ setSelectedIndices(new Set(filteredIndices));
75
+ }
76
+ if (input === "n") {
77
+ setSelectedIndices(new Set());
21
78
  }
22
79
  if (input === " ") {
23
80
  setSelectedIndices((prev) => {
24
81
  const next = new Set(prev);
25
- if (next.has(cursorIndex))
26
- next.delete(cursorIndex);
82
+ if (next.has(focusedIndex))
83
+ next.delete(focusedIndex);
27
84
  else
28
- next.add(cursorIndex);
85
+ next.add(focusedIndex);
29
86
  return next;
30
87
  });
31
88
  }
32
89
  if (key.return) {
33
- const selected = updates.filter((_, i) => selectedIndices.has(i));
34
- onComplete(selected);
90
+ onComplete(updates.filter((_, index) => selectedIndices.has(index)));
35
91
  }
36
92
  });
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"] }) })] }));
93
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review TUI" }), _jsx(Text, { color: "gray", children: "Left/Right filter Up/Down move Space toggle A select all view 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: "Updates" }), filteredIndices.length === 0 ? (_jsx(Text, { color: "gray", children: "No updates match this filter." })) : (filteredIndices.map((index, visibleIndex) => {
94
+ const update = updates[index];
95
+ const isFocused = visibleIndex === boundedCursor;
96
+ const isSelected = selectedIndices.has(index);
97
+ 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(VersionDiff, { from: update.fromRange, to: update.toVersionResolved })] }, `${update.packagePath}:${update.name}`));
98
+ }))] }), _jsxs(Box, { marginLeft: 1, width: 46, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Details" }), focusedUpdate ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: focusedUpdate.name }), _jsxs(Text, { color: "gray", children: ["package: ", focusedUpdate.packagePath] }), _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: ["impact: ", 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"] }), 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 update selected." }))] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { color: "gray", children: [selectedIndices.size, " selected of ", updates.length, ". Filter: ", activeFilter, "."] }) })] }));
42
99
  }
43
100
  export async function runTui(updates) {
44
101
  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.2",
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,10 @@
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
+ "perf:smoke": "node scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=resolve node scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=ci node scripts/perf-smoke.mjs",
60
+ "perf:check": "node scripts/perf-smoke.mjs",
61
+ "perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve node scripts/perf-smoke.mjs",
62
+ "perf:ci": "RAINY_UPDATES_PERF_SCENARIO=ci node scripts/perf-smoke.mjs",
60
63
  "test:prod": "node dist/bin/cli.js --help && node dist/bin/cli.js --version",
61
64
  "prepublishOnly": "bun run check && bun run build && bun run test:prod"
62
65
  },