🛠️ Update various documentation, scripts, and configuration templates to enhance clarity, functionality, and maintainability across the project
This commit is contained in:
823
install/merge-managed-mcp-config.mjs
Normal file
823
install/merge-managed-mcp-config.mjs
Normal file
@@ -0,0 +1,823 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
"Usage: node install/merge-managed-mcp-config.mjs --target <config.json> --template <template.jsonc> --server-key <servers|mcpServers> [--overrides <mcp.local.jsonc>] [--set NAME=value]",
|
||||
);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
replacements: {},
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
|
||||
if (arg === "--target") {
|
||||
options.target = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--template") {
|
||||
options.template = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--server-key") {
|
||||
options.serverKey = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--overrides") {
|
||||
options.overrides = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--set") {
|
||||
const assignment = argv[index + 1] ?? "";
|
||||
const equalsIndex = assignment.indexOf("=");
|
||||
if (equalsIndex <= 0) {
|
||||
throw new Error(`Invalid --set assignment: ${assignment}`);
|
||||
}
|
||||
const key = assignment.slice(0, equalsIndex);
|
||||
const value = assignment.slice(equalsIndex + 1);
|
||||
options.replacements[key] = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
if (!options.target || !options.template || !options.serverKey) {
|
||||
usage();
|
||||
throw new Error(
|
||||
"--target, --template, and --server-key are all required.",
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripJsonComments(input) {
|
||||
let output = "";
|
||||
let inString = false;
|
||||
let escaping = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
const char = input[index];
|
||||
const next = input[index + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (char === "\n") {
|
||||
inLineComment = false;
|
||||
output += char;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
index += 1;
|
||||
} else if (char === "\n" || char === "\r") {
|
||||
output += char;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
output += char;
|
||||
if (escaping) {
|
||||
escaping = false;
|
||||
} else if (char === "\\") {
|
||||
escaping = true;
|
||||
} else if (char === '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = true;
|
||||
output += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
output += char;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function stripTrailingCommas(input) {
|
||||
let output = "";
|
||||
let inString = false;
|
||||
let escaping = false;
|
||||
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
const char = input[index];
|
||||
|
||||
if (inString) {
|
||||
output += char;
|
||||
if (escaping) {
|
||||
escaping = false;
|
||||
} else if (char === "\\") {
|
||||
escaping = true;
|
||||
} else if (char === '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = true;
|
||||
output += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ",") {
|
||||
let lookahead = index + 1;
|
||||
while (lookahead < input.length && /\s/.test(input[lookahead])) {
|
||||
lookahead += 1;
|
||||
}
|
||||
if (input[lookahead] === "}" || input[lookahead] === "]") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
output += char;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function renderTemplate(input, replacements) {
|
||||
return input.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, key) => {
|
||||
if (!(key in replacements)) {
|
||||
return match;
|
||||
}
|
||||
return replacements[key].replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
});
|
||||
}
|
||||
|
||||
function parseJsonc(input, label) {
|
||||
const sanitized = stripTrailingCommas(stripJsonComments(input)).trim();
|
||||
if (!sanitized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(sanitized);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse ${label}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!isPlainObject(parsed)) {
|
||||
throw new Error(`${label} must contain a JSON object at the root.`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseJsoncAst(input, label) {
|
||||
let index = 0;
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`Failed to parse ${label}: ${message}`);
|
||||
}
|
||||
|
||||
function skipTrivia() {
|
||||
while (index < input.length) {
|
||||
const char = input[index];
|
||||
const next = input[index + 1];
|
||||
|
||||
if (/\s/.test(char)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
index += 2;
|
||||
while (index < input.length && input[index] !== "\n") {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "*") {
|
||||
index += 2;
|
||||
while (
|
||||
index < input.length &&
|
||||
!(input[index] === "*" && input[index + 1] === "/")
|
||||
) {
|
||||
index += 1;
|
||||
}
|
||||
if (index >= input.length) {
|
||||
fail("unterminated block comment");
|
||||
}
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function parseStringNode() {
|
||||
const start = index;
|
||||
index += 1;
|
||||
|
||||
while (index < input.length) {
|
||||
const char = input[index];
|
||||
|
||||
if (char === "\\") {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
index += 1;
|
||||
const raw = input.slice(start, index);
|
||||
return {
|
||||
type: "string",
|
||||
start,
|
||||
end: index,
|
||||
value: JSON.parse(raw),
|
||||
};
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
fail("unterminated string literal");
|
||||
}
|
||||
|
||||
function parseLiteralNode(expectedText, value) {
|
||||
const start = index;
|
||||
if (!input.startsWith(expectedText, index)) {
|
||||
fail(`expected ${expectedText}`);
|
||||
}
|
||||
index += expectedText.length;
|
||||
return {
|
||||
type: typeof value,
|
||||
start,
|
||||
end: index,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function parseNumberNode() {
|
||||
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(
|
||||
input.slice(index),
|
||||
);
|
||||
if (!match) {
|
||||
fail(`invalid number at offset ${index}`);
|
||||
}
|
||||
|
||||
const start = index;
|
||||
index += match[0].length;
|
||||
return {
|
||||
type: "number",
|
||||
start,
|
||||
end: index,
|
||||
value: Number(match[0]),
|
||||
};
|
||||
}
|
||||
|
||||
function parseArrayNode() {
|
||||
const start = index;
|
||||
index += 1;
|
||||
const elements = [];
|
||||
|
||||
skipTrivia();
|
||||
while (index < input.length && input[index] !== "]") {
|
||||
const valueNode = parseValueNode();
|
||||
elements.push(valueNode);
|
||||
skipTrivia();
|
||||
|
||||
if (input[index] === ",") {
|
||||
index += 1;
|
||||
skipTrivia();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (input[index] !== "]") {
|
||||
fail(`expected ',' or ']' at offset ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (input[index] !== "]") {
|
||||
fail("unterminated array");
|
||||
}
|
||||
|
||||
index += 1;
|
||||
return {
|
||||
type: "array",
|
||||
start,
|
||||
end: index,
|
||||
elements,
|
||||
value: elements.map((element) => element.value),
|
||||
};
|
||||
}
|
||||
|
||||
function parseObjectNode() {
|
||||
const start = index;
|
||||
index += 1;
|
||||
const properties = [];
|
||||
const value = {};
|
||||
|
||||
skipTrivia();
|
||||
while (index < input.length && input[index] !== "}") {
|
||||
if (input[index] !== '"') {
|
||||
fail(`expected string property name at offset ${index}`);
|
||||
}
|
||||
|
||||
const keyNode = parseStringNode();
|
||||
const key = keyNode.value;
|
||||
skipTrivia();
|
||||
|
||||
if (input[index] !== ":") {
|
||||
fail(`expected ':' after property name at offset ${index}`);
|
||||
}
|
||||
|
||||
index += 1;
|
||||
skipTrivia();
|
||||
|
||||
const valueNode = parseValueNode();
|
||||
const property = {
|
||||
key,
|
||||
keyNode,
|
||||
value: valueNode,
|
||||
hasTrailingComma: false,
|
||||
};
|
||||
|
||||
properties.push(property);
|
||||
value[key] = valueNode.value;
|
||||
|
||||
skipTrivia();
|
||||
if (input[index] === ",") {
|
||||
property.hasTrailingComma = true;
|
||||
index += 1;
|
||||
skipTrivia();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (input[index] !== "}") {
|
||||
fail(`expected ',' or '}' at offset ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (input[index] !== "}") {
|
||||
fail("unterminated object");
|
||||
}
|
||||
|
||||
index += 1;
|
||||
return {
|
||||
type: "object",
|
||||
start,
|
||||
end: index,
|
||||
properties,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function parseValueNode() {
|
||||
skipTrivia();
|
||||
|
||||
const char = input[index];
|
||||
if (char === "{") {
|
||||
return parseObjectNode();
|
||||
}
|
||||
if (char === "[") {
|
||||
return parseArrayNode();
|
||||
}
|
||||
if (char === '"') {
|
||||
return parseStringNode();
|
||||
}
|
||||
if (char === "t") {
|
||||
return parseLiteralNode("true", true);
|
||||
}
|
||||
if (char === "f") {
|
||||
return parseLiteralNode("false", false);
|
||||
}
|
||||
if (char === "n") {
|
||||
return parseLiteralNode("null", null);
|
||||
}
|
||||
if (char === "-" || /\d/.test(char ?? "")) {
|
||||
return parseNumberNode();
|
||||
}
|
||||
|
||||
fail(`unexpected token at offset ${index}`);
|
||||
}
|
||||
|
||||
skipTrivia();
|
||||
const root = parseValueNode();
|
||||
skipTrivia();
|
||||
|
||||
if (index !== input.length) {
|
||||
fail(`unexpected trailing content at offset ${index}`);
|
||||
}
|
||||
|
||||
if (root.type !== "object") {
|
||||
throw new Error(`${label} must contain a JSON object at the root.`);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function detectIndentUnit(input) {
|
||||
const matches = input.match(/^( +|\t+)\S/m);
|
||||
if (!matches) {
|
||||
return " ";
|
||||
}
|
||||
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
function detectEol(input) {
|
||||
return input.includes("\r\n") ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
function lineStartIndex(input, index) {
|
||||
const newlineIndex = input.lastIndexOf("\n", index - 1);
|
||||
return newlineIndex === -1 ? 0 : newlineIndex + 1;
|
||||
}
|
||||
|
||||
function lineIndentAt(input, index) {
|
||||
const start = lineStartIndex(input, index);
|
||||
let end = start;
|
||||
while (end < input.length && (input[end] === " " || input[end] === "\t")) {
|
||||
end += 1;
|
||||
}
|
||||
return input.slice(start, end);
|
||||
}
|
||||
|
||||
function renderValue(value, propertyIndent, indentUnit, eol) {
|
||||
const raw = JSON.stringify(value, null, indentUnit);
|
||||
if (!raw.includes("\n")) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return raw
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : `${propertyIndent}${line}`))
|
||||
.join(eol);
|
||||
}
|
||||
|
||||
function renderProperty(key, value, propertyIndent, indentUnit, eol) {
|
||||
return `${JSON.stringify(key)}: ${renderValue(value, propertyIndent, indentUnit, eol)}`;
|
||||
}
|
||||
|
||||
function getObjectChildIndent(input, objectNode, indentUnit) {
|
||||
if (objectNode.properties.length > 0) {
|
||||
return lineIndentAt(input, objectNode.properties[0].keyNode.start);
|
||||
}
|
||||
|
||||
return `${lineIndentAt(input, objectNode.start)}${indentUnit}`;
|
||||
}
|
||||
|
||||
function buildMergeEdits(
|
||||
input,
|
||||
objectNode,
|
||||
managedSettings,
|
||||
indentUnit,
|
||||
eol,
|
||||
edits,
|
||||
) {
|
||||
const missingEntries = [];
|
||||
|
||||
for (const [key, managedValue] of Object.entries(managedSettings)) {
|
||||
const property = objectNode.properties.find(
|
||||
(candidate) => candidate.key === key,
|
||||
);
|
||||
|
||||
if (!property) {
|
||||
missingEntries.push([key, managedValue]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPlainObject(managedValue) && property.value.type === "object") {
|
||||
buildMergeEdits(
|
||||
input,
|
||||
property.value,
|
||||
managedValue,
|
||||
indentUnit,
|
||||
eol,
|
||||
edits,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDeepStrictEqual(property.value.value, managedValue)) {
|
||||
const propertyIndent = lineIndentAt(input, property.keyNode.start);
|
||||
edits.push({
|
||||
start: property.value.start,
|
||||
end: property.value.end,
|
||||
text: renderValue(managedValue, propertyIndent, indentUnit, eol),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (missingEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyIndent = getObjectChildIndent(input, objectNode, indentUnit);
|
||||
const closingIndent = lineIndentAt(input, objectNode.end);
|
||||
const closingBraceIndex = objectNode.end - 1;
|
||||
const renderedProperties = missingEntries
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${propertyIndent}${renderProperty(key, value, propertyIndent, indentUnit, eol)}`,
|
||||
)
|
||||
.join(`,${eol}`);
|
||||
|
||||
if (objectNode.properties.length === 0) {
|
||||
edits.push({
|
||||
start: objectNode.start + 1,
|
||||
end: closingBraceIndex,
|
||||
text: `${eol}${renderedProperties}${eol}${closingIndent}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lastProperty = objectNode.properties[objectNode.properties.length - 1];
|
||||
if (!lastProperty.hasTrailingComma) {
|
||||
edits.push({
|
||||
start: lastProperty.value.end,
|
||||
end: lastProperty.value.end,
|
||||
text: ",",
|
||||
});
|
||||
}
|
||||
|
||||
const closingLineStart = lineStartIndex(input, objectNode.end);
|
||||
const insertBeforeClosingLine = closingLineStart > lastProperty.value.end;
|
||||
|
||||
edits.push({
|
||||
start: insertBeforeClosingLine ? closingLineStart : objectNode.end,
|
||||
end: insertBeforeClosingLine ? closingLineStart : objectNode.end,
|
||||
text: insertBeforeClosingLine
|
||||
? `${renderedProperties}${eol}`
|
||||
: `${eol}${renderedProperties}${eol}${closingIndent}`,
|
||||
});
|
||||
}
|
||||
|
||||
function buildRemovalEdits(input, objectNode, keysToRemove, edits) {
|
||||
const removeSet = new Set(keysToRemove);
|
||||
if (removeSet.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties = objectNode.properties;
|
||||
const closingBraceIndex = objectNode.end - 1;
|
||||
|
||||
for (let index = 0; index < properties.length; index += 1) {
|
||||
if (!removeSet.has(properties[index].key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let endIndex = index;
|
||||
while (
|
||||
endIndex + 1 < properties.length &&
|
||||
removeSet.has(properties[endIndex + 1].key)
|
||||
) {
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
const firstProperty = properties[index];
|
||||
const lastProperty = properties[endIndex];
|
||||
const previousProperty = index > 0 ? properties[index - 1] : null;
|
||||
const nextProperty =
|
||||
endIndex + 1 < properties.length ? properties[endIndex + 1] : null;
|
||||
|
||||
if (!previousProperty && !nextProperty) {
|
||||
edits.push({
|
||||
start: objectNode.start + 1,
|
||||
end: closingBraceIndex,
|
||||
text:
|
||||
closingBraceIndex > lastProperty.value.end
|
||||
? input.slice(lastProperty.value.end, closingBraceIndex)
|
||||
: "",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextProperty) {
|
||||
edits.push({
|
||||
start: firstProperty.keyNode.start,
|
||||
end: nextProperty.keyNode.start,
|
||||
text: "",
|
||||
});
|
||||
index = endIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
edits.push({
|
||||
start: previousProperty.value.end,
|
||||
end: closingBraceIndex,
|
||||
text:
|
||||
closingBraceIndex > lastProperty.value.end
|
||||
? input.slice(lastProperty.value.end, closingBraceIndex)
|
||||
: "",
|
||||
});
|
||||
index = endIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function applyEdits(input, edits) {
|
||||
return edits
|
||||
.sort((left, right) => right.start - left.start || right.end - left.end)
|
||||
.reduce(
|
||||
(text, edit) =>
|
||||
`${text.slice(0, edit.start)}${edit.text}${text.slice(edit.end)}`,
|
||||
input,
|
||||
);
|
||||
}
|
||||
|
||||
function readLocalOverrides(filePath) {
|
||||
if (!filePath || !fs.existsSync(filePath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return parseJsonc(fs.readFileSync(filePath, "utf8"), filePath);
|
||||
}
|
||||
|
||||
function getServerOverride(overrides, name) {
|
||||
const serverMap = isPlainObject(overrides.servers) ? overrides.servers : {};
|
||||
const value = serverMap[name];
|
||||
return isPlainObject(value) ? value : {};
|
||||
}
|
||||
|
||||
function normalizeString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function buildTemplateReplacements(replacements, overrides) {
|
||||
const giteaOverrides = getServerOverride(overrides, "gitea");
|
||||
|
||||
return {
|
||||
...replacements,
|
||||
GITEA_SERVER_URL: normalizeString(giteaOverrides.serverUrl),
|
||||
GITEA_TOKEN: normalizeString(giteaOverrides.token),
|
||||
};
|
||||
}
|
||||
|
||||
function isServerEnabled(name, overrides) {
|
||||
const serverOverride = getServerOverride(overrides, name);
|
||||
if (serverOverride.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name === "gitea") {
|
||||
return (
|
||||
serverOverride.enabled === true &&
|
||||
normalizeString(serverOverride.serverUrl) !== "" &&
|
||||
normalizeString(serverOverride.token) !== ""
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectManagedConfig(managedConfig, serverKey, overrides) {
|
||||
const managedServers = managedConfig[serverKey];
|
||||
if (!isPlainObject(managedServers)) {
|
||||
throw new Error(
|
||||
`${serverKey} in ${serverKey} template must be an object at the root.`,
|
||||
);
|
||||
}
|
||||
|
||||
const selectedServers = {};
|
||||
for (const [name, serverConfig] of Object.entries(managedServers)) {
|
||||
if (isServerEnabled(name, overrides)) {
|
||||
selectedServers[name] = serverConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...managedConfig,
|
||||
[serverKey]: selectedServers,
|
||||
};
|
||||
}
|
||||
|
||||
function removeStaleManagedServers(
|
||||
input,
|
||||
rootNode,
|
||||
serverKey,
|
||||
managedServerNames,
|
||||
desiredServerNames,
|
||||
) {
|
||||
const serverProperty = rootNode.properties.find(
|
||||
(candidate) => candidate.key === serverKey,
|
||||
);
|
||||
|
||||
if (!serverProperty || serverProperty.value.type !== "object") {
|
||||
return input;
|
||||
}
|
||||
|
||||
const removableKeys = serverProperty.value.properties
|
||||
.map((property) => property.key)
|
||||
.filter(
|
||||
(key) => managedServerNames.has(key) && !desiredServerNames.has(key),
|
||||
);
|
||||
|
||||
if (removableKeys.length === 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const edits = [];
|
||||
buildRemovalEdits(input, serverProperty.value, removableKeys, edits);
|
||||
return applyEdits(input, edits);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const localOverrides = readLocalOverrides(options.overrides);
|
||||
const replacements = buildTemplateReplacements(
|
||||
options.replacements,
|
||||
localOverrides,
|
||||
);
|
||||
|
||||
const templateText = fs.readFileSync(options.template, "utf8");
|
||||
const renderedTemplate = renderTemplate(templateText, replacements);
|
||||
const managedConfig = parseJsonc(renderedTemplate, options.template);
|
||||
const desiredConfig = selectManagedConfig(
|
||||
managedConfig,
|
||||
options.serverKey,
|
||||
localOverrides,
|
||||
);
|
||||
|
||||
const managedServers = desiredConfig[options.serverKey];
|
||||
const allManagedServerNames = new Set(Object.keys(managedConfig[options.serverKey]));
|
||||
const desiredServerNames = new Set(Object.keys(managedServers));
|
||||
|
||||
const targetText = fs.existsSync(options.target)
|
||||
? fs.readFileSync(options.target, "utf8")
|
||||
: "{}\n";
|
||||
const targetAst = parseJsoncAst(targetText, options.target);
|
||||
const cleanedTargetText = removeStaleManagedServers(
|
||||
targetText,
|
||||
targetAst,
|
||||
options.serverKey,
|
||||
allManagedServerNames,
|
||||
desiredServerNames,
|
||||
);
|
||||
const cleanedTargetAst = parseJsoncAst(cleanedTargetText, options.target);
|
||||
const indentUnit = detectIndentUnit(targetText);
|
||||
const eol = detectEol(targetText);
|
||||
const edits = [];
|
||||
|
||||
buildMergeEdits(
|
||||
cleanedTargetText,
|
||||
cleanedTargetAst,
|
||||
desiredConfig,
|
||||
indentUnit,
|
||||
eol,
|
||||
edits,
|
||||
);
|
||||
|
||||
const output =
|
||||
edits.length === 0 ? cleanedTargetText : applyEdits(cleanedTargetText, edits);
|
||||
|
||||
parseJsonc(output, options.target);
|
||||
fs.mkdirSync(path.dirname(options.target), { recursive: true });
|
||||
|
||||
if (output === targetText) {
|
||||
console.log(`Managed MCP config already up to date: ${options.target}`);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(options.target, output, "utf8");
|
||||
console.log(`Merged managed MCP config into: ${options.target}`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user