diff --git a/config/helpers.js b/config/helpers.js index d7d679a5a5..9636f8c9d5 100644 --- a/config/helpers.js +++ b/config/helpers.js @@ -7,6 +7,12 @@ const path = require('path'); const readline = require('readline'); const { execSync } = require('child_process'); +/** @typedef {(message: string) => void} ConsoleColor */ +/** @typedef {{ orange: ConsoleColor, green: ConsoleColor, red: ConsoleColor, blue: ConsoleColor, purple: ConsoleColor, cyan: ConsoleColor, yellow: ConsoleColor, white: ConsoleColor, gray: ConsoleColor }} ColoredConsole */ + +const coloredConsole = /** @type {Console & ColoredConsole} */ (console); + +/** @param {string} query @returns {Promise} */ const askQuestion = (query) => { const rl = readline.createInterface({ input: process.stdin, @@ -21,16 +27,18 @@ const askQuestion = (query) => { ); }; +/** @param {string} query @returns {Promise} */ const askMultiLineQuestion = (query) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); - console.cyan(query); + coloredConsole.cyan(query); return new Promise((resolve) => { - let lines = []; + /** @type {string[]} */ + const lines = []; rl.on('line', (line) => { if (line.trim() === '.') { rl.close(); @@ -46,16 +54,22 @@ function isDockerRunning() { try { execSync('docker info'); return true; - } catch (e) { + } catch (_error) { return false; } } +/** + * Recursively removes a directory's node_modules. + * Retries on transient ENOTEMPTY/EBUSY errors that fs.rmSync intermittently + * throws on macOS (APFS) and Windows when entries are removed concurrently. + */ +/** @param {string} dir */ function deleteNodeModules(dir) { const nodeModulesPath = path.join(dir, 'node_modules'); if (fs.existsSync(nodeModulesPath)) { - console.purple(`Deleting node_modules in ${dir}`); - fs.rmSync(nodeModulesPath, { recursive: true }); + coloredConsole.purple(`Deleting node_modules in ${dir}`); + fs.rmSync(nodeModulesPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } } @@ -65,15 +79,15 @@ const silentExit = (code = 0) => { }; // Set the console colours -console.orange = (msg) => console.log('\x1b[33m%s\x1b[0m', msg); -console.green = (msg) => console.log('\x1b[32m%s\x1b[0m', msg); -console.red = (msg) => console.log('\x1b[31m%s\x1b[0m', msg); -console.blue = (msg) => console.log('\x1b[34m%s\x1b[0m', msg); -console.purple = (msg) => console.log('\x1b[35m%s\x1b[0m', msg); -console.cyan = (msg) => console.log('\x1b[36m%s\x1b[0m', msg); -console.yellow = (msg) => console.log('\x1b[33m%s\x1b[0m', msg); -console.white = (msg) => console.log('\x1b[37m%s\x1b[0m', msg); -console.gray = (msg) => console.log('\x1b[90m%s\x1b[0m', msg); +coloredConsole.orange = (/** @type {string} */ msg) => console.log('\x1b[33m%s\x1b[0m', msg); +coloredConsole.green = (/** @type {string} */ msg) => console.log('\x1b[32m%s\x1b[0m', msg); +coloredConsole.red = (/** @type {string} */ msg) => console.log('\x1b[31m%s\x1b[0m', msg); +coloredConsole.blue = (/** @type {string} */ msg) => console.log('\x1b[34m%s\x1b[0m', msg); +coloredConsole.purple = (/** @type {string} */ msg) => console.log('\x1b[35m%s\x1b[0m', msg); +coloredConsole.cyan = (/** @type {string} */ msg) => console.log('\x1b[36m%s\x1b[0m', msg); +coloredConsole.yellow = (/** @type {string} */ msg) => console.log('\x1b[33m%s\x1b[0m', msg); +coloredConsole.white = (/** @type {string} */ msg) => console.log('\x1b[37m%s\x1b[0m', msg); +coloredConsole.gray = (/** @type {string} */ msg) => console.log('\x1b[90m%s\x1b[0m', msg); module.exports = { askQuestion, @@ -81,4 +95,5 @@ module.exports = { silentExit, isDockerRunning, deleteNodeModules, + coloredConsole, }; diff --git a/config/invite-user.js b/config/invite-user.js index 7dfa38e454..6025d99629 100644 --- a/config/invite-user.js +++ b/config/invite-user.js @@ -3,27 +3,29 @@ const mongoose = require('mongoose'); const { checkEmailConfig, createInvite } = require('@librechat/api'); const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const { askQuestion, silentExit } = require('./helpers'); -const { createToken, findToken } = require('~/models'); -const { sendEmail } = require('~/server/utils'); +const { askQuestion, silentExit, coloredConsole } = require('./helpers'); +const { createToken, findToken } = require('../api/models'); +const { sendEmail } = require('../api/server/utils'); const connect = require('./connect'); (async () => { await connect(); - console.purple('--------------------------'); - console.purple('Invite a new user account!'); - console.purple('--------------------------'); + coloredConsole.purple('--------------------------'); + coloredConsole.purple('Invite a new user account!'); + coloredConsole.purple('--------------------------'); if (process.argv.length < 5) { - console.orange('Usage: npm run invite-user '); - console.orange('Note: if you do not pass in the arguments, you will be prompted for them.'); - console.purple('--------------------------'); + coloredConsole.orange('Usage: npm run invite-user '); + coloredConsole.orange( + 'Note: if you do not pass in the arguments, you will be prompted for them.', + ); + coloredConsole.purple('--------------------------'); } // Check if email service is enabled if (!checkEmailConfig()) { - console.red('Error: Email service is not enabled!'); + coloredConsole.red('Error: Email service is not enabled!'); silentExit(1); } @@ -40,20 +42,20 @@ const connect = require('./connect'); email = email.trim().toLowerCase(); // Validate the email if (!email.includes('@')) { - console.red('Error: Invalid email address!'); + coloredConsole.red('Error: Invalid email address!'); silentExit(1); } // Check if the user already exists const userExists = await User.findOne({ email }); if (userExists) { - console.red('Error: A user with that email already exists!'); + coloredConsole.red('Error: A user with that email already exists!'); silentExit(1); } const token = await createInvite(email, { createToken, findToken }); if (typeof token !== 'string') { - console.red('Error: Failed to create the invite token!'); + coloredConsole.red('Error: Failed to create the invite token!'); silentExit(1); } @@ -62,7 +64,7 @@ const connect = require('./connect'); const appName = process.env.APP_TITLE || 'LibreChat'; if (!checkEmailConfig()) { - console.green('Send this link to the user:', inviteLink); + coloredConsole.green(`Send this link to the user: ${inviteLink}`); silentExit(0); } @@ -73,17 +75,17 @@ const connect = require('./connect'); payload: { appName: appName, inviteLink: inviteLink, - year: new Date().getFullYear(), + year: String(new Date().getFullYear()), }, template: 'inviteUser.handlebars', }); } catch (error) { - console.error('Error: ' + error.message); + console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); silentExit(1); } // Done! - console.green('Invitation sent successfully!'); + coloredConsole.green('Invitation sent successfully!'); silentExit(0); })(); diff --git a/config/update.js b/config/update.js index d6ffd55904..ed560d1f47 100644 --- a/config/update.js +++ b/config/update.js @@ -94,7 +94,7 @@ async function validateDockerRunning() { const imageName = singleCompose ? 'librechat_single' : 'librechat'; try { execSync(`${sudo}docker rmi ${imageName}:latest`, { stdio: 'inherit' }); - } catch (e) { + } catch (_error) { console.purple('Failed to remove Docker image librechat:latest. It might not exist.'); } console.purple('Removing all unused dangling Docker images...');