@rainy-updates/cli 0.4.4 → 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 +68 -0
- package/README.md +24 -1
- package/dist/bin/cli.js +132 -18
- package/dist/cache/cache.d.ts +1 -1
- package/dist/cache/cache.js +62 -9
- package/dist/config/loader.d.ts +8 -1
- package/dist/core/baseline.d.ts +23 -0
- package/dist/core/baseline.js +72 -0
- package/dist/core/check.js +25 -13
- package/dist/core/fix-pr.d.ts +7 -0
- package/dist/core/fix-pr.js +68 -0
- package/dist/core/init-ci.d.ts +1 -1
- package/dist/core/init-ci.js +9 -1
- package/dist/core/options.d.ts +6 -1
- package/dist/core/options.js +212 -9
- package/dist/core/warm-cache.js +5 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/format.js +3 -0
- package/dist/output/github.js +3 -0
- package/dist/output/sarif.js +21 -1
- package/dist/registry/npm.d.ts +14 -3
- package/dist/registry/npm.js +117 -25
- package/dist/types/index.d.ts +19 -0
- package/dist/utils/semver.d.ts +1 -0
- package/dist/utils/semver.js +24 -0
- package/dist/workspace/discover.js +50 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,74 @@
|
|
|
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
|
+
|
|
44
|
+
## [0.5.0-rc.1] - 2026-02-27
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- New CI rollout controls:
|
|
49
|
+
- `--fail-on none|patch|minor|major|any`
|
|
50
|
+
- `--max-updates <n>`
|
|
51
|
+
- New baseline workflow command:
|
|
52
|
+
- `baseline --save --file <path>` to snapshot dependency state
|
|
53
|
+
- `baseline --check --file <path>` to detect dependency drift
|
|
54
|
+
- New `init-ci --mode enterprise` template:
|
|
55
|
+
- Node runtime matrix (`20`, `22`)
|
|
56
|
+
- stricter default permissions
|
|
57
|
+
- artifact retention policy
|
|
58
|
+
- built-in rollout gate flags (`--fail-on`, `--max-updates`)
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Dependency target selection now evaluates available package versions from registry metadata, improving `patch|minor|major` accuracy.
|
|
63
|
+
- CLI parser now rejects unknown options and missing option values with explicit errors (safer CI behavior).
|
|
64
|
+
- SARIF output now reports the actual package version dynamically.
|
|
65
|
+
|
|
66
|
+
### Tests
|
|
67
|
+
|
|
68
|
+
- Added baseline snapshot/diff tests.
|
|
69
|
+
- Added enterprise workflow generation tests.
|
|
70
|
+
- Added semver target selection tests using available version sets.
|
|
71
|
+
- Added parser tests for baseline command, rollout flags, and unknown option rejection.
|
|
72
|
+
|
|
5
73
|
## [0.4.4] - 2026-02-27
|
|
6
74
|
|
|
7
75
|
### Changed
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ pnpm add -D @rainy-updates/cli
|
|
|
25
25
|
- `check`: analyze dependencies and report available updates.
|
|
26
26
|
- `upgrade`: rewrite dependency ranges in manifests, optionally install lockfile updates.
|
|
27
27
|
- `warm-cache`: prefetch package metadata for fast and offline checks.
|
|
28
|
+
- `baseline`: save and compare dependency baseline snapshots.
|
|
28
29
|
|
|
29
30
|
## Quick usage
|
|
30
31
|
|
|
@@ -38,9 +39,16 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
|
|
|
38
39
|
# 3) Apply upgrades with workspace sync
|
|
39
40
|
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
40
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
|
+
|
|
41
45
|
# 4) Warm cache for deterministic offline checks
|
|
42
46
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
43
47
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
48
|
+
|
|
49
|
+
# 5) Save and compare baseline drift in CI
|
|
50
|
+
npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
|
|
51
|
+
npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
|
|
44
52
|
```
|
|
45
53
|
|
|
46
54
|
## What it does in production
|
|
@@ -108,7 +116,7 @@ Generate a workflow in the target project automatically:
|
|
|
108
116
|
|
|
109
117
|
```bash
|
|
110
118
|
# strict mode (recommended)
|
|
111
|
-
npx @rainy-updates/cli init-ci --mode
|
|
119
|
+
npx @rainy-updates/cli init-ci --mode enterprise --schedule weekly
|
|
112
120
|
|
|
113
121
|
# lightweight mode
|
|
114
122
|
npx @rainy-updates/cli init-ci --mode minimal --schedule daily
|
|
@@ -121,6 +129,7 @@ Generated file:
|
|
|
121
129
|
Modes:
|
|
122
130
|
|
|
123
131
|
- `strict`: warm-cache + offline check + artifacts + SARIF upload.
|
|
132
|
+
- `enterprise`: strict checks + runtime matrix + retention policy + rollout gates.
|
|
124
133
|
- `minimal`: fast check-only workflow for quick adoption.
|
|
125
134
|
|
|
126
135
|
Schedule:
|
|
@@ -140,12 +149,19 @@ Schedule:
|
|
|
140
149
|
- `--concurrency <n>`
|
|
141
150
|
- `--cache-ttl <seconds>`
|
|
142
151
|
- `--offline`
|
|
152
|
+
- `--fail-on none|patch|minor|major|any`
|
|
153
|
+
- `--max-updates <n>`
|
|
143
154
|
- `--policy-file <path>`
|
|
144
155
|
- `--format table|json|minimal|github`
|
|
145
156
|
- `--json-file <path>`
|
|
146
157
|
- `--github-output <path>`
|
|
147
158
|
- `--sarif-file <path>`
|
|
148
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`
|
|
149
165
|
- `--ci`
|
|
150
166
|
|
|
151
167
|
### Upgrade-only
|
|
@@ -154,6 +170,12 @@ Schedule:
|
|
|
154
170
|
- `--pm auto|npm|pnpm`
|
|
155
171
|
- `--sync`
|
|
156
172
|
|
|
173
|
+
### Baseline-only
|
|
174
|
+
|
|
175
|
+
- `--save`
|
|
176
|
+
- `--check`
|
|
177
|
+
- `--file <path>`
|
|
178
|
+
|
|
157
179
|
## Config support
|
|
158
180
|
|
|
159
181
|
Configuration can be loaded from:
|
|
@@ -175,6 +197,7 @@ rainy-updates --version
|
|
|
175
197
|
- Node.js 20+ runtime.
|
|
176
198
|
- Works with npm and pnpm workflows.
|
|
177
199
|
- Uses optional `undici` pool path for high-throughput HTTP.
|
|
200
|
+
- Reads `.npmrc` default and scoped registries for private package environments.
|
|
178
201
|
- Cache-first architecture for speed and resilience.
|
|
179
202
|
|
|
180
203
|
## CI/CD included
|
package/dist/bin/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import { check } from "../core/check.js";
|
|
|
8
8
|
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
|
+
import { diffBaseline, saveBaseline } from "../core/baseline.js";
|
|
12
|
+
import { applyFixPr } from "../core/fix-pr.js";
|
|
11
13
|
import { renderResult } from "../output/format.js";
|
|
12
14
|
import { writeGitHubOutput } from "../output/github.js";
|
|
13
15
|
import { createSarifReport } from "../output/sarif.js";
|
|
@@ -34,22 +36,47 @@ async function main() {
|
|
|
34
36
|
: `CI workflow already exists at ${workflow.path}. Use --force to overwrite.\n`);
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
if (parsed.command === "baseline") {
|
|
40
|
+
if (parsed.options.action === "save") {
|
|
41
|
+
const saved = await saveBaseline(parsed.options);
|
|
42
|
+
process.stdout.write(`Saved baseline at ${saved.filePath} (${saved.entries} entries)\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const diff = await diffBaseline(parsed.options);
|
|
46
|
+
const changes = diff.added.length + diff.removed.length + diff.changed.length;
|
|
47
|
+
if (changes === 0) {
|
|
48
|
+
process.stdout.write(`No baseline drift detected (${diff.filePath}).\n`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
process.stdout.write(`Baseline drift detected (${diff.filePath}).\n`);
|
|
52
|
+
if (diff.added.length > 0) {
|
|
53
|
+
process.stdout.write(`Added: ${diff.added.length}\n`);
|
|
54
|
+
}
|
|
55
|
+
if (diff.removed.length > 0) {
|
|
56
|
+
process.stdout.write(`Removed: ${diff.removed.length}\n`);
|
|
57
|
+
}
|
|
58
|
+
if (diff.changed.length > 0) {
|
|
59
|
+
process.stdout.write(`Changed: ${diff.changed.length}\n`);
|
|
60
|
+
}
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
47
63
|
}
|
|
64
|
+
const result = await runCommand(parsed);
|
|
48
65
|
if (parsed.options.prReportFile) {
|
|
49
66
|
const markdown = renderPrReport(result);
|
|
50
67
|
await fs.mkdir(path.dirname(parsed.options.prReportFile), { recursive: true });
|
|
51
68
|
await fs.writeFile(parsed.options.prReportFile, markdown + "\n", "utf8");
|
|
52
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
|
+
}
|
|
53
80
|
if (parsed.options.githubOutputFile) {
|
|
54
81
|
await writeGitHubOutput(parsed.options.githubOutputFile, result);
|
|
55
82
|
}
|
|
@@ -58,13 +85,9 @@ async function main() {
|
|
|
58
85
|
await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
|
|
59
86
|
await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
|
|
60
87
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (result.errors.length > 0) {
|
|
66
|
-
process.exitCode = 2;
|
|
67
|
-
}
|
|
88
|
+
const rendered = renderResult(result, parsed.options.format);
|
|
89
|
+
process.stdout.write(rendered + "\n");
|
|
90
|
+
process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
68
91
|
}
|
|
69
92
|
catch (error) {
|
|
70
93
|
process.stderr.write(`rainy-updates: ${String(error)}\n`);
|
|
@@ -74,6 +97,34 @@ async function main() {
|
|
|
74
97
|
void main();
|
|
75
98
|
function renderHelp(command) {
|
|
76
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
|
+
}
|
|
77
128
|
if (isCommand && command === "warm-cache") {
|
|
78
129
|
return `rainy-updates warm-cache [options]
|
|
79
130
|
|
|
@@ -106,6 +157,11 @@ Options:
|
|
|
106
157
|
--target patch|minor|major|latest
|
|
107
158
|
--policy-file <path>
|
|
108
159
|
--concurrency <n>
|
|
160
|
+
--fix-pr
|
|
161
|
+
--fix-branch <name>
|
|
162
|
+
--fix-commit-message <text>
|
|
163
|
+
--fix-dry-run
|
|
164
|
+
--no-pr-report
|
|
109
165
|
--json-file <path>
|
|
110
166
|
--pr-report-file <path>`;
|
|
111
167
|
}
|
|
@@ -117,8 +173,21 @@ Create a GitHub Actions workflow template at:
|
|
|
117
173
|
|
|
118
174
|
Options:
|
|
119
175
|
--force
|
|
120
|
-
--mode minimal|strict
|
|
176
|
+
--mode minimal|strict|enterprise
|
|
121
177
|
--schedule weekly|daily|off`;
|
|
178
|
+
}
|
|
179
|
+
if (isCommand && command === "baseline") {
|
|
180
|
+
return `rainy-updates baseline [options]
|
|
181
|
+
|
|
182
|
+
Save or compare dependency baseline snapshots.
|
|
183
|
+
|
|
184
|
+
Options:
|
|
185
|
+
--save
|
|
186
|
+
--check
|
|
187
|
+
--file <path>
|
|
188
|
+
--workspace
|
|
189
|
+
--dep-kinds deps,dev,optional,peer
|
|
190
|
+
--ci`;
|
|
122
191
|
}
|
|
123
192
|
return `rainy-updates <command> [options]
|
|
124
193
|
|
|
@@ -127,6 +196,7 @@ Commands:
|
|
|
127
196
|
upgrade Apply updates to manifests
|
|
128
197
|
warm-cache Warm local cache for fast/offline checks
|
|
129
198
|
init-ci Scaffold GitHub Actions workflow
|
|
199
|
+
baseline Save/check dependency baseline snapshots
|
|
130
200
|
|
|
131
201
|
Global options:
|
|
132
202
|
--cwd <path>
|
|
@@ -138,6 +208,13 @@ Global options:
|
|
|
138
208
|
--sarif-file <path>
|
|
139
209
|
--pr-report-file <path>
|
|
140
210
|
--policy-file <path>
|
|
211
|
+
--fail-on none|patch|minor|major|any
|
|
212
|
+
--max-updates <n>
|
|
213
|
+
--fix-pr
|
|
214
|
+
--fix-branch <name>
|
|
215
|
+
--fix-commit-message <text>
|
|
216
|
+
--fix-dry-run
|
|
217
|
+
--no-pr-report
|
|
141
218
|
--concurrency <n>
|
|
142
219
|
--cache-ttl <seconds>
|
|
143
220
|
--offline
|
|
@@ -145,6 +222,24 @@ Global options:
|
|
|
145
222
|
--help, -h
|
|
146
223
|
--version, -v`;
|
|
147
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
|
+
}
|
|
148
243
|
async function readPackageVersion() {
|
|
149
244
|
const currentFile = fileURLToPath(import.meta.url);
|
|
150
245
|
const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
|
|
@@ -152,3 +247,22 @@ async function readPackageVersion() {
|
|
|
152
247
|
const parsed = JSON.parse(content);
|
|
153
248
|
return parsed.version ?? "0.0.0";
|
|
154
249
|
}
|
|
250
|
+
function resolveExitCode(result, failOn, maxUpdates, ciMode) {
|
|
251
|
+
if (result.errors.length > 0)
|
|
252
|
+
return 2;
|
|
253
|
+
if (typeof maxUpdates === "number" && result.updates.length > maxUpdates)
|
|
254
|
+
return 1;
|
|
255
|
+
const effectiveFailOn = failOn && failOn !== "none" ? failOn : ciMode ? "any" : "none";
|
|
256
|
+
if (!shouldFailForUpdates(result.updates, effectiveFailOn))
|
|
257
|
+
return 0;
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
function shouldFailForUpdates(updates, failOn) {
|
|
261
|
+
if (failOn === "none")
|
|
262
|
+
return false;
|
|
263
|
+
if (failOn === "any" || failOn === "patch")
|
|
264
|
+
return updates.length > 0;
|
|
265
|
+
if (failOn === "minor")
|
|
266
|
+
return updates.some((update) => update.diffType === "minor" || update.diffType === "major");
|
|
267
|
+
return updates.some((update) => update.diffType === "major");
|
|
268
|
+
}
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export declare class VersionCache {
|
|
|
5
5
|
static create(customPath?: string): Promise<VersionCache>;
|
|
6
6
|
getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
|
|
7
7
|
getAny(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
|
|
8
|
-
set(packageName: string, target: TargetLevel, latestVersion: string, ttlSeconds: number): Promise<void>;
|
|
8
|
+
set(packageName: string, target: TargetLevel, latestVersion: string, availableVersions: string[], ttlSeconds: number): Promise<void>;
|
|
9
9
|
}
|
package/dist/cache/cache.js
CHANGED
|
@@ -9,7 +9,13 @@ class FileCacheStore {
|
|
|
9
9
|
async get(packageName, target) {
|
|
10
10
|
const entries = await this.readEntries();
|
|
11
11
|
const key = this.getKey(packageName, target);
|
|
12
|
-
|
|
12
|
+
const entry = entries[key];
|
|
13
|
+
if (!entry)
|
|
14
|
+
return null;
|
|
15
|
+
return {
|
|
16
|
+
...entry,
|
|
17
|
+
availableVersions: Array.isArray(entry.availableVersions) ? entry.availableVersions : [entry.latestVersion],
|
|
18
|
+
};
|
|
13
19
|
}
|
|
14
20
|
async set(entry) {
|
|
15
21
|
const entries = await this.readEntries();
|
|
@@ -39,31 +45,63 @@ class SqliteCacheStore {
|
|
|
39
45
|
package_name TEXT NOT NULL,
|
|
40
46
|
target TEXT NOT NULL,
|
|
41
47
|
latest_version TEXT NOT NULL,
|
|
48
|
+
available_versions TEXT NOT NULL,
|
|
42
49
|
fetched_at INTEGER NOT NULL,
|
|
43
50
|
ttl_seconds INTEGER NOT NULL,
|
|
44
51
|
PRIMARY KEY (package_name, target)
|
|
45
52
|
);
|
|
46
53
|
`);
|
|
54
|
+
this.ensureSchema();
|
|
47
55
|
}
|
|
48
56
|
async get(packageName, target) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
57
|
+
let row;
|
|
58
|
+
try {
|
|
59
|
+
row = this.db
|
|
60
|
+
.prepare(`SELECT package_name, target, latest_version, available_versions, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
|
|
61
|
+
.get(packageName, target);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
row = this.db
|
|
65
|
+
.prepare(`SELECT package_name, target, latest_version, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
|
|
66
|
+
.get(packageName, target);
|
|
67
|
+
}
|
|
52
68
|
if (!row)
|
|
53
69
|
return null;
|
|
54
70
|
return {
|
|
55
71
|
packageName: row.package_name,
|
|
56
72
|
target: row.target,
|
|
57
73
|
latestVersion: row.latest_version,
|
|
74
|
+
availableVersions: parseJsonArray(row.available_versions ?? row.latest_version, row.latest_version),
|
|
58
75
|
fetchedAt: row.fetched_at,
|
|
59
76
|
ttlSeconds: row.ttl_seconds,
|
|
60
77
|
};
|
|
61
78
|
}
|
|
62
79
|
async set(entry) {
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
try {
|
|
81
|
+
this.db
|
|
82
|
+
.prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, available_versions, fetched_at, ttl_seconds)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
84
|
+
.run(entry.packageName, entry.target, entry.latestVersion, JSON.stringify(entry.availableVersions), entry.fetchedAt, entry.ttlSeconds);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
this.db
|
|
88
|
+
.prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, fetched_at, ttl_seconds)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
90
|
+
.run(entry.packageName, entry.target, entry.latestVersion, entry.fetchedAt, entry.ttlSeconds);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
ensureSchema() {
|
|
94
|
+
try {
|
|
95
|
+
const columns = this.db.prepare("PRAGMA table_info(versions);").all();
|
|
96
|
+
const hasAvailableVersions = columns.some((column) => column.name === "available_versions");
|
|
97
|
+
if (!hasAvailableVersions) {
|
|
98
|
+
this.db.exec("ALTER TABLE versions ADD COLUMN available_versions TEXT;");
|
|
99
|
+
}
|
|
100
|
+
this.db.exec("UPDATE versions SET available_versions = latest_version WHERE available_versions IS NULL;");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Best-effort migration.
|
|
104
|
+
}
|
|
67
105
|
}
|
|
68
106
|
}
|
|
69
107
|
export class VersionCache {
|
|
@@ -92,11 +130,12 @@ export class VersionCache {
|
|
|
92
130
|
async getAny(packageName, target) {
|
|
93
131
|
return this.store.get(packageName, target);
|
|
94
132
|
}
|
|
95
|
-
async set(packageName, target, latestVersion, ttlSeconds) {
|
|
133
|
+
async set(packageName, target, latestVersion, availableVersions, ttlSeconds) {
|
|
96
134
|
await this.store.set({
|
|
97
135
|
packageName,
|
|
98
136
|
target,
|
|
99
137
|
latestVersion,
|
|
138
|
+
availableVersions,
|
|
100
139
|
fetchedAt: Date.now(),
|
|
101
140
|
ttlSeconds,
|
|
102
141
|
});
|
|
@@ -115,3 +154,17 @@ async function tryCreateSqliteStore(dbPath) {
|
|
|
115
154
|
}
|
|
116
155
|
return null;
|
|
117
156
|
}
|
|
157
|
+
function parseJsonArray(raw, fallback) {
|
|
158
|
+
if (typeof raw !== "string")
|
|
159
|
+
return [fallback];
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
if (!Array.isArray(parsed))
|
|
163
|
+
return [fallback];
|
|
164
|
+
const values = parsed.filter((value) => typeof value === "string");
|
|
165
|
+
return values.length > 0 ? values : [fallback];
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [fallback];
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DependencyKind, OutputFormat, TargetLevel } from "../types/index.js";
|
|
1
|
+
import type { DependencyKind, FailOnLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
2
2
|
export interface FileConfig {
|
|
3
3
|
target?: TargetLevel;
|
|
4
4
|
filter?: string;
|
|
@@ -15,6 +15,13 @@ export interface FileConfig {
|
|
|
15
15
|
offline?: boolean;
|
|
16
16
|
policyFile?: string;
|
|
17
17
|
prReportFile?: string;
|
|
18
|
+
failOn?: FailOnLevel;
|
|
19
|
+
maxUpdates?: number;
|
|
20
|
+
fixPr?: boolean;
|
|
21
|
+
fixBranch?: string;
|
|
22
|
+
fixCommitMessage?: string;
|
|
23
|
+
fixDryRun?: boolean;
|
|
24
|
+
noPrReport?: boolean;
|
|
18
25
|
install?: boolean;
|
|
19
26
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
20
27
|
sync?: boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BaselineOptions, DependencyKind } from "../types/index.js";
|
|
2
|
+
interface BaselineEntry {
|
|
3
|
+
packagePath: string;
|
|
4
|
+
kind: DependencyKind;
|
|
5
|
+
name: string;
|
|
6
|
+
range: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BaselineSaveResult {
|
|
9
|
+
filePath: string;
|
|
10
|
+
entries: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BaselineDiffResult {
|
|
13
|
+
filePath: string;
|
|
14
|
+
added: BaselineEntry[];
|
|
15
|
+
removed: BaselineEntry[];
|
|
16
|
+
changed: Array<{
|
|
17
|
+
before: BaselineEntry;
|
|
18
|
+
after: BaselineEntry;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function saveBaseline(options: BaselineOptions): Promise<BaselineSaveResult>;
|
|
22
|
+
export declare function diffBaseline(options: BaselineOptions): Promise<BaselineDiffResult>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { collectDependencies, readManifest } from "../parsers/package-json.js";
|
|
4
|
+
import { discoverPackageDirs } from "../workspace/discover.js";
|
|
5
|
+
export async function saveBaseline(options) {
|
|
6
|
+
const entries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
|
|
7
|
+
const payload = {
|
|
8
|
+
version: 1,
|
|
9
|
+
createdAt: new Date().toISOString(),
|
|
10
|
+
entries,
|
|
11
|
+
};
|
|
12
|
+
await fs.mkdir(path.dirname(options.filePath), { recursive: true });
|
|
13
|
+
await fs.writeFile(options.filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
14
|
+
return {
|
|
15
|
+
filePath: options.filePath,
|
|
16
|
+
entries: entries.length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function diffBaseline(options) {
|
|
20
|
+
const content = await fs.readFile(options.filePath, "utf8");
|
|
21
|
+
const baseline = JSON.parse(content);
|
|
22
|
+
const currentEntries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
|
|
23
|
+
const baselineMap = new Map(baseline.entries.map((entry) => [toKey(entry), entry]));
|
|
24
|
+
const currentMap = new Map(currentEntries.map((entry) => [toKey(entry), entry]));
|
|
25
|
+
const added = [];
|
|
26
|
+
const removed = [];
|
|
27
|
+
const changed = [];
|
|
28
|
+
for (const [key, current] of currentMap) {
|
|
29
|
+
const base = baselineMap.get(key);
|
|
30
|
+
if (!base) {
|
|
31
|
+
added.push(current);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (base.range !== current.range) {
|
|
35
|
+
changed.push({ before: base, after: current });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [key, base] of baselineMap) {
|
|
39
|
+
if (!currentMap.has(key)) {
|
|
40
|
+
removed.push(base);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
filePath: options.filePath,
|
|
45
|
+
added: sortEntries(added),
|
|
46
|
+
removed: sortEntries(removed),
|
|
47
|
+
changed: changed.sort((a, b) => toKey(a.after).localeCompare(toKey(b.after))),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function collectBaselineEntries(cwd, workspace, includeKinds) {
|
|
51
|
+
const packageDirs = await discoverPackageDirs(cwd, workspace);
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const packageDir of packageDirs) {
|
|
54
|
+
const manifest = await readManifest(packageDir);
|
|
55
|
+
const deps = collectDependencies(manifest, includeKinds);
|
|
56
|
+
for (const dep of deps) {
|
|
57
|
+
entries.push({
|
|
58
|
+
packagePath: path.relative(cwd, packageDir) || ".",
|
|
59
|
+
kind: dep.kind,
|
|
60
|
+
name: dep.name,
|
|
61
|
+
range: dep.range,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return sortEntries(entries);
|
|
66
|
+
}
|
|
67
|
+
function toKey(entry) {
|
|
68
|
+
return `${entry.packagePath}::${entry.kind}::${entry.name}`;
|
|
69
|
+
}
|
|
70
|
+
function sortEntries(entries) {
|
|
71
|
+
return [...entries].sort((a, b) => toKey(a).localeCompare(toKey(b)));
|
|
72
|
+
}
|