/** * Coolify SDK — Fluent, resource-based API for Coolify. * * Every parameter from CoolifyService is exposed. Nothing is lost. * * @module */ import { isErr, type Result } from "@mks2508/no-throw"; import { CoolifyService } from "./coolify/index.js"; import type { ICoolifyAppOptions, ICoolifyApplication, ICoolifyDatabase, ICoolifyDatabaseBackup, ICoolifyDeleteResult, ICoolifyDeployment, ICoolifyDeployResult, ICoolifyDestination, ICoolifyEnvironment, ICoolifyLogs, ICoolifyLogsOptions, ICoolifyPrivateKey, ICoolifyProject, ICoolifyServer, ICoolifyServerDomain, ICoolifyServerResource, ICoolifyService as ICoolifyServiceType, ICoolifyTeam, ICoolifyUpdateOptions, ICoolifyVersion, IProgressCallback, } from "./coolify/types.js"; import type { ICoolifyEnvVar } from "./coolify/index.js"; import { parseEnvContent } from "./utils/env-parser.js"; /** SDK configuration options. */ export interface ICoolifyOptions { /** Coolify instance URL (e.g., https://coolify.example.com) */ url: string; /** API token (generate in Coolify > Settings > API Tokens) */ token: string; } /** Cascade delete options. */ export interface IDeleteOptions { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; } /** Pagination options. */ export interface IPaginationOptions { page?: number; perPage?: number; } function unwrap(result: Result): T { if (isErr(result)) throw result.error; return result.value; } // ─── Resource Classes ──────────────────────────────────────────────────────── class ApplicationsResource { constructor(private svc: CoolifyService) {} /** List all applications. Supports filtering and pagination. */ async list( options?: { teamId?: string; projectId?: string } & IPaginationOptions, ): Promise { return unwrap( await this.svc.listApplications( options?.teamId, options?.projectId, options?.page, options?.perPage, ), ); } async listSummaries() { return unwrap(await this.svc.listApplicationSummaries()); } /** Get a single application by UUID with full details (including settings and watch_paths). */ async get(uuid: string): Promise { return unwrap(await this.svc.getApplication(uuid)); } /** Resolve application by name, domain, or UUID. */ async resolve(query: string): Promise { return unwrap(await this.svc.resolveApplication(query)); } /** Create a new application. All ICoolifyAppOptions supported. */ async create(options: ICoolifyAppOptions, onProgress?: IProgressCallback) { return unwrap(await this.svc.createApplication(options, onProgress)); } /** Deploy an application. Supports force rebuild and progress tracking. */ async deploy( uuid: string, options?: { force?: boolean; tag?: string }, onProgress?: IProgressCallback, ): Promise { return unwrap(await this.svc.deploy({ uuid, ...options }, onProgress)); } /** Start application. Supports force and instant_deploy. */ async start( uuid: string, options?: { force?: boolean; instantDeploy?: boolean }, ): Promise { return unwrap(await this.svc.startApplication(uuid, options)); } async stop(uuid: string): Promise { return unwrap(await this.svc.stopApplication(uuid)); } async restart(uuid: string): Promise { return unwrap(await this.svc.restartApplication(uuid)); } /** Delete application with optional cascade options. */ async delete( uuid: string, options?: IDeleteOptions, ): Promise { return unwrap(await this.svc.deleteApplication(uuid, options)); } /** Update application configuration. All ICoolifyUpdateOptions supported. */ async update( uuid: string, options: ICoolifyUpdateOptions, ): Promise { return unwrap(await this.svc.updateApplication(uuid, options)); } /** Get application logs. Supports lines, serviceName, follow. */ async logs( uuid: string, options?: ICoolifyLogsOptions, ): Promise { return unwrap(await this.svc.getApplicationLogs(uuid, options)); } /** Get deployment history with optional pagination. */ async deployments( uuid: string, skip?: number, take?: number, ): Promise { return unwrap(await this.svc.getApplicationDeployments(uuid, skip, take)); } async exec(uuid: string, command: string) { return unwrap(await this.svc.executeCommand(uuid, command)); } async envVars(uuid: string): Promise { return unwrap(await this.svc.getEnvironmentVariables(uuid)); } async setEnv( uuid: string, key: string, value: string, isBuildTime?: boolean, ) { return unwrap( await this.svc.setEnvironmentVariable(uuid, key, value, isBuildTime), ); } async bulkSetEnv( uuid: string, vars: Array<{ key: string; value: string; is_preview?: boolean }>, ) { return unwrap(await this.svc.bulkUpdateEnvironmentVariables(uuid, vars)); } async deleteEnv(uuid: string, key: string) { return unwrap(await this.svc.deleteEnvironmentVariable(uuid, key)); } /** * Sync environment variables from a local .env file to Coolify. * * @param uuid - Application UUID * @param options - Sync options * @returns Sync result with changes applied */ async syncEnv( uuid: string, options: { /** Path to .env file (default: reads from .env in cwd) */ filePath?: string; /** Preview changes without applying */ dryRun?: boolean; /** Delete vars not in file */ prune?: boolean; /** Callback for progress updates (optional) */ onProgress?: (update: { type: 'add' | 'update' | 'remove'; key: string; value?: string; }) => void; } = {}, ): Promise<{ added: Array<{ key: string; value: string }>; updated: Array<{ key: string; value: string; oldValue: string }>; removed: string[]; skipped: number; }> { const { filePath, dryRun = false, prune = false, onProgress } = options; // 1. Read and parse .env file (absolute path resolution + Node fs fallback) let envContent: string; const { readFileSync } = await import('node:fs'); const { resolve, isAbsolute } = await import('node:path'); const target = filePath || '.env'; const absoluteTarget = isAbsolute(target) ? target : resolve(process.cwd(), target); try { // Prefer Node fs (more portable across Bun versions + clearer errors than Bun.file()). envContent = readFileSync(absoluteTarget, 'utf-8'); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error( `Cannot read env file at ${absoluteTarget} (resolved from ${target}): ${msg}`, ); } const localVars = this.parseEnvContent(envContent); if (localVars.size === 0) { return { added: [], updated: [], removed: [], skipped: 0 }; } // 2. Get current vars from Coolify const currentVarsList = await this.envVars(uuid); const currentVars = new Map( currentVarsList.map((v) => [v.key, v.value]), ); // 3. Calculate changes const toAdd: Array<{ key: string; value: string }> = []; const toUpdate: Array<{ key: string; value: string; oldValue: string; }> = []; const toRemove: string[] = []; for (const [key, value] of localVars.entries()) { const currentValue = currentVars.get(key); if (!currentValue) { toAdd.push({ key, value }); } else if (currentValue !== value) { toUpdate.push({ key, value, oldValue: currentValue }); } } if (prune) { for (const key of currentVars.keys()) { if (!localVars.has(key)) { toRemove.push(key); } } } // 4. Apply changes (unless dry-run) if (!dryRun) { // Add new variables for (const { key, value } of toAdd) { await this.setEnv(uuid, key, value, false); onProgress?.({ type: 'add', key, value }); } // Update existing variables for (const { key, value } of toUpdate) { await this.setEnv(uuid, key, value, false); onProgress?.({ type: 'update', key, value }); } // Remove pruned variables for (const key of toRemove) { await this.deleteEnv(uuid, key); onProgress?.({ type: 'remove', key }); } } else { // Report what would happen in dry-run for (const { key, value } of toAdd) { onProgress?.({ type: 'add', key, value }); } for (const { key, value } of toUpdate) { onProgress?.({ type: 'update', key, value }); } for (const key of toRemove) { onProgress?.({ type: 'remove', key }); } } return { added: toAdd, updated: toUpdate, removed: toRemove, skipped: currentVars.size - toUpdate.length - toRemove.length, }; } /** * Parse .env file content into a Map. * Delegates to the shared utility in `./utils/env-parser.js` so the SDK * and CLI use a single implementation. * * @param content - The .env file content * @returns Map of environment variables */ private parseEnvContent(content: string): Map { return parseEnvContent(content); } } class DatabasesResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions): Promise { return unwrap( await this.svc.listDatabases(options?.page, options?.perPage), ); } async listSummaries() { return unwrap(await this.svc.listDatabaseSummaries()); } async get(uuid: string): Promise { return unwrap(await this.svc.getDatabase(uuid)); } async create(type: string, data: Record) { return unwrap(await this.svc.createDatabase(type, data)); } async update(uuid: string, data: Record) { return unwrap(await this.svc.updateDatabase(uuid, data)); } async start(uuid: string) { return unwrap(await this.svc.startDatabase(uuid)); } async stop(uuid: string) { return unwrap(await this.svc.stopDatabase(uuid)); } async restart(uuid: string) { return unwrap(await this.svc.restartDatabase(uuid)); } /** Delete with optional cascade options. */ async delete(uuid: string, options?: IDeleteOptions) { return unwrap(await this.svc.deleteDatabase(uuid, options)); } async backups(uuid: string): Promise { return unwrap(await this.svc.listDatabaseBackups(uuid)); } async getBackup(dbUuid: string, backupUuid: string) { return unwrap(await this.svc.getDatabaseBackup(dbUuid, backupUuid)); } async createBackup(dbUuid: string, data: Record) { return unwrap(await this.svc.createDatabaseBackup(dbUuid, data)); } async deleteBackup(dbUuid: string, backupUuid: string) { return unwrap(await this.svc.deleteDatabaseBackup(dbUuid, backupUuid)); } /** List env vars for a database. */ async envVars(uuid: string): Promise { return unwrap(await this.svc.listDatabaseEnvVars(uuid)); } /** * Bulk set (create-or-update) env vars for a database. * * Note: the database schema is narrower than applications — only * `is_literal`, `is_multiline`, `is_shown_once` are accepted. No * `is_preview` / `is_buildtime` / `is_runtime`. */ async bulkSetEnv( uuid: string, vars: Array<{ key: string; value: string; is_literal?: boolean; is_multiline?: boolean; is_shown_once?: boolean; }>, ) { return unwrap(await this.svc.bulkUpdateDatabaseEnvVars(uuid, vars)); } /** Delete a database env var by key. Resolves key → UUID, then DELETE. */ async deleteEnv(uuid: string, key: string) { return unwrap(await this.svc.deleteDatabaseEnvVar(uuid, key)); } } class ServicesResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions): Promise { return unwrap(await this.svc.listServices(options?.page, options?.perPage)); } async listSummaries() { return unwrap(await this.svc.listServiceSummaries()); } async get(uuid: string): Promise { return unwrap(await this.svc.getService(uuid)); } async create(data: Record) { return unwrap(await this.svc.createService(data)); } async update(uuid: string, data: Record) { return unwrap(await this.svc.updateService(uuid, data)); } async start(uuid: string) { return unwrap(await this.svc.startService(uuid)); } async stop(uuid: string) { return unwrap(await this.svc.stopService(uuid)); } async restart(uuid: string) { return unwrap(await this.svc.restartService(uuid)); } /** Delete with optional cascade options. */ async delete(uuid: string, options?: IDeleteOptions) { return unwrap(await this.svc.deleteService(uuid, options)); } async envVars(uuid: string): Promise { return unwrap(await this.svc.listServiceEnvVars(uuid)); } /** * Sets (creates or updates) a single env var for a service. * * Delegates to the bulk endpoint `PATCH /services/{uuid}/envs/bulk`, * which has create-or-update semantics — calling this for the same key * a second time updates the override value rather than failing with * 409 "already exists" (which is what the raw `POST /services/{uuid}/envs` * endpoint does). Mirrors `ApplicationsResource.setEnv`. * * @param uuid - Service UUID * @param data - Env var data (key, value, is_preview) */ async setEnv( uuid: string, data: { key: string; value: string; is_preview?: boolean }, ) { return unwrap(await this.svc.bulkUpdateServiceEnvVars(uuid, [data])); } /** * Bulk set (create-or-update) env vars for a service. * Mirrors `ApplicationsResource.bulkSetEnv`. * * @param uuid - Service UUID * @param vars - Array of env var definitions to upsert */ async bulkSetEnv( uuid: string, vars: Array<{ key: string; value: string; is_preview?: boolean; is_buildtime?: boolean; is_runtime?: boolean; }>, ) { return unwrap(await this.svc.bulkUpdateServiceEnvVars(uuid, vars)); } /** Delete a service env var by key. Resolves key → UUID, then DELETE. */ async deleteEnv(uuid: string, key: string) { return unwrap(await this.svc.deleteServiceEnvVar(uuid, key)); } } class ServersResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions): Promise { return unwrap(await this.svc.listServers(options?.page, options?.perPage)); } async listSummaries() { return unwrap(await this.svc.listServerSummaries()); } async resolve(query: string): Promise { return unwrap(await this.svc.resolveServer(query)); } async get(uuid: string): Promise { return unwrap(await this.svc.getServer(uuid)); } async create(data: Record) { return unwrap(await this.svc.createServer(data)); } async delete(uuid: string) { return unwrap(await this.svc.deleteServer(uuid)); } async resources(uuid: string): Promise { return unwrap(await this.svc.getServerResources(uuid)); } async domains(uuid: string): Promise { return unwrap(await this.svc.getServerDomains(uuid)); } async destinations(uuid: string): Promise { return unwrap(await this.svc.getServerDestinations(uuid)); } async validate(uuid: string) { return unwrap(await this.svc.validateServer(uuid)); } } class ProjectsResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions): Promise { return unwrap(await this.svc.listProjects(options?.page, options?.perPage)); } async create(name: string, description?: string): Promise { return unwrap(await this.svc.createProject(name, description)); } async update(uuid: string, data: { name?: string; description?: string }) { return unwrap(await this.svc.updateProject(uuid, data)); } async delete(uuid: string) { return unwrap(await this.svc.deleteProject(uuid)); } async environments(projectUuid: string): Promise { return unwrap(await this.svc.getProjectEnvironments(projectUuid)); } async createEnvironment( projectUuid: string, data: { name: string; description?: string }, ) { return unwrap(await this.svc.createProjectEnvironment(projectUuid, data)); } } class TeamsResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions): Promise { return unwrap(await this.svc.listTeams(options?.page, options?.perPage)); } async current(): Promise { return unwrap(await this.svc.getCurrentTeam()); } async get(id: number): Promise { return unwrap(await this.svc.getTeam(id)); } async members(teamId: number) { return unwrap(await this.svc.getTeamMembers(teamId)); } } class KeysResource { constructor(private svc: CoolifyService) {} async list(): Promise { return unwrap(await this.svc.listPrivateKeys()); } async get(uuid: string): Promise { return unwrap(await this.svc.getPrivateKey(uuid)); } async create(data: { name: string; private_key: string; description?: string; }) { return unwrap(await this.svc.createPrivateKey(data)); } async update( uuid: string, data: { name?: string; private_key?: string; description?: string }, ) { return unwrap(await this.svc.updatePrivateKey(uuid, data)); } async delete(uuid: string) { return unwrap(await this.svc.deletePrivateKey(uuid)); } } class DeploymentsResource { constructor(private svc: CoolifyService) {} async active(options?: IPaginationOptions): Promise { return unwrap( await this.svc.listDeployments(options?.page, options?.perPage), ); } async get(uuid: string): Promise { return unwrap(await this.svc.getDeployment(uuid)); } /** Get deployment with build logs. */ async logs(uuid: string) { return unwrap(await this.svc.getDeploymentLogs(uuid)); } async cancel(uuid: string) { return unwrap(await this.svc.cancelDeployment(uuid)); } } class DiagnoseResource { constructor(private svc: CoolifyService) {} async app(query: string) { return unwrap(await this.svc.diagnoseApplication(query)); } async server(query: string) { return unwrap(await this.svc.diagnoseServer(query)); } async infrastructure() { return unwrap(await this.svc.findInfrastructureIssues()); } /** Analyze a failed deployment — extracts errors from build logs. */ async deployFailure(deploymentUuid: string) { const { analyzeDeployFailure } = await import("./network.js"); return unwrap(await analyzeDeployFailure(this.svc, deploymentUuid)); } } /** * Network diagnostics — inspect Docker networks, proxy, DNS, connectivity. */ class NetworkResource { constructor(private svc: CoolifyService) {} /** * Inspect container network environment. * * @param appUuid - Application UUID * @param servicesToTest - Service names to test connectivity (e.g., ['db', 'redis']) */ async inspect(appUuid: string, servicesToTest?: string[]) { const { inspectNetwork } = await import("./network.js"); return unwrap(await inspectNetwork(this.svc, appUuid, servicesToTest)); } } class BatchResource { constructor(private svc: CoolifyService) {} async restartProject(projectUuid: string) { return unwrap(await this.svc.restartProjectApps(projectUuid)); } async redeployProject(projectUuid: string, force?: boolean) { return unwrap(await this.svc.redeployProjectApps(projectUuid, force)); } async stopAll() { return unwrap(await this.svc.stopAllApps()); } } class GitHubAppsResource { constructor(private svc: CoolifyService) {} async list(options?: IPaginationOptions) { return unwrap( await this.svc.listGithubApps(options?.page, options?.perPage), ); } async create(data: Record) { return unwrap(await this.svc.createGitHubApp(data)); } async update(id: number, data: Record) { return unwrap(await this.svc.updateGitHubApp(id, data)); } async delete(id: number) { return unwrap(await this.svc.deleteGitHubApp(id)); } } // ─── Main SDK Class ────────────────────────────────────────────────────────── /** * Coolify SDK — The main entry point for interacting with a Coolify instance. * * Every parameter from the internal CoolifyService is exposed. * Nothing is lost — progress callbacks, pagination, cascade deletes, all supported. */ export class Coolify { /** @internal */ readonly svc: CoolifyService; private initialized = false; readonly applications: ApplicationsResource; readonly databases: DatabasesResource; readonly services: ServicesResource; readonly servers: ServersResource; readonly projects: ProjectsResource; readonly teams: TeamsResource; readonly keys: KeysResource; readonly deployments: DeploymentsResource; readonly diagnose: DiagnoseResource; readonly network: NetworkResource; readonly batch: BatchResource; readonly githubApps: GitHubAppsResource; constructor(options?: ICoolifyOptions) { this.svc = new CoolifyService(); if (options) { process.env.COOLIFY_URL = options.url; process.env.COOLIFY_TOKEN = options.token; } const autoInit = (resource: T): T => { return new Proxy(resource, { get: (target, prop, receiver) => { const value = Reflect.get(target, prop, receiver); if (typeof value !== "function") return value; return async (...args: unknown[]) => { await this.ensureInit(); return (value as Function).apply(target, args); }; }, }); }; this.applications = autoInit(new ApplicationsResource(this.svc)); this.databases = autoInit(new DatabasesResource(this.svc)); this.services = autoInit(new ServicesResource(this.svc)); this.servers = autoInit(new ServersResource(this.svc)); this.projects = autoInit(new ProjectsResource(this.svc)); this.teams = autoInit(new TeamsResource(this.svc)); this.keys = autoInit(new KeysResource(this.svc)); this.deployments = autoInit(new DeploymentsResource(this.svc)); this.diagnose = autoInit(new DiagnoseResource(this.svc)); this.network = autoInit(new NetworkResource(this.svc)); this.batch = autoInit(new BatchResource(this.svc)); this.githubApps = autoInit(new GitHubAppsResource(this.svc)); } static fromEnv(): Coolify { return new Coolify(); } private async ensureInit(): Promise { if (this.initialized) return; const initResult = await this.svc.init(); if (isErr(initResult)) { throw new Error(`Coolify connection failed: ${initResult.error.message}`); } this.initialized = true; } async version(): Promise { await this.ensureInit(); return unwrap(await this.svc.getVersion()); } }