renovate-mcp 0.6.0 → 0.8.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 +66 -13
- package/dist/data/managers.generated.d.ts +12 -0
- package/dist/data/managers.generated.js +132 -0
- package/dist/data/presets.generated.d.ts +1 -1
- package/dist/data/presets.generated.js +1 -1
- package/dist/index.js +14 -6
- package/dist/lib/configExplainer.d.ts +64 -0
- package/dist/lib/configExplainer.js +132 -0
- package/dist/lib/configLinter.d.ts +1 -1
- package/dist/lib/configLinter.js +76 -0
- package/dist/lib/credentialResolver.d.ts +13 -0
- package/dist/lib/credentialResolver.js +15 -0
- package/dist/lib/customManagerPreview.js +154 -29
- package/dist/lib/dryRunDiff.d.ts +65 -0
- package/dist/lib/dryRunDiff.js +0 -0
- package/dist/lib/externalPresetFetcher.js +1 -8
- package/dist/lib/presetResolver.d.ts +18 -0
- package/dist/lib/presetResolver.js +4 -4
- package/dist/lib/setupCheck.d.ts +36 -0
- package/dist/lib/setupCheck.js +81 -0
- package/dist/lib/version.d.ts +9 -0
- package/dist/lib/version.js +21 -0
- package/dist/tools/dryRun.js +70 -16
- package/dist/tools/dryRunDiff.d.ts +2 -0
- package/dist/tools/dryRunDiff.js +25 -0
- package/dist/tools/explainConfig.d.ts +2 -0
- package/dist/tools/explainConfig.js +83 -0
- package/dist/tools/getVersion.d.ts +2 -0
- package/dist/tools/getVersion.js +18 -0
- package/dist/tools/lintConfig.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ An MCP server for designing [Renovate](https://github.com/renovatebot/renovate)
|
|
|
13
13
|
- [What this is NOT](#what-this-is-not)
|
|
14
14
|
- [Requirements](#requirements)
|
|
15
15
|
- [Install](#install)
|
|
16
|
+
- [Platform setup](#platform-setup)
|
|
16
17
|
- [Example prompts](#example-prompts)
|
|
17
18
|
- [Example session](#example-session)
|
|
18
19
|
- [Development](#development)
|
|
@@ -21,17 +22,20 @@ An MCP server for designing [Renovate](https://github.com/renovatebot/renovate)
|
|
|
21
22
|
|
|
22
23
|
## What it does
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Eleven tools plus a preset reference:
|
|
25
26
|
|
|
26
27
|
| Tool | Purpose |
|
|
27
28
|
| --- | --- |
|
|
28
|
-
| `check_setup` | Report Renovate CLI + validator availability, versions, and install hints. Also runs at startup. |
|
|
29
|
+
| `check_setup` | Report Renovate CLI + validator availability, versions, and install hints. Also surfaces a `platformContext` block (`RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` values, token-presence booleans, the platform `dry_run` would pick when its input is unset, and notes about likely misconfigurations) so callers can verify env before invoking `dry_run`. Token values are never echoed — only presence booleans. Also runs at startup. |
|
|
30
|
+
| `get_version` | Report the renovate-mcp server version and whether it's a released build (running from `node_modules`) or a local/dev build (typically launched via `command: node` against a checkout). |
|
|
29
31
|
| `read_config` | Locate and parse a repo's Renovate config (`renovate.json`, `renovate.json5`, `.renovaterc*`, `package.json#renovate`, …) in priority order. |
|
|
30
32
|
| `resolve_config` | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS with `externalPresets: true`. |
|
|
33
|
+
| `explain_config` | Inverse of `resolve_config`: walk the same preset tree but annotate every leaf field with the chain of presets that touched it. Each leaf is `{ value, setBy }` where `setBy` lists every contribution in merge order — last entry wins for scalars; for arrays each entry adds its own slice. Same offline-by-default behaviour and same `externalPresets` / `endpoint` / `platform` opt-ins as `resolve_config`. |
|
|
31
34
|
| `preview_custom_manager` | Preview a `customManagers` (regex) entry against a local repo — shows file/line hits and extracted dep info. Offline. |
|
|
32
35
|
| `validate_config` | Run `renovate-config-validator` against a file or inline object. |
|
|
33
|
-
| `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses
|
|
34
|
-
| `dry_run` | Run Renovate with `--dry-run` and return the structured JSON report. Defaults to `--platform=local` against `repoPath`; pass `platform` + `endpoint` + `token` + `repository` to run as a real GitHub/GitLab client (needed when the config extends `local>` presets on a private host). No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. |
|
|
36
|
+
| `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses — malformed `/…/` regex patterns plus unknown manager names in `matchManagers` / `excludeManagers`. Offline. |
|
|
37
|
+
| `dry_run` | Run Renovate with `--dry-run` and return the structured JSON report. Defaults to `--platform=local` against `repoPath`; pass `platform` + `endpoint` + `token` + `repository` to run as a real GitHub/GitLab client (needed when the config extends `local>` presets on a private host). When `platform` is not passed, the tool reads `RENOVATE_PLATFORM` from the MCP server's env before defaulting to `local`. When `token` is not passed, falls back to `RENOVATE_TOKEN` from MCP env, then to `GITLAB_TOKEN` (when `platform=gitlab`) or `GITHUB_TOKEN` (when `platform=github`) — auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI, since Renovate itself only reads that one var. `repository` accepts GitHub-style `owner/repo` and GitLab nested-group paths like `group/subgroup/project`. `endpoint`/`token` also flow through in local mode so `gitlab>…` / `github>…` preset shortcuts can be redirected at a self-hosted host without setting up a full remote run. No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. |
|
|
38
|
+
| `dry_run_diff` | Semantic diff between two `dry_run` reports — `added` / `removed` / `changed` proposed updates plus a compact text rendering. Stateless; takes both reports as inputs. Useful when iterating on a config to see exactly what each tweak did. |
|
|
35
39
|
| `write_config` | Validate, then atomically write a config to disk. Refuses to save invalid configs unless `force: true`. |
|
|
36
40
|
| `renovate://presets` (resource) | Markdown index of all 1000+ built-in presets grouped by namespace. |
|
|
37
41
|
| `renovate://presets/{namespace}` (resource template) | Markdown listing for a single namespace (e.g. `renovate://presets/config`) — cheaper than the full index. |
|
|
@@ -43,7 +47,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
|
|
|
43
47
|
|
|
44
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.
|
|
45
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`.
|
|
46
|
-
- **`preview_custom_manager` is a subset of Renovate's regex manager.** It covers `customType: "regex"`, `matchStringsStrategy
|
|
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`.
|
|
47
51
|
- **`validate_config` / `dry_run` aren't exercised in CI.** CI deliberately doesn't install Renovate, so tools that shell out to it are only covered by unit/integration tests that don't require the binary. Run them locally against the Renovate install you intend to deploy with.
|
|
48
52
|
|
|
49
53
|
## Requirements
|
|
@@ -57,7 +61,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
|
|
|
57
61
|
|
|
58
62
|
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override binary locations.
|
|
59
63
|
- `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
|
|
60
|
-
- `
|
|
64
|
+
- 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.
|
|
61
65
|
- 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`.
|
|
62
66
|
|
|
63
67
|
> **Note:** all env vars (tokens, `COMPOSER_AUTH`, `RENOVATE_HOST_RULES`, binary overrides, …) must be set on the MCP server process itself — via the `env` key in `claude_desktop_config.json` / `.mcp.json`, not your shell — since the MCP server runs as a child of the client and does not inherit shell env.
|
|
@@ -106,6 +110,51 @@ Restart your client and prompt it:
|
|
|
106
110
|
|
|
107
111
|
A response listing namespaces like `config`, `docker`, `npm`, `helpers`, … confirms the server is reachable and the resource is exposed. If the client reports the tool or resource as unavailable, re-check the config file path and command.
|
|
108
112
|
|
|
113
|
+
## Platform setup
|
|
114
|
+
|
|
115
|
+
The four common configurations differ in three settings: `RENOVATE_PLATFORM`, `RENOVATE_ENDPOINT`, and which token env var you set. Pick the row that matches your environment and put the values in your client's `mcpServers.renovate.env` block.
|
|
116
|
+
|
|
117
|
+
| Setup | `RENOVATE_PLATFORM` | `RENOVATE_ENDPOINT` | Token env var |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| github.com | `github` | (omit — defaults to `https://api.github.com`) | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
120
|
+
| GitHub Enterprise | `github` | `https://github.example.com/api/v3/` | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
121
|
+
| gitlab.com | `gitlab` | (omit — defaults to `https://gitlab.com/api/v4`) | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
122
|
+
| Self-hosted GitLab | `gitlab` | `https://gitlab.example.com/api/v4/` | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
123
|
+
|
|
124
|
+
Notes that apply to every row:
|
|
125
|
+
|
|
126
|
+
- All env vars must be on the MCP server process — set them via the `env` key in `.mcp.json` / `claude_desktop_config.json`, not your shell, since the MCP server runs as a child of the client and does not inherit shell env.
|
|
127
|
+
- `RENOVATE_TOKEN` wins when both it and the platform-specific var are set. `dry_run` and `resolve_config` honour the same precedence; for `dry_run` the platform-specific var is auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI (Renovate itself only reads that one var).
|
|
128
|
+
- For repository identifiers, GitLab accepts nested-group paths like `group/subgroup/project`, not just `group/project`.
|
|
129
|
+
- If you only intend to use the offline tools (`read_config`, `resolve_config` without `externalPresets`, `preview_custom_manager`, `lint_config`), you can skip all of the above.
|
|
130
|
+
|
|
131
|
+
### Worked example — self-hosted GitLab
|
|
132
|
+
|
|
133
|
+
```jsonc
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"renovate": {
|
|
137
|
+
"command": "npx",
|
|
138
|
+
"args": ["-y", "renovate-mcp"],
|
|
139
|
+
"env": {
|
|
140
|
+
"RENOVATE_PLATFORM": "gitlab",
|
|
141
|
+
"RENOVATE_ENDPOINT": "https://gitlab.example.com/api/v4/",
|
|
142
|
+
"GITLAB_TOKEN": "<your token>"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is enough for both `dry_run` (remote-platform runs) and `resolve_config` (private preset fetches). With this set, `dry_run` against `group/subgroup/project` works without passing `platform` / `endpoint` / `token` per call: `RENOVATE_PLATFORM` and the auto-translated `GITLAB_TOKEN` cover the platform side, and `RENOVATE_ENDPOINT` is inherited naturally by the spawned Renovate CLI. For `resolve_config`, `platform` and `endpoint` are tool *inputs* (not env vars) — pass them when you need `local>` presets routed through your host, since `resolve_config` is in-process and doesn't read `RENOVATE_*` env vars itself.
|
|
150
|
+
|
|
151
|
+
### `local>` presets
|
|
152
|
+
|
|
153
|
+
A config that extends `local>owner/repo:preset` only resolves when there's a platform context to expand it against:
|
|
154
|
+
|
|
155
|
+
- **For `dry_run`** — pass `platform` + `endpoint` + `repository` (token falls back to env). Use `dryRunMode=extract` if you only need manifest extraction; the preset preflight is skipped in that mode.
|
|
156
|
+
- **For `resolve_config`** — pass `platform` + `endpoint` as inputs and the tool rewrites `local>` into `<platform>>` and fetches over HTTPS. Without these inputs, `local>` stays in `presetsUnresolved` with a pointer to the workaround. Run `dry_run` afterwards for full-fidelity merging.
|
|
157
|
+
|
|
109
158
|
## Example prompts
|
|
110
159
|
|
|
111
160
|
Once the server is wired up, try prompts like these. They're written for Claude but work with any MCP-capable client.
|
|
@@ -114,6 +163,7 @@ Once the server is wired up, try prompts like these. They're written for Claude
|
|
|
114
163
|
|
|
115
164
|
- "Read the Renovate config in this repo and summarize what it actually does — expand every preset so I can see the real effective behavior."
|
|
116
165
|
- "Resolve my config and list anything that landed in `presetsUnresolved`, with the reason for each."
|
|
166
|
+
- "Why is my `prCreation` set to `not-pending`? Use `explain_config` to trace which preset set it."
|
|
117
167
|
|
|
118
168
|
**Browsing presets**
|
|
119
169
|
|
|
@@ -121,13 +171,11 @@ Once the server is wired up, try prompts like these. They're written for Claude
|
|
|
121
171
|
- "What does `config:recommended` actually enable? Show me its expanded JSON."
|
|
122
172
|
- "Find a built-in preset that pins GitHub Actions digests."
|
|
123
173
|
|
|
124
|
-
**Self-hosted GitLab / GitHub Enterprise**
|
|
174
|
+
**Self-hosted GitLab / GitHub Enterprise** (env set per [Platform setup](#platform-setup))
|
|
125
175
|
|
|
126
176
|
- "Resolve my config with external presets enabled, fetching `gitlab>platform/renovate-presets` from our self-hosted GitLab at `https://gitlab.example.com/api/v4`. Route `local>` presets through the same host."
|
|
127
177
|
- "Expand `github>acme/renovate-config//base` from our GitHub Enterprise at `https://github.acme.corp/api/v3`."
|
|
128
|
-
- "Dry-run `
|
|
129
|
-
|
|
130
|
-
(Reminder: auth tokens must be set on the MCP server process — via the `env` key in `.mcp.json` / `claude_desktop_config.json`, not your shell.)
|
|
178
|
+
- "Dry-run `infrastructure/kubernetes/our-platform` (a nested-group GitLab project) so Renovate can fetch the `local>` presets the config extends."
|
|
131
179
|
|
|
132
180
|
**Authoring a custom manager (regex)**
|
|
133
181
|
|
|
@@ -177,12 +225,15 @@ npm test # vitest run (builds first)
|
|
|
177
225
|
npm run test:watch # vitest watch mode
|
|
178
226
|
npm run test:coverage # vitest run --coverage (writes coverage/ report)
|
|
179
227
|
npm run generate:presets # regenerate src/data/presets.generated.ts from the renovate devDep
|
|
228
|
+
npm run generate:managers # regenerate src/data/managers.generated.ts from the renovate devDep
|
|
180
229
|
```
|
|
181
230
|
|
|
182
|
-
The preset catalogue at `src/data/presets.generated.ts`
|
|
231
|
+
The preset catalogue at `src/data/presets.generated.ts` and the manager-name list at `src/data/managers.generated.ts` are committed snapshots of Renovate's built-in presets and manager registry. Runtime code never imports the `renovate` package — only the `scripts/generate-*.mjs` scripts do. Regenerate both after bumping the `renovate` devDependency.
|
|
183
232
|
|
|
184
233
|
CI runs `typecheck`, `build`, and `test:coverage` on Node 24 for every PR and push to `main` (see `.github/workflows/ci.yml`). Coverage is uploaded as a per-run artifact; no threshold is enforced yet.
|
|
185
234
|
|
|
235
|
+
`.github/workflows/claude.yml` is maintainer tooling: it lets the repo owner trigger [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) by mentioning `@claude` in an issue, issue comment, PR review, or PR review comment. It's gated on `sender.login == repository_owner`, so mentions from anyone else are ignored. The workflow needs the `CLAUDE_CODE_OAUTH_TOKEN` secret on the repo; outside contributors and forks do not need any Anthropic credentials to work on this project.
|
|
236
|
+
|
|
186
237
|
## Release flow
|
|
187
238
|
|
|
188
239
|
Releases are automated via [release-please](https://github.com/googleapis/release-please) (see `.github/workflows/release.yaml`):
|
|
@@ -202,7 +253,7 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
|
|
|
202
253
|
|
|
203
254
|
- `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).
|
|
204
255
|
- `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.
|
|
205
|
-
- `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. The ruleset is intentionally small (
|
|
256
|
+
- `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.
|
|
206
257
|
- `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.
|
|
207
258
|
- `preview_custom_manager` exposes two separate safety caps so the warning text can name which one tripped: `maxFilesWalked` (default 2000) bounds the directory walk before any `fileMatch` testing, and `maxFilesMatched` (default 500) bounds the result set after `fileMatch` is applied. Previously a single `maxFilesScanned` conflated the two, leaving the user unable to tell whether to narrow `fileMatch` or widen the walk.
|
|
208
259
|
- `preview_custom_manager` runs every user-supplied regex on a `worker_threads` worker with a wall-clock budget per operation (default 2 s, configurable via `matchTimeoutMs`). Catastrophic backtracking — e.g. `^(a+)+b$` against `aaaa…c`, or `(.*)*=` against a modestly sized file — would otherwise pin the MCP server's event loop indefinitely. On timeout the worker is terminated and a warning is appended identifying which `fileMatch[i]` or `matchStrings[i]` was aborted, so the user can simplify the pattern or raise the budget.
|
|
@@ -210,8 +261,10 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
|
|
|
210
261
|
- `resolve_config` expands `extends` against a committed snapshot of Renovate's built-in presets (`src/data/presets.generated.ts`). External `github>` / `gitlab>` fetching is opt-in, uses each platform's contents API with a 10 s timeout, and caches results per call. The `endpoint` input swaps in a custom API base for GHE / self-hosted GitLab; `platform` additionally rewrites `local>` presets to be fetched against that endpoint.
|
|
211
262
|
- `resolve_config` merges preset bodies with a close approximation of Renovate's own `mergeChildConfig` — arrays concat, objects recursively merge, scalars overwrite — not a bit-identical port. Rule-specific semantics for `hostRules`, `regexManagers` / `customManagers`, and certain boolean flags aren't modeled here. Every response carries `mergeQuality: "preview"` plus a human-readable `disclaimer` so callers can't miss the limitation; run `dry_run` for authoritative output.
|
|
212
263
|
- When `resolve_config` encounters template tokens outside its supported subset, it records a structured entry in `warnings`: under-argument cases (`{{arg2}}` referenced when only one arg was passed) substitute an empty string, while non-positional tokens (`{{packageRules}}`, Handlebars helpers like `{{#if …}}`) pass through verbatim.
|
|
264
|
+
- `explain_config` reuses the same expansion machinery as `resolve_config` (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so the two can't drift — if `resolve_config` resolves a preset, `explain_config` does too. The annotated layer threads each preset body's leaves through a parallel `mergeAnnotated` that preserves contributors instead of dropping them, then pins each contribution with the source name (literal `extends` entry, or `<own>` for the user's input config) and a `via` chain naming every parent preset traversed to reach it. Arrays accumulate every contribution in `setBy` and concat their `value`s; scalars list every contribution in merge order with the last as the winner. `merge_quality: "preview"` carries the same disclaimer as `resolve_config` — Renovate's rule-specific semantics for `hostRules`, `regexManagers`, and certain boolean flags aren't modelled.
|
|
213
265
|
- `dry_run` uses `--report-type=file` so we get a structured JSON report instead of scraping stdout. When a `hostRules` input is passed it's written to a mode-0600 temp file in `os.tmpdir()`, handed to Renovate via the `RENOVATE_CONFIG_FILE` env var (the CLI has no `--config-file` flag), and deleted in a `finally` block. Token/password values — including the platform `token` input — are scrubbed from the detected `problems` list and the `logTail` fallback before returning.
|
|
214
|
-
- `dry_run` defaults to `--platform=local` so no host token is required, but that mode can't resolve `local>` presets (they have no platform context to expand against) and silently hides non-default-host GitHub/GitLab setups. Passing `platform` + `endpoint` + `token` + `repository` switches the run to a real platform client so the preset-fetch path works end-to-end; `--dry-run` is still set, so no PRs are opened. As a guard against the silent-failure mode, `dry_run` preflight-checks the repo's config: if it extends any `local>…` preset while the effective platform is `local`, the tool fails fast with specific remediation rather than spawning a Renovate run that would opaquely report `config-validation`.
|
|
266
|
+
- `dry_run` defaults to `--platform=local` so no host token is required, but that mode can't resolve `local>` presets (they have no platform context to expand against) and silently hides non-default-host GitHub/GitLab setups. When the caller doesn't pass a `platform` input, the tool first falls back to `RENOVATE_PLATFORM` from the MCP server's env (if it's one of `local`/`github`/`gitlab`) before defaulting to `local` — without that fallback, the wrapper would unconditionally pass `--platform=local` and silently override an env var the user had set in their `mcp.json`. Passing `platform` + `endpoint` + `token` + `repository` (or relying on the `RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` env vars) switches the run to a real platform client so the preset-fetch path works end-to-end; `--dry-run` is still set, so no PRs are opened. The repository is passed to Renovate as a positional argument because the CLI has no `--repository` flag. `endpoint` and `token` are also forwarded in the default `platform=local` mode so `gitlab>…` / `github>…` preset shortcuts (which are otherwise hardcoded to gitlab.com/github.com) can be redirected at a self-hosted host. As a guard against the silent-failure mode, `dry_run` preflight-checks the repo's config: if it extends any `local>…` preset while the effective platform is `local`, the tool fails fast with specific remediation rather than spawning a Renovate run that would opaquely report `config-validation`. The preflight is skipped for `dryRunMode=extract` so manifest-only extraction can still be attempted.
|
|
215
267
|
- `dry_run` returns a top-level `ok` boolean that is `false` whenever the CLI exited non-zero OR the structured report contains a validation/error-level problem (`level >= 40`, `message === "config-validation"`, or a non-empty `validationError` field). Renovate frequently writes exit-code 0 alongside report-level failures — trusting the exit code alone hides runs that did nothing.
|
|
216
268
|
- `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.
|
|
269
|
+
- `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.
|
|
217
270
|
- `write_config` writes to a temp file, validates, then atomically renames — so a failed validation never leaves a broken config on disk.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const RENOVATE_VERSION = "43.141.5";
|
|
2
|
+
/**
|
|
3
|
+
* All manager names accepted by Renovate's `matchManagers` /
|
|
4
|
+
* `excludeManagers` fields. Includes both regular managers and custom
|
|
5
|
+
* managers (which may also be referenced with a `custom.` prefix).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ALL_MANAGERS: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Custom manager names. Each of these may also be referenced as
|
|
10
|
+
* `custom.<name>` in `matchManagers` / `excludeManagers`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CUSTOM_MANAGERS: readonly string[];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.141.5.
|
|
2
|
+
// Do not edit by hand. Run `npm run generate:managers` to refresh.
|
|
3
|
+
export const RENOVATE_VERSION = "43.141.5";
|
|
4
|
+
/**
|
|
5
|
+
* All manager names accepted by Renovate's `matchManagers` /
|
|
6
|
+
* `excludeManagers` fields. Includes both regular managers and custom
|
|
7
|
+
* managers (which may also be referenced with a `custom.` prefix).
|
|
8
|
+
*/
|
|
9
|
+
export const ALL_MANAGERS = Object.freeze([
|
|
10
|
+
"ansible",
|
|
11
|
+
"ansible-galaxy",
|
|
12
|
+
"ant",
|
|
13
|
+
"argocd",
|
|
14
|
+
"asdf",
|
|
15
|
+
"azure-pipelines",
|
|
16
|
+
"batect",
|
|
17
|
+
"batect-wrapper",
|
|
18
|
+
"bazel",
|
|
19
|
+
"bazel-module",
|
|
20
|
+
"bazelisk",
|
|
21
|
+
"bicep",
|
|
22
|
+
"bitbucket-pipelines",
|
|
23
|
+
"bitrise",
|
|
24
|
+
"buildkite",
|
|
25
|
+
"buildpacks",
|
|
26
|
+
"bun",
|
|
27
|
+
"bun-version",
|
|
28
|
+
"bundler",
|
|
29
|
+
"cake",
|
|
30
|
+
"cargo",
|
|
31
|
+
"cdnurl",
|
|
32
|
+
"circleci",
|
|
33
|
+
"cloudbuild",
|
|
34
|
+
"cocoapods",
|
|
35
|
+
"composer",
|
|
36
|
+
"conan",
|
|
37
|
+
"copier",
|
|
38
|
+
"cpanfile",
|
|
39
|
+
"crossplane",
|
|
40
|
+
"crow",
|
|
41
|
+
"deps-edn",
|
|
42
|
+
"devbox",
|
|
43
|
+
"devcontainer",
|
|
44
|
+
"docker-compose",
|
|
45
|
+
"dockerfile",
|
|
46
|
+
"droneci",
|
|
47
|
+
"fleet",
|
|
48
|
+
"flux",
|
|
49
|
+
"fvm",
|
|
50
|
+
"git-submodules",
|
|
51
|
+
"github-actions",
|
|
52
|
+
"gitlabci",
|
|
53
|
+
"gitlabci-include",
|
|
54
|
+
"glasskube",
|
|
55
|
+
"gleam",
|
|
56
|
+
"gomod",
|
|
57
|
+
"gradle",
|
|
58
|
+
"gradle-wrapper",
|
|
59
|
+
"haskell-cabal",
|
|
60
|
+
"helm-requirements",
|
|
61
|
+
"helm-values",
|
|
62
|
+
"helmfile",
|
|
63
|
+
"helmsman",
|
|
64
|
+
"helmv3",
|
|
65
|
+
"hermit",
|
|
66
|
+
"homeassistant-manifest",
|
|
67
|
+
"homebrew",
|
|
68
|
+
"html",
|
|
69
|
+
"jenkins",
|
|
70
|
+
"jsonata",
|
|
71
|
+
"jsonnet-bundler",
|
|
72
|
+
"kotlin-script",
|
|
73
|
+
"kubernetes",
|
|
74
|
+
"kustomize",
|
|
75
|
+
"leiningen",
|
|
76
|
+
"maven",
|
|
77
|
+
"maven-wrapper",
|
|
78
|
+
"meteor",
|
|
79
|
+
"mint",
|
|
80
|
+
"mise",
|
|
81
|
+
"mix",
|
|
82
|
+
"nix",
|
|
83
|
+
"nodenv",
|
|
84
|
+
"npm",
|
|
85
|
+
"nuget",
|
|
86
|
+
"nvm",
|
|
87
|
+
"ocb",
|
|
88
|
+
"osgi",
|
|
89
|
+
"pep621",
|
|
90
|
+
"pep723",
|
|
91
|
+
"pip-compile",
|
|
92
|
+
"pip_requirements",
|
|
93
|
+
"pip_setup",
|
|
94
|
+
"pipenv",
|
|
95
|
+
"pixi",
|
|
96
|
+
"poetry",
|
|
97
|
+
"pre-commit",
|
|
98
|
+
"pub",
|
|
99
|
+
"puppet",
|
|
100
|
+
"pyenv",
|
|
101
|
+
"quadlet",
|
|
102
|
+
"regex",
|
|
103
|
+
"renovate-config-presets",
|
|
104
|
+
"ruby-version",
|
|
105
|
+
"runtime-version",
|
|
106
|
+
"sbt",
|
|
107
|
+
"scalafmt",
|
|
108
|
+
"setup-cfg",
|
|
109
|
+
"sveltos",
|
|
110
|
+
"swift",
|
|
111
|
+
"tekton",
|
|
112
|
+
"terraform",
|
|
113
|
+
"terraform-version",
|
|
114
|
+
"terragrunt",
|
|
115
|
+
"terragrunt-version",
|
|
116
|
+
"tflint-plugin",
|
|
117
|
+
"travis",
|
|
118
|
+
"typst",
|
|
119
|
+
"unity3d",
|
|
120
|
+
"velaci",
|
|
121
|
+
"vendir",
|
|
122
|
+
"woodpecker",
|
|
123
|
+
"xcodegen"
|
|
124
|
+
]);
|
|
125
|
+
/**
|
|
126
|
+
* Custom manager names. Each of these may also be referenced as
|
|
127
|
+
* `custom.<name>` in `matchManagers` / `excludeManagers`.
|
|
128
|
+
*/
|
|
129
|
+
export const CUSTOM_MANAGERS = Object.freeze([
|
|
130
|
+
"jsonata",
|
|
131
|
+
"regex"
|
|
132
|
+
]);
|
|
@@ -3,6 +3,6 @@ export interface GeneratedPreset {
|
|
|
3
3
|
description: string | null;
|
|
4
4
|
body: Record<string, unknown>;
|
|
5
5
|
}
|
|
6
|
-
export declare const RENOVATE_VERSION = "43.
|
|
6
|
+
export declare const RENOVATE_VERSION = "43.141.5";
|
|
7
7
|
export declare const PRESETS: Record<string, GeneratedPreset>;
|
|
8
8
|
export declare const PRESET_NAMES: readonly string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.139.5.
|
|
2
2
|
// Do not edit by hand. Run `npm run generate:presets` to refresh.
|
|
3
|
-
export const RENOVATE_VERSION = "43.
|
|
3
|
+
export const RENOVATE_VERSION = "43.141.5";
|
|
4
4
|
export const PRESETS = {
|
|
5
5
|
"abandonments:recommended": {
|
|
6
6
|
"namespace": "abandonments",
|
package/dist/index.js
CHANGED
|
@@ -6,22 +6,28 @@ import { registerReadConfig } from "./tools/readConfig.js";
|
|
|
6
6
|
import { registerValidateConfig } from "./tools/validateConfig.js";
|
|
7
7
|
import { registerLintConfig } from "./tools/lintConfig.js";
|
|
8
8
|
import { registerResolveConfig } from "./tools/resolveConfig.js";
|
|
9
|
+
import { registerExplainConfig } from "./tools/explainConfig.js";
|
|
9
10
|
import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
|
|
10
11
|
import { registerDryRun } from "./tools/dryRun.js";
|
|
12
|
+
import { registerDryRunDiff } from "./tools/dryRunDiff.js";
|
|
11
13
|
import { registerWriteConfig } from "./tools/writeConfig.js";
|
|
14
|
+
import { registerGetVersion } from "./tools/getVersion.js";
|
|
12
15
|
import { registerPresetResources } from "./resources/presets.js";
|
|
13
16
|
import { checkSetup, startupBanner } from "./lib/setupCheck.js";
|
|
17
|
+
import { SERVER_VERSION } from "./lib/version.js";
|
|
14
18
|
const BASE_INSTRUCTIONS = [
|
|
15
19
|
"Design and debug Renovate configurations interactively.",
|
|
16
20
|
"",
|
|
17
21
|
"Workflow:",
|
|
18
22
|
" 1. read_config — inspect the current config in a repo",
|
|
19
23
|
" 2. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
|
|
20
|
-
" 3.
|
|
21
|
-
" 4.
|
|
22
|
-
" 5.
|
|
23
|
-
" 6.
|
|
24
|
-
" 7.
|
|
24
|
+
" 3. explain_config — inverse of resolve_config: trace which preset set each field (offline)",
|
|
25
|
+
" 4. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
|
|
26
|
+
" 5. validate_config — check a proposed config against Renovate's schema",
|
|
27
|
+
" 6. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
|
|
28
|
+
" 7. dry_run — preview what Renovate would actually do (no PRs)",
|
|
29
|
+
" 8. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
|
|
30
|
+
" 9. write_config — save the agreed-upon config (validates first)",
|
|
25
31
|
"",
|
|
26
32
|
"If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
|
|
27
33
|
"Built-in preset reference: renovate://presets (namespace index), renovate://presets/{namespace} (one namespace), renovate://preset/{name} (one preset's expanded JSON).",
|
|
@@ -33,16 +39,18 @@ const requireCliRaw = process.env.RENOVATE_MCP_REQUIRE_CLI?.trim().toLowerCase()
|
|
|
33
39
|
const suppressBanner = requireCliRaw === "false" || requireCliRaw === "0";
|
|
34
40
|
const banner = suppressBanner ? null : startupBanner(setup);
|
|
35
41
|
const instructions = banner ? [BASE_INSTRUCTIONS, "", banner].join("\n") : BASE_INSTRUCTIONS;
|
|
36
|
-
const SERVER_VERSION = "0.6.0"; // x-release-please-version
|
|
37
42
|
const server = new McpServer({ name: "renovate-mcp", version: SERVER_VERSION }, { instructions });
|
|
38
43
|
registerCheckSetup(server);
|
|
39
44
|
registerReadConfig(server);
|
|
40
45
|
registerResolveConfig(server);
|
|
46
|
+
registerExplainConfig(server);
|
|
41
47
|
registerPreviewCustomManager(server);
|
|
42
48
|
registerValidateConfig(server);
|
|
43
49
|
registerLintConfig(server);
|
|
44
50
|
registerDryRun(server);
|
|
51
|
+
registerDryRunDiff(server);
|
|
45
52
|
registerWriteConfig(server);
|
|
53
|
+
registerGetVersion(server);
|
|
46
54
|
registerPresetResources(server);
|
|
47
55
|
const transport = new StdioServerTransport();
|
|
48
56
|
await server.connect(transport);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type PresetWarning, type ResolveOptions, type UnresolvedPreset } from "./presetResolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sentinel used as the `source` of contributions that come from the user's own
|
|
4
|
+
* input config (i.e. the keys siblings of `extends`, not from any preset).
|
|
5
|
+
* Distinguishable from any real preset name because angle brackets cannot
|
|
6
|
+
* appear in a Renovate preset reference.
|
|
7
|
+
*/
|
|
8
|
+
export declare const OWN_SOURCE = "<own>";
|
|
9
|
+
export interface Contribution {
|
|
10
|
+
/**
|
|
11
|
+
* Where the value came from. `<own>` for the user's input config; otherwise
|
|
12
|
+
* the preset reference exactly as it appeared in `extends` (e.g.
|
|
13
|
+
* `config:recommended`, `:automergeAll`, `github>acme/renovate-config`).
|
|
14
|
+
*/
|
|
15
|
+
source: string;
|
|
16
|
+
/**
|
|
17
|
+
* Path of presets that brought this contribution in, outermost first. Empty
|
|
18
|
+
* when the value comes from `<own>` or from a top-level entry in the user's
|
|
19
|
+
* `extends`. For nested presets, lists every parent preset between the
|
|
20
|
+
* user's root and `source`.
|
|
21
|
+
*/
|
|
22
|
+
via: string[];
|
|
23
|
+
/**
|
|
24
|
+
* The exact value `source` contributed at the point of the merge — for
|
|
25
|
+
* scalars this is the value `source` set, before any later preset/own
|
|
26
|
+
* override; for arrays this is just the slice `source` added.
|
|
27
|
+
*/
|
|
28
|
+
value: unknown;
|
|
29
|
+
}
|
|
30
|
+
export type AnnotatedNode = AnnotatedLeaf | AnnotatedObject;
|
|
31
|
+
export interface AnnotatedLeaf {
|
|
32
|
+
/**
|
|
33
|
+
* Final merged value of this field. Last `setBy` entry is the winner for
|
|
34
|
+
* scalars; for arrays, every contribution's `value` is concatenated into
|
|
35
|
+
* `value`.
|
|
36
|
+
*/
|
|
37
|
+
value: unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Contributions in merge order. The last entry is the winner for scalars;
|
|
40
|
+
* for arrays each entry adds its own slice to `value`.
|
|
41
|
+
*/
|
|
42
|
+
setBy: Contribution[];
|
|
43
|
+
}
|
|
44
|
+
export type AnnotatedObject = {
|
|
45
|
+
[key: string]: AnnotatedNode;
|
|
46
|
+
};
|
|
47
|
+
export interface ExplainResult {
|
|
48
|
+
explanation: AnnotatedObject;
|
|
49
|
+
presetsResolved: string[];
|
|
50
|
+
presetsUnresolved: UnresolvedPreset[];
|
|
51
|
+
warnings: PresetWarning[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Inverse of `resolveConfig`: walk the same preset tree but, instead of
|
|
55
|
+
* collapsing everything into a flat resolved object, annotate each leaf field
|
|
56
|
+
* with the chain of presets that touched it. The last entry in `setBy` is the
|
|
57
|
+
* winner; `value` is what `resolveConfig` would have returned.
|
|
58
|
+
*
|
|
59
|
+
* Sharing as much as possible with `presetResolver`'s expansion logic
|
|
60
|
+
* (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so
|
|
61
|
+
* the two tools stay consistent — if `resolve_config` resolves a preset,
|
|
62
|
+
* `explain_config` does too, by definition.
|
|
63
|
+
*/
|
|
64
|
+
export declare function explainConfig(config: Record<string, unknown>, options?: ResolveOptions): Promise<ExplainResult>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { applyArgs, loadPresetBody, parsePreset, recordTemplateWarnings, } from "./presetResolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sentinel used as the `source` of contributions that come from the user's own
|
|
4
|
+
* input config (i.e. the keys siblings of `extends`, not from any preset).
|
|
5
|
+
* Distinguishable from any real preset name because angle brackets cannot
|
|
6
|
+
* appear in a Renovate preset reference.
|
|
7
|
+
*/
|
|
8
|
+
export const OWN_SOURCE = "<own>";
|
|
9
|
+
/**
|
|
10
|
+
* Inverse of `resolveConfig`: walk the same preset tree but, instead of
|
|
11
|
+
* collapsing everything into a flat resolved object, annotate each leaf field
|
|
12
|
+
* with the chain of presets that touched it. The last entry in `setBy` is the
|
|
13
|
+
* winner; `value` is what `resolveConfig` would have returned.
|
|
14
|
+
*
|
|
15
|
+
* Sharing as much as possible with `presetResolver`'s expansion logic
|
|
16
|
+
* (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so
|
|
17
|
+
* the two tools stay consistent — if `resolve_config` resolves a preset,
|
|
18
|
+
* `explain_config` does too, by definition.
|
|
19
|
+
*/
|
|
20
|
+
export async function explainConfig(config, options = {}) {
|
|
21
|
+
const presetsResolved = [];
|
|
22
|
+
const presetsUnresolved = [];
|
|
23
|
+
const warnings = [];
|
|
24
|
+
const stack = [];
|
|
25
|
+
const ctx = {
|
|
26
|
+
fetchExternal: options.fetchExternal ?? false,
|
|
27
|
+
timeoutMs: options.timeoutMs,
|
|
28
|
+
endpoint: options.endpoint,
|
|
29
|
+
platform: options.platform,
|
|
30
|
+
cache: new Map(),
|
|
31
|
+
};
|
|
32
|
+
const annotated = await expandAnnotated(config, null, [], presetsResolved, presetsUnresolved, warnings, stack, ctx);
|
|
33
|
+
// The user's input is always a JSON object, so the root annotation is always
|
|
34
|
+
// an object, not a leaf. The cast is safe; falling back to `{}` is just
|
|
35
|
+
// defence in depth.
|
|
36
|
+
const explanation = isAnnotatedLeaf(annotated) ? {} : annotated;
|
|
37
|
+
return { explanation, presetsResolved, presetsUnresolved, warnings };
|
|
38
|
+
}
|
|
39
|
+
async function expandAnnotated(input, ownerName, viaChain, resolvedList, unresolvedList, warningsList, stack, ctx) {
|
|
40
|
+
let accumulated = {};
|
|
41
|
+
const rawExtends = input.extends;
|
|
42
|
+
if (Array.isArray(rawExtends) && rawExtends.length > 0) {
|
|
43
|
+
// Children of `input` are reached by going through `ownerName` (if any).
|
|
44
|
+
// The user's root has no name; for it `viaChain` and `childVia` are equal.
|
|
45
|
+
const childVia = ownerName === null ? viaChain : [...viaChain, ownerName];
|
|
46
|
+
for (const entry of rawExtends) {
|
|
47
|
+
if (typeof entry !== "string") {
|
|
48
|
+
unresolvedList.push({
|
|
49
|
+
preset: String(entry),
|
|
50
|
+
reason: "Preset entry must be a string.",
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const parsed = parsePreset(entry);
|
|
55
|
+
if (stack.includes(parsed.key)) {
|
|
56
|
+
unresolvedList.push({
|
|
57
|
+
preset: entry,
|
|
58
|
+
reason: `Cycle detected: ${[...stack, parsed.key].join(" → ")}`,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const body = await loadPresetBody(parsed, ctx, unresolvedList);
|
|
63
|
+
if (!body)
|
|
64
|
+
continue;
|
|
65
|
+
const { value, missingArgs, unknownTemplates } = applyArgs(body, parsed.args);
|
|
66
|
+
recordTemplateWarnings(entry, parsed.args.length, missingArgs, unknownTemplates, warningsList);
|
|
67
|
+
stack.push(parsed.key);
|
|
68
|
+
const subNode = await expandAnnotated(value, entry, childVia, resolvedList, unresolvedList, warningsList, stack, ctx);
|
|
69
|
+
stack.pop();
|
|
70
|
+
accumulated = mergeAnnotated(accumulated, subNode);
|
|
71
|
+
resolvedList.push(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const { extends: _drop, ...ownKeys } = input;
|
|
75
|
+
const source = ownerName ?? OWN_SOURCE;
|
|
76
|
+
const ownAnnotated = annotateValue(ownKeys, source, viaChain);
|
|
77
|
+
return mergeAnnotated(accumulated, ownAnnotated);
|
|
78
|
+
}
|
|
79
|
+
function annotateValue(value, source, via) {
|
|
80
|
+
if (isPlainObject(value)) {
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const [k, v] of Object.entries(value)) {
|
|
83
|
+
out[k] = annotateValue(v, source, via);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
return { value, setBy: [{ source, via: [...via], value }] };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mirrors `mergeConfig` in `presetResolver.ts`: arrays concat, plain objects
|
|
91
|
+
* recurse, everything else `b` wins. The annotated layer additionally keeps
|
|
92
|
+
* each contribution in `setBy` so callers can see the full chain that produced
|
|
93
|
+
* the final value.
|
|
94
|
+
*/
|
|
95
|
+
function mergeAnnotated(a, b) {
|
|
96
|
+
const aIsLeaf = isAnnotatedLeaf(a);
|
|
97
|
+
const bIsLeaf = isAnnotatedLeaf(b);
|
|
98
|
+
if (!aIsLeaf && !bIsLeaf) {
|
|
99
|
+
const out = { ...a };
|
|
100
|
+
for (const [k, bChild] of Object.entries(b)) {
|
|
101
|
+
const existing = out[k];
|
|
102
|
+
out[k] = existing ? mergeAnnotated(existing, bChild) : bChild;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
if (aIsLeaf && bIsLeaf) {
|
|
107
|
+
if (Array.isArray(a.value) && Array.isArray(b.value)) {
|
|
108
|
+
return {
|
|
109
|
+
value: [...a.value, ...b.value],
|
|
110
|
+
setBy: [...a.setBy, ...b.setBy],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
value: b.value,
|
|
115
|
+
setBy: [...a.setBy, ...b.setBy],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Mixed leaf / object — `b` wins entirely (matches `mergeConfig`'s "b wins
|
|
119
|
+
// unless both plain objects" rule). `a`'s contributions to the discarded
|
|
120
|
+
// shape are dropped, mirroring the resolved config losing them.
|
|
121
|
+
return b;
|
|
122
|
+
}
|
|
123
|
+
function isAnnotatedLeaf(node) {
|
|
124
|
+
if (typeof node !== "object" || node === null)
|
|
125
|
+
return false;
|
|
126
|
+
if (!("value" in node) || !("setBy" in node))
|
|
127
|
+
return false;
|
|
128
|
+
return Array.isArray(node.setBy);
|
|
129
|
+
}
|
|
130
|
+
function isPlainObject(v) {
|
|
131
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
132
|
+
}
|