@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.
- package/CHANGELOG.md +134 -0
- package/README.md +90 -31
- package/dist/bin/cli.js +11 -126
- package/dist/bin/dispatch.js +35 -32
- package/dist/bin/help.js +79 -2
- package/dist/bin/main.d.ts +1 -0
- package/dist/bin/main.js +126 -0
- package/dist/cache/cache.js +13 -11
- package/dist/commands/audit/parser.js +38 -2
- package/dist/commands/audit/runner.js +41 -61
- package/dist/commands/audit/targets.js +13 -13
- package/dist/commands/bisect/oracle.js +31 -11
- package/dist/commands/bisect/parser.js +3 -3
- package/dist/commands/bisect/runner.js +16 -8
- package/dist/commands/changelog/fetcher.js +11 -5
- package/dist/commands/dashboard/parser.js +144 -1
- package/dist/commands/dashboard/runner.d.ts +2 -2
- package/dist/commands/dashboard/runner.js +67 -37
- package/dist/commands/doctor/parser.js +53 -4
- package/dist/commands/doctor/runner.js +2 -2
- package/dist/commands/ga/parser.js +43 -4
- package/dist/commands/ga/runner.js +22 -13
- package/dist/commands/health/parser.js +38 -2
- package/dist/commands/health/runner.js +5 -1
- package/dist/commands/hook/parser.d.ts +2 -0
- package/dist/commands/hook/parser.js +40 -0
- package/dist/commands/hook/runner.d.ts +2 -0
- package/dist/commands/hook/runner.js +174 -0
- package/dist/commands/licenses/parser.js +39 -0
- package/dist/commands/licenses/runner.js +9 -5
- package/dist/commands/resolve/graph/builder.js +5 -1
- package/dist/commands/resolve/parser.js +39 -0
- package/dist/commands/resolve/runner.js +14 -4
- package/dist/commands/review/parser.js +101 -4
- package/dist/commands/review/runner.js +31 -5
- package/dist/commands/snapshot/parser.js +39 -0
- package/dist/commands/snapshot/runner.js +21 -18
- package/dist/commands/snapshot/store.d.ts +0 -12
- package/dist/commands/snapshot/store.js +26 -38
- package/dist/commands/unused/parser.js +39 -0
- package/dist/commands/unused/runner.js +10 -8
- package/dist/commands/unused/scanner.d.ts +2 -1
- package/dist/commands/unused/scanner.js +65 -52
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +2 -5
- package/dist/config/policy.js +20 -11
- package/dist/core/analysis/run-silenced.js +0 -1
- package/dist/core/artifacts.js +6 -5
- package/dist/core/baseline.js +3 -5
- package/dist/core/check.js +7 -3
- package/dist/core/ci.js +52 -1
- package/dist/core/decision-plan.d.ts +14 -0
- package/dist/core/decision-plan.js +107 -0
- package/dist/core/doctor/result.js +8 -5
- package/dist/core/fix-pr-batch.js +38 -28
- package/dist/core/fix-pr.js +27 -24
- package/dist/core/init-ci.js +34 -28
- package/dist/core/options.d.ts +4 -1
- package/dist/core/options.js +152 -4
- package/dist/core/review-model.js +3 -0
- package/dist/core/summary.js +6 -0
- package/dist/core/upgrade.js +64 -2
- package/dist/core/verification.d.ts +2 -0
- package/dist/core/verification.js +108 -0
- package/dist/core/warm-cache.js +7 -3
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/git/scope.d.ts +19 -0
- package/dist/git/scope.js +167 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/format.js +15 -0
- package/dist/output/github.js +6 -0
- package/dist/output/sarif.js +12 -18
- package/dist/parsers/package-json.js +2 -4
- package/dist/pm/detect.d.ts +40 -1
- package/dist/pm/detect.js +152 -9
- package/dist/pm/install.d.ts +3 -1
- package/dist/pm/install.js +18 -17
- package/dist/registry/npm.js +34 -76
- package/dist/rup +0 -0
- package/dist/types/index.d.ts +134 -5
- package/dist/ui/tui.d.ts +4 -1
- package/dist/ui/tui.js +156 -67
- package/dist/utils/io.js +5 -6
- package/dist/utils/lockfile.js +24 -19
- package/dist/utils/runtime-paths.d.ts +4 -0
- package/dist/utils/runtime-paths.js +35 -0
- package/dist/utils/runtime.d.ts +7 -0
- package/dist/utils/runtime.js +32 -0
- package/dist/workspace/discover.d.ts +7 -1
- package/dist/workspace/discover.js +67 -54
- package/package.json +24 -19
- package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
- package/dist/ui/dashboard/DashboardTUI.js +0 -34
- package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
- package/dist/ui/dashboard/components/DetailPanel.js +0 -30
- package/dist/ui/dashboard/components/Footer.d.ts +0 -4
- package/dist/ui/dashboard/components/Footer.js +0 -9
- package/dist/ui/dashboard/components/Header.d.ts +0 -4
- package/dist/ui/dashboard/components/Header.js +0 -12
- package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
- package/dist/ui/dashboard/components/Sidebar.js +0 -23
- package/dist/ui/dashboard/store.d.ts +0 -34
- package/dist/ui/dashboard/store.js +0 -148
|
@@ -1,47 +1,77 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
export async function runDashboard(options) {
|
|
8
|
-
|
|
9
|
-
const resolvedConfig = await loadConfig(options.cwd);
|
|
10
|
-
// Create an initial check result. In a real scenario, this could run
|
|
11
|
-
// progressively in the TUI, but for simplicity we fetch initial data first.
|
|
12
|
-
const checkResult = await check({
|
|
1
|
+
import { createDecisionPlan, defaultDecisionPlanPath, filterReviewItemsByFocus, writeDecisionPlan, } from "../../core/decision-plan.js";
|
|
2
|
+
import { buildReviewResult, renderReviewResult } from "../../core/review-model.js";
|
|
3
|
+
import { applySelectedUpdates } from "../../core/upgrade.js";
|
|
4
|
+
import { runVerification } from "../../core/verification.js";
|
|
5
|
+
import { runTui } from "../../ui/tui.js";
|
|
6
|
+
import { writeStderr, writeStdout } from "../../utils/runtime.js";
|
|
7
|
+
export async function runDashboard(options, prebuiltReview) {
|
|
8
|
+
const review = prebuiltReview ?? (await buildReviewResult({
|
|
13
9
|
...options,
|
|
14
|
-
|
|
15
|
-
logLevel: "error",
|
|
16
|
-
});
|
|
17
|
-
// Render the interactive Ink Dashboard
|
|
18
|
-
const { waitUntilExit } = render(React.createElement(DashboardTUI, {
|
|
19
|
-
options,
|
|
20
|
-
initialResult: checkResult,
|
|
10
|
+
interactive: false,
|
|
21
11
|
}));
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
const visibleItems = filterReviewItemsByFocus(review.items, options.focus);
|
|
13
|
+
const selectedItems = await selectDashboardItems(options, visibleItems);
|
|
14
|
+
const decisionPlan = createDecisionPlan({
|
|
15
|
+
review,
|
|
16
|
+
selectedItems,
|
|
17
|
+
sourceCommand: "dashboard",
|
|
18
|
+
mode: options.mode,
|
|
19
|
+
focus: options.focus,
|
|
20
|
+
});
|
|
21
|
+
const decisionPlanFile = options.decisionPlanFile ?? defaultDecisionPlanPath(options.cwd);
|
|
22
|
+
await writeDecisionPlan(decisionPlanFile, decisionPlan);
|
|
23
|
+
review.decisionPlan = decisionPlan;
|
|
24
|
+
review.summary.decisionPlan = decisionPlanFile;
|
|
25
|
+
review.summary.interactiveSurface = "dashboard";
|
|
26
|
+
review.summary.queueFocus = options.focus;
|
|
27
|
+
review.summary.suggestedCommand =
|
|
28
|
+
options.mode === "upgrade" || options.applySelected
|
|
29
|
+
? `rup upgrade --from-plan ${decisionPlanFile}`
|
|
30
|
+
: `rup upgrade --from-plan ${decisionPlanFile}`;
|
|
31
|
+
if ((options.mode === "upgrade" || options.applySelected) && selectedItems.length > 0) {
|
|
30
32
|
await applySelectedUpdates({
|
|
31
33
|
...options,
|
|
32
|
-
install: false,
|
|
34
|
+
install: false,
|
|
33
35
|
packageManager: "auto",
|
|
34
|
-
sync:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
sync: options.workspace,
|
|
37
|
+
}, selectedItems.map((item) => item.update));
|
|
38
|
+
if (options.verify !== "none") {
|
|
39
|
+
const verification = await runVerification({
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
verify: options.verify,
|
|
42
|
+
testCommand: options.testCommand,
|
|
43
|
+
verificationReportFile: options.verificationReportFile,
|
|
44
|
+
packageManager: "auto",
|
|
45
|
+
});
|
|
46
|
+
if (!verification.passed) {
|
|
47
|
+
review.errors.push(...verification.checks
|
|
48
|
+
.filter((check) => !check.passed)
|
|
49
|
+
.map((check) => `Verification failed for ${check.name}: ${check.error ?? check.command}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
}
|
|
53
|
+
writeStdout(renderReviewResult({
|
|
54
|
+
...review,
|
|
55
|
+
items: selectedItems,
|
|
56
|
+
updates: selectedItems.map((item) => item.update),
|
|
57
|
+
}) + "\n");
|
|
58
|
+
writeStderr(`[dashboard] decision plan written to ${decisionPlanFile}\n`);
|
|
42
59
|
return {
|
|
43
60
|
completed: true,
|
|
44
|
-
errors:
|
|
45
|
-
warnings:
|
|
61
|
+
errors: review.errors,
|
|
62
|
+
warnings: review.warnings,
|
|
63
|
+
selectedUpdates: selectedItems.length,
|
|
64
|
+
decisionPlanFile,
|
|
46
65
|
};
|
|
47
66
|
}
|
|
67
|
+
async function selectDashboardItems(options, visibleItems) {
|
|
68
|
+
if (visibleItems.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return runTui(visibleItems, {
|
|
72
|
+
title: options.mode === "upgrade"
|
|
73
|
+
? "Rainy Dashboard: Upgrade Queue"
|
|
74
|
+
: "Rainy Dashboard: Review Queue",
|
|
75
|
+
subtitle: `focus=${options.focus} mode=${options.mode} Enter confirms the selected decision set`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { exitProcess, getRuntimeCwd, writeStdout, } from "../../utils/runtime.js";
|
|
3
3
|
export function parseDoctorArgs(args) {
|
|
4
4
|
const options = {
|
|
5
|
-
cwd:
|
|
5
|
+
cwd: getRuntimeCwd(),
|
|
6
6
|
target: "latest",
|
|
7
7
|
filter: undefined,
|
|
8
8
|
reject: undefined,
|
|
@@ -36,11 +36,21 @@ export function parseDoctorArgs(args) {
|
|
|
36
36
|
cooldownDays: undefined,
|
|
37
37
|
prLimit: undefined,
|
|
38
38
|
onlyChanged: false,
|
|
39
|
+
affected: false,
|
|
40
|
+
staged: false,
|
|
41
|
+
baseRef: undefined,
|
|
42
|
+
headRef: undefined,
|
|
43
|
+
sinceRef: undefined,
|
|
39
44
|
ciProfile: "minimal",
|
|
40
45
|
lockfileMode: "preserve",
|
|
41
46
|
interactive: false,
|
|
42
47
|
showImpact: true,
|
|
43
48
|
showHomepage: true,
|
|
49
|
+
decisionPlanFile: undefined,
|
|
50
|
+
verify: "none",
|
|
51
|
+
testCommand: undefined,
|
|
52
|
+
verificationReportFile: undefined,
|
|
53
|
+
ciGate: "check",
|
|
44
54
|
verdictOnly: false,
|
|
45
55
|
includeChangelog: false,
|
|
46
56
|
agentReport: false,
|
|
@@ -59,6 +69,39 @@ export function parseDoctorArgs(args) {
|
|
|
59
69
|
options.workspace = true;
|
|
60
70
|
continue;
|
|
61
71
|
}
|
|
72
|
+
if (current === "--only-changed") {
|
|
73
|
+
options.onlyChanged = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (current === "--affected") {
|
|
77
|
+
options.affected = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (current === "--staged") {
|
|
81
|
+
options.staged = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (current === "--base" && next) {
|
|
85
|
+
options.baseRef = next;
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (current === "--base")
|
|
90
|
+
throw new Error("Missing value for --base");
|
|
91
|
+
if (current === "--head" && next) {
|
|
92
|
+
options.headRef = next;
|
|
93
|
+
i += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (current === "--head")
|
|
97
|
+
throw new Error("Missing value for --head");
|
|
98
|
+
if (current === "--since" && next) {
|
|
99
|
+
options.sinceRef = next;
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (current === "--since")
|
|
104
|
+
throw new Error("Missing value for --since");
|
|
62
105
|
if (current === "--verdict-only") {
|
|
63
106
|
options.verdictOnly = true;
|
|
64
107
|
continue;
|
|
@@ -79,8 +122,8 @@ export function parseDoctorArgs(args) {
|
|
|
79
122
|
if (current === "--json-file")
|
|
80
123
|
throw new Error("Missing value for --json-file");
|
|
81
124
|
if (current === "--help" || current === "-h") {
|
|
82
|
-
|
|
83
|
-
|
|
125
|
+
writeStdout(DOCTOR_HELP);
|
|
126
|
+
exitProcess(0);
|
|
84
127
|
}
|
|
85
128
|
if (current.startsWith("-"))
|
|
86
129
|
throw new Error(`Unknown doctor option: ${current}`);
|
|
@@ -99,6 +142,12 @@ Options:
|
|
|
99
142
|
--include-changelog Include release note summaries in the aggregated review data
|
|
100
143
|
--agent-report Print a prompt-ready remediation report for coding agents
|
|
101
144
|
--workspace Scan all workspace packages
|
|
145
|
+
--only-changed Limit analysis to changed packages
|
|
146
|
+
--affected Include changed packages and their dependents
|
|
147
|
+
--staged Limit analysis to staged changes
|
|
148
|
+
--base <ref> Compare changes against a base git ref
|
|
149
|
+
--head <ref> Compare changes against a head git ref
|
|
150
|
+
--since <ref> Compare changes since a git ref
|
|
102
151
|
--json-file <path> Write JSON doctor report to file
|
|
103
152
|
--cwd <path>
|
|
104
153
|
`.trimStart();
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
1
|
import { buildReviewResult, createDoctorResult, renderDoctorAgentReport, renderDoctorResult, } from "../../core/review-model.js";
|
|
3
2
|
import { stableStringify } from "../../utils/stable-json.js";
|
|
4
3
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
4
|
+
import { writeStdout } from "../../utils/runtime.js";
|
|
5
5
|
export async function runDoctor(options) {
|
|
6
6
|
const review = await buildReviewResult(options);
|
|
7
7
|
const doctor = createDoctorResult(review);
|
|
8
8
|
const output = options.agentReport
|
|
9
9
|
? renderDoctorAgentReport(doctor)
|
|
10
10
|
: renderDoctorResult(doctor, options.verdictOnly);
|
|
11
|
-
|
|
11
|
+
writeStdout(output + "\n");
|
|
12
12
|
if (options.jsonFile) {
|
|
13
13
|
await writeFileAtomic(options.jsonFile, stableStringify(doctor, 2) + "\n");
|
|
14
14
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { exitProcess, getRuntimeCwd, writeStdout, } from "../../utils/runtime.js";
|
|
3
3
|
export function parseGaArgs(args) {
|
|
4
4
|
const options = {
|
|
5
|
-
cwd:
|
|
5
|
+
cwd: getRuntimeCwd(),
|
|
6
6
|
workspace: false,
|
|
7
|
+
affected: false,
|
|
8
|
+
staged: false,
|
|
9
|
+
baseRef: undefined,
|
|
10
|
+
headRef: undefined,
|
|
11
|
+
sinceRef: undefined,
|
|
7
12
|
jsonFile: undefined,
|
|
8
13
|
};
|
|
9
14
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -20,6 +25,35 @@ export function parseGaArgs(args) {
|
|
|
20
25
|
options.workspace = true;
|
|
21
26
|
continue;
|
|
22
27
|
}
|
|
28
|
+
if (current === "--affected") {
|
|
29
|
+
options.affected = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (current === "--staged") {
|
|
33
|
+
options.staged = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (current === "--base" && next) {
|
|
37
|
+
options.baseRef = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (current === "--base")
|
|
42
|
+
throw new Error("Missing value for --base");
|
|
43
|
+
if (current === "--head" && next) {
|
|
44
|
+
options.headRef = next;
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (current === "--head")
|
|
49
|
+
throw new Error("Missing value for --head");
|
|
50
|
+
if (current === "--since" && next) {
|
|
51
|
+
options.sinceRef = next;
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (current === "--since")
|
|
56
|
+
throw new Error("Missing value for --since");
|
|
23
57
|
if (current === "--json-file" && next) {
|
|
24
58
|
options.jsonFile = path.resolve(options.cwd, next);
|
|
25
59
|
i += 1;
|
|
@@ -28,8 +62,8 @@ export function parseGaArgs(args) {
|
|
|
28
62
|
if (current === "--json-file")
|
|
29
63
|
throw new Error("Missing value for --json-file");
|
|
30
64
|
if (current === "--help" || current === "-h") {
|
|
31
|
-
|
|
32
|
-
|
|
65
|
+
writeStdout(GA_HELP);
|
|
66
|
+
exitProcess(0);
|
|
33
67
|
}
|
|
34
68
|
if (current.startsWith("-"))
|
|
35
69
|
throw new Error(`Unknown ga option: ${current}`);
|
|
@@ -45,6 +79,11 @@ Usage:
|
|
|
45
79
|
|
|
46
80
|
Options:
|
|
47
81
|
--workspace Evaluate workspace package coverage
|
|
82
|
+
--affected Evaluate changed workspace packages and dependents
|
|
83
|
+
--staged Limit GA discovery to staged changes
|
|
84
|
+
--base <ref> Compare changes against a base git ref
|
|
85
|
+
--head <ref> Compare changes against a head git ref
|
|
86
|
+
--since <ref> Compare changes since a git ref
|
|
48
87
|
--json-file <path> Write JSON GA report to file
|
|
49
88
|
--cwd <path>
|
|
50
89
|
`.trimStart();
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
2
|
import { VersionCache } from "../../cache/cache.js";
|
|
5
|
-
import {
|
|
3
|
+
import { detectPackageManagerDetails } from "../../pm/detect.js";
|
|
6
4
|
import { discoverPackageDirs } from "../../workspace/discover.js";
|
|
7
5
|
import { stableStringify } from "../../utils/stable-json.js";
|
|
8
6
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
7
|
+
import { writeStdout } from "../../utils/runtime.js";
|
|
9
8
|
export async function runGa(options) {
|
|
10
|
-
const packageManager = await
|
|
11
|
-
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace
|
|
9
|
+
const packageManager = await detectPackageManagerDetails(options.cwd);
|
|
10
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
|
|
11
|
+
git: options,
|
|
12
|
+
includeDependents: options.affected === true,
|
|
13
|
+
});
|
|
12
14
|
const cache = await VersionCache.create();
|
|
13
15
|
const checks = [];
|
|
14
16
|
checks.push({
|
|
15
17
|
name: "package-manager",
|
|
16
|
-
status: packageManager === "unknown" ? "warn" : "pass",
|
|
17
|
-
detail: packageManager === "unknown"
|
|
18
|
-
? "No supported lockfile was detected.
|
|
19
|
-
: `Detected package manager: ${packageManager}.`,
|
|
18
|
+
status: packageManager.manager === "unknown" ? "warn" : "pass",
|
|
19
|
+
detail: packageManager.manager === "unknown"
|
|
20
|
+
? "No supported lockfile was detected. Rainy can still run, but runtime/package verification will fall back to generic defaults."
|
|
21
|
+
: `Detected package manager: ${packageManager.manager} via ${packageManager.source}${packageManager.lockfile ? ` (${packageManager.lockfile})` : ""}${packageManager.packageManagerField ? ` [${packageManager.packageManagerField}]` : ""}. Runtime and verification can align with this package ecosystem, including Bun.`,
|
|
20
22
|
});
|
|
21
23
|
checks.push({
|
|
22
24
|
name: "workspace-discovery",
|
|
@@ -40,6 +42,14 @@ export async function runGa(options) {
|
|
|
40
42
|
? "Built CLI entrypoint exists in dist/bin/cli.js."
|
|
41
43
|
: "Built CLI entrypoint is missing; run the build before publishing a release artifact.",
|
|
42
44
|
});
|
|
45
|
+
const compiledBinaryExists = await fileExists(path.resolve(options.cwd, "dist/rup"));
|
|
46
|
+
checks.push({
|
|
47
|
+
name: "runtime-artifacts",
|
|
48
|
+
status: compiledBinaryExists ? "pass" : "warn",
|
|
49
|
+
detail: compiledBinaryExists
|
|
50
|
+
? "Compiled Bun runtime artifact exists in dist/rup."
|
|
51
|
+
: "Compiled Bun runtime artifact is missing; run bun run build:exe before publishing Bun-first release artifacts.",
|
|
52
|
+
});
|
|
43
53
|
checks.push({
|
|
44
54
|
name: "benchmark-gates",
|
|
45
55
|
status: (await fileExists(path.resolve(options.cwd, "scripts/perf-smoke.mjs"))) &&
|
|
@@ -61,14 +71,14 @@ export async function runGa(options) {
|
|
|
61
71
|
const result = {
|
|
62
72
|
ready: errors.length === 0,
|
|
63
73
|
projectPath: options.cwd,
|
|
64
|
-
packageManager,
|
|
74
|
+
packageManager: packageManager.manager,
|
|
65
75
|
workspacePackages: packageDirs.length,
|
|
66
76
|
cacheBackend: cache.backend,
|
|
67
77
|
checks,
|
|
68
78
|
warnings,
|
|
69
79
|
errors,
|
|
70
80
|
};
|
|
71
|
-
|
|
81
|
+
writeStdout(renderGaResult(result) + "\n");
|
|
72
82
|
if (options.jsonFile) {
|
|
73
83
|
await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
|
|
74
84
|
}
|
|
@@ -120,8 +130,7 @@ async function detectLockfile(cwd) {
|
|
|
120
130
|
}
|
|
121
131
|
async function fileExists(filePath) {
|
|
122
132
|
try {
|
|
123
|
-
await
|
|
124
|
-
return true;
|
|
133
|
+
return await Bun.file(filePath).exists();
|
|
125
134
|
}
|
|
126
135
|
catch {
|
|
127
136
|
return false;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { getRuntimeCwd } from "../../utils/runtime.js";
|
|
3
3
|
export function parseHealthArgs(args) {
|
|
4
4
|
const options = {
|
|
5
|
-
cwd:
|
|
5
|
+
cwd: getRuntimeCwd(),
|
|
6
6
|
workspace: false,
|
|
7
|
+
affected: false,
|
|
8
|
+
staged: false,
|
|
9
|
+
baseRef: undefined,
|
|
10
|
+
headRef: undefined,
|
|
11
|
+
sinceRef: undefined,
|
|
7
12
|
staleDays: 365,
|
|
8
13
|
includeDeprecated: true,
|
|
9
14
|
includeAlternatives: false,
|
|
@@ -28,6 +33,37 @@ export function parseHealthArgs(args) {
|
|
|
28
33
|
index += 1;
|
|
29
34
|
continue;
|
|
30
35
|
}
|
|
36
|
+
if (current === "--affected") {
|
|
37
|
+
options.affected = true;
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (current === "--staged") {
|
|
42
|
+
options.staged = true;
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (current === "--base" && next) {
|
|
47
|
+
options.baseRef = next;
|
|
48
|
+
index += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (current === "--base")
|
|
52
|
+
throw new Error("Missing value for --base");
|
|
53
|
+
if (current === "--head" && next) {
|
|
54
|
+
options.headRef = next;
|
|
55
|
+
index += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (current === "--head")
|
|
59
|
+
throw new Error("Missing value for --head");
|
|
60
|
+
if (current === "--since" && next) {
|
|
61
|
+
options.sinceRef = next;
|
|
62
|
+
index += 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (current === "--since")
|
|
66
|
+
throw new Error("Missing value for --since");
|
|
31
67
|
if (current === "--stale" && next) {
|
|
32
68
|
// Accept "12m" → 365, "6m" → 180, "365d" → 365, or plain number
|
|
33
69
|
const match = next.match(/^(\d+)(m|d)?$/);
|
|
@@ -89,7 +89,11 @@ export async function runHealth(options) {
|
|
|
89
89
|
errors: [],
|
|
90
90
|
warnings: [],
|
|
91
91
|
};
|
|
92
|
-
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace
|
|
92
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
|
|
93
|
+
git: options,
|
|
94
|
+
includeKinds: ["dependencies", "devDependencies", "optionalDependencies"],
|
|
95
|
+
includeDependents: options.affected === true,
|
|
96
|
+
});
|
|
93
97
|
const versionMap = new Map();
|
|
94
98
|
for (const dir of packageDirs) {
|
|
95
99
|
let manifest;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function parseHookArgs(args) {
|
|
3
|
+
let cwd = process.cwd();
|
|
4
|
+
let action = "doctor";
|
|
5
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
6
|
+
const current = args[index];
|
|
7
|
+
const next = args[index + 1];
|
|
8
|
+
if ((current === "install" || current === "uninstall" || current === "doctor") && index === 0) {
|
|
9
|
+
action = current;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (current === "--cwd" && next) {
|
|
13
|
+
cwd = path.resolve(next);
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (current === "--cwd") {
|
|
18
|
+
throw new Error("Missing value for --cwd");
|
|
19
|
+
}
|
|
20
|
+
if (current === "--help" || current === "-h") {
|
|
21
|
+
process.stdout.write(HOOK_HELP);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
if (current.startsWith("-")) {
|
|
25
|
+
throw new Error(`Unknown hook option: ${current}`);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unexpected hook argument: ${current}`);
|
|
28
|
+
}
|
|
29
|
+
return { cwd, action };
|
|
30
|
+
}
|
|
31
|
+
const HOOK_HELP = `
|
|
32
|
+
rup hook — Install or inspect Rainy-managed git hooks
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
rup hook <install|uninstall|doctor> [options]
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--cwd <path> Working directory (default: cwd)
|
|
39
|
+
--help Show this help
|
|
40
|
+
`.trimStart();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { chmod, mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeStdout } from "../../utils/runtime.js";
|
|
4
|
+
const MANAGED_MARKER = "# rainy-updates managed hook";
|
|
5
|
+
const HOOKS = [
|
|
6
|
+
{
|
|
7
|
+
name: "pre-commit",
|
|
8
|
+
command: '$(resolve_rup) unused --workspace --staged && $(resolve_rup) resolve --workspace --staged',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "pre-push",
|
|
12
|
+
command: '$(resolve_rup) audit --workspace --affected --report summary',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
export async function runHook(options) {
|
|
16
|
+
const hookDir = await resolveHookDir(options.cwd);
|
|
17
|
+
const result = {
|
|
18
|
+
action: options.action,
|
|
19
|
+
hookDir,
|
|
20
|
+
installed: [],
|
|
21
|
+
removed: [],
|
|
22
|
+
checked: [],
|
|
23
|
+
errors: [],
|
|
24
|
+
warnings: [],
|
|
25
|
+
};
|
|
26
|
+
if (options.action === "install") {
|
|
27
|
+
await mkdir(hookDir, { recursive: true });
|
|
28
|
+
for (const hook of HOOKS) {
|
|
29
|
+
const hookPath = path.join(hookDir, hook.name);
|
|
30
|
+
const existing = await readHookState(hookPath);
|
|
31
|
+
if (existing.status === "foreign") {
|
|
32
|
+
result.warnings.push(`Skipped ${hook.name}: existing hook is not Rainy-managed.`);
|
|
33
|
+
result.checked.push({ name: hook.name, status: "foreign" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
await Bun.write(hookPath, renderHookScript(hook.command));
|
|
37
|
+
await makeExecutable(hookPath);
|
|
38
|
+
result.installed.push(hook.name);
|
|
39
|
+
result.checked.push({ name: hook.name, status: "managed" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (options.action === "uninstall") {
|
|
43
|
+
for (const hook of HOOKS) {
|
|
44
|
+
const hookPath = path.join(hookDir, hook.name);
|
|
45
|
+
const existing = await readHookState(hookPath);
|
|
46
|
+
if (existing.status === "managed") {
|
|
47
|
+
await unlink(hookPath).catch(() => undefined);
|
|
48
|
+
result.removed.push(hook.name);
|
|
49
|
+
}
|
|
50
|
+
else if (existing.status === "foreign") {
|
|
51
|
+
result.warnings.push(`Left ${hook.name} untouched: existing hook is not Rainy-managed.`);
|
|
52
|
+
}
|
|
53
|
+
result.checked.push({ name: hook.name, status: existing.status });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
for (const hook of HOOKS) {
|
|
58
|
+
const hookPath = path.join(hookDir, hook.name);
|
|
59
|
+
const existing = await readHookState(hookPath);
|
|
60
|
+
result.checked.push({ name: hook.name, status: existing.status });
|
|
61
|
+
if (existing.status === "foreign") {
|
|
62
|
+
result.warnings.push(`${hook.name} exists but is not Rainy-managed.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
writeStdout(renderHookResult(result) + "\n");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function resolveHookDir(cwd) {
|
|
70
|
+
const resolved = await runGit(cwd, ["rev-parse", "--git-path", "hooks"]);
|
|
71
|
+
if (!resolved.ok) {
|
|
72
|
+
throw new Error(`Unable to resolve git hooks directory: ${resolved.error}`);
|
|
73
|
+
}
|
|
74
|
+
const hookDir = resolved.stdout.trim();
|
|
75
|
+
if (hookDir.length === 0) {
|
|
76
|
+
throw new Error("Git hooks directory could not be determined.");
|
|
77
|
+
}
|
|
78
|
+
return path.resolve(cwd, hookDir);
|
|
79
|
+
}
|
|
80
|
+
async function readHookState(hookPath) {
|
|
81
|
+
try {
|
|
82
|
+
const file = Bun.file(hookPath);
|
|
83
|
+
if (!(await file.exists())) {
|
|
84
|
+
return { status: "missing" };
|
|
85
|
+
}
|
|
86
|
+
const content = await file.text();
|
|
87
|
+
return content.includes(MANAGED_MARKER)
|
|
88
|
+
? { status: "managed", content }
|
|
89
|
+
: { status: "foreign", content };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { status: "missing" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function renderHookScript(command) {
|
|
96
|
+
return `#!/bin/sh
|
|
97
|
+
${MANAGED_MARKER}
|
|
98
|
+
set -eu
|
|
99
|
+
|
|
100
|
+
resolve_rup() {
|
|
101
|
+
if command -v rup >/dev/null 2>&1; then
|
|
102
|
+
printf '%s' "rup"
|
|
103
|
+
return
|
|
104
|
+
fi
|
|
105
|
+
if command -v rainy-updates >/dev/null 2>&1; then
|
|
106
|
+
printf '%s' "rainy-updates"
|
|
107
|
+
return
|
|
108
|
+
fi
|
|
109
|
+
if command -v rainy-up >/dev/null 2>&1; then
|
|
110
|
+
printf '%s' "rainy-up"
|
|
111
|
+
return
|
|
112
|
+
fi
|
|
113
|
+
printf '%s\n' "Rainy Updates CLI not found in PATH." >&2
|
|
114
|
+
exit 127
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
${command}
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
async function makeExecutable(filePath) {
|
|
121
|
+
await chmod(filePath, 0o755);
|
|
122
|
+
}
|
|
123
|
+
async function runGit(cwd, args) {
|
|
124
|
+
try {
|
|
125
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
126
|
+
cwd,
|
|
127
|
+
stdout: "pipe",
|
|
128
|
+
stderr: "pipe",
|
|
129
|
+
});
|
|
130
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
131
|
+
new Response(proc.stdout).text(),
|
|
132
|
+
new Response(proc.stderr).text(),
|
|
133
|
+
proc.exited,
|
|
134
|
+
]);
|
|
135
|
+
if (code !== 0) {
|
|
136
|
+
return { ok: false, error: stderr.trim() || `git ${args.join(" ")} failed` };
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, stdout };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return { ok: false, error: String(error) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function renderHookResult(result) {
|
|
145
|
+
const lines = [
|
|
146
|
+
`Hook action: ${result.action}`,
|
|
147
|
+
`Hook dir: ${result.hookDir ?? "unknown"}`,
|
|
148
|
+
];
|
|
149
|
+
if (result.installed.length > 0) {
|
|
150
|
+
lines.push(`Installed: ${result.installed.join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
if (result.removed.length > 0) {
|
|
153
|
+
lines.push(`Removed: ${result.removed.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
if (result.checked.length > 0) {
|
|
156
|
+
lines.push("Checks:");
|
|
157
|
+
for (const check of result.checked) {
|
|
158
|
+
lines.push(`- ${check.name}: ${check.status}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (result.warnings.length > 0) {
|
|
162
|
+
lines.push("Warnings:");
|
|
163
|
+
for (const warning of result.warnings) {
|
|
164
|
+
lines.push(`- ${warning}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (result.errors.length > 0) {
|
|
168
|
+
lines.push("Errors:");
|
|
169
|
+
for (const error of result.errors) {
|
|
170
|
+
lines.push(`- ${error}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|