renovate-mcp 0.9.4 → 0.11.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.
- package/README.md +6 -3
- package/dist/lib/renovateCli.d.ts +39 -7
- package/dist/lib/renovateCli.js +59 -9
- package/dist/lib/runtimeWarnings.d.ts +25 -0
- package/dist/lib/runtimeWarnings.js +106 -0
- package/dist/lib/setupCheck.d.ts +23 -1
- package/dist/lib/setupCheck.js +63 -18
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/dryRun.js +5 -2
- package/dist/tools/validateConfig.js +7 -3
- package/dist/tools/writeConfig.js +25 -17
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
|
|
|
48
48
|
- **Not a Renovate replacement.** This server doesn't open PRs, run scheduled updates, or execute in CI — it's a design-time companion for a local `renovate.json`. Use the real Renovate for the actual dependency-update pipeline.
|
|
49
49
|
- **`resolve_config` is preview-quality.** Preset expansion runs against a committed snapshot, and template substitution implements only positional `{{argN}}` placeholders — non-positional tokens and Handlebars helpers are flagged in `warnings` and pass through verbatim. For authoritative output, run `dry_run`.
|
|
50
50
|
- **`preview_custom_manager` is a subset of Renovate's regex manager.** It covers `customType: "regex"`, `matchStringsStrategy` of `any` / `combination` / `recursive`, and `{{groupName}}` template substitution only — other custom types and full Handlebars (helpers, conditionals) are not implemented. Use it for fast regex iteration; confirm with `dry_run`.
|
|
51
|
-
- **`validate_config` / `dry_run` aren't exercised in CI.**
|
|
51
|
+
- **`validate_config` / `dry_run` aren't exercised end-to-end in CI.** The bundled Renovate is available (it's a runtime dep), but the integration tests use fake binaries via `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` env overrides for determinism and speed. Run the tools locally against a real config to validate behaviour.
|
|
52
52
|
|
|
53
53
|
## Requirements
|
|
54
54
|
|
|
@@ -56,11 +56,12 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
|
|
|
56
56
|
|
|
57
57
|
- Linux or macOS. Windows is not supported — `package.json` declares `"os": ["darwin", "linux"]`, so `npm i` surfaces an `EBADPLATFORM` warning on Windows, and the server exits with a clear stderr message at startup. Run on WSL2 or a Linux/macOS host instead.
|
|
58
58
|
- Node.js ≥ 24 (aligns with Renovate's own engine requirement).
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `resolve_config`, `explain_config`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
|
|
60
61
|
|
|
61
62
|
**Optional**
|
|
62
63
|
|
|
63
|
-
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override
|
|
64
|
+
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override the bundled binaries (e.g., to point at a different Renovate install). When set, the override always wins.
|
|
64
65
|
- `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
|
|
65
66
|
- Platform env vars — `RENOVATE_PLATFORM`, `RENOVATE_ENDPOINT`, and a token (`RENOVATE_TOKEN` / `GITHUB_TOKEN` / `GITLAB_TOKEN`). Needed for `dry_run` against a remote platform and for `resolve_config` with `externalPresets: true`. See [Platform setup](#platform-setup) for the matrix and a worked example.
|
|
66
67
|
- Private-registry credentials for `dry_run` — whatever Renovate itself would need at lookup time (`COMPOSER_AUTH`, `NPM_TOKEN` / `.npmrc`, Docker registry creds, or a `RENOVATE_HOST_RULES` JSON blob). Alternatively encode these as `hostRules` in the Renovate config, or pass them per-call via the `hostRules` input on `dry_run` (no MCP restart needed). Per-call `hostRules` are appended to whatever the repo's own config declares. Values reach Renovate as JSON through the tool-call transport, so the calling LLM sees them in its context — prefer the env route if that matters. Without any of these, Renovate's lookup often returns 0 updates silently; `dry_run` scans its logs for auth failures and surfaces them under `problems`.
|
|
@@ -255,6 +256,7 @@ Secrets required on the repo: `RELEASE_PLEASE_TOKEN` (a PAT for release-please).
|
|
|
255
256
|
Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); this section covers implementation decisions behind the scope that *is* supported.
|
|
256
257
|
|
|
257
258
|
- `validate_config`, `dry_run`, and `write_config` shell out to the Renovate CLI rather than importing Renovate as a library — this decouples our Node version from Renovate's (currently Node 24).
|
|
259
|
+
- The `renovate` package is bundled as a runtime dependency so users don't need a separate `npm i -g renovate`. We still spawn the binary as a child process (via `node node_modules/renovate/dist/renovate.js`) — never `import "renovate"` from `src/`. Resolution order is `RENOVATE_BIN` env override → bundled JS entry resolved via `require.resolve("renovate/package.json")` → bare tool name on `PATH`. The env override is the supported escape hatch when a user wants a different Renovate version.
|
|
258
260
|
- `resolve_config`, `preview_custom_manager`, and `lint_config` are fully in-process and never invoke the Renovate CLI, so they work without a Renovate install.
|
|
259
261
|
- `lint_config` is a semantic lint pass that sits alongside `validate_config` rather than replacing it: schema validation catches structural bugs, the linter catches Renovate-specific footguns that schema validation declares valid — most commonly a pattern like `"matchPackageNames": ["/devops\\/pipelines\\/.+"]` where a trailing `/` is missing and Renovate silently degrades the value to an exact-string match that never hits, or a typo like `"matchManagers": ["npmm"]` that silently applies the rule to zero packages. The ruleset is intentionally small (three rules: `dead-regex-missing-slash`, `unwrapped-regex`, `matchManagers-unknown-name`), scoped to the four regex-aware fields plus `matchManagers` / `excludeManagers`, and tuned to avoid false positives on benign exact strings containing a `.`. The valid-manager list is snapshotted from the `renovate` devDep at `src/data/managers.generated.ts` (regenerate with `npm run generate:managers`), and unknown names get a Damerau-Levenshtein "did you mean?" suggestion when something close enough exists. Rule IDs are stable so findings can be suppressed by callers.
|
|
260
262
|
- `preview_custom_manager` honors `.gitignore` (including nested `.gitignore`s and `.git/info/exclude`) when walking the repo, so generated/vendored directories like `dist/`, `.next/`, `target/`, `__pycache__/` don't crowd out real hits against the `maxFilesWalked` cap. `node_modules/` and `.git/` are always skipped as a safety net even when no `.gitignore` is present.
|
|
@@ -274,3 +276,4 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
|
|
|
274
276
|
- `dry_run` emits MCP progress notifications only when the caller's `tools/call` includes `_meta.progressToken` — no-op otherwise, so legacy clients see zero overhead. A 5-second heartbeat ticks while the child runs; each tick's message is best-effort enriched with the latest Renovate JSON-log `msg` seen on stdout. Notifications are also emitted at start and completion. We deliberately don't couple to Renovate's log schema beyond reading `msg`, since that schema isn't a stable API.
|
|
275
277
|
- `dry_run_diff` is stateless on purpose: both reports are passed as inputs, no per-repo state is kept on the server. Updates are keyed by `(manager, packageFile, depName)` so a version bump on the same dep shows up once under `changed` rather than twice as `removed + added`. Compared per identity: `newValue`, `newVersion`, `updateType`, `branchName`, `groupName`, `schedule`. Either input can be the raw Renovate report or the full `dry_run` summary (with the `report` key) — the tool unwraps `report` automatically.
|
|
276
278
|
- `write_config` writes to a temp file, validates, then atomically renames — so a failed validation never leaves a broken config on disk. The `force: true` escape hatch is gated behind a separate `confirmForce: "YES_OVERRIDE_VALIDATION"` literal: a single boolean is too easy for a prompt-injected agent to flip, but emitting an unusual sentinel string is a deliberate step that's harder to navigate accidentally.
|
|
279
|
+
- `check_setup` and the shell-out tools (`validate_config`, `write_config`, `dry_run`) detect Renovate runtime conditions that degrade the tool silently — currently the bundled `re2` native module failing to load (typically after a Node major-version upgrade leaves a prebuilt `re2.node` compiled against the old ABI). When the WARN appears in Renovate's stderr, the tool response carries a `warnings: [{ kind: "re2-unusable", … }]` entry alongside its normal output and `check_setup` aggregates the same warning into `SetupStatus.warnings` plus the startup `instructions` banner. Renovate keeps running on JavaScript `RegExp` so calls don't fail, but regex-heavy operations (custom managers, validation, lookups) are noticeably slower; the fix hint is to reinstall renovate-mcp (which reinstalls the bundled `renovate` and rebuilds its native deps), or run `npm rebuild re2` inside the renovate-mcp install's `node_modules/renovate`. This is passive detection only — the server doesn't rebuild anything itself.
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { type RuntimeWarning } from "./runtimeWarnings.js";
|
|
1
2
|
export interface RunResult {
|
|
2
3
|
stdout: string;
|
|
3
4
|
stderr: string;
|
|
4
5
|
exitCode: number;
|
|
6
|
+
/**
|
|
7
|
+
* Renovate runtime conditions detected from stderr that the caller benefits
|
|
8
|
+
* from knowing about even though the run itself may have succeeded (e.g. RE2
|
|
9
|
+
* native-module dlopen failure causing a silent slow-path fallback). Always
|
|
10
|
+
* present — empty array when nothing was detected.
|
|
11
|
+
*/
|
|
12
|
+
runtimeWarnings: RuntimeWarning[];
|
|
5
13
|
}
|
|
6
14
|
export interface RunOptions {
|
|
7
15
|
cwd?: string;
|
|
@@ -23,17 +31,41 @@ export interface RunOptions {
|
|
|
23
31
|
* timeouts.
|
|
24
32
|
*/
|
|
25
33
|
export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
|
|
34
|
+
export interface ResolvedRenovateTool {
|
|
35
|
+
/** Process to spawn. */
|
|
36
|
+
cmd: string;
|
|
37
|
+
/** Args prepended to the caller's args (e.g., the JS file path when `cmd` is `node`). */
|
|
38
|
+
prefixArgs: string[];
|
|
39
|
+
/** Where the binary came from — drives setup-status messaging. */
|
|
40
|
+
source: "env" | "bundled" | "path";
|
|
41
|
+
/**
|
|
42
|
+
* Raw user-visible identifier for the resolved binary. For `env` it's the
|
|
43
|
+
* env-var value (often an absolute path); for `bundled` and `path` it's the
|
|
44
|
+
* tool name. Callers compose this with `source` when rendering a label —
|
|
45
|
+
* keeping it raw avoids producing nested parens in error messages like
|
|
46
|
+
* `MISSING — <err> (<command>)`.
|
|
47
|
+
*/
|
|
48
|
+
command: string;
|
|
49
|
+
}
|
|
26
50
|
/**
|
|
27
|
-
* Resolve how to invoke a Renovate CLI tool.
|
|
28
|
-
* RENOVATE_BIN
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
51
|
+
* Resolve how to invoke a Renovate CLI tool. Resolution order:
|
|
52
|
+
* 1. `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` env override — if set,
|
|
53
|
+
* always wins (even if it points at a broken path; we surface the spawn
|
|
54
|
+
* error rather than silently falling through).
|
|
55
|
+
* 2. Bundled binary — the `renovate` package is a runtime dep, so its
|
|
56
|
+
* `bin/renovate.js` / `bin/config-validator.js` ship with the install.
|
|
57
|
+
* Spawned as `node <jsPath>` to sidestep shebang/permission concerns.
|
|
58
|
+
* 3. Bare tool name on `PATH` — last-resort fallback for the unusual case
|
|
59
|
+
* where the bundled lookup fails (e.g. a corrupted node_modules). Only
|
|
60
|
+
* manually tested; unit-testing this branch would require monkey-patching
|
|
61
|
+
* `require.resolve` to fake a bundled-lookup failure.
|
|
32
62
|
*/
|
|
33
|
-
export declare function resolveRenovateTool(tool: "renovate" | "renovate-config-validator"):
|
|
63
|
+
export declare function resolveRenovateTool(tool: "renovate" | "renovate-config-validator"): ResolvedRenovateTool;
|
|
34
64
|
/**
|
|
35
65
|
* Centralized message for when a Renovate CLI binary can't be spawned (ENOENT,
|
|
36
66
|
* permission denied, etc.). Used by all tools that shell out so users get
|
|
37
|
-
* consistent, actionable hints instead of raw spawn errors.
|
|
67
|
+
* consistent, actionable hints instead of raw spawn errors. Renovate ships
|
|
68
|
+
* bundled with the server, so the most likely cause of failure here is a
|
|
69
|
+
* deliberately broken `RENOVATE_BIN` override or a corrupted install.
|
|
38
70
|
*/
|
|
39
71
|
export declare function formatMissingBinaryError(tool: "renovate" | "renovate-config-validator", cause: Error): string;
|
package/dist/lib/renovateCli.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { detectRuntimeWarnings } from "./runtimeWarnings.js";
|
|
6
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
2
7
|
/**
|
|
3
8
|
* Spawn a command and capture stdout/stderr. Never throws on non-zero exit —
|
|
4
9
|
* caller inspects exitCode. Throws only for spawn errors (ENOENT etc.) and
|
|
@@ -78,7 +83,12 @@ export function run(cmd, args, opts = {}) {
|
|
|
78
83
|
reject(new Error(`Command timed out after ${opts.timeoutMs}ms: ${cmd} ${args.join(" ")}`));
|
|
79
84
|
return;
|
|
80
85
|
}
|
|
81
|
-
resolve({
|
|
86
|
+
resolve({
|
|
87
|
+
stdout,
|
|
88
|
+
stderr,
|
|
89
|
+
exitCode: code ?? -1,
|
|
90
|
+
runtimeWarnings: detectRuntimeWarnings(stderr),
|
|
91
|
+
});
|
|
82
92
|
});
|
|
83
93
|
if (opts.stdin != null) {
|
|
84
94
|
child.stdin.end(opts.stdin);
|
|
@@ -89,26 +99,66 @@ export function run(cmd, args, opts = {}) {
|
|
|
89
99
|
});
|
|
90
100
|
}
|
|
91
101
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `
|
|
102
|
+
* Locate the JS entry-point for one of Renovate's CLI tools inside the
|
|
103
|
+
* `renovate` npm package shipped as a runtime dep. Returns `null` if the
|
|
104
|
+
* package can't be resolved or its `bin` field doesn't list the tool —
|
|
105
|
+
* callers fall back to PATH lookup so a user with a deliberately broken
|
|
106
|
+
* `renovate` install (or an unusual install topology that hides it from
|
|
107
|
+
* `require.resolve`) still has an escape hatch.
|
|
108
|
+
*/
|
|
109
|
+
function resolveBundledBinary(tool) {
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = requireFromHere.resolve("renovate/package.json");
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
113
|
+
const binEntry = pkg.bin?.[tool];
|
|
114
|
+
if (typeof binEntry !== "string")
|
|
115
|
+
return null;
|
|
116
|
+
const binAbs = path.join(path.dirname(pkgPath), binEntry);
|
|
117
|
+
if (!fs.existsSync(binAbs))
|
|
118
|
+
return null;
|
|
119
|
+
return binAbs;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve how to invoke a Renovate CLI tool. Resolution order:
|
|
127
|
+
* 1. `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` env override — if set,
|
|
128
|
+
* always wins (even if it points at a broken path; we surface the spawn
|
|
129
|
+
* error rather than silently falling through).
|
|
130
|
+
* 2. Bundled binary — the `renovate` package is a runtime dep, so its
|
|
131
|
+
* `bin/renovate.js` / `bin/config-validator.js` ship with the install.
|
|
132
|
+
* Spawned as `node <jsPath>` to sidestep shebang/permission concerns.
|
|
133
|
+
* 3. Bare tool name on `PATH` — last-resort fallback for the unusual case
|
|
134
|
+
* where the bundled lookup fails (e.g. a corrupted node_modules). Only
|
|
135
|
+
* manually tested; unit-testing this branch would require monkey-patching
|
|
136
|
+
* `require.resolve` to fake a bundled-lookup failure.
|
|
97
137
|
*/
|
|
98
138
|
export function resolveRenovateTool(tool) {
|
|
99
139
|
const envKey = tool === "renovate" ? "RENOVATE_BIN" : "RENOVATE_CONFIG_VALIDATOR_BIN";
|
|
100
|
-
|
|
140
|
+
const envValue = process.env[envKey];
|
|
141
|
+
if (envValue) {
|
|
142
|
+
return { cmd: envValue, prefixArgs: [], source: "env", command: envValue };
|
|
143
|
+
}
|
|
144
|
+
const bundled = resolveBundledBinary(tool);
|
|
145
|
+
if (bundled) {
|
|
146
|
+
return { cmd: process.execPath, prefixArgs: [bundled], source: "bundled", command: tool };
|
|
147
|
+
}
|
|
148
|
+
return { cmd: tool, prefixArgs: [], source: "path", command: tool };
|
|
101
149
|
}
|
|
102
150
|
/**
|
|
103
151
|
* Centralized message for when a Renovate CLI binary can't be spawned (ENOENT,
|
|
104
152
|
* permission denied, etc.). Used by all tools that shell out so users get
|
|
105
|
-
* consistent, actionable hints instead of raw spawn errors.
|
|
153
|
+
* consistent, actionable hints instead of raw spawn errors. Renovate ships
|
|
154
|
+
* bundled with the server, so the most likely cause of failure here is a
|
|
155
|
+
* deliberately broken `RENOVATE_BIN` override or a corrupted install.
|
|
106
156
|
*/
|
|
107
157
|
export function formatMissingBinaryError(tool, cause) {
|
|
108
158
|
const envKey = tool === "renovate" ? "RENOVATE_BIN" : "RENOVATE_CONFIG_VALIDATOR_BIN";
|
|
109
159
|
return [
|
|
110
160
|
`Failed to run \`${tool}\`: ${cause.message}.`,
|
|
111
|
-
`
|
|
161
|
+
`Renovate ships bundled with renovate-mcp; if the bundled binary fails to load, set ${envKey} to a working binary or reinstall renovate-mcp.`,
|
|
112
162
|
"Call the `check_setup` tool for a full diagnostic.",
|
|
113
163
|
].join(" ");
|
|
114
164
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect Renovate runtime warnings emitted to stderr that callers benefit from
|
|
3
|
+
* knowing about even though Renovate itself keeps running. Currently the only
|
|
4
|
+
* detected condition is the RE2 native-module dlopen failure: when the bundled
|
|
5
|
+
* `re2.node` was compiled against a different Node ABI than the one currently
|
|
6
|
+
* running, Renovate logs a single WARN and silently falls back to JavaScript
|
|
7
|
+
* `RegExp`. The fallback is functional but significantly slower for
|
|
8
|
+
* regex-heavy operations (custom managers, validation, lookups), and the
|
|
9
|
+
* degradation is otherwise invisible to MCP callers.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the parsing pattern in `lookupProblems.ts`: line-by-line scan,
|
|
12
|
+
* try-JSON-then-plain-text per line, dedupe, and cap the output. We also keep
|
|
13
|
+
* the parser narrow on purpose — Renovate emits many benign WARNs during
|
|
14
|
+
* normal runs (preset deprecations, etc.) and a generic capture would leak
|
|
15
|
+
* them into our responses.
|
|
16
|
+
*/
|
|
17
|
+
export type RuntimeWarningKind = "re2-unusable";
|
|
18
|
+
export interface RuntimeWarning {
|
|
19
|
+
kind: RuntimeWarningKind;
|
|
20
|
+
message: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
fix: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function detectRuntimeWarnings(stderr: string): RuntimeWarning[];
|
|
25
|
+
export declare function dedupeRuntimeWarnings(warnings: RuntimeWarning[]): RuntimeWarning[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect Renovate runtime warnings emitted to stderr that callers benefit from
|
|
3
|
+
* knowing about even though Renovate itself keeps running. Currently the only
|
|
4
|
+
* detected condition is the RE2 native-module dlopen failure: when the bundled
|
|
5
|
+
* `re2.node` was compiled against a different Node ABI than the one currently
|
|
6
|
+
* running, Renovate logs a single WARN and silently falls back to JavaScript
|
|
7
|
+
* `RegExp`. The fallback is functional but significantly slower for
|
|
8
|
+
* regex-heavy operations (custom managers, validation, lookups), and the
|
|
9
|
+
* degradation is otherwise invisible to MCP callers.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the parsing pattern in `lookupProblems.ts`: line-by-line scan,
|
|
12
|
+
* try-JSON-then-plain-text per line, dedupe, and cap the output. We also keep
|
|
13
|
+
* the parser narrow on purpose — Renovate emits many benign WARNs during
|
|
14
|
+
* normal runs (preset deprecations, etc.) and a generic capture would leak
|
|
15
|
+
* them into our responses.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_DETAIL_LENGTH = 300;
|
|
18
|
+
const MAX_WARNINGS = 10;
|
|
19
|
+
const RE2_FIX_HINT = "The RE2 native module bundled with Renovate failed to load (likely after a Node version upgrade). Renovate is falling back to JavaScript `RegExp`, which is significantly slower for regex-heavy operations. To restore vectorized regex: reinstall renovate-mcp (which reinstalls the bundled `renovate` and rebuilds its native deps), or run `npm rebuild re2` inside the renovate-mcp install's `node_modules/renovate`. If `RENOVATE_BIN` points at a different Renovate install, run `npm rebuild re2` there instead.";
|
|
20
|
+
const RE2_MESSAGE = "Renovate's RE2 native module is unusable; falling back to JavaScript `RegExp` (slower).";
|
|
21
|
+
function truncate(s) {
|
|
22
|
+
if (s.length <= MAX_DETAIL_LENGTH)
|
|
23
|
+
return s;
|
|
24
|
+
return `${s.slice(0, MAX_DETAIL_LENGTH - 1)}…`;
|
|
25
|
+
}
|
|
26
|
+
function isRe2DlopenFailure(err) {
|
|
27
|
+
if (!err || typeof err !== "object")
|
|
28
|
+
return null;
|
|
29
|
+
const obj = err;
|
|
30
|
+
const code = typeof obj.code === "string" ? obj.code : undefined;
|
|
31
|
+
const message = typeof obj.message === "string" ? obj.message : undefined;
|
|
32
|
+
if (code !== "ERR_DLOPEN_FAILED")
|
|
33
|
+
return null;
|
|
34
|
+
if (!message || !/re2\.node/.test(message))
|
|
35
|
+
return null;
|
|
36
|
+
return { detail: truncate(message) };
|
|
37
|
+
}
|
|
38
|
+
export function detectRuntimeWarnings(stderr) {
|
|
39
|
+
if (!stderr)
|
|
40
|
+
return [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const pushRe2 = (detail) => {
|
|
44
|
+
if (seen.has("re2-unusable"))
|
|
45
|
+
return;
|
|
46
|
+
if (warnings.length >= MAX_WARNINGS)
|
|
47
|
+
return;
|
|
48
|
+
seen.add("re2-unusable");
|
|
49
|
+
const w = { kind: "re2-unusable", message: RE2_MESSAGE, fix: RE2_FIX_HINT };
|
|
50
|
+
if (detail)
|
|
51
|
+
w.detail = detail;
|
|
52
|
+
warnings.push(w);
|
|
53
|
+
};
|
|
54
|
+
for (const rawLine of stderr.split(/\r?\n/)) {
|
|
55
|
+
const line = rawLine.trim();
|
|
56
|
+
if (!line)
|
|
57
|
+
continue;
|
|
58
|
+
if (warnings.length >= MAX_WARNINGS)
|
|
59
|
+
break;
|
|
60
|
+
if (line.startsWith("{")) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(line);
|
|
63
|
+
if (parsed && typeof parsed === "object") {
|
|
64
|
+
const msg = typeof parsed.msg === "string" ? parsed.msg : "";
|
|
65
|
+
if (msg.includes("RE2 not usable")) {
|
|
66
|
+
const detail = parsed.err && typeof parsed.err === "object" && parsed.err !== null
|
|
67
|
+
? typeof parsed.err.message === "string"
|
|
68
|
+
? truncate(parsed.err.message)
|
|
69
|
+
: undefined
|
|
70
|
+
: undefined;
|
|
71
|
+
pushRe2(detail);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const re2Err = isRe2DlopenFailure(parsed.err);
|
|
75
|
+
if (re2Err) {
|
|
76
|
+
pushRe2(re2Err.detail);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// fall through to plain-text matching below
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (/\bRE2 not usable\b/.test(line)) {
|
|
86
|
+
pushRe2();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (/\bERR_DLOPEN_FAILED\b/.test(line) && /re2\.node/.test(line)) {
|
|
90
|
+
pushRe2(truncate(line));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return warnings;
|
|
95
|
+
}
|
|
96
|
+
export function dedupeRuntimeWarnings(warnings) {
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const w of warnings) {
|
|
100
|
+
if (seen.has(w.kind))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(w.kind);
|
|
103
|
+
out.push(w);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
package/dist/lib/setupCheck.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { type RuntimeWarning } from "./runtimeWarnings.js";
|
|
1
2
|
export type RenovateBinary = "renovate" | "renovate-config-validator";
|
|
2
3
|
export interface BinaryStatus {
|
|
3
4
|
tool: RenovateBinary;
|
|
4
5
|
command: string;
|
|
6
|
+
/**
|
|
7
|
+
* Where the resolved binary came from. `bundled` means renovate-mcp's own
|
|
8
|
+
* `node_modules/renovate` was used (the default); `env` means the user set
|
|
9
|
+
* `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN`; `path` means the bundled
|
|
10
|
+
* lookup failed and we fell through to the bare tool name on `PATH`.
|
|
11
|
+
*/
|
|
12
|
+
source: "env" | "bundled" | "path";
|
|
5
13
|
found: boolean;
|
|
6
14
|
version?: string;
|
|
7
15
|
error?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Runtime warnings parsed from this binary's `--version` stderr (e.g. RE2
|
|
18
|
+
* dlopen failure). Present only when at least one warning was detected.
|
|
19
|
+
*/
|
|
20
|
+
runtimeWarnings?: RuntimeWarning[];
|
|
8
21
|
}
|
|
9
22
|
export type DryRunPlatform = "local" | "github" | "gitlab";
|
|
10
23
|
export interface PlatformContext {
|
|
@@ -36,6 +49,13 @@ export interface SetupStatus {
|
|
|
36
49
|
platformContext: PlatformContext;
|
|
37
50
|
ok: boolean;
|
|
38
51
|
hints: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Deduped union of `runtimeWarnings` from every binary's `--version` probe.
|
|
54
|
+
* Distinct from `hints`: hints are "things you need to install / fix to use
|
|
55
|
+
* this server"; warnings are "Renovate is running but degraded." Empty array
|
|
56
|
+
* when nothing was detected.
|
|
57
|
+
*/
|
|
58
|
+
warnings: RuntimeWarning[];
|
|
39
59
|
}
|
|
40
60
|
export declare function checkSetup(): Promise<SetupStatus>;
|
|
41
61
|
/**
|
|
@@ -56,7 +76,9 @@ export declare function unavailableTools(status: SetupStatus): string[];
|
|
|
56
76
|
/**
|
|
57
77
|
* Concise startup banner appended to the server's MCP `instructions` so the
|
|
58
78
|
* LLM is primed to treat CLI-missing as a partial-availability signal rather
|
|
59
|
-
* than a setup error.
|
|
79
|
+
* than a setup error. Also emits a banner — independent of binary availability
|
|
80
|
+
* — when Renovate runtime warnings are detected (e.g. RE2 dlopen failure
|
|
81
|
+
* causing a slow-path fallback). Returns `null` when nothing needs surfacing.
|
|
60
82
|
*/
|
|
61
83
|
export declare function startupBanner(status: SetupStatus): string | null;
|
|
62
84
|
export declare function describeSetup(status: SetupStatus): string;
|
package/dist/lib/setupCheck.js
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
import { run, resolveRenovateTool } from "./renovateCli.js";
|
|
2
|
+
import { dedupeRuntimeWarnings } from "./runtimeWarnings.js";
|
|
2
3
|
const DRY_RUN_PLATFORMS = ["local", "github", "gitlab"];
|
|
3
4
|
const VERSION_TIMEOUT_MS = 10_000;
|
|
4
|
-
const INSTALL_HINT = "
|
|
5
|
+
const INSTALL_HINT = "Renovate ships bundled with renovate-mcp; if that fails to load, set RENOVATE_BIN / RENOVATE_CONFIG_VALIDATOR_BIN to a working binary or reinstall renovate-mcp.";
|
|
5
6
|
async function checkBinary(tool) {
|
|
6
|
-
const
|
|
7
|
+
const resolved = resolveRenovateTool(tool);
|
|
8
|
+
const command = resolved.command;
|
|
7
9
|
try {
|
|
8
|
-
const result = await run(
|
|
10
|
+
const result = await run(resolved.cmd, [...resolved.prefixArgs, "--version"], {
|
|
11
|
+
timeoutMs: VERSION_TIMEOUT_MS,
|
|
12
|
+
});
|
|
13
|
+
const runtimeWarnings = result.runtimeWarnings.length ? result.runtimeWarnings : undefined;
|
|
9
14
|
if (result.exitCode !== 0) {
|
|
10
15
|
return {
|
|
11
16
|
tool,
|
|
12
17
|
command,
|
|
18
|
+
source: resolved.source,
|
|
13
19
|
found: false,
|
|
14
20
|
error: (result.stderr || result.stdout).trim() || `exit code ${result.exitCode}`,
|
|
21
|
+
runtimeWarnings,
|
|
15
22
|
};
|
|
16
23
|
}
|
|
17
24
|
return {
|
|
18
25
|
tool,
|
|
19
26
|
command,
|
|
27
|
+
source: resolved.source,
|
|
20
28
|
found: true,
|
|
21
29
|
version: result.stdout.trim() || undefined,
|
|
30
|
+
runtimeWarnings,
|
|
22
31
|
};
|
|
23
32
|
}
|
|
24
33
|
catch (err) {
|
|
25
34
|
return {
|
|
26
35
|
tool,
|
|
27
36
|
command,
|
|
37
|
+
source: resolved.source,
|
|
28
38
|
found: false,
|
|
29
39
|
error: err.message,
|
|
30
40
|
};
|
|
@@ -48,6 +58,10 @@ export async function checkSetup() {
|
|
|
48
58
|
if (!renovateConfigValidator.found) {
|
|
49
59
|
hints.push(`renovate-config-validator not reachable at \`${renovateConfigValidator.command}\`. ${INSTALL_HINT}`);
|
|
50
60
|
}
|
|
61
|
+
const warnings = dedupeRuntimeWarnings([
|
|
62
|
+
...(renovate.runtimeWarnings ?? []),
|
|
63
|
+
...(renovateConfigValidator.runtimeWarnings ?? []),
|
|
64
|
+
]);
|
|
51
65
|
return {
|
|
52
66
|
node: process.version,
|
|
53
67
|
renovate,
|
|
@@ -56,6 +70,7 @@ export async function checkSetup() {
|
|
|
56
70
|
platformContext: inspectPlatformContext(process.env),
|
|
57
71
|
ok: renovate.found && renovateConfigValidator.found,
|
|
58
72
|
hints,
|
|
73
|
+
warnings,
|
|
59
74
|
};
|
|
60
75
|
}
|
|
61
76
|
/**
|
|
@@ -139,21 +154,34 @@ export function unavailableTools(status) {
|
|
|
139
154
|
/**
|
|
140
155
|
* Concise startup banner appended to the server's MCP `instructions` so the
|
|
141
156
|
* LLM is primed to treat CLI-missing as a partial-availability signal rather
|
|
142
|
-
* than a setup error.
|
|
157
|
+
* than a setup error. Also emits a banner — independent of binary availability
|
|
158
|
+
* — when Renovate runtime warnings are detected (e.g. RE2 dlopen failure
|
|
159
|
+
* causing a slow-path fallback). Returns `null` when nothing needs surfacing.
|
|
143
160
|
*/
|
|
144
161
|
export function startupBanner(status) {
|
|
145
162
|
const unavailable = unavailableTools(status);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
const sections = [];
|
|
164
|
+
if (unavailable.length > 0) {
|
|
165
|
+
const unavailList = unavailable.map((n) => `\`${n}\``).join(", ");
|
|
166
|
+
const offlineList = OFFLINE_TOOLS.map((n) => `\`${n}\``).join(", ");
|
|
167
|
+
sections.push([
|
|
168
|
+
"Partial availability:",
|
|
169
|
+
` Renovate CLI not reachable — only blocks: ${unavailList}.`,
|
|
170
|
+
` Offline tools (${offlineList}) still work; do not flag this as a setup problem when the task only needs those.`,
|
|
171
|
+
" Renovate ships bundled with renovate-mcp; if you see this banner, the bundled binary failed to spawn or `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` points at a broken binary. Call `check_setup` for a full diagnostic.",
|
|
172
|
+
" Set `RENOVATE_MCP_REQUIRE_CLI=false` to suppress this notice if you have consciously chosen the offline subset.",
|
|
173
|
+
].join("\n"));
|
|
174
|
+
}
|
|
175
|
+
if (status.warnings.length > 0) {
|
|
176
|
+
const lines = ["Renovate runtime warnings:"];
|
|
177
|
+
for (const w of status.warnings) {
|
|
178
|
+
lines.push(` - ${w.message}`);
|
|
179
|
+
lines.push(` Fix: ${w.fix}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push(" These do not block tool calls. Surface this if a user reports unexpectedly slow runs.");
|
|
182
|
+
sections.push(lines.join("\n"));
|
|
183
|
+
}
|
|
184
|
+
return sections.length === 0 ? null : sections.join("\n\n");
|
|
157
185
|
}
|
|
158
186
|
export function describeSetup(status) {
|
|
159
187
|
const lines = [];
|
|
@@ -187,10 +215,27 @@ export function describeSetup(status) {
|
|
|
187
215
|
for (const h of status.hints)
|
|
188
216
|
lines.push(` - ${h}`);
|
|
189
217
|
}
|
|
218
|
+
if (status.warnings.length > 0) {
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push("Warnings:");
|
|
221
|
+
for (const w of status.warnings) {
|
|
222
|
+
lines.push(` - ${w.message}`);
|
|
223
|
+
if (w.detail)
|
|
224
|
+
lines.push(` Detail: ${w.detail}`);
|
|
225
|
+
lines.push(` Fix: ${w.fix}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
190
228
|
return lines.join("\n");
|
|
191
229
|
}
|
|
192
230
|
function formatBinary(s) {
|
|
193
|
-
if (s.found)
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
if (s.found) {
|
|
232
|
+
const version = s.version ?? "(version unknown)";
|
|
233
|
+
if (s.source === "bundled")
|
|
234
|
+
return `${version} (bundled)`;
|
|
235
|
+
const prefix = s.source === "env" ? "env" : "PATH";
|
|
236
|
+
return `${version} (${prefix}: ${s.command})`;
|
|
237
|
+
}
|
|
238
|
+
if (s.source === "bundled")
|
|
239
|
+
return `MISSING — ${s.error ?? "unknown error"} (bundled)`;
|
|
240
|
+
return `MISSING — ${s.error ?? "unknown error"} (${s.command})`;
|
|
196
241
|
}
|
package/dist/lib/version.d.ts
CHANGED
package/dist/lib/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
export const SERVER_VERSION = "0.
|
|
2
|
+
export const SERVER_VERSION = "0.11.0"; // x-release-please-version
|
|
3
3
|
// "released" = the server is running from a `node_modules/` install (global
|
|
4
4
|
// `npm i -g`, project-local `npm i`, or `npx renovate-mcp`). Anything else —
|
|
5
5
|
// most commonly an MCP config of `{"command": "node", "args": [".../dist/index.js"]}`
|
package/dist/tools/dryRun.js
CHANGED
|
@@ -132,7 +132,7 @@ export function registerDryRun(server) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
const reportPath = path.join(tmpdir(), `renovate-mcp-report-${randomUUID()}.json`);
|
|
135
|
-
const
|
|
135
|
+
const tool = resolveRenovateTool("renovate");
|
|
136
136
|
const ruleList = hostRules ?? [];
|
|
137
137
|
// When the caller doesn't pass `platform`, fall back to RENOVATE_PLATFORM
|
|
138
138
|
// from the MCP server's env before defaulting to `local`. Without this,
|
|
@@ -291,7 +291,7 @@ export function registerDryRun(server) {
|
|
|
291
291
|
emit(`Starting Renovate dry-run (${dryRunMode})`);
|
|
292
292
|
heartbeat = setInterval(() => emit(buildDryRunHeartbeatMessage(Date.now() - startedAt, lastLogMsg)), HEARTBEAT_MS);
|
|
293
293
|
}
|
|
294
|
-
const result = await run(
|
|
294
|
+
const result = await run(tool.cmd, [...tool.prefixArgs, ...args], {
|
|
295
295
|
cwd: repoPath,
|
|
296
296
|
env: childEnv,
|
|
297
297
|
timeoutMs,
|
|
@@ -337,6 +337,9 @@ export function registerDryRun(server) {
|
|
|
337
337
|
if (reportErrors.length > 0) {
|
|
338
338
|
summary.reportErrors = reportErrors;
|
|
339
339
|
}
|
|
340
|
+
if (result.runtimeWarnings.length > 0) {
|
|
341
|
+
summary.warnings = result.runtimeWarnings;
|
|
342
|
+
}
|
|
340
343
|
// If no structured report, surface the last bit of stderr so Claude can
|
|
341
344
|
// debug without blowing up the context with Renovate's verbose logs.
|
|
342
345
|
if (!report) {
|
|
@@ -37,19 +37,23 @@ export function registerValidateConfig(server) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
40
|
+
const tool = resolveRenovateTool("renovate-config-validator");
|
|
41
41
|
const args = [];
|
|
42
42
|
if (strict)
|
|
43
43
|
args.push("--strict");
|
|
44
44
|
args.push(target);
|
|
45
|
-
const result = await run(
|
|
45
|
+
const result = await run(tool.cmd, [...tool.prefixArgs, ...args], { timeoutMs: 30_000 });
|
|
46
46
|
const valid = result.exitCode === 0;
|
|
47
47
|
const output = (result.stdout + result.stderr).trim();
|
|
48
|
+
const payload = { valid, output };
|
|
49
|
+
if (result.runtimeWarnings.length > 0) {
|
|
50
|
+
payload.warnings = result.runtimeWarnings;
|
|
51
|
+
}
|
|
48
52
|
return {
|
|
49
53
|
content: [
|
|
50
54
|
{
|
|
51
55
|
type: "text",
|
|
52
|
-
text: JSON.stringify(
|
|
56
|
+
text: JSON.stringify(payload, null, 2),
|
|
53
57
|
},
|
|
54
58
|
],
|
|
55
59
|
isError: !valid,
|
|
@@ -84,46 +84,54 @@ export function registerWriteConfig(server) {
|
|
|
84
84
|
let valid = false;
|
|
85
85
|
let validationOutput = "";
|
|
86
86
|
let validatorMissing = false;
|
|
87
|
+
let runtimeWarnings = [];
|
|
87
88
|
try {
|
|
88
|
-
const
|
|
89
|
-
const v = await run(
|
|
89
|
+
const tool = resolveRenovateTool("renovate-config-validator");
|
|
90
|
+
const v = await run(tool.cmd, [...tool.prefixArgs, tmp], { timeoutMs: 30_000 });
|
|
90
91
|
validationOutput = (v.stdout + v.stderr).trim();
|
|
91
92
|
valid = v.exitCode === 0;
|
|
93
|
+
runtimeWarnings = v.runtimeWarnings;
|
|
92
94
|
}
|
|
93
95
|
catch (err) {
|
|
94
96
|
validatorMissing = true;
|
|
95
97
|
validationOutput = formatMissingBinaryError("renovate-config-validator", err);
|
|
96
98
|
}
|
|
97
99
|
if (!valid && !force) {
|
|
100
|
+
const failPayload = {
|
|
101
|
+
wrote: false,
|
|
102
|
+
reason: validatorMissing ? "validator-unavailable" : "validation-failed",
|
|
103
|
+
validationOutput,
|
|
104
|
+
hint: validatorMissing
|
|
105
|
+
? "The bundled renovate-config-validator failed to spawn. Set RENOVATE_CONFIG_VALIDATOR_BIN to a working binary or reinstall renovate-mcp, then retry. Pass force=true to skip validation entirely."
|
|
106
|
+
: "Pass force=true to write anyway.",
|
|
107
|
+
};
|
|
108
|
+
if (runtimeWarnings.length > 0)
|
|
109
|
+
failPayload.warnings = runtimeWarnings;
|
|
98
110
|
return {
|
|
99
111
|
isError: true,
|
|
100
112
|
content: [
|
|
101
113
|
{
|
|
102
114
|
type: "text",
|
|
103
|
-
text: JSON.stringify(
|
|
104
|
-
wrote: false,
|
|
105
|
-
reason: validatorMissing ? "validator-unavailable" : "validation-failed",
|
|
106
|
-
validationOutput,
|
|
107
|
-
hint: validatorMissing
|
|
108
|
-
? "Install renovate-config-validator (or set RENOVATE_CONFIG_VALIDATOR_BIN), then retry. Pass force=true to skip validation entirely."
|
|
109
|
-
: "Pass force=true to write anyway.",
|
|
110
|
-
}, null, 2),
|
|
115
|
+
text: JSON.stringify(failPayload, null, 2),
|
|
111
116
|
},
|
|
112
117
|
],
|
|
113
118
|
};
|
|
114
119
|
}
|
|
115
120
|
await fs.rename(tmp, target);
|
|
121
|
+
const okPayload = {
|
|
122
|
+
wrote: true,
|
|
123
|
+
path: rel,
|
|
124
|
+
bytes: payload.length,
|
|
125
|
+
valid,
|
|
126
|
+
validationOutput: valid ? undefined : validationOutput,
|
|
127
|
+
};
|
|
128
|
+
if (runtimeWarnings.length > 0)
|
|
129
|
+
okPayload.warnings = runtimeWarnings;
|
|
116
130
|
return {
|
|
117
131
|
content: [
|
|
118
132
|
{
|
|
119
133
|
type: "text",
|
|
120
|
-
text: JSON.stringify(
|
|
121
|
-
wrote: true,
|
|
122
|
-
path: rel,
|
|
123
|
-
bytes: payload.length,
|
|
124
|
-
valid,
|
|
125
|
-
validationOutput: valid ? undefined : validationOutput,
|
|
126
|
-
}, null, 2),
|
|
134
|
+
text: JSON.stringify(okPayload, null, 2),
|
|
127
135
|
},
|
|
128
136
|
],
|
|
129
137
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "renovate-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "MCP server for designing Renovate configurations interactively.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,12 +59,17 @@
|
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
60
60
|
"ignore": "^7.0.5",
|
|
61
61
|
"json5": "^2.2.3",
|
|
62
|
+
"renovate": "^43.139.5",
|
|
62
63
|
"zod": "^4.0.0"
|
|
63
64
|
},
|
|
65
|
+
"overrides": {
|
|
66
|
+
"fast-uri": "^3.1.2",
|
|
67
|
+
"fast-xml-builder": "^1.1.7",
|
|
68
|
+
"simple-git": "^3.36.0"
|
|
69
|
+
},
|
|
64
70
|
"devDependencies": {
|
|
65
71
|
"@types/node": "^24.0.0",
|
|
66
72
|
"@vitest/coverage-v8": "^4.1.0",
|
|
67
|
-
"renovate": "^43.139.5",
|
|
68
73
|
"typescript": "^6.0.0",
|
|
69
74
|
"vitest": "^4.1.0"
|
|
70
75
|
},
|