@rainy-updates/cli 0.6.0 → 0.6.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 (63) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +12 -0
  3. package/dist/bin/cli.js +12 -127
  4. package/dist/bin/dispatch.js +6 -0
  5. package/dist/bin/help.js +48 -0
  6. package/dist/bin/main.d.ts +1 -0
  7. package/dist/bin/main.js +126 -0
  8. package/dist/commands/audit/parser.js +36 -0
  9. package/dist/commands/audit/runner.js +17 -18
  10. package/dist/commands/bisect/oracle.js +21 -17
  11. package/dist/commands/bisect/runner.js +4 -3
  12. package/dist/commands/dashboard/parser.js +41 -0
  13. package/dist/commands/dashboard/runner.js +3 -0
  14. package/dist/commands/doctor/parser.js +44 -0
  15. package/dist/commands/ga/parser.js +39 -0
  16. package/dist/commands/ga/runner.js +73 -9
  17. package/dist/commands/health/parser.js +36 -0
  18. package/dist/commands/health/runner.js +5 -1
  19. package/dist/commands/hook/parser.d.ts +2 -0
  20. package/dist/commands/hook/parser.js +40 -0
  21. package/dist/commands/hook/runner.d.ts +2 -0
  22. package/dist/commands/hook/runner.js +174 -0
  23. package/dist/commands/licenses/parser.js +39 -0
  24. package/dist/commands/licenses/runner.js +5 -1
  25. package/dist/commands/resolve/graph/builder.js +5 -1
  26. package/dist/commands/resolve/parser.js +39 -0
  27. package/dist/commands/resolve/runner.js +5 -0
  28. package/dist/commands/review/parser.js +44 -0
  29. package/dist/commands/snapshot/parser.js +39 -0
  30. package/dist/commands/snapshot/runner.js +4 -1
  31. package/dist/commands/unused/parser.js +39 -0
  32. package/dist/commands/unused/runner.js +4 -1
  33. package/dist/commands/unused/scanner.d.ts +2 -1
  34. package/dist/commands/unused/scanner.js +60 -44
  35. package/dist/core/check.js +5 -1
  36. package/dist/core/doctor/findings.js +4 -4
  37. package/dist/core/init-ci.js +28 -26
  38. package/dist/core/options.d.ts +4 -1
  39. package/dist/core/options.js +57 -0
  40. package/dist/core/verification.js +11 -9
  41. package/dist/core/warm-cache.js +5 -1
  42. package/dist/generated/version.d.ts +1 -0
  43. package/dist/generated/version.js +2 -0
  44. package/dist/git/scope.d.ts +19 -0
  45. package/dist/git/scope.js +167 -0
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.js +1 -0
  48. package/dist/output/sarif.js +2 -8
  49. package/dist/pm/detect.d.ts +37 -0
  50. package/dist/pm/detect.js +133 -2
  51. package/dist/pm/install.d.ts +2 -1
  52. package/dist/pm/install.js +7 -5
  53. package/dist/rup +0 -0
  54. package/dist/types/index.d.ts +59 -1
  55. package/dist/ui/dashboard-state.d.ts +7 -0
  56. package/dist/ui/dashboard-state.js +44 -0
  57. package/dist/ui/tui.d.ts +3 -0
  58. package/dist/ui/tui.js +311 -111
  59. package/dist/utils/shell.d.ts +6 -0
  60. package/dist/utils/shell.js +18 -0
  61. package/dist/workspace/discover.d.ts +7 -1
  62. package/dist/workspace/discover.js +12 -3
  63. package/package.json +16 -8
@@ -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, {
@@ -11,7 +11,7 @@ export function buildDoctorFindings(review) {
11
11
  summary: error,
12
12
  details: "Execution errors make the scan incomplete or require operator review.",
13
13
  help: "Resolve execution and registry issues before treating the run as clean.",
14
- recommendedAction: "Run `rup review --interactive` after fixing execution failures.",
14
+ recommendedAction: "Run `rup dashboard --mode review` after fixing execution failures.",
15
15
  evidence: [error],
16
16
  });
17
17
  }
@@ -43,7 +43,7 @@ export function buildDoctorFindings(review) {
43
43
  summary: `${item.update.name} has ${item.update.peerConflictSeverity} peer conflicts after the proposed upgrade.`,
44
44
  details: item.peerConflicts[0]?.suggestion,
45
45
  help: "Inspect peer dependency requirements before applying the update.",
46
- recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup review --interactive`.",
46
+ recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup dashboard --mode review`.",
47
47
  evidence: item.peerConflicts.map((conflict) => `${conflict.requester} -> ${conflict.peer}@${conflict.requiredRange}`),
48
48
  });
49
49
  }
@@ -59,7 +59,7 @@ export function buildDoctorFindings(review) {
59
59
  summary: `${item.update.name} violates the current license policy.`,
60
60
  details: item.license?.license,
61
61
  help: "Keep denied licenses out of the approved update set.",
62
- recommendedAction: item.update.recommendedAction ?? "Block this package in `rup review --interactive`.",
62
+ recommendedAction: item.update.recommendedAction ?? "Block this package in `rup dashboard --mode review --focus blocked`.",
63
63
  });
64
64
  }
65
65
  if ((item.update.advisoryCount ?? 0) > 0) {
@@ -106,7 +106,7 @@ export function buildDoctorFindings(review) {
106
106
  workspace,
107
107
  summary: `${item.update.name} is a major version upgrade.`,
108
108
  help: "Major upgrades should be reviewed explicitly before being applied.",
109
- recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup review --interactive`.",
109
+ recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup dashboard --mode review --focus major`.",
110
110
  });
111
111
  }
112
112
  if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
@@ -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,8 +1,8 @@
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";
5
- import { readEnv } from "../utils/runtime.js";
5
+ import { buildShellInvocation } from "../utils/shell.js";
6
6
  export async function runVerification(options) {
7
7
  const mode = options.verify;
8
8
  if (mode === "none") {
@@ -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,14 +42,14 @@ 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();
48
50
  try {
49
- const shell = readEnv("SHELL") || "sh";
50
- const proc = Bun.spawn([shell, "-lc", command], {
51
+ const invocation = buildShellInvocation(command);
52
+ const proc = Bun.spawn([invocation.shell, ...invocation.args], {
51
53
  cwd,
52
54
  stdin: "inherit",
53
55
  stdout: "inherit",
@@ -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.2";
@@ -0,0 +1,2 @@
1
+ // This file is generated by scripts/sync-version.mjs.
2
+ export const CLI_VERSION = "0.6.2";
@@ -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>;
@@ -0,0 +1,167 @@
1
+ import path from "node:path";
2
+ import { readManifest } from "../parsers/package-json.js";
3
+ import { buildWorkspaceGraph } from "../workspace/graph.js";
4
+ export function hasGitScope(options) {
5
+ return (options.onlyChanged === true ||
6
+ options.affected === true ||
7
+ options.staged === true ||
8
+ typeof options.baseRef === "string" ||
9
+ typeof options.headRef === "string" ||
10
+ typeof options.sinceRef === "string");
11
+ }
12
+ export async function scopePackageDirsByGit(cwd, packageDirs, options, config = {}) {
13
+ if (!hasGitScope(options)) {
14
+ return {
15
+ packageDirs,
16
+ warnings: [],
17
+ changedFiles: [],
18
+ };
19
+ }
20
+ const changedFiles = await listChangedFiles(cwd, options);
21
+ if ("warnings" in changedFiles) {
22
+ return {
23
+ packageDirs,
24
+ warnings: changedFiles.warnings,
25
+ changedFiles: [],
26
+ };
27
+ }
28
+ const changedPackageDirs = mapFilesToPackageDirs(cwd, packageDirs, changedFiles.files);
29
+ if (changedPackageDirs.length === 0) {
30
+ return {
31
+ packageDirs: [],
32
+ warnings: [],
33
+ changedFiles: changedFiles.files,
34
+ };
35
+ }
36
+ if (!config.includeDependents) {
37
+ return {
38
+ packageDirs: changedPackageDirs,
39
+ warnings: [],
40
+ changedFiles: changedFiles.files,
41
+ };
42
+ }
43
+ const affectedPackageDirs = await expandToDependents(changedPackageDirs, packageDirs, config.includeKinds ?? ["dependencies", "devDependencies"]);
44
+ return {
45
+ packageDirs: affectedPackageDirs,
46
+ warnings: [],
47
+ changedFiles: changedFiles.files,
48
+ };
49
+ }
50
+ async function listChangedFiles(cwd, options) {
51
+ const primaryArgs = buildGitDiffArgs(options);
52
+ const diff = await runGit(cwd, primaryArgs);
53
+ if (!diff.ok) {
54
+ return {
55
+ warnings: [
56
+ `Git scope could not be resolved (${diff.error}). Falling back to full workspace scan.`,
57
+ ],
58
+ };
59
+ }
60
+ const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard"]);
61
+ const files = new Set();
62
+ for (const value of [...diff.lines, ...(untracked.ok ? untracked.lines : [])]) {
63
+ const trimmed = value.trim();
64
+ if (trimmed.length === 0)
65
+ continue;
66
+ files.add(trimmed);
67
+ }
68
+ return {
69
+ files: Array.from(files).sort((left, right) => left.localeCompare(right)),
70
+ };
71
+ }
72
+ function buildGitDiffArgs(options) {
73
+ if (options.staged) {
74
+ return ["diff", "--name-only", "--cached"];
75
+ }
76
+ if (options.baseRef && options.headRef) {
77
+ return ["diff", "--name-only", `${options.baseRef}...${options.headRef}`];
78
+ }
79
+ if (options.baseRef) {
80
+ return ["diff", "--name-only", `${options.baseRef}...HEAD`];
81
+ }
82
+ if (options.sinceRef) {
83
+ return ["diff", "--name-only", `${options.sinceRef}..HEAD`];
84
+ }
85
+ return ["diff", "--name-only", "HEAD"];
86
+ }
87
+ async function runGit(cwd, args) {
88
+ try {
89
+ const proc = Bun.spawn(["git", ...args], {
90
+ cwd,
91
+ stdout: "pipe",
92
+ stderr: "pipe",
93
+ });
94
+ const [stdout, stderr, exitCode] = await Promise.all([
95
+ new Response(proc.stdout).text(),
96
+ new Response(proc.stderr).text(),
97
+ proc.exited,
98
+ ]);
99
+ if (exitCode !== 0) {
100
+ const message = stderr.trim() || `git ${args.join(" ")} exited with code ${exitCode}`;
101
+ return { ok: false, error: message };
102
+ }
103
+ return {
104
+ ok: true,
105
+ lines: stdout.split(/\r?\n/).filter(Boolean),
106
+ };
107
+ }
108
+ catch (error) {
109
+ return { ok: false, error: String(error) };
110
+ }
111
+ }
112
+ function mapFilesToPackageDirs(cwd, packageDirs, files) {
113
+ const sortedDirs = [...packageDirs].sort((left, right) => right.length - left.length);
114
+ const matched = new Set();
115
+ for (const file of files) {
116
+ const absoluteFile = path.resolve(cwd, file);
117
+ let bestMatch;
118
+ for (const packageDir of sortedDirs) {
119
+ const relative = path.relative(packageDir, absoluteFile);
120
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
121
+ bestMatch = packageDir;
122
+ break;
123
+ }
124
+ }
125
+ if (bestMatch) {
126
+ matched.add(bestMatch);
127
+ }
128
+ }
129
+ return Array.from(matched).sort((left, right) => left.localeCompare(right));
130
+ }
131
+ async function expandToDependents(changedPackageDirs, packageDirs, includeKinds) {
132
+ const manifestsByPath = new Map();
133
+ for (const packageDir of packageDirs) {
134
+ try {
135
+ manifestsByPath.set(packageDir, await readManifest(packageDir));
136
+ }
137
+ catch {
138
+ // Skip unreadable manifests; callers already handle manifest read failures.
139
+ }
140
+ }
141
+ const graph = buildWorkspaceGraph(manifestsByPath, includeKinds);
142
+ const pathByName = new Map(graph.nodes.map((node) => [node.packageName, node.packagePath]));
143
+ const nameByPath = new Map(graph.nodes.map((node) => [node.packagePath, node.packageName]));
144
+ const dependents = new Map();
145
+ for (const node of graph.nodes) {
146
+ for (const dependency of node.dependsOn) {
147
+ const list = dependents.get(dependency) ?? [];
148
+ list.push(node.packageName);
149
+ dependents.set(dependency, list);
150
+ }
151
+ }
152
+ const selected = new Set(changedPackageDirs);
153
+ const queue = changedPackageDirs
154
+ .map((packageDir) => nameByPath.get(packageDir))
155
+ .filter((value) => typeof value === "string");
156
+ while (queue.length > 0) {
157
+ const current = queue.shift();
158
+ for (const dependent of dependents.get(current) ?? []) {
159
+ const dependentPath = pathByName.get(dependent);
160
+ if (!dependentPath || selected.has(dependentPath))
161
+ continue;
162
+ selected.add(dependentPath);
163
+ queue.push(dependent);
164
+ }
165
+ }
166
+ return Array.from(selected).sort((left, right) => left.localeCompare(right));
167
+ }