🖍️ refactor: Typed Console Colors and Hardened Script Helpers (#14778)

* 🛠️ refactor: Enhance console color handling and improve deleteNodeModules function

* refactor: Use coloredConsole for consistent console output in invite-user.js

* refactor: Convert year to string format in invite user payload
This commit is contained in:
Danny Avila 2026-08-12 22:48:36 -04:00 committed by GitHub
parent 8f1f961212
commit 3a3a8dcad0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 49 additions and 32 deletions

View file

@ -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<string>} */
const askQuestion = (query) => {
const rl = readline.createInterface({
input: process.stdin,
@ -21,16 +27,18 @@ const askQuestion = (query) => {
);
};
/** @param {string} query @returns {Promise<string>} */
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,
};

View file

@ -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 <email>');
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 <email>');
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);
})();

View file

@ -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...');