renovate-mcp 0.1.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/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/data/presets.generated.d.ts +8 -0
- package/dist/data/presets.generated.js +20416 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/lib/configLocations.d.ts +9 -0
- package/dist/lib/configLocations.js +61 -0
- package/dist/lib/customManagerPreview.d.ts +49 -0
- package/dist/lib/customManagerPreview.js +153 -0
- package/dist/lib/externalPresetFetcher.d.ts +15 -0
- package/dist/lib/externalPresetFetcher.js +130 -0
- package/dist/lib/presetResolver.d.ts +41 -0
- package/dist/lib/presetResolver.js +204 -0
- package/dist/lib/renovateCli.d.ts +31 -0
- package/dist/lib/renovateCli.js +75 -0
- package/dist/lib/setupCheck.d.ts +18 -0
- package/dist/lib/setupCheck.js +82 -0
- package/dist/resources/presets.d.ts +2 -0
- package/dist/resources/presets.js +82 -0
- package/dist/tools/checkSetup.d.ts +2 -0
- package/dist/tools/checkSetup.js +21 -0
- package/dist/tools/dryRun.d.ts +2 -0
- package/dist/tools/dryRun.js +88 -0
- package/dist/tools/previewCustomManager.d.ts +2 -0
- package/dist/tools/previewCustomManager.js +85 -0
- package/dist/tools/readConfig.d.ts +2 -0
- package/dist/tools/readConfig.js +35 -0
- package/dist/tools/resolveConfig.d.ts +2 -0
- package/dist/tools/resolveConfig.js +68 -0
- package/dist/tools/validateConfig.d.ts +2 -0
- package/dist/tools/validateConfig.js +78 -0
- package/dist/tools/writeConfig.d.ts +2 -0
- package/dist/tools/writeConfig.js +83 -0
- package/package.json +63 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerCheckSetup } from "./tools/checkSetup.js";
|
|
5
|
+
import { registerReadConfig } from "./tools/readConfig.js";
|
|
6
|
+
import { registerValidateConfig } from "./tools/validateConfig.js";
|
|
7
|
+
import { registerResolveConfig } from "./tools/resolveConfig.js";
|
|
8
|
+
import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
|
|
9
|
+
import { registerDryRun } from "./tools/dryRun.js";
|
|
10
|
+
import { registerWriteConfig } from "./tools/writeConfig.js";
|
|
11
|
+
import { registerPresetResources } from "./resources/presets.js";
|
|
12
|
+
import { checkSetup, describeSetup } from "./lib/setupCheck.js";
|
|
13
|
+
const BASE_INSTRUCTIONS = [
|
|
14
|
+
"Design and debug Renovate configurations interactively.",
|
|
15
|
+
"",
|
|
16
|
+
"Workflow:",
|
|
17
|
+
" 1. read_config — inspect the current config in a repo",
|
|
18
|
+
" 2. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
|
|
19
|
+
" 3. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
|
|
20
|
+
" 4. validate_config — check a proposed config against Renovate's schema",
|
|
21
|
+
" 5. dry_run — preview what Renovate would actually do (no PRs)",
|
|
22
|
+
" 6. write_config — save the agreed-upon config (validates first)",
|
|
23
|
+
"",
|
|
24
|
+
"If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
|
|
25
|
+
"Built-in preset reference is available at renovate://presets.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const setup = await checkSetup();
|
|
28
|
+
const instructions = setup.ok
|
|
29
|
+
? BASE_INSTRUCTIONS
|
|
30
|
+
: [BASE_INSTRUCTIONS, "", "Startup setup check:", describeSetup(setup)].join("\n");
|
|
31
|
+
const SERVER_VERSION = "0.1.0"; // x-release-please-version
|
|
32
|
+
const server = new McpServer({ name: "renovate-mcp", version: SERVER_VERSION }, { instructions });
|
|
33
|
+
registerCheckSetup(server);
|
|
34
|
+
registerReadConfig(server);
|
|
35
|
+
registerResolveConfig(server);
|
|
36
|
+
registerPreviewCustomManager(server);
|
|
37
|
+
registerValidateConfig(server);
|
|
38
|
+
registerDryRun(server);
|
|
39
|
+
registerWriteConfig(server);
|
|
40
|
+
registerPresetResources(server);
|
|
41
|
+
const transport = new StdioServerTransport();
|
|
42
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ConfigFormat = "json" | "json5" | "package.json";
|
|
2
|
+
export interface LocatedConfig {
|
|
3
|
+
absPath: string;
|
|
4
|
+
relPath: string;
|
|
5
|
+
format: ConfigFormat;
|
|
6
|
+
raw: string;
|
|
7
|
+
config: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function locateConfig(repoPath: string): Promise<LocatedConfig | null>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import JSON5 from "json5";
|
|
4
|
+
const CANDIDATES = [
|
|
5
|
+
{ path: "renovate.json", format: "json" },
|
|
6
|
+
{ path: "renovate.json5", format: "json5" },
|
|
7
|
+
{ path: ".github/renovate.json", format: "json" },
|
|
8
|
+
{ path: ".github/renovate.json5", format: "json5" },
|
|
9
|
+
{ path: ".gitlab/renovate.json", format: "json" },
|
|
10
|
+
{ path: ".gitlab/renovate.json5", format: "json5" },
|
|
11
|
+
{ path: ".renovaterc", format: "json" },
|
|
12
|
+
{ path: ".renovaterc.json", format: "json" },
|
|
13
|
+
{ path: ".renovaterc.json5", format: "json5" },
|
|
14
|
+
];
|
|
15
|
+
export async function locateConfig(repoPath) {
|
|
16
|
+
for (const candidate of CANDIDATES) {
|
|
17
|
+
const abs = path.join(repoPath, candidate.path);
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(abs, "utf8");
|
|
20
|
+
const config = candidate.format === "json5" ? JSON5.parse(raw) : JSON.parse(raw);
|
|
21
|
+
if (!isPlainObject(config)) {
|
|
22
|
+
throw new Error(`${candidate.path} does not contain a JSON object`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
absPath: abs,
|
|
26
|
+
relPath: candidate.path,
|
|
27
|
+
format: candidate.format,
|
|
28
|
+
raw,
|
|
29
|
+
config,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code !== "ENOENT")
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pkgAbs = path.join(repoPath, "package.json");
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.readFile(pkgAbs, "utf8");
|
|
40
|
+
const pkg = JSON.parse(raw);
|
|
41
|
+
if (isPlainObject(pkg) &&
|
|
42
|
+
"renovate" in pkg &&
|
|
43
|
+
isPlainObject(pkg.renovate)) {
|
|
44
|
+
return {
|
|
45
|
+
absPath: pkgAbs,
|
|
46
|
+
relPath: "package.json",
|
|
47
|
+
format: "package.json",
|
|
48
|
+
raw,
|
|
49
|
+
config: pkg.renovate,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err.code !== "ENOENT")
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function isPlainObject(v) {
|
|
60
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface CustomManager {
|
|
2
|
+
customType: string;
|
|
3
|
+
fileMatch: string[];
|
|
4
|
+
matchStrings: string[];
|
|
5
|
+
matchStringsStrategy?: string;
|
|
6
|
+
depNameTemplate?: string;
|
|
7
|
+
packageNameTemplate?: string;
|
|
8
|
+
currentValueTemplate?: string;
|
|
9
|
+
currentDigestTemplate?: string;
|
|
10
|
+
datasourceTemplate?: string;
|
|
11
|
+
versioningTemplate?: string;
|
|
12
|
+
registryUrlTemplate?: string;
|
|
13
|
+
depTypeTemplate?: string;
|
|
14
|
+
extractVersionTemplate?: string;
|
|
15
|
+
autoReplaceStringTemplate?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PreviewHit {
|
|
18
|
+
file: string;
|
|
19
|
+
matchStringIndex: number;
|
|
20
|
+
line: number;
|
|
21
|
+
match: string;
|
|
22
|
+
groups: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export interface ExtractedDep {
|
|
25
|
+
file: string;
|
|
26
|
+
line: number;
|
|
27
|
+
depName?: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
currentValue?: string;
|
|
30
|
+
currentDigest?: string;
|
|
31
|
+
datasource?: string;
|
|
32
|
+
versioning?: string;
|
|
33
|
+
registryUrl?: string;
|
|
34
|
+
depType?: string;
|
|
35
|
+
extractVersion?: string;
|
|
36
|
+
autoReplaceString?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface PreviewOptions {
|
|
39
|
+
maxFilesScanned?: number;
|
|
40
|
+
maxHitsPerFile?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface PreviewResult {
|
|
43
|
+
filesScanned: number;
|
|
44
|
+
filesMatched: string[];
|
|
45
|
+
hits: PreviewHit[];
|
|
46
|
+
extractedDeps: ExtractedDep[];
|
|
47
|
+
warnings: string[];
|
|
48
|
+
}
|
|
49
|
+
export declare function previewCustomManager(repoPath: string, manager: CustomManager, options?: PreviewOptions): Promise<PreviewResult>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_MAX_FILES_SCANNED = 2000;
|
|
4
|
+
const DEFAULT_MAX_HITS_PER_FILE = 100;
|
|
5
|
+
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
6
|
+
const TEMPLATE_FIELD_MAP = [
|
|
7
|
+
["depNameTemplate", "depName"],
|
|
8
|
+
["packageNameTemplate", "packageName"],
|
|
9
|
+
["currentValueTemplate", "currentValue"],
|
|
10
|
+
["currentDigestTemplate", "currentDigest"],
|
|
11
|
+
["datasourceTemplate", "datasource"],
|
|
12
|
+
["versioningTemplate", "versioning"],
|
|
13
|
+
["registryUrlTemplate", "registryUrl"],
|
|
14
|
+
["depTypeTemplate", "depType"],
|
|
15
|
+
["extractVersionTemplate", "extractVersion"],
|
|
16
|
+
["autoReplaceStringTemplate", "autoReplaceString"],
|
|
17
|
+
];
|
|
18
|
+
export async function previewCustomManager(repoPath, manager, options = {}) {
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const maxFilesScanned = options.maxFilesScanned ?? DEFAULT_MAX_FILES_SCANNED;
|
|
21
|
+
const maxHitsPerFile = options.maxHitsPerFile ?? DEFAULT_MAX_HITS_PER_FILE;
|
|
22
|
+
if (manager.matchStringsStrategy && manager.matchStringsStrategy !== "any") {
|
|
23
|
+
warnings.push(`matchStringsStrategy='${manager.matchStringsStrategy}' is not yet supported by this preview tool; treating as 'any'. Run dry_run for full-fidelity behavior.`);
|
|
24
|
+
}
|
|
25
|
+
const fileMatchRes = manager.fileMatch.map((s) => compileRegex(s));
|
|
26
|
+
const matchStringRes = manager.matchStrings.map((s) => compileRegex(s, "gm"));
|
|
27
|
+
const filesMatched = [];
|
|
28
|
+
let filesScanned = 0;
|
|
29
|
+
for await (const rel of walk(repoPath)) {
|
|
30
|
+
if (filesScanned >= maxFilesScanned) {
|
|
31
|
+
warnings.push(`Stopped after scanning ${maxFilesScanned} files. Increase maxFilesScanned to widen the search.`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
filesScanned++;
|
|
35
|
+
if (fileMatchRes.some((re) => re.test(rel))) {
|
|
36
|
+
filesMatched.push(rel);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const hits = [];
|
|
40
|
+
const extractedDeps = [];
|
|
41
|
+
for (const rel of filesMatched) {
|
|
42
|
+
const abs = path.join(repoPath, rel);
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = await fs.readFile(abs, "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
warnings.push(`Could not read ${rel}: ${err.message}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
let perFileHits = 0;
|
|
52
|
+
for (let i = 0; i < matchStringRes.length; i++) {
|
|
53
|
+
const re = matchStringRes[i];
|
|
54
|
+
// RegExp objects with the `g` flag are stateful — reset before iterating
|
|
55
|
+
// each file so that previous calls don't shift lastIndex.
|
|
56
|
+
re.lastIndex = 0;
|
|
57
|
+
for (const m of content.matchAll(re)) {
|
|
58
|
+
if (perFileHits >= maxHitsPerFile) {
|
|
59
|
+
warnings.push(`${rel}: capped at ${maxHitsPerFile} hits. Increase maxHitsPerFile to see more.`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const groups = (m.groups ?? {});
|
|
63
|
+
const line = lineNumberAt(content, m.index ?? 0);
|
|
64
|
+
const matchStr = m[0];
|
|
65
|
+
hits.push({ file: rel, matchStringIndex: i, line, match: matchStr, groups });
|
|
66
|
+
extractedDeps.push(buildExtractedDep(rel, line, groups, manager));
|
|
67
|
+
perFileHits++;
|
|
68
|
+
}
|
|
69
|
+
if (perFileHits >= maxHitsPerFile)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { filesScanned, filesMatched, hits, extractedDeps, warnings };
|
|
74
|
+
}
|
|
75
|
+
function buildExtractedDep(file, line, groups, manager) {
|
|
76
|
+
const dep = { file, line };
|
|
77
|
+
// Named capture groups are the baseline — Renovate populates dep fields
|
|
78
|
+
// directly from groups whose names match dep keys.
|
|
79
|
+
for (const [, depKey] of TEMPLATE_FIELD_MAP) {
|
|
80
|
+
if (depKey in groups) {
|
|
81
|
+
const v = groups[depKey];
|
|
82
|
+
if (v !== undefined)
|
|
83
|
+
dep[depKey] = v;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Templates override matching named groups.
|
|
87
|
+
for (const [tmplKey, depKey] of TEMPLATE_FIELD_MAP) {
|
|
88
|
+
const tmpl = manager[tmplKey];
|
|
89
|
+
if (typeof tmpl === "string") {
|
|
90
|
+
dep[depKey] = applyTemplate(tmpl, groups);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return dep;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Simple `{{var}}` substitution. Renovate uses full Handlebars; we cover the
|
|
97
|
+
* common case where templates reference named capture groups directly. Helpers
|
|
98
|
+
* like `{{#if}}` or `{{lookup}}` are not implemented — those degrade to a
|
|
99
|
+
* literal placeholder, which the user will notice in the preview.
|
|
100
|
+
*/
|
|
101
|
+
function applyTemplate(tmpl, groups) {
|
|
102
|
+
return tmpl.replace(/\{\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}\}/g, (_, name) => groups[name] ?? "");
|
|
103
|
+
}
|
|
104
|
+
function compileRegex(source, extraFlags = "") {
|
|
105
|
+
try {
|
|
106
|
+
// De-duplicate flags in case the source already had `g`/`m`.
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const flags = (extraFlags).split("").filter((c) => {
|
|
109
|
+
if (seen.has(c))
|
|
110
|
+
return false;
|
|
111
|
+
seen.add(c);
|
|
112
|
+
return true;
|
|
113
|
+
}).join("");
|
|
114
|
+
return new RegExp(source, flags);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw new Error(`Invalid regex /${source}/: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function lineNumberAt(content, index) {
|
|
121
|
+
let line = 1;
|
|
122
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
123
|
+
if (content.charCodeAt(i) === 10 /* \n */)
|
|
124
|
+
line++;
|
|
125
|
+
}
|
|
126
|
+
return line;
|
|
127
|
+
}
|
|
128
|
+
async function* walk(root) {
|
|
129
|
+
const stack = [""];
|
|
130
|
+
while (stack.length > 0) {
|
|
131
|
+
const relDir = stack.pop();
|
|
132
|
+
const absDir = path.join(root, relDir);
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
if (SKIP_DIRS.has(entry.name))
|
|
143
|
+
continue;
|
|
144
|
+
stack.push(path.join(relDir, entry.name));
|
|
145
|
+
}
|
|
146
|
+
else if (entry.isFile()) {
|
|
147
|
+
// Always emit POSIX-style paths so users on macOS/Linux/Windows write
|
|
148
|
+
// the same fileMatch regexes.
|
|
149
|
+
yield path.join(relDir, entry.name).split(path.sep).join("/");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ParsedPreset } from "./presetResolver.js";
|
|
2
|
+
export interface FetchOptions {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
cache?: Map<string, Promise<FetchResult>>;
|
|
5
|
+
/** Injectable for tests. Defaults to globalThis.fetch. */
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export type FetchResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
body: Record<string, unknown>;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function fetchExternalPreset(parsed: ParsedPreset, options?: FetchOptions): Promise<FetchResult>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
2
|
+
export async function fetchExternalPreset(parsed, options = {}) {
|
|
3
|
+
const cache = options.cache;
|
|
4
|
+
const cacheKey = parsed.key;
|
|
5
|
+
if (cache?.has(cacheKey)) {
|
|
6
|
+
return cache.get(cacheKey);
|
|
7
|
+
}
|
|
8
|
+
const promise = dispatch(parsed, options);
|
|
9
|
+
cache?.set(cacheKey, promise);
|
|
10
|
+
return promise;
|
|
11
|
+
}
|
|
12
|
+
function dispatch(parsed, options) {
|
|
13
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
15
|
+
switch (parsed.source) {
|
|
16
|
+
case "github":
|
|
17
|
+
return fetchGitHub(parsed, timeoutMs, fetchImpl);
|
|
18
|
+
case "gitlab":
|
|
19
|
+
return fetchGitLab(parsed, timeoutMs, fetchImpl);
|
|
20
|
+
case "bitbucket":
|
|
21
|
+
case "gitea":
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
ok: false,
|
|
24
|
+
reason: `${parsed.source}> presets are not yet supported. Track progress in issue #10.`,
|
|
25
|
+
});
|
|
26
|
+
case "local":
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: "local> presets require platform/repo context that resolve_config does not have; out of scope.",
|
|
30
|
+
});
|
|
31
|
+
case "npm":
|
|
32
|
+
return Promise.resolve({
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: "npm-hosted presets are not yet supported. Host the preset on GitHub or GitLab, or track progress in issue #10.",
|
|
35
|
+
});
|
|
36
|
+
default:
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: `Unknown preset source: ${parsed.source ?? "(none)"}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function fetchGitHub(parsed, timeoutMs, fetchImpl) {
|
|
44
|
+
if (!parsed.repoPath || !parsed.repoPath.includes("/")) {
|
|
45
|
+
return { ok: false, reason: `Invalid github preset: ${parsed.original}` };
|
|
46
|
+
}
|
|
47
|
+
const file = presetFileName(parsed);
|
|
48
|
+
const ref = parsed.ref ?? "HEAD";
|
|
49
|
+
const url = `https://api.github.com/repos/${parsed.repoPath}/contents/${encodeFilePath(file)}?ref=${encodeURIComponent(ref)}`;
|
|
50
|
+
const token = process.env.GITHUB_TOKEN || process.env.RENOVATE_TOKEN;
|
|
51
|
+
const headers = {
|
|
52
|
+
Accept: "application/vnd.github.raw",
|
|
53
|
+
"User-Agent": "renovate-mcp",
|
|
54
|
+
};
|
|
55
|
+
if (token)
|
|
56
|
+
headers.Authorization = `Bearer ${token}`;
|
|
57
|
+
return fetchJson(url, headers, timeoutMs, parsed.original, fetchImpl);
|
|
58
|
+
}
|
|
59
|
+
async function fetchGitLab(parsed, timeoutMs, fetchImpl) {
|
|
60
|
+
if (!parsed.repoPath) {
|
|
61
|
+
return { ok: false, reason: `Invalid gitlab preset: ${parsed.original}` };
|
|
62
|
+
}
|
|
63
|
+
const file = presetFileName(parsed);
|
|
64
|
+
const ref = parsed.ref ?? "HEAD";
|
|
65
|
+
const url = `https://gitlab.com/api/v4/projects/${encodeURIComponent(parsed.repoPath)}/repository/files/${encodeURIComponent(file)}/raw?ref=${encodeURIComponent(ref)}`;
|
|
66
|
+
const token = process.env.GITLAB_TOKEN || process.env.RENOVATE_TOKEN;
|
|
67
|
+
const headers = { "User-Agent": "renovate-mcp" };
|
|
68
|
+
if (token)
|
|
69
|
+
headers["PRIVATE-TOKEN"] = token;
|
|
70
|
+
return fetchJson(url, headers, timeoutMs, parsed.original, fetchImpl);
|
|
71
|
+
}
|
|
72
|
+
function presetFileName(parsed) {
|
|
73
|
+
if (parsed.subpath)
|
|
74
|
+
return ensureJson(parsed.subpath);
|
|
75
|
+
if (parsed.presetName)
|
|
76
|
+
return ensureJson(parsed.presetName);
|
|
77
|
+
return "default.json";
|
|
78
|
+
}
|
|
79
|
+
function ensureJson(name) {
|
|
80
|
+
return /\.(json5?|jsonc)$/i.test(name) ? name : `${name}.json`;
|
|
81
|
+
}
|
|
82
|
+
function encodeFilePath(file) {
|
|
83
|
+
return file.split("/").map(encodeURIComponent).join("/");
|
|
84
|
+
}
|
|
85
|
+
async function fetchJson(url, headers, timeoutMs, presetName, fetchImpl) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetchImpl(url, { headers, signal: controller.signal });
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""} when fetching ${presetName}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const text = await res.text();
|
|
97
|
+
try {
|
|
98
|
+
const body = JSON.parse(text);
|
|
99
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: `Preset body for ${presetName} is not a JSON object.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, body: body };
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: `Preset body for ${presetName} is not valid JSON: ${e.message}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
const err = e;
|
|
116
|
+
if (err.name === "AbortError") {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: `Timed out after ${timeoutMs}ms fetching ${presetName}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `Network error fetching ${presetName}: ${err.message}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface UnresolvedPreset {
|
|
2
|
+
preset: string;
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ResolveResult {
|
|
6
|
+
resolved: Record<string, unknown>;
|
|
7
|
+
presetsResolved: string[];
|
|
8
|
+
presetsUnresolved: UnresolvedPreset[];
|
|
9
|
+
}
|
|
10
|
+
export interface ResolveOptions {
|
|
11
|
+
/** When true, fetch external presets (github>, gitlab>, …) over the network. */
|
|
12
|
+
fetchExternal?: boolean;
|
|
13
|
+
/** Per-request fetch timeout; forwarded to the external fetcher. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export type ExternalSource = "github" | "gitlab" | "bitbucket" | "gitea" | "local" | "npm";
|
|
17
|
+
export interface ParsedPreset {
|
|
18
|
+
/** Canonical identifier used as map key and for cycle detection. */
|
|
19
|
+
key: string;
|
|
20
|
+
/** Original un-normalized string as it appeared in `extends`. */
|
|
21
|
+
original: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
/** For external (non-builtin) presets. */
|
|
24
|
+
source?: ExternalSource;
|
|
25
|
+
/** Git sources: "owner/repo". npm: the package name. */
|
|
26
|
+
repoPath?: string;
|
|
27
|
+
/** Optional `:presetName` fragment. */
|
|
28
|
+
presetName?: string;
|
|
29
|
+
/** Optional `//subpath` fragment. */
|
|
30
|
+
subpath?: string;
|
|
31
|
+
/** Optional `#ref` (branch/tag/commit) fragment. */
|
|
32
|
+
ref?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a Renovate config by expanding every preset in `extends`. Built-in
|
|
36
|
+
* presets resolve offline against the committed catalogue; external presets
|
|
37
|
+
* (`github>`, `gitlab>`, …) are only fetched when `fetchExternal` is true —
|
|
38
|
+
* otherwise they land in `presetsUnresolved` with a network reason.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveConfig(config: Record<string, unknown>, options?: ResolveOptions): Promise<ResolveResult>;
|
|
41
|
+
export declare function parsePreset(raw: string): ParsedPreset;
|