@rainy-updates/cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/dist/bin/cli.d.ts +2 -0
  5. package/dist/bin/cli.js +146 -0
  6. package/dist/cache/cache.d.ts +9 -0
  7. package/dist/cache/cache.js +125 -0
  8. package/dist/config/loader.d.ts +22 -0
  9. package/dist/config/loader.js +35 -0
  10. package/dist/config/policy.d.ts +16 -0
  11. package/dist/config/policy.js +32 -0
  12. package/dist/core/check.d.ts +2 -0
  13. package/dist/core/check.js +141 -0
  14. package/dist/core/init-ci.d.ts +4 -0
  15. package/dist/core/init-ci.js +20 -0
  16. package/dist/core/options.d.ts +17 -0
  17. package/dist/core/options.js +238 -0
  18. package/dist/core/upgrade.d.ts +2 -0
  19. package/dist/core/upgrade.js +88 -0
  20. package/dist/core/warm-cache.d.ts +2 -0
  21. package/dist/core/warm-cache.js +89 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +7 -0
  24. package/dist/output/format.d.ts +2 -0
  25. package/dist/output/format.js +56 -0
  26. package/dist/output/github.d.ts +3 -0
  27. package/dist/output/github.js +30 -0
  28. package/dist/output/pr-report.d.ts +2 -0
  29. package/dist/output/pr-report.js +38 -0
  30. package/dist/output/sarif.d.ts +2 -0
  31. package/dist/output/sarif.js +60 -0
  32. package/dist/parsers/package-json.d.ts +5 -0
  33. package/dist/parsers/package-json.js +35 -0
  34. package/dist/pm/detect.d.ts +1 -0
  35. package/dist/pm/detect.js +20 -0
  36. package/dist/pm/install.d.ts +1 -0
  37. package/dist/pm/install.js +21 -0
  38. package/dist/registry/npm.d.ts +14 -0
  39. package/dist/registry/npm.js +128 -0
  40. package/dist/types/index.d.ts +86 -0
  41. package/dist/types/index.js +1 -0
  42. package/dist/utils/async-pool.d.ts +1 -0
  43. package/dist/utils/async-pool.js +21 -0
  44. package/dist/utils/pattern.d.ts +1 -0
  45. package/dist/utils/pattern.js +11 -0
  46. package/dist/utils/semver.d.ts +13 -0
  47. package/dist/utils/semver.js +80 -0
  48. package/dist/workspace/discover.d.ts +1 -0
  49. package/dist/workspace/discover.js +88 -0
  50. package/dist/workspace/graph.d.ts +13 -0
  51. package/dist/workspace/graph.js +86 -0
  52. package/package.json +66 -0
@@ -0,0 +1,32 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export async function loadPolicy(cwd, policyFile) {
4
+ const candidates = policyFile ? [policyFile] : [
5
+ path.join(cwd, ".rainyupdates-policy.json"),
6
+ path.join(cwd, "rainy-updates.policy.json"),
7
+ ];
8
+ for (const candidate of candidates) {
9
+ const filePath = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
10
+ try {
11
+ const content = await fs.readFile(filePath, "utf8");
12
+ const parsed = JSON.parse(content);
13
+ return {
14
+ ignorePatterns: parsed.ignore ?? [],
15
+ packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [
16
+ pkg,
17
+ {
18
+ maxTarget: rule.maxTarget,
19
+ ignore: rule.ignore === true,
20
+ },
21
+ ])),
22
+ };
23
+ }
24
+ catch {
25
+ // noop
26
+ }
27
+ }
28
+ return {
29
+ ignorePatterns: [],
30
+ packageRules: new Map(),
31
+ };
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckOptions, CheckResult } from "../types/index.js";
2
+ export declare function check(options: CheckOptions): Promise<CheckResult>;
@@ -0,0 +1,141 @@
1
+ import path from "node:path";
2
+ import { collectDependencies, readManifest } from "../parsers/package-json.js";
3
+ import { matchesPattern } from "../utils/pattern.js";
4
+ import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersion } from "../utils/semver.js";
5
+ import { VersionCache } from "../cache/cache.js";
6
+ import { NpmRegistryClient } from "../registry/npm.js";
7
+ import { detectPackageManager } from "../pm/detect.js";
8
+ import { discoverPackageDirs } from "../workspace/discover.js";
9
+ import { loadPolicy } from "../config/policy.js";
10
+ export async function check(options) {
11
+ const packageManager = await detectPackageManager(options.cwd);
12
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
13
+ const cache = await VersionCache.create();
14
+ const registryClient = new NpmRegistryClient();
15
+ const policy = await loadPolicy(options.cwd, options.policyFile);
16
+ const updates = [];
17
+ const errors = [];
18
+ const warnings = [];
19
+ let totalDependencies = 0;
20
+ const tasks = [];
21
+ let skipped = 0;
22
+ for (const packageDir of packageDirs) {
23
+ let manifest;
24
+ try {
25
+ manifest = await readManifest(packageDir);
26
+ }
27
+ catch (error) {
28
+ errors.push(`Failed to read package.json in ${packageDir}: ${String(error)}`);
29
+ continue;
30
+ }
31
+ const dependencies = collectDependencies(manifest, options.includeKinds);
32
+ totalDependencies += dependencies.length;
33
+ for (const dep of dependencies) {
34
+ if (!matchesPattern(dep.name, options.filter))
35
+ continue;
36
+ if (options.reject && matchesPattern(dep.name, options.reject))
37
+ continue;
38
+ const rule = policy.packageRules.get(dep.name);
39
+ if (rule?.ignore === true) {
40
+ skipped += 1;
41
+ continue;
42
+ }
43
+ if (policy.ignorePatterns.some((pattern) => matchesPattern(dep.name, pattern))) {
44
+ skipped += 1;
45
+ continue;
46
+ }
47
+ tasks.push({ packageDir, dependency: dep });
48
+ }
49
+ }
50
+ const uniquePackageNames = Array.from(new Set(tasks.map((task) => task.dependency.name)));
51
+ const resolvedVersions = new Map();
52
+ const unresolvedPackages = [];
53
+ for (const packageName of uniquePackageNames) {
54
+ const cached = await cache.getValid(packageName, options.target);
55
+ if (cached) {
56
+ resolvedVersions.set(packageName, cached.latestVersion);
57
+ }
58
+ else {
59
+ unresolvedPackages.push(packageName);
60
+ }
61
+ }
62
+ if (unresolvedPackages.length > 0) {
63
+ if (options.offline) {
64
+ for (const packageName of unresolvedPackages) {
65
+ const stale = await cache.getAny(packageName, options.target);
66
+ if (stale) {
67
+ resolvedVersions.set(packageName, stale.latestVersion);
68
+ warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
69
+ }
70
+ else {
71
+ errors.push(`Offline cache miss for ${packageName}. Run once without --offline to warm cache.`);
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ const fetched = await registryClient.resolveManyLatestVersions(unresolvedPackages, {
77
+ concurrency: options.concurrency,
78
+ });
79
+ for (const [packageName, version] of fetched.versions) {
80
+ resolvedVersions.set(packageName, version);
81
+ if (version) {
82
+ await cache.set(packageName, options.target, version, options.cacheTtlSeconds);
83
+ }
84
+ }
85
+ for (const [packageName, error] of fetched.errors) {
86
+ const stale = await cache.getAny(packageName, options.target);
87
+ if (stale) {
88
+ resolvedVersions.set(packageName, stale.latestVersion);
89
+ warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
90
+ }
91
+ else {
92
+ errors.push(`Unable to resolve ${packageName}: ${error}`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ for (const task of tasks) {
98
+ const latestVersion = resolvedVersions.get(task.dependency.name);
99
+ if (!latestVersion)
100
+ continue;
101
+ const rule = policy.packageRules.get(task.dependency.name);
102
+ const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
103
+ const picked = pickTargetVersion(task.dependency.range, latestVersion, effectiveTarget);
104
+ if (!picked)
105
+ continue;
106
+ const nextRange = applyRangeStyle(task.dependency.range, picked);
107
+ if (nextRange === task.dependency.range)
108
+ continue;
109
+ updates.push({
110
+ packagePath: path.resolve(task.packageDir),
111
+ name: task.dependency.name,
112
+ kind: task.dependency.kind,
113
+ fromRange: task.dependency.range,
114
+ toRange: nextRange,
115
+ toVersionResolved: picked,
116
+ diffType: classifyDiff(task.dependency.range, picked),
117
+ filtered: false,
118
+ reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
119
+ });
120
+ }
121
+ const summary = {
122
+ scannedPackages: packageDirs.length,
123
+ totalDependencies,
124
+ checkedDependencies: tasks.length,
125
+ updatesFound: updates.length,
126
+ upgraded: 0,
127
+ skipped,
128
+ warmedPackages: 0,
129
+ };
130
+ return {
131
+ projectPath: options.cwd,
132
+ packagePaths: packageDirs,
133
+ packageManager,
134
+ target: options.target,
135
+ timestamp: new Date().toISOString(),
136
+ summary,
137
+ updates,
138
+ errors,
139
+ warnings,
140
+ };
141
+ }
@@ -0,0 +1,4 @@
1
+ export declare function initCiWorkflow(cwd: string, force: boolean): Promise<{
2
+ path: string;
3
+ created: boolean;
4
+ }>;
@@ -0,0 +1,20 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export async function initCiWorkflow(cwd, force) {
4
+ const workflowPath = path.join(cwd, ".github", "workflows", "rainy-updates.yml");
5
+ try {
6
+ if (!force) {
7
+ await fs.access(workflowPath);
8
+ return { path: workflowPath, created: false };
9
+ }
10
+ }
11
+ catch {
12
+ // missing file, continue create
13
+ }
14
+ await fs.mkdir(path.dirname(workflowPath), { recursive: true });
15
+ await fs.writeFile(workflowPath, workflowTemplate(), "utf8");
16
+ return { path: workflowPath, created: true };
17
+ }
18
+ function workflowTemplate() {
19
+ return `name: Rainy Updates\n\non:\n schedule:\n - cron: '0 8 * * 1'\n workflow_dispatch:\n\njobs:\n dependency-updates:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n - name: Run rainy updates\n run: |\n npx @rainy-updates/cli check \\\n --workspace \\\n --ci \\\n --concurrency 32 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { CheckOptions, UpgradeOptions } from "../types/index.js";
2
+ export type ParsedCliArgs = {
3
+ command: "check";
4
+ options: CheckOptions;
5
+ } | {
6
+ command: "upgrade";
7
+ options: UpgradeOptions;
8
+ } | {
9
+ command: "warm-cache";
10
+ options: CheckOptions;
11
+ } | {
12
+ command: "init-ci";
13
+ options: CheckOptions & {
14
+ force: boolean;
15
+ };
16
+ };
17
+ export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
@@ -0,0 +1,238 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { loadConfig } from "../config/loader.js";
4
+ const DEFAULT_INCLUDE_KINDS = [
5
+ "dependencies",
6
+ "devDependencies",
7
+ "optionalDependencies",
8
+ "peerDependencies",
9
+ ];
10
+ const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci"];
11
+ export async function parseCliArgs(argv) {
12
+ const firstArg = argv[0];
13
+ const isKnownCommand = KNOWN_COMMANDS.includes(firstArg);
14
+ if (firstArg && !firstArg.startsWith("-") && !isKnownCommand) {
15
+ throw new Error(`Unknown command: ${firstArg}`);
16
+ }
17
+ const command = isKnownCommand ? argv[0] : "check";
18
+ const hasExplicitCommand = isKnownCommand;
19
+ const args = hasExplicitCommand ? argv.slice(1) : argv.slice(0);
20
+ const base = {
21
+ cwd: process.cwd(),
22
+ target: "latest",
23
+ filter: undefined,
24
+ reject: undefined,
25
+ cacheTtlSeconds: 3600,
26
+ includeKinds: DEFAULT_INCLUDE_KINDS,
27
+ ci: false,
28
+ format: "table",
29
+ workspace: false,
30
+ jsonFile: undefined,
31
+ githubOutputFile: undefined,
32
+ sarifFile: undefined,
33
+ concurrency: 16,
34
+ offline: false,
35
+ policyFile: undefined,
36
+ prReportFile: undefined,
37
+ };
38
+ let force = false;
39
+ let resolvedConfig = await loadConfig(base.cwd);
40
+ applyConfig(base, resolvedConfig);
41
+ for (let index = 0; index < args.length; index += 1) {
42
+ const current = args[index];
43
+ const next = args[index + 1];
44
+ if (current === "--target" && next) {
45
+ base.target = ensureTarget(next);
46
+ index += 1;
47
+ continue;
48
+ }
49
+ if (current === "--filter" && next) {
50
+ base.filter = next;
51
+ index += 1;
52
+ continue;
53
+ }
54
+ if (current === "--reject" && next) {
55
+ base.reject = next;
56
+ index += 1;
57
+ continue;
58
+ }
59
+ if (current === "--cwd" && next) {
60
+ base.cwd = path.resolve(next);
61
+ resolvedConfig = await loadConfig(base.cwd);
62
+ applyConfig(base, resolvedConfig);
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (current === "--cache-ttl" && next) {
67
+ const parsed = Number(next);
68
+ if (!Number.isFinite(parsed) || parsed < 0) {
69
+ throw new Error("--cache-ttl must be a positive number");
70
+ }
71
+ base.cacheTtlSeconds = parsed;
72
+ index += 1;
73
+ continue;
74
+ }
75
+ if (current === "--format" && next) {
76
+ base.format = ensureFormat(next);
77
+ index += 1;
78
+ continue;
79
+ }
80
+ if (current === "--ci") {
81
+ base.ci = true;
82
+ continue;
83
+ }
84
+ if (current === "--workspace") {
85
+ base.workspace = true;
86
+ continue;
87
+ }
88
+ if (current === "--json-file" && next) {
89
+ base.jsonFile = path.resolve(next);
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (current === "--github-output" && next) {
94
+ base.githubOutputFile = path.resolve(next);
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (current === "--sarif-file" && next) {
99
+ base.sarifFile = path.resolve(next);
100
+ index += 1;
101
+ continue;
102
+ }
103
+ if (current === "--concurrency" && next) {
104
+ const parsed = Number(next);
105
+ if (!Number.isInteger(parsed) || parsed <= 0) {
106
+ throw new Error("--concurrency must be a positive integer");
107
+ }
108
+ base.concurrency = parsed;
109
+ index += 1;
110
+ continue;
111
+ }
112
+ if (current === "--offline") {
113
+ base.offline = true;
114
+ continue;
115
+ }
116
+ if (current === "--policy-file" && next) {
117
+ base.policyFile = path.resolve(next);
118
+ index += 1;
119
+ continue;
120
+ }
121
+ if (current === "--pr-report-file" && next) {
122
+ base.prReportFile = path.resolve(next);
123
+ index += 1;
124
+ continue;
125
+ }
126
+ if (current === "--force") {
127
+ force = true;
128
+ continue;
129
+ }
130
+ if (current === "--dep-kinds" && next) {
131
+ base.includeKinds = parseDependencyKinds(next);
132
+ index += 1;
133
+ continue;
134
+ }
135
+ }
136
+ if (command === "upgrade") {
137
+ const configPm = resolvedConfig.packageManager;
138
+ const cliPm = parsePackageManager(args);
139
+ const upgradeOptions = {
140
+ ...base,
141
+ install: args.includes("--install") || resolvedConfig.install === true,
142
+ packageManager: cliPm === "auto" ? (configPm ?? "auto") : cliPm,
143
+ sync: args.includes("--sync") || resolvedConfig.sync === true,
144
+ };
145
+ return { command, options: upgradeOptions };
146
+ }
147
+ if (command === "warm-cache") {
148
+ return { command, options: base };
149
+ }
150
+ if (command === "init-ci") {
151
+ return { command, options: { ...base, force } };
152
+ }
153
+ return {
154
+ command: "check",
155
+ options: base,
156
+ };
157
+ }
158
+ function applyConfig(base, config) {
159
+ if (config.target)
160
+ base.target = config.target;
161
+ if (config.filter !== undefined)
162
+ base.filter = config.filter;
163
+ if (config.reject !== undefined)
164
+ base.reject = config.reject;
165
+ if (typeof config.cacheTtlSeconds === "number")
166
+ base.cacheTtlSeconds = config.cacheTtlSeconds;
167
+ if (Array.isArray(config.includeKinds) && config.includeKinds.length > 0)
168
+ base.includeKinds = config.includeKinds;
169
+ if (typeof config.ci === "boolean")
170
+ base.ci = config.ci;
171
+ if (config.format)
172
+ base.format = config.format;
173
+ if (typeof config.workspace === "boolean")
174
+ base.workspace = config.workspace;
175
+ if (typeof config.jsonFile === "string")
176
+ base.jsonFile = path.resolve(base.cwd, config.jsonFile);
177
+ if (typeof config.githubOutputFile === "string") {
178
+ base.githubOutputFile = path.resolve(base.cwd, config.githubOutputFile);
179
+ }
180
+ if (typeof config.sarifFile === "string") {
181
+ base.sarifFile = path.resolve(base.cwd, config.sarifFile);
182
+ }
183
+ if (typeof config.concurrency === "number" && Number.isInteger(config.concurrency) && config.concurrency > 0) {
184
+ base.concurrency = config.concurrency;
185
+ }
186
+ if (typeof config.offline === "boolean") {
187
+ base.offline = config.offline;
188
+ }
189
+ if (typeof config.policyFile === "string") {
190
+ base.policyFile = path.resolve(base.cwd, config.policyFile);
191
+ }
192
+ if (typeof config.prReportFile === "string") {
193
+ base.prReportFile = path.resolve(base.cwd, config.prReportFile);
194
+ }
195
+ }
196
+ function parsePackageManager(args) {
197
+ const index = args.indexOf("--pm");
198
+ if (index === -1)
199
+ return "auto";
200
+ const value = args[index + 1] ?? "auto";
201
+ if (value === "auto" || value === "npm" || value === "pnpm") {
202
+ return value;
203
+ }
204
+ throw new Error("--pm must be auto, npm or pnpm");
205
+ }
206
+ function ensureTarget(value) {
207
+ if (value === "patch" || value === "minor" || value === "major" || value === "latest") {
208
+ return value;
209
+ }
210
+ throw new Error("--target must be patch, minor, major, latest");
211
+ }
212
+ function ensureFormat(value) {
213
+ if (value === "table" || value === "json" || value === "minimal" || value === "github") {
214
+ return value;
215
+ }
216
+ throw new Error("--format must be table, json, minimal or github");
217
+ }
218
+ function parseDependencyKinds(value) {
219
+ const mapped = value
220
+ .split(",")
221
+ .map((item) => item.trim())
222
+ .filter(Boolean)
223
+ .map((item) => {
224
+ if (item === "dependencies" || item === "deps")
225
+ return "dependencies";
226
+ if (item === "devDependencies" || item === "dev")
227
+ return "devDependencies";
228
+ if (item === "optionalDependencies" || item === "optional")
229
+ return "optionalDependencies";
230
+ if (item === "peerDependencies" || item === "peer")
231
+ return "peerDependencies";
232
+ throw new Error(`Unknown dependency kind: ${item}`);
233
+ });
234
+ if (mapped.length === 0) {
235
+ throw new Error("--dep-kinds requires at least one value");
236
+ }
237
+ return Array.from(new Set(mapped));
238
+ }
@@ -0,0 +1,2 @@
1
+ import type { UpgradeOptions, UpgradeResult } from "../types/index.js";
2
+ export declare function upgrade(options: UpgradeOptions): Promise<UpgradeResult>;
@@ -0,0 +1,88 @@
1
+ import { check } from "./check.js";
2
+ import { readManifest, writeManifest } from "../parsers/package-json.js";
3
+ import { installDependencies } from "../pm/install.js";
4
+ import { applyRangeStyle, parseVersion, compareVersions } from "../utils/semver.js";
5
+ import { buildWorkspaceGraph } from "../workspace/graph.js";
6
+ export async function upgrade(options) {
7
+ const checkResult = await check(options);
8
+ if (checkResult.updates.length === 0) {
9
+ return {
10
+ ...checkResult,
11
+ changed: false,
12
+ };
13
+ }
14
+ const manifestsByPath = new Map();
15
+ for (const update of checkResult.updates) {
16
+ const manifestPath = update.packagePath;
17
+ let manifest = manifestsByPath.get(manifestPath);
18
+ if (!manifest) {
19
+ manifest = await readManifest(manifestPath);
20
+ manifestsByPath.set(manifestPath, manifest);
21
+ }
22
+ applyDependencyVersion(manifest, update.kind, update.name, update.toRange);
23
+ }
24
+ if (options.sync) {
25
+ const graph = buildWorkspaceGraph(manifestsByPath, options.includeKinds);
26
+ if (graph.cycles.length > 0) {
27
+ checkResult.warnings.push(`Workspace graph contains cycle(s): ${graph.cycles.map((cycle) => cycle.join(" -> ")).join(" | ")}`);
28
+ }
29
+ applyWorkspaceSync(manifestsByPath, graph.orderedPaths, graph.localPackageNames, options.includeKinds, checkResult.updates);
30
+ }
31
+ for (const [manifestPath, manifest] of manifestsByPath) {
32
+ await writeManifest(manifestPath, manifest);
33
+ }
34
+ if (options.install) {
35
+ await installDependencies(options.cwd, options.packageManager, checkResult.packageManager);
36
+ }
37
+ return {
38
+ ...checkResult,
39
+ changed: true,
40
+ summary: {
41
+ ...checkResult.summary,
42
+ upgraded: checkResult.updates.length,
43
+ },
44
+ };
45
+ }
46
+ function applyWorkspaceSync(manifestsByPath, orderedPaths, localPackageNames, includeKinds, updates) {
47
+ const desiredByPackage = new Map();
48
+ for (const update of updates) {
49
+ const current = desiredByPackage.get(update.name);
50
+ if (!current) {
51
+ desiredByPackage.set(update.name, update.toVersionResolved);
52
+ continue;
53
+ }
54
+ const currentParsed = parseVersion(current);
55
+ const nextParsed = parseVersion(update.toVersionResolved);
56
+ if (!currentParsed || !nextParsed) {
57
+ desiredByPackage.set(update.name, update.toVersionResolved);
58
+ continue;
59
+ }
60
+ if (compareVersions(nextParsed, currentParsed) > 0) {
61
+ desiredByPackage.set(update.name, update.toVersionResolved);
62
+ }
63
+ }
64
+ for (const manifestPath of orderedPaths) {
65
+ const manifest = manifestsByPath.get(manifestPath);
66
+ if (!manifest)
67
+ continue;
68
+ for (const kind of includeKinds) {
69
+ const section = manifest[kind];
70
+ if (!section)
71
+ continue;
72
+ for (const [depName, depRange] of Object.entries(section)) {
73
+ const desiredVersion = desiredByPackage.get(depName);
74
+ if (!desiredVersion)
75
+ continue;
76
+ if (localPackageNames.has(depName) && depRange.startsWith("workspace:"))
77
+ continue;
78
+ section[depName] = applyRangeStyle(depRange, desiredVersion);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ function applyDependencyVersion(manifest, kind, depName, nextRange) {
84
+ const section = manifest[kind];
85
+ if (!section || !section[depName])
86
+ return;
87
+ section[depName] = nextRange;
88
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckOptions, CheckResult } from "../types/index.js";
2
+ export declare function warmCache(options: CheckOptions): Promise<CheckResult>;
@@ -0,0 +1,89 @@
1
+ import { collectDependencies, readManifest } from "../parsers/package-json.js";
2
+ import { matchesPattern } from "../utils/pattern.js";
3
+ import { VersionCache } from "../cache/cache.js";
4
+ import { NpmRegistryClient } from "../registry/npm.js";
5
+ import { detectPackageManager } from "../pm/detect.js";
6
+ import { discoverPackageDirs } from "../workspace/discover.js";
7
+ export async function warmCache(options) {
8
+ const packageManager = await detectPackageManager(options.cwd);
9
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
10
+ const cache = await VersionCache.create();
11
+ const registryClient = new NpmRegistryClient();
12
+ const errors = [];
13
+ const warnings = [];
14
+ let totalDependencies = 0;
15
+ const packageNames = new Set();
16
+ for (const packageDir of packageDirs) {
17
+ try {
18
+ const manifest = await readManifest(packageDir);
19
+ const dependencies = collectDependencies(manifest, options.includeKinds);
20
+ totalDependencies += dependencies.length;
21
+ for (const dep of dependencies) {
22
+ if (!matchesPattern(dep.name, options.filter))
23
+ continue;
24
+ if (options.reject && matchesPattern(dep.name, options.reject))
25
+ continue;
26
+ packageNames.add(dep.name);
27
+ }
28
+ }
29
+ catch (error) {
30
+ errors.push(`Failed to read package.json in ${packageDir}: ${String(error)}`);
31
+ }
32
+ }
33
+ const names = Array.from(packageNames);
34
+ const needsFetch = [];
35
+ for (const pkg of names) {
36
+ const valid = await cache.getValid(pkg, options.target);
37
+ if (!valid)
38
+ needsFetch.push(pkg);
39
+ }
40
+ let warmed = 0;
41
+ if (needsFetch.length > 0) {
42
+ if (options.offline) {
43
+ for (const pkg of needsFetch) {
44
+ const stale = await cache.getAny(pkg, options.target);
45
+ if (stale) {
46
+ warnings.push(`Using stale cache for ${pkg} in offline warm-cache mode.`);
47
+ warmed += 1;
48
+ }
49
+ else {
50
+ errors.push(`Offline cache miss for ${pkg}. Cannot warm cache in --offline mode.`);
51
+ }
52
+ }
53
+ }
54
+ else {
55
+ const fetched = await registryClient.resolveManyLatestVersions(needsFetch, {
56
+ concurrency: options.concurrency,
57
+ });
58
+ for (const [pkg, version] of fetched.versions) {
59
+ if (version) {
60
+ await cache.set(pkg, options.target, version, options.cacheTtlSeconds);
61
+ warmed += 1;
62
+ }
63
+ }
64
+ for (const [pkg, error] of fetched.errors) {
65
+ errors.push(`Unable to warm ${pkg}: ${error}`);
66
+ }
67
+ }
68
+ }
69
+ const summary = {
70
+ scannedPackages: packageDirs.length,
71
+ totalDependencies,
72
+ checkedDependencies: names.length,
73
+ updatesFound: 0,
74
+ upgraded: 0,
75
+ skipped: 0,
76
+ warmedPackages: warmed,
77
+ };
78
+ return {
79
+ projectPath: options.cwd,
80
+ packagePaths: packageDirs,
81
+ packageManager,
82
+ target: options.target,
83
+ timestamp: new Date().toISOString(),
84
+ summary,
85
+ updates: [],
86
+ errors,
87
+ warnings,
88
+ };
89
+ }
@@ -0,0 +1,8 @@
1
+ export { check } from "./core/check.js";
2
+ export { upgrade } from "./core/upgrade.js";
3
+ export { warmCache } from "./core/warm-cache.js";
4
+ export { initCiWorkflow } from "./core/init-ci.js";
5
+ export { createSarifReport } from "./output/sarif.js";
6
+ export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
7
+ export { renderPrReport } from "./output/pr-report.js";
8
+ export type { CheckOptions, CheckResult, DependencyKind, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { check } from "./core/check.js";
2
+ export { upgrade } from "./core/upgrade.js";
3
+ export { warmCache } from "./core/warm-cache.js";
4
+ export { initCiWorkflow } from "./core/init-ci.js";
5
+ export { createSarifReport } from "./output/sarif.js";
6
+ export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
7
+ export { renderPrReport } from "./output/pr-report.js";
@@ -0,0 +1,2 @@
1
+ import type { CheckResult, OutputFormat } from "../types/index.js";
2
+ export declare function renderResult(result: CheckResult, format: OutputFormat): string;