esa-cli
Version:
A CLI for operating Alibaba Cloud ESA Functions and Pages.
313 lines (312 loc) • 10.3 kB
JavaScript
import os from 'os';
import path from 'path';
import chalk from 'chalk';
import Table from 'cli-table3';
import ora from 'ora';
import { format, createLogger } from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import t from '../i18n/index.js';
import { getProjectConfig } from '../utils/fileUtils/index.js';
const transport = new DailyRotateFile({
filename: path.join(os.homedir(), '.esa-logs/esa-debug-%DATE%.log'),
level: 'info',
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '10m',
maxFiles: '7d'
});
class Logger {
constructor() {
this.spinnerText = '';
const { combine, timestamp, label, printf } = format;
const customFormat = printf(({ level, message, label: printLabel, timestamp: printTimestamp }) => {
var _a;
let colorizedLevel;
const projName = ((_a = getProjectConfig()) === null || _a === void 0 ? void 0 : _a.name) || 'Outside';
switch (level) {
case 'warn':
colorizedLevel = chalk.yellow(level);
break;
case 'info':
colorizedLevel = chalk.green(level);
break;
case 'error':
colorizedLevel = chalk.red(level);
case 'verbose':
colorizedLevel = chalk.magenta(level);
break;
case 'debug':
colorizedLevel = chalk.grey(level);
break;
case 'silly':
colorizedLevel = chalk.white(level);
break;
default:
colorizedLevel = level;
}
return `${printTimestamp} [${chalk.green(printLabel)}] ${colorizedLevel} in ${chalk.italic(projName)}: ${message}`;
});
this.logger = createLogger({
level: 'info',
format: combine(label({ label: 'ESA' }), timestamp(), customFormat),
transports: [transport]
});
this.spinner = ora('Loading...');
}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
// Object.freeze(Logger.instance);
}
return Logger.instance;
}
get ora() {
return this.spinner;
}
setLogLevel(level) {
this.logger.level = level;
}
/**
* Start a sub-step: show a spinner with the provided message.
* If a spinner is already running, just update its text.
*/
startSubStep(message) {
this.spinnerText = message;
this.spinner.text = message;
if (!this.spinner.isSpinning) {
this.spinner.start();
}
}
/**
* End a sub-step: stop loading and replace spinner with `├` and final message.
* This overwrites the previous spinner line with the provided message.
*/
endSubStep(message) {
// console.log(chalk.gray('├') + ' ' + this.spinnerText);
try {
if (this.spinner && this.spinner.isSpinning) {
this.spinner.stop();
}
}
catch (_a) { }
console.log(chalk.gray(`│ `));
console.log(chalk.gray('├ ') + this.spinnerText);
console.log(chalk.gray(`│ ${message}`));
}
stopSpinner() {
try {
if (this.spinner && this.spinner.isSpinning) {
this.spinner.stop();
}
}
catch (_a) { }
}
/**
* Prepare terminal output just before showing an interactive prompt.
* - Stops any active spinner
* - Replaces the previous line with a clean `╰ <text>` indicator
*/
prepareForPrompt(text) {
this.stopSpinner();
const content = `╰ ${text || ''}`;
this.replacePrevLine(content);
}
/**
* Consolidate interactive prompt output after completion by replacing
* the previous N lines with a concise summary line.
* Defaults to 2 lines (prompt + answer line in most cases).
*/
consolidateAfterPrompt(summary, linesToReplace = 2) {
const content = `├ ${summary}`;
this.replacePrevLines(linesToReplace, content);
}
log(message) {
console.log(message);
}
subLog(message) {
console.log(`\t${message}`);
}
success(message) {
console.log(`🎉 ${chalk.bgGreen(' SUCCESS ')} ${chalk.green(message)}`);
}
debug(message) {
this.logger.debug(message);
if (this.logger.level === 'debug') {
console.log(`${chalk.grey('[DEBUG]')} ${message}`);
}
}
info(message) {
this.logger.info(message);
}
ask(message) {
console.log(`❓ ${message}`);
}
point(message) {
console.log(`👉🏻 ${chalk.green(message)}`);
}
block() {
console.log('\n');
}
warn(message) {
this.logger.warn(message);
console.log(`\n${chalk.bgYellow(' WARNING ')} ${chalk.yellow(message)}`);
}
error(message) {
this.logger.error(message);
console.log(`\n❌ ${chalk.bgRed(' ERROR ')} ${chalk.red(message)}`);
}
subError(message) {
console.log(`\n${chalk.red(message)}`);
}
http(message) {
this.logger.http(message);
}
url(message) {
console.log(`🔗 ${chalk.blue(message)}`);
}
verbose(message) {
this.logger.verbose(message);
}
silly(message) {
this.logger.silly(message);
}
announcement(message) {
// todo
console.log(message);
}
notInProject() {
this.block();
this.error('Missing ESA project configuration (esa.jsonc or esa.toml)');
this.block();
this.log('If there is code to deploy, you can either:');
this.subLog(`- Specify an entry-point to your Routine via the command line (ex: ${chalk.green('esa-cli deploy src/index.ts')})`);
this.subLog('- Or create an "esa.jsonc" file (recommended):');
console.log('```jsonc\n' +
'{\n' +
' "name": "my-routine",\n' +
' "entry": "src/index.ts",\n' +
' "dev": { "port": 18080 }\n' +
'}\n' +
'```');
this.subLog('- Or, if you prefer TOML, create an "esa.toml" file:');
console.log('```toml\n' +
'name = "my-routine"\n' +
'entry = "src/index.ts"\n' +
'\n' +
'[dev]\n' +
'port = 18080\n' +
'```\n');
this.log('If you are deploying a directory of static assets, you can either:');
this.subLog(`- Create an "esa.jsonc" file (recommended) and run ${chalk.green('esa-cli deploy -a ./dist')}`);
console.log('```jsonc\n' +
'{\n' +
' "name": "my-routine",\n' +
' "assets": {\n' +
' "directory": "./dist"\n' +
' }\n' +
'}\n' +
'```');
this.subLog(`- Or create an "esa.toml" file and run ${chalk.green('esa-cli deploy -a ./dist')}`);
console.log('```toml\n' +
'name = "my-routine"\n' +
'\n' +
'[assets]\n' +
'directory = "./dist"\n' +
'```\n');
this.log('Alternatively, initialize a new ESA project:');
this.log(chalk.green('$ esa-cli init my-project'));
this.block();
}
pathEacces(localPath) {
this.block();
this.log(chalk.yellow(t('common_eacces_intro', { localPath }).d(`You do not have permission to ${localPath}, please use`)));
this.block();
this.log(chalk.green(`$ ${chalk.red('sudo')} esa-cli <Command>`));
this.block();
this.subLog(chalk.yellow('OR'));
this.block();
this.log(chalk.green(`$ sudo chmod -R 777 ${localPath}`));
}
table(head, data, width = []) {
const table = new Table({
head,
colWidths: width
});
table.push(...data);
this.log(table.toString());
this.block();
}
tree(messages) {
if (messages.length === 0)
return;
const lines = [];
lines.push(`╭ ${messages[0]}`);
for (let i = 1; i < messages.length - 1; i++) {
lines.push(`│ ${messages[i]}`);
}
if (messages.length > 1) {
lines.push(`╰ ${messages[messages.length - 1]}`);
}
console.log(lines.join('\n'));
}
StepHeader(title, step, total) {
console.log(`\n╭ ${title} ${chalk.green(`Step ${step} of ${total}`)}`);
console.log('│');
}
StepItem(prompt) {
console.log(`├ ${prompt}`);
}
StepStart(prompt) {
console.log(`╭ ${prompt}`);
}
StepKV(key, value) {
const orange = chalk.hex('#FFA500');
console.log(`│ ${orange(key)} ${value}`);
}
StepSpacer() {
console.log('│');
}
StepEnd(str) {
console.log(`╰ ${str || ''}`);
}
StepEndInline() {
try {
process.stdout.write('╰ ');
}
catch (_a) {
console.log('╰');
}
}
divider() {
console.log(chalk.yellow('--------------------------------------------------------'));
}
// Replace the previous single terminal line with new content
replacePrevLine(content) {
try {
// Move cursor up 1 line, clear it, carriage return, print new content
process.stdout.write('\x1b[1A');
process.stdout.write('\x1b[2K');
process.stdout.write('\r');
console.log(content);
}
catch (_a) {
console.log(content);
}
}
// Replace multiple previous lines with one consolidated line
replacePrevLines(linesToReplace, content) {
try {
for (let i = 0; i < linesToReplace; i++) {
process.stdout.write('\x1b[1A'); // move up
process.stdout.write('\x1b[2K'); // clear line
}
process.stdout.write('\r');
console.log(content);
}
catch (_a) {
console.log(content);
}
}
}
const logger = Logger.getInstance();
export default logger;