svnh
Version:
A Node.js CLI tool to manage projects with a custom helper.
833 lines (733 loc) • 37.3 kB
JavaScript
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();