@rainy-updates/cli 0.5.0-rc.1 → 0.5.0-rc.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.
- package/CHANGELOG.md +39 -0
- package/README.md +9 -0
- package/dist/bin/cli.js +70 -11
- package/dist/config/loader.d.ts +5 -0
- package/dist/core/check.js +1 -1
- package/dist/core/fix-pr.d.ts +7 -0
- package/dist/core/fix-pr.js +68 -0
- package/dist/core/options.js +79 -5
- package/dist/core/warm-cache.js +1 -1
- package/dist/output/format.js +3 -0
- package/dist/output/github.js +3 -0
- package/dist/registry/npm.d.ts +1 -1
- package/dist/registry/npm.js +88 -13
- package/dist/types/index.d.ts +8 -0
- package/dist/workspace/discover.js +50 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0-rc.2] - 2026-02-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- New fix-PR automation flags for CI branch workflows:
|
|
10
|
+
- `--fix-pr`
|
|
11
|
+
- `--fix-branch <name>`
|
|
12
|
+
- `--fix-commit-message <text>`
|
|
13
|
+
- `--fix-dry-run`
|
|
14
|
+
- `--no-pr-report`
|
|
15
|
+
- New summary metadata for fix-PR execution:
|
|
16
|
+
- `fixPrApplied`
|
|
17
|
+
- `fixBranchName`
|
|
18
|
+
- `fixCommitSha`
|
|
19
|
+
- New GitHub output values for fix-PR state:
|
|
20
|
+
- `fix_pr_applied`
|
|
21
|
+
- `fix_pr_branch`
|
|
22
|
+
- `fix_pr_commit`
|
|
23
|
+
- Added command-specific help output for `check --help`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `check --fix-pr` now executes update application flow to support branch+commit automation without requiring `upgrade`.
|
|
28
|
+
- Default PR report path is auto-assigned when `--fix-pr` is enabled: `.artifacts/deps-report.md`.
|
|
29
|
+
- CLI path-like options are resolved against the final effective `--cwd` value (stable behavior when option order varies).
|
|
30
|
+
- Workspace discovery now supports recursive patterns (`**`) and negated patterns (`!pattern`) with safer directory traversal defaults.
|
|
31
|
+
- Registry resolution now loads `.npmrc` scope mappings (`@scope:registry=...`) from user and project config.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Prevented stale output contracts by writing fix-PR metadata into JSON/GitHub/SARIF artifact flow after git automation is resolved.
|
|
36
|
+
|
|
37
|
+
### Tests
|
|
38
|
+
|
|
39
|
+
- Added parser tests for fix-PR flags and final-cwd path resolution.
|
|
40
|
+
- Added workspace discovery coverage for recursive and negated patterns.
|
|
41
|
+
- Added fix-PR dry-run workflow test in temporary git repos.
|
|
42
|
+
- Extended GitHub output tests for new fix-PR keys.
|
|
43
|
+
|
|
5
44
|
## [0.5.0-rc.1] - 2026-02-27
|
|
6
45
|
|
|
7
46
|
### Added
|
package/README.md
CHANGED
|
@@ -39,6 +39,9 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
|
|
|
39
39
|
# 3) Apply upgrades with workspace sync
|
|
40
40
|
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
41
41
|
|
|
42
|
+
# 3b) Generate a fix branch + commit for CI automation
|
|
43
|
+
npx @rainy-updates/cli check --workspace --fix-pr --fix-branch chore/rainy-updates
|
|
44
|
+
|
|
42
45
|
# 4) Warm cache for deterministic offline checks
|
|
43
46
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
44
47
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
@@ -154,6 +157,11 @@ Schedule:
|
|
|
154
157
|
- `--github-output <path>`
|
|
155
158
|
- `--sarif-file <path>`
|
|
156
159
|
- `--pr-report-file <path>`
|
|
160
|
+
- `--fix-pr`
|
|
161
|
+
- `--fix-branch <name>`
|
|
162
|
+
- `--fix-commit-message <text>`
|
|
163
|
+
- `--fix-dry-run`
|
|
164
|
+
- `--no-pr-report`
|
|
157
165
|
- `--ci`
|
|
158
166
|
|
|
159
167
|
### Upgrade-only
|
|
@@ -189,6 +197,7 @@ rainy-updates --version
|
|
|
189
197
|
- Node.js 20+ runtime.
|
|
190
198
|
- Works with npm and pnpm workflows.
|
|
191
199
|
- Uses optional `undici` pool path for high-throughput HTTP.
|
|
200
|
+
- Reads `.npmrc` default and scoped registries for private package environments.
|
|
192
201
|
- Cache-first architecture for speed and resilience.
|
|
193
202
|
|
|
194
203
|
## CI/CD included
|
package/dist/bin/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { upgrade } from "../core/upgrade.js";
|
|
|
9
9
|
import { warmCache } from "../core/warm-cache.js";
|
|
10
10
|
import { initCiWorkflow } from "../core/init-ci.js";
|
|
11
11
|
import { diffBaseline, saveBaseline } from "../core/baseline.js";
|
|
12
|
+
import { applyFixPr } from "../core/fix-pr.js";
|
|
12
13
|
import { renderResult } from "../output/format.js";
|
|
13
14
|
import { writeGitHubOutput } from "../output/github.js";
|
|
14
15
|
import { createSarifReport } from "../output/sarif.js";
|
|
@@ -60,22 +61,22 @@ async function main() {
|
|
|
60
61
|
process.exitCode = 1;
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
|
-
const result = parsed
|
|
64
|
-
? await upgrade(parsed.options)
|
|
65
|
-
: parsed.command === "warm-cache"
|
|
66
|
-
? await warmCache(parsed.options)
|
|
67
|
-
: await check(parsed.options);
|
|
68
|
-
const rendered = renderResult(result, parsed.options.format);
|
|
69
|
-
process.stdout.write(rendered + "\n");
|
|
70
|
-
if (parsed.options.jsonFile) {
|
|
71
|
-
await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
|
|
72
|
-
await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
|
|
73
|
-
}
|
|
64
|
+
const result = await runCommand(parsed);
|
|
74
65
|
if (parsed.options.prReportFile) {
|
|
75
66
|
const markdown = renderPrReport(result);
|
|
76
67
|
await fs.mkdir(path.dirname(parsed.options.prReportFile), { recursive: true });
|
|
77
68
|
await fs.writeFile(parsed.options.prReportFile, markdown + "\n", "utf8");
|
|
78
69
|
}
|
|
70
|
+
if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
|
|
71
|
+
const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
|
|
72
|
+
result.summary.fixPrApplied = fixResult.applied;
|
|
73
|
+
result.summary.fixBranchName = fixResult.branchName;
|
|
74
|
+
result.summary.fixCommitSha = fixResult.commitSha;
|
|
75
|
+
}
|
|
76
|
+
if (parsed.options.jsonFile) {
|
|
77
|
+
await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
|
|
78
|
+
await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
|
|
79
|
+
}
|
|
79
80
|
if (parsed.options.githubOutputFile) {
|
|
80
81
|
await writeGitHubOutput(parsed.options.githubOutputFile, result);
|
|
81
82
|
}
|
|
@@ -84,6 +85,8 @@ async function main() {
|
|
|
84
85
|
await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
|
|
85
86
|
await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
|
|
86
87
|
}
|
|
88
|
+
const rendered = renderResult(result, parsed.options.format);
|
|
89
|
+
process.stdout.write(rendered + "\n");
|
|
87
90
|
process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
88
91
|
}
|
|
89
92
|
catch (error) {
|
|
@@ -94,6 +97,34 @@ async function main() {
|
|
|
94
97
|
void main();
|
|
95
98
|
function renderHelp(command) {
|
|
96
99
|
const isCommand = command && !command.startsWith("-");
|
|
100
|
+
if (isCommand && command === "check") {
|
|
101
|
+
return `rainy-updates check [options]
|
|
102
|
+
|
|
103
|
+
Detect available dependency updates.
|
|
104
|
+
|
|
105
|
+
Options:
|
|
106
|
+
--workspace
|
|
107
|
+
--target patch|minor|major|latest
|
|
108
|
+
--filter <pattern>
|
|
109
|
+
--reject <pattern>
|
|
110
|
+
--dep-kinds deps,dev,optional,peer
|
|
111
|
+
--concurrency <n>
|
|
112
|
+
--cache-ttl <seconds>
|
|
113
|
+
--policy-file <path>
|
|
114
|
+
--offline
|
|
115
|
+
--fix-pr
|
|
116
|
+
--fix-branch <name>
|
|
117
|
+
--fix-commit-message <text>
|
|
118
|
+
--fix-dry-run
|
|
119
|
+
--no-pr-report
|
|
120
|
+
--json-file <path>
|
|
121
|
+
--github-output <path>
|
|
122
|
+
--sarif-file <path>
|
|
123
|
+
--pr-report-file <path>
|
|
124
|
+
--fail-on none|patch|minor|major|any
|
|
125
|
+
--max-updates <n>
|
|
126
|
+
--ci`;
|
|
127
|
+
}
|
|
97
128
|
if (isCommand && command === "warm-cache") {
|
|
98
129
|
return `rainy-updates warm-cache [options]
|
|
99
130
|
|
|
@@ -126,6 +157,11 @@ Options:
|
|
|
126
157
|
--target patch|minor|major|latest
|
|
127
158
|
--policy-file <path>
|
|
128
159
|
--concurrency <n>
|
|
160
|
+
--fix-pr
|
|
161
|
+
--fix-branch <name>
|
|
162
|
+
--fix-commit-message <text>
|
|
163
|
+
--fix-dry-run
|
|
164
|
+
--no-pr-report
|
|
129
165
|
--json-file <path>
|
|
130
166
|
--pr-report-file <path>`;
|
|
131
167
|
}
|
|
@@ -174,6 +210,11 @@ Global options:
|
|
|
174
210
|
--policy-file <path>
|
|
175
211
|
--fail-on none|patch|minor|major|any
|
|
176
212
|
--max-updates <n>
|
|
213
|
+
--fix-pr
|
|
214
|
+
--fix-branch <name>
|
|
215
|
+
--fix-commit-message <text>
|
|
216
|
+
--fix-dry-run
|
|
217
|
+
--no-pr-report
|
|
177
218
|
--concurrency <n>
|
|
178
219
|
--cache-ttl <seconds>
|
|
179
220
|
--offline
|
|
@@ -181,6 +222,24 @@ Global options:
|
|
|
181
222
|
--help, -h
|
|
182
223
|
--version, -v`;
|
|
183
224
|
}
|
|
225
|
+
async function runCommand(parsed) {
|
|
226
|
+
if (parsed.command === "upgrade") {
|
|
227
|
+
return await upgrade(parsed.options);
|
|
228
|
+
}
|
|
229
|
+
if (parsed.command === "warm-cache") {
|
|
230
|
+
return await warmCache(parsed.options);
|
|
231
|
+
}
|
|
232
|
+
if (parsed.options.fixPr) {
|
|
233
|
+
const upgradeOptions = {
|
|
234
|
+
...parsed.options,
|
|
235
|
+
install: false,
|
|
236
|
+
packageManager: "auto",
|
|
237
|
+
sync: false,
|
|
238
|
+
};
|
|
239
|
+
return await upgrade(upgradeOptions);
|
|
240
|
+
}
|
|
241
|
+
return await check(parsed.options);
|
|
242
|
+
}
|
|
184
243
|
async function readPackageVersion() {
|
|
185
244
|
const currentFile = fileURLToPath(import.meta.url);
|
|
186
245
|
const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface FileConfig {
|
|
|
17
17
|
prReportFile?: string;
|
|
18
18
|
failOn?: FailOnLevel;
|
|
19
19
|
maxUpdates?: number;
|
|
20
|
+
fixPr?: boolean;
|
|
21
|
+
fixBranch?: string;
|
|
22
|
+
fixCommitMessage?: string;
|
|
23
|
+
fixDryRun?: boolean;
|
|
24
|
+
noPrReport?: boolean;
|
|
20
25
|
install?: boolean;
|
|
21
26
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
22
27
|
sync?: boolean;
|
package/dist/core/check.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function check(options) {
|
|
|
11
11
|
const packageManager = await detectPackageManager(options.cwd);
|
|
12
12
|
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
13
13
|
const cache = await VersionCache.create();
|
|
14
|
-
const registryClient = new NpmRegistryClient();
|
|
14
|
+
const registryClient = new NpmRegistryClient(options.cwd);
|
|
15
15
|
const policy = await loadPolicy(options.cwd, options.policyFile);
|
|
16
16
|
const updates = [];
|
|
17
17
|
const errors = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckResult, RunOptions } from "../types/index.js";
|
|
2
|
+
export interface FixPrResult {
|
|
3
|
+
applied: boolean;
|
|
4
|
+
branchName?: string;
|
|
5
|
+
commitSha?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function applyFixPr(options: RunOptions, result: CheckResult, extraFiles: string[]): Promise<FixPrResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function applyFixPr(options, result, extraFiles) {
|
|
4
|
+
if (!options.fixPr)
|
|
5
|
+
return { applied: false };
|
|
6
|
+
if (result.updates.length === 0)
|
|
7
|
+
return { applied: false };
|
|
8
|
+
const status = await runGit(options.cwd, ["status", "--porcelain"]);
|
|
9
|
+
if (status.stdout.trim().length > 0) {
|
|
10
|
+
throw new Error("Cannot run --fix-pr with a dirty git working tree.");
|
|
11
|
+
}
|
|
12
|
+
const branch = options.fixBranch ?? "chore/rainy-updates";
|
|
13
|
+
const branchCheck = await runGit(options.cwd, ["rev-parse", "--verify", "--quiet", branch], true);
|
|
14
|
+
if (branchCheck.code === 0) {
|
|
15
|
+
await runGit(options.cwd, ["checkout", branch]);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
await runGit(options.cwd, ["checkout", "-b", branch]);
|
|
19
|
+
}
|
|
20
|
+
if (options.fixDryRun) {
|
|
21
|
+
return {
|
|
22
|
+
applied: false,
|
|
23
|
+
branchName: branch,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const manifestFiles = Array.from(new Set(result.packagePaths.map((pkgPath) => path.join(pkgPath, "package.json"))));
|
|
27
|
+
const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles]));
|
|
28
|
+
if (filesToStage.length > 0) {
|
|
29
|
+
await runGit(options.cwd, ["add", "--", ...filesToStage]);
|
|
30
|
+
}
|
|
31
|
+
const stagedCheck = await runGit(options.cwd, ["diff", "--cached", "--quiet"], true);
|
|
32
|
+
if (stagedCheck.code === 0) {
|
|
33
|
+
return {
|
|
34
|
+
applied: false,
|
|
35
|
+
branchName: branch,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${result.updates.length} updates)`;
|
|
39
|
+
await runGit(options.cwd, ["commit", "-m", message]);
|
|
40
|
+
const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
|
|
41
|
+
return {
|
|
42
|
+
applied: true,
|
|
43
|
+
branchName: branch,
|
|
44
|
+
commitSha: rev.stdout.trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function runGit(cwd, args, allowNonZero = false) {
|
|
48
|
+
return await new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
50
|
+
let stdout = "";
|
|
51
|
+
let stderr = "";
|
|
52
|
+
child.stdout.on("data", (chunk) => {
|
|
53
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
54
|
+
});
|
|
55
|
+
child.stderr.on("data", (chunk) => {
|
|
56
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
57
|
+
});
|
|
58
|
+
child.on("error", reject);
|
|
59
|
+
child.on("exit", (code) => {
|
|
60
|
+
const normalized = code ?? 1;
|
|
61
|
+
if (normalized !== 0 && !allowNonZero) {
|
|
62
|
+
reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve({ code: normalized, stdout, stderr });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
package/dist/core/options.js
CHANGED
|
@@ -36,12 +36,22 @@ export async function parseCliArgs(argv) {
|
|
|
36
36
|
prReportFile: undefined,
|
|
37
37
|
failOn: "none",
|
|
38
38
|
maxUpdates: undefined,
|
|
39
|
+
fixPr: false,
|
|
40
|
+
fixBranch: "chore/rainy-updates",
|
|
41
|
+
fixCommitMessage: undefined,
|
|
42
|
+
fixDryRun: false,
|
|
43
|
+
noPrReport: false,
|
|
39
44
|
};
|
|
40
45
|
let force = false;
|
|
41
46
|
let initCiMode = "enterprise";
|
|
42
47
|
let initCiSchedule = "weekly";
|
|
43
48
|
let baselineAction = "check";
|
|
44
49
|
let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
|
|
50
|
+
let jsonFileRaw;
|
|
51
|
+
let githubOutputRaw;
|
|
52
|
+
let sarifFileRaw;
|
|
53
|
+
let policyFileRaw;
|
|
54
|
+
let prReportFileRaw;
|
|
45
55
|
let resolvedConfig = await loadConfig(base.cwd);
|
|
46
56
|
applyConfig(base, resolvedConfig);
|
|
47
57
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -111,7 +121,7 @@ export async function parseCliArgs(argv) {
|
|
|
111
121
|
continue;
|
|
112
122
|
}
|
|
113
123
|
if (current === "--json-file" && next) {
|
|
114
|
-
|
|
124
|
+
jsonFileRaw = next;
|
|
115
125
|
index += 1;
|
|
116
126
|
continue;
|
|
117
127
|
}
|
|
@@ -119,7 +129,7 @@ export async function parseCliArgs(argv) {
|
|
|
119
129
|
throw new Error("Missing value for --json-file");
|
|
120
130
|
}
|
|
121
131
|
if (current === "--github-output" && next) {
|
|
122
|
-
|
|
132
|
+
githubOutputRaw = next;
|
|
123
133
|
index += 1;
|
|
124
134
|
continue;
|
|
125
135
|
}
|
|
@@ -127,7 +137,7 @@ export async function parseCliArgs(argv) {
|
|
|
127
137
|
throw new Error("Missing value for --github-output");
|
|
128
138
|
}
|
|
129
139
|
if (current === "--sarif-file" && next) {
|
|
130
|
-
|
|
140
|
+
sarifFileRaw = next;
|
|
131
141
|
index += 1;
|
|
132
142
|
continue;
|
|
133
143
|
}
|
|
@@ -151,7 +161,7 @@ export async function parseCliArgs(argv) {
|
|
|
151
161
|
continue;
|
|
152
162
|
}
|
|
153
163
|
if (current === "--policy-file" && next) {
|
|
154
|
-
|
|
164
|
+
policyFileRaw = next;
|
|
155
165
|
index += 1;
|
|
156
166
|
continue;
|
|
157
167
|
}
|
|
@@ -159,7 +169,7 @@ export async function parseCliArgs(argv) {
|
|
|
159
169
|
throw new Error("Missing value for --policy-file");
|
|
160
170
|
}
|
|
161
171
|
if (current === "--pr-report-file" && next) {
|
|
162
|
-
|
|
172
|
+
prReportFileRaw = next;
|
|
163
173
|
index += 1;
|
|
164
174
|
continue;
|
|
165
175
|
}
|
|
@@ -170,6 +180,34 @@ export async function parseCliArgs(argv) {
|
|
|
170
180
|
force = true;
|
|
171
181
|
continue;
|
|
172
182
|
}
|
|
183
|
+
if (current === "--fix-pr") {
|
|
184
|
+
base.fixPr = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (current === "--fix-branch" && next) {
|
|
188
|
+
base.fixBranch = next;
|
|
189
|
+
index += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (current === "--fix-branch") {
|
|
193
|
+
throw new Error("Missing value for --fix-branch");
|
|
194
|
+
}
|
|
195
|
+
if (current === "--fix-commit-message" && next) {
|
|
196
|
+
base.fixCommitMessage = next;
|
|
197
|
+
index += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (current === "--fix-commit-message") {
|
|
201
|
+
throw new Error("Missing value for --fix-commit-message");
|
|
202
|
+
}
|
|
203
|
+
if (current === "--fix-dry-run") {
|
|
204
|
+
base.fixDryRun = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (current === "--no-pr-report") {
|
|
208
|
+
base.noPrReport = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
173
211
|
if (current === "--install" && command === "upgrade") {
|
|
174
212
|
continue;
|
|
175
213
|
}
|
|
@@ -249,6 +287,27 @@ export async function parseCliArgs(argv) {
|
|
|
249
287
|
}
|
|
250
288
|
throw new Error(`Unexpected argument: ${current}`);
|
|
251
289
|
}
|
|
290
|
+
if (jsonFileRaw) {
|
|
291
|
+
base.jsonFile = path.resolve(base.cwd, jsonFileRaw);
|
|
292
|
+
}
|
|
293
|
+
if (githubOutputRaw) {
|
|
294
|
+
base.githubOutputFile = path.resolve(base.cwd, githubOutputRaw);
|
|
295
|
+
}
|
|
296
|
+
if (sarifFileRaw) {
|
|
297
|
+
base.sarifFile = path.resolve(base.cwd, sarifFileRaw);
|
|
298
|
+
}
|
|
299
|
+
if (policyFileRaw) {
|
|
300
|
+
base.policyFile = path.resolve(base.cwd, policyFileRaw);
|
|
301
|
+
}
|
|
302
|
+
if (prReportFileRaw) {
|
|
303
|
+
base.prReportFile = path.resolve(base.cwd, prReportFileRaw);
|
|
304
|
+
}
|
|
305
|
+
if (base.noPrReport) {
|
|
306
|
+
base.prReportFile = undefined;
|
|
307
|
+
}
|
|
308
|
+
else if (base.fixPr && !base.prReportFile) {
|
|
309
|
+
base.prReportFile = path.resolve(base.cwd, ".artifacts/deps-report.md");
|
|
310
|
+
}
|
|
252
311
|
if (command === "upgrade") {
|
|
253
312
|
const configPm = resolvedConfig.packageManager;
|
|
254
313
|
const cliPm = parsePackageManager(args);
|
|
@@ -335,6 +394,21 @@ function applyConfig(base, config) {
|
|
|
335
394
|
if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
|
|
336
395
|
base.maxUpdates = config.maxUpdates;
|
|
337
396
|
}
|
|
397
|
+
if (typeof config.fixPr === "boolean") {
|
|
398
|
+
base.fixPr = config.fixPr;
|
|
399
|
+
}
|
|
400
|
+
if (typeof config.fixBranch === "string" && config.fixBranch.length > 0) {
|
|
401
|
+
base.fixBranch = config.fixBranch;
|
|
402
|
+
}
|
|
403
|
+
if (typeof config.fixCommitMessage === "string" && config.fixCommitMessage.length > 0) {
|
|
404
|
+
base.fixCommitMessage = config.fixCommitMessage;
|
|
405
|
+
}
|
|
406
|
+
if (typeof config.fixDryRun === "boolean") {
|
|
407
|
+
base.fixDryRun = config.fixDryRun;
|
|
408
|
+
}
|
|
409
|
+
if (typeof config.noPrReport === "boolean") {
|
|
410
|
+
base.noPrReport = config.noPrReport;
|
|
411
|
+
}
|
|
338
412
|
}
|
|
339
413
|
function parsePackageManager(args) {
|
|
340
414
|
const index = args.indexOf("--pm");
|
package/dist/core/warm-cache.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function warmCache(options) {
|
|
|
8
8
|
const packageManager = await detectPackageManager(options.cwd);
|
|
9
9
|
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
10
10
|
const cache = await VersionCache.create();
|
|
11
|
-
const registryClient = new NpmRegistryClient();
|
|
11
|
+
const registryClient = new NpmRegistryClient(options.cwd);
|
|
12
12
|
const errors = [];
|
|
13
13
|
const warnings = [];
|
|
14
14
|
let totalDependencies = 0;
|
package/dist/output/format.js
CHANGED
|
@@ -52,5 +52,8 @@ export function renderResult(result, format) {
|
|
|
52
52
|
}
|
|
53
53
|
lines.push("");
|
|
54
54
|
lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
|
|
55
|
+
if (result.summary.fixPrApplied) {
|
|
56
|
+
lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
|
|
57
|
+
}
|
|
55
58
|
return lines.join("\n");
|
|
56
59
|
}
|
package/dist/output/github.js
CHANGED
|
@@ -8,6 +8,9 @@ export async function writeGitHubOutput(filePath, result) {
|
|
|
8
8
|
`checked_dependencies=${result.summary.checkedDependencies}`,
|
|
9
9
|
`scanned_packages=${result.summary.scannedPackages}`,
|
|
10
10
|
`warmed_packages=${result.summary.warmedPackages}`,
|
|
11
|
+
`fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
|
|
12
|
+
`fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
|
|
13
|
+
`fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
|
|
11
14
|
];
|
|
12
15
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
13
16
|
await fs.writeFile(filePath, lines.join("\n") + "\n", "utf8");
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ResolveManyResult {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class NpmRegistryClient {
|
|
13
13
|
private readonly requesterPromise;
|
|
14
|
-
constructor();
|
|
14
|
+
constructor(cwd?: string);
|
|
15
15
|
resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
|
|
16
16
|
latestVersion: string | null;
|
|
17
17
|
versions: string[];
|
package/dist/registry/npm.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
1
5
|
import { asyncPool } from "../utils/async-pool.js";
|
|
2
6
|
const DEFAULT_TIMEOUT_MS = 8000;
|
|
3
7
|
const USER_AGENT = "@rainy-updates/cli";
|
|
8
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
4
9
|
export class NpmRegistryClient {
|
|
5
10
|
requesterPromise;
|
|
6
|
-
constructor() {
|
|
7
|
-
this.requesterPromise = createRequester();
|
|
11
|
+
constructor(cwd) {
|
|
12
|
+
this.requesterPromise = createRequester(cwd);
|
|
8
13
|
}
|
|
9
14
|
async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
10
15
|
const requester = await this.requesterPromise;
|
|
@@ -79,15 +84,18 @@ export class NpmRegistryClient {
|
|
|
79
84
|
function sleep(ms) {
|
|
80
85
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
86
|
}
|
|
82
|
-
async function createRequester() {
|
|
83
|
-
const
|
|
87
|
+
async function createRequester(cwd) {
|
|
88
|
+
const registryConfig = await loadRegistryConfig(cwd ?? process.cwd());
|
|
89
|
+
const undiciRequester = await tryCreateUndiciRequester(registryConfig);
|
|
84
90
|
if (undiciRequester)
|
|
85
91
|
return undiciRequester;
|
|
86
92
|
return async (packageName, timeoutMs) => {
|
|
87
93
|
const controller = new AbortController();
|
|
88
94
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
95
|
+
const registry = resolveRegistryForPackage(packageName, registryConfig);
|
|
96
|
+
const url = buildRegistryUrl(registry, packageName);
|
|
89
97
|
try {
|
|
90
|
-
const response = await fetch(
|
|
98
|
+
const response = await fetch(url, {
|
|
91
99
|
headers: {
|
|
92
100
|
accept: "application/json",
|
|
93
101
|
"user-agent": USER_AGENT,
|
|
@@ -102,21 +110,17 @@ async function createRequester() {
|
|
|
102
110
|
}
|
|
103
111
|
};
|
|
104
112
|
}
|
|
105
|
-
async function tryCreateUndiciRequester() {
|
|
113
|
+
async function tryCreateUndiciRequester(registryConfig) {
|
|
106
114
|
try {
|
|
107
115
|
const dynamicImport = Function("specifier", "return import(specifier)");
|
|
108
116
|
const undici = await dynamicImport("undici");
|
|
109
|
-
const pool = new undici.Pool("https://registry.npmjs.org", {
|
|
110
|
-
connections: 20,
|
|
111
|
-
pipelining: 10,
|
|
112
|
-
allowH2: true,
|
|
113
|
-
});
|
|
114
117
|
return async (packageName, timeoutMs) => {
|
|
115
118
|
const controller = new AbortController();
|
|
116
119
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
|
+
const registry = resolveRegistryForPackage(packageName, registryConfig);
|
|
121
|
+
const url = buildRegistryUrl(registry, packageName);
|
|
117
122
|
try {
|
|
118
|
-
const res = await
|
|
119
|
-
path: `/${encodeURIComponent(packageName)}`,
|
|
123
|
+
const res = await undici.request(url, {
|
|
120
124
|
method: "GET",
|
|
121
125
|
headers: {
|
|
122
126
|
accept: "application/json",
|
|
@@ -143,3 +147,74 @@ async function tryCreateUndiciRequester() {
|
|
|
143
147
|
return null;
|
|
144
148
|
}
|
|
145
149
|
}
|
|
150
|
+
async function loadRegistryConfig(cwd) {
|
|
151
|
+
const homeNpmrc = path.join(os.homedir(), ".npmrc");
|
|
152
|
+
const projectNpmrc = path.join(cwd, ".npmrc");
|
|
153
|
+
const merged = new Map();
|
|
154
|
+
for (const filePath of [homeNpmrc, projectNpmrc]) {
|
|
155
|
+
try {
|
|
156
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
157
|
+
const parsed = parseNpmrc(content);
|
|
158
|
+
for (const [key, value] of parsed) {
|
|
159
|
+
merged.set(key, value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// ignore missing/unreadable file
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const defaultRegistry = normalizeRegistryUrl(merged.get("registry") ?? DEFAULT_REGISTRY);
|
|
167
|
+
const scopedRegistries = new Map();
|
|
168
|
+
for (const [key, value] of merged) {
|
|
169
|
+
if (!key.startsWith("@") || !key.endsWith(":registry"))
|
|
170
|
+
continue;
|
|
171
|
+
const scope = key.slice(0, key.indexOf(":registry"));
|
|
172
|
+
if (scope.length > 1) {
|
|
173
|
+
scopedRegistries.set(scope, normalizeRegistryUrl(value));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { defaultRegistry, scopedRegistries };
|
|
177
|
+
}
|
|
178
|
+
function parseNpmrc(content) {
|
|
179
|
+
const values = new Map();
|
|
180
|
+
const lines = content.split(/\r?\n/);
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
184
|
+
continue;
|
|
185
|
+
const separator = trimmed.indexOf("=");
|
|
186
|
+
if (separator <= 0)
|
|
187
|
+
continue;
|
|
188
|
+
const key = trimmed.slice(0, separator).trim();
|
|
189
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
190
|
+
if (key.length > 0 && value.length > 0) {
|
|
191
|
+
values.set(key, value);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return values;
|
|
195
|
+
}
|
|
196
|
+
function normalizeRegistryUrl(value) {
|
|
197
|
+
const normalized = value.endsWith("/") ? value : `${value}/`;
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
function resolveRegistryForPackage(packageName, config) {
|
|
201
|
+
const scope = extractScope(packageName);
|
|
202
|
+
if (scope) {
|
|
203
|
+
const scoped = config.scopedRegistries.get(scope);
|
|
204
|
+
if (scoped)
|
|
205
|
+
return scoped;
|
|
206
|
+
}
|
|
207
|
+
return config.defaultRegistry;
|
|
208
|
+
}
|
|
209
|
+
function extractScope(packageName) {
|
|
210
|
+
if (!packageName.startsWith("@"))
|
|
211
|
+
return null;
|
|
212
|
+
const firstSlash = packageName.indexOf("/");
|
|
213
|
+
if (firstSlash <= 1)
|
|
214
|
+
return null;
|
|
215
|
+
return packageName.slice(0, firstSlash);
|
|
216
|
+
}
|
|
217
|
+
function buildRegistryUrl(registry, packageName) {
|
|
218
|
+
const base = normalizeRegistryUrl(registry);
|
|
219
|
+
return new URL(encodeURIComponent(packageName), base).toString();
|
|
220
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface RunOptions {
|
|
|
21
21
|
prReportFile?: string;
|
|
22
22
|
failOn?: FailOnLevel;
|
|
23
23
|
maxUpdates?: number;
|
|
24
|
+
fixPr?: boolean;
|
|
25
|
+
fixBranch?: string;
|
|
26
|
+
fixCommitMessage?: string;
|
|
27
|
+
fixDryRun?: boolean;
|
|
28
|
+
noPrReport?: boolean;
|
|
24
29
|
}
|
|
25
30
|
export interface CheckOptions extends RunOptions {
|
|
26
31
|
}
|
|
@@ -60,6 +65,9 @@ export interface Summary {
|
|
|
60
65
|
upgraded: number;
|
|
61
66
|
skipped: number;
|
|
62
67
|
warmedPackages: number;
|
|
68
|
+
fixPrApplied?: boolean;
|
|
69
|
+
fixBranchName?: string;
|
|
70
|
+
fixCommitSha?: string;
|
|
63
71
|
}
|
|
64
72
|
export interface CheckResult {
|
|
65
73
|
projectPath: string;
|
|
@@ -5,14 +5,21 @@ export async function discoverPackageDirs(cwd, workspaceMode) {
|
|
|
5
5
|
return [cwd];
|
|
6
6
|
}
|
|
7
7
|
const roots = new Set([cwd]);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const patterns = [...(await readPackageJsonWorkspacePatterns(cwd)), ...(await readPnpmWorkspacePatterns(cwd))];
|
|
9
|
+
const include = patterns.filter((item) => !item.startsWith("!"));
|
|
10
|
+
const exclude = patterns.filter((item) => item.startsWith("!")).map((item) => item.slice(1));
|
|
11
|
+
for (const pattern of include) {
|
|
12
|
+
const dirs = await expandWorkspacePattern(cwd, pattern);
|
|
12
13
|
for (const dir of dirs) {
|
|
13
14
|
roots.add(dir);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
for (const pattern of exclude) {
|
|
18
|
+
const dirs = await expandWorkspacePattern(cwd, pattern);
|
|
19
|
+
for (const dir of dirs) {
|
|
20
|
+
roots.delete(dir);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
16
23
|
const existing = [];
|
|
17
24
|
for (const dir of roots) {
|
|
18
25
|
const packageJsonPath = path.join(dir, "package.json");
|
|
@@ -21,7 +28,7 @@ export async function discoverPackageDirs(cwd, workspaceMode) {
|
|
|
21
28
|
existing.push(dir);
|
|
22
29
|
}
|
|
23
30
|
catch {
|
|
24
|
-
// ignore
|
|
31
|
+
// ignore missing package.json
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
return existing.sort();
|
|
@@ -53,7 +60,7 @@ async function readPnpmWorkspacePatterns(cwd) {
|
|
|
53
60
|
const trimmed = line.trim();
|
|
54
61
|
if (!trimmed.startsWith("-"))
|
|
55
62
|
continue;
|
|
56
|
-
const value = trimmed.replace(/^-\s*/, "").replace(/^['
|
|
63
|
+
const value = trimmed.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, "");
|
|
57
64
|
if (value.length > 0) {
|
|
58
65
|
patterns.push(value);
|
|
59
66
|
}
|
|
@@ -64,23 +71,48 @@ async function readPnpmWorkspacePatterns(cwd) {
|
|
|
64
71
|
return [];
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
|
-
async function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
72
|
-
const starIndex = normalized.indexOf("*");
|
|
73
|
-
const basePart = normalized.slice(0, starIndex).replace(/\/$/, "");
|
|
74
|
-
const suffix = normalized.slice(starIndex + 1);
|
|
75
|
-
if (suffix.length > 0 && suffix !== "/") {
|
|
74
|
+
async function expandWorkspacePattern(cwd, pattern) {
|
|
75
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
76
|
+
if (normalized.length === 0)
|
|
76
77
|
return [];
|
|
78
|
+
if (!normalized.includes("*")) {
|
|
79
|
+
return [path.resolve(cwd, normalized)];
|
|
80
|
+
}
|
|
81
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
82
|
+
const results = new Set();
|
|
83
|
+
await collectMatches(path.resolve(cwd), segments, 0, results);
|
|
84
|
+
return Array.from(results);
|
|
85
|
+
}
|
|
86
|
+
async function collectMatches(baseDir, segments, index, out) {
|
|
87
|
+
if (index >= segments.length) {
|
|
88
|
+
out.add(baseDir);
|
|
89
|
+
return;
|
|
77
90
|
}
|
|
78
|
-
const
|
|
91
|
+
const segment = segments[index];
|
|
92
|
+
if (segment === "**") {
|
|
93
|
+
await collectMatches(baseDir, segments, index + 1, out);
|
|
94
|
+
const children = await readChildDirs(baseDir);
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
await collectMatches(child, segments, index, out);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (segment === "*") {
|
|
101
|
+
const children = await readChildDirs(baseDir);
|
|
102
|
+
for (const child of children) {
|
|
103
|
+
await collectMatches(child, segments, index + 1, out);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await collectMatches(path.join(baseDir, segment), segments, index + 1, out);
|
|
108
|
+
}
|
|
109
|
+
async function readChildDirs(dir) {
|
|
79
110
|
try {
|
|
80
|
-
const entries = await fs.readdir(
|
|
111
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
81
112
|
return entries
|
|
82
113
|
.filter((entry) => entry.isDirectory())
|
|
83
|
-
.
|
|
114
|
+
.filter((entry) => entry.name !== "node_modules" && !entry.name.startsWith("."))
|
|
115
|
+
.map((entry) => path.join(dir, entry.name));
|
|
84
116
|
}
|
|
85
117
|
catch {
|
|
86
118
|
return [];
|