import { parseArgs } from "node:util"; import chalk from "chalk"; import { loadBinding } from "#core/binding"; import type { MusePlugin } from "#core/plugins"; import { MuseError } from "#errors"; import { version } from "../../package.json"; interface CLIFlags { help: boolean; verbose: boolean; stdout: boolean; } interface Loggers { log: (msg: string) => void; verbose: (msg: string) => void; } enum ExitCode { Success = 0, Error = 1, } export const USAGE = ` ${chalk.bold("muse")} - Compile and process troves of data ${chalk.dim(`v${version}`)} Usage: muse [/path/to/binding.yaml] Options: --stdout -s Output final data to stdout --verbose, -v Enable verbose logging --help, -h Show this help message `.trim(); /** * Given an array of CLI arguments, parse them into a structured object * * @param argv The arguments to parse * @returns The parsed CLI arguments * * @throws {CLIError} if the arguments are invalid */ function parseCLIArguments(argv: string[]): [string, CLIFlags] { const { values: options, positionals: args } = parseArgs({ args: argv, options: { help: { short: "h", default: false, type: "boolean", }, verbose: { short: "v", default: false, type: "boolean", }, stdout: { short: "s", default: false, type: "boolean", }, }, strict: true, allowPositionals: true, }); return [args[0] ?? "./", options]; } function getStepLogLine(plugin: MusePlugin): string { const { name, description } = plugin; let processorLogLine = chalk.bold(`↪ ${name}`); if (description && description.length > 0) { processorLogLine += chalk.dim(` (${description})`); } return processorLogLine; } async function processBinding( inputFilePath: string, flags: CLIFlags, { log, verbose }: Loggers, ) { // Load the binding let binding = await loadBinding(inputFilePath); verbose(`Binding ${binding.bindingPath}`); const entryCount = binding.entries.length; const stepCount = binding.plugins.length; const stepWord = stepCount === 1 ? "step" : "steps"; log(`Processing ${entryCount} entries with ${stepCount} ${stepWord}`); const processStart = performance.now(); // Run the data through relevant plugins for (const plugin of binding.plugins) { log(getStepLogLine(plugin)); const { step } = plugin; binding = await step(binding); } const processEnd = performance.now(); const processTime = ((processEnd - processStart) / 1000).toFixed(2); verbose(`Processing completed in ${processTime}s`); if (flags.stdout) { console.log(JSON.stringify(binding, null, 2)); } return ExitCode.Success; } async function main(): Promise { const [inputFilePath, flags] = parseCLIArguments(Bun.argv.slice(2)); // If --help is specified, print usage and exit if (flags.help) { console.log(USAGE); return ExitCode.Success; } const logFn = !flags.stdout ? console.log : () => {}; const verboseFn = flags.verbose ? (msg: string) => { console.log(chalk.dim(msg)); } : () => {}; const lastProcessResult = await processBinding(inputFilePath, flags, { log: logFn, verbose: verboseFn, }); return lastProcessResult; } try { const exitCode = await main(); process.exit(exitCode); } catch (error) { if (error instanceof MuseError) { console.error(chalk.red(error.fmt())); } else { console.error("An unexpected error occurred"); console.error(error); } process.exit(1); }