UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

153 lines (150 loc) 6.6 kB
#!/usr/bin/env node import { exec } from 'node:child_process'; import { promisify } from 'node:util'; /** * Implementation of the command service using Node.js child_process. * Provides functionality to execute shell commands. */ class NodeCommandService { /** CLI interface service for user interaction */ CLI_INTERFACE_SERVICE; /** * Promisified version of the exec function from child_process. * Allows for async/await usage of command execution. */ EXEC_ASYNC = promisify(exec); constructor(cliInterfaceService) { this.CLI_INTERFACE_SERVICE = cliInterfaceService; } /** * Executes a shell command. * @param command - The shell command to execute * @returns Promise that resolves when the command completes successfully * @throws Will throw an error if the command execution fails, except for npm install which offers retry options */ async execute(command) { try { await this.EXEC_ASYNC(command); } catch (error) { // Check if the failed command is npm if (this.isNpmCommand(command)) { this.formatAndParseNpmError(command, error); await this.handleNpmInstallFailure(command); } else { // For non-npm commands, throw the error as before throw error; } } } /** * Formats and outputs npm error in readable format. * @param command - The original npm command that failed * @param error - npm error object * @returns void */ formatAndParseNpmError(command, error) { const parsedError = this.parseNpmError(error.stderr); this.CLI_INTERFACE_SERVICE.error("NPM command failed."); this.CLI_INTERFACE_SERVICE.info(`Command: ${command}`); this.CLI_INTERFACE_SERVICE.info("Error details:"); if (parsedError.errorCode) { this.CLI_INTERFACE_SERVICE.warn(`Code: ${parsedError.errorCode}`); } if (parsedError.conflictDetails.length > 0) { this.CLI_INTERFACE_SERVICE.warn("Dependency conflict:"); for (const detail of parsedError.conflictDetails) { this.CLI_INTERFACE_SERVICE.warn(`- ${detail}`); } } if (parsedError.resolutionAdvice) { this.CLI_INTERFACE_SERVICE.info(`Resolution: ${parsedError.resolutionAdvice}`); } if (parsedError.logFile) { this.CLI_INTERFACE_SERVICE.info(`Log file: ${parsedError.logFile}`); } if (!parsedError.errorCode && parsedError.conflictDetails.length === 0 && !parsedError.resolutionAdvice && !parsedError.logFile) { this.CLI_INTERFACE_SERVICE.error("Unknown error occurred."); } } /** * Handles npm install command failures by offering retry options to the user. * @param originalCommand - The original npm command that failed * @returns Promise that resolves when the chosen action completes * @throws Will throw an error if the user chooses to cancel or if retried command still fails */ async handleNpmInstallFailure(originalCommand) { this.CLI_INTERFACE_SERVICE.warn("npm command execution failed."); const options = [ { label: "Retry with --force", value: "force" }, { label: "Retry with --legacy-peer-deps", value: "legacy-peer-deps" }, { label: "Cancel command execution", value: "cancel" }, ]; const choice = await this.CLI_INTERFACE_SERVICE.select("How would you like to proceed?", options); switch (choice) { case "force": { this.CLI_INTERFACE_SERVICE.info("Retrying with --force flag..."); await this.EXEC_ASYNC(`${originalCommand} --force`); this.CLI_INTERFACE_SERVICE.success("Execution completed with --force flag."); break; } case "legacy-peer-deps": { this.CLI_INTERFACE_SERVICE.info("Retrying with --legacy-peer-deps flag..."); await this.EXEC_ASYNC(`${originalCommand} --legacy-peer-deps`); this.CLI_INTERFACE_SERVICE.success("Execution completed with --legacy-peer-deps flag."); break; } case "cancel": { this.CLI_INTERFACE_SERVICE.info("Execution cancelled by user."); throw new Error("npm command execution was cancelled by user."); } default: { throw new Error("Invalid option selected."); } } } /** * Determines whether command is npm package management command. * @param command - The command to check * @returns True if command is supported npm command */ isNpmCommand(command) { const normalizedCommand = command.trim(); return normalizedCommand.startsWith("npm install") || normalizedCommand.startsWith("npm ci") || normalizedCommand.startsWith("npm update") || normalizedCommand.startsWith("npm uninstall"); } /** * Parses npm error output to structured information. * @param stderr - npm stderr output * @returns Parsed npm error details */ parseNpmError(stderr) { const parsedError = { conflictDetails: [], errorCode: null, logFile: null, resolutionAdvice: null, }; if (!stderr) { return parsedError; } const lines = stderr.split("\n").filter((line) => line.trim()); for (const line of lines) { if (line.includes("npm error code")) { parsedError.errorCode = line.replace("npm error code", "").trim(); } else if (line.includes("While resolving") || line.includes("Found") || line.includes("Could not resolve dependency") || line.includes("Conflicting peer dependency")) { parsedError.conflictDetails.push(line.replace("npm error", "").trim()); } else if (line.includes("Fix the upstream dependency conflict") || line.includes("--force") || line.includes("--legacy-peer-deps")) { parsedError.resolutionAdvice = line.replace("npm error", "").trim(); } else if (line.includes("A complete log of this run can be found in")) { parsedError.logFile = line.replace("npm error", "").trim(); } } return parsedError; } } export { NodeCommandService }; //# sourceMappingURL=node-command.service.js.map