UNPKG

create-cen-app

Version:
238 lines (208 loc) 7.44 kB
import * as p from "@clack/prompts"; import chalk from "chalk"; import { execa } from "execa"; import fs from "fs-extra"; import { AvailableFlavours } from "~/installers/index.js"; import { getUserPythonVersions } from "~/utils/getUserPythonVersion.js"; interface PreflightCheckOptions { projectName: string; projectDir: string; noInstall: boolean; flavour: AvailableFlavours; } interface PreflightCheckResult { noInstall: boolean; missingDependencies: string[]; } export const preflightCheck = async ({ projectDir, projectName, noInstall, flavour, }: PreflightCheckOptions): Promise<PreflightCheckResult> => { if (fs.existsSync(projectDir)) { if (fs.readdirSync(projectDir).length === 0) { if (projectName !== ".") p.log.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`); } else { const overwriteDir = await p.select({ message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( projectName, )} already exists and isn't empty. How would you like to proceed?`, options: [ { label: "Abort installation (recommended)", value: "abort", }, { label: "Clear the directory and continue installation", value: "clear", }, { label: "Continue installation and overwrite conflicting files", value: "overwrite", }, ], }); if (p.isCancel(overwriteDir) || overwriteDir === "abort") { p.log.error("Aborting installation..."); process.exit(1); } const overwriteAction = overwriteDir === "clear" ? "clear the directory" : "overwrite conflicting files"; const confirmOverwriteDir = await p.confirm({ message: `Are you sure you want to ${overwriteAction}?`, initialValue: false, }); if (p.isCancel(confirmOverwriteDir) || !confirmOverwriteDir) { p.log.error("Aborting installation..."); process.exit(1); } if (overwriteDir === "clear") { p.log.info(`\nEmptying ${chalk.cyan.bold(projectName)} and creating CEN app..\n`); fs.emptyDirSync(projectDir); } } } let shouldSetNoInstall = noInstall; let missingDependencies: string[] = []; let missingCriticalDependencies: string[] = []; p.log.info("Checking for dependencies...\n"); //@ts-expect-error - java flavour will be added in a future PR, ignoring type error for now if (flavour !== "go" && flavour !== "java") { const uvInstalled = await checkIfUvInstalled(); if (!uvInstalled) { missingCriticalDependencies.push("uv"); p.log.error(chalk.red("❌ uv is not installed")); p.log.message( chalk.cyan.bold("Install uv: https://docs.astral.sh/uv/getting-started/installation/"), ); } else { p.log.success(`${chalk.green("uv is installed")}`); } } const dockerInstalled = await checkIfDockerInstalled(); if (!dockerInstalled) { missingDependencies.push("Docker CLI"); p.log.warn(chalk.yellow("Docker CLI is not installed")); p.log.message(chalk.cyan.bold("Install Docker with Homebrew: brew install docker\n")); p.log.message( chalk.cyan.bold("Install Docker Runtime (colima): https://github.com/abiosoft/colima/ "), ); } else { p.log.success(`${chalk.green("Docker CLI is installed")}`); const dockerComposeInstalled = await checkIfDockerComposeInstalled(); if (!dockerComposeInstalled) { missingDependencies.push("Docker Compose"); p.log.warn(chalk.yellow("Docker Compose is not available")); p.log.message( chalk.cyan.bold("Install Docker Compose with Homebrew: brew install docker-compose\n"), ); p.log.message( chalk.cyan.bold("Install Docker Runtime (colima): https://github.com/abiosoft/colima/ "), ); } else { p.log.success(`${chalk.green("Docker Compose is installed")}`); } } //@ts-expect-error - java flavour will be added in a future PR, ignoring type error for now if (flavour !== "go" && flavour !== "java") { const pythonInstalled = await checkIfPythonVersionsInstalled(); if (!pythonInstalled) { missingCriticalDependencies.push("Python 3.10+"); p.log.error( chalk.red("❌ You need Python 3.10, 3.11, or 3.12 installed to use this template"), ); p.log.message(chalk.cyan.bold("Install Python: https://www.python.org/downloads/")); } else { p.log.success(`${chalk.green("Python 3.10, 3.11, or 3.12 is installed")}`); } } if (flavour !== "backend-only" && flavour !== "backend-only-no-db") { const nodeInstalled = await checkIfNodeInstalled(); if (!nodeInstalled) { missingCriticalDependencies.push("Node.js 20+"); p.log.error(chalk.red("❌ You need Node.js 20.x or higher to use this template")); p.log.message(chalk.cyan.bold("Install Node.js: https://nodejs.org/")); } else { p.log.success(`${chalk.green("Node.js 20.x or higher is installed")}`); } } if (missingDependencies.length > 0 || missingCriticalDependencies.length > 0) { const missingMsg = `Missing dependencies: ${[ ...missingCriticalDependencies, ...missingDependencies, ].join(", ")}`; p.log.warn(chalk.yellow(missingMsg)); const continueAnyway = await p.confirm({ message: `Would you like to continue anyway?`, initialValue: missingCriticalDependencies.length === 0, }); if (p.isCancel(continueAnyway) || !continueAnyway) { p.log.error(chalk.red("Aborting installation...")); process.exit(1); } if (missingCriticalDependencies.length > 0) { shouldSetNoInstall = true; } } return { noInstall: shouldSetNoInstall, missingDependencies: [...missingCriticalDependencies, ...missingDependencies], }; }; export const checkIfUvInstalled = async () => { try { const { stdout } = await execa("uv", ["--version"]); return stdout !== ""; } catch { return false; } }; const checkIfDockerInstalled = async () => { try { const { stdout } = await execa("docker", ["--version"]); return stdout !== ""; } catch { return false; } }; const checkIfDockerComposeInstalled = async () => { let isInstalled = false; try { const { stdout } = await execa("docker", ["compose", "version"]); isInstalled = stdout !== ""; } catch { isInstalled = false; } if (!isInstalled) { try { const { stdout } = await execa("docker-compose", ["version"]); isInstalled = stdout !== ""; } catch { isInstalled = false; } } return isInstalled; }; const checkIfPythonVersionsInstalled = async () => { const supportedVersions = ["3.10", "3.11", "3.12"]; const pythonVersions = await getUserPythonVersions(); if (!pythonVersions) { return false; } const versions = pythonVersions .map((pv) => pv.version?.match(/(\d+\.\d+\.\d+)$/)?.[1]) .filter((v): v is string => v !== undefined); return versions.some((v) => supportedVersions.some((sv) => v.startsWith(sv))); }; const checkIfNodeInstalled = async () => { try { const { stdout } = await execa("node", ["--version"]); // Node version output format is 'v20.x.x' const version = stdout?.slice(1).split(".")[0]; return version ? parseInt(version) >= 20 : false; } catch { return false; } };