Files
komodo-mcp-server/scripts/generate_types.ts
T

435 lines
18 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env tsx
/**
* Auto-generates mapping tables in src/types.ts from komodo_client package types.
*
* Run: npm run generate-types
*
* Parses the package's .d.ts files to extract request name literals from the
* ReadRequest, WriteRequest, ExecuteRequest discriminated unions, then derives
* resource-type → request-name mappings using Komodo's naming conventions.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const PKG_TYPES = resolve(
import.meta.dirname,
"../node_modules/komodo_client/dist/types.d.ts",
);
const PKG_RESPONSES = resolve(
import.meta.dirname,
"../node_modules/komodo_client/dist/responses.d.ts",
);
const OUT = resolve(import.meta.dirname, "../src/types.ts");
// ── Parse type literals from a discriminated union ──────────────────────────
function extractTypeLiterals(fileContent: string, unionName: string): string[] {
// Find the union: "export type ReadRequest = { type: "Foo"; ... } | { type: "Bar"; ... }"
const regex = new RegExp(
`export type ${unionName} =([\\s\\S]*?)(?=\\nexport |$)`,
);
const match = fileContent.match(regex);
if (!match) throw new Error(`Could not find union ${unionName}`);
const body = match[1];
const literals: string[] = [];
const typeRegex = /type:\s*"([^"]+)"/g;
let m: RegExpExecArray | null;
while ((m = typeRegex.exec(body)) !== null) {
literals.push(m[1]);
}
return literals;
}
// ── Parse response map keys ─────────────────────────────────────────────────
function extractResponseKeys(
fileContent: string,
mapName: string,
): string[] {
const regex = new RegExp(
`export type ${mapName} =([\\s\\S]*?)(?=\\nexport |$)`,
);
const match = fileContent.match(regex);
if (!match) throw new Error(`Could not find response map ${mapName}`);
const body = match[1];
const keys: string[] = [];
const keyRegex = /(\w+):\s*/g;
let m: RegExpExecArray | null;
while ((m = keyRegex.exec(body)) !== null) {
keys.push(m[1]);
}
return keys;
}
// ── Resource type mapping rules ─────────────────────────────────────────────
// Maps Komodo PascalCase resource names to our snake_case resource types.
// Only resources that have List*/Get*/Create*/Update*/Delete* endpoints qualify.
const RESOURCE_NAME_TO_TYPE: Record<string, string> = {
Stack: "stack",
Build: "build",
Server: "server",
Procedure: "procedure",
Deployment: "deployment",
Alerter: "alerter",
ImageRegistryAccount: "image_registry_account",
ResourceSync: "sync_resource",
User: "user",
Tag: "tag",
Action: "action",
Repo: "repo",
Builder: "builder",
Swarm: "swarm",
Variable: "variable",
UserGroup: "user_group",
GitProviderAccount: "git_provider_account",
};
function deriveResourceType(requestName: string): string | null {
for (const [pascal, snake] of Object.entries(RESOURCE_NAME_TO_TYPE)) {
if (requestName === `List${pascal}s`) return snake;
if (requestName === `Get${pascal}`) return snake;
if (requestName === `Create${pascal}`) return snake;
if (requestName === `Update${pascal}`) return snake;
if (requestName === `Delete${pascal}`) return snake;
if (requestName === `Copy${pascal}`) return snake;
if (requestName === `Rename${pascal}`) return snake;
if (requestName === `Get${pascal}sSummary`) return snake;
}
return null;
}
// ── Main ────────────────────────────────────────────────────────────────────
function main() {
const typesContent = readFileSync(PKG_TYPES, "utf-8");
const responsesContent = readFileSync(PKG_RESPONSES, "utf-8");
const readRequests = extractTypeLiterals(typesContent, "ReadRequest");
const writeRequests = extractTypeLiterals(typesContent, "WriteRequest");
const executeRequests = extractTypeLiterals(typesContent, "ExecuteRequest");
// ── LIST_REQUEST_MAP ────────────────────────────────────────────────────
const listEntries: [string, string][] = [];
for (const name of readRequests) {
const resourceType = deriveResourceType(name);
if (resourceType && name.startsWith("List") && !name.startsWith("ListFull")) {
listEntries.push([resourceType, name]);
}
}
// ── GET_REQUEST_MAP ─────────────────────────────────────────────────────
const getEntries: [string, string][] = [];
for (const name of readRequests) {
const resourceType = deriveResourceType(name);
if (resourceType && name.startsWith("Get") && !name.endsWith("Summary") && !name.endsWith("ActionState")) {
getEntries.push([resourceType, name]);
}
}
// ── CREATE_REQUEST_MAP ──────────────────────────────────────────────────
const createEntries: [string, string][] = [];
for (const name of writeRequests) {
const resourceType = deriveResourceType(name);
if (resourceType && name.startsWith("Create")) {
createEntries.push([resourceType, name]);
}
}
// ── UPDATE_REQUEST_MAP ──────────────────────────────────────────────────
// Special cases where the update name doesn't follow Update{Resource}
const UPDATE_OVERRIDES: Record<string, string> = {
tag: "UpdateTagColor",
variable: "UpdateVariableValue",
};
const updateEntries: [string, string][] = [];
for (const name of writeRequests) {
if (name.startsWith("Update") && !name.startsWith("UpdateResourceMeta") &&
!name.startsWith("UpdateServerPublicKey") && !name.startsWith("UpdateOnboardingKey") &&
!name.startsWith("UpdateUser") && !name.startsWith("UpdatePermission") &&
!name.startsWith("UpdateVariableDescription") && !name.startsWith("UpdateVariableIsSecret") &&
!name.startsWith("UpdateGitProviderAccount") && !name.startsWith("UpdateImageRegistryAccount")) {
const resourceType = deriveResourceType(name);
if (resourceType) {
updateEntries.push([resourceType, name]);
}
}
}
// Apply overrides
for (const [rt, name] of Object.entries(UPDATE_OVERRIDES)) {
const idx = updateEntries.findIndex(([r]) => r === rt);
if (idx >= 0) updateEntries[idx][1] = name;
else updateEntries.push([rt, name]);
}
// ── DELETE_REQUEST_MAP ──────────────────────────────────────────────────
const deleteEntries: [string, string][] = [];
for (const name of writeRequests) {
const resourceType = deriveResourceType(name);
if (resourceType && name.startsWith("Delete")) {
deleteEntries.push([resourceType, name]);
}
}
// ── EXECUTE_REQUEST_MAP ─────────────────────────────────────────────────
const EXECUTE_OVERRIDES: Record<string, { route: string; name: string }> = {
run_build: { route: "execute", name: "RunBuild" },
deploy_stack_service: { route: "execute", name: "RunStackService" },
run_server_prune_images: { route: "execute", name: "PruneImages" },
run_server_prune_containers: { route: "execute", name: "PruneContainers" },
run_server_prune_networks: { route: "execute", name: "PruneNetworks" },
};
const executeEntries: [string, { route: string; name: string }][] = [];
for (const name of executeRequests) {
// Convert PascalCase to snake_case for the operation name
const snake = name
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase();
// Check overrides first
if (EXECUTE_OVERRIDES[snake]) {
executeEntries.push([snake, EXECUTE_OVERRIDES[snake]]);
continue;
}
// Default: route is "execute", name is the PascalCase name
executeEntries.push([snake, { route: "execute", name }]);
}
// ── SUMMARY_REQUEST_MAP ─────────────────────────────────────────────────
const summaryEntries: [string, string][] = [];
for (const name of readRequests) {
if (name.endsWith("Summary")) {
const resourceType = deriveResourceType(name);
if (resourceType) {
summaryEntries.push([resourceType, name]);
}
}
}
// ── INSPECT maps ──────────────────────────────────────────────────────
// INSPECT_PARAM_KEY: snake_case type → param key for server-scoped Docker objects
// INSPECT_REQUEST_MAP: snake_case type → request name
const INSPECT_PARAM_ENTRIES: [string, string][] = [
["container", "container"],
["image", "image"],
["network", "network"],
["volume", "volume"],
];
const INSPECT_REQUEST_ENTRIES: [string, string][] = [
["container", "InspectContainer"],
["image", "InspectImage"],
["network", "InspectNetwork"],
["volume", "InspectVolume"],
["deployment_container", "InspectDeploymentContainer"],
["deployment_swarm_service", "InspectDeploymentSwarmService"],
["stack_container", "InspectStackContainer"],
["stack_swarm_info", "InspectStackSwarmInfo"],
["stack_swarm_service", "InspectStackSwarmService"],
["swarm", "InspectSwarm"],
["swarm_config", "InspectSwarmConfig"],
["swarm_node", "InspectSwarmNode"],
["swarm_secret", "InspectSwarmSecret"],
["swarm_service", "InspectSwarmService"],
["swarm_stack", "InspectSwarmStack"],
["swarm_task", "InspectSwarmTask"],
];
// ── SEARCH_LOG_REQUEST_MAP ──────────────────────────────────────────────
const SEARCH_LOG_MAP: Record<string, string> = {
container: "SearchContainerLog",
deployment: "SearchDeploymentLog",
stack: "SearchStackLog",
swarm_service: "SearchSwarmServiceLog",
};
const searchLogEntries = Object.entries(SEARCH_LOG_MAP);
// ── LIST_DETAIL_REQUEST_MAP ─────────────────────────────────────────────
// These are the "extra" list endpoints that don't map to a resource type
const LIST_DETAIL_OVERRIDES: Record<string, string> = {
containers: "ListContainers",
all_containers: "ListAllContainers",
images: "ListImages",
networks: "ListNetworks",
volumes: "ListVolumes",
system_processes: "ListSystemProcesses",
schedules: "ListSchedules",
permissions: "ListPermissions",
api_keys: "ListApiKeys",
api_keys_for_service_user: "ListApiKeysForServiceUser",
onboarding_keys: "ListOnboardingKeys",
secrets: "ListSecrets",
updates: "ListUpdates",
build_versions: "ListBuildVersions",
compose_projects: "ListComposeProjects",
user_target_permissions: "ListUserTargetPermissions",
full_stacks: "ListFullStacks",
full_builds: "ListFullBuilds",
full_servers: "ListFullServers",
full_deployments: "ListFullDeployments",
full_procedures: "ListFullProcedures",
full_repos: "ListFullRepos",
full_builders: "ListFullBuilders",
full_swarms: "ListFullSwarms",
full_actions: "ListFullActions",
full_alerters: "ListFullAlerters",
full_resource_syncs: "ListFullResourceSyncs",
swarm_configs: "ListSwarmConfigs",
swarm_networks: "ListSwarmNetworks",
swarm_nodes: "ListSwarmNodes",
swarm_secrets: "ListSwarmSecrets",
swarm_services: "ListSwarmServices",
swarm_stacks: "ListSwarmStacks",
swarm_tasks: "ListSwarmTasks",
git_providers_from_config: "ListGitProvidersFromConfig",
image_registries_from_config: "ListImageRegistriesFromConfig",
common_build_extra_args: "ListCommonBuildExtraArgs",
common_deployment_extra_args: "ListCommonDeploymentExtraArgs",
common_stack_build_extra_args: "ListCommonStackBuildExtraArgs",
common_stack_extra_args: "ListCommonStackExtraArgs",
image_history: "ListImageHistory",
all_stack_services: "ListAllStackServices",
};
const listDetailEntries = Object.entries(LIST_DETAIL_OVERRIDES);
// ── COPY_REQUEST_MAP ────────────────────────────────────────────────────
const copyEntries: [string, string][] = [];
for (const name of writeRequests) {
const resourceType = deriveResourceType(name);
if (resourceType && name.startsWith("Copy")) {
copyEntries.push([resourceType, name]);
}
}
// ── RENAME_REQUEST_MAP ──────────────────────────────────────────────────
const renameEntries: [string, string][] = [];
for (const name of writeRequests) {
if (name.startsWith("Rename")) {
const resourceType = deriveResourceType(name);
if (resourceType) {
renameEntries.push([resourceType, name]);
}
}
}
// ── Generate output ─────────────────────────────────────────────────────
const out = `import { z } from "zod";
export const ResourceType = z.enum([
"stack",
"build",
"server",
"procedure",
"deployment",
"alerter",
"image_registry_account",
"sync_resource",
"user",
"tag",
"action",
"repo",
"builder",
"swarm",
"variable",
"user_group",
"git_provider_account",
]);
export type ResourceType = z.infer<typeof ResourceType>;
export const ExecuteOperation = z.enum([
${executeEntries.map(([k]) => `"${k}"`).join(",\n ")}
]);
export type ExecuteOperation = z.infer<typeof ExecuteOperation>;
// ── Auto-generated from komodo_client package types ────────────────────────
// Run \`npm run generate-types\` to re-sync with upstream API.
export const LIST_REQUEST_MAP: Record<ResourceType, string> = {
${sortEntries(listEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const SEARCH_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(listEntries.filter(([k]) => ["stack", "build", "server", "procedure", "deployment", "alerter", "image_registry_account", "sync_resource"].includes(k))).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const GET_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(getEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const CREATE_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(createEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const UPDATE_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(updateEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const DELETE_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(deleteEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const EXECUTE_REQUEST_MAP: Record<
ExecuteOperation,
{ route: "execute" | "read" | "write"; name: string }
> = {
${sortEntries(executeEntries).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(",\n ")}
};
export const SUMMARY_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
${sortEntries(summaryEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const INSPECT_PARAM_KEY: Record<string, string> = {
${INSPECT_PARAM_ENTRIES.map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const INSPECT_REQUEST_MAP: Record<string, string> = {
${INSPECT_REQUEST_ENTRIES.map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const SEARCH_LOG_REQUEST_MAP: Record<string, string> = {
${searchLogEntries.map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const LIST_DETAIL_REQUEST_MAP: Record<string, string> = {
${listDetailEntries.map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const COPY_REQUEST_MAP: Record<string, string> = {
${sortEntries(copyEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
export const RENAME_REQUEST_MAP: Record<string, string> = {
${sortEntries(renameEntries).map(([k, v]) => `${k}: "${v}"`).join(",\n ")}
};
`;
writeFileSync(OUT, out, "utf-8");
// Summary
console.log(`Generated ${OUT}`);
console.log(` ReadRequest variants: ${readRequests.length}`);
console.log(` WriteRequest variants: ${writeRequests.length}`);
console.log(` ExecuteRequest variants: ${executeRequests.length}`);
console.log(` LIST_REQUEST_MAP: ${listEntries.length} entries`);
console.log(` GET_REQUEST_MAP: ${getEntries.length} entries`);
console.log(` CREATE_REQUEST_MAP: ${createEntries.length} entries`);
console.log(` UPDATE_REQUEST_MAP: ${updateEntries.length} entries`);
console.log(` DELETE_REQUEST_MAP: ${deleteEntries.length} entries`);
console.log(` EXECUTE_REQUEST_MAP: ${executeEntries.length} entries`);
console.log(` SUMMARY_REQUEST_MAP: ${summaryEntries.length} entries`);
console.log(` INSPECT_REQUEST_MAP: ${INSPECT_REQUEST_ENTRIES.length} entries`);
console.log(` COPY_REQUEST_MAP: ${copyEntries.length} entries`);
console.log(` RENAME_REQUEST_MAP: ${renameEntries.length} entries`);
}
function sortEntries(entries: [string, string][]): [string, string][] {
return [...entries].sort(([a], [b]) => a.localeCompare(b));
}
main();