@rainy-updates/cli 0.6.0 → 0.6.1

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 (55) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/bin/cli.js +12 -127
  3. package/dist/bin/dispatch.js +6 -0
  4. package/dist/bin/help.js +47 -0
  5. package/dist/bin/main.d.ts +1 -0
  6. package/dist/bin/main.js +126 -0
  7. package/dist/commands/audit/parser.js +36 -0
  8. package/dist/commands/audit/runner.js +17 -18
  9. package/dist/commands/bisect/oracle.js +18 -15
  10. package/dist/commands/bisect/runner.js +4 -3
  11. package/dist/commands/dashboard/parser.js +41 -0
  12. package/dist/commands/doctor/parser.js +44 -0
  13. package/dist/commands/ga/parser.js +39 -0
  14. package/dist/commands/ga/runner.js +10 -7
  15. package/dist/commands/health/parser.js +36 -0
  16. package/dist/commands/health/runner.js +5 -1
  17. package/dist/commands/hook/parser.d.ts +2 -0
  18. package/dist/commands/hook/parser.js +40 -0
  19. package/dist/commands/hook/runner.d.ts +2 -0
  20. package/dist/commands/hook/runner.js +174 -0
  21. package/dist/commands/licenses/parser.js +39 -0
  22. package/dist/commands/licenses/runner.js +5 -1
  23. package/dist/commands/resolve/graph/builder.js +5 -1
  24. package/dist/commands/resolve/parser.js +39 -0
  25. package/dist/commands/resolve/runner.js +5 -0
  26. package/dist/commands/review/parser.js +44 -0
  27. package/dist/commands/snapshot/parser.js +39 -0
  28. package/dist/commands/snapshot/runner.js +4 -1
  29. package/dist/commands/unused/parser.js +39 -0
  30. package/dist/commands/unused/runner.js +4 -1
  31. package/dist/commands/unused/scanner.d.ts +2 -1
  32. package/dist/commands/unused/scanner.js +60 -44
  33. package/dist/core/check.js +5 -1
  34. package/dist/core/init-ci.js +28 -26
  35. package/dist/core/options.d.ts +4 -1
  36. package/dist/core/options.js +57 -0
  37. package/dist/core/verification.js +8 -6
  38. package/dist/core/warm-cache.js +5 -1
  39. package/dist/generated/version.d.ts +1 -0
  40. package/dist/generated/version.js +2 -0
  41. package/dist/git/scope.d.ts +19 -0
  42. package/dist/git/scope.js +167 -0
  43. package/dist/index.d.ts +2 -1
  44. package/dist/index.js +1 -0
  45. package/dist/output/sarif.js +2 -8
  46. package/dist/pm/detect.d.ts +37 -0
  47. package/dist/pm/detect.js +133 -2
  48. package/dist/pm/install.d.ts +2 -1
  49. package/dist/pm/install.js +7 -5
  50. package/dist/rup +0 -0
  51. package/dist/types/index.d.ts +58 -0
  52. package/dist/ui/tui.js +152 -64
  53. package/dist/workspace/discover.d.ts +7 -1
  54. package/dist/workspace/discover.js +12 -3
  55. package/package.json +10 -5
@@ -37,6 +37,11 @@ export function parseReviewArgs(args) {
37
37
  cooldownDays: undefined,
38
38
  prLimit: undefined,
39
39
  onlyChanged: false,
40
+ affected: false,
41
+ staged: false,
42
+ baseRef: undefined,
43
+ headRef: undefined,
44
+ sinceRef: undefined,
40
45
  ciProfile: "minimal",
41
46
  lockfileMode: "preserve",
42
47
  interactive: false,
@@ -68,6 +73,39 @@ export function parseReviewArgs(args) {
68
73
  options.workspace = true;
69
74
  continue;
70
75
  }
76
+ if (current === "--only-changed") {
77
+ options.onlyChanged = true;
78
+ continue;
79
+ }
80
+ if (current === "--affected") {
81
+ options.affected = true;
82
+ continue;
83
+ }
84
+ if (current === "--staged") {
85
+ options.staged = true;
86
+ continue;
87
+ }
88
+ if (current === "--base" && next) {
89
+ options.baseRef = next;
90
+ i += 1;
91
+ continue;
92
+ }
93
+ if (current === "--base")
94
+ throw new Error("Missing value for --base");
95
+ if (current === "--head" && next) {
96
+ options.headRef = next;
97
+ i += 1;
98
+ continue;
99
+ }
100
+ if (current === "--head")
101
+ throw new Error("Missing value for --head");
102
+ if (current === "--since" && next) {
103
+ options.sinceRef = next;
104
+ i += 1;
105
+ continue;
106
+ }
107
+ if (current === "--since")
108
+ throw new Error("Missing value for --since");
71
109
  if (current === "--interactive") {
72
110
  options.interactive = true;
73
111
  continue;
@@ -224,6 +262,12 @@ Options:
224
262
  --test-command <cmd> Override the command used for test verification
225
263
  --show-changelog Fetch release notes summaries for review output
226
264
  --workspace Scan all workspace packages
265
+ --only-changed Limit analysis to changed packages
266
+ --affected Include changed packages and their dependents
267
+ --staged Limit analysis to staged changes
268
+ --base <ref> Compare changes against a base git ref
269
+ --head <ref> Compare changes against a head git ref
270
+ --since <ref> Compare changes since a git ref
227
271
  --policy-file <path> Load policy overrides
228
272
  --json-file <path> Write JSON review report to file
229
273
  --registry-timeout-ms <n>
@@ -3,6 +3,11 @@ export function parseSnapshotArgs(args) {
3
3
  const options = {
4
4
  cwd: process.cwd(),
5
5
  workspace: false,
6
+ affected: false,
7
+ staged: false,
8
+ baseRef: undefined,
9
+ headRef: undefined,
10
+ sinceRef: undefined,
6
11
  action: "list",
7
12
  label: undefined,
8
13
  snapshotId: undefined,
@@ -25,6 +30,35 @@ export function parseSnapshotArgs(args) {
25
30
  options.workspace = true;
26
31
  continue;
27
32
  }
33
+ if (current === "--affected") {
34
+ options.affected = true;
35
+ continue;
36
+ }
37
+ if (current === "--staged") {
38
+ options.staged = true;
39
+ continue;
40
+ }
41
+ if (current === "--base" && next) {
42
+ options.baseRef = next;
43
+ i++;
44
+ continue;
45
+ }
46
+ if (current === "--base")
47
+ throw new Error("Missing value for --base");
48
+ if (current === "--head" && next) {
49
+ options.headRef = next;
50
+ i++;
51
+ continue;
52
+ }
53
+ if (current === "--head")
54
+ throw new Error("Missing value for --head");
55
+ if (current === "--since" && next) {
56
+ options.sinceRef = next;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (current === "--since")
61
+ throw new Error("Missing value for --since");
28
62
  if (current === "--label" && next) {
29
63
  options.label = next;
30
64
  i++;
@@ -75,6 +109,11 @@ Options:
75
109
  --label <name> Human-readable label for the snapshot
76
110
  --store <path> Custom snapshot store file (default: .rup-snapshots.json)
77
111
  --workspace Include all workspace packages
112
+ --affected Include changed workspace packages and dependents
113
+ --staged Limit snapshot scope to staged changes
114
+ --base <ref> Compare changes against a base git ref
115
+ --head <ref> Compare changes against a head git ref
116
+ --since <ref> Compare changes since a git ref
78
117
  --cwd <path> Working directory (default: cwd)
79
118
  --help Show this help
80
119
  `.trimStart();
@@ -16,7 +16,10 @@ export async function runSnapshot(options) {
16
16
  errors: [],
17
17
  warnings: [],
18
18
  };
19
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
19
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
20
+ git: options,
21
+ includeDependents: options.affected === true,
22
+ });
20
23
  const store = new SnapshotStore(options.cwd, options.storeFile);
21
24
  switch (options.action) {
22
25
  // ─ save ──────────────────────────────────────────────────────────────────
@@ -3,6 +3,11 @@ export function parseUnusedArgs(args) {
3
3
  const options = {
4
4
  cwd: process.cwd(),
5
5
  workspace: false,
6
+ affected: false,
7
+ staged: false,
8
+ baseRef: undefined,
9
+ headRef: undefined,
10
+ sinceRef: undefined,
6
11
  srcDirs: DEFAULT_SRC_DIRS,
7
12
  includeDevDependencies: true,
8
13
  fix: false,
@@ -24,6 +29,35 @@ export function parseUnusedArgs(args) {
24
29
  options.workspace = true;
25
30
  continue;
26
31
  }
32
+ if (current === "--affected") {
33
+ options.affected = true;
34
+ continue;
35
+ }
36
+ if (current === "--staged") {
37
+ options.staged = true;
38
+ continue;
39
+ }
40
+ if (current === "--base" && next) {
41
+ options.baseRef = next;
42
+ i++;
43
+ continue;
44
+ }
45
+ if (current === "--base")
46
+ throw new Error("Missing value for --base");
47
+ if (current === "--head" && next) {
48
+ options.headRef = next;
49
+ i++;
50
+ continue;
51
+ }
52
+ if (current === "--head")
53
+ throw new Error("Missing value for --head");
54
+ if (current === "--since" && next) {
55
+ options.sinceRef = next;
56
+ i++;
57
+ continue;
58
+ }
59
+ if (current === "--since")
60
+ throw new Error("Missing value for --since");
27
61
  if (current === "--src" && next) {
28
62
  options.srcDirs = next
29
63
  .split(",")
@@ -81,6 +115,11 @@ Usage:
81
115
  Options:
82
116
  --src <dirs> Comma-separated source directories to scan (default: src)
83
117
  --workspace Scan all workspace packages
118
+ --affected Scan changed workspace packages and their dependents
119
+ --staged Limit scanning to staged changes
120
+ --base <ref> Compare changes against a base git ref
121
+ --head <ref> Compare changes against a head git ref
122
+ --since <ref> Compare changes since a git ref
84
123
  --no-dev Exclude devDependencies from unused detection
85
124
  --fix Remove unused dependencies from package.json
86
125
  --dry-run Preview changes without writing
@@ -24,7 +24,10 @@ export async function runUnused(options) {
24
24
  errors: [],
25
25
  warnings: [],
26
26
  };
27
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
27
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
28
+ git: options,
29
+ includeDependents: false,
30
+ });
28
31
  for (const packageDir of packageDirs) {
29
32
  // ─ Read manifest ─────────────────────────────────────────────────────────
30
33
  let manifest;
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Extracts all imported package names from a single source file.
2
+ * Extracts all imported package names from a single source file using AST.
3
3
  *
4
4
  * Handles:
5
5
  * - ESM static: import ... from "pkg"
6
6
  * - ESM dynamic: import("pkg")
7
7
  * - CJS: require("pkg")
8
+ * - ESM re-export: export ... from "pkg"
8
9
  *
9
10
  * Strips subpath imports (e.g. "lodash/merge" → "lodash"),
10
11
  * skips relative imports and node: builtins.
@@ -1,37 +1,63 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
2
+ import { parseSync } from "oxc-parser";
3
3
  /**
4
- * Extracts all imported package names from a single source file.
4
+ * Extracts all imported package names from a single source file using AST.
5
5
  *
6
6
  * Handles:
7
7
  * - ESM static: import ... from "pkg"
8
8
  * - ESM dynamic: import("pkg")
9
9
  * - CJS: require("pkg")
10
+ * - ESM re-export: export ... from "pkg"
10
11
  *
11
12
  * Strips subpath imports (e.g. "lodash/merge" → "lodash"),
12
13
  * skips relative imports and node: builtins.
13
14
  */
14
15
  export function extractImportsFromSource(source) {
15
16
  const names = new Set();
16
- // ESM static import: from "pkg" or from 'pkg'
17
- const staticImport = /from\s+['"]([^'"]+)['"]/g;
18
- for (const match of source.matchAll(staticImport)) {
19
- addPackageName(names, match[1]);
20
- }
21
- // ESM dynamic import: import("pkg") or import('pkg')
22
- const dynamicImport = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
23
- for (const match of source.matchAll(dynamicImport)) {
24
- addPackageName(names, match[1]);
25
- }
26
- // CJS require: require("pkg") or require('pkg')
27
- const cjsRequire = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
28
- for (const match of source.matchAll(cjsRequire)) {
29
- addPackageName(names, match[1]);
17
+ try {
18
+ const parseResult = parseSync("unknown.ts", source, {
19
+ sourceType: "module",
20
+ });
21
+ const walk = (node) => {
22
+ if (!node)
23
+ return;
24
+ if (node.type === "ImportDeclaration" && node.source?.value) {
25
+ addPackageName(names, node.source.value);
26
+ }
27
+ else if (node.type === "ExportNamedDeclaration" && node.source?.value) {
28
+ addPackageName(names, node.source.value);
29
+ }
30
+ else if (node.type === "ExportAllDeclaration" && node.source?.value) {
31
+ addPackageName(names, node.source.value);
32
+ }
33
+ else if (node.type === "ImportExpression" && node.source?.value) {
34
+ addPackageName(names, node.source.value);
35
+ }
36
+ else if (node.type === "CallExpression") {
37
+ if (node.callee?.type === "Identifier" &&
38
+ node.callee.name === "require" &&
39
+ node.arguments?.[0]?.type === "StringLiteral") {
40
+ addPackageName(names, node.arguments[0].value);
41
+ }
42
+ }
43
+ // Traverse children
44
+ for (const key in node) {
45
+ if (node[key] && typeof node[key] === "object") {
46
+ if (Array.isArray(node[key])) {
47
+ for (const child of node[key]) {
48
+ walk(child);
49
+ }
50
+ }
51
+ else {
52
+ walk(node[key]);
53
+ }
54
+ }
55
+ }
56
+ };
57
+ walk(parseResult.program);
30
58
  }
31
- // export ... from "pkg"
32
- const reExport = /\bexport\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
33
- for (const match of source.matchAll(reExport)) {
34
- addPackageName(names, match[1]);
59
+ catch (err) {
60
+ // Fallback or ignore parse errors
35
61
  }
36
62
  return names;
37
63
  }
@@ -87,40 +113,30 @@ const IGNORED_DIRS = new Set([
87
113
  */
88
114
  export async function scanDirectory(dir) {
89
115
  const allImports = new Set();
90
- await walkDirectory(dir, allImports);
91
- return allImports;
92
- }
93
- async function walkDirectory(dir, collector) {
94
- let entries;
95
- try {
96
- entries = await fs.readdir(dir, { withFileTypes: true });
97
- }
98
- catch {
99
- return;
100
- }
116
+ const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}");
101
117
  const tasks = [];
102
- for (const entry of entries) {
103
- const entryName = entry.name;
104
- if (IGNORED_DIRS.has(entryName))
105
- continue;
106
- const fullPath = path.join(dir, entryName);
107
- if (entry.isDirectory()) {
108
- tasks.push(walkDirectory(fullPath, collector));
118
+ for await (const file of glob.scan(dir)) {
119
+ // Bun.Glob returns relative paths
120
+ const fullPath = path.join(dir, file);
121
+ // Quick check to ignore certain directories in the path
122
+ if (fullPath.includes("/node_modules/") ||
123
+ fullPath.includes("/.git/") ||
124
+ fullPath.includes("/dist/") ||
125
+ fullPath.includes("/build/") ||
126
+ fullPath.includes("/out/") ||
127
+ fullPath.includes("/.next/") ||
128
+ fullPath.includes("/.nuxt/")) {
109
129
  continue;
110
130
  }
111
- if (!entry.isFile())
112
- continue;
113
- const ext = path.extname(entryName).toLowerCase();
114
- if (!SOURCE_EXTENSIONS.has(ext))
115
- continue;
116
131
  tasks.push(Bun.file(fullPath)
117
132
  .text()
118
133
  .then((source) => {
119
134
  for (const name of extractImportsFromSource(source)) {
120
- collector.add(name);
135
+ allImports.add(name);
121
136
  }
122
137
  })
123
138
  .catch(() => undefined));
124
139
  }
125
140
  await Promise.all(tasks);
141
+ return allImports;
126
142
  }
@@ -18,7 +18,11 @@ export async function check(options) {
18
18
  let registryMs = 0;
19
19
  const discoveryStartedAt = Date.now();
20
20
  const packageManager = await detectPackageManager(options.cwd);
21
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
21
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
22
+ git: options,
23
+ includeKinds: options.includeKinds,
24
+ includeDependents: options.affected === true,
25
+ });
22
26
  discoveryMs += Date.now() - discoveryStartedAt;
23
27
  const cache = await VersionCache.create();
24
28
  const registryClient = new NpmRegistryClient(options.cwd, {
@@ -1,6 +1,6 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { detectPackageManager } from "../pm/detect.js";
3
+ import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile, detectPackageManagerDetails, } from "../pm/detect.js";
4
4
  export async function initCiWorkflow(cwd, force, options) {
5
5
  const workflowPath = path.join(cwd, ".github", "workflows", "rainy-updates.yml");
6
6
  try {
@@ -13,8 +13,8 @@ export async function initCiWorkflow(cwd, force, options) {
13
13
  catch {
14
14
  // missing file, continue create
15
15
  }
16
- const detected = await detectPackageManager(cwd);
17
- const packageManager = detected === "unknown" || detected === "yarn" ? "npm" : detected;
16
+ const detected = await detectPackageManagerDetails(cwd);
17
+ const packageManager = createPackageManagerProfile("auto", detected);
18
18
  const scheduleBlock = renderScheduleBlock(options.schedule);
19
19
  const workflow = options.mode === "minimal"
20
20
  ? minimalWorkflowTemplate(scheduleBlock, packageManager)
@@ -32,32 +32,34 @@ function renderScheduleBlock(schedule) {
32
32
  const cron = schedule === "daily" ? "0 8 * * *" : "0 8 * * 1";
33
33
  return ` schedule:\n - cron: '${cron}'\n workflow_dispatch:`;
34
34
  }
35
- function installStep(packageManager) {
36
- if (packageManager === "bun") {
37
- return ` - name: Install dependencies\n run: bun install`;
35
+ function installStep(profile) {
36
+ const install = buildInstallInvocation(profile, { frozen: true, ci: true });
37
+ return ` - name: Install dependencies\n run: ${install.display}`;
38
+ }
39
+ function runtimeSetupSteps(profile) {
40
+ const lines = [
41
+ ` - name: Checkout\n uses: actions/checkout@v4`,
42
+ ];
43
+ if (profile.manager !== "bun") {
44
+ lines.push(` - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: 22`);
45
+ }
46
+ lines.push(` - name: Setup Bun\n uses: oven-sh/setup-bun@v1`);
47
+ if (profile.manager === "pnpm" || profile.manager === "yarn") {
48
+ lines.push(` - name: Enable Corepack\n run: corepack enable`);
38
49
  }
39
- if (packageManager === "pnpm") {
40
- return ` - name: Setup pnpm\n uses: pnpm/action-setup@v4\n with:\n version: 9\n\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;
50
+ if (profile.manager === "pnpm") {
51
+ lines.push(` - name: Prepare pnpm\n run: corepack prepare pnpm@9 --activate`);
41
52
  }
42
- return ` - name: Install dependencies\n run: npm ci`;
53
+ return lines.join("\n\n");
43
54
  }
44
- function minimalWorkflowTemplate(scheduleBlock, packageManager) {
45
- return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --gate check \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
55
+ function minimalWorkflowTemplate(scheduleBlock, profile) {
56
+ return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n${runtimeSetupSteps(profile)}\n\n${installStep(profile)}\n\n - name: Run dependency check\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --gate check \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
46
57
  }
47
- function strictWorkflowTemplate(scheduleBlock, packageManager) {
48
- return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
58
+ function strictWorkflowTemplate(scheduleBlock, profile) {
59
+ return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n${runtimeSetupSteps(profile)}\n\n${installStep(profile)}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
49
60
  }
50
- function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
51
- let detectedPmInstall = "npm ci";
52
- let testCmd = "npm test";
53
- if (packageManager === "pnpm") {
54
- detectedPmInstall =
55
- "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile";
56
- testCmd = "pnpm test";
57
- }
58
- else if (packageManager === "bun") {
59
- detectedPmInstall = "bun install";
60
- testCmd = "bun test";
61
- }
62
- return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Replay approved plan with verification\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate upgrade \\\n --from-plan .artifacts/decision-plan.json \\\n --verify test \\\n --test-command "${testCmd}" \\\n --verification-report-file .artifacts/verification-node-\${{ matrix.node }}.json\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
61
+ function enterpriseWorkflowTemplate(scheduleBlock, profile) {
62
+ const install = buildInstallInvocation(profile, { frozen: true, ci: true });
63
+ const testCmd = buildTestCommand(profile);
64
+ return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n${profile.manager === "pnpm" || profile.manager === "yarn" ? ' - name: Enable Corepack\n run: corepack enable\n\n' : ""}${profile.manager === "pnpm" ? ' - name: Prepare pnpm\n run: corepack prepare pnpm@9 --activate\n\n' : ""} - name: Install dependencies\n run: ${install.display}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Replay approved plan with verification\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate upgrade \\\n --from-plan .artifacts/decision-plan.json \\\n --verify test \\\n --test-command "${testCmd}" \\\n --verification-report-file .artifacts/verification-node-\${{ matrix.node }}.json\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
63
65
  }
@@ -1,4 +1,4 @@
1
- import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel, DashboardOptions, GaOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel, DashboardOptions, GaOptions, HookOptions } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -58,6 +58,9 @@ export type ParsedCliArgs = {
58
58
  } | {
59
59
  command: "ga";
60
60
  options: GaOptions;
61
+ } | {
62
+ command: "hook";
63
+ options: HookOptions;
61
64
  };
62
65
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
63
66
  export declare function ensureRiskLevel(value: string): RiskLevel;
@@ -25,6 +25,7 @@ const KNOWN_COMMANDS = [
25
25
  "doctor",
26
26
  "dashboard",
27
27
  "ga",
28
+ "hook",
28
29
  ];
29
30
  export async function parseCliArgs(argv) {
30
31
  const firstArg = argv[0];
@@ -82,6 +83,10 @@ export async function parseCliArgs(argv) {
82
83
  const { parseGaArgs } = await import("../commands/ga/parser.js");
83
84
  return { command, options: parseGaArgs(args) };
84
85
  }
86
+ if (command === "hook") {
87
+ const { parseHookArgs } = await import("../commands/hook/parser.js");
88
+ return { command, options: parseHookArgs(args) };
89
+ }
85
90
  const base = {
86
91
  cwd: getRuntimeCwd(),
87
92
  target: "latest",
@@ -117,6 +122,11 @@ export async function parseCliArgs(argv) {
117
122
  cooldownDays: undefined,
118
123
  prLimit: undefined,
119
124
  onlyChanged: false,
125
+ affected: false,
126
+ staged: false,
127
+ baseRef: undefined,
128
+ headRef: undefined,
129
+ sinceRef: undefined,
120
130
  ciProfile: "minimal",
121
131
  lockfileMode: "preserve",
122
132
  interactive: false,
@@ -457,6 +467,38 @@ export async function parseCliArgs(argv) {
457
467
  base.onlyChanged = true;
458
468
  continue;
459
469
  }
470
+ if (current === "--affected") {
471
+ base.affected = true;
472
+ continue;
473
+ }
474
+ if (current === "--staged") {
475
+ base.staged = true;
476
+ continue;
477
+ }
478
+ if (current === "--base" && next) {
479
+ base.baseRef = next;
480
+ index += 1;
481
+ continue;
482
+ }
483
+ if (current === "--base") {
484
+ throw new Error("Missing value for --base");
485
+ }
486
+ if (current === "--head" && next) {
487
+ base.headRef = next;
488
+ index += 1;
489
+ continue;
490
+ }
491
+ if (current === "--head") {
492
+ throw new Error("Missing value for --head");
493
+ }
494
+ if (current === "--since" && next) {
495
+ base.sinceRef = next;
496
+ index += 1;
497
+ continue;
498
+ }
499
+ if (current === "--since") {
500
+ throw new Error("Missing value for --since");
501
+ }
460
502
  if (current === "--interactive") {
461
503
  base.interactive = true;
462
504
  continue;
@@ -726,6 +768,21 @@ function applyConfig(base, config) {
726
768
  if (typeof config.onlyChanged === "boolean") {
727
769
  base.onlyChanged = config.onlyChanged;
728
770
  }
771
+ if (typeof config.affected === "boolean") {
772
+ base.affected = config.affected;
773
+ }
774
+ if (typeof config.staged === "boolean") {
775
+ base.staged = config.staged;
776
+ }
777
+ if (typeof config.baseRef === "string" && config.baseRef.length > 0) {
778
+ base.baseRef = config.baseRef;
779
+ }
780
+ if (typeof config.headRef === "string" && config.headRef.length > 0) {
781
+ base.headRef = config.headRef;
782
+ }
783
+ if (typeof config.sinceRef === "string" && config.sinceRef.length > 0) {
784
+ base.sinceRef = config.sinceRef;
785
+ }
729
786
  if (typeof config.ciProfile === "string") {
730
787
  base.ciProfile = ensureCiProfile(config.ciProfile);
731
788
  }
@@ -1,4 +1,4 @@
1
- import { detectPackageManager, resolvePackageManager } from "../pm/detect.js";
1
+ import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile, detectPackageManagerDetails, } from "../pm/detect.js";
2
2
  import { installDependencies } from "../pm/install.js";
3
3
  import { stableStringify } from "../utils/stable-json.js";
4
4
  import { writeFileAtomic } from "../utils/io.js";
@@ -15,15 +15,17 @@ export async function runVerification(options) {
15
15
  return result;
16
16
  }
17
17
  const checks = [];
18
- const detected = await detectPackageManager(options.cwd);
18
+ const detected = await detectPackageManagerDetails(options.cwd);
19
+ const profile = createPackageManagerProfile(options.packageManager, detected, "bun");
19
20
  if (includesInstall(mode)) {
20
- checks.push(await runCheck("install", `${resolvePackageManager(options.packageManager, detected)} install`, async () => {
21
+ const installInvocation = buildInstallInvocation(profile);
22
+ checks.push(await runCheck("install", installInvocation.display, async () => {
21
23
  await installDependencies(options.cwd, options.packageManager, detected);
22
24
  }));
23
25
  }
24
26
  if (includesTest(mode)) {
25
27
  const command = options.testCommand ??
26
- defaultTestCommand(options.packageManager, detected);
28
+ defaultTestCommand(profile);
27
29
  checks.push(await runShellCheck(options.cwd, command));
28
30
  }
29
31
  const result = {
@@ -40,8 +42,8 @@ function includesInstall(mode) {
40
42
  function includesTest(mode) {
41
43
  return mode === "test" || mode === "install,test";
42
44
  }
43
- function defaultTestCommand(packageManager, detected) {
44
- return `${resolvePackageManager(packageManager, detected, "bun")} test`;
45
+ function defaultTestCommand(profile) {
46
+ return buildTestCommand(profile);
45
47
  }
46
48
  async function runShellCheck(cwd, command) {
47
49
  const startedAt = Date.now();
@@ -14,7 +14,11 @@ export async function warmCache(options) {
14
14
  let registryMs = 0;
15
15
  const discoveryStartedAt = Date.now();
16
16
  const packageManager = await detectPackageManager(options.cwd);
17
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
17
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
18
+ git: options,
19
+ includeKinds: options.includeKinds,
20
+ includeDependents: options.affected === true,
21
+ });
18
22
  discoveryMs += Date.now() - discoveryStartedAt;
19
23
  const cache = await VersionCache.create();
20
24
  const registryClient = new NpmRegistryClient(options.cwd, {
@@ -0,0 +1 @@
1
+ export declare const CLI_VERSION = "0.6.1";
@@ -0,0 +1,2 @@
1
+ // This file is generated by scripts/sync-version.mjs.
2
+ export const CLI_VERSION = "0.6.1";
@@ -0,0 +1,19 @@
1
+ import type { DependencyKind } from "../types/index.js";
2
+ export interface GitScopeOptions {
3
+ onlyChanged?: boolean;
4
+ affected?: boolean;
5
+ staged?: boolean;
6
+ baseRef?: string;
7
+ headRef?: string;
8
+ sinceRef?: string;
9
+ }
10
+ export interface ScopedPackageDirsResult {
11
+ packageDirs: string[];
12
+ warnings: string[];
13
+ changedFiles: string[];
14
+ }
15
+ export declare function hasGitScope(options: GitScopeOptions): boolean;
16
+ export declare function scopePackageDirsByGit(cwd: string, packageDirs: string[], options: GitScopeOptions, config?: {
17
+ includeKinds?: DependencyKind[];
18
+ includeDependents?: boolean;
19
+ }): Promise<ScopedPackageDirsResult>;