feat: 🧑‍💻 Turn scripts into an interactive CLI (#41)

WARNING: This PR changes the kurtosis package to use the one from
upstream, not our fork, as it was not working at the moment. This should
be changed when fixed.

In this PR:
1. Turn `launch-kurtosis` script into a CLI, which parses parameters and
can interactively run multiple steps.
2. Separate steps of such CLI into their own scripts.
1. New script created `launch-kurtosis`, which detects if an enclave is
running and prompts to relaunch it if it is.
2. New script created `deploy-contracts` to deploy all contracts. It can
optionally verify them as well.
3. Each step can be interactively opted in/out, or choose whether to run
it via CLI params.
4. The CLI offers a help command as well.
5. Cleanup logs of CLI. Logs of internal commands ran can be printed
with LOG_LEVEL=debug. In case of error, they are always printed out.
This commit is contained in:
Facundo Farall 2025-04-15 11:01:24 -03:00 committed by GitHub
parent b621f1c04a
commit 6310f0d3fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 532 additions and 146 deletions

View file

@ -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=="],

View file

@ -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",

View file

@ -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<boolean> => {
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);
});
}

190
test/scripts/e2e-cli.ts Normal file
View file

@ -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<void> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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();

View file

@ -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<boolean> => {
const text = await $`kurtosis enclave ls | grep "datahaven-ethereum" | grep RUNNING`.text();
return text.length > 0;
};

View file

@ -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<boolean> => {
const { exitCode } = await $`kurtosis version`.nothrow().quiet();
return exitCode === 0;
};
const checkKurtosisRunning = async (): Promise<boolean> => {
const text = await $`kurtosis enclave ls | grep RUNNING`.text();
return text.length > 0;
};
const checkDockerRunning = async (): Promise<boolean> => {
const { exitCode } = await $`docker system info`.nothrow();
return exitCode === 0;
};
const checkForgeInstalled = async (): Promise<boolean> => {
const { exitCode } = await $`forge --version`.nothrow();
return exitCode === 0;
};
main();

View file

@ -42,7 +42,7 @@ const serviceMappings: ServiceMapping[] = [
}
];
export async function getServicesFromDocker(): Promise<ServiceInfo[]> {
export const getServicesFromDocker = async (): Promise<ServiceInfo[]> => {
const docker = new Docker();
const containers = await docker.listContainers();
@ -103,7 +103,7 @@ export async function getServicesFromDocker(): Promise<ServiceInfo[]> {
}
return services;
}
};
export const getPublicPort = async (
containerName: string,

View file

@ -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";

51
test/utils/input.ts Normal file
View file

@ -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<boolean> => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise<boolean>((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");
}
});
});
};

View file

@ -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`));
};