diff --git a/test/bun.lock b/test/bun.lock index 5b0848fd..e23384d7 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -7,6 +7,7 @@ "@biomejs/biome": "^1.9.4", "@dotenvx/dotenvx": "^1.39.0", "@types/dockerode": "^3.3.37", + "chalk": "^5.4.1", "dockerode": "^4.0.5", "dotenv": "^16.4.7", "pino": "^9.6.0", @@ -121,6 +122,8 @@ "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], diff --git a/test/package.json b/test/package.json index bc8fe97d..20d3ff1a 100644 --- a/test/package.json +++ b/test/package.json @@ -6,12 +6,11 @@ "scripts": { "fmt": "biome check .", "fmt:fix": "biome check --write .", - "demo": "bun run scripts/placeholder.ts", - "start:e2e:verified": "bun run scripts/start-kurtosis.ts --verified", - "start:e2e:minimal": "bun run scripts/start-kurtosis.ts", + "start:e2e:verified": "bun run scripts/e2e-cli.ts --verified", + "start:e2e:minimal": "bun run scripts/e2e-cli.ts", "stop:e2e": "kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f", "stop:e2e:verified": "bun stop:e2e", - "stop:e2e:minimal": "bun stop:e2e", + "stop:e2e:minimal": "bun stop:e2e", "stop:kurtosis-engine": "kurtosis engine stop && docker container prune -f", "test:e2e": "bun test suites/e2e" }, @@ -25,6 +24,7 @@ "@biomejs/biome": "^1.9.4", "@dotenvx/dotenvx": "^1.39.0", "@types/dockerode": "^3.3.37", + "chalk": "^5.4.1", "dockerode": "^4.0.5", "dotenv": "^16.4.7", "pino": "^9.6.0", diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts new file mode 100644 index 00000000..7d81f44f --- /dev/null +++ b/test/scripts/deploy-contracts.ts @@ -0,0 +1,145 @@ +import { $ } from "bun"; +import invariant from "tiny-invariant"; +import { logger, printHeader, promptWithTimeout } from "utils"; + +interface DeployContractsOptions { + rpcUrl: string; + verified?: boolean; + blockscoutBackendUrl?: string; + deployContracts?: boolean; +} + +/** + * Deploys smart contracts to the specified RPC URL + * + * @param options - Configuration options for deployment + * @param options.rpcUrl - The RPC URL to deploy to + * @param options.verified - Whether to verify contracts (requires blockscoutBackendUrl) + * @param options.blockscoutBackendUrl - URL for the Blockscout API (required if verified is true) + * @param options.deployContracts - Flag to control deployment (if undefined, will prompt) + * @returns Promise resolving to true if contracts were deployed successfully, false if skipped + */ +export const deployContracts = async (options: DeployContractsOptions): Promise => { + const { rpcUrl, verified = false, blockscoutBackendUrl, deployContracts } = options; + + // Check if deployContracts option was set via flags, or prompt if not + let shouldDeployContracts = deployContracts; + if (shouldDeployContracts === undefined) { + shouldDeployContracts = await promptWithTimeout( + "Do you want to deploy the smart contracts?", + true, + 10 + ); + } else { + logger.info( + `Using flag option: ${shouldDeployContracts ? "will deploy" : "will not deploy"} smart contracts` + ); + } + + if (!shouldDeployContracts) { + logger.info("Skipping contract deployment. Done!"); + return false; + } + + // Check if required parameters are provided + invariant(rpcUrl, "โŒ RPC URL is required"); + if (verified) { + invariant(blockscoutBackendUrl, "โŒ Blockscout backend URL is required for verification"); + } + + printHeader("Deploying Smart Contracts"); + + // Build contracts + logger.info("๐Ÿ›ณ๏ธ Building contracts..."); + const { + exitCode: buildExitCode, + stderr: buildStderr, + stdout: buildStdout + } = await $`forge build`.cwd("../contracts").nothrow().quiet(); + + if (buildExitCode !== 0) { + logger.error(buildStderr.toString()); + throw Error("โŒ Contracts have failed to build properly."); + } + logger.debug(buildStdout.toString()); + + // Get forge path + const { stdout: forgePath } = await $`which forge`.quiet(); + const forgeExecutable = forgePath.toString().trim(); + + // Prepare deployment command + let deployCommand = `${forgeExecutable} script script/deploy/DeployLocal.s.sol --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; + + if (verified && blockscoutBackendUrl) { + deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`; + logger.info("๐Ÿ” Contract verification enabled"); + } + + logger.info("โณ Deploying contracts (this might take a few minutes)..."); + + const { exitCode: deployExitCode, stderr: deployStderr } = await $`sh -c ${deployCommand}` + .cwd("../contracts") + .nothrow(); + + if (deployExitCode !== 0) { + logger.error(deployStderr.toString()); + throw Error("โŒ Contracts have failed to deploy properly."); + } + + logger.success("Contracts deployed successfully"); + return true; +}; + +// Allow script to be run directly with CLI arguments +if (import.meta.main) { + const args = process.argv.slice(2); + const options: { + rpcUrl?: string; + verified: boolean; + blockscoutBackendUrl?: string; + deployContracts?: boolean; + } = { + verified: args.includes("--verified"), + deployContracts: args.includes("--deploy-contracts") + ? true + : args.includes("--no-deploy-contracts") + ? false + : undefined + }; + + // Extract RPC URL + const rpcUrlIndex = args.indexOf("--rpc-url"); + if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) { + options.rpcUrl = args[rpcUrlIndex + 1]; + } + + // Extract Blockscout URL if verification is enabled + if (options.verified) { + const blockscoutUrlIndex = args.indexOf("--blockscout-url"); + if (blockscoutUrlIndex !== -1 && blockscoutUrlIndex + 1 < args.length) { + options.blockscoutBackendUrl = args[blockscoutUrlIndex + 1]; + } + } + + // Check required parameters + if (!options.rpcUrl) { + console.error("Error: --rpc-url parameter is required"); + process.exit(1); + } + + if (options.verified && !options.blockscoutBackendUrl) { + console.error("Error: --blockscout-url parameter is required when using --verified"); + process.exit(1); + } + + // Run deployment + deployContracts({ + rpcUrl: options.rpcUrl, + verified: options.verified, + blockscoutBackendUrl: options.blockscoutBackendUrl, + deployContracts: options.deployContracts + }).catch((error) => { + console.error("Deployment failed:", error); + process.exit(1); + }); +} diff --git a/test/scripts/e2e-cli.ts b/test/scripts/e2e-cli.ts new file mode 100644 index 00000000..1cd3c285 --- /dev/null +++ b/test/scripts/e2e-cli.ts @@ -0,0 +1,190 @@ +import { $ } from "bun"; +import invariant from "tiny-invariant"; +import chalk from "chalk"; +import { logger, printDivider, printHeader } from "utils"; +import sendTxn from "./send-txn"; +import { launchKurtosis } from "./launch-kurtosis"; +import { deployContracts } from "./deploy-contracts"; + +interface ScriptOptions { + verified: boolean; + launchKurtosis?: boolean; + deployContracts?: boolean; + help?: boolean; +} + +async function main() { + const args = process.argv.slice(2); + + // Parse command-line arguments + const options: ScriptOptions = { + verified: args.includes("--verified"), + launchKurtosis: parseFlag(args, "launchKurtosis"), + deployContracts: parseFlag(args, "deploy-contracts"), + help: args.includes("--help") || args.includes("-h") + }; + + // Show help menu if requested + if (options.help) { + printHelp(); + return; + } + + logger.info(`Running with options: ${getOptionsString(options)}`); + + const timeStart = performance.now(); + + printHeader("Environment Checks"); + + await checkDependencies(); + + // Clean up and launch Kurtosis enclave + const { services } = await launchKurtosis({ + launchKurtosis: options.launchKurtosis + }); + + // Send test transaction + printHeader("Setting Up Blockchain"); + const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // Anvil test acc1 + const networkRpcUrl = services.find((s) => s.service === "reth-1-rpc")?.url; + invariant(networkRpcUrl, "โŒ Network RPC URL not found"); + + logger.info("๐Ÿ’ธ Sending test transaction..."); + await sendTxn(privateKey, networkRpcUrl); + + printDivider(); + + // Display service information in a clean table + printHeader("Service Endpoints"); + + console.table( + services + .filter((s) => ["reth-1-rpc", "reth-2-rpc", "blockscout-backend", "dora"].includes(s.service)) + .concat([ + { service: "blockscout", port: "3000", url: "http://127.0.0.1:3000" }, + { service: "kurtosis-web", port: "9711", url: "http://127.0.0.1:9711" } + ]) + ); + + printDivider(); + + // Show completion information + const timeEnd = performance.now(); + const minutes = ((timeEnd - timeStart) / (1000 * 60)).toFixed(1); + + logger.success(`Kurtosis network started successfully in ${minutes} minutes`); + + printDivider(); + + // Deploy contracts using the extracted function + const blockscoutBackendUrl = services.find((s) => s.service === "blockscout-backend")?.url; + await deployContracts({ + rpcUrl: networkRpcUrl, + verified: options.verified, + blockscoutBackendUrl, + deployContracts: options.deployContracts + }); +} + +// Helper function to check all dependencies at once +const checkDependencies = async (): Promise => { + if (!(await checkKurtosisInstalled())) { + logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install"); + throw Error("โŒ Kurtosis CLI application not found."); + } + + logger.success("Kurtosis CLI found"); + + if (!(await checkDockerRunning())) { + logger.error("Is Docker Running? Unable to make connection to docker daemon"); + throw Error("โŒ Error connecting to Docker"); + } + + logger.success("Docker is running"); + + if (!(await checkForgeInstalled())) { + logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation"); + throw Error("โŒ forge binary not found in PATH"); + } + + logger.success("Forge is installed"); +}; + +const checkKurtosisInstalled = async (): Promise => { + const { exitCode, stderr, stdout } = await $`kurtosis version`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +const checkDockerRunning = async (): Promise => { + const { exitCode, stderr, stdout } = await $`docker system info`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +const checkForgeInstalled = async (): Promise => { + const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; + +// Helper function to format options as a string +function getOptionsString(options: ScriptOptions): string { + const optionStrings: string[] = []; + if (options.verified) optionStrings.push("verified"); + if (options.launchKurtosis !== undefined) + optionStrings.push(`launchKurtosis=${options.launchKurtosis}`); + if (options.deployContracts !== undefined) + optionStrings.push(`deployContracts=${options.deployContracts}`); + return optionStrings.length ? optionStrings.join(", ") : "no options"; +} + +// Print help menu +function printHelp(): void { + console.log(chalk.bold.cyan("\nDatahaven Kurtosis Startup Script")); + console.log(chalk.gray("=".repeat(40))); + console.log(` +${chalk.yellow("Available Options:")} + +${chalk.green("--verified")} Use contract verification via Blockscout +${chalk.green("--launchKurtosis")} Clean and launch Kurtosis enclave if already running +${chalk.green("--no-launchKurtosis")} Keep existing Kurtosis enclave if already running +${chalk.green("--deploy-contracts")} Deploy smart contracts after Kurtosis starts +${chalk.green("--no-deploy-contracts")} Skip smart contract deployment +${chalk.green("--help, -h")} Show this help menu + +${chalk.yellow("Examples:")} + ${chalk.gray("# Start with interactive prompts")} + bun run start-kurtosis + + ${chalk.gray("# Start with verification and automatic redeploy")} + bun run start-kurtosis --verified --redeploy + + ${chalk.gray("# Start without deploying contracts")} + bun run start-kurtosis --no-deploy-contracts +`); +} + +// Parse and handle boolean flags with negations +function parseFlag(args: string[], flagName: string): boolean | undefined { + const positiveFlag = `--${flagName}`; + const negativeFlag = `--no-${flagName}`; + + if (args.includes(positiveFlag)) return true; + if (args.includes(negativeFlag)) return false; + return undefined; +} + +main(); diff --git a/test/scripts/launch-kurtosis.ts b/test/scripts/launch-kurtosis.ts new file mode 100644 index 00000000..57d26717 --- /dev/null +++ b/test/scripts/launch-kurtosis.ts @@ -0,0 +1,95 @@ +import { $ } from "bun"; +import { getServicesFromDocker, logger, printDivider, printHeader, promptWithTimeout } from "utils"; + +/** + * Launches a Kurtosis Ethereum network enclave for testing. + * + * This function checks if a Kurtosis network is already running. If it is: + * - With `launchKurtosis: false` - keeps the existing enclave + * - With `launchKurtosis: true` - cleans and relaunches the enclave + * - With `launchKurtosis: undefined` - prompts the user to decide whether to relaunch + * + * If no network is running, it launches a new one. + * + * @param options - Configuration options + * @param options.launchKurtosis - Whether to forcibly launch Kurtosis (true), keep existing (false), or prompt user (undefined) + * @returns Object containing success status and Docker services information + */ +export const launchKurtosis = async (options: { launchKurtosis?: boolean } = {}) => { + if (await checkKurtosisRunning()) { + logger.info("โ„น๏ธ Kurtosis network is already running."); + + // Check if launchKurtosis option was set via flags + if (options.launchKurtosis === false) { + logger.info("Keeping existing Kurtosis enclave. Exiting..."); + return { success: true, services: await getServicesFromDocker() }; + } + + if (options.launchKurtosis === true) { + logger.info("Proceeding to clean and relaunch the Kurtosis enclave..."); + } else { + // Use promptWithTimeout if launchKurtosis is undefined + const shouldRelaunch = await promptWithTimeout( + "Do you want to clean and relaunch the Kurtosis enclave?", + true, + 10 + ); + + if (!shouldRelaunch) { + logger.info("Keeping existing Kurtosis enclave. Exiting..."); + return { success: true, services: await getServicesFromDocker() }; + } + + logger.info("Proceeding to clean and relaunch the Kurtosis enclave..."); + } + } + + // Start Kurtosis network + printHeader("Starting Kurtosis Network"); + + // Clean up Docker and Kurtosis + logger.info("๐Ÿงน Cleaning up Docker and Kurtosis environments..."); + logger.debug(await $`kurtosis enclave stop datahaven-ethereum`.nothrow().text()); + logger.debug(await $`kurtosis clean`.text()); + logger.debug(await $`kurtosis engine stop`.text()); + logger.debug(await $`docker system prune -f`.nothrow().text()); + + // Pull necessary Docker images + if (process.platform === "darwin") { + logger.debug("Detected macOS, pulling container images with linux/amd64 platform..."); + logger.debug( + await $`docker pull ghcr.io/blockscout/smart-contract-verifier:latest --platform linux/amd64`.text() + ); + } + + // Run Kurtosis + logger.info("๐Ÿš€ Starting Kurtosis enclave..."); + const { stderr, stdout, exitCode } = + await $`kurtosis run github.com/ethpandaops/ethereum-package --args-file configs/minimal.yaml --enclave datahaven-ethereum` + .nothrow() + .quiet(); + + if (exitCode !== 0) { + logger.error(stderr.toString()); + throw Error("โŒ Kurtosis network has failed to start properly."); + } + logger.debug(stdout.toString()); + + // Get service information from Docker + logger.info("๐Ÿ” Detecting Docker container ports..."); + const services = await getServicesFromDocker(); + + printDivider(); + + return { success: true, services }; +}; + +/** + * Checks if a Kurtosis enclave named "datahaven-ethereum" is currently running. + * + * @returns True if the enclave is running, false otherwise + */ +const checkKurtosisRunning = async (): Promise => { + const text = await $`kurtosis enclave ls | grep "datahaven-ethereum" | grep RUNNING`.text(); + return text.length > 0; +}; diff --git a/test/scripts/start-kurtosis.ts b/test/scripts/start-kurtosis.ts deleted file mode 100644 index ea1c3cea..00000000 --- a/test/scripts/start-kurtosis.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { $ } from "bun"; -import invariant from "tiny-invariant"; -import { getServicesFromDocker, logger } from "utils"; -import sendTxn from "./send-txn"; - -async function main() { - const args = process.argv.slice(2); - const isVerified = args.includes("--verified"); - logger.info(`Running with --verified: ${isVerified}`); - - const timeStart = performance.now(); - logger.debug(`Running on ${process.platform}`); - - if (!(await checkKurtosisInstalled())) { - logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install"); - throw Error("โŒ Kurtosis CLI application not found."); - } - - if (!(await checkDockerRunning())) { - logger.error("Is Docker Running? Unable to make connection to docker daemon"); - throw Error("โŒ Error connecting to Docker"); - } - - if (!(await checkForgeInstalled())) { - logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation"); - throw Error("โŒ forge binary not found in PATH"); - } - - logger.info("๐Ÿช” Starting Kurtosis network..."); - - if (await checkKurtosisRunning()) { - logger.info("โ„น๏ธ Kurtosis network is already running. Quitting..."); - return; - } - - await $`docker system prune -f`.nothrow(); - await $`kurtosis clean`; - - if (process.platform === "darwin") { - logger.debug("Detected macOS, pulling container images with linux/amd64 platform..."); - await $`docker pull ghcr.io/blockscout/smart-contract-verifier:latest --platform linux/amd64`; - } - - const { stderr, stdout, exitCode } = - await $`kurtosis run github.com/Moonsong-Labs/ethereum-package --args-file configs/minimal.yaml --enclave datahaven-ethereum`.nothrow(); - - if (exitCode !== 0) { - logger.error(stderr.toString()); - throw Error("โŒ Kurtosis network has failed to start properly."); - } - - // Get service information from Docker instead of parsing stdout - logger.info("๐Ÿ” Detecting Docker container ports..."); - const services = await getServicesFromDocker(); - - logger.info("================================================"); - - const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // Anvil test acc1 - const networkRpcUrl = services.find((s) => s.service === "reth-1-rpc")?.url; - invariant(networkRpcUrl, "โŒ Network RPC URL not found"); - await sendTxn(privateKey, networkRpcUrl); - - // Deploy all the contracts - logger.info("๐Ÿ›ณ๏ธ Deploying contracts..."); - const { exitCode: buildExitCode, stderr: buildStderr } = await $`forge build` - .cwd("../contracts") - .nothrow(); - - if (buildExitCode !== 0) { - logger.error(buildStderr.toString()); - throw Error("โŒ Contracts have failed to build properly."); - } - - const { stdout: forgePath } = await $`which forge`.quiet(); - const forgeExecutable = forgePath.toString().trim(); - logger.info(`Using forge at: ${forgeExecutable}`); - - const blockscoutBackendUrl = services.find((s) => s.service === "blockscout-backend")?.url; - invariant(blockscoutBackendUrl, "โŒ Blockscout backend URL not found"); - - let deployCommand = `${forgeExecutable} script script/deploy/DeployLocal.s.sol --rpc-url ${networkRpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; - - if (isVerified) { - deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`; - } - - console.log(`Running command: ${deployCommand}`); - - const { exitCode: deployExitCode, stderr: deployStderr } = await $`sh -c ${deployCommand}` - .cwd("../contracts") - .nothrow(); - - if (deployExitCode !== 0) { - logger.error(deployStderr.toString()); - throw Error("โŒ Contracts have failed to deploy properly."); - } - - logger.info("================================================"); - - console.table([ - ...services, - { service: "blockscout", port: "3000", url: "http://127.0.0.1:3000" }, - { - service: "kurtosis-web", - port: "9711", - url: "http://127.0.0.1:9711" - } - ]); - - logger.info("================================================"); - - const timeEnd = performance.now(); - - logger.info( - `๐Ÿ’š Kurtosis network has started successfully in ${((timeEnd - timeStart) / (1000 * 60)).toFixed(1)} minutes` - ); -} - -const checkKurtosisInstalled = async (): Promise => { - const { exitCode } = await $`kurtosis version`.nothrow().quiet(); - return exitCode === 0; -}; - -const checkKurtosisRunning = async (): Promise => { - const text = await $`kurtosis enclave ls | grep RUNNING`.text(); - return text.length > 0; -}; - -const checkDockerRunning = async (): Promise => { - const { exitCode } = await $`docker system info`.nothrow(); - return exitCode === 0; -}; - -const checkForgeInstalled = async (): Promise => { - const { exitCode } = await $`forge --version`.nothrow(); - return exitCode === 0; -}; - -main(); diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 60fa4d97..00b4f7b0 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -42,7 +42,7 @@ const serviceMappings: ServiceMapping[] = [ } ]; -export async function getServicesFromDocker(): Promise { +export const getServicesFromDocker = async (): Promise => { const docker = new Docker(); const containers = await docker.listContainers(); @@ -103,7 +103,7 @@ export async function getServicesFromDocker(): Promise { } return services; -} +}; export const getPublicPort = async ( containerName: string, diff --git a/test/utils/index.ts b/test/utils/index.ts index 99236803..196aa70b 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,6 +1,7 @@ export * from "./blockscout"; export * from "./constants"; export * from "./docker"; +export * from "./input"; export * from "./logger"; export * from "./rpc"; export * from "./viem"; diff --git a/test/utils/input.ts b/test/utils/input.ts new file mode 100644 index 00000000..6a9fa348 --- /dev/null +++ b/test/utils/input.ts @@ -0,0 +1,51 @@ +import chalk from "chalk"; +import readline from "node:readline"; +// Helper function to create an interactive prompt with timeout +export const promptWithTimeout = async ( + question: string, + defaultValue: boolean, + timeoutSeconds: number +): Promise => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + const defaultText = defaultValue ? "Y/n" : "y/N"; + + // Create a visually striking prompt + const border = chalk.yellow("=".repeat(question.length + 40)); + console.log("\n"); + console.log(border); + console.log(chalk.yellow("โ–ถ ") + chalk.bold.cyan(question)); + console.log( + chalk.magenta( + `โฑ Will default to ${chalk.bold(defaultValue ? "YES" : "NO")} in ${chalk.bold(timeoutSeconds)} seconds` + ) + ); + console.log(border); + const fullQuestion = chalk.green(`\nโžค Please enter your choice [${chalk.bold(defaultText)}]: `); + + const timer = setTimeout(() => { + console.log( + `\n${chalk.yellow("โฑ")} ${chalk.bold("Timeout reached, using default:")} ${chalk.green(defaultValue ? "YES" : "NO")}\n` + ); + rl.close(); + resolve(defaultValue); + }, timeoutSeconds * 1000); + + rl.question(fullQuestion, (answer) => { + clearTimeout(timer); + rl.close(); + + if (answer.trim() === "") { + resolve(defaultValue); + } else { + const normalizedAnswer = answer.trim().toLowerCase(); + console.log(""); + resolve(normalizedAnswer === "y" || normalizedAnswer === "yes"); + } + }); + }); +}; diff --git a/test/utils/logger.ts b/test/utils/logger.ts index bbfc1075..0b9c3e15 100644 --- a/test/utils/logger.ts +++ b/test/utils/logger.ts @@ -1,9 +1,49 @@ import pino from "pino"; import pinoPretty from "pino-pretty"; +import chalk from "chalk"; const logLevel = process.env.LOG_LEVEL || "info"; const stream = pinoPretty({ colorize: true }); -export const logger = pino({ level: logLevel }, stream); + +// Custom logger type with success method +interface CustomLogger extends pino.Logger { + success(msg: string, ...args: any[]): void; +} + +// Create the base logger with proper configuration +export const logger: CustomLogger = pino( + { + level: logLevel + }, + stream +) as CustomLogger; + +// Add custom success method to the logger +logger.success = function (message: string) { + this.info(`โœ… ${message}`); +}; + +// Simple progress bar function +export const printProgress = (percent: number) => { + const width = 30; + const completed = Math.floor(width * (percent / 100)); + const remaining = width - completed; + + const bar = chalk.green("โ–ˆ".repeat(completed)) + chalk.gray("โ–‘".repeat(remaining)); + + console.log(`\n${chalk.bold("Progress:")} ${bar} ${percent}%\n`); +}; + +// Print a section header +export const printHeader = (title: string) => { + console.log(`\n${chalk.bold.cyan(`โ–ถ ${title}`)}`); + console.log(chalk.gray("โ”€".repeat(title.length + 3))); +}; + +// Print a divider +export const printDivider = () => { + console.log(chalk.gray(`\n${"โ”€".repeat(50)}\n`)); +};