@rainy-updates/cli 0.5.7 → 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 (105) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +11 -126
  4. package/dist/bin/dispatch.js +35 -32
  5. package/dist/bin/help.js +79 -2
  6. package/dist/bin/main.d.ts +1 -0
  7. package/dist/bin/main.js +126 -0
  8. package/dist/cache/cache.js +13 -11
  9. package/dist/commands/audit/parser.js +38 -2
  10. package/dist/commands/audit/runner.js +41 -61
  11. package/dist/commands/audit/targets.js +13 -13
  12. package/dist/commands/bisect/oracle.js +31 -11
  13. package/dist/commands/bisect/parser.js +3 -3
  14. package/dist/commands/bisect/runner.js +16 -8
  15. package/dist/commands/changelog/fetcher.js +11 -5
  16. package/dist/commands/dashboard/parser.js +144 -1
  17. package/dist/commands/dashboard/runner.d.ts +2 -2
  18. package/dist/commands/dashboard/runner.js +67 -37
  19. package/dist/commands/doctor/parser.js +53 -4
  20. package/dist/commands/doctor/runner.js +2 -2
  21. package/dist/commands/ga/parser.js +43 -4
  22. package/dist/commands/ga/runner.js +22 -13
  23. package/dist/commands/health/parser.js +38 -2
  24. package/dist/commands/health/runner.js +5 -1
  25. package/dist/commands/hook/parser.d.ts +2 -0
  26. package/dist/commands/hook/parser.js +40 -0
  27. package/dist/commands/hook/runner.d.ts +2 -0
  28. package/dist/commands/hook/runner.js +174 -0
  29. package/dist/commands/licenses/parser.js +39 -0
  30. package/dist/commands/licenses/runner.js +9 -5
  31. package/dist/commands/resolve/graph/builder.js +5 -1
  32. package/dist/commands/resolve/parser.js +39 -0
  33. package/dist/commands/resolve/runner.js +14 -4
  34. package/dist/commands/review/parser.js +101 -4
  35. package/dist/commands/review/runner.js +31 -5
  36. package/dist/commands/snapshot/parser.js +39 -0
  37. package/dist/commands/snapshot/runner.js +21 -18
  38. package/dist/commands/snapshot/store.d.ts +0 -12
  39. package/dist/commands/snapshot/store.js +26 -38
  40. package/dist/commands/unused/parser.js +39 -0
  41. package/dist/commands/unused/runner.js +10 -8
  42. package/dist/commands/unused/scanner.d.ts +2 -1
  43. package/dist/commands/unused/scanner.js +65 -52
  44. package/dist/config/loader.d.ts +2 -2
  45. package/dist/config/loader.js +2 -5
  46. package/dist/config/policy.js +20 -11
  47. package/dist/core/analysis/run-silenced.js +0 -1
  48. package/dist/core/artifacts.js +6 -5
  49. package/dist/core/baseline.js +3 -5
  50. package/dist/core/check.js +7 -3
  51. package/dist/core/ci.js +52 -1
  52. package/dist/core/decision-plan.d.ts +14 -0
  53. package/dist/core/decision-plan.js +107 -0
  54. package/dist/core/doctor/result.js +8 -5
  55. package/dist/core/fix-pr-batch.js +38 -28
  56. package/dist/core/fix-pr.js +27 -24
  57. package/dist/core/init-ci.js +34 -28
  58. package/dist/core/options.d.ts +4 -1
  59. package/dist/core/options.js +152 -4
  60. package/dist/core/review-model.js +3 -0
  61. package/dist/core/summary.js +6 -0
  62. package/dist/core/upgrade.js +64 -2
  63. package/dist/core/verification.d.ts +2 -0
  64. package/dist/core/verification.js +108 -0
  65. package/dist/core/warm-cache.js +7 -3
  66. package/dist/generated/version.d.ts +1 -0
  67. package/dist/generated/version.js +2 -0
  68. package/dist/git/scope.d.ts +19 -0
  69. package/dist/git/scope.js +167 -0
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js +1 -0
  72. package/dist/output/format.js +15 -0
  73. package/dist/output/github.js +6 -0
  74. package/dist/output/sarif.js +12 -18
  75. package/dist/parsers/package-json.js +2 -4
  76. package/dist/pm/detect.d.ts +40 -1
  77. package/dist/pm/detect.js +152 -9
  78. package/dist/pm/install.d.ts +3 -1
  79. package/dist/pm/install.js +18 -17
  80. package/dist/registry/npm.js +34 -76
  81. package/dist/rup +0 -0
  82. package/dist/types/index.d.ts +134 -5
  83. package/dist/ui/tui.d.ts +4 -1
  84. package/dist/ui/tui.js +156 -67
  85. package/dist/utils/io.js +5 -6
  86. package/dist/utils/lockfile.js +24 -19
  87. package/dist/utils/runtime-paths.d.ts +4 -0
  88. package/dist/utils/runtime-paths.js +35 -0
  89. package/dist/utils/runtime.d.ts +7 -0
  90. package/dist/utils/runtime.js +32 -0
  91. package/dist/workspace/discover.d.ts +7 -1
  92. package/dist/workspace/discover.js +67 -54
  93. package/package.json +24 -19
  94. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  95. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  96. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  97. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  98. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  99. package/dist/ui/dashboard/components/Footer.js +0 -9
  100. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  101. package/dist/ui/dashboard/components/Header.js +0 -12
  102. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  103. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  104. package/dist/ui/dashboard/store.d.ts +0 -34
  105. package/dist/ui/dashboard/store.js +0 -148
package/dist/core/ci.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { check } from "./check.js";
2
2
  import { warmCache } from "./warm-cache.js";
3
+ import { buildReviewResult, createDoctorResult } from "./review-model.js";
4
+ import { createDecisionPlan, defaultDecisionPlanPath, writeDecisionPlan, } from "./decision-plan.js";
5
+ import { upgrade } from "./upgrade.js";
3
6
  export async function runCi(options) {
4
7
  const profile = options.ciProfile;
5
8
  if (profile !== "minimal") {
@@ -16,5 +19,53 @@ export async function runCi(options) {
16
19
  offline: profile === "minimal" ? options.offline : true,
17
20
  concurrency: profile === "enterprise" ? Math.max(options.concurrency, 32) : options.concurrency,
18
21
  };
19
- return await check(checkOptions);
22
+ if (options.ciGate === "check") {
23
+ return await check(checkOptions);
24
+ }
25
+ if (options.ciGate === "doctor") {
26
+ const review = await buildReviewResult(checkOptions);
27
+ createDoctorResult(review);
28
+ return reviewToCheckResult(review);
29
+ }
30
+ if (options.ciGate === "review") {
31
+ const review = await buildReviewResult(checkOptions);
32
+ const selectedItems = review.items.filter((item) => item.update.selectedByDefault !== false &&
33
+ item.update.decisionState !== "blocked");
34
+ const decisionPlanFile = options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd);
35
+ const plan = createDecisionPlan({
36
+ review,
37
+ selectedItems,
38
+ sourceCommand: "ci",
39
+ mode: "review",
40
+ focus: "all",
41
+ });
42
+ await writeDecisionPlan(decisionPlanFile, plan);
43
+ review.decisionPlan = plan;
44
+ review.summary.decisionPlan = decisionPlanFile;
45
+ review.summary.interactiveSurface = "dashboard";
46
+ review.summary.queueFocus = "all";
47
+ review.summary.suggestedCommand = `rup upgrade --from-plan ${decisionPlanFile}`;
48
+ return reviewToCheckResult(review);
49
+ }
50
+ const decisionPlanFile = options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd);
51
+ return upgrade({
52
+ ...checkOptions,
53
+ install: false,
54
+ packageManager: "auto",
55
+ sync: checkOptions.workspace,
56
+ fromPlanFile: decisionPlanFile,
57
+ });
58
+ }
59
+ function reviewToCheckResult(review) {
60
+ return {
61
+ projectPath: review.projectPath,
62
+ packagePaths: review.analysis.check.packagePaths,
63
+ packageManager: review.analysis.check.packageManager,
64
+ target: review.target,
65
+ timestamp: review.analysis.check.timestamp,
66
+ summary: review.summary,
67
+ updates: review.updates,
68
+ errors: review.errors,
69
+ warnings: review.warnings,
70
+ };
20
71
  }
@@ -0,0 +1,14 @@
1
+ import type { DashboardMode, DecisionPlan, PackageUpdate, QueueFocus, ReviewItem, ReviewResult, UpgradeOptions } from "../types/index.js";
2
+ export declare function defaultDecisionPlanPath(cwd: string): string;
3
+ export declare function filterReviewItemsByFocus(items: ReviewItem[], focus: QueueFocus): ReviewItem[];
4
+ export declare function createDecisionPlan(input: {
5
+ review: ReviewResult;
6
+ selectedItems: ReviewItem[];
7
+ sourceCommand: string;
8
+ mode: DashboardMode;
9
+ focus: QueueFocus;
10
+ }): DecisionPlan;
11
+ export declare function writeDecisionPlan(filePath: string, plan: DecisionPlan): Promise<void>;
12
+ export declare function readDecisionPlan(filePath: string): Promise<DecisionPlan>;
13
+ export declare function selectedUpdatesFromPlan(plan: DecisionPlan): PackageUpdate[];
14
+ export declare function resolveDecisionPlanPath(options: Pick<UpgradeOptions, "cwd" | "decisionPlanFile">, explicit?: string): string;
@@ -0,0 +1,107 @@
1
+ import path from "node:path";
2
+ import { stableStringify } from "../utils/stable-json.js";
3
+ import { writeFileAtomic } from "../utils/io.js";
4
+ export function defaultDecisionPlanPath(cwd) {
5
+ return path.resolve(cwd, ".artifacts/decision-plan.json");
6
+ }
7
+ export function filterReviewItemsByFocus(items, focus) {
8
+ if (focus === "security") {
9
+ return items.filter((item) => item.advisories.length > 0);
10
+ }
11
+ if (focus === "risk") {
12
+ return items.filter((item) => item.update.riskLevel === "critical" ||
13
+ item.update.riskLevel === "high");
14
+ }
15
+ if (focus === "major") {
16
+ return items.filter((item) => item.update.diffType === "major");
17
+ }
18
+ if (focus === "blocked") {
19
+ return items.filter((item) => item.update.decisionState === "blocked");
20
+ }
21
+ if (focus === "workspace") {
22
+ return items.filter((item) => Boolean(item.update.workspaceGroup));
23
+ }
24
+ return items;
25
+ }
26
+ export function createDecisionPlan(input) {
27
+ const selectedKeys = new Set(input.selectedItems.map((item) => packageUpdateKey(item.update)));
28
+ const items = input.review.items.map((item) => ({
29
+ ...toDecisionPlanItem(item.update),
30
+ selected: selectedKeys.has(packageUpdateKey(item.update)),
31
+ }));
32
+ return {
33
+ contractVersion: "1",
34
+ createdAt: new Date().toISOString(),
35
+ sourceCommand: input.sourceCommand,
36
+ mode: input.mode,
37
+ focus: input.focus,
38
+ projectPath: input.review.projectPath,
39
+ target: input.review.target,
40
+ interactiveSurface: "dashboard",
41
+ summary: {
42
+ totalItems: items.length,
43
+ selectedItems: items.filter((item) => item.selected).length,
44
+ },
45
+ items,
46
+ };
47
+ }
48
+ export async function writeDecisionPlan(filePath, plan) {
49
+ await writeFileAtomic(filePath, stableStringify(plan, 2) + "\n");
50
+ }
51
+ export async function readDecisionPlan(filePath) {
52
+ const parsed = (await Bun.file(filePath).json());
53
+ if (parsed.contractVersion !== "1" ||
54
+ !Array.isArray(parsed.items) ||
55
+ typeof parsed.projectPath !== "string") {
56
+ throw new Error(`Invalid decision plan: ${filePath}`);
57
+ }
58
+ return parsed;
59
+ }
60
+ export function selectedUpdatesFromPlan(plan) {
61
+ return plan.items.filter((item) => item.selected).map(toPackageUpdate);
62
+ }
63
+ export function resolveDecisionPlanPath(options, explicit) {
64
+ return (explicit ?? options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd));
65
+ }
66
+ function toDecisionPlanItem(update) {
67
+ return {
68
+ packagePath: update.packagePath,
69
+ name: update.name,
70
+ kind: update.kind,
71
+ fromRange: update.fromRange,
72
+ toRange: update.toRange,
73
+ toVersionResolved: update.toVersionResolved,
74
+ diffType: update.diffType,
75
+ riskLevel: update.riskLevel,
76
+ riskScore: update.riskScore,
77
+ policyAction: update.policyAction,
78
+ decisionState: update.decisionState,
79
+ selected: true,
80
+ };
81
+ }
82
+ function toPackageUpdate(item) {
83
+ return {
84
+ packagePath: item.packagePath,
85
+ name: item.name,
86
+ kind: item.kind,
87
+ fromRange: item.fromRange,
88
+ toRange: item.toRange,
89
+ toVersionResolved: item.toVersionResolved,
90
+ diffType: item.diffType,
91
+ filtered: false,
92
+ autofix: true,
93
+ riskLevel: item.riskLevel,
94
+ riskScore: item.riskScore,
95
+ policyAction: item.policyAction,
96
+ decisionState: item.decisionState,
97
+ };
98
+ }
99
+ function packageUpdateKey(update) {
100
+ return [
101
+ update.packagePath,
102
+ update.kind,
103
+ update.name,
104
+ update.fromRange,
105
+ update.toRange,
106
+ ].join("::");
107
+ }
@@ -17,6 +17,7 @@ export function createDoctorResult(review) {
17
17
  review.summary.primaryFindingCode = findings[0]?.code;
18
18
  review.summary.primaryFindingCategory = findings[0]?.category;
19
19
  review.summary.nextActionReason = nextActionReason;
20
+ review.summary.suggestedCommand = recommendedCommand;
20
21
  return {
21
22
  verdict,
22
23
  score,
@@ -31,11 +32,13 @@ export function createDoctorResult(review) {
31
32
  }
32
33
  function recommendDoctorCommand(review, verdict) {
33
34
  if (verdict === "blocked")
34
- return "rup review --interactive";
35
- if ((review.summary.securityPackages ?? 0) > 0)
36
- return "rup review --security-only";
37
- if (review.errors.length > 0 || review.items.length > 0)
38
- return "rup review --interactive";
35
+ return "rup dashboard --mode review --focus blocked";
36
+ if ((review.summary.securityPackages ?? 0) > 0) {
37
+ return "rup dashboard --mode review --focus security";
38
+ }
39
+ if (review.errors.length > 0 || review.items.length > 0) {
40
+ return "rup dashboard --mode review";
41
+ }
39
42
  return "rup check";
40
43
  }
41
44
  function describeNextActionReason(review, verdict) {
@@ -1,8 +1,7 @@
1
- import { spawn } from "node:child_process";
2
- import { promises as fs } from "node:fs";
3
- import os from "node:os";
1
+ import { $ } from "bun";
4
2
  import path from "node:path";
5
3
  import { readManifest, writeManifest } from "../parsers/package-json.js";
4
+ import { createTempDir } from "../utils/runtime-paths.js";
6
5
  export async function applyFixPrBatches(options, result) {
7
6
  const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
8
7
  if (!options.fixPr || autofixUpdates.length === 0) {
@@ -12,14 +11,25 @@ export async function applyFixPrBatches(options, result) {
12
11
  const groups = groupUpdates(autofixUpdates, options.groupBy);
13
12
  const plans = planFixPrBatches(groups, options.fixBranch ?? "chore/rainy-updates", options.fixPrBatchSize ?? 1);
14
13
  if (options.fixDryRun) {
15
- return { applied: false, branches: plans.map((plan) => plan.branchName), commits: [] };
14
+ return {
15
+ applied: false,
16
+ branches: plans.map((plan) => plan.branchName),
17
+ commits: [],
18
+ };
16
19
  }
17
20
  const branches = [];
18
21
  const commits = [];
19
22
  for (const plan of plans) {
20
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "rainy-fix-pr-batch-"));
23
+ const tempDir = await createTempDir("rainy-fix-pr-batch-");
21
24
  try {
22
- await runGit(options.cwd, ["worktree", "add", "-B", plan.branchName, tempDir, baseRef]);
25
+ await runGit(options.cwd, [
26
+ "worktree",
27
+ "add",
28
+ "-B",
29
+ plan.branchName,
30
+ tempDir,
31
+ baseRef,
32
+ ]);
23
33
  await applyUpdatesInWorktree(options.cwd, tempDir, plan.updates);
24
34
  await stageUpdatedManifests(options.cwd, tempDir, plan.updates);
25
35
  const message = renderCommitMessage(options, plan, plans.length);
@@ -47,7 +57,9 @@ export function planFixPrBatches(groups, baseBranch, batchSize) {
47
57
  chunks.push(groups.slice(index, index + size));
48
58
  }
49
59
  return chunks.map((chunk, index) => {
50
- const suffix = chunk.length === 1 ? sanitizeBranchToken(chunk[0]?.key ?? `batch-${index + 1}`) : `batch-${index + 1}`;
60
+ const suffix = chunk.length === 1
61
+ ? sanitizeBranchToken(chunk[0]?.key ?? `batch-${index + 1}`)
62
+ : `batch-${index + 1}`;
51
63
  return {
52
64
  index: index + 1,
53
65
  groups: chunk,
@@ -147,27 +159,25 @@ function renderCommitMessage(options, plan, totalBatches) {
147
159
  return `${baseMessage} (${plan.index}/${totalBatches}, ${plan.updates.length} updates)`;
148
160
  }
149
161
  function sanitizeBranchToken(value) {
150
- return value.replace(/^@/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "batch";
162
+ return (value
163
+ .replace(/^@/, "")
164
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
165
+ .replace(/^-+|-+$/g, "") || "batch");
151
166
  }
152
167
  async function runGit(cwd, args, allowNonZero = false) {
153
- return await new Promise((resolve, reject) => {
154
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
155
- let stdout = "";
156
- let stderr = "";
157
- child.stdout.on("data", (chunk) => {
158
- stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
159
- });
160
- child.stderr.on("data", (chunk) => {
161
- stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
162
- });
163
- child.on("error", reject);
164
- child.on("exit", (code) => {
165
- const normalized = code ?? 1;
166
- if (normalized !== 0 && !allowNonZero) {
167
- reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
168
- return;
169
- }
170
- resolve({ code: normalized, stdout, stderr });
171
- });
172
- });
168
+ try {
169
+ const res = await $ `git ${args}`.cwd(cwd).quiet().nothrow();
170
+ const code = res.exitCode;
171
+ const stdout = res.stdout.toString();
172
+ const stderr = res.stderr.toString();
173
+ if (code !== 0 && !allowNonZero) {
174
+ throw new Error(`git ${args.join(" ")} failed (${code}): ${stderr.trim()}`);
175
+ }
176
+ return { code, stdout, stderr };
177
+ }
178
+ catch (err) {
179
+ if (allowNonZero)
180
+ return { code: 1, stdout: "", stderr: "" };
181
+ throw err instanceof Error ? err : new Error(String(err));
182
+ }
173
183
  }
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { $ } from "bun";
2
2
  import path from "node:path";
3
3
  export async function applyFixPr(options, result, extraFiles) {
4
4
  if (!options.fixPr)
@@ -42,7 +42,8 @@ export async function applyFixPr(options, result, extraFiles) {
42
42
  : [];
43
43
  const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles, ...lockfileFiles]
44
44
  .map((entry) => path.resolve(options.cwd, entry))
45
- .filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) || entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
45
+ .filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) ||
46
+ entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
46
47
  if (filesToStage.length > 0) {
47
48
  await runGit(options.cwd, ["add", "--", ...filesToStage]);
48
49
  }
@@ -54,7 +55,8 @@ export async function applyFixPr(options, result, extraFiles) {
54
55
  commitSha: "",
55
56
  };
56
57
  }
57
- const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${autofixUpdates.length} updates)`;
58
+ const message = options.fixCommitMessage ??
59
+ `chore(deps): apply rainy-updates (${autofixUpdates.length} updates)`;
58
60
  await runGit(options.cwd, ["commit", "-m", message]);
59
61
  const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
60
62
  return {
@@ -64,30 +66,31 @@ export async function applyFixPr(options, result, extraFiles) {
64
66
  };
65
67
  }
66
68
  async function runGit(cwd, args, allowNonZero = false) {
67
- return await new Promise((resolve, reject) => {
68
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
69
- let stdout = "";
70
- let stderr = "";
71
- child.stdout.on("data", (chunk) => {
72
- stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
73
- });
74
- child.stderr.on("data", (chunk) => {
75
- stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
76
- });
77
- child.on("error", reject);
78
- child.on("exit", (code) => {
79
- const normalized = code ?? 1;
80
- if (normalized !== 0 && !allowNonZero) {
81
- reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
82
- return;
83
- }
84
- resolve({ code: normalized, stdout, stderr });
85
- });
86
- });
69
+ try {
70
+ const res = await $ `git ${args}`.cwd(cwd).quiet().nothrow();
71
+ const code = res.exitCode;
72
+ const stdout = res.stdout.toString();
73
+ const stderr = res.stderr.toString();
74
+ if (code !== 0 && !allowNonZero) {
75
+ throw new Error(`git ${args.join(" ")} failed (${code}): ${stderr.trim()}`);
76
+ }
77
+ return { code, stdout, stderr };
78
+ }
79
+ catch (err) {
80
+ if (allowNonZero)
81
+ return { code: 1, stdout: "", stderr: "" };
82
+ throw err instanceof Error ? err : new Error(String(err));
83
+ }
87
84
  }
88
85
  async function collectChangedLockfiles(cwd) {
89
86
  const status = await runGit(cwd, ["status", "--porcelain"], true);
90
- const allowed = new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
87
+ const allowed = new Set([
88
+ "package-lock.json",
89
+ "npm-shrinkwrap.json",
90
+ "pnpm-lock.yaml",
91
+ "yarn.lock",
92
+ "bun.lock",
93
+ ]);
91
94
  const changed = status.stdout
92
95
  .split(/\r?\n/)
93
96
  .map((line) => line.trim())
@@ -1,17 +1,20 @@
1
- import { access, writeFile, mkdir } from "node:fs/promises";
1
+ import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile, detectPackageManagerDetails, } from "../pm/detect.js";
3
4
  export async function initCiWorkflow(cwd, force, options) {
4
5
  const workflowPath = path.join(cwd, ".github", "workflows", "rainy-updates.yml");
5
6
  try {
6
7
  if (!force) {
7
- await access(workflowPath);
8
- return { path: workflowPath, created: false };
8
+ if (await Bun.file(workflowPath).exists()) {
9
+ return { path: workflowPath, created: false };
10
+ }
9
11
  }
10
12
  }
11
13
  catch {
12
14
  // missing file, continue create
13
15
  }
14
- const packageManager = await detectPackageManager(cwd);
16
+ const detected = await detectPackageManagerDetails(cwd);
17
+ const packageManager = createPackageManagerProfile("auto", detected);
15
18
  const scheduleBlock = renderScheduleBlock(options.schedule);
16
19
  const workflow = options.mode === "minimal"
17
20
  ? minimalWorkflowTemplate(scheduleBlock, packageManager)
@@ -19,19 +22,9 @@ export async function initCiWorkflow(cwd, force, options) {
19
22
  ? strictWorkflowTemplate(scheduleBlock, packageManager)
20
23
  : enterpriseWorkflowTemplate(scheduleBlock, packageManager);
21
24
  await mkdir(path.dirname(workflowPath), { recursive: true });
22
- await writeFile(workflowPath, workflow, "utf8");
25
+ await Bun.write(workflowPath, workflow);
23
26
  return { path: workflowPath, created: true };
24
27
  }
25
- async function detectPackageManager(cwd) {
26
- const pnpmLock = path.join(cwd, "pnpm-lock.yaml");
27
- try {
28
- await access(pnpmLock);
29
- return "pnpm";
30
- }
31
- catch {
32
- return "npm";
33
- }
34
- }
35
28
  function renderScheduleBlock(schedule) {
36
29
  if (schedule === "off") {
37
30
  return " workflow_dispatch:";
@@ -39,21 +32,34 @@ function renderScheduleBlock(schedule) {
39
32
  const cron = schedule === "daily" ? "0 8 * * *" : "0 8 * * 1";
40
33
  return ` schedule:\n - cron: '${cron}'\n workflow_dispatch:`;
41
34
  }
42
- function installStep(packageManager) {
43
- if (packageManager === "pnpm") {
44
- 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`;
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`);
49
+ }
50
+ if (profile.manager === "pnpm") {
51
+ lines.push(` - name: Prepare pnpm\n run: corepack prepare pnpm@9 --activate`);
45
52
  }
46
- return ` - name: Install dependencies\n run: npm ci`;
53
+ return lines.join("\n\n");
47
54
  }
48
- function minimalWorkflowTemplate(scheduleBlock, packageManager) {
49
- 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 Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\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`;
50
57
  }
51
- function strictWorkflowTemplate(scheduleBlock, packageManager) {
52
- 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 Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\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`;
53
60
  }
54
- function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
55
- const detectedPmInstall = packageManager === "pnpm"
56
- ? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
57
- : "npm ci";
58
- 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: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\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: 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`;
59
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;