/** * Init command - Link existing Coolify app or create new one. * * @module */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { execSync } from "node:child_process"; import prompts from "prompts"; import * as p from "@clack/prompts"; import chalk from "chalk"; import ora, { Ora } from "ora"; import { isOk, isErr } from "@mks2508/no-throw"; import { getCoolifyService } from "../../coolify/index.js"; import type { ICoolifyGithubApp } from "../../coolify/types.js"; import { writeCoolifyState, loadCoolifyState, type ICoolifyDeployState } from "../coolify-state.js"; import { createSpinner, createStatusIndicator, } from "../ui/spinners.js"; import { createEnvTable, createSummaryCard, } from "../ui/index.js"; /** * TTY detection — guards interactive @clack/prompts usage. */ const isTTY = process.stdout.isTTY === true; interface IInitOptions { yes?: boolean; force?: boolean; name?: string; link?: boolean; } interface IGitInfo { repoUrl: string; branch: string; name: string; owner: string; repo: string; } interface IDockerConfigs { hasDockerfile: boolean; hasCompose: boolean; dockerfilePath?: string; composePath?: string; /** Ports detected from Dockerfile EXPOSE directives */ detectedPorts?: number[]; } interface IExistingApps { exact: Array<{ uuid: string; name: string; project: string; projectName: string; status: string; branch: string; fqdn?: string; }>; partial: Array<{ uuid: string; name: string; project: string; projectName: string; status: string; branch: string; fqdn?: string; }>; } /** * Init command handler with enhanced UX/UI. */ export async function initCommand(options: IInitOptions): Promise { console.log(""); console.log(chalk.bold.cyan("┌─────────────────────────────────────────────┐")); console.log(chalk.bold.cyan("│") + chalk.bold.white(" 🐳 Coolify Init") + chalk.bold.cyan(" │")); console.log(chalk.bold.cyan("│") + chalk.gray(" Link existing app or create new deployment") + " │"); console.log(chalk.bold.cyan("└─────────────────────────────────────────────┘")); console.log(""); // 1. Check for existing .coolify.json const existingState = loadCoolifyState(); if (existingState && !options.force) { console.log( chalk.yellow(" ⚠ Existing configuration found:"), chalk.gray(`.coolify.json`), ); console.log(""); console.log(chalk.gray(" App: ") + chalk.cyan(existingState.appUuid.slice(0, 8)) + chalk.gray("...")); console.log(""); if (!options.yes) { const confirm = await prompts({ type: "confirm", name: "value", message: chalk.yellow("Overwrite existing configuration?"), initial: false, }); if (!confirm.value) { console.log(""); console.log(chalk.gray(" ✓ Keeping existing configuration")); console.log(""); return; } } } // 2. Detect Git info with enhanced display const gitSpinner = createSpinner({ text: "Detecting Git repository...", color: "cyan", }).start(); const gitInfo = detectGitInfo(process.cwd()); if (!gitInfo) { gitSpinner.fail("Not a Git repository"); console.error(chalk.red(" ✗ Not in a Git repository")); console.error(chalk.gray(" Run: ") + chalk.cyan("git init")); console.error(""); return; } gitSpinner.succeed("Git repository detected"); // Display Git info beautifully console.log(""); console.log(chalk.gray(" ") + chalk.bold("Repository:")); console.log( " " + chalk.cyan("📦") + chalk.gray(" URL: ") + chalk.white(gitInfo.repoUrl), ); console.log( " " + chalk.cyan("🌿") + chalk.gray(" Branch: ") + chalk.green(gitInfo.branch), ); console.log( " " + chalk.cyan("👤") + chalk.gray(" Owner: ") + chalk.white(gitInfo.owner), ); console.log(""); // 3. Detect Docker configs const dockerSpinner = createSpinner({ text: "Scanning for Docker configurations...", color: "blue", }).start(); const dockerConfigs = detectDockerConfigs(process.cwd()); dockerSpinner.stop(); // Display Docker status const dockerfileStatus = dockerConfigs.hasDockerfile ? chalk.green("✓ Found") : chalk.red("✗ Missing"); const composeStatus = dockerConfigs.hasCompose ? chalk.green("✓ Found") : chalk.red("✗ Missing"); console.log(chalk.gray(" ") + chalk.bold("Docker Configuration:")); console.log(" " + dockerfileStatus + chalk.gray(" Dockerfile")); console.log(" " + composeStatus + chalk.gray(" docker-compose.yml")); console.log(""); // 4. If no Docker configs and not in link mode, offer scaffolder if (!dockerConfigs.hasDockerfile && !dockerConfigs.hasCompose && !options.link) { console.log(chalk.yellow(" ⚠ No Docker configuration found")); console.log(""); if (!options.yes) { const scaffolder = await prompts({ type: "confirm", name: "value", message: chalk.cyan("Generate Docker configs with create-bunspace?"), initial: true, }); if (scaffolder.value) { console.log(""); console.log(chalk.gray(" Run this command:")); console.log( " " + chalk.cyan("create-bunspace coolify init") + chalk.gray(" # Generate Dockerfile + docker-compose.yml"), ); console.log(""); return; } } } // 5. Initialize Coolify connection const coolifySpinner = createSpinner({ text: "Connecting to Coolify instance...", color: "magenta", }).start(); const coolify = getCoolifyService(); const initResult = await coolify.init(); if (isErr(initResult)) { coolifySpinner.fail("Connection failed"); console.error(chalk.red(" ✗ Failed to connect to Coolify")); console.error(chalk.gray(` ${initResult.error.message}`)); console.error(""); return; } coolifySpinner.succeed("Connected to Coolify"); // 6. Search for existing apps with visual feedback const searchSpinner = createSpinner({ text: "Searching for existing deployments...", color: "yellow", }).start(); const existingApps = await findExistingApps(gitInfo.repoUrl, gitInfo.branch, coolify); if (existingApps.exact.length > 0) { searchSpinner.succeed( `Found ${chalk.bold.green(String(existingApps.exact.length))} existing deployment(s)`, ); console.log(""); console.log(chalk.gray(" ") + chalk.bold("Existing Deployments:")); console.log(""); for (let i = 0; i < existingApps.exact.length; i++) { const app = existingApps.exact[i]; const statusIcon = app.status.includes("running") || app.status.includes("healthy") ? chalk.green("●") : chalk.yellow("○"); const prefix = i === 0 ? "┌─ " : i === existingApps.exact.length - 1 ? "└─ " : "├─ "; const last = i === existingApps.exact.length - 1; console.log(chalk.gray(prefix) + statusIcon + " " + chalk.bold.white(app.name)); console.log( chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") + chalk.gray(" Project: ") + chalk.cyan(app.projectName), ); console.log( chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") + chalk.gray(" Status: ") + chalk.white(app.status), ); console.log( chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") + chalk.gray(" Branch: ") + chalk.yellow(app.branch), ); console.log( chalk.gray(i === existingApps.exact.length - 1 ? "│ " : "│ ") + chalk.gray(" UUID: ") + chalk.gray(app.uuid.slice(0, 8)) + chalk.gray("..."), ); if (!last) { console.log(chalk.gray("│")); } } console.log(""); if (!options.yes) { const linkChoice = await prompts({ type: "select", name: "value", message: chalk.cyan("Select deployment to link:"), choices: [ ...existingApps.exact.map((app, idx) => ({ title: `${app.name} ${chalk.gray(`(${app.projectName})`)}`, value: app.uuid, description: app.status, })), { title: chalk.red("➜ Create new deployment instead"), value: "create", }, ], }); console.log(""); if (linkChoice.value === "create") { await createNewApp(gitInfo, dockerConfigs, options, coolify); } else { await linkToApp(linkChoice.value as string, coolify, gitInfo, dockerConfigs); } } else { // Auto-link to first app console.log( chalk.gray(" → Auto-linking to: ") + chalk.bold.white(existingApps.exact[0].name), ); console.log(""); await linkToApp(existingApps.exact[0].uuid, coolify, gitInfo, dockerConfigs); } } else if (existingApps.partial.length > 0) { searchSpinner.warn( `Found ${chalk.bold.yellow(String(existingApps.partial.length))} app(s) from same repo (different branches)`, ); console.log(""); console.log(chalk.gray(" ") + chalk.bold("Existing Deployments (different branches):")); console.log(""); for (const app of existingApps.partial) { const statusIcon = app.status.includes("running") ? chalk.green("●") : chalk.yellow("○"); console.log( " " + statusIcon + " " + chalk.bold.white(app.name) + chalk.gray(` (${chalk.yellow(app.branch)})`), ); console.log( " " + chalk.gray("Project: ") + chalk.cyan(app.projectName) + chalk.gray(" | UUID: ") + chalk.gray(app.uuid.slice(0, 8)) + chalk.gray("..."), ); } console.log(""); if (options.link) { console.log(chalk.red(" ✗ Cannot link: no matching deployment found")); console.log(""); return; } if (!options.yes) { const choice = await prompts({ type: "select", name: "value", message: chalk.cyan("What would you like to do?"), choices: [ { title: chalk.green("➜ Create new deployment"), value: "create" }, { title: chalk.yellow("🔗 Link to existing deployment (different branch)"), value: "link", }, { title: chalk.red("✗ Cancel"), value: "cancel" }, ], }); console.log(""); if (choice.value === "create") { await createNewApp(gitInfo, dockerConfigs, options, coolify); } else if (choice.value === "link") { const linkChoice = await prompts({ type: "select", name: "value", message: chalk.cyan("Select deployment to link:"), choices: existingApps.partial.map((app) => ({ title: `${app.name} (${app.branch})`, value: app.uuid, })), }); console.log(""); await linkToApp(linkChoice.value as string, coolify, gitInfo, dockerConfigs); } else { console.log(""); console.log(chalk.gray(" ✓ Cancelled")); console.log(""); return; } } else { await createNewApp(gitInfo, dockerConfigs, options, coolify); } } else { searchSpinner.stop(); console.log(""); console.log(chalk.yellow(" ⚠ No existing deployments found for this repository")); console.log(""); if (options.link) { console.log(chalk.red(" ✗ Cannot link: no existing deployments found")); console.log(""); return; } await createNewApp(gitInfo, dockerConfigs, options, coolify); } } /** * Detect Git repository information. */ function detectGitInfo(cwd: string): IGitInfo | null { try { const repoUrl = execSync("git remote get-url origin", { cwd, encoding: "utf-8" }).trim(); const branch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim(); const name = cwd.split("/").pop() || "app"; // Normalize Git URL let normalizedUrl = repoUrl; // Convert SSH to HTTPS: git@github.com:user/repo.git → https://github.com/user/repo if (normalizedUrl.startsWith("git@")) { normalizedUrl = normalizedUrl.replace(/^git@([^:]+):/, "https://$1/"); } // Remove .git suffix normalizedUrl = normalizedUrl.replace(/\.git$/, ""); // Extract owner and repo const urlParts = normalizedUrl.split("/"); const owner = urlParts[urlParts.length - 2] || ""; const repo = urlParts[urlParts.length - 1] || ""; return { repoUrl: normalizedUrl, branch, name, owner, repo }; } catch { return null; } } /** * Detect Docker configuration files. */ function detectDockerConfigs(cwd: string): IDockerConfigs { const hasDockerfile = existsSync(join(cwd, "Dockerfile")); const hasCompose = existsSync(join(cwd, "docker-compose.yml")); // Check common subdirectories const appsDir = join(cwd, "apps"); let dockerfilePath: string | undefined; let composePath: string | undefined; if (!hasDockerfile && !hasCompose && existsSync(appsDir)) { // Look for Dockerfile in subdirectories const entries = require("node:fs").readdirSync(appsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const df = join(appsDir, entry.name, "Dockerfile"); if (existsSync(df)) { dockerfilePath = df; break; } } } } const resolvedDockerfile = dockerfilePath || (hasDockerfile ? join(cwd, "Dockerfile") : undefined); // Parse EXPOSE directives from Dockerfile let detectedPorts: number[] = []; if (resolvedDockerfile) { const { parseDockerfileExpose } = require("../../utils/format.js"); detectedPorts = parseDockerfileExpose(resolvedDockerfile); } return { hasDockerfile: hasDockerfile || !!dockerfilePath, hasCompose, dockerfilePath: dockerfilePath || (hasDockerfile ? "Dockerfile" : undefined), composePath: hasCompose ? "docker-compose.yml" : undefined, detectedPorts, }; } /** * Search for existing apps in Coolify with enhanced matching. */ async function findExistingApps( repoUrl: string, branch: string, coolify: any, ): Promise { const listResult = await coolify.listApplications(); if (isErr(listResult)) { return { exact: [], partial: [] }; } const allApps = listResult.value; // Get all projects to map names const projectsResult = await coolify.listProjects(); const projectsMap = new Map( isOk(projectsResult) ? projectsResult.value.map((p: any) => [p.uuid, p.name] as [string, string]) : [], ); // Extract owner/repo from URL const urlParts = repoUrl.split("/"); const ownerRepo = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`; const exact: IExistingApps["exact"] = []; const partial: IExistingApps["partial"] = []; for (const app of allApps) { const projectName = projectsMap.get(app.project_uuid || "") || "Unknown"; // Check by git_repository (owner/repo format) if (app.git_repository === ownerRepo) { if (app.git_branch === branch) { exact.push({ uuid: app.uuid, name: app.name, project: app.project_uuid || "", projectName, status: app.status, branch: app.git_branch || "unknown", fqdn: app.fqdn, }); } else { partial.push({ uuid: app.uuid, name: app.name, project: app.project_uuid || "", projectName, status: app.status, branch: app.git_branch || "unknown", fqdn: app.fqdn, }); } continue; } // Check by full URL if (app.git_full_url && app.git_full_url.includes(ownerRepo)) { if (app.git_branch === branch) { exact.push({ uuid: app.uuid, name: app.name, project: app.project_uuid || "", projectName, status: app.status, branch: app.git_branch || "unknown", fqdn: app.fqdn, }); } else { partial.push({ uuid: app.uuid, name: app.name, project: app.project_uuid || "", projectName, status: app.status, branch: app.git_branch || "unknown", fqdn: app.fqdn, }); } } } return { exact, partial }; } /** * Link to an existing app and create .coolify.json with enhanced output. */ async function linkToApp( appUuid: string, coolify: any, gitInfo: IGitInfo, dockerConfigs: IDockerConfigs, ): Promise { const linkSpinner = createSpinner({ text: "Fetching deployment details...", color: "blue", }).start(); try { // Use getApplication to get full application details const appResult = await coolify.getApplication(appUuid); if (isErr(appResult)) { throw new Error(appResult.error.message); } const app = appResult.value; linkSpinner.succeed("Deployment details retrieved"); // Get project and environment info by searching through projects const projectSpinner = createSpinner({ text: "Resolving project and environment...", color: "cyan", }).start(); let projectUuid = ""; let environmentUuid = ""; let projectName = "Unknown"; let environmentName = "Unknown"; // Get all projects const projectsResult = await coolify.listProjects(); if (isOk(projectsResult)) { // Search through each project's environments to find matching environment_id for (const project of projectsResult.value) { const envsResult = await coolify.getProjectEnvironments(project.uuid); if (isOk(envsResult)) { const matchingEnv = envsResult.value.find( (e: any) => e.id === app.environment_id, ); if (matchingEnv) { projectUuid = project.uuid; projectName = project.name; environmentUuid = matchingEnv.uuid; environmentName = matchingEnv.name; break; } } } } projectSpinner.succeed("Project and environment resolved"); // Build complete state with additional metadata const state: ICoolifyDeployState = { appUuid: app.uuid, appName: app.name, serverUuid: app.destination?.server?.uuid || "", serverName: app.destination?.server?.name, projectUuid, projectName, environmentUuid, environmentName, domain: app.fqdn, dockerComposePath: dockerConfigs.composePath || app.dockerfile_location, baseDirectory: app.base_directory || "/", branch: gitInfo.branch, gitRepository: app.git_full_url || gitInfo.repoUrl, sourceType: app.source_type, type: app.type, buildPack: app.build_pack, autoDeployEnabled: app.is_auto_deploy_enabled ?? false, updatedAt: new Date().toISOString(), coolifyUrl: process.env.COOLIFY_URL, }; writeCoolifyState(state); console.log(""); console.log(chalk.green(" ✓ Successfully linked to deployment!")); console.log(""); // Display summary card const appType = getAppTypeFromSourceType(app.source_type); console.log(createSummaryCard("Deployment Details", { "Name": { value: app.name, color: chalk.white }, "Project": { value: projectName, color: chalk.cyan }, "Environment": { value: environmentName, color: chalk.magenta }, "Branch": { value: gitInfo.branch, color: chalk.yellow }, "Type": { value: appType, color: chalk.gray }, "Status": { value: app.status, color: app.status.includes("running") ? chalk.green : chalk.yellow }, })); console.log(chalk.gray(" ") + chalk.bold("Next steps:")); console.log(""); console.log( " " + chalk.cyan("coolify-cli show") + chalk.gray(" - Show deployment details"), ); console.log( " " + chalk.cyan("coolify-cli deploy") + chalk.gray(" - Trigger deployment"), ); console.log( " " + chalk.cyan("coolify-cli env sync") + chalk.gray(" - Sync environment variables"), ); console.log(""); } catch (error) { linkSpinner.fail("Failed to link"); console.error(chalk.red(` ✗ ${error}`)); console.log(""); } } /** * Create a new app in Coolify with enhanced UX. */ async function createNewApp( gitInfo: IGitInfo, dockerConfigs: IDockerConfigs, options: IInitOptions, coolify: any, ): Promise { // List servers const serversSpinner = createSpinner({ text: "Fetching available servers...", color: "blue", }).start(); const serversResult = await coolify.listServers(); serversSpinner.stop(); if (isErr(serversResult)) { console.error(chalk.red(" ✗ Failed to list servers")); return; } const servers = serversResult.value; // List projects const projectsSpinner = createSpinner({ text: "Fetching available projects...", color: "cyan", }).start(); const projectsResult = await coolify.listProjects(); projectsSpinner.stop(); if (isErr(projectsResult)) { console.error(chalk.red(" ✗ Failed to list projects")); return; } const projects = projectsResult.value; let serverUuid: string; let projectUuid: string; let appName: string; if (options.yes) { // Auto-mode: use first available serverUuid = servers[0].uuid; projectUuid = projects[0].uuid; appName = options.name || gitInfo.name; } else { // Interactive mode console.log(""); console.log(chalk.gray(" ") + chalk.bold("Configuration:")); console.log(""); const serverChoice = await prompts({ type: "select", name: "value", message: chalk.cyan("Select server:"), choices: servers.map((s: { name: string; uuid: string; ip?: string }) => ({ title: `${s.name} ${s.ip ? chalk.gray(`(${s.ip})`) : ""}`, value: s.uuid, })), }); serverUuid = serverChoice.value; const projectChoices = [ ...projects.map((p: { name: string; uuid: string }) => ({ title: p.name, value: p.uuid, })), { title: chalk.green("+ Create new project"), value: "__new__" }, ]; const projectChoice = await prompts({ type: "select", name: "value", message: chalk.cyan("Select project:"), choices: projectChoices, }); if (projectChoice.value === "__new__") { const projectName = await prompts({ type: "text", name: "value", message: chalk.cyan("Project name:"), initial: gitInfo.name, }); const newProjectSpinner = createSpinner({ text: "Creating new project...", color: "green", }).start(); const newProjectResult = await coolify.createProject(projectName.value); if (isErr(newProjectResult)) { newProjectSpinner.fail("Failed to create project"); console.error(chalk.red(` ✗ ${newProjectResult.error.message}`)); return; } projectUuid = newProjectResult.value.uuid; newProjectSpinner.succeed( `Project ${chalk.bold.white(projectName.value)} created`, ); } else { projectUuid = projectChoice.value; } const nameChoice = await prompts({ type: "text", name: "value", message: chalk.cyan("Deployment name:"), initial: gitInfo.name, }); appName = nameChoice.value; } // Determine build pack let buildPack: "dockerfile" | "dockercompose" | "nixpacks" = "dockerfile"; if (dockerConfigs.hasCompose) { buildPack = "dockercompose"; } // Create app const deploySpinner = createSpinner({ text: "Creating new deployment...", color: "green", }).start(); try { // Get GitHub App (required for private-github-app type) const ghAppsResult = await coolify.listGithubAppsAll(); if (isErr(ghAppsResult)) { deploySpinner.fail("Failed to list GitHub Apps"); console.error(chalk.red(` ✗ ${ghAppsResult.error.message}`)); return; } const privateApps = ghAppsResult.value.filter((a: ICoolifyGithubApp) => !a.is_public); // 0 private apps → loud error if (privateApps.length === 0) { deploySpinner.fail("No private GitHub Apps found"); console.error( chalk.red(" Configure a private GitHub App in Coolify: Settings → Sources → GitHub App"), ); return; } // 1 private app → use silently; 2+ → interactive picker let selectedGhAppUuid: string; if (privateApps.length === 1) { selectedGhAppUuid = privateApps[0].uuid; } else { if (!isTTY) { deploySpinner.fail( "Multiple private GitHub Apps found but stdin is not a TTY. " + "Pass --github-app-uuid to select explicitly.", ); return; } const selected = await p.select({ message: "Select a GitHub App:", options: privateApps.map((app) => ({ label: app.name, value: app.uuid, hint: app.organization ?? undefined, })), }); if (p.isCancel(selected)) { deploySpinner.stop("Cancelled."); return; } selectedGhAppUuid = selected as string; } // Get environments for the project const envsResult = await coolify.getProjectEnvironments(projectUuid); if (isErr(envsResult) || envsResult.value.length === 0) { deploySpinner.fail("No environments found"); console.error(chalk.red(" ✗ No environments found for project")); return; } const environmentUuid = envsResult.value[0].uuid; const newAppResult = await coolify.createApplication({ name: appName, projectUuid, environmentUuid, serverUuid, type: "private-github-app", githubAppUuid: selectedGhAppUuid, githubRepoUrl: gitInfo.repoUrl, branch: gitInfo.branch, buildPack, portsExposes: dockerConfigs.detectedPorts && dockerConfigs.detectedPorts.length > 0 ? dockerConfigs.detectedPorts.join(",") : "3000", baseDirectory: "/", isAutoDeployEnabled: true, }); if (isErr(newAppResult)) { deploySpinner.fail("Failed to create deployment"); console.error(chalk.red(` ✗ ${newAppResult.error.message}`)); return; } const newApp = newAppResult.value; deploySpinner.succeed(`Deployment ${chalk.bold.white(appName)} created`); // Get project and environment names for the state file const project = projects.find((p: any) => p.uuid === projectUuid); const envsForResult = await coolify.getProjectEnvironments(projectUuid); const environment = isOk(envsForResult) && envsForResult.value.length > 0 ? envsForResult.value.find((e: any) => e.uuid === environmentUuid) || envsForResult.value[0] : undefined; // Get server name const serversResult = await coolify.listServers(); const server = isOk(serversResult) ? serversResult.value.find((s: any) => s.uuid === serverUuid) : undefined; // Write .coolify.json with complete metadata const state: ICoolifyDeployState = { appUuid: newApp.uuid!, appName, serverUuid, serverName: server?.name, projectUuid, projectName: project?.name, environmentUuid, environmentName: environment?.name, dockerComposePath: dockerConfigs.composePath, baseDirectory: "/", branch: gitInfo.branch, gitRepository: gitInfo.repoUrl, sourceType: "App\\Models\\GithubApp", type: "private-github-app", buildPack, autoDeployEnabled: true, updatedAt: new Date().toISOString(), coolifyUrl: process.env.COOLIFY_URL, }; writeCoolifyState(state); console.log(""); console.log(chalk.green(" ✓ Deployment configured successfully!")); console.log(""); console.log(createSummaryCard("Deployment Details", { "Name": { value: appName, color: chalk.white }, "UUID": { value: newApp.uuid!.slice(0, 8) + "...", color: chalk.gray }, "Type": { value: "private-github-app", color: chalk.gray }, "Build Pack": { value: buildPack, color: chalk.cyan }, })); console.log(chalk.gray(" ") + chalk.bold("Next steps:")); console.log(""); console.log( " " + chalk.cyan("coolify-cli show") + chalk.gray(" - Show deployment details"), ); console.log( " " + chalk.cyan("coolify-cli deploy") + chalk.gray(" - Trigger first deployment"), ); console.log( " " + chalk.cyan("coolify-cli env sync") + chalk.gray(" - Sync environment variables"), ); console.log(""); } catch (error) { deploySpinner.fail("Failed to create deployment"); console.error(chalk.red(` ✗ ${error}`)); console.log(""); } } /** * Map source_type to readable app type. */ function getAppTypeFromSourceType(sourceType: string | undefined): string { if (!sourceType) return "unknown"; if (sourceType.includes("GithubApp")) return "github-app"; if (sourceType.includes("DeployKey")) return "deploy-key"; if (sourceType.includes("Dockerfile")) return "dockerfile"; if (sourceType.includes("DockerCompose")) return "docker-compose"; if (sourceType.includes("DockerImage")) return "docker-image"; if (sourceType.includes("Public")) return "public"; return "unknown"; }