UNPKG

svnh

Version:

A Node.js CLI tool to manage projects with a custom helper.

833 lines (733 loc) 37.3 kB
#!/usr/bin/env node import fs from "fs"; import { execSync } from "child_process"; import path from "path"; import https from "https"; import http from "http"; import os from "os"; import chalk from "chalk"; import ora from "ora"; import inquirer from "inquirer"; import dotenv from "dotenv"; dotenv.config(); const NODE_ENV = process.env.NODE_ENV || 'production'; const IS_DEVELOPMENT = NODE_ENV === 'development'; let INSTALLER_VERSION = "1.0.0"; try { const packageJsonPath = path.join(process.cwd(), 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJson.version) { INSTALLER_VERSION = packageJson.version; } } } catch (e) { console.warn(chalk.yellow(`Warning: Could not read installer's package.json to determine version. Defaulting to ${INSTALLER_VERSION}. Error: ${e.message}`)); } const NODE_HELPER_TARBALL_NAME = "node_helper.tgz"; const NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON = "@your-scope/node-helper"; const PERSISTENT_INSTALLER_STORE_DIR = path.join(os.homedir(), '.my-installer-store'); const GO_BACK_SIGNAL = '__GO_BACK__'; const GO_BACK_SIGNAL_STRING = '< Back'; const HELP_MESSAGE = ` Usage: node index.js [command] [options] [arguments] Commands: create [projectName] [devPort] Create a new Node.js project. (projectName and devPort are optional, will prompt if omitted) update [projectName] Update '${NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON}' in an existing project. (projectName is optional, will prompt if omitted) Options: --version, -v Show installer version. --help, -h Show this help message. --env <mode> Specify environment mode ('development' or 'production'). Defaults to 'production'. Arguments: [projectName] Name of the new project to create/update. [devPort] Port for local development server (only for 'create' command in 'development' env). Interactive Mode: If no commands or flags are provided via CLI, an interactive menu will guide you. Examples: node index.js # Start interactive main menu node index.js create # Interactively create a new project node index.js update my-existing-app # Update 'my-existing-app' project node index.js --version # Show installer version `; class InstallerError extends Error { constructor(message, originalError = null, code = 'INSTALLER_ERROR', projectPath = null, moduleName = null) { super(message); this.name = 'InstallerError'; this.originalError = originalError; this.code = code; this.projectPath = projectPath; this.moduleName = moduleName; } } class ProjectNotFoundError extends InstallerError { constructor(projectPath) { super(`Project directory not found at: ${projectPath}`, null, 'PROJECT_NOT_FOUND', projectPath); this.name = 'ProjectNotFoundError'; } } class PackageJsonNotFoundError extends InstallerError { constructor(projectPath) { super(`package.json not found in project directory: ${projectPath}. Is it a valid Node.js project?`, null, 'PACKAGE_JSON_NOT_FOUND', projectPath); this.name = 'PackageJsonNotFoundError'; } } class ModuleNotFoundError extends InstallerError { constructor(projectName, moduleName) { super(`Module '${moduleName}' not found in project '${projectName}'.`, null, 'MODULE_NOT_FOUND', null, moduleName); this.name = 'ModuleNotFoundError'; } } function createCustomProgressSpinner(text, color = 'white') { const spinner = ora({ text: chalk[color](text), spinner: { interval: 80, frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] }, color: color }); let currentProgress = 0; const updateText = () => { spinner.text = chalk[color](`${text} ${Math.floor(currentProgress)}%`); }; return { start: (max = 100, initial = 0) => { currentProgress = initial; spinner.start(); updateText(); }, update: (value) => { currentProgress = value; updateText(); }, succeed: (message) => { spinner.succeed(chalk.green(message || 'Done!')); }, fail: (message) => { spinner.fail(chalk.red(message || 'Failed!')); }, stop: () => { spinner.stop(); } }; } function simulateTaskProgress(customSpinnerInstance, initialProgress, maxProgress, durationMs) { return new Promise((resolve) => { let progress = initialProgress; const interval = setInterval(() => { progress += Math.random() * (maxProgress - progress) * 0.1 + 0.5; if (progress >= maxProgress) { progress = maxProgress; clearInterval(interval); resolve(); } customSpinnerInstance.update(progress); }, durationMs / (maxProgress - initialProgress + 10)); }); } function isCommandAvailable(command) { try { execSync(`command -v ${command} || where ${command}`, { stdio: 'ignore' }); return true; } catch (e) { return false; } } async function askAuthenticationCredentials(mainSpinner, fileUrl) { let AUTH_USERNAME = process.env.NODE_HELPER_AUTH_USERNAME; let AUTH_PASSWORD = process.env.NODE_HELPER_AUTH_PASSWORD; if (!AUTH_USERNAME || !AUTH_PASSWORD) { mainSpinner.stop(); console.log(chalk.cyan(`\n🔑 Authentication credentials not found in environment (NODE_HELPER_AUTH_USERNAME/PASSWORD).`)); console.log(chalk.cyan(` Please enter them now for the download server:`)); while (true) { const authAnswers = await inquirer.prompt([ { type: 'input', name: 'username', message: 'Username:', validate: input => input.trim() !== '' || 'Username cannot be empty.' }, { type: 'password', name: 'password', message: 'Password:', mask: '*' } // Removed: The explicit 'goBack' list choice from this prompt. // This is now handled by the subsequent confirmation step. ]); AUTH_USERNAME = authAnswers.username; AUTH_PASSWORD = authAnswers.password; const confirmCredentials = await inquirer.prompt([ { type: 'list', name: 'action', message: chalk.magenta(`Confirm download from ${fileUrl} with user '${AUTH_USERNAME}'?`), choices: [ { name: 'Yes, proceed', value: 'proceed' }, { name: 'No, re-enter credentials', value: 're-enter' }, { name: GO_BACK_SIGNAL_STRING + ' to main menu', value: GO_BACK_SIGNAL } ], default: 'proceed', loop: false } ]); if (confirmCredentials.action === 'proceed') { mainSpinner.start(); return { AUTH_USERNAME, AUTH_PASSWORD }; } else if (confirmCredentials.action === GO_BACK_SIGNAL) { mainSpinner.start(); return GO_BACK_SIGNAL; } // If action is 're-enter', the loop continues to re-prompt username/password } } else { mainSpinner.stop(); const confirmCredentials = await inquirer.prompt([ { type: 'list', name: 'action', message: chalk.magenta(`Confirm download with user '${AUTH_USERNAME}'?`), choices: [ { name: 'Yes, proceed', value: 'proceed' }, { name: GO_BACK_SIGNAL_STRING + ' to main menu', value: GO_BACK_SIGNAL } ], default: 'proceed', loop: false } ]); mainSpinner.start(); if (confirmCredentials.action === 'proceed') { return { AUTH_USERNAME, AUTH_PASSWORD }; } else { return GO_BACK_SIGNAL; } } } async function askInstallLocation(mainSpinner, initialPath = null) { while (true) { mainSpinner.stop(); const choices = []; if (initialPath) { choices.push({ name: `Continue with previously selected path (${initialPath})`, value: initialPath }); } choices.push( { name: `Current directory (${process.cwd()})`, value: process.cwd() }, { name: 'Specify a custom path', value: 'custom_path' }, { name: GO_BACK_SIGNAL_STRING + ' to main menu', value: GO_BACK_SIGNAL } ); const locationAnswers = await inquirer.prompt([ { type: 'list', name: 'locationChoice', message: 'Where would you like to create/update the project?', choices: choices, default: initialPath || process.cwd(), loop: false } ]); mainSpinner.start(); if (locationAnswers.locationChoice === GO_BACK_SIGNAL) { return GO_BACK_SIGNAL; } else if (locationAnswers.locationChoice === 'custom_path') { mainSpinner.stop(); const customPathAnswer = await inquirer.prompt([ { type: 'input', name: 'customPath', message: `Enter the custom path (absolute or relative, or type "${GO_BACK_SIGNAL_STRING}" to go back):`, validate: (input) => { const trimmedInput = input.trim(); if (trimmedInput === GO_BACK_SIGNAL_STRING) { return true; } if (!trimmedInput) return 'Path cannot be empty.'; const resolvedPath = path.resolve(trimmedInput); try { if (fs.existsSync(resolvedPath)) { fs.accessSync(resolvedPath, fs.constants.W_OK); } else { const parentDir = path.dirname(resolvedPath); if (!fs.existsSync(parentDir)) { return `Parent directory "${parentDir}" does not exist. Please create it first.`; } fs.accessSync(parentDir, fs.constants.W_OK); } return true; } catch (e) { return `Cannot write to path "${resolvedPath}" or its parent: ${e.message}. Check permissions.`; } } } ]); mainSpinner.start(); if (customPathAnswer.customPath.trim() === GO_BACK_SIGNAL_STRING) { continue; } return path.resolve(customPathAnswer.customPath); } else { return locationAnswers.locationChoice; } } } async function askProjectName(operationType, basePath, initialProjectName, mainSpinner) { let projectName = initialProjectName; while (true) { if (!projectName) { mainSpinner.stop(); const answer = await inquirer.prompt([ { type: "input", name: "projectName", message: `What is the name of the project to ${operationType}?`, validate: (input) => { if (!input.trim()) { return "Project name cannot be empty."; } const projectExists = fs.existsSync(path.join(basePath, input)); if (operationType === 'create' && projectExists) { return chalk.red(`A directory named "${input}" already exists at "${path.join(basePath, input)}". Please choose a different name.`); } if (operationType === 'update' && !projectExists) { return chalk.red(`Project directory "${input}" not found at "${path.join(basePath, input)}". Cannot update non-existent project.`); } return true; }, } ]); mainSpinner.start(); projectName = answer.projectName; } const validationResult = (input) => { if (!input.trim()) { return "Project name cannot be empty."; } const projectExists = fs.existsSync(path.join(basePath, input)); if (operationType === 'create' && projectExists) { return chalk.red(`A directory named "${input}" already exists at "${path.join(basePath, input)}". Please choose a different name.`); } if (operationType === 'update' && !projectExists) { return chalk.red(`Project directory "${input}" not found at "${path.join(basePath, input)}". Cannot update non-existent project.`); } return true; }; if (validationResult(projectName) === true) { mainSpinner.start(); return projectName; } else { console.error(chalk.red(validationResult(projectName))); projectName = undefined; } } } async function mainInstaller() { const args = process.argv.slice(2); const cliCommand = args[0]; const cliProjectName = args[1]; const cliDevPort = args[2]; const cliEnvFlagIndex = args.indexOf('--env'); const cliEnvValue = cliEnvFlagIndex !== -1 ? args[cliEnvFlagIndex + 1] : undefined; if (args.includes('--version') || args.includes('-v')) { console.log(chalk.blue(`\nInstaller Version: ${INSTALLER_VERSION}\n`)); process.exit(0); } if (args.includes('--help') || args.includes('-h')) { console.log(chalk.blue(HELP_MESSAGE)); process.exit(0); } const topLevelSpinner = ora({ text: 'Initializing installer...', color: 'cyan' }).start(); let selectedCommand = cliCommand; while (true) { if (!selectedCommand || selectedCommand.startsWith('--')) { topLevelSpinner.stop(); const mainMenuAnswers = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'Create New Project', value: 'create' }, { name: 'Update Existing Project', value: 'update' }, { name: 'Show Installer Version', value: 'version' }, { name: 'Show Help', value: 'help' }, { name: 'Exit Installer', value: 'exit' } ], default: 'create' } ]); topLevelSpinner.start(); selectedCommand = mainMenuAnswers.action; } switch (selectedCommand) { case 'version': console.log(chalk.blue(`\nInstaller Version: ${INSTALLER_VERSION}\n`)); selectedCommand = undefined; break; case 'help': console.log(chalk.blue(HELP_MESSAGE)); selectedCommand = undefined; break; case 'create': case 'update': topLevelSpinner.stop(); let result = await handleProjectOperation(selectedCommand, cliProjectName, cliDevPort, cliEnvValue); if (result === GO_BACK_SIGNAL) { selectedCommand = undefined; topLevelSpinner.start(); } else { return; } break; case 'exit': console.log(chalk.yellow("👋 Exiting installer. Goodbye!")); process.exit(0); default: console.error(chalk.red(`\n❌ Error: Unknown command '${selectedCommand}'. Use --help for usage.`)); process.exit(1); } } topLevelSpinner.stop(); } async function handleProjectOperation(operationType, projectNameParam, devPortParam, nodeEnvParam) { const mainSpinner = ora({ text: `Starting ${operationType} operation...`, color: 'cyan' }).start(); const downloadProgressIndicator = createCustomProgressSpinner("Downloading files", 'blue'); const postDownloadProgressIndicator = createCustomProgressSpinner("Processing project", 'yellow'); let packageManagerCmd = 'npm'; let packageManagerName = 'npm'; mainSpinner.text = `Checking for package manager...`; try { execSync(`pnpm -v`, { stdio: 'ignore' }); packageManagerCmd = 'pnpm'; packageManagerName = 'pnpm'; mainSpinner.succeed(chalk.green(`Using pnpm.`)); } catch (e) { mainSpinner.warn(chalk.yellow('pnpm not found. Attempting to install it globally via npm...')); try { execSync(`npm install -g pnpm`, { stdio: 'pipe' }); execSync(`pnpm -v`, { stdio: 'ignore' }); packageManagerCmd = 'pnpm'; packageManagerName = 'pnpm'; mainSpinner.succeed(chalk.green('pnpm installed globally and will be used.')); } catch (pnpmError) { mainSpinner.warn(chalk.yellow(`Could not install pnpm: ${pnpmError.message}. Falling back to npm.`)); packageManagerCmd = 'npm'; packageManagerName = 'npm'; mainSpinner.succeed(chalk.green(`Using npm.`)); } } const NODE_ENV_FOR_OPERATION = nodeEnvParam || process.env.NODE_ENV || 'production'; const IS_DEVELOPMENT_FOR_OPERATION = NODE_ENV_FOR_OPERATION === 'development'; const SERVER_DOMAIN_PROD = "npm.sherin.fun"; const HTTPS_PORT_PROD = process.env.HTTPS_PORT_PROD || 443; const HTTP_PORT_DEV = process.env.HTTP_PORT_DEV || 3000; let fileUrl; let requestModule; if (IS_DEVELOPMENT_FOR_OPERATION) { const portToUse = devPortParam || HTTP_PORT_DEV; fileUrl = `http://127.0.0.1:${portToUse}/download`; // Removed: console.log(chalk.yellow(`\n⚠️ Running installer in DEVELOPMENT mode. Downloading from ${fileUrl}`)); } else { fileUrl = `https://${SERVER_DOMAIN_PROD}:${HTTPS_PORT_PROD}/download`; requestModule = https; // Removed: console.log(chalk.green(`\n🚀 Running installer in PRODUCTION mode. Downloading from ${fileUrl}`)); } const authResult = await askAuthenticationCredentials(mainSpinner, fileUrl); if (authResult === GO_BACK_SIGNAL) { mainSpinner.stop(); return GO_BACK_SIGNAL; } const { AUTH_USERNAME, AUTH_PASSWORD } = authResult; const authHeader = 'Basic ' + Buffer.from(`${AUTH_USERNAME}:${AUTH_PASSWORD}`).toString('base64'); mainSpinner.text = `Ensuring installer store is ready: ${PERSISTENT_INSTALLER_STORE_DIR}...`; try { if (!fs.existsSync(PERSISTENT_INSTALLER_STORE_DIR)) { fs.mkdirSync(PERSISTENT_INSTALLER_STORE_DIR, { recursive: true }); mainSpinner.succeed(chalk.green(`Created installer store directory.`)); } else { mainSpinner.succeed(chalk.green(`Installer store ready.`)); } } catch (e) { throw new InstallerError(`Failed to prepare installer store directory: ${PERSISTENT_INSTALLER_STORE_DIR}. Check permissions.`, e, 'STORE_DIR_CREATION_FAILED'); } let actualDownloadedFilePath = ''; let file = null; mainSpinner.text = `Downloading latest ${NODE_HELPER_TARBALL_NAME} to local store...`; downloadProgressIndicator.start(100, 0); try { await new Promise((resolve, reject) => { const requestOptions = { method: 'GET', headers: { 'Authorization': authHeader } }; const req = requestModule.get(fileUrl, requestOptions, (response) => { if (response.statusCode >= 400) { const errorMsg = `Server responded with status ${response.statusCode}: ${response.statusMessage}. Check URL, server status, and credentials.`; response.resume(); return reject(new InstallerError(errorMsg, null, 'DOWNLOAD_HTTP_ERROR')); } let responseFilename = NODE_HELPER_TARBALL_NAME; const contentDisposition = response.headers['content-disposition']; if (contentDisposition) { const filenameMatch = /filename="?([^"]+)"?/.exec(contentDisposition); if (filenameMatch && filenameMatch[1]) { responseFilename = filenameMatch[1]; } } actualDownloadedFilePath = path.join(PERSISTENT_INSTALLER_STORE_DIR, responseFilename); file = fs.createWriteStream(actualDownloadedFilePath); const contentLength = response.headers['content-length']; let downloadedBytes = 0; response.on("data", (chunk) => { downloadedBytes += chunk.length; downloadProgressIndicator.update(contentLength ? Math.floor((downloadedBytes / parseInt(contentLength, 10)) * 100) : Math.min(99, Math.floor(downloadedBytes / 1024))); }); response.pipe(file); file.on("finish", () => { file.close(); downloadProgressIndicator.succeed('Essential files downloaded to store.'); resolve(); }); }).on("error", (err) => { downloadProgressIndicator.fail('Download failed!'); reject(new InstallerError(`Failed to download file from server: ${err.message}. Ensure the server is running and accessible.`, err, 'DOWNLOAD_FAILED')); }); req.setTimeout(10000, () => { req.destroy(); downloadProgressIndicator.fail('Download timed out!'); reject(new InstallerError('Download request timed out. Check network connection and server availability.', null, 'DOWNLOAD_TIMEOUT')); }); }); } catch (e) { if (file && !file.closed) { file.close(); } if (actualDownloadedFilePath && fs.existsSync(actualDownloadedFilePath)) { try { fs.unlinkSync(actualDownloadedFilePath); // Removed: console.log(chalk.dim(`(Cleaned up incomplete download: ${path.basename(actualDownloadedFilePath)})`)); } catch (cleanupErr) { console.warn(chalk.yellow(`Warning: Could not delete incomplete download at ${actualDownloadedFilePath}: ${cleanupErr.message}`)); } } throw e; } mainSpinner.text = 'Running pre-installation checks...'; try { if (!isCommandAvailable(packageManagerCmd)) { throw new InstallerError(`Critical Error: '${packageManagerCmd}' command not found or not installable. Please ensure Node.js and npm are correctly installed.`, null, 'PKG_MANAGER_NOT_FOUND'); } if (!isCommandAvailable('tar')) { throw new InstallerError("Critical Error: 'tar' command not found. This installer requires 'tar'. Please ensure 'tar' is in your system's PATH.", null, 'TAR_NOT_FOUND'); } mainSpinner.succeed(chalk.green('Pre-installation checks passed.')); } catch (e) { mainSpinner.fail(chalk.red('Pre-installation checks failed!')); throw e; } try { let operationResult; if (operationType === 'create') { operationResult = await handleCreateProject(projectNameParam, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, actualDownloadedFilePath); } else { operationResult = await handleUpdateProject(projectNameParam, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, actualDownloadedFilePath); } if (operationResult === GO_BACK_SIGNAL) { mainSpinner.stop(); return GO_BACK_SIGNAL; } mainSpinner.succeed(chalk.green('Operation completed successfully!')); return true; } catch (e) { mainSpinner.fail(chalk.red('Operation failed!')); if (operationType === 'create' && fs.existsSync(e.projectPath || '')) { // Removed: console.log(chalk.yellow(`Cleaning up partially created project directory: ${e.projectPath || 'Unknown Path'}`)); try { fs.rmSync(e.projectPath, { recursive: true, force: true }); // Removed: console.log(chalk.green(`Cleaned up.`)); } catch (cleanupErr) { console.warn(chalk.yellow(`Warning: Could not delete incomplete project directory ${e.projectPath || 'Unknown Path'}: ${cleanupErr.message}`)); } } throw e; } finally { } } async function handleCreateProject(projectNameParam, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, dedicatedStoreTarballPath) { let projectName = projectNameParam; let basePath = null; let projectPath; while (true) { const installLocationResult = await askInstallLocation(mainSpinner, basePath); if (installLocationResult === GO_BACK_SIGNAL) { mainSpinner.stop(); return GO_BACK_SIGNAL; } basePath = installLocationResult; while (true) { if (!projectName) { const projectNameResult = await askProjectName('create', basePath, projectName, mainSpinner); projectName = projectNameResult; } projectPath = path.join(basePath, projectName); mainSpinner.stop(); const confirmAnswers = await inquirer.prompt([ { type: 'list', name: 'action', message: chalk.magenta(`Confirm creating project '${projectName}' in "${projectPath}"?`), choices: [ { name: 'Yes, create project', value: 'create' }, { name: 'No, re-enter project name', value: 're-enter' }, { name: GO_BACK_SIGNAL_STRING + ' to project location selection', value: GO_BACK_SIGNAL } ], default: 'create', loop: false } ]); mainSpinner.start(); if (confirmAnswers.action === 'create') { break; } else if (confirmAnswers.action === 're-enter') { projectName = undefined; } else if (confirmAnswers.action === GO_BACK_SIGNAL) { projectName = undefined; break; } } if (projectName !== undefined) { break; } } console.log(chalk.cyan(`\n✨ Preparing to create new project '${projectName}' in '${basePath}'...\n`)); mainSpinner.text = `Creating project directory...`; try { fs.mkdirSync(projectPath, { recursive: true }); mainSpinner.succeed(chalk.green('Project directory created.')); } catch (e) { throw new InstallerError(`Failed to create project directory: ${projectPath}. Check permissions.`, e, 'PROJECT_DIR_CREATION_FAILED', projectPath); } await installModuleAndSetupProject(projectName, projectPath, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, dedicatedStoreTarballPath, false); return true; } async function handleUpdateProject(projectNameParam, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, dedicatedStoreTarballPath) { let projectName = projectNameParam; let basePath = null; let projectPath; let projectPackageJsonPath; let projectPackageJson; while (true) { const installLocationResult = await askInstallLocation(mainSpinner, basePath); if (installLocationResult === GO_BACK_SIGNAL) { mainSpinner.stop(); return GO_BACK_SIGNAL; } basePath = installLocationResult; while (true) { if (!projectName) { const projectNameResult = await askProjectName('update', basePath, projectName, mainSpinner); projectName = projectNameResult; } projectPath = path.join(basePath, projectName); projectPackageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(projectPath)) { console.error(chalk.red(`Project directory "${projectName}" not found at ${projectPath}.`)); projectName = undefined; continue; } if (!fs.existsSync(projectPackageJsonPath)) { console.error(chalk.red(`package.json not found in project "${projectName}" at ${projectPackageJsonPath}. Is it a valid Node.js project?`)); projectName = undefined; continue; } try { projectPackageJson = JSON.parse(fs.readFileSync(projectPackageJsonPath, 'utf8')); } catch (e) { console.error(chalk.red(`Failed to read or parse package.json for project '${projectName}' at ${projectPackageJsonPath}: ${e.message}.`)); projectName = undefined; continue; } const hasNodeHelper = projectPackageJson.dependencies && (projectPackageJson.dependencies[NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON] || projectPackageJson.dependencies['node-helper']); if (!hasNodeHelper) { console.error(chalk.red(`Module '${NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON}' (or 'node-helper') not found in project '${projectName}' dependencies.`)); console.error(chalk.red(`Please ensure your project uses this module before attempting to update it.`)); projectName = undefined; continue; } break; } if (projectName !== undefined) { break; } } console.log(chalk.cyan(`\n✨ Attempting to update project '${projectName}' in '${basePath}'...\n`)); await installModuleAndSetupProject(projectName, projectPath, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, dedicatedStoreTarballPath, true); return true; } async function installModuleAndSetupProject(projectName, projectPath, packageManagerCmd, packageManagerName, mainSpinner, postDownloadProgressIndicator, dedicatedStoreTarballPath, isUpdateOperation) { mainSpinner.text = `Preparing project for module installation...`; postDownloadProgressIndicator.start(100, 0); await simulateTaskProgress(postDownloadProgressIndicator, 0, 10, 800); if (!isUpdateOperation) { mainSpinner.text = `Running ${packageManagerName} init...`; execSync(`cd "${projectPath}" && ${packageManagerCmd} init ${packageManagerCmd === 'npm' ? '-y' : ''}`, { stdio: "pipe" }); postDownloadProgressIndicator.update(10); mainSpinner.succeed(chalk.green(`Project initialized.`)); } else { mainSpinner.text = 'Skipping project init in existing directory.'; postDownloadProgressIndicator.update(10); mainSpinner.succeed(chalk.green(`Skipped project initialization.`)); } mainSpinner.text = `Adding module from local store...`; await simulateTaskProgress(postDownloadProgressIndicator, 10, 80, 3000); try { execSync(`cd "${projectPath}" && ${packageManagerCmd} add "${dedicatedStoreTarballPath}"`, { stdio: "pipe" }); postDownloadProgressIndicator.update(80); mainSpinner.succeed(chalk.green(`Module installed and linked.`)); } catch (e) { throw new InstallerError(`Failed to add module from store with ${packageManagerName} add.`, e, 'MODULE_ADD_FAILED', projectPath); } mainSpinner.text = `Running final ${packageManagerName} install...`; await simulateTaskProgress(postDownloadProgressIndicator, 80, 90, 500); try { execSync(`cd "${projectPath}" && ${packageManagerCmd} install`, { stdio: "pipe" }); postDownloadProgressIndicator.update(90); mainSpinner.succeed(chalk.green(`Lockfile generated.`)); } catch (e) { throw new InstallerError(`Failed to run final ${packageManagerName} install.`, e, 'FINAL_INSTALL_FAILED', projectPath); } await simulateTaskProgress(postDownloadProgressIndicator, 90, 98, 500); const indexJsContentTemplate = (actualModuleName) => ` const { initializeGlobalErrorHandler, ApplicationError, VW_Environment, Implementation_Manager, } = require('node_helper'); initializeGlobalErrorHandler(); const routes = require('./routedefinitions'); const initServer = async () => { try { await VW_Environment.setEnvironment(); await Implementation_Manager.initializeImplementation(); Implementation_Manager.initializeHttpAndStartServer(routes); } catch (err) { throw err instanceof ApplicationError ? err : new ApplicationError({ message: err.message, errorObject: err }); } }; initServer(); `; if (!isUpdateOperation || !fs.existsSync(path.join(projectPath, 'index.js'))) { let actualModuleNameForNewProject = NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON; try { const projectPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8')); if (projectPackageJson.dependencies && projectPackageJson.dependencies[NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON]) { actualModuleNameForNewProject = NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON; } else if (projectPackageJson.dependencies && projectPackageJson.dependencies['node-helper']) { actualModuleNameForNewProject = 'node-helper'; } else { console.warn(chalk.yellow(`Warning: Could not confirm module name from project's package.json. Defaulting to '${NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON}'.`)); } } catch (readJsonError) { console.warn(chalk.yellow(`Warning: Failed to read project's package.json to determine module name. Defaulting to '${NODE_HELPER_MODULE_NAME_IN_PACKAGE_JSON}'. Error: ${readJsonError.message}`)); } fs.writeFileSync(path.join(projectPath, "index.js"), indexJsContentTemplate(actualModuleNameForNewProject)); fs.writeFileSync(path.join(projectPath, "routedefinitions.js"), "module.exports = {};"); mainSpinner.succeed(chalk.green('Core project files created.')); } else { mainSpinner.text = 'Skipping core project files creation in existing directory.'; mainSpinner.succeed(chalk.green('Skipped core files creation.')); } postDownloadProgressIndicator.update(98); postDownloadProgressIndicator.succeed('Project process complete!'); } async function runProgram() { try { await mainInstaller(); console.log(chalk.greenBright("\n🚀 Operation Completed Successfully! 🎉")); } catch (err) { console.error(chalk.red(`\n\n❌ INSTALLER ERROR: ${err.message}`)); if (err instanceof InstallerError) { if (err.code) console.error(chalk.red(` Error Code: ${err.code}`)); if (err.projectPath) console.error(chalk.red(` Project Path: ${err.projectPath}`)); if (err.moduleName) console.error(chalk.red(` Module Name: ${err.moduleName}`)); if (err.originalError) { console.error(chalk.red(` Original Cause:`)); console.error(chalk.red(err.originalError.message || String(err.originalError))); if (err.originalError.stderr) console.error(chalk.red(` Stderr: ${err.originalError.stderr.toString().trim()}`)); if (err.originalError.stdout) console.error(chalk.red(` Stdout: ${err.originalError.stdout.toString().trim()}`)); } } else { console.error(chalk.red(`\nAn unexpected error occurred:`)); console.error(chalk.red(err.stack || err.toString())); } process.exit(1); } } runProgram();