/** * Coolify service for MCP server and CLI. * * Provides all Coolify API operations for deployment management. * * @module */ import { ok, err, isErr, type Result } from "@mks2508/no-throw"; import { component } from "@mks2508/better-logger"; import { loadConfig, type ICoolifyConfig } from "./config.js"; import { type ICoolifyAppOptions, type ICoolifyAppResult, type ICoolifyApplication, type ICoolifyDatabase, type ICoolifyDatabaseBackup, type ICoolifyDeleteResult, type ICoolifyDeployment, type ICoolifyDeployOptions, type ICoolifyDeployResult, type ICoolifyDestination, type ICoolifyEnvironment, type ICoolifyGithubApp, type ICoolifyLogs, type ICoolifyLogsOptions, type ICoolifyPrivateKey, type ICoolifyProject, type ICoolifyServer, type ICoolifyServerDomain, type ICoolifyServerResource, type ICoolifyService as ICoolifyServiceType, type ICoolifyTeam, type ICoolifyUpdateOptions, type ICoolifyVersion, type ICoolifyInfrastructureTree, type ICoolifyProjectNode, type ICoolifyEnvironmentNode, type ICoolifyResource, type IProgressCallback, } from "./types.js"; const log = component("CoolifyService"); /** * Coolify API response type. */ interface ICoolifyApiResponse { data?: T; error?: string; status: number; durationMs?: number; } /** * Environment variable from Coolify API. */ export interface ICoolifyEnvVar { uuid: string; key: string; value: string; real_value?: string; is_buildtime: boolean; is_runtime: boolean; is_required: boolean; } /** * Coolify service for deployment operations. * * @example * ```typescript * const coolify = new CoolifyService() * const initResult = await coolify.init() * if (initResult.isErr()) { * console.error(initResult.error.message) * return * } * * const deployResult = await coolify.deploy({ uuid: 'app-uuid' }) * if (deployResult.isOk()) { * console.log('Deployment UUID:', deployResult.value.deploymentUuid) * } * ``` */ export class CoolifyService { private baseUrl: string | undefined; private token: string | undefined; private config: ICoolifyConfig = {}; /** * Checks if the service is configured with URL and token. * * @returns true if both URL and token are set */ isConfigured(): boolean { const hasUrl = !!this.baseUrl || !!process.env.COOLIFY_URL; const hasToken = !!this.token || !!process.env.COOLIFY_TOKEN; return hasUrl && hasToken; } /** * Initializes the Coolify service by loading configuration. * * @returns Result indicating success or error */ async init(): Promise> { const configResult = await loadConfig(); if (isErr(configResult)) { log.error("Failed to load config"); return err(configResult.error); } this.config = configResult.value; this.baseUrl = this.config.url || process.env.COOLIFY_URL; this.token = this.config.token || process.env.COOLIFY_TOKEN; if (!this.baseUrl) { log.error("No Coolify URL configured"); log.info( "Set COOLIFY_URL environment variable or run: coolify-mcp config set url ", ); return err( new Error( "No Coolify URL configured. Set COOLIFY_URL or use config command.", ), ); } if (!this.token) { log.error("No Coolify token configured"); log.info( "Set COOLIFY_TOKEN environment variable or run: coolify-mcp config set token ", ); return err( new Error( "No Coolify token configured. Set COOLIFY_TOKEN or use config command.", ), ); } log.debug("Coolify connection configured"); return ok(undefined); } /** * Makes a request to the Coolify API. * * @param endpoint - API endpoint * @param options - Fetch options * @returns API response with data or error */ private async request( endpoint: string, options: RequestInit = {}, ): Promise> { const startTime = Date.now(); if (!this.baseUrl || !this.token) { return { error: "Coolify not configured", status: 0, durationMs: Date.now() - startTime, }; } try { const baseUrl = this.baseUrl.replace(/\/+$/, ""); const url = `${baseUrl}/api/v1${endpoint}`; const response = await fetch(url, { ...options, headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", Accept: "application/json", ...options.headers, }, }); const text = await response.text(); const durationMs = Date.now() - startTime; let data: T | undefined; try { data = text ? JSON.parse(text) : undefined; } catch (parseErr) { const preview = text ? text.slice(0, 200) : "(empty body)"; const method = options.method ?? "GET"; log.warn( `Failed to parse JSON response from ${method} ${endpoint} (status ${response.status}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}. Body preview: ${preview}`, ); if (!response.ok) { return { error: text || `HTTP ${response.status}`, status: response.status, durationMs, }; } // OK status but body is not JSON — synthesize an error so the caller's error path triggers. // Avoids silently returning undefined data which causes cryptic downstream crashes. return { error: `Response was not valid JSON (status ${response.status}): ${preview}`, status: response.status, durationMs, }; } if (!response.ok) { const parsed = data as | { message?: string; errors?: Record } | undefined; let errorMessage = parsed?.message || `HTTP ${response.status}`; // Include validation errors if present (Coolify returns { message, errors: { field: [reasons] } }) if (parsed?.errors) { const details = Object.entries(parsed.errors) .map( ([field, reasons]) => `${field}: ${Array.isArray(reasons) ? reasons.join(", ") : String(reasons)}`, ) .join("; "); errorMessage += ` — ${details}`; } return { error: errorMessage, status: response.status, durationMs }; } return { data, status: response.status, durationMs }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { error: message, status: 0, durationMs: Date.now() - startTime }; } } /** * Deploys an application. * * @param options - Deployment options * @param onProgress - Optional progress callback (0-100, message, step) * @returns Result with deployment info or error */ async deploy( options: ICoolifyDeployOptions, onProgress?: IProgressCallback, ): Promise> { if (!options.uuid && !options.tag) { return err(new Error("Either uuid or tag is required")); } const appId = options.uuid?.slice(0, 8) || options.tag || "unknown"; onProgress?.(5, `Preparing deployment for ${appId}...`); log.info(`Deploying application ${options.uuid || options.tag}`); onProgress?.(25, "Validating deployment configuration"); onProgress?.(50, "Triggering build pipeline..."); // Build query parameters for deploy endpoint const params = new URLSearchParams(); if (options.uuid) params.set("uuid", options.uuid); if (options.tag) params.set("tag", options.tag); if (options.force) params.set("force", "true"); const endpoint = `/deploy${params.toString() ? `?${params.toString()}` : ""}`; const result = await this.request<{ deployments: Array<{ message: string; resource_uuid: string; deployment_uuid: string; }>; }>(endpoint, { method: "GET", }); if (result.error) { log.error(`Deployment failed: ${result.error}`); return err(new Error(result.error)); } // Response is { deployments: [{ message, resource_uuid, deployment_uuid }] } const deployments = result.data?.deployments || []; if (deployments.length === 0) { log.error("No deployments started"); return err( new Error("No deployments started - check application configuration"), ); } const deployment = deployments[0]; onProgress?.(90, "Build started on Coolify server"); onProgress?.(100, "Deployment triggered"); log.success(`Deployment started: ${deployment.deployment_uuid}`); return ok({ success: true, deploymentUuid: deployment.deployment_uuid, resourceUuid: deployment.resource_uuid, }); } /** * Creates a new application in Coolify. * * Uses type-specific endpoints for different application types: * - /applications/public - Public Git repository * - /applications/private-github-app - Private repo with GitHub App * - /applications/private-deploy-key - Private repo with deploy key * - /applications/dockerfile - Dockerfile-based application * - /applications/docker-image - Docker image * - /applications/docker-compose - Docker Compose application * * @param options - Application options * @param onProgress - Optional progress callback (0-100, message, step) * @returns Result with application UUID or error */ async createApplication( options: ICoolifyAppOptions, onProgress?: IProgressCallback, ): Promise> { onProgress?.(5, `Preparing app "${options.name}"`); const appType = options.type || "public"; log.info(`Creating application ${options.name} (type: ${appType})`); onProgress?.(25, `Validating server ${options.serverUuid.slice(0, 8)}...`); onProgress?.(50, "Sending creation request to Coolify API..."); // Determine endpoint based on application type const endpointMap: Record = { public: "/applications/public", "private-github-app": "/applications/private-github-app", "private-deploy-key": "/applications/private-deploy-key", dockerfile: "/applications/dockerfile", "docker-image": "/applications/dockerimage", "docker-compose": "/applications/dockercompose", dockerimage: "/applications/dockerimage", dockercompose: "/applications/dockercompose", }; const endpoint = endpointMap[appType] || "/applications/public"; // Build request body based on application type const body: Record = { name: options.name, description: options.description, project_uuid: options.projectUuid, environment_uuid: options.environmentUuid, environment_name: options.environmentName, server_uuid: options.serverUuid, }; // Git repository — applies to all types that have a repo URL. // Coolify's API expects the full URL (https://, http://, git://, or git@). if (options.githubRepoUrl) { body.git_repository = options.githubRepoUrl; } // Type-specific fields if ( appType === "public" || appType === "private-github-app" || appType === "private-deploy-key" ) { if (options.githubAppUuid) { body.github_app_uuid = options.githubAppUuid; } // private_key_uuid is required for private-deploy-key type if (appType === "private-deploy-key" && options.privateKeyUuid) { body.private_key_uuid = options.privateKeyUuid; } body.git_branch = options.branch || "main"; body.build_pack = options.buildPack || "dockerfile"; if (options.portsExposes) { body.ports_exposes = options.portsExposes; } // Dockerfile / Docker Compose configuration // Coolify validates these paths against /^\/.*$/ regex — prepend leading slash // if caller passed a relative path (CLI --help suggests "apps/x/Dockerfile" without /) if (options.dockerfileLocation) { const p = options.dockerfileLocation; body.dockerfile_location = p.startsWith("/") ? p : `/${p}`; } if (options.dockerComposeLocation) { const p = options.dockerComposeLocation; body.docker_compose_location = p.startsWith("/") ? p : `/${p}`; } if (options.baseDirectory) { const p = options.baseDirectory; body.base_directory = p.startsWith("/") ? p : `/${p}`; } } else if (appType === "docker-image" && options.dockerImage) { body.docker_registry_image_name = options.dockerImage; } else if (appType === "docker-compose" && options.dockerCompose) { body.docker_compose_raw = options.dockerCompose; } log.debug(`Create application body: ${JSON.stringify(body, null, 2)}`); log.debug(`Endpoint: POST ${endpoint}`); const result = await this.request<{ uuid: string }>(endpoint, { method: "POST", body: JSON.stringify(body), }); if (result.error) { log.error(`Failed to create application: ${result.error}`); return err(new Error(result.error)); } onProgress?.(100, `Application "${options.name}" created`); log.success(`Application created: ${result.data?.uuid}`); return ok({ success: true, uuid: result.data?.uuid, }); } /** * Sets environment variables for an application. * * Delegates to {@link bulkUpdateEnvironmentVariables} so the same * create-or-update logic applies. This fixes the post-delete scenario * (where the variable was deleted via the API and only its base value * from docker-compose/git remains visible) and prevents duplicates * that the singular POST endpoint would create when called for a key * that already exists. Also eliminates the N+1 API calls and the * TOCTOU race between the DELETE and the re-POST that the previous * per-key POST -> DELETE -> re-POST dance had. * * @param appUuid - Application UUID * @param envVars - Environment variables to set * @returns Result indicating success or error */ async setEnvironmentVariables( appUuid: string, envVars: Record, ): Promise> { log.info( `Setting ${Object.keys(envVars).length} environment variables for ${appUuid}`, ); const vars = Object.entries(envVars).map(([key, value]) => ({ key, value, is_buildtime: false, is_runtime: true, })); const result = await this.bulkUpdateEnvironmentVariables(appUuid, vars); if (isErr(result)) { return err(result.error); } log.success(`${vars.length} environment variables set`); return ok(undefined); } /** * Gets environment variables for an application. * * @param appUuid - Application UUID * @returns Result with environment variables or error */ async getEnvironmentVariables( appUuid: string, ): Promise> { log.info(`Getting environment variables for ${appUuid}`); const result = await this.request( `/applications/${appUuid}/envs`, ); if (result.error) { log.error(`Failed to get env vars: ${result.error}`); return err(new Error(result.error)); } log.success(`Environment variables retrieved for ${appUuid}`); return ok(result.data || []); } /** * Sets a single environment variable for an application. * * Delegates to {@link bulkUpdateEnvironmentVariables} so the same * create-or-update logic applies. This fixes the post-delete scenario * (where the variable was deleted via the API and only its base value * from docker-compose/git remains visible) and prevents duplicates * that the singular PATCH endpoint would create when called without * the variable's UUID. * * @param appUuid - Application UUID * @param key - Variable name * @param value - Variable value * @param isBuildTime - Whether the variable is available at build time * (only for new vars; existing runtime-only vars will be flipped to * build-time and vice versa). * @returns Result indicating success or error */ async setEnvironmentVariable( appUuid: string, key: string, value: string, isBuildTime: boolean = false, ): Promise> { log.info(`Setting environment variable ${key} for ${appUuid} (buildtime: ${isBuildTime})`); const result = await this.bulkUpdateEnvironmentVariables(appUuid, [ { key, value, is_buildtime: isBuildTime, is_runtime: !isBuildTime, }, ]); if (isErr(result)) { return err(result.error); } log.success(`Environment variable ${key} set for ${appUuid}`); return ok(undefined); } /** * Deletes an environment variable from an application. * * @param appUuid - Application UUID * @param key - Variable name to delete * @returns Result indicating success or error */ async deleteEnvironmentVariable( appUuid: string, key: string, ): Promise> { log.info(`Deleting environment variable ${key} from ${appUuid}`); // First get all env vars to find the UUID of the one to delete const envVarsResult = await this.getEnvironmentVariables(appUuid); if (isErr(envVarsResult)) { return err(envVarsResult.error); } const envVar = envVarsResult.value.find((ev) => ev.key === key); if (!envVar) { log.error(`Environment variable ${key} not found`); return err(new Error(`Environment variable ${key} not found`)); } const result = await this.request( `/applications/${appUuid}/envs/${envVar.uuid}`, { method: "DELETE", }, ); if (result.error) { log.error(`Failed to delete env var: ${result.error}`); return err(new Error(result.error)); } log.success(`Environment variable ${key} deleted from ${appUuid}`); return ok(undefined); } /** * Gets the status of an application. * * @param appUuid - Application UUID * @returns Result with status or error */ async getApplicationStatus(appUuid: string): Promise> { const result = await this.request<{ status: string }>( `/applications/${appUuid}`, ); if (result.error) { return err(new Error(result.error)); } return ok(result.data?.status || "unknown"); } /** * Lists available servers in Coolify. * * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with servers or error */ async listServers( page?: number, perPage?: number, ): Promise> { let endpoint = "/servers"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) { return err(new Error(result.error)); } return ok(result.data || []); } /** * Gets details of a specific server. * * @param serverUuid - Server UUID * @returns Result with server details or error */ async getServer(serverUuid: string): Promise> { log.info(`Getting server details for ${serverUuid}`); const result = await this.request(`/servers/${serverUuid}`); if (result.error) { log.error(`Failed to get server: ${result.error}`); return err(new Error(result.error)); } log.success(`Server details retrieved: ${serverUuid}`); return ok(result.data as ICoolifyServer); } /** * Lists all GitHub Apps configured in Coolify. * * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with GitHub Apps list or error */ async listGithubApps( page?: number, perPage?: number, ): Promise> { let endpoint = "/github-apps"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Lists all GitHub Apps configured in Coolify. * Fetches all pages internally and returns a flat list. * * @param perPage - Items per page for each request (default: 50) * @returns Result with all GitHub Apps or error */ async listGithubAppsAll( perPage = 50, ): Promise> { const allApps: ICoolifyGithubApp[] = []; let page = 1; while (true) { const result = await this.listGithubApps(page, perPage); if (isErr(result)) return err(result.error); const apps = result.value; if (apps.length === 0) break; allApps.push(...apps); // If we got fewer than perPage, we've reached the last page if (apps.length < perPage) break; page++; } return ok(allApps); } /** * Lists all projects. * * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with projects list or error */ async listProjects( page?: number, perPage?: number, ): Promise> { let endpoint = "/projects"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) { return err(new Error(result.error)); } return ok(result.data || []); } /** * Creates a new project. * * @param name - Project name * @param description - Optional project description * @returns Result with created project or error */ async createProject( name: string, description?: string, ): Promise> { log.info(`Creating project: ${name}`); const result = await this.request("/projects", { method: "POST", body: JSON.stringify({ name, description: description || "" }), }); if (result.error) { log.error(`Failed to create project: ${result.error}`); return err(new Error(result.error)); } log.success(`Project created: ${result.data?.uuid}`); return ok(result.data!); } /** * Gets environments for a project. * * @param projectUuid - Project UUID * @returns Result with environments list or error */ async getProjectEnvironments( projectUuid: string, ): Promise> { log.info(`Getting environments for project ${projectUuid}`); const result = await this.request<{ environments: ICoolifyEnvironment[] }>( `/projects/${projectUuid}`, ); if (result.error) { log.error(`Failed to get environments: ${result.error}`); return err(new Error(result.error)); } log.success(`Environments retrieved for project ${projectUuid}`); return ok(result.data?.environments || []); } /** * Lists all teams. * * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with teams list or error */ async listTeams( page?: number, perPage?: number, ): Promise> { let endpoint = "/teams"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) { return err(new Error(result.error)); } return ok(result.data || []); } /** * Gets available destinations for a server. * * @param serverUuid - Server UUID * @returns Result with destinations or error */ async getServerDestinations( serverUuid: string, ): Promise> { const result = await this.request<{ destinations: ICoolifyDestination[]; }>(`/servers/${serverUuid}`); if (result.error) { return err(new Error(result.error)); } return ok(result.data?.destinations || []); } /** * Lists all applications. * * @param teamId - Optional team ID to filter by * @param projectId - Optional project ID to filter by * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with applications list or error */ async listApplications( teamId?: string, projectId?: string, page?: number, perPage?: number, ): Promise> { log.info("Listing applications"); let endpoint = "/applications"; const params = new URLSearchParams(); if (teamId) params.set("team_id", teamId); if (projectId) params.set("project_id", projectId); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) { log.error(`Failed to list applications: ${result.error}`); return err(new Error(result.error)); } log.success(`Listed ${result.data?.length || 0} applications`); return ok(result.data || []); } /** * Deletes an application. * * @param appUuid - Application UUID * @param options - Delete options for cascade deletion * @returns Result indicating success or error */ async deleteApplication( appUuid: string, options?: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; }, ): Promise> { log.info(`Deleting application ${appUuid}`); const params = new URLSearchParams(); if (options?.deleteConfigurations) params.set("delete_configurations", "true"); if (options?.deleteVolumes) params.set("delete_volumes", "true"); if (options?.dockerCleanup) params.set("docker_cleanup", "true"); if (options?.deleteConnectedNetworks) params.set("delete_connected_networks", "true"); const queryString = params.toString(); const endpoint = `/applications/${appUuid}${queryString ? `?${queryString}` : ""}`; const result = await this.request(endpoint, { method: "DELETE", }); if (result.error) { log.error(`Failed to delete application: ${result.error}`); return err(new Error(result.error)); } log.success(`Application deleted: ${appUuid}`); return ok({ success: true, message: "Application deleted" }); } /** * Updates an application configuration. * * @param appUuid - Application UUID * @param options - Update options * @returns Result with updated application or error */ async updateApplication( appUuid: string, options: ICoolifyUpdateOptions, ): Promise> { log.info(`Updating application ${appUuid}`); const body: Record = {}; if (options.name) body.name = options.name; if (options.description) body.description = options.description; if (options.buildPack) body.build_pack = options.buildPack; if (options.gitBranch) body.git_branch = options.gitBranch; if (options.portsExposes) body.ports_exposes = options.portsExposes; if (options.installCommand) body.install_command = options.installCommand; if (options.buildCommand) body.build_command = options.buildCommand; if (options.startCommand) body.start_command = options.startCommand; if (options.dockerfileLocation) body.dockerfile_location = options.dockerfileLocation; if (options.baseDirectory) body.base_directory = options.baseDirectory; if (options.domains) body.domains = options.domains; if (options.dockerComposeDomains) body.docker_compose_domains = options.dockerComposeDomains; if (options.dockerComposeRaw !== undefined) body.docker_compose_raw = options.dockerComposeRaw; if (options.isForceHttpsEnabled !== undefined) body.is_force_https_enabled = options.isForceHttpsEnabled; if (options.isAutoDeployEnabled !== undefined) body.is_auto_deploy_enabled = options.isAutoDeployEnabled; if (options.watchPaths !== undefined) body.watch_paths = options.watchPaths; if (options.healthCheckEnabled !== undefined) body.health_check_enabled = options.healthCheckEnabled; if (options.healthCheckPath) body.health_check_path = options.healthCheckPath; if (options.healthCheckPort) body.health_check_port = String(options.healthCheckPort); if (options.healthCheckMethod) body.health_check_method = options.healthCheckMethod; if (options.healthCheckInterval) body.health_check_interval = options.healthCheckInterval; if (options.healthCheckTimeout) body.health_check_timeout = options.healthCheckTimeout; if (options.healthCheckRetries) body.health_check_retries = options.healthCheckRetries; if (options.healthCheckStartPeriod) body.health_check_start_period = options.healthCheckStartPeriod; if (options.healthCheckReturnCode) body.health_check_return_code = options.healthCheckReturnCode; const result = await this.request( `/applications/${appUuid}`, { method: "PATCH", body: JSON.stringify(body), }, ); if (result.error) { log.error(`Failed to update application: ${result.error}`); return err(new Error(result.error)); } log.success(`Application updated: ${appUuid}`); return ok(result.data as ICoolifyApplication); } /** * Gets application logs. * * @param appUuid - Application UUID * @param options - Log retrieval options * @returns Result with logs or error */ async getApplicationLogs( appUuid: string, options: ICoolifyLogsOptions = {}, ): Promise> { log.info(`Getting logs for application ${appUuid}`); const params = new URLSearchParams(); if (options.follow) params.set("follow", "true"); if (options.tail) params.set("lines", options.tail.toString()); if (options.serviceName) params.set("service_name", options.serviceName); const endpoint = `/applications/${appUuid}/logs${params.toString() ? `?${params.toString()}` : ""}`; const result = await this.request<{ logs: string | string[] }>(endpoint); if (result.error) { log.error(`Failed to get logs: ${result.error}`); return err(new Error(result.error)); } // Coolify API returns logs as a single newline-delimited string for // docker-compose apps, but as string[] for single-container apps. const rawLogs = result.data?.logs; const logsArray: string[] = Array.isArray(rawLogs) ? rawLogs : typeof rawLogs === "string" ? rawLogs.split("\n").filter((l: string) => l.length > 0) : []; log.success(`Logs retrieved for application: ${appUuid}`); return ok({ logs: logsArray, timestamp: new Date().toISOString(), }); } /** * Executes a command on an application's running container. * * @param appUuid - Application UUID * @param command - Shell command to execute * @returns Result with command output or error */ async executeCommand( appUuid: string, command: string, ): Promise> { log.info(`Executing command on application ${appUuid}`); const result = await this.request<{ message?: string; response?: string; }>(`/applications/${appUuid}/execute-command`, { method: "POST", body: JSON.stringify({ command }), }); if (result.error) { log.error(`Failed to execute command: ${result.error}`); return err(new Error(result.error)); } log.success(`Command executed on ${appUuid}`); return ok(result.data || { message: "Command executed" }); } /** * Bulk updates environment variables for an application. * * Uses the bulk endpoint `PATCH /applications/{appUuid}/envs/bulk` which * has create-or-update semantics: if the variable exists in the override * table, its value is updated; otherwise a new override is created. This * is the only reliable way to update a variable that has been deleted * (i.e. its base value from docker-compose/git is still visible in * `getEnvironmentVariables` but no override row exists). * * @param appUuid - Application UUID * @param envVars - Array of variable definitions to upsert * @returns Result indicating success or error */ async bulkUpdateEnvironmentVariables( appUuid: string, envVars: Array<{ key: string; value: string; is_preview?: boolean; is_buildtime?: boolean; is_runtime?: boolean; }>, ): Promise> { log.info(`Bulk updating ${envVars.length} env vars for ${appUuid}`); // Coolify bulk endpoint requires { data: [...] } envelope, not a raw array. // Sending a raw array yields 400 "Bulk data is required." from the API. const result = await this.request<{ message: string }>( `/applications/${appUuid}/envs/bulk`, { method: "PATCH", body: JSON.stringify({ data: envVars }), }, ); if (result.error) { log.error(`Failed to bulk update env vars: ${result.error}`); return err(new Error(result.error)); } log.success(`Bulk updated ${envVars.length} env vars for ${appUuid}`); return ok(result.data || { message: "Environment variables updated" }); } /** * Bulk updates environment variables for a database. * * Uses the bulk endpoint `PATCH /databases/{databaseUuid}/envs/bulk` which * has create-or-update semantics. Note: the database schema is narrower * than applications — it accepts `key`, `value`, `is_literal`, * `is_multiline`, `is_shown_once` but does NOT accept `is_preview`, * `is_buildtime` or `is_runtime` (those flags are application-only). * * @param databaseUuid - Database UUID * @param envVars - Array of variable definitions to upsert * @returns Result indicating success or error */ async bulkUpdateDatabaseEnvVars( databaseUuid: string, envVars: Array<{ key: string; value: string; is_literal?: boolean; is_multiline?: boolean; is_shown_once?: boolean; }>, ): Promise> { log.info(`Bulk updating ${envVars.length} env vars for database ${databaseUuid}`); // Coolify bulk endpoint requires { data: [...] } envelope, not a raw array. // Sending a raw array yields 400 "Bulk data is required." from the API. const result = await this.request<{ message: string }>( `/databases/${databaseUuid}/envs/bulk`, { method: "PATCH", body: JSON.stringify({ data: envVars }), }, ); if (result.error) { log.error(`Failed to bulk update database env vars: ${result.error}`); return err(new Error(result.error)); } log.success(`Bulk updated ${envVars.length} env vars for database ${databaseUuid}`); return ok(result.data || { message: "Environment variables updated" }); } /** * Gets deployment history for an application. * * @param appUuid - Application UUID * @returns Result with deployment history or error */ async getApplicationDeploymentHistory( appUuid: string, ): Promise> { log.info(`Getting deployment history for ${appUuid}`); // Coolify API: /deployments/applications/{appUuid} // Response: { count: number, deployments: ICoolifyDeployment[] } const result = await this.request<{ count: number; deployments: ICoolifyDeployment[]; }>(`/deployments/applications/${appUuid}`); if (result.error) { log.error(`Failed to get deployment history: ${result.error}`); return err(new Error(result.error)); } log.success(`Deployment history retrieved for ${appUuid}`); return ok(result.data?.deployments || []); } /** * Starts a stopped application. * Note: Coolify API uses GET for application start/stop/restart. * * @param appUuid - Application UUID * @param options - Optional start options (force, instant_deploy) * @returns Result with application status or error */ async startApplication( appUuid: string, options?: { force?: boolean; instantDeploy?: boolean }, ): Promise> { log.info(`Starting application ${appUuid}`); const params = new URLSearchParams(); if (options?.force) params.set("force", "true"); if (options?.instantDeploy) params.set("instant_deploy", "true"); const queryString = params.toString(); const endpoint = `/applications/${appUuid}/start${queryString ? `?${queryString}` : ""}`; const result = await this.request(endpoint, { method: "GET", }); if (result.error) { log.error(`Failed to start application: ${result.error}`); return err(new Error(result.error)); } log.success(`Application started: ${appUuid}`); return ok(result.data as ICoolifyApplication); } /** * Stops a running application. * Note: Coolify API uses GET for application start/stop/restart. * * @param appUuid - Application UUID * @returns Result with application status or error */ async stopApplication( appUuid: string, ): Promise> { log.info(`Stopping application ${appUuid}`); const result = await this.request( `/applications/${appUuid}/stop`, { method: "GET" }, ); if (result.error) { log.error(`Failed to stop application: ${result.error}`); return err(new Error(result.error)); } log.success(`Application stopped: ${appUuid}`); return ok(result.data as ICoolifyApplication); } /** * Restarts an application. * Note: Coolify API uses GET for application start/stop/restart. * * @param appUuid - Application UUID * @returns Result with application status or error */ async restartApplication( appUuid: string, ): Promise> { log.info(`Restarting application ${appUuid}`); const result = await this.request( `/applications/${appUuid}/restart`, { method: "GET" }, ); if (result.error) { log.error(`Failed to restart application: ${result.error}`); return err(new Error(result.error)); } log.success(`Application restarted: ${appUuid}`); return ok(result.data as ICoolifyApplication); } // =========================================================================== // Version / Health // =========================================================================== /** * Gets the Coolify server version. * * @returns Result with version info or error */ async getVersion(): Promise> { log.info("Getting Coolify version"); // /version returns plain text, not JSON if (!this.baseUrl || !this.token) { return err(new Error("Coolify not configured")); } try { const baseUrl = this.baseUrl.replace(/\/+$/, ""); const url = `${baseUrl}/api/v1/version`; const response = await fetch(url, { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { return err( new Error(`HTTP ${response.status}: ${response.statusText}`), ); } const version = (await response.text()).trim(); log.success(`Coolify version: ${version}`); return ok({ version }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return err(new Error(message)); } } // =========================================================================== // Database endpoints // =========================================================================== /** * Lists all databases. * * @param page - Optional page number * @param perPage - Optional items per page * @returns Result with databases list or error */ async listDatabases( page?: number, perPage?: number, ): Promise> { log.info("Listing databases"); let endpoint = "/databases"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) endpoint += `?${params.toString()}`; const result = await this.request(endpoint); if (result.error) return err(new Error(result.error)); log.success(`Listed ${result.data?.length || 0} databases`); return ok(result.data || []); } /** * Gets details of a specific database. * * @param uuid - Database UUID * @returns Result with database details or error */ async getDatabase(uuid: string): Promise> { log.info(`Getting database ${uuid}`); const result = await this.request(`/databases/${uuid}`); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyDatabase); } /** * Creates a database of the specified type. * * @param dbType - Database type (postgresql, mysql, mariadb, mongodb, redis, keydb, clickhouse, dragonfly) * @param data - Database creation data * @returns Result with created database UUID or error */ async createDatabase( dbType: string, data: Record, ): Promise> { log.info(`Creating ${dbType} database`); const result = await this.request<{ uuid: string }>( `/databases/${dbType}`, { method: "POST", body: JSON.stringify(data), }, ); if (result.error) return err(new Error(result.error)); log.success(`Database created: ${result.data?.uuid}`); return ok(result.data!); } /** * Updates a database configuration. * * @param uuid - Database UUID * @param data - Update data * @returns Result with updated database or error */ async updateDatabase( uuid: string, data: Record, ): Promise> { log.info(`Updating database ${uuid}`); const result = await this.request(`/databases/${uuid}`, { method: "PATCH", body: JSON.stringify(data), }); if (result.error) return err(new Error(result.error)); log.success(`Database updated: ${uuid}`); return ok(result.data as ICoolifyDatabase); } /** * Deletes a database. * * @param uuid - Database UUID * @param options - Delete options for cascade deletion * @returns Result indicating success or error */ async deleteDatabase( uuid: string, options?: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; }, ): Promise> { log.info(`Deleting database ${uuid}`); const params = new URLSearchParams(); if (options?.deleteConfigurations) params.set("delete_configurations", "true"); if (options?.deleteVolumes) params.set("delete_volumes", "true"); if (options?.dockerCleanup) params.set("docker_cleanup", "true"); if (options?.deleteConnectedNetworks) params.set("delete_connected_networks", "true"); const queryString = params.toString(); const endpoint = `/databases/${uuid}${queryString ? `?${queryString}` : ""}`; const result = await this.request(endpoint, { method: "DELETE", }); if (result.error) return err(new Error(result.error)); log.success(`Database deleted: ${uuid}`); return ok({ success: true, message: "Database deleted" }); } /** * Starts a database. * * @param uuid - Database UUID * @returns Result indicating success or error */ async startDatabase( uuid: string, ): Promise> { log.info(`Starting database ${uuid}`); const result = await this.request<{ message: string }>( `/databases/${uuid}/start`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Database started: ${uuid}`); return ok(result.data || { message: "Database started" }); } /** * Stops a database. * * @param uuid - Database UUID * @returns Result indicating success or error */ async stopDatabase( uuid: string, ): Promise> { log.info(`Stopping database ${uuid}`); const result = await this.request<{ message: string }>( `/databases/${uuid}/stop`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Database stopped: ${uuid}`); return ok(result.data || { message: "Database stopped" }); } /** * Restarts a database. * * @param uuid - Database UUID * @returns Result indicating success or error */ async restartDatabase( uuid: string, ): Promise> { log.info(`Restarting database ${uuid}`); const result = await this.request<{ message: string }>( `/databases/${uuid}/restart`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Database restarted: ${uuid}`); return ok(result.data || { message: "Database restarted" }); } // =========================================================================== // Database Backup endpoints // =========================================================================== /** * Lists backups for a database. * * @param databaseUuid - Database UUID * @returns Result with backups list or error */ async listDatabaseBackups( databaseUuid: string, ): Promise> { log.info(`Listing backups for database ${databaseUuid}`); const result = await this.request( `/databases/${databaseUuid}/backups`, ); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Gets a specific database backup. * * @param databaseUuid - Database UUID * @param backupUuid - Backup UUID * @returns Result with backup details or error */ async getDatabaseBackup( databaseUuid: string, backupUuid: string, ): Promise> { const result = await this.request( `/databases/${databaseUuid}/backups/${backupUuid}`, ); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyDatabaseBackup); } /** * Creates a database backup. * * @param databaseUuid - Database UUID * @param data - Backup creation data * @returns Result with created backup or error */ async createDatabaseBackup( databaseUuid: string, data: Record, ): Promise> { log.info(`Creating backup for database ${databaseUuid}`); const result = await this.request( `/databases/${databaseUuid}/backups`, { method: "POST", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); log.success("Database backup created"); return ok(result.data as ICoolifyDatabaseBackup); } /** * Updates a database backup. * * @param databaseUuid - Database UUID * @param backupUuid - Backup UUID * @param data - Update data * @returns Result indicating success or error */ async updateDatabaseBackup( databaseUuid: string, backupUuid: string, data: Record, ): Promise> { const result = await this.request<{ message: string }>( `/databases/${databaseUuid}/backups/${backupUuid}`, { method: "PATCH", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); return ok(result.data || { message: "Backup updated" }); } /** * Deletes a database backup. * * @param databaseUuid - Database UUID * @param backupUuid - Backup UUID * @returns Result indicating success or error */ async deleteDatabaseBackup( databaseUuid: string, backupUuid: string, ): Promise> { const result = await this.request<{ message: string }>( `/databases/${databaseUuid}/backups/${backupUuid}`, { method: "DELETE" }, ); if (result.error) return err(new Error(result.error)); return ok(result.data || { message: "Backup deleted" }); } // =========================================================================== // Service endpoints // =========================================================================== /** * Lists all services. * * @param page - Optional page number * @param perPage - Optional items per page * @returns Result with services list or error */ async listServices( page?: number, perPage?: number, ): Promise> { log.info("Listing services"); let endpoint = "/services"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) endpoint += `?${params.toString()}`; const result = await this.request(endpoint); if (result.error) return err(new Error(result.error)); log.success(`Listed ${result.data?.length || 0} services`); return ok(result.data || []); } /** * Gets details of a specific service. * * @param uuid - Service UUID * @returns Result with service details or error */ async getService(uuid: string): Promise> { log.info(`Getting service ${uuid}`); const result = await this.request(`/services/${uuid}`); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyServiceType); } /** * Creates a new service. * * @param data - Service creation data * @returns Result with created service or error */ async createService( data: Record, ): Promise> { log.info("Creating service"); const result = await this.request<{ uuid: string }>("/services", { method: "POST", body: JSON.stringify(data), }); if (result.error) return err(new Error(result.error)); log.success(`Service created: ${result.data?.uuid}`); return ok(result.data!); } /** * Updates a service configuration. * * @param uuid - Service UUID * @param data - Update data * @returns Result with updated service or error */ async updateService( uuid: string, data: Record, ): Promise> { log.info(`Updating service ${uuid}`); const result = await this.request( `/services/${uuid}`, { method: "PATCH", body: JSON.stringify(data), }, ); if (result.error) return err(new Error(result.error)); log.success(`Service updated: ${uuid}`); return ok(result.data as ICoolifyServiceType); } /** * Deletes a service. * * @param uuid - Service UUID * @param options - Delete options for cascade deletion * @returns Result indicating success or error */ async deleteService( uuid: string, options?: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; }, ): Promise> { log.info(`Deleting service ${uuid}`); const params = new URLSearchParams(); if (options?.deleteConfigurations) params.set("delete_configurations", "true"); if (options?.deleteVolumes) params.set("delete_volumes", "true"); if (options?.dockerCleanup) params.set("docker_cleanup", "true"); if (options?.deleteConnectedNetworks) params.set("delete_connected_networks", "true"); const queryString = params.toString(); const endpoint = `/services/${uuid}${queryString ? `?${queryString}` : ""}`; const result = await this.request(endpoint, { method: "DELETE", }); if (result.error) return err(new Error(result.error)); log.success(`Service deleted: ${uuid}`); return ok({ success: true, message: "Service deleted" }); } /** * Gets the full infrastructure tree: Projects → Environments → Resources. * * Fetches all projects, apps, databases, and services in parallel, * then groups them by environment_id into a hierarchical tree. * * @returns Result with the full infrastructure tree or error */ async getInfrastructureTree(): Promise< Result > { log.info("Building infrastructure tree"); // Fetch all data in parallel const [projectsResult, appsResult, dbsResult, svcsResult, serversResult] = await Promise.all([ this.listProjects(), this.listApplications(), this.listDatabases(), this.listServices(), this.listServers(), ]); if (isErr(projectsResult)) return err(projectsResult.error); if (isErr(appsResult)) return err(appsResult.error); if (isErr(serversResult)) return err(serversResult.error); const projects = projectsResult.value; const apps = appsResult.value; const dbs = isErr(dbsResult) ? [] : dbsResult.value; const svcs = isErr(svcsResult) ? [] : svcsResult.value; const servers = serversResult.value; // Fetch environments for each project in parallel const envResults = await Promise.allSettled( projects.map((p) => this.getProjectEnvironments(p.uuid)), ); // Build environment_id → { projectUuid, envName, envUuid } mapping const envIdMap = new Map< number, { projectUuid: string; envName: string; envUuid: string } >(); for (let i = 0; i < projects.length; i++) { const envResult = envResults[i]; if (envResult.status === "fulfilled" && !isErr(envResult.value)) { for (const env of envResult.value.value) { envIdMap.set(env.id, { projectUuid: projects[i].uuid, envName: env.name, envUuid: env.uuid, }); } } } // Build project nodes const projectNodes: ICoolifyProjectNode[] = projects.map((p) => ({ uuid: p.uuid, name: p.name, description: p.description, environments: [], })); const projectMap = new Map(); for (const node of projectNodes) { projectMap.set(node.uuid, node); } // Populate environments from envIdMap const envNodeMap = new Map(); for (const [envId, info] of envIdMap) { const project = projectMap.get(info.projectUuid); if (!project) continue; let envNode = project.environments.find((e) => e.id === envId); if (!envNode) { envNode = { id: envId, uuid: info.envUuid, name: info.envName, resources: [], }; project.environments.push(envNode); } envNodeMap.set(envId, envNode); } // Assign apps to environments for (const app of apps) { const envNode = app.environment_id ? envNodeMap.get(app.environment_id) : undefined; const resource: ICoolifyResource = { uuid: app.uuid, name: app.name, kind: "app", status: app.status, fqdn: app.fqdn, }; if (envNode) { envNode.resources.push(resource); } } // Assign databases to environments for (const db of dbs) { const envNode = (db as any).environment_id ? envNodeMap.get((db as any).environment_id) : undefined; const resource: ICoolifyResource = { uuid: db.uuid, name: db.name, kind: "database", status: db.status, dbType: db.type, }; if (envNode) { envNode.resources.push(resource); } } // Assign services to environments for (const svc of svcs) { const envNode = (svc as any).environment_id ? envNodeMap.get((svc as any).environment_id) : undefined; const resource: ICoolifyResource = { uuid: svc.uuid, name: svc.name, kind: "service", status: svc.status, }; if (envNode) { envNode.resources.push(resource); } } // Filter out empty projects const populatedProjects = projectNodes.filter( (p) => p.environments.some((e) => e.resources.length > 0), ); // Aggregate counts const allStatuses = [ ...apps.map((a) => a.status), ...dbs.map((d) => d.status), ...svcs.map((s) => s.status), ]; const counts = { projects: populatedProjects.length, apps: apps.length, databases: dbs.length, services: svcs.length, healthy: allStatuses.filter((s) => s.includes("healthy")).length, running: allStatuses.filter( (s) => s.startsWith("running") && !s.includes("healthy"), ).length, stopped: allStatuses.filter((s) => s.includes("exited")).length, unhealthy: allStatuses.filter((s) => s.includes("unhealthy")).length, }; const server = servers[0] || { name: "Unknown" }; log.success( `Infrastructure tree built: ${counts.projects} projects, ${counts.apps} apps, ${counts.databases} dbs, ${counts.services} svcs`, ); return ok({ server: { name: server.name, ip: server.ip, uuid: server.uuid, }, projects: populatedProjects, counts, }); } /** * Starts a service. * Note: Coolify API uses GET for service start/stop/restart. * * @param uuid - Service UUID * @returns Result indicating success or error */ async startService( uuid: string, ): Promise> { log.info(`Starting service ${uuid}`); const result = await this.request<{ message: string }>( `/services/${uuid}/start`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Service started: ${uuid}`); return ok(result.data || { message: "Service started" }); } /** * Stops a service. * Note: Coolify API uses GET for service start/stop/restart. * * @param uuid - Service UUID * @returns Result indicating success or error */ async stopService(uuid: string): Promise> { log.info(`Stopping service ${uuid}`); const result = await this.request<{ message: string }>( `/services/${uuid}/stop`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Service stopped: ${uuid}`); return ok(result.data || { message: "Service stopped" }); } /** * Restarts a service. * Note: Coolify API uses GET for service start/stop/restart. * * @param uuid - Service UUID * @returns Result indicating success or error */ async restartService( uuid: string, ): Promise> { log.info(`Restarting service ${uuid}`); const result = await this.request<{ message: string }>( `/services/${uuid}/restart`, { method: "GET" }, ); if (result.error) return err(new Error(result.error)); log.success(`Service restarted: ${uuid}`); return ok(result.data || { message: "Service restarted" }); } /** * Lists environment variables for a service. * * @param uuid - Service UUID * @returns Result with env vars list or error */ async listServiceEnvVars( uuid: string, ): Promise> { const result = await this.request( `/services/${uuid}/envs`, ); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Bulk updates environment variables for a service. * * Uses the bulk endpoint `PATCH /services/{uuid}/envs/bulk` which has * create-or-update semantics: if the variable exists in the override * table, its value is updated; otherwise a new override is created. * * This is the only reliable way to update a variable that has been * deleted (i.e. its base value from docker-compose/git is still visible * in `listServiceEnvVars` but no override row exists), and the only way * to set the same key twice without the `POST /services/{uuid}/envs` * returning 409 "already exists". * * @param serviceUuid - Service UUID * @param envVars - Array of variable definitions to upsert * @returns Result indicating success or error */ async bulkUpdateServiceEnvVars( serviceUuid: string, envVars: Array<{ key: string; value: string; is_preview?: boolean; is_buildtime?: boolean; is_runtime?: boolean; }>, ): Promise> { log.info(`Bulk updating ${envVars.length} env vars for service ${serviceUuid}`); // Coolify bulk endpoint requires { data: [...] } envelope, not a raw array. // Sending a raw array yields 400 "Bulk data is required." from the API. const result = await this.request<{ message: string }>( `/services/${serviceUuid}/envs/bulk`, { method: "PATCH", body: JSON.stringify({ data: envVars }), }, ); if (result.error) { log.error(`Failed to bulk update service env vars: ${result.error}`); return err(new Error(result.error)); } log.success(`Bulk updated ${envVars.length} env vars for service ${serviceUuid}`); return ok(result.data || { message: "Environment variables updated" }); } /** * Creates an environment variable for a service. * * NOTE: Prefer `bulkUpdateServiceEnvVars` for any non-trivial use case. * This endpoint uses `POST /services/{uuid}/envs` and returns 409 * "already exists" when the key is already present as an override, * with no recovery path on the server side. Bulk has create-or-update * semantics and handles both new and existing keys reliably. * * Kept for callers that explicitly want a strict create-only operation. * * @param uuid - Service UUID * @param data - Env var data (key, value, is_preview) * @returns Result with created env var UUID or error */ async createServiceEnvVar( uuid: string, data: { key: string; value: string; is_preview?: boolean }, ): Promise> { const result = await this.request<{ uuid: string }>( `/services/${uuid}/envs`, { method: "POST", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); return ok(result.data!); } /** * Deletes an environment variable from a service. * * @param serviceUuid - Service UUID * @param key - Variable name to delete * @returns Result indicating success or error */ async deleteServiceEnvVar( serviceUuid: string, key: string, ): Promise> { log.info(`Deleting environment variable ${key} from service ${serviceUuid}`); // First get all env vars to find the UUID of the one to delete const envVarsResult = await this.listServiceEnvVars(serviceUuid); if (isErr(envVarsResult)) { return err(envVarsResult.error); } const envVar = envVarsResult.value.find((ev) => ev.key === key); if (!envVar) { log.error(`Environment variable ${key} not found on service ${serviceUuid}`); return err(new Error(`Environment variable ${key} not found`)); } const result = await this.request( `/services/${serviceUuid}/envs/${envVar.uuid}`, { method: "DELETE", }, ); if (result.error) { log.error(`Failed to delete service env var: ${result.error}`); return err(new Error(result.error)); } log.success(`Environment variable ${key} deleted from service ${serviceUuid}`); return ok(undefined); } /** * Lists environment variables for a database. * * @param databaseUuid - Database UUID * @returns Result with env vars list or error */ async listDatabaseEnvVars( databaseUuid: string, ): Promise> { log.info(`Listing env vars for database ${databaseUuid}`); const result = await this.request( `/databases/${databaseUuid}/envs`, ); if (result.error) { log.error(`Failed to list database env vars: ${result.error}`); return err(new Error(result.error)); } return ok(result.data || []); } /** * Deletes an environment variable from a database. * * @param databaseUuid - Database UUID * @param key - Variable name to delete * @returns Result indicating success or error */ async deleteDatabaseEnvVar( databaseUuid: string, key: string, ): Promise> { log.info(`Deleting environment variable ${key} from database ${databaseUuid}`); // First get all env vars to find the UUID of the one to delete const envVarsResult = await this.listDatabaseEnvVars(databaseUuid); if (isErr(envVarsResult)) { return err(envVarsResult.error); } const envVar = envVarsResult.value.find((ev) => ev.key === key); if (!envVar) { log.error(`Environment variable ${key} not found on database ${databaseUuid}`); return err(new Error(`Environment variable ${key} not found`)); } const result = await this.request( `/databases/${databaseUuid}/envs/${envVar.uuid}`, { method: "DELETE", }, ); if (result.error) { log.error(`Failed to delete database env var: ${result.error}`); return err(new Error(result.error)); } log.success(`Environment variable ${key} deleted from database ${databaseUuid}`); return ok(undefined); } // =========================================================================== // Additional Server endpoints // =========================================================================== /** * Gets resources deployed on a server. * * @param serverUuid - Server UUID * @returns Result with server resources or error */ async getServerResources( serverUuid: string, ): Promise> { log.info(`Getting resources for server ${serverUuid}`); const result = await this.request( `/servers/${serverUuid}/resources`, ); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Gets domains configured on a server. * * @param serverUuid - Server UUID * @returns Result with server domains or error */ async getServerDomains( serverUuid: string, ): Promise> { log.info(`Getting domains for server ${serverUuid}`); const result = await this.request( `/servers/${serverUuid}/domains`, ); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Validates a server connection. * * @param serverUuid - Server UUID * @returns Result with validation status or error */ async validateServer( serverUuid: string, ): Promise> { log.info(`Validating server ${serverUuid}`); const result = await this.request<{ message: string }>( `/servers/${serverUuid}/validate`, ); if (result.error) return err(new Error(result.error)); return ok(result.data || { message: "Server validated" }); } /** * Creates a new server. * * @param data - Server creation data * @returns Result with created server UUID or error */ async createServer( data: Record, ): Promise> { log.info("Creating server"); const result = await this.request<{ uuid: string }>("/servers", { method: "POST", body: JSON.stringify(data), }); if (result.error) return err(new Error(result.error)); log.success(`Server created: ${result.data?.uuid}`); return ok(result.data!); } /** * Deletes a server. * * @param serverUuid - Server UUID * @returns Result indicating success or error */ async deleteServer( serverUuid: string, ): Promise> { log.info(`Deleting server ${serverUuid}`); const result = await this.request<{ message: string }>( `/servers/${serverUuid}`, { method: "DELETE" }, ); if (result.error) return err(new Error(result.error)); log.success(`Server deleted: ${serverUuid}`); return ok(result.data || { message: "Server deleted" }); } // =========================================================================== // Additional Project endpoints // =========================================================================== /** * Updates a project. * * @param uuid - Project UUID * @param data - Update data (name, description) * @returns Result with updated project or error */ async updateProject( uuid: string, data: { name?: string; description?: string }, ): Promise> { log.info(`Updating project ${uuid}`); const result = await this.request(`/projects/${uuid}`, { method: "PATCH", body: JSON.stringify(data), }); if (result.error) return err(new Error(result.error)); log.success(`Project updated: ${uuid}`); return ok(result.data as ICoolifyProject); } /** * Deletes a project. * * @param uuid - Project UUID * @returns Result indicating success or error */ async deleteProject( uuid: string, ): Promise> { log.info(`Deleting project ${uuid}`); const result = await this.request<{ message: string }>( `/projects/${uuid}`, { method: "DELETE" }, ); if (result.error) return err(new Error(result.error)); log.success(`Project deleted: ${uuid}`); return ok(result.data || { message: "Project deleted" }); } /** * Creates a new environment within a project. * * @param projectUuid - Project UUID * @param data - Environment creation data (name, description) * @returns Result with created environment UUID or error */ async createProjectEnvironment( projectUuid: string, data: { name: string; description?: string }, ): Promise> { log.info(`Creating environment in project ${projectUuid}`); const result = await this.request<{ uuid: string }>( `/projects/${projectUuid}/environments`, { method: "POST", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); log.success("Environment created"); return ok(result.data!); } // =========================================================================== // Additional Team endpoints // =========================================================================== /** * Gets the current team. * * @returns Result with current team or error */ async getCurrentTeam(): Promise> { log.info("Getting current team"); const result = await this.request("/teams/current"); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyTeam); } /** * Gets a specific team by ID. * * @param id - Team ID * @returns Result with team details or error */ async getTeam(id: number): Promise> { log.info(`Getting team ${id}`); const result = await this.request(`/teams/${id}`); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyTeam); } /** * Gets members of a specific team. * * @param id - Team ID * @returns Result with team members or error */ async getTeamMembers( id: number, ): Promise< Result, Error> > { log.info(`Getting members for team ${id}`); const result = await this.request< Array<{ id: number; name: string; email: string }> >(`/teams/${id}/members`); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } // =========================================================================== // Deployment Control // =========================================================================== /** * Cancels a deployment. * * @param deploymentUuid - Deployment UUID * @returns Result indicating success or error */ async cancelDeployment( deploymentUuid: string, ): Promise> { log.info(`Cancelling deployment ${deploymentUuid}`); const result = await this.request<{ message: string }>( `/deployments/${deploymentUuid}/cancel`, { method: "POST" }, ); if (result.error) return err(new Error(result.error)); log.success(`Deployment cancelled: ${deploymentUuid}`); return ok(result.data || { message: "Deployment cancelled" }); } // =========================================================================== // SSH / Private Key endpoints // =========================================================================== /** * Lists all private keys. * * @returns Result with private keys list or error */ async listPrivateKeys(): Promise> { log.info("Listing private keys"); const result = await this.request("/security/keys"); if (result.error) return err(new Error(result.error)); return ok(result.data || []); } /** * Gets a specific private key. * * @param uuid - Private key UUID * @returns Result with private key details or error */ async getPrivateKey( uuid: string, ): Promise> { const result = await this.request( `/security/keys/${uuid}`, ); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyPrivateKey); } /** * Creates a new private key. * * @param data - Key creation data (name, private_key, description) * @returns Result with created key UUID or error */ async createPrivateKey(data: { name: string; private_key: string; description?: string; }): Promise> { log.info("Creating private key"); const result = await this.request<{ uuid: string }>("/security/keys", { method: "POST", body: JSON.stringify(data), }); if (result.error) return err(new Error(result.error)); log.success(`Private key created: ${result.data?.uuid}`); return ok(result.data!); } /** * Updates a private key. * * @param uuid - Private key UUID * @param data - Update data * @returns Result with updated key or error */ async updatePrivateKey( uuid: string, data: { name?: string; private_key?: string; description?: string }, ): Promise> { const result = await this.request( `/security/keys/${uuid}`, { method: "PATCH", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); return ok(result.data as ICoolifyPrivateKey); } /** * Deletes a private key. * * @param uuid - Private key UUID * @returns Result indicating success or error */ async deletePrivateKey( uuid: string, ): Promise> { log.info(`Deleting private key ${uuid}`); const result = await this.request<{ message: string }>( `/security/keys/${uuid}`, { method: "DELETE" }, ); if (result.error) return err(new Error(result.error)); log.success(`Private key deleted: ${uuid}`); return ok(result.data || { message: "Private key deleted" }); } // =========================================================================== // GitHub App endpoints (additional) // =========================================================================== /** * Creates a GitHub App configuration. * * @param data - GitHub App creation data * @returns Result with created app or error */ async createGitHubApp( data: Record, ): Promise> { log.info("Creating GitHub App"); const result = await this.request<{ id: number; uuid: string }>( "/github-apps", { method: "POST", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); return ok(result.data!); } /** * Updates a GitHub App configuration. * * @param id - GitHub App ID * @param data - Update data * @returns Result with updated app or error */ async updateGitHubApp( id: number, data: Record, ): Promise> { const result = await this.request<{ message: string }>( `/github-apps/${id}`, { method: "PATCH", body: JSON.stringify(data) }, ); if (result.error) return err(new Error(result.error)); return ok(result.data || { message: "GitHub App updated" }); } /** * Deletes a GitHub App configuration. * * @param id - GitHub App ID * @returns Result indicating success or error */ async deleteGitHubApp( id: number, ): Promise> { log.info(`Deleting GitHub App ${id}`); const result = await this.request<{ message: string }>( `/github-apps/${id}`, { method: "DELETE" }, ); if (result.error) return err(new Error(result.error)); return ok(result.data || { message: "GitHub App deleted" }); } /** * Lists all active and queued deployments. * * @param page - Optional page number for pagination * @param perPage - Optional number of items per page * @returns Result with deployments list or error */ async listDeployments( page?: number, perPage?: number, ): Promise> { log.info("Listing active deployments"); let endpoint = "/deployments"; const params = new URLSearchParams(); if (page) params.set("page", page.toString()); if (perPage) params.set("per_page", perPage.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const result = await this.request(endpoint); if (result.error) { log.error(`Failed to list deployments: ${result.error}`); return err(new Error(result.error)); } log.success(`Listed ${result.data?.length || 0} active deployments`); return ok(result.data || []); } /** * Gets detailed information about a specific deployment. * * @param deploymentUuid - Deployment UUID * @returns Result with deployment details or error */ async getDeployment( deploymentUuid: string, ): Promise> { log.info(`Getting deployment details for ${deploymentUuid}`); const result = await this.request( `/deployments/${deploymentUuid}`, ); if (result.error) { log.error(`Failed to get deployment: ${result.error}`); return err(new Error(result.error)); } log.success(`Deployment details retrieved: ${deploymentUuid}`); return ok(result.data as ICoolifyDeployment); } /** * Gets logs for a specific deployment. * * @param deploymentUuid - Deployment UUID * @returns Result with deployment status and logs or error */ async getDeploymentLogs( deploymentUuid: string, ): Promise< Result<{ status: string; logs: string; deployment_uuid: string }, Error> > { log.info(`Getting deployment logs for ${deploymentUuid}`); const result = await this.request<{ status: string; logs: string; deployment_uuid: string; }>(`/deployments/${deploymentUuid}`); if (result.error) { log.error(`Failed to get deployment logs: ${result.error}`); return err(new Error(result.error)); } log.success(`Deployment logs retrieved: ${deploymentUuid}`); return ok({ status: result.data?.status || "unknown", logs: result.data?.logs || "", deployment_uuid: result.data?.deployment_uuid || deploymentUuid, }); } /** * Gets deployment history for a specific application. * * @param appUuid - Application UUID * @param skip - Number of deployments to skip * @param take - Number of deployments to return * @returns Result with deployments list or error */ async getApplicationDeployments( appUuid: string, skip: number = 0, take: number = 10, ): Promise> { log.info(`Getting deployments for application ${appUuid}`); const params = new URLSearchParams(); if (skip > 0) params.set("skip", skip.toString()); if (take !== 10) params.set("take", take.toString()); const endpoint = `/applications/${appUuid}/deployments${params.toString() ? `?${params.toString()}` : ""}`; const result = await this.request<{ count: number; deployments: ICoolifyDeployment[]; }>(endpoint); if (result.error) { log.error(`Failed to get application deployments: ${result.error}`); return err(new Error(result.error)); } const deployments = result.data?.deployments || []; log.success(`Retrieved ${deployments.length} deployments for ${appUuid}`); return ok(deployments); } /** * Lists deployments for a specific application. * * Uses the /deployments/applications/{appUuid} endpoint which returns * all deployments (active, queued, and completed) for a single application. * This differs from listDeployments() which returns ALL deployments globally. * * @param appUuid - Application UUID * @returns Result with deployments list or error */ async listApplicationDeployments( appUuid: string, ): Promise> { log.info(`Listing deployments for application ${appUuid}`); // API returns { count: number, deployments: ICoolifyDeployment[] } const result = await this.request<{ count: number; deployments: ICoolifyDeployment[]; }>(`/deployments/applications/${appUuid}`); if (result.error) { log.error(`Failed to list application deployments: ${result.error}`); return err(new Error(result.error)); } const deployments = result.data?.deployments || []; log.success(`Listed ${deployments.length} deployments for ${appUuid}`); return ok(deployments); } // =========================================================================== // Smart Resolution Helpers // =========================================================================== /** * Gets full application details by UUID. * * Makes a direct GET request to /api/v1/applications/{uuid} which returns * complete application data including project_uuid and environment_uuid. * * @param appUuid - Application UUID * @returns Result with full application details or error */ async getApplication( appUuid: string, ): Promise> { log.info(`Getting application details: ${appUuid}`); const result = await this.request( `/applications/${appUuid}`, ); if (result.error) { return err(new Error(result.error)); } if (!result.data) { return err(new Error("Application not found")); } return ok(result.data); } /** * Resolves an application by UUID, name, or domain (FQDN). * * @param query - UUID, name, or domain to search for * @returns Result with application or error */ async resolveApplication( query: string, ): Promise> { log.info(`Resolving application: ${query}`); // If it looks like a UUID, try direct lookup first if (this.isLikelyUuid(query)) { const apps = await this.listApplications(); if (isErr(apps)) return err(apps.error); const match = apps.value.find((a) => a.uuid === query); if (match) return ok(match); } // Search by name or domain const apps = await this.listApplications(); if (isErr(apps)) return err(apps.error); const lowerQuery = query.toLowerCase(); const matches = apps.value.filter( (a) => a.name?.toLowerCase() === lowerQuery || a.fqdn?.toLowerCase().includes(lowerQuery) || a.uuid.startsWith(query), ); if (matches.length === 1) return ok(matches[0]); if (matches.length === 0) { return err( new Error( `No application found matching "${query}". Use 'list' to see available applications.`, ), ); } const names = matches.map((a) => ` - ${a.name} (${a.uuid})`).join("\n"); return err( new Error( `Multiple applications match "${query}":\n${names}\nPlease use the full UUID.`, ), ); } /** * Resolves a server by UUID, name, or IP address. * * @param query - UUID, name, or IP to search for * @returns Result with server or error */ async resolveServer(query: string): Promise> { log.info(`Resolving server: ${query}`); const servers = await this.listServers(); if (isErr(servers)) return err(servers.error); const lowerQuery = query.toLowerCase(); const match = servers.value.find( (s) => s.uuid === query || s.name?.toLowerCase() === lowerQuery || s.ip === query || s.uuid.startsWith(query), ); if (match) return ok(match); return err( new Error( `No server found matching "${query}". Use 'servers' to see available servers.`, ), ); } /** * Checks if a string looks like a UUID (Coolify or standard format). * * @param query - String to check * @returns true if it looks like a UUID */ private isLikelyUuid(query: string): boolean { if (/^[a-z0-9]{20,}$/i.test(query)) return true; if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( query, ) ) return true; return false; } // =========================================================================== // Diagnostics // =========================================================================== /** * Diagnoses an application by aggregating health, logs, deployments, and env vars. * * @param query - Application UUID, name, or domain * @returns Result with diagnostic report or error */ async diagnoseApplication(query: string): Promise< Result< { application: ICoolifyApplication; recentDeployments: ICoolifyDeployment[]; envVarCount: number; recentLogs: string[]; issues: string[]; }, Error > > { log.info(`Diagnosing application: ${query}`); const appResult = await this.resolveApplication(query); if (isErr(appResult)) return err(appResult.error); const app = appResult.value; const issues: string[] = []; // Gather data in parallel const [deploymentsResult, envResult, logsResult] = await Promise.all([ this.getApplicationDeploymentHistory(app.uuid), this.getEnvironmentVariables(app.uuid), this.getApplicationLogs(app.uuid, { tail: 20 }), ]); const deployments = isErr(deploymentsResult) ? [] : deploymentsResult.value; const envVarCount = isErr(envResult) ? 0 : envResult.value.length; const recentLogs = isErr(logsResult) ? [] : logsResult.value.logs; // Check for issues if (app.status?.includes("stopped")) { issues.push("Application is stopped"); } if (!app.fqdn) { issues.push("No domain configured"); } if (envVarCount === 0) { issues.push("No environment variables set"); } const recentDeploys = deployments.slice(-5); const failedDeploys = recentDeploys.filter((d) => d.status?.includes("failed"), ); if (failedDeploys.length > 0) { issues.push( `${failedDeploys.length} of last ${recentDeploys.length} deployments failed`, ); } log.success(`Diagnosis complete for ${app.name}`); return ok({ application: app, recentDeployments: recentDeploys, envVarCount, recentLogs, issues, }); } /** * Diagnoses a server by aggregating health, resources, and domains. * * @param query - Server UUID, name, or IP * @returns Result with diagnostic report or error */ async diagnoseServer(query: string): Promise< Result< { server: ICoolifyServer; resources: ICoolifyServerResource[]; domains: ICoolifyServerDomain[]; issues: string[]; }, Error > > { log.info(`Diagnosing server: ${query}`); const serverResult = await this.resolveServer(query); if (isErr(serverResult)) return err(serverResult.error); const server = serverResult.value; const issues: string[] = []; const [resourcesResult, domainsResult] = await Promise.all([ this.getServerResources(server.uuid), this.getServerDomains(server.uuid), ]); const resources = isErr(resourcesResult) ? [] : resourcesResult.value; const domains = isErr(domainsResult) ? [] : domainsResult.value; if (!server.is_reachable) { issues.push("Server is not reachable"); } if (!server.is_usable) { issues.push("Server is not usable"); } if (resources.length === 0) { issues.push("No resources deployed on this server"); } log.success(`Server diagnosis complete for ${server.name}`); return ok({ server, resources, domains, issues }); } /** * Scans all infrastructure for potential issues. * * @returns Result with issues report or error */ async findInfrastructureIssues(): Promise< Result< { totalServers: number; totalApps: number; totalDatabases: number; totalServices: number; issues: Array<{ type: string; resource: string; uuid: string; message: string; }>; }, Error > > { log.info("Scanning infrastructure for issues"); const [serversR, appsR, dbsR, svcsR] = await Promise.all([ this.listServers(), this.listApplications(), this.listDatabases(), this.listServices(), ]); const servers = isErr(serversR) ? [] : serversR.value; const apps = isErr(appsR) ? [] : appsR.value; const dbs = isErr(dbsR) ? [] : dbsR.value; const svcs = isErr(svcsR) ? [] : svcsR.value; const issues: Array<{ type: string; resource: string; uuid: string; message: string; }> = []; for (const server of servers) { if (!server.is_reachable) { issues.push({ type: "server", resource: server.name, uuid: server.uuid, message: "Server unreachable", }); } } for (const app of apps) { if (app.status?.includes("stopped")) { issues.push({ type: "application", resource: app.name, uuid: app.uuid, message: "Application stopped", }); } if (app.status?.includes("failed") || app.status?.includes("error")) { issues.push({ type: "application", resource: app.name, uuid: app.uuid, message: `Status: ${app.status}`, }); } } for (const db of dbs) { if (db.status?.includes("stopped") || db.status?.includes("exited")) { issues.push({ type: "database", resource: db.name, uuid: db.uuid, message: `Database stopped: ${db.status}`, }); } } for (const svc of svcs) { if (svc.status?.includes("stopped") || svc.status?.includes("exited")) { issues.push({ type: "service", resource: svc.name, uuid: svc.uuid, message: `Service stopped: ${svc.status}`, }); } } log.success( `Infrastructure scan complete: ${issues.length} issue(s) found`, ); return ok({ totalServers: servers.length, totalApps: apps.length, totalDatabases: dbs.length, totalServices: svcs.length, issues, }); } // =========================================================================== // Batch Operations // =========================================================================== /** * Restarts all applications in a project. * * @param projectUuid - Project UUID * @returns Result with batch operation results */ async restartProjectApps( projectUuid: string, ): Promise< Result<{ total: number; succeeded: number; failed: string[] }, Error> > { log.info(`Restarting all apps in project ${projectUuid}`); const appsResult = await this.listApplications(undefined, projectUuid); if (isErr(appsResult)) return err(appsResult.error); const apps = appsResult.value; const failed: string[] = []; let succeeded = 0; for (const app of apps) { const result = await this.restartApplication(app.uuid); if (isErr(result)) { failed.push(`${app.name} (${app.uuid}): ${result.error.message}`); } else { succeeded++; } } log.success(`Restarted ${succeeded}/${apps.length} apps in project`); return ok({ total: apps.length, succeeded, failed }); } /** * Redeploys all applications in a project. * * @param projectUuid - Project UUID * @param force - Force rebuild * @returns Result with batch operation results */ async redeployProjectApps( projectUuid: string, force: boolean = false, ): Promise< Result<{ total: number; succeeded: number; failed: string[] }, Error> > { log.info(`Redeploying all apps in project ${projectUuid}`); const appsResult = await this.listApplications(undefined, projectUuid); if (isErr(appsResult)) return err(appsResult.error); const apps = appsResult.value; const failed: string[] = []; let succeeded = 0; for (const app of apps) { const result = await this.deploy({ uuid: app.uuid, force }); if (isErr(result)) { failed.push(`${app.name} (${app.uuid}): ${result.error.message}`); } else { succeeded++; } } log.success(`Redeployed ${succeeded}/${apps.length} apps in project`); return ok({ total: apps.length, succeeded, failed }); } /** * Stops all running applications. * * @returns Result with batch operation results */ async stopAllApps(): Promise< Result<{ total: number; succeeded: number; failed: string[] }, Error> > { log.info("Stopping all applications"); const appsResult = await this.listApplications(); if (isErr(appsResult)) return err(appsResult.error); const running = appsResult.value.filter( (a) => !a.status?.includes("stopped"), ); const failed: string[] = []; let succeeded = 0; for (const app of running) { const result = await this.stopApplication(app.uuid); if (isErr(result)) { failed.push(`${app.name} (${app.uuid}): ${result.error.message}`); } else { succeeded++; } } log.success(`Stopped ${succeeded}/${running.length} apps`); return ok({ total: running.length, succeeded, failed }); } // =========================================================================== // Summary Types (Token Optimization for MCP) // =========================================================================== /** * Lists applications with minimal fields for token efficiency. * * @returns Result with application summaries or error */ async listApplicationSummaries(): Promise< Result< Array<{ uuid: string; name: string; status: string; fqdn: string | null; }>, Error > > { const result = await this.listApplications(); if (isErr(result)) return err(result.error); return ok( result.value.map((a) => ({ uuid: a.uuid, name: a.name, status: a.status, fqdn: a.fqdn || null, })), ); } /** * Lists servers with minimal fields for token efficiency. * * @returns Result with server summaries or error */ async listServerSummaries(): Promise< Result< Array<{ uuid: string; name: string; ip: string; is_reachable: boolean; }>, Error > > { const result = await this.listServers(); if (isErr(result)) return err(result.error); return ok( result.value.map((s) => ({ uuid: s.uuid, name: s.name, ip: s.ip || "", is_reachable: s.is_reachable || false, })), ); } /** * Lists databases with minimal fields for token efficiency. * * @returns Result with database summaries or error */ async listDatabaseSummaries(): Promise< Result< Array<{ uuid: string; name: string; type: string; status: string; }>, Error > > { const result = await this.listDatabases(); if (isErr(result)) return err(result.error); return ok( result.value.map((d) => ({ uuid: d.uuid, name: d.name, type: d.type, status: d.status, })), ); } /** * Lists services with minimal fields for token efficiency. * * @returns Result with service summaries or error */ async listServiceSummaries(): Promise< Result< Array<{ uuid: string; name: string; type: string; status: string; }>, Error > > { const result = await this.listServices(); if (isErr(result)) return err(result.error); return ok( result.value.map((s) => ({ uuid: s.uuid, name: s.name, type: s.type, status: s.status, })), ); } } let instance: CoolifyService | null = null; /** * Gets the singleton CoolifyService instance. * * @returns The CoolifyService instance */ export function getCoolifyService(): CoolifyService { if (!instance) { instance = new CoolifyService(); } return instance; } // Re-export types export type { ICoolifyServer, ICoolifyServerResource, ICoolifyServerDomain, ICoolifyDestination, ICoolifyProject, ICoolifyTeam, ICoolifyApplication, ICoolifyDatabase, ICoolifyDatabaseBackup, ICoolifyService as ICoolifyServiceType, ICoolifyPrivateKey, ICoolifyDeployment, ICoolifyVersion, ICoolifyAppOptions, ICoolifyAppResult, ICoolifyDeployOptions, ICoolifyDeployResult, ICoolifyDeleteResult, ICoolifyUpdateOptions, ICoolifyLogsOptions, ICoolifyLogs, IProgressCallback, ICoolifyInfrastructureTree, ICoolifyProjectNode, ICoolifyEnvironmentNode, ICoolifyResource, } from "./types.js";